/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.service.text; import com.foundationdb.ais.model.AkibanInformationSchema; import com.foundationdb.ais.model.FullTextIndex; import com.foundationdb.ais.model.IndexColumn; import com.foundationdb.ais.model.IndexName; import com.foundationdb.qp.operator.QueryBindings; import com.foundationdb.qp.operator.QueryContext; import com.foundationdb.qp.rowtype.RowType; import com.foundationdb.server.collation.AkCollator; import com.foundationdb.server.error.AkibanInternalException; import com.foundationdb.server.explain.*; import com.foundationdb.server.service.ServiceManager; import com.foundationdb.server.service.session.Session; import com.foundationdb.server.types.texpressions.TEvaluatableExpression; import com.foundationdb.server.types.texpressions.TPreparedExpression; import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import java.io.File; import java.util.List; public class FullTextQueryBuilder { protected final IndexName indexName; protected final FullTextIndexInfos infos; protected final QueryContext buildContext; /** Construct directly for non-SQL and testing. */ public FullTextQueryBuilder(IndexName indexName, FullTextIndexService service) { this.indexName = indexName; this.infos = service; this.buildContext = null; } /** Construct for given index and context */ public FullTextQueryBuilder(FullTextIndex index, AkibanInformationSchema ais, QueryContext buildContext) { this.indexName = index.getIndexName(); ServiceManager serviceManager = null; if (buildContext != null) { try { serviceManager = buildContext.getServiceManager(); } catch (UnsupportedOperationException ex) { } } if (serviceManager != null) { this.infos = serviceManager.getServiceByClass(FullTextIndexService.class); } else { this.infos = new TestFullTextIndexInfos(ais); } this.buildContext = buildContext; } /** For testing without services running (or even stored AIS). */ static class TestFullTextIndexInfos extends FullTextIndexInfosImpl { private final AkibanInformationSchema ais; private final File dummyPath = new File("."); // Does not matter. public TestFullTextIndexInfos(AkibanInformationSchema ais) { this.ais = ais; } @Override protected AkibanInformationSchema getAIS(Session session) { return ais; } @Override protected File getIndexPath() { return dummyPath; } } static class Constant implements FullTextQueryExpression { private final Query query; public Constant(Query query) { this.query = query; } @Override public boolean needsBindings() { return false; } @Override public Query getQuery(QueryContext context, QueryBindings bindings) { return query; } @Override public CompoundExplainer getExplainer(ExplainContext context) { CompoundExplainer explainer = new CompoundExplainer(Type.LITERAL); explainer.addAttribute(Label.OPERAND, PrimitiveExplainer.getInstance(query.toString())); return explainer; } @Override public String toString() { return query.toString(); } } public FullTextQueryExpression staticQuery(Query query) { return new Constant(query); } /** A string in Lucene query syntax. */ public FullTextQueryExpression parseQuery(final String query) { return parseQuery(null, query); } public FullTextQueryExpression parseQuery(IndexColumn defaultField, final String query) { final String fieldName = (defaultField == null) ? null : defaultField.getColumn().getName(); if (buildContext != null) { return new Constant(infos.parseQuery(buildContext, indexName, fieldName, query)); } return new FullTextQueryExpression() { @Override public boolean needsBindings() { return false; } @Override public Query getQuery(QueryContext context, QueryBindings bindings) { return infos.parseQuery(context, indexName, fieldName, query); } @Override public CompoundExplainer getExplainer(ExplainContext context) { CompoundExplainer explainer = new CompoundExplainer(Type.LITERAL); explainer.addAttribute(Label.OPERAND, PrimitiveExplainer.getInstance(query)); return explainer; } @Override public String toString() { return query; } }; } public FullTextQueryExpression parseQuery(IndexColumn defaultField, final TPreparedExpression qexpr) { final String fieldName = (defaultField == null) ? null : defaultField.getColumn().getName(); return new FullTextQueryExpression() { @Override public boolean needsBindings() { return true; } @Override public Query getQuery(QueryContext context, QueryBindings bindings) { TEvaluatableExpression qeval = qexpr.build(); qeval.with(context); qeval.with(bindings); qeval.evaluate(); if (qeval.resultValue().isNull()) return null; String query = qeval.resultValue().getString(); return infos.parseQuery(context, indexName, fieldName, query); } @Override public CompoundExplainer getExplainer(ExplainContext context) { CompoundExplainer explainer = new CompoundExplainer(Type.FUNCTION); explainer.addAttribute(Label.NAME, PrimitiveExplainer.getInstance("PARSE")); explainer.addAttribute(Label.OPERAND, qexpr.getExplainer(context)); return explainer; } @Override public String toString() { return qexpr.toString(); } }; } public FullTextQueryExpression matchQuery(IndexColumn field, String key) { String fieldName = checkFieldForMatch(field); return new Constant(new TermQuery(new Term(fieldName, key))); } public FullTextQueryExpression matchQuery(IndexColumn field, final TPreparedExpression qexpr) { final String fieldName = checkFieldForMatch(field); return new FullTextQueryExpression() { @Override public boolean needsBindings() { return true; } @Override public Query getQuery(QueryContext context, QueryBindings bindings) { TEvaluatableExpression qeval = qexpr.build(); qeval.with(context); qeval.with(bindings); qeval.evaluate(); if (qeval.resultValue().isNull()) return null; String query = qeval.resultValue().getString(); return new TermQuery(new Term(fieldName, query)); } @Override public CompoundExplainer getExplainer(ExplainContext context) { CompoundExplainer explainer = new CompoundExplainer(Type.FUNCTION); explainer.addAttribute(Label.NAME, PrimitiveExplainer.getInstance("TERM")); explainer.addAttribute(Label.OPERAND, qexpr.getExplainer(context)); return explainer; } @Override public String toString() { return qexpr.toString(); } }; } protected String checkFieldForMatch(IndexColumn field) { String fieldName = field.getColumn().getName(); AkCollator collator = field.getColumn().getCollator(); if ((collator != null) && !collator.isCaseSensitive()) { throw new AkibanInternalException("Building a term for field that may need analysis: " + fieldName); } return fieldName; } public enum BooleanType { SHOULD, MUST, NOT }; public FullTextQueryExpression booleanQuery(final List<FullTextQueryExpression> queries, final List<BooleanType> types) { boolean isConstant = true; for (FullTextQueryExpression query : queries) { if (!(query instanceof Constant)) { isConstant = false; break; } } FullTextQueryExpression result = new FullTextQueryExpression() { @Override public boolean needsBindings() { for (FullTextQueryExpression query : queries) { if (query.needsBindings()) { return true; } } return false; } @Override public Query getQuery(QueryContext context, QueryBindings bindings) { BooleanQuery query = new BooleanQuery(); for (int i = 0; i < queries.size(); i++) { BooleanClause.Occur occur; switch (types.get(i)) { case MUST: occur = BooleanClause.Occur.MUST; break; case NOT: occur = BooleanClause.Occur.MUST_NOT; break; case SHOULD: occur = BooleanClause.Occur.SHOULD; break; default: throw new IllegalArgumentException(types.get(i).toString()); } query.add(queries.get(i).getQuery(context, bindings), occur); } return query; } @Override public CompoundExplainer getExplainer(ExplainContext context) { CompoundExplainer explainer = new CompoundExplainer(Type.FUNCTION); explainer.addAttribute(Label.NAME, PrimitiveExplainer.getInstance("AND")); for (FullTextQueryExpression query : queries) { explainer.get().put(Label.OPERAND, query.getExplainer(context)); } return explainer; } @Override public String toString() { return queries.toString(); } }; if (isConstant) { return new Constant(result.getQuery(buildContext, null)); } else { return result; } } public IndexScan_FullText scanOperator(String query, int limit) { return scanOperator(parseQuery(query), limit); } public IndexScan_FullText scanOperator(FullTextQueryExpression query, int limit) { RowType rowType = null; if (buildContext != null) rowType = infos.searchRowType(buildContext.getSession(), indexName); return new IndexScan_FullText(indexName, query, limit, rowType); } }