package org.infinispan.query.dsl.embedded.impl; import java.io.IOException; import java.io.StringReader; import java.util.Map; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.RegexpQuery; import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; import org.apache.lucene.search.TermRangeQuery; import org.hibernate.search.analyzer.impl.LuceneAnalyzerReference; import org.hibernate.search.analyzer.spi.AnalyzerReference; import org.hibernate.search.bridge.FieldBridge; import org.hibernate.search.bridge.builtin.NumericFieldBridge; import org.hibernate.search.bridge.builtin.impl.NullEncodingTwoWayFieldBridge; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.query.dsl.BooleanJunction; import org.hibernate.search.query.dsl.EntityContext; import org.hibernate.search.query.dsl.FieldCustomization; import org.hibernate.search.query.dsl.PhraseContext; import org.hibernate.search.query.dsl.QueryBuilder; import org.hibernate.search.query.dsl.QueryContextBuilder; import org.hibernate.search.query.dsl.RangeMatchingContext; import org.hibernate.search.query.dsl.RangeTerminationExcludable; import org.hibernate.search.query.dsl.impl.FieldBridgeCustomization; import org.hibernate.search.spi.SearchIntegrator; import org.infinispan.objectfilter.impl.ql.PropertyPath; import org.infinispan.objectfilter.impl.syntax.AggregationExpr; import org.infinispan.objectfilter.impl.syntax.AndExpr; import org.infinispan.objectfilter.impl.syntax.BetweenExpr; import org.infinispan.objectfilter.impl.syntax.BooleanExpr; import org.infinispan.objectfilter.impl.syntax.ComparisonExpr; import org.infinispan.objectfilter.impl.syntax.ConstantBooleanExpr; import org.infinispan.objectfilter.impl.syntax.ConstantValueExpr; import org.infinispan.objectfilter.impl.syntax.FullTextBoostExpr; import org.infinispan.objectfilter.impl.syntax.FullTextOccurExpr; import org.infinispan.objectfilter.impl.syntax.FullTextRangeExpr; import org.infinispan.objectfilter.impl.syntax.FullTextRegexpExpr; import org.infinispan.objectfilter.impl.syntax.FullTextTermExpr; import org.infinispan.objectfilter.impl.syntax.IsNullExpr; import org.infinispan.objectfilter.impl.syntax.LikeExpr; import org.infinispan.objectfilter.impl.syntax.NotExpr; import org.infinispan.objectfilter.impl.syntax.OrExpr; import org.infinispan.objectfilter.impl.syntax.PropertyValueExpr; import org.infinispan.objectfilter.impl.syntax.Visitor; import org.infinispan.objectfilter.impl.syntax.parser.IckleParsingResult; import org.infinispan.query.logging.Log; import org.jboss.logging.Logger; /** * An *Expr {@link Visitor} that transforms a {@link IckleParsingResult} into a {@link LuceneQueryParsingResult}. * <p> * NOTE: This is not stateless, not threadsafe, so it can only be used for a single transformation at a time. * * @author anistor@redhat.com * @since 9.0 */ public final class LuceneQueryMaker<TypeMetadata> implements Visitor<Query, Query> { private static final Log log = Logger.getMessageLogger(Log.class, LuceneQueryMaker.class.getName()); private static final char LUCENE_SINGLE_CHARACTER_WILDCARD = '?'; private static final char LUCENE_MULTIPLE_CHARACTERS_WILDCARD = '*'; private static final char LUCENE_WILDCARD_ESCAPE_CHARACTER = '\\'; private final QueryContextBuilder queryContextBuilder; private final FieldBridgeAndAnalyzerProvider<TypeMetadata> fieldBridgeAndAnalyzerProvider; private final SearchIntegrator searchFactory; private Map<String, Object> namedParameters; private QueryBuilder queryBuilder; private TypeMetadata entityType; private Analyzer entityAnalyzer; /** * This provides some glue code for Hibernate Search. Implementations are different for embedded and remote use case. */ public interface FieldBridgeAndAnalyzerProvider<TypeMetadata> { /** * Returns the field bridge to be applied when executing queries on the given property of the given entity type. * * @param typeMetadata the entity type hosting the given property; may either identify an actual Java type or a * virtual type managed by the given implementation; never {@code null} * @param propertyPath an array of strings denoting the property path; never {@code null} * @return the field bridge to be used for querying the given property; may be {@code null} */ FieldBridge getFieldBridge(TypeMetadata typeMetadata, String[] propertyPath); /** * Get the analyzer to be used for a property. */ Analyzer getAnalyzer(SearchIntegrator searchIntegrator, TypeMetadata typeMetadata, String[] propertyPath); /** * Populate the EntityContext with the analyzers that will be used for properties. * * @param parsingResult the parsed query * @param entityContext the entity context to populate */ void overrideAnalyzers(IckleParsingResult<TypeMetadata> parsingResult, EntityContext entityContext); } LuceneQueryMaker(SearchIntegrator searchFactory, FieldBridgeAndAnalyzerProvider<TypeMetadata> fieldBridgeAndAnalyzerProvider) { if (searchFactory == null) { throw new IllegalArgumentException("searchFactory argument cannot be null"); } this.fieldBridgeAndAnalyzerProvider = fieldBridgeAndAnalyzerProvider; this.queryContextBuilder = searchFactory.buildQueryBuilder(); this.searchFactory = searchFactory; } public LuceneQueryParsingResult<TypeMetadata> transform(IckleParsingResult<TypeMetadata> parsingResult, Map<String, Object> namedParameters, Class<?> targetedType) { this.namedParameters = namedParameters; EntityContext entityContext = queryContextBuilder.forEntity(targetedType); fieldBridgeAndAnalyzerProvider.overrideAnalyzers(parsingResult, entityContext); queryBuilder = entityContext.get(); entityType = parsingResult.getTargetEntityMetadata(); AnalyzerReference analyzerReference = ((ExtendedSearchIntegrator) searchFactory).getAnalyzerReference(targetedType); if (analyzerReference.is(LuceneAnalyzerReference.class)) { entityAnalyzer = analyzerReference.unwrap(LuceneAnalyzerReference.class).getAnalyzer(); } Query query = makeQuery(parsingResult.getWhereClause()); // an all negative top level boolean query is not allowed; needs a bit of rewriting if (query instanceof BooleanQuery) { BooleanQuery booleanQuery = (BooleanQuery) query; boolean allClausesAreMustNot = booleanQuery.clauses().stream().allMatch(c -> c.getOccur() == BooleanClause.Occur.MUST_NOT); if (allClausesAreMustNot) { //It is illegal to have only must-not queries, in this case we need to add a positive clause to match everything else. BooleanQuery.Builder builder = new BooleanQuery.Builder(); for (BooleanClause clause : booleanQuery.clauses()) { builder.add(clause.getQuery(), BooleanClause.Occur.MUST_NOT); } builder.add(new MatchAllDocsQuery(), BooleanClause.Occur.FILTER); query = builder.build(); } } Sort sort = makeSort(parsingResult.getSortFields()); return new LuceneQueryParsingResult<>(query, parsingResult.getTargetEntityName(), parsingResult.getTargetEntityMetadata(), parsingResult.getProjections(), sort); } private Query makeQuery(BooleanExpr expr) { return expr == null ? queryBuilder.all().createQuery() : expr.acceptVisitor(this); } private Sort makeSort(org.infinispan.objectfilter.SortField[] sortFields) { if (sortFields == null || sortFields.length == 0) { return null; } SortField[] fields = new SortField[sortFields.length]; for (int i = 0; i < fields.length; i++) { org.infinispan.objectfilter.SortField sf = sortFields[i]; SortField.Type sortType = SortField.Type.STRING; FieldBridge fieldBridge = fieldBridgeAndAnalyzerProvider.getFieldBridge(entityType, sf.getPath().asArrayPath()); if (fieldBridge instanceof NullEncodingTwoWayFieldBridge) { fieldBridge = ((NullEncodingTwoWayFieldBridge) fieldBridge).unwrap(FieldBridge.class); } // Determine sort type based on FieldBridgeType. SortField.BYTE and SortField.SHORT are not covered yet! if (fieldBridge instanceof NumericFieldBridge) { switch ((NumericFieldBridge) fieldBridge) { case INT_FIELD_BRIDGE: sortType = SortField.Type.INT; break; case LONG_FIELD_BRIDGE: sortType = SortField.Type.LONG; break; case FLOAT_FIELD_BRIDGE: sortType = SortField.Type.FLOAT; break; case DOUBLE_FIELD_BRIDGE: sortType = SortField.Type.DOUBLE; break; } } fields[i] = new SortField(sf.getPath().asStringPath(), sortType, !sf.isAscending()); } return new Sort(fields); } @Override public Query visit(FullTextOccurExpr fullTextOccurExpr) { Query child = fullTextOccurExpr.getChild().acceptVisitor(this); return new BooleanQuery.Builder() .add(child, convertOccur(fullTextOccurExpr)) //TODO [anistor] the parent should 'absorb' this sub-expression to avoid the superfluous single-child BooleanQuery .build(); } private BooleanClause.Occur convertOccur(FullTextOccurExpr fullTextOccurExpr) { switch (fullTextOccurExpr.getOccur()) { case SHOULD: return BooleanClause.Occur.SHOULD; case MUST: return BooleanClause.Occur.MUST; case MUST_NOT: return BooleanClause.Occur.MUST_NOT; case FILTER: return BooleanClause.Occur.FILTER; } throw new IllegalArgumentException("Unknown boolean occur clause: " + fullTextOccurExpr.getOccur()); } @Override public Query visit(FullTextBoostExpr fullTextBoostExpr) { Query child = fullTextBoostExpr.getChild().acceptVisitor(this); return new BoostQuery(child, fullTextBoostExpr.getBoost()); } private boolean isMultiTermText(PropertyPath<?> propertyPath, String text) { Analyzer analyzer = fieldBridgeAndAnalyzerProvider.getAnalyzer(searchFactory, entityType, propertyPath.asArrayPath()); if (analyzer == null) { analyzer = entityAnalyzer; } if (analyzer != null) { int terms = 0; try (TokenStream tokenStream = analyzer.tokenStream(propertyPath.asStringPathWithoutAlias(), new StringReader(text))) { PositionIncrementAttribute posIncAtt = tokenStream.addAttribute(PositionIncrementAttribute.class); tokenStream.reset(); while (tokenStream.incrementToken()) { if (posIncAtt.getPositionIncrement() > 0) { if (++terms > 1) { break; } } } tokenStream.end(); } catch (IOException e) { // Highly unlikely to happen when reading from a StringReader. log.error(e); } return terms > 1; } // fallback to good old indexOf return text.trim().indexOf(' ') != -1; } @Override public Query visit(FullTextTermExpr fullTextTermExpr) { PropertyValueExpr propertyValueExpr = (PropertyValueExpr) fullTextTermExpr.getChild(); String text = fullTextTermExpr.getTerm(); int asteriskPos = text.indexOf(LUCENE_MULTIPLE_CHARACTERS_WILDCARD); int questionPos = text.indexOf(LUCENE_SINGLE_CHARACTER_WILDCARD); if (asteriskPos == -1 && questionPos == -1) { if (isMultiTermText(propertyValueExpr.getPropertyPath(), text)) { // phrase query PhraseContext phrase = queryBuilder.phrase(); if (fullTextTermExpr.getFuzzySlop() != null) { phrase = phrase.withSlop(fullTextTermExpr.getFuzzySlop()); } return phrase.onField(propertyValueExpr.getPropertyPath().asStringPath()).sentence(text).createQuery(); } else { // just a single term if (fullTextTermExpr.getFuzzySlop() != null) { // fuzzy query return applyFieldBridge(true, propertyValueExpr.getPropertyPath(), queryBuilder.keyword() .fuzzy().withEditDistanceUpTo(fullTextTermExpr.getFuzzySlop()) .onField(propertyValueExpr.getPropertyPath().asStringPath())) .matching(text).createQuery(); } // term query return applyFieldBridge(true, propertyValueExpr.getPropertyPath(), queryBuilder.keyword().onField(propertyValueExpr.getPropertyPath().asStringPath())) .matching(text).createQuery(); } } else { if (fullTextTermExpr.getFuzzySlop() != null) { throw log.getPrefixWildcardOrRegexpQueriesCannotBeFuzzy(fullTextTermExpr.toQueryString()); } if (questionPos == -1 && asteriskPos == text.length() - 1) { // term prefix query String prefix = text.substring(0, text.length() - 1); return new PrefixQuery(new Term(propertyValueExpr.getPropertyPath().asStringPath(), prefix)); } // wildcard query return applyFieldBridge(true, propertyValueExpr.getPropertyPath(), queryBuilder.keyword().wildcard().onField(propertyValueExpr.getPropertyPath().asStringPath())) .matching(text).createQuery(); } } @Override public Query visit(FullTextRegexpExpr fullTextRegexpExpr) { PropertyValueExpr propertyValueExpr = (PropertyValueExpr) fullTextRegexpExpr.getChild(); String regexp = fullTextRegexpExpr.getRegexp(); // regexp query return new RegexpQuery(new Term(propertyValueExpr.getPropertyPath().asStringPath(), regexp)); //todo [anistor] fieldbridge? } @Override public Query visit(FullTextRangeExpr fullTextRangeExpr) { PropertyValueExpr propertyValueExpr = (PropertyValueExpr) fullTextRangeExpr.getChild(); //todo [anistor] incomplete implementation ? if (fullTextRangeExpr.getLower() == null && fullTextRangeExpr.getUpper() == null) { return new TermRangeQuery(propertyValueExpr.getPropertyPath().asStringPath(), null, null, fullTextRangeExpr.isIncludeLower(), fullTextRangeExpr.isIncludeUpper()); } RangeMatchingContext rangeMatchingContext = applyFieldBridge(true, propertyValueExpr.getPropertyPath(), queryBuilder.range().onField(propertyValueExpr.getPropertyPath().asStringPath())); RangeTerminationExcludable t = null; if (fullTextRangeExpr.getLower() != null) { t = rangeMatchingContext.above(fullTextRangeExpr.getLower()); if (!fullTextRangeExpr.isIncludeLower()) { t.excludeLimit(); } } if (fullTextRangeExpr.getUpper() != null) { t = rangeMatchingContext.below(fullTextRangeExpr.getUpper()); if (!fullTextRangeExpr.isIncludeUpper()) { t.excludeLimit(); } } return t.createQuery(); } @Override public Query visit(NotExpr notExpr) { Query transformedChild = notExpr.getChild().acceptVisitor(this); return queryBuilder.bool().must(transformedChild).not().createQuery(); } @Override public Query visit(OrExpr orExpr) { BooleanJunction<BooleanJunction> booleanJunction = queryBuilder.bool(); for (BooleanExpr c : orExpr.getChildren()) { Query transformedChild = c.acceptVisitor(this); booleanJunction.should(transformedChild); } return booleanJunction.createQuery(); } @Override public Query visit(AndExpr andExpr) { BooleanQuery.Builder builder = new BooleanQuery.Builder(); for (BooleanExpr c : andExpr.getChildren()) { boolean isNegative = c instanceof NotExpr; if (isNegative) { // minor optimization: unwrap negated predicates and add child directly to this predicate c = ((NotExpr) c).getChild(); } Query transformedChild = c.acceptVisitor(this); if (transformedChild instanceof BooleanQuery) { // child absorption BooleanQuery booleanQuery = (BooleanQuery) transformedChild; if (booleanQuery.clauses().size() == 1) { BooleanClause clause = booleanQuery.clauses().get(0); BooleanClause.Occur occur = clause.getOccur(); if (isNegative) { occur = occur == BooleanClause.Occur.MUST_NOT ? BooleanClause.Occur.MUST : BooleanClause.Occur.MUST_NOT; } builder.add(clause.getQuery(), occur); } else { builder.add(transformedChild, isNegative ? BooleanClause.Occur.MUST_NOT : BooleanClause.Occur.MUST); } } else { builder.add(transformedChild, isNegative ? BooleanClause.Occur.MUST_NOT : BooleanClause.Occur.MUST); } } return builder.build(); } @Override public Query visit(IsNullExpr isNullExpr) { PropertyValueExpr propertyValueExpr = (PropertyValueExpr) isNullExpr.getChild(); return applyFieldBridge(false, propertyValueExpr.getPropertyPath(), queryBuilder.keyword().onField(propertyValueExpr.getPropertyPath().asStringPath())).matching(null).createQuery(); } @Override public Query visit(ComparisonExpr comparisonExpr) { PropertyValueExpr propertyValueExpr = (PropertyValueExpr) comparisonExpr.getLeftChild(); ConstantValueExpr constantValueExpr = (ConstantValueExpr) comparisonExpr.getRightChild(); Comparable value = constantValueExpr.getConstantValueAs(propertyValueExpr.getPrimitiveType(), namedParameters); switch (comparisonExpr.getComparisonType()) { case NOT_EQUAL: Query q = applyFieldBridge(false, propertyValueExpr.getPropertyPath(), queryBuilder.keyword().onField(propertyValueExpr.getPropertyPath().asStringPath())) .matching(value).createQuery(); return queryBuilder.bool().must(q).not().createQuery(); case EQUAL: return applyFieldBridge(false, propertyValueExpr.getPropertyPath(), queryBuilder.keyword().onField(propertyValueExpr.getPropertyPath().asStringPath())) .matching(value).createQuery(); case LESS: return applyFieldBridge(false, propertyValueExpr.getPropertyPath(), queryBuilder.range().onField(propertyValueExpr.getPropertyPath().asStringPath())) .below(value).excludeLimit().createQuery(); case LESS_OR_EQUAL: return applyFieldBridge(false, propertyValueExpr.getPropertyPath(), queryBuilder.range().onField(propertyValueExpr.getPropertyPath().asStringPath())) .below(value).createQuery(); case GREATER: return applyFieldBridge(false, propertyValueExpr.getPropertyPath(), queryBuilder.range().onField(propertyValueExpr.getPropertyPath().asStringPath())) .above(value).excludeLimit().createQuery(); case GREATER_OR_EQUAL: return applyFieldBridge(false, propertyValueExpr.getPropertyPath(), queryBuilder.range().onField(propertyValueExpr.getPropertyPath().asStringPath())) .above(value).createQuery(); default: throw new IllegalStateException("Unexpected comparison type: " + comparisonExpr.getComparisonType()); } } @Override public Query visit(BetweenExpr betweenExpr) { PropertyValueExpr propertyValueExpr = (PropertyValueExpr) betweenExpr.getLeftChild(); ConstantValueExpr fromValueExpr = (ConstantValueExpr) betweenExpr.getFromChild(); ConstantValueExpr toValueExpr = (ConstantValueExpr) betweenExpr.getToChild(); Comparable fromValue = fromValueExpr.getConstantValueAs(propertyValueExpr.getPrimitiveType(), namedParameters); Comparable toValue = toValueExpr.getConstantValueAs(propertyValueExpr.getPrimitiveType(), namedParameters); return applyFieldBridge(false, propertyValueExpr.getPropertyPath(), queryBuilder.range().onField(propertyValueExpr.getPropertyPath().asStringPath())) .from(fromValue).to(toValue).createQuery(); } @Override public Query visit(LikeExpr likeExpr) { PropertyValueExpr propertyValueExpr = (PropertyValueExpr) likeExpr.getChild(); StringBuilder lucenePattern = new StringBuilder(likeExpr.getPattern(namedParameters)); // transform 'Like' pattern into Lucene wildcard pattern boolean isEscaped = false; for (int i = 0; i < lucenePattern.length(); i++) { char c = lucenePattern.charAt(i); if (!isEscaped && c == likeExpr.getEscapeChar()) { isEscaped = true; lucenePattern.deleteCharAt(i); } else { if (isEscaped) { isEscaped = false; } else { if (c == LikeExpr.MULTIPLE_CHARACTERS_WILDCARD) { lucenePattern.setCharAt(i, LUCENE_MULTIPLE_CHARACTERS_WILDCARD); continue; } else if (c == LikeExpr.SINGLE_CHARACTER_WILDCARD) { lucenePattern.setCharAt(i, LUCENE_SINGLE_CHARACTER_WILDCARD); continue; } } if (c == LUCENE_SINGLE_CHARACTER_WILDCARD || c == LUCENE_MULTIPLE_CHARACTERS_WILDCARD) { lucenePattern.insert(i, LUCENE_WILDCARD_ESCAPE_CHARACTER); i++; } } } return applyFieldBridge(false, propertyValueExpr.getPropertyPath(), queryBuilder.keyword().wildcard().onField(propertyValueExpr.getPropertyPath().asStringPath())) .matching(lucenePattern.toString()).createQuery(); } @Override public Query visit(ConstantBooleanExpr constantBooleanExpr) { Query all = queryBuilder.all().createQuery(); return constantBooleanExpr.getValue() ? all : queryBuilder.bool().must(all).not().createQuery(); } @Override public Query visit(ConstantValueExpr constantValueExpr) { throw new IllegalStateException("This node type should not be visited"); } @Override public Query visit(PropertyValueExpr propertyValueExpr) { throw new IllegalStateException("This node type should not be visited"); } @Override public Query visit(AggregationExpr aggregationExpr) { throw new IllegalStateException("This node type should not be visited"); } private <F extends FieldCustomization> F applyFieldBridge(boolean isAnalyzed, PropertyPath<?> propertyPath, F field) { FieldBridge fieldBridge = fieldBridgeAndAnalyzerProvider.getFieldBridge(entityType, propertyPath.asArrayPath()); if (fieldBridge != null) { ((FieldBridgeCustomization) field).withFieldBridge(fieldBridge); } if (!isAnalyzed) { field.ignoreAnalyzer(); } return field; } }