package org.infinispan.objectfilter.impl.syntax;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Expands a filter expression into a superset of it, using only indexed fields. The expanded expression is computed by
* applying <a href="http://en.wikipedia.org/wiki/Boole%27s_expansion_theorem">Boole's expansion theorem</a> to all
* non-indexed fields and then ignoring the non-indexed fields from the resulting product.
*
* @author anistor@redhat.com
* @since 8.0
*/
public final class BooleShannonExpansion {
/**
* The maximum number of cofactors we allow in the product before giving up.
*/
private final int maxExpansionCofactors;
// todo [anistor] besides indexed vs non-indexed we need to detect occurrences of cross-relationship spurious matches and apply a second in-memory filtering phase
private final IndexedFieldProvider.FieldIndexingMetadata fieldIndexingMetadata;
public BooleShannonExpansion(int maxExpansionCofactors, IndexedFieldProvider.FieldIndexingMetadata fieldIndexingMetadata) {
this.maxExpansionCofactors = maxExpansionCofactors;
this.fieldIndexingMetadata = fieldIndexingMetadata;
}
private class Collector extends ExprVisitor {
private boolean foundIndexed = false;
private final Set<PrimaryPredicateExpr> predicatesToRemove = new LinkedHashSet<>();
@Override
public BooleanExpr visit(FullTextBoostExpr fullTextBoostExpr) {
fullTextBoostExpr.getChild().acceptVisitor(this);
return fullTextBoostExpr;
}
@Override
public BooleanExpr visit(FullTextOccurExpr fullTextOccurExpr) {
fullTextOccurExpr.getChild().acceptVisitor(this);
return fullTextOccurExpr;
}
@Override
public BooleanExpr visit(FullTextTermExpr fullTextTermExpr) {
PropertyValueExpr propertyValueExpr = (PropertyValueExpr) fullTextTermExpr.getChild();
if (fieldIndexingMetadata.isIndexed(propertyValueExpr.getPropertyPath().asArrayPath())) {
foundIndexed = true;
} else {
predicatesToRemove.add(fullTextTermExpr);
}
return fullTextTermExpr;
}
@Override
public BooleanExpr visit(FullTextRegexpExpr fullTextRegexpExpr) {
PropertyValueExpr propertyValueExpr = (PropertyValueExpr) fullTextRegexpExpr.getChild();
if (fieldIndexingMetadata.isIndexed(propertyValueExpr.getPropertyPath().asArrayPath())) {
foundIndexed = true;
} else {
predicatesToRemove.add(fullTextRegexpExpr);
}
return fullTextRegexpExpr;
}
@Override
public BooleanExpr visit(FullTextRangeExpr fullTextRangeExpr) {
PropertyValueExpr propertyValueExpr = (PropertyValueExpr) fullTextRangeExpr.getChild();
if (fieldIndexingMetadata.isIndexed(propertyValueExpr.getPropertyPath().asArrayPath())) {
foundIndexed = true;
} else {
predicatesToRemove.add(fullTextRangeExpr);
}
return fullTextRangeExpr;
}
@Override
public BooleanExpr visit(NotExpr notExpr) {
notExpr.getChild().acceptVisitor(this);
return notExpr;
}
@Override
public BooleanExpr visit(OrExpr orExpr) {
for (BooleanExpr c : orExpr.getChildren()) {
c.acceptVisitor(this);
}
return orExpr;
}
@Override
public BooleanExpr visit(AndExpr andExpr) {
for (BooleanExpr c : andExpr.getChildren()) {
c.acceptVisitor(this);
}
return andExpr;
}
@Override
public BooleanExpr visit(ConstantBooleanExpr constantBooleanExpr) {
return constantBooleanExpr;
}
@Override
public BooleanExpr visit(IsNullExpr isNullExpr) {
PropertyValueExpr propertyValueExpr = (PropertyValueExpr) isNullExpr.getChild();
if (fieldIndexingMetadata.isIndexed(propertyValueExpr.getPropertyPath().asArrayPath())) {
foundIndexed = true;
} else {
predicatesToRemove.add(isNullExpr);
}
return isNullExpr;
}
@Override
public BooleanExpr visit(ComparisonExpr comparisonExpr) {
PropertyValueExpr propertyValueExpr = (PropertyValueExpr) comparisonExpr.getLeftChild();
if (fieldIndexingMetadata.isIndexed(propertyValueExpr.getPropertyPath().asArrayPath())) {
foundIndexed = true;
} else {
predicatesToRemove.add(comparisonExpr);
}
return comparisonExpr;
}
@Override
public BooleanExpr visit(LikeExpr likeExpr) {
PropertyValueExpr propertyValueExpr = (PropertyValueExpr) likeExpr.getChild();
if (fieldIndexingMetadata.isIndexed(propertyValueExpr.getPropertyPath().asArrayPath())) {
foundIndexed = true;
} else {
predicatesToRemove.add(likeExpr);
}
return likeExpr;
}
@Override
public ValueExpr visit(ConstantValueExpr constantValueExpr) {
return constantValueExpr;
}
@Override
public ValueExpr visit(PropertyValueExpr propertyValueExpr) {
return propertyValueExpr;
}
@Override
public ValueExpr visit(AggregationExpr aggregationExpr) {
return aggregationExpr;
}
}
private static class Replacer extends ExprVisitor {
private final PrimaryPredicateExpr toReplace;
private final ConstantBooleanExpr with;
private boolean found = false;
private Replacer(PrimaryPredicateExpr toReplace, ConstantBooleanExpr with) {
this.toReplace = toReplace;
this.with = with;
}
@Override
public BooleanExpr visit(NotExpr notExpr) {
BooleanExpr transformedChild = notExpr.getChild().acceptVisitor(this);
if (transformedChild instanceof ConstantBooleanExpr) {
return ((ConstantBooleanExpr) transformedChild).negate();
}
return new NotExpr(transformedChild);
}
@Override
public BooleanExpr visit(OrExpr orExpr) {
List<BooleanExpr> newChildren = new ArrayList<>(orExpr.getChildren().size());
for (BooleanExpr c : orExpr.getChildren()) {
BooleanExpr e = c.acceptVisitor(this);
if (e instanceof ConstantBooleanExpr) {
if (((ConstantBooleanExpr) e).getValue()) {
return ConstantBooleanExpr.TRUE;
}
} else {
if (e instanceof OrExpr) {
newChildren.addAll(((OrExpr) e).getChildren());
} else {
newChildren.add(e);
}
}
}
PredicateOptimisations.optimizePredicates(newChildren, false);
if (newChildren.size() == 1) {
return newChildren.get(0);
}
return new OrExpr(newChildren);
}
@Override
public BooleanExpr visit(AndExpr andExpr) {
List<BooleanExpr> newChildren = new ArrayList<>(andExpr.getChildren().size());
for (BooleanExpr c : andExpr.getChildren()) {
BooleanExpr e = c.acceptVisitor(this);
if (e instanceof ConstantBooleanExpr) {
if (!((ConstantBooleanExpr) e).getValue()) {
return ConstantBooleanExpr.FALSE;
}
} else {
if (e instanceof AndExpr) {
newChildren.addAll(((AndExpr) e).getChildren());
} else {
newChildren.add(e);
}
}
}
PredicateOptimisations.optimizePredicates(newChildren, true);
if (newChildren.size() == 1) {
return newChildren.get(0);
}
return new AndExpr(newChildren);
}
@Override
public BooleanExpr visit(ConstantBooleanExpr constantBooleanExpr) {
return constantBooleanExpr;
}
@Override
public BooleanExpr visit(IsNullExpr isNullExpr) {
return replacePredicate(isNullExpr);
}
@Override
public BooleanExpr visit(ComparisonExpr comparisonExpr) {
return replacePredicate(comparisonExpr);
}
@Override
public BooleanExpr visit(LikeExpr likeExpr) {
return replacePredicate(likeExpr);
}
@Override
public ValueExpr visit(ConstantValueExpr constantValueExpr) {
return constantValueExpr;
}
@Override
public ValueExpr visit(PropertyValueExpr propertyValueExpr) {
return propertyValueExpr;
}
@Override
public ValueExpr visit(AggregationExpr aggregationExpr) {
return aggregationExpr;
}
private BooleanExpr replacePredicate(PrimaryPredicateExpr primaryPredicateExpr) {
switch (PredicateOptimisations.comparePrimaryPredicates(false, primaryPredicateExpr, false, toReplace)) {
case 0:
found = true;
return with;
case 1:
found = true;
return with.negate();
default:
return primaryPredicateExpr;
}
}
}
/**
* Creates a less restrictive (expanded) query that matches the same objects as the input query plus potentially some
* more.
*
* @param booleanExpr the expression to expand
* @return the expanded query if some of the fields are non-indexed or the input query if all fields are indexed
*/
public BooleanExpr expand(BooleanExpr booleanExpr) {
if (booleanExpr == null || booleanExpr instanceof ConstantBooleanExpr) {
return booleanExpr;
}
Collector collector = new Collector();
booleanExpr.acceptVisitor(collector);
if (!collector.foundIndexed) {
return ConstantBooleanExpr.TRUE;
}
if (!collector.predicatesToRemove.isEmpty()) {
int numCofactors = 1;
for (PrimaryPredicateExpr e : collector.predicatesToRemove) {
Replacer replacer1 = new Replacer(e, ConstantBooleanExpr.TRUE);
BooleanExpr e1 = booleanExpr.acceptVisitor(replacer1);
if (!replacer1.found) {
continue;
}
if (e1 == ConstantBooleanExpr.TRUE) {
return ConstantBooleanExpr.TRUE;
}
Replacer replacer2 = new Replacer(e, ConstantBooleanExpr.FALSE);
BooleanExpr e2 = booleanExpr.acceptVisitor(replacer2);
if (e2 == ConstantBooleanExpr.TRUE) {
return ConstantBooleanExpr.TRUE;
}
if (e1 == ConstantBooleanExpr.FALSE) {
booleanExpr = e2;
} else if (e2 == ConstantBooleanExpr.FALSE) {
booleanExpr = e1;
} else {
numCofactors *= 2;
OrExpr disjunction;
if (e1 instanceof OrExpr) {
disjunction = (OrExpr) e1;
if (e2 instanceof OrExpr) {
disjunction.getChildren().addAll(((OrExpr) e2).getChildren());
} else {
disjunction.getChildren().add(e2);
}
} else if (e2 instanceof OrExpr) {
disjunction = (OrExpr) e2;
disjunction.getChildren().add(e1);
} else {
disjunction = new OrExpr(e1, e2);
}
PredicateOptimisations.optimizePredicates(disjunction.getChildren(), false);
booleanExpr = disjunction;
}
if (numCofactors > maxExpansionCofactors) {
// expansion is too big, it's better to do full scan rather than search the index with a huge and
// complex query that is a disjunction of many predicates so will very likely match everything anyway
return ConstantBooleanExpr.TRUE;
}
}
}
return booleanExpr;
}
}