| // Copyright (C) 2013 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.lucene; |
| |
| import static org.apache.lucene.search.BooleanClause.Occur.MUST; |
| import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT; |
| import static org.apache.lucene.search.BooleanClause.Occur.SHOULD; |
| |
| import com.google.common.collect.Lists; |
| import com.google.gerrit.server.index.ChangeField; |
| import com.google.gerrit.server.index.FieldType; |
| import com.google.gerrit.server.index.IndexPredicate; |
| import com.google.gerrit.server.index.IntegerRangePredicate; |
| import com.google.gerrit.server.index.RegexPredicate; |
| import com.google.gerrit.server.index.Schema; |
| import com.google.gerrit.server.index.TimestampRangePredicate; |
| import com.google.gerrit.server.query.AndPredicate; |
| import com.google.gerrit.server.query.NotPredicate; |
| import com.google.gerrit.server.query.OrPredicate; |
| import com.google.gerrit.server.query.Predicate; |
| import com.google.gerrit.server.query.QueryParseException; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.SortKeyPredicate; |
| |
| import org.apache.lucene.analysis.Analyzer; |
| import org.apache.lucene.index.Term; |
| import org.apache.lucene.search.BooleanQuery; |
| import org.apache.lucene.search.MatchAllDocsQuery; |
| import org.apache.lucene.search.NumericRangeQuery; |
| import org.apache.lucene.search.PrefixQuery; |
| import org.apache.lucene.search.Query; |
| import org.apache.lucene.search.RegexpQuery; |
| import org.apache.lucene.search.TermQuery; |
| import org.apache.lucene.util.BytesRef; |
| import org.apache.lucene.util.NumericUtils; |
| |
| import java.util.Date; |
| import java.util.List; |
| |
| public class QueryBuilder { |
| private static final String ID_FIELD = ChangeField.LEGACY_ID.getName(); |
| |
| public static Term idTerm(ChangeData cd) { |
| return intTerm(ID_FIELD, cd.getId().get()); |
| } |
| |
| public static Term idTerm(int id) { |
| return intTerm(ID_FIELD, id); |
| } |
| |
| private final Schema<ChangeData> schema; |
| private final org.apache.lucene.util.QueryBuilder queryBuilder; |
| |
| public QueryBuilder(Schema<ChangeData> schema, Analyzer analyzer) { |
| this.schema = schema; |
| queryBuilder = new org.apache.lucene.util.QueryBuilder(analyzer); |
| } |
| |
| public Query toQuery(Predicate<ChangeData> p) throws QueryParseException { |
| if (p instanceof AndPredicate) { |
| return and(p); |
| } else if (p instanceof OrPredicate) { |
| return or(p); |
| } else if (p instanceof NotPredicate) { |
| return not(p); |
| } else if (p instanceof IndexPredicate) { |
| return fieldQuery((IndexPredicate<ChangeData>) p); |
| } else { |
| throw new QueryParseException("cannot create query for index: " + p); |
| } |
| } |
| |
| private Query or(Predicate<ChangeData> p) |
| throws QueryParseException { |
| try { |
| BooleanQuery q = new BooleanQuery(); |
| for (int i = 0; i < p.getChildCount(); i++) { |
| q.add(toQuery(p.getChild(i)), SHOULD); |
| } |
| return q; |
| } catch (BooleanQuery.TooManyClauses e) { |
| throw new QueryParseException("cannot create query for index: " + p, e); |
| } |
| } |
| |
| private Query and(Predicate<ChangeData> p) |
| throws QueryParseException { |
| try { |
| BooleanQuery b = new BooleanQuery(); |
| List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount()); |
| for (int i = 0; i < p.getChildCount(); i++) { |
| Predicate<ChangeData> c = p.getChild(i); |
| if (c instanceof NotPredicate) { |
| Predicate<ChangeData> n = c.getChild(0); |
| if (n instanceof TimestampRangePredicate) { |
| b.add(notTimestamp((TimestampRangePredicate<ChangeData>) n), MUST); |
| } else { |
| not.add(toQuery(n)); |
| } |
| } else { |
| b.add(toQuery(c), MUST); |
| } |
| } |
| for (Query q : not) { |
| b.add(q, MUST_NOT); |
| } |
| return b; |
| } catch (BooleanQuery.TooManyClauses e) { |
| throw new QueryParseException("cannot create query for index: " + p, e); |
| } |
| } |
| |
| private Query not(Predicate<ChangeData> p) |
| throws QueryParseException { |
| Predicate<ChangeData> n = p.getChild(0); |
| if (n instanceof TimestampRangePredicate) { |
| return notTimestamp((TimestampRangePredicate<ChangeData>) n); |
| } |
| |
| // Lucene does not support negation, start with all and subtract. |
| BooleanQuery q = new BooleanQuery(); |
| q.add(new MatchAllDocsQuery(), MUST); |
| q.add(toQuery(n), MUST_NOT); |
| return q; |
| } |
| |
| private Query fieldQuery(IndexPredicate<ChangeData> p) |
| throws QueryParseException { |
| if (p.getType() == FieldType.INTEGER) { |
| return intQuery(p); |
| } else if (p.getType() == FieldType.INTEGER_RANGE) { |
| return intRangeQuery(p); |
| } else if (p.getType() == FieldType.TIMESTAMP) { |
| return timestampQuery(p); |
| } else if (p.getType() == FieldType.EXACT) { |
| return exactQuery(p); |
| } else if (p.getType() == FieldType.PREFIX) { |
| return prefixQuery(p); |
| } else if (p.getType() == FieldType.FULL_TEXT) { |
| return fullTextQuery(p); |
| } else if (p instanceof SortKeyPredicate) { |
| return sortKeyQuery((SortKeyPredicate) p); |
| } else { |
| throw badFieldType(p.getType()); |
| } |
| } |
| |
| private static Term intTerm(String name, int value) { |
| BytesRef bytes = new BytesRef(NumericUtils.BUF_SIZE_INT); |
| NumericUtils.intToPrefixCodedBytes(value, 0, bytes); |
| return new Term(name, bytes); |
| } |
| |
| private Query intQuery(IndexPredicate<ChangeData> p) |
| throws QueryParseException { |
| int value; |
| try { |
| // Can't use IntPredicate because it and IndexPredicate are different |
| // subclasses of OperatorPredicate. |
| value = Integer.valueOf(p.getValue()); |
| } catch (IllegalArgumentException e) { |
| throw new QueryParseException("not an integer: " + p.getValue()); |
| } |
| return new TermQuery(intTerm(p.getField().getName(), value)); |
| } |
| |
| private Query intRangeQuery(IndexPredicate<ChangeData> p) |
| throws QueryParseException { |
| if (p instanceof IntegerRangePredicate) { |
| IntegerRangePredicate<ChangeData> r = |
| (IntegerRangePredicate<ChangeData>) p; |
| int minimum = r.getMinimumValue(); |
| int maximum = r.getMaximumValue(); |
| if (minimum == maximum) { |
| // Just fall back to a standard integer query. |
| return new TermQuery(intTerm(p.getField().getName(), minimum)); |
| } else { |
| return NumericRangeQuery.newIntRange( |
| r.getField().getName(), |
| minimum, |
| maximum, |
| true, |
| true); |
| } |
| } |
| throw new QueryParseException("not an integer range: " + p); |
| } |
| |
| private Query sortKeyQuery(SortKeyPredicate p) { |
| long min = p.getMinValue(schema); |
| long max = p.getMaxValue(schema); |
| return NumericRangeQuery.newLongRange( |
| p.getField().getName(), |
| min != Long.MIN_VALUE ? min : null, |
| max != Long.MAX_VALUE ? max : null, |
| false, false); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private Query timestampQuery(IndexPredicate<ChangeData> p) |
| throws QueryParseException { |
| if (p instanceof TimestampRangePredicate) { |
| TimestampRangePredicate<ChangeData> r = |
| (TimestampRangePredicate<ChangeData>) p; |
| if (r.getField() == ChangeField.LEGACY_UPDATED) { |
| return NumericRangeQuery.newIntRange( |
| r.getField().getName(), |
| toIndexTimeInMinutes(r.getMinTimestamp()), |
| toIndexTimeInMinutes(r.getMaxTimestamp()), |
| true, true); |
| } else { |
| return NumericRangeQuery.newLongRange( |
| r.getField().getName(), |
| r.getMinTimestamp().getTime(), |
| r.getMaxTimestamp().getTime(), |
| true, true); |
| } |
| } |
| throw new QueryParseException("not a timestamp: " + p); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private Query notTimestamp(TimestampRangePredicate<ChangeData> r) |
| throws QueryParseException { |
| if (r.getMinTimestamp().getTime() == 0) { |
| if (r.getField() == ChangeField.LEGACY_UPDATED) { |
| return NumericRangeQuery.newIntRange( |
| r.getField().getName(), |
| toIndexTimeInMinutes(r.getMaxTimestamp()), |
| null, |
| true, true); |
| } else { |
| return NumericRangeQuery.newLongRange( |
| r.getField().getName(), |
| r.getMaxTimestamp().getTime(), |
| null, |
| true, true); |
| } |
| } |
| throw new QueryParseException("cannot negate: " + r); |
| } |
| |
| private Query exactQuery(IndexPredicate<ChangeData> p) { |
| if (p instanceof RegexPredicate<?>) { |
| return regexQuery(p); |
| } else { |
| return new TermQuery(new Term(p.getField().getName(), p.getValue())); |
| } |
| } |
| |
| private Query regexQuery(IndexPredicate<ChangeData> p) { |
| String re = p.getValue(); |
| if (re.startsWith("^")) { |
| re = re.substring(1); |
| } |
| if (re.endsWith("$") && !re.endsWith("\\$")) { |
| re = re.substring(0, re.length() - 1); |
| } |
| return new RegexpQuery(new Term(p.getField().getName(), re)); |
| } |
| |
| private Query prefixQuery(IndexPredicate<ChangeData> p) { |
| return new PrefixQuery(new Term(p.getField().getName(), p.getValue())); |
| } |
| |
| private Query fullTextQuery(IndexPredicate<ChangeData> p) { |
| return queryBuilder.createPhraseQuery(p.getField().getName(), p.getValue()); |
| } |
| |
| public int toIndexTimeInMinutes(Date ts) { |
| return (int) (ts.getTime() / 60000); |
| } |
| |
| public static IllegalArgumentException badFieldType(FieldType<?> t) { |
| return new IllegalArgumentException("unknown index field type " + t); |
| } |
| } |