/* * Copyright 2014, Mysema Ltd * * 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.querydsl.elasticsearch2; import java.util.Collection; import java.util.Set; import javax.annotation.Nullable; import org.apache.lucene.queryparser.flexible.core.util.StringUtils; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.IdsQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.sort.SortBuilder; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import com.querydsl.core.types.*; import com.querydsl.core.types.dsl.Expressions; /** * Serializes the given Querydsl query to a String query for Elasticsearch * * @author Kevin Leturc */ public class ElasticsearchSerializer implements Visitor<Object, BoolQueryBuilder> { /** AND and OR operands. */ private static final Set<Operator> AND_OR = Sets.<Operator>newHashSet(Ops.AND, Ops.OR); public Object handle(Expression<?> expression) { BoolQueryBuilder context = QueryBuilders.boolQuery(); QueryBuilder query = (QueryBuilder) expression.accept(this, context); if (!context.hasClauses() && query != null) { context.must(query); } return context; } public SortBuilder toSort(OrderSpecifier<?> orderBy) { Object key = orderBy.getTarget().accept(this, null); return SortBuilders.fieldSort(key.toString()).order(orderBy.getOrder() == Order.ASC ? SortOrder.ASC : SortOrder.DESC); } @Nullable @Override public Object visit(Constant<?> expr, @Nullable BoolQueryBuilder context) { if (Enum.class.isAssignableFrom(expr.getType())) { return ((Enum<?>) expr.getConstant()).name(); } else { return expr.getConstant(); } } @Nullable @Override public Object visit(FactoryExpression<?> expr, @Nullable BoolQueryBuilder context) { return null; } public String asDBKey(Operation<?> expr, int index) { return StringUtils.toString(asDBValue(expr, index)); } public Object asDBValue(Operation<?> expr, int index) { return expr.getArg(index).accept(this, null); } @Nullable @Override public Object visit(Operation<?> expr, @Nullable BoolQueryBuilder context) { Preconditions.checkNotNull(context); Operator op = expr.getOperator(); if (op == Ops.EQ) { Expression<?> keyArg = expr.getArg(0); String value = StringUtils.toString(asDBValue(expr, 1)); if (keyArg instanceof Path<?> && isIdPath((Path<?>) expr.getArg(0))) { return QueryBuilders.idsQuery().ids(value); } else { // Currently all queries are made with ignore case sensitive // Because the query to get exact value have to be run on a not_analyzed field return QueryBuilders.queryStringQuery(value).field(asDBKey(expr, 0)); } } else if (op == Ops.EQ_IGNORE_CASE) { String value = StringUtils.toString(asDBValue(expr, 1)); return QueryBuilders.queryStringQuery(value).field(asDBKey(expr, 0)); } else if (op == Ops.NE) { // Decompose the query as NOT and EQ query return visit( Expressions.predicate( Ops.NOT, Expressions.predicate( Ops.EQ, expr.getArg(0), expr.getArg(1))), context); } else if (op == Ops.STRING_IS_EMPTY) { return QueryBuilders.queryStringQuery("").field(asDBKey(expr, 0)); } else if (op == Ops.AND || op == Ops.OR) { Operation<?> left = (Operation<?>) expr.getArg(0); Operation<?> right = (Operation<?>) expr.getArg(1); // Perform the left expression QueryBuilder leftResult = visitSubAndOr(op, context, left); // Perform the right expression QueryBuilder rightResult = visitSubAndOr(op, context, right); if (op == Ops.AND) { safeMust(context, leftResult); safeMust(context, rightResult); } else { safeShould(context, leftResult); safeShould(context, rightResult); } return null; } else if (op == Ops.IN) { int constIndex = 0; int exprIndex = 1; if (expr.getArg(1) instanceof Constant<?>) { constIndex = 1; exprIndex = 0; } Expression<?> keyExpr = expr.getArg(exprIndex); if (keyExpr instanceof Path<?> && isIdPath((Path<?>) keyExpr)) { IdsQueryBuilder idsQuery = QueryBuilders.idsQuery(); // Hope this is the only case for Elasticsearch ids Collection<?> values = (Collection<?>) ((Constant<?>) expr.getArg(constIndex)).getConstant(); for (Object value : values) { idsQuery.addIds(StringUtils.toString(value)); } return idsQuery; } else { // Currently all queries are made with ignore case sensitive // Because the query to get exact value have to be run on a not_analyzed field BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); String key = asDBKey(expr, exprIndex); if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) { Collection<?> values = (Collection<?>) ((Constant<?>) expr.getArg(constIndex)).getConstant(); for (Object value : values) { boolQuery.should(QueryBuilders.queryStringQuery(StringUtils.toString(value)).field(key)); } return boolQuery; } } } else if (op == Ops.NOT_IN) { // Decompose the query as NOT and IN query return visit( Expressions.predicate( Ops.NOT, Expressions.predicate( Ops.IN, expr.getArg(1))), context); } else if (op == Ops.BETWEEN) { Object from = asDBValue(expr, 1); Object to = asDBValue(expr, 2); return QueryBuilders.rangeQuery(asDBKey(expr, 0)).from(from).to(to); } else if (op == Ops.LT) { return QueryBuilders.rangeQuery(asDBKey(expr, 0)).lt(asDBValue(expr, 1)); } else if (op == Ops.GT) { return QueryBuilders.rangeQuery(asDBKey(expr, 0)).gt(asDBValue(expr, 1)); } else if (op == Ops.LOE) { return QueryBuilders.rangeQuery(asDBKey(expr, 0)).lte(asDBValue(expr, 1)); } else if (op == Ops.GOE) { return QueryBuilders.rangeQuery(asDBKey(expr, 0)).gte(asDBValue(expr, 1)); } else if (op == Ops.STARTS_WITH) { // Currently all queries are made with ignore case sensitive String value = StringUtils.toString(asDBValue(expr, 1)); return QueryBuilders.queryStringQuery(value + "*").field(asDBKey(expr, 0)).analyzeWildcard(true); } else if (op == Ops.STARTS_WITH_IC) { String value = StringUtils.toString(asDBValue(expr, 1)); return QueryBuilders.queryStringQuery(value + "*").field(asDBKey(expr, 0)).analyzeWildcard(true); } else if (op == Ops.ENDS_WITH) { // Currently all queries are made with ignore case sensitive String value = StringUtils.toString(asDBValue(expr, 1)); return QueryBuilders.queryStringQuery("*" + value).field(asDBKey(expr, 0)).analyzeWildcard(true); } else if (op == Ops.ENDS_WITH_IC) { String value = StringUtils.toString(asDBValue(expr, 1)); return QueryBuilders.queryStringQuery("*" + value).field(asDBKey(expr, 0)).analyzeWildcard(true); } else if (op == Ops.STRING_CONTAINS) { String value = StringUtils.toString(asDBValue(expr, 1)); return QueryBuilders.queryStringQuery("*" + value + "*").field(asDBKey(expr, 0)).analyzeWildcard(true); } else if (op == Ops.NOT) { // Handle the not's child BoolQueryBuilder subContext = QueryBuilders.boolQuery(); QueryBuilder result = (QueryBuilder) expr.getArg(0).accept(this, subContext); if (result == null) { result = subContext; } return QueryBuilders.boolQuery().mustNot(result); } throw new UnsupportedOperationException("Illegal operation " + expr); } @Nullable @Override public Object visit(ParamExpression<?> expr, @Nullable BoolQueryBuilder context) { return null; } @Nullable @Override public Object visit(Path<?> expr, @Nullable BoolQueryBuilder context) { PathMetadata metadata = expr.getMetadata(); return getKeyForPath(expr, metadata); } @Nullable @Override public Object visit(SubQueryExpression<?> expr, @Nullable BoolQueryBuilder context) { return null; } @Nullable @Override public Object visit(TemplateExpression<?> expr, @Nullable BoolQueryBuilder context) { return null; } protected String getKeyForPath(Path<?> expr, PathMetadata metadata) { if (isIdPath(expr)) { return "_id"; } else { return metadata.getElement().toString(); } } protected boolean isIdPath(Path<?> expr) { return "id".equals(expr.getMetadata().getElement().toString()); } private QueryBuilder visitSubAndOr(Operator op, BoolQueryBuilder context, Operation<?> subOperation) { QueryBuilder result; if (AND_OR.contains(subOperation.getOperator()) && subOperation.getOperator() != op) { // Opposite case, if current operator is an AND so sub operation is a OR, so create a sub query result = QueryBuilders.boolQuery(); subOperation.accept(this, (BoolQueryBuilder) result); } else { // Here let's do recursive if sub operation has the same operator than the current one (result is null) // or it's another operator than AND/OR so add it to query result = (QueryBuilder) subOperation.accept(this, context); } return result; } private void safeMust(BoolQueryBuilder context, QueryBuilder query) { if (query != null) { context.must(query); } } private void safeShould(BoolQueryBuilder context, QueryBuilder query) { if (query != null) { context.should(query); } } }