| // Copyright (C) 2014 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.elasticsearch; |
| |
| import com.google.gerrit.elasticsearch.builders.BoolQueryBuilder; |
| import com.google.gerrit.elasticsearch.builders.QueryBuilder; |
| import com.google.gerrit.elasticsearch.builders.QueryBuilders; |
| import com.google.gerrit.index.FieldDef; |
| import com.google.gerrit.index.FieldType; |
| import com.google.gerrit.index.query.AndPredicate; |
| import com.google.gerrit.index.query.IndexPredicate; |
| import com.google.gerrit.index.query.IntegerRangePredicate; |
| import com.google.gerrit.index.query.NotPredicate; |
| import com.google.gerrit.index.query.OrPredicate; |
| import com.google.gerrit.index.query.PostFilterPredicate; |
| import com.google.gerrit.index.query.Predicate; |
| import com.google.gerrit.index.query.QueryParseException; |
| import com.google.gerrit.index.query.RegexPredicate; |
| import com.google.gerrit.index.query.TimestampRangePredicate; |
| import com.google.gerrit.server.query.change.AfterPredicate; |
| import java.time.Instant; |
| |
| public class ElasticQueryBuilder { |
| |
| <T> QueryBuilder toQueryBuilder(Predicate<T> 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<T>) p); |
| } else if (p instanceof PostFilterPredicate) { |
| return QueryBuilders.matchAllQuery(); |
| } else { |
| throw new QueryParseException("cannot create query for index: " + p); |
| } |
| } |
| |
| private <T> BoolQueryBuilder and(Predicate<T> p) throws QueryParseException { |
| BoolQueryBuilder b = QueryBuilders.boolQuery(); |
| for (Predicate<T> c : p.getChildren()) { |
| b.must(toQueryBuilder(c)); |
| } |
| return b; |
| } |
| |
| private <T> BoolQueryBuilder or(Predicate<T> p) throws QueryParseException { |
| BoolQueryBuilder q = QueryBuilders.boolQuery(); |
| for (Predicate<T> c : p.getChildren()) { |
| q.should(toQueryBuilder(c)); |
| } |
| return q; |
| } |
| |
| private <T> QueryBuilder not(Predicate<T> p) throws QueryParseException { |
| Predicate<T> n = p.getChild(0); |
| if (n instanceof TimestampRangePredicate) { |
| return notTimestamp((TimestampRangePredicate<T>) n); |
| } |
| |
| // Lucene does not support negation, start with all and subtract. |
| BoolQueryBuilder q = QueryBuilders.boolQuery(); |
| q.must(QueryBuilders.matchAllQuery()); |
| q.mustNot(toQueryBuilder(n)); |
| return q; |
| } |
| |
| private <T> QueryBuilder fieldQuery(IndexPredicate<T> p) throws QueryParseException { |
| FieldType<?> type = p.getType(); |
| FieldDef<?, ?> field = p.getField(); |
| String name = field.getName(); |
| String value = p.getValue(); |
| |
| if (type == FieldType.INTEGER) { |
| // QueryBuilder encodes integer fields as prefix coded bits, |
| // which elasticsearch's queryString can't handle. |
| // Create integer terms with string representations instead. |
| return QueryBuilders.termQuery(name, value); |
| } else if (type == FieldType.INTEGER_RANGE) { |
| return intRangeQuery(p); |
| } else if (type == FieldType.TIMESTAMP) { |
| return timestampQuery(p); |
| } else if (type == FieldType.EXACT) { |
| return exactQuery(p); |
| } else if (type == FieldType.PREFIX) { |
| return QueryBuilders.matchPhrasePrefixQuery(name, value); |
| } else if (type == FieldType.FULL_TEXT) { |
| return QueryBuilders.matchPhraseQuery(name, value); |
| } else { |
| throw FieldType.badFieldType(p.getType()); |
| } |
| } |
| |
| private <T> QueryBuilder intRangeQuery(IndexPredicate<T> p) throws QueryParseException { |
| if (p instanceof IntegerRangePredicate) { |
| IntegerRangePredicate<T> r = (IntegerRangePredicate<T>) p; |
| int minimum = r.getMinimumValue(); |
| int maximum = r.getMaximumValue(); |
| if (minimum == maximum) { |
| // Just fall back to a standard integer query. |
| return QueryBuilders.termQuery(p.getField().getName(), minimum); |
| } |
| return QueryBuilders.rangeQuery(p.getField().getName()).gte(minimum).lte(maximum); |
| } |
| throw new QueryParseException("not an integer range: " + p); |
| } |
| |
| private <T> QueryBuilder notTimestamp(TimestampRangePredicate<T> r) throws QueryParseException { |
| if (r.getMinTimestamp().getTime() == 0) { |
| return QueryBuilders.rangeQuery(r.getField().getName()) |
| .gt(Instant.ofEpochMilli(r.getMaxTimestamp().getTime())); |
| } |
| throw new QueryParseException("cannot negate: " + r); |
| } |
| |
| private <T> QueryBuilder timestampQuery(IndexPredicate<T> p) throws QueryParseException { |
| if (p instanceof TimestampRangePredicate) { |
| TimestampRangePredicate<T> r = (TimestampRangePredicate<T>) p; |
| if (p instanceof AfterPredicate) { |
| return QueryBuilders.rangeQuery(r.getField().getName()) |
| .gte(Instant.ofEpochMilli(r.getMinTimestamp().getTime())); |
| } |
| return QueryBuilders.rangeQuery(r.getField().getName()) |
| .gte(Instant.ofEpochMilli(r.getMinTimestamp().getTime())) |
| .lte(Instant.ofEpochMilli(r.getMaxTimestamp().getTime())); |
| } |
| throw new QueryParseException("not a timestamp: " + p); |
| } |
| |
| private <T> QueryBuilder exactQuery(IndexPredicate<T> p) { |
| String name = p.getField().getName(); |
| String value = p.getValue(); |
| |
| if (!p.getField().isRepeatable() && value.isEmpty()) { |
| return new BoolQueryBuilder().mustNot(QueryBuilders.existsQuery(name)); |
| } else if (p instanceof RegexPredicate) { |
| if (value.startsWith("^")) { |
| value = value.substring(1); |
| } |
| if (value.endsWith("$") && !value.endsWith("\\$") && !value.endsWith("\\\\$")) { |
| value = value.substring(0, value.length() - 1); |
| } |
| return QueryBuilders.regexpQuery(name + ".key", value); |
| } else { |
| return QueryBuilders.termQuery(name + ".key", value); |
| } |
| } |
| } |