summaryrefslogtreecommitdiffstats
path: root/predicate-search-core/src/main/java/com/yahoo
diff options
context:
space:
mode:
Diffstat (limited to 'predicate-search-core/src/main/java/com/yahoo')
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/BinaryFormat.java167
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/BooleanPredicate.java54
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/Conjunction.java93
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/Disjunction.java93
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureConjunction.java104
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureRange.java190
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureSet.java119
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/Negation.java72
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/Predicate.java79
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateHash.java111
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateOperator.java12
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateValue.java9
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/Predicates.java69
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/RangeEdgePartition.java86
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/RangePartition.java67
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/SimplePredicates.java70
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/document/predicate/package-info.java8
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/PredicateQueryParser.java132
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/SubqueryBitmap.java21
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/AndOrSimplifier.java70
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/BooleanSimplifier.java93
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/ComplexNodeTransformer.java159
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/NotNodeReorderer.java67
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/OrSimplifier.java71
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/PredicateOptions.java87
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/PredicateProcessor.java24
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/package-info.java4
-rw-r--r--predicate-search-core/src/main/java/com/yahoo/search/predicate/package-info.java4
28 files changed, 2135 insertions, 0 deletions
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/BinaryFormat.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/BinaryFormat.java
new file mode 100644
index 00000000000..89dcbc59d99
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/BinaryFormat.java
@@ -0,0 +1,167 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Inspector;
+import com.yahoo.slime.Slime;
+
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class BinaryFormat {
+
+ final static String NODE_TYPE = "type";
+ final static String KEY = "key";
+ final static String SET = "feature_set";
+ final static String RANGE_MIN = "range_min";
+ final static String RANGE_MAX = "range_max";
+ final static String CHILDREN = "children";
+ final static String PARTITIONS = "partitions";
+ final static String EDGE_PARTITIONS = "edge_partitions";
+ final static String HASHED_PARTITIONS = "hashed_partitions";
+ final static String HASHED_EDGE_PARTITIONS = "hashed_edge_partitions";
+ final static String HASH = "hash";
+ final static String PAYLOAD = "payload";
+ final static String LABEL = "label";
+ final static String VALUE = "value";
+ final static String LOWER_BOUND = "lower_bound";
+ final static String UPPER_BOUND = "upper_bound";
+
+ final static int TYPE_CONJUNCTION = 1;
+ final static int TYPE_DISJUNCTION = 2;
+ final static int TYPE_NEGATION = 3;
+ final static int TYPE_FEATURE_SET = 4;
+ final static int TYPE_FEATURE_RANGE = 5;
+ final static int TYPE_TRUE = 6;
+ final static int TYPE_FALSE = 7;
+
+ public static byte[] encode(Predicate predicate) {
+ Objects.requireNonNull(predicate, "predicate");
+ Slime slime = new Slime();
+ encode(predicate, slime.setObject());
+ return com.yahoo.slime.BinaryFormat.encode(slime);
+ }
+
+ public static Predicate decode(byte[] buf) {
+ Objects.requireNonNull(buf, "buf");
+ Slime slime = com.yahoo.slime.BinaryFormat.decode(buf);
+ return decode(slime.get());
+ }
+
+ private static Predicate decode(Inspector in) {
+ switch ((int)in.field(NODE_TYPE).asLong()) {
+ case TYPE_CONJUNCTION:
+ Conjunction conjunction = new Conjunction();
+ in = in.field(CHILDREN);
+ for (int i = 0, len = in.children(); i < len; ++i) {
+ conjunction.addOperand(decode(in.entry(i)));
+ }
+ return conjunction;
+
+ case TYPE_DISJUNCTION:
+ Disjunction disjunction = new Disjunction();
+ in = in.field(CHILDREN);
+ for (int i = 0, len = in.children(); i < len; ++i) {
+ disjunction.addOperand(decode(in.entry(i)));
+ }
+ return disjunction;
+
+ case TYPE_NEGATION:
+ return new Negation(decode(in.field(CHILDREN).entry(0)));
+
+ case TYPE_FEATURE_RANGE:
+ FeatureRange featureRange = new FeatureRange(in.field(KEY).asString());
+ if (in.field(RANGE_MIN).valid()) {
+ featureRange.setFromInclusive(in.field(RANGE_MIN).asLong());
+ }
+ if (in.field(RANGE_MAX).valid()) {
+ featureRange.setToInclusive(in.field(RANGE_MAX).asLong());
+ }
+ Inspector p_in = in.field(PARTITIONS);
+ for (int i = 0, len = p_in.children(); i < len; ++i) {
+ featureRange.addPartition(new RangePartition(p_in.entry(i).asString()));
+ }
+ p_in = in.field(EDGE_PARTITIONS);
+ for (int i = 0, len = p_in.children(); i < len; ++i) {
+ Inspector obj = p_in.entry(i);
+ featureRange.addPartition(new RangeEdgePartition(
+ obj.field(LABEL).asString(), obj.field(VALUE).asLong(),
+ (int)obj.field(LOWER_BOUND).asLong(), (int)obj.field(UPPER_BOUND).asLong()));
+ }
+ return featureRange;
+
+ case TYPE_FEATURE_SET:
+ FeatureSet featureSet = new FeatureSet(in.field(KEY).asString());
+ in = in.field(SET);
+ for (int i = 0, len = in.children(); i < len; ++i) {
+ featureSet.addValue(in.entry(i).asString());
+ }
+ return featureSet;
+
+ case TYPE_TRUE:
+ return new BooleanPredicate(true);
+
+ case TYPE_FALSE:
+ return new BooleanPredicate(false);
+
+ default:
+ throw new UnsupportedOperationException(String.valueOf(in.field(NODE_TYPE).asLong()));
+ }
+ }
+
+ private static void encode(Predicate predicate, Cursor out) {
+ if (predicate instanceof Conjunction) {
+ out.setLong(NODE_TYPE, TYPE_CONJUNCTION);
+ out = out.setArray(CHILDREN);
+ for (Predicate operand : ((Conjunction)predicate).getOperands()) {
+ encode(operand, out.addObject());
+ }
+ } else if (predicate instanceof Disjunction) {
+ out.setLong(NODE_TYPE, TYPE_DISJUNCTION);
+ out = out.setArray(CHILDREN);
+ for (Predicate operand : ((Disjunction)predicate).getOperands()) {
+ encode(operand, out.addObject());
+ }
+ } else if (predicate instanceof FeatureRange) {
+ FeatureRange range = (FeatureRange) predicate;
+ out.setLong(NODE_TYPE, TYPE_FEATURE_RANGE);
+ out.setString(KEY, range.getKey());
+ Long from = range.getFromInclusive();
+ if (from != null) {
+ out.setLong(RANGE_MIN, from);
+ }
+ Long to = range.getToInclusive();
+ if (to != null) {
+ out.setLong(RANGE_MAX, to);
+ }
+ Cursor p_out = out.setArray(HASHED_PARTITIONS);
+ for (RangePartition p : range.getPartitions()) {
+ p_out.addLong(PredicateHash.hash64(p.getLabel()));
+ }
+ p_out = out.setArray(HASHED_EDGE_PARTITIONS);
+ for (RangeEdgePartition p : range.getEdgePartitions()) {
+ Cursor obj = p_out.addObject();
+ obj.setLong(HASH, PredicateHash.hash64(p.getLabel()));
+ obj.setLong(VALUE, p.getValue());
+ obj.setLong(PAYLOAD, p.encodeBounds());
+ }
+ } else if (predicate instanceof FeatureSet) {
+ out.setLong(NODE_TYPE, TYPE_FEATURE_SET);
+ out.setString(KEY, ((FeatureSet)predicate).getKey());
+ out = out.setArray(SET);
+ for (String value : ((FeatureSet)predicate).getValues()) {
+ out.addString(value);
+ }
+ } else if (predicate instanceof Negation) {
+ out.setLong(NODE_TYPE, TYPE_NEGATION);
+ out = out.setArray(CHILDREN);
+ encode(((Negation)predicate).getOperand(), out.addObject());
+ } else if (predicate instanceof BooleanPredicate) {
+ out.setLong(NODE_TYPE, ((BooleanPredicate)predicate).getValue() ? TYPE_TRUE : TYPE_FALSE);
+ } else {
+ throw new UnsupportedOperationException(predicate.getClass().getName());
+ }
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/BooleanPredicate.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/BooleanPredicate.java
new file mode 100644
index 00000000000..507d48bf241
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/BooleanPredicate.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class BooleanPredicate extends PredicateValue {
+
+ private boolean value;
+
+ public BooleanPredicate(boolean value) {
+ this.value = value;
+ }
+
+ public boolean getValue() {
+ return value;
+ }
+
+ public BooleanPredicate setValue(boolean value) {
+ this.value = value;
+ return this;
+ }
+
+ @Override
+ public BooleanPredicate clone() throws CloneNotSupportedException {
+ return (BooleanPredicate)super.clone();
+ }
+
+ @Override
+ public int hashCode() {
+ return value ? Boolean.TRUE.hashCode() : Boolean.FALSE.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof BooleanPredicate)) {
+ return false;
+ }
+ BooleanPredicate rhs = (BooleanPredicate)obj;
+ if (value != rhs.value) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ out.append(value ? "true" : "false");
+ }
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/Conjunction.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Conjunction.java
new file mode 100644
index 00000000000..d917e374b69
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Conjunction.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class Conjunction extends PredicateOperator {
+
+ private List<Predicate> operands;
+
+ public Conjunction(Predicate... operands) {
+ this(Arrays.asList(operands));
+ }
+
+ public Conjunction(List<? extends Predicate> operands) {
+ this.operands = new ArrayList<>(operands);
+ }
+
+ public Conjunction addOperand(Predicate operand) {
+ operands.add(operand);
+ return this;
+ }
+
+ public Conjunction addOperands(Collection<? extends Predicate> operands) {
+ this.operands.addAll(operands);
+ return this;
+ }
+
+ public Conjunction setOperands(Collection<? extends Predicate> operands) {
+ this.operands.clear();
+ this.operands.addAll(operands);
+ return this;
+ }
+
+ @Override
+ public List<Predicate> getOperands() {
+ return operands;
+ }
+
+ @Override
+ public Conjunction clone() throws CloneNotSupportedException {
+ Conjunction obj = (Conjunction)super.clone();
+ obj.operands = new ArrayList<>(operands.size());
+ for (Predicate operand : operands) {
+ obj.operands.add(operand.clone());
+ }
+ return obj;
+ }
+
+ @Override
+ public int hashCode() {
+ return operands.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof Conjunction)) {
+ return false;
+ }
+ Conjunction rhs = (Conjunction)obj;
+ if (!operands.equals(rhs.operands)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ for (Iterator<Predicate> it = operands.iterator(); it.hasNext(); ) {
+ Predicate operand = it.next();
+ if (operand instanceof Disjunction || operand instanceof FeatureConjunction) {
+ out.append('(');
+ operand.appendTo(out);
+ out.append(')');
+ } else {
+ operand.appendTo(out);
+ }
+ if (it.hasNext()) {
+ out.append(" and ");
+ }
+ }
+ }
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/Disjunction.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Disjunction.java
new file mode 100644
index 00000000000..c5fa6ba1f70
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Disjunction.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class Disjunction extends PredicateOperator {
+
+ private List<Predicate> operands;
+
+ public Disjunction(Predicate... operands) {
+ this(Arrays.asList(operands));
+ }
+
+ public Disjunction(List<? extends Predicate> operands) {
+ this.operands = new ArrayList<>(operands);
+ }
+
+ public Disjunction addOperand(Predicate operand) {
+ operands.add(operand);
+ return this;
+ }
+
+ public Disjunction addOperands(Collection<? extends Predicate> operands) {
+ this.operands.addAll(operands);
+ return this;
+ }
+
+ public Disjunction setOperands(Collection<? extends Predicate> operands) {
+ this.operands.clear();
+ this.operands.addAll(operands);
+ return this;
+ }
+
+ @Override
+ public List<Predicate> getOperands() {
+ return operands;
+ }
+
+ @Override
+ public Disjunction clone() throws CloneNotSupportedException {
+ Disjunction obj = (Disjunction)super.clone();
+ obj.operands = new ArrayList<>(operands.size());
+ for (Predicate operand : operands) {
+ obj.operands.add(operand.clone());
+ }
+ return obj;
+ }
+
+ @Override
+ public int hashCode() {
+ return operands.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof Disjunction)) {
+ return false;
+ }
+ Disjunction rhs = (Disjunction)obj;
+ if (!operands.equals(rhs.operands)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ for (Iterator<Predicate> it = operands.iterator(); it.hasNext(); ) {
+ Predicate operand = it.next();
+ if (operand instanceof Conjunction) {
+ out.append('(');
+ operand.appendTo(out);
+ out.append(')');
+ } else {
+ operand.appendTo(out);
+ }
+ if (it.hasNext()) {
+ out.append(" or ");
+ }
+ }
+ }
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureConjunction.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureConjunction.java
new file mode 100644
index 00000000000..ba6bb32e05f
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureConjunction.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A FeatureConjunction is a special type of Conjunction where
+ * all children are either a FeatureSet or a Negation (with an underlying FeatureSet).
+ * The underlying FeatureSets may only have one value.
+ *
+ * @author bjorncs
+ */
+public class FeatureConjunction extends PredicateOperator {
+ private final List<Predicate> operands;
+
+ public FeatureConjunction(List<Predicate> operands) {
+ validateOperands(operands);
+ this.operands = new ArrayList<>(operands);
+ }
+
+ private static void validateOperands(List<Predicate> operands) {
+ if (operands.size() <= 1) {
+ throw new IllegalArgumentException("Number of operands must 2 or more, was: " + operands.size());
+ }
+ if (!operands.stream()
+ .allMatch(FeatureConjunction::isValidFeatureConjunctionOperand)) {
+ throw new IllegalArgumentException(
+ "A FeatureConjunction may only contain instances of Negation and FeatureSet, " +
+ "and a FeatureSet may only have one value.");
+ }
+
+ long uniqueKeys = operands.stream().map(FeatureConjunction::getFeatureSetKey).distinct().count();
+ if (operands.size() > uniqueKeys) {
+ throw new IllegalArgumentException("Each FeatureSet key must have a unique key.");
+ }
+ }
+
+ private static String getFeatureSetKey(Predicate predicate) {
+ if (predicate instanceof FeatureSet) {
+ return ((FeatureSet) predicate).getKey();
+ } else {
+ Negation negation = (Negation) predicate;
+ return ((FeatureSet) negation.getOperand()).getKey();
+ }
+ }
+
+ public static boolean isValidFeatureConjunctionOperand(Predicate operand) {
+ return operand instanceof Negation
+ && ((Negation) operand).getOperand() instanceof FeatureSet
+ && isValidFeatureConjunctionOperand(((Negation) operand).getOperand())
+ || operand instanceof FeatureSet && ((FeatureSet) operand).getValues().size() == 1;
+ }
+
+ @Override
+ public List<Predicate> getOperands() {
+ return operands;
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ for (Iterator<Predicate> it = operands.iterator(); it.hasNext(); ) {
+ Predicate operand = it.next();
+ if (operand instanceof Disjunction) {
+ out.append('(');
+ operand.appendTo(out);
+ out.append(')');
+ } else {
+ operand.appendTo(out);
+ }
+ if (it.hasNext()) {
+ out.append(" conj ");
+ }
+ }
+ }
+
+ @Override
+ public FeatureConjunction clone() throws CloneNotSupportedException {
+ return new FeatureConjunction(operands);
+ }
+
+ @Override
+ public int hashCode() {
+ return operands.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof FeatureConjunction)) {
+ return false;
+ }
+ FeatureConjunction rhs = (FeatureConjunction)obj;
+ if (!operands.equals(rhs.operands)) {
+ return false;
+ }
+ return true;
+ }
+
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureRange.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureRange.java
new file mode 100644
index 00000000000..2ca09ee98f0
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureRange.java
@@ -0,0 +1,190 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class FeatureRange extends PredicateValue {
+
+ private String key;
+ private Long from;
+ private Long to;
+ private List<RangePartition> partitions;
+ private List<RangeEdgePartition> edgePartitions;
+
+ public FeatureRange(String key) {
+ this(key, null, null);
+ }
+
+ public FeatureRange(String key, Long fromInclusive, Long toInclusive) {
+ setKey(key);
+ setFromInclusive(fromInclusive);
+ setToInclusive(toInclusive);
+ partitions = new ArrayList<>();
+ edgePartitions = new ArrayList<>();
+ }
+
+ public FeatureRange setKey(String key) {
+ Objects.requireNonNull(key, "key");
+ this.key = key;
+ return this;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public FeatureRange setFromInclusive(Long from) {
+ if (from != null && to != null && from > to) {
+ throw new IllegalArgumentException("Expected 'from' less than or equal to " + to + ", got " + from + ".");
+ }
+ this.from = from;
+ return this;
+ }
+
+ public Long getFromInclusive() {
+ return from;
+ }
+
+ public FeatureRange setToInclusive(Long to) {
+ if (from != null && to != null && from > to) {
+ throw new IllegalArgumentException("Expected 'to' greater than or equal to " + from + ", got " + to + ".");
+ }
+ this.to = to;
+ return this;
+ }
+
+ public Long getToInclusive() {
+ return to;
+ }
+
+ public void addPartition(RangePartition p) {
+ if (p instanceof RangeEdgePartition) {
+ edgePartitions.add((RangeEdgePartition)p);
+ } else {
+ partitions.add(p);
+ }
+ }
+
+ public List<RangeEdgePartition> getEdgePartitions() {
+ return edgePartitions;
+ }
+
+ public List<RangePartition> getPartitions() {
+ return partitions;
+ }
+
+ public void clearPartitions() {
+ partitions.clear();
+ edgePartitions.clear();
+ }
+
+ @Override
+ public FeatureRange clone() throws CloneNotSupportedException {
+ return (FeatureRange)super.clone();
+ }
+
+ @Override
+ public int hashCode() {
+ return ((key.hashCode() + (from != null ? from.hashCode() : 0)) * 31 + (to != null ? to.hashCode() : 0)) * 31;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof FeatureRange)) {
+ return false;
+ }
+ FeatureRange rhs = (FeatureRange)obj;
+ if (!key.equals(rhs.key)) {
+ return false;
+ }
+ if (!Objects.equals(from, rhs.from)) {
+ return false;
+ }
+ if (!Objects.equals(to, rhs.to)) {
+ return false;
+ }
+ return partitions.equals(rhs.partitions) && edgePartitions.equals(rhs.edgePartitions);
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ appendQuotedTo(key, out);
+ out.append(" in [");
+ if (from != null) {
+ out.append(from);
+ }
+ out.append("..");
+ if (to != null) {
+ out.append(to);
+ }
+ if (!partitions.isEmpty() || !edgePartitions.isEmpty()) {
+ out.append(" (");
+ for (RangeEdgePartition p : edgePartitions) {
+ p.appendTo(out);
+ out.append(',');
+ }
+ for (RangePartition p : partitions) {
+ p.appendTo(out);
+ out.append(',');
+ }
+ out.deleteCharAt(out.length() - 1); // Remove extra ','
+ out.append(")");
+ }
+ out.append("]");
+ }
+
+ public static FeatureRange buildFromMixedIn(String key, List<String> partitions, int arity) {
+ Long fromInclusive = null;
+ Long toInclusive = null;
+ long from = 0;
+ long to = 0;
+ for (String p : partitions) {
+ String[] parts = p.split(",");
+ if (parts.length == 1) {
+ String[] subparts = parts[0].split("=|-");
+ int offset = subparts.length == 3? 0 : 1;
+ if (subparts.length < 3 || subparts.length > 4) {
+ throw new IllegalArgumentException("MIXED_IN range partition must be on the form label=val-val");
+ }
+ from = Long.parseLong(subparts[offset + 1]);
+ to = Long.parseLong(subparts[offset + 2]);
+ if (parts[0].contains("=-")) {
+ long tmp = from;
+ from = -to;
+ to = -tmp;
+ }
+ } else {
+ if (parts.length != 3) {
+ throw new IllegalArgumentException("MIXED_IN range edge partition must be on the form label=val,val,payload");
+ }
+ long value = Long.parseLong(parts[1]);
+ long payload = Long.parseLong(parts[2]);
+ if ((payload & 0xc0000000) == 0x80000000L) {
+ from = value + (payload & 0xffff);
+ to = value + arity - 1;
+ } else if ((payload & 0xc0000000) == 0x40000000L) {
+ from = value;
+ to = value + (payload & 0xffff);
+ } else {
+ from = value + (payload >> 16);
+ to = value + (payload & 0xffff);
+ }
+ }
+ if (fromInclusive == null || fromInclusive > from) {
+ fromInclusive = from;
+ }
+ if (toInclusive == null || toInclusive > to) {
+ toInclusive = to;
+ }
+ }
+ return new FeatureRange(key, fromInclusive, toInclusive);
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureSet.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureSet.java
new file mode 100644
index 00000000000..be560514995
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureSet.java
@@ -0,0 +1,119 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class FeatureSet extends PredicateValue {
+
+ private Set<String> values;
+ private String key;
+
+ public FeatureSet(String key, String... values) {
+ this(key, Arrays.asList(values));
+ }
+
+ public FeatureSet(String key, Collection<String> values) {
+ Objects.requireNonNull(key, "key");
+ if (values == null) {
+ throw new NullPointerException("values");
+ }
+ this.key = key;
+ this.values = new TreeSet<>(values);
+ }
+
+ public FeatureSet setKey(String key) {
+ Objects.requireNonNull(key, "key");
+ this.key = key;
+ return this;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public FeatureSet addValue(String value) {
+ Objects.requireNonNull(value, "value");
+ values.add(value);
+ return this;
+ }
+
+ public FeatureSet addValues(Collection<String> values) {
+ if (values == null) {
+ throw new NullPointerException("values");
+ }
+ this.values.addAll(values);
+ return this;
+ }
+
+ public FeatureSet setValues(Collection<String> values) {
+ if (values == null) {
+ throw new NullPointerException("values");
+ }
+ this.values.clear();
+ this.values.addAll(values);
+ return this;
+ }
+
+ public Set<String> getValues() {
+ return values;
+ }
+
+ @Override
+ public FeatureSet clone() throws CloneNotSupportedException {
+ FeatureSet obj = (FeatureSet)super.clone();
+ obj.values = new TreeSet<>(values);
+ return obj;
+ }
+
+ @Override
+ public int hashCode() {
+ return (key.hashCode() + values.hashCode()) * 31;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof FeatureSet)) {
+ return false;
+ }
+ FeatureSet rhs = (FeatureSet)obj;
+ if (!key.equals(rhs.key)) {
+ return false;
+ }
+ if (!values.equals(rhs.values)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ appendInAsTo("in", out);
+ }
+
+ protected void appendNegatedTo(StringBuilder out) {
+ appendInAsTo("not in", out);
+ }
+
+ private void appendInAsTo(String in, StringBuilder out) {
+ appendQuotedTo(key, out);
+ out.append(' ').append(in).append(" [");
+ for (Iterator<String> it = values.iterator(); it.hasNext(); ) {
+ appendQuotedTo(it.next(), out);
+ if (it.hasNext()) {
+ out.append(", ");
+ }
+ }
+ out.append("]");
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/Negation.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Negation.java
new file mode 100644
index 00000000000..c5a4cb802ee
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Negation.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class Negation extends PredicateOperator {
+
+ private Predicate operand;
+
+ public Negation(Predicate operand) {
+ Objects.requireNonNull(operand, "operand");
+ this.operand = operand;
+ }
+
+ public Negation setOperand(Predicate operand) {
+ Objects.requireNonNull(operand, "operand");
+ this.operand = operand;
+ return this;
+ }
+
+ public Predicate getOperand() {
+ return operand;
+ }
+
+ @Override
+ public List<Predicate> getOperands() {
+ return java.util.Arrays.asList(operand);
+ }
+
+ @Override
+ public Negation clone() throws CloneNotSupportedException {
+ Negation obj = (Negation)super.clone();
+ obj.operand = operand.clone();
+ return obj;
+ }
+
+ @Override
+ public int hashCode() {
+ return operand.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof Negation)) {
+ return false;
+ }
+ Negation rhs = (Negation)obj;
+ if (!operand.equals(rhs.operand)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ if (operand instanceof FeatureSet) {
+ ((FeatureSet)operand).appendNegatedTo(out);
+ } else {
+ out.append("not (");
+ operand.appendTo(out);
+ out.append(')');
+ }
+ }
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/Predicate.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Predicate.java
new file mode 100644
index 00000000000..2446d3e3524
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Predicate.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import com.yahoo.document.predicate.parser.PredicateLexer;
+import com.yahoo.document.predicate.parser.PredicateParser;
+import com.yahoo.text.Ascii;
+import org.antlr.runtime.ANTLRStringStream;
+import org.antlr.runtime.CommonTokenStream;
+import org.antlr.runtime.RecognitionException;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public abstract class Predicate implements Cloneable {
+
+ private final static char QUOTE_CHAR = '\'';
+ private final static Ascii.Encoder ASCII_ENCODER = Ascii.newEncoder(StandardCharsets.UTF_8, QUOTE_CHAR);
+ private final static Ascii.Decoder ASCII_DECODER = Ascii.newDecoder(StandardCharsets.UTF_8);
+
+ @Override
+ public Predicate clone() throws CloneNotSupportedException {
+ return (Predicate)super.clone();
+ }
+
+ @Override
+ public final String toString() {
+ StringBuilder out = new StringBuilder();
+ appendTo(out);
+ return out.toString();
+ }
+
+ protected abstract void appendTo(StringBuilder out);
+
+ protected static void appendQuotedTo(String str, StringBuilder out) {
+ String encoded = asciiEncode(str);
+ if (requiresQuote(encoded)) {
+ out.append(QUOTE_CHAR).append(encoded).append(QUOTE_CHAR);
+ } else {
+ out.append(str);
+ }
+ }
+
+ private static boolean requiresQuote(String str) {
+ for (int i = 0, len = str.length(); i < len; i = str.offsetByCodePoints(i, 1)) {
+ int c = str.codePointAt(i);
+ if (c == Ascii.ESCAPE_CHAR || !Character.isLetterOrDigit(c)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static String asciiEncode(String str) {
+ return ASCII_ENCODER.encode(str);
+ }
+
+ public static String asciiDecode(String str) {
+ return ASCII_DECODER.decode(str);
+ }
+
+ public static Predicate fromBinary(byte[] buf) {
+ return BinaryFormat.decode(buf);
+ }
+
+ public static Predicate fromString(String str) {
+ ANTLRStringStream input = new ANTLRStringStream(str);
+ PredicateLexer lexer = new PredicateLexer(input);
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ PredicateParser parser = new PredicateParser(tokens);
+ try {
+ return parser.predicate();
+ } catch (RecognitionException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateHash.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateHash.java
new file mode 100644
index 00000000000..8c0c9ee590e
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateHash.java
@@ -0,0 +1,111 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+
+import com.yahoo.text.Utf8;
+
+/**
+ * @author <a href="mailto:magnarn@yahoo-inc.com">Magnar Nedland</a>
+ * This hash function must match the corresponding C++ hash function in 'ConvertClass',
+ * currently located at 'rise_platform/src/interface/cpp/api/ConvertClass.h'
+ */
+public class PredicateHash {
+ @SuppressWarnings("fallthrough")
+ public static long hash64(String s) {
+ byte[] bytes = Utf8.toBytes(s);
+ long a=0L, b=0L;
+ long c=0x9e3779b97f4a7c13L;
+ int len = bytes.length;
+ int offset = 0;
+ while (len >= 24) {
+ c += ((0xffL & (long)bytes[offset + 23]) << 56) +
+ ((0xffL & (long)bytes[offset + 22]) << 48) +
+ ((0xffL & (long)bytes[offset + 21]) << 40) +
+ ((0xffL & (long)bytes[offset + 20]) << 32) +
+ ((0xffL & (long)bytes[offset + 19]) << 24) +
+ ((0xffL & (long)bytes[offset + 18]) << 16) +
+ ((0xffL & (long)bytes[offset + 17]) << 8) +
+ ((0xffL & (long)bytes[offset + 16]));
+
+ b += ((0xffL & (long)bytes[offset + 15]) << 56) +
+ ((0xffL & (long)bytes[offset + 14]) << 48) +
+ ((0xffL & (long)bytes[offset + 13]) << 40) +
+ ((0xffL & (long)bytes[offset + 12]) << 32) +
+ ((0xffL & (long)bytes[offset + 11]) << 24) +
+ ((0xffL & (long)bytes[offset + 10]) << 16) +
+ ((0xffL & (long)bytes[offset + 9]) << 8) +
+ ((0xffL & (long)bytes[offset + 8]));
+
+ a += ((0xffL & (long)bytes[offset + 7]) << 56) +
+ ((0xffL & (long)bytes[offset + 6]) << 48) +
+ ((0xffL & (long)bytes[offset + 5]) << 40) +
+ ((0xffL & (long)bytes[offset + 4]) << 32) +
+ ((0xffL & (long)bytes[offset + 3]) << 24) +
+ ((0xffL & (long)bytes[offset + 2]) << 16) +
+ ((0xffL & (long)bytes[offset + 1]) << 8) +
+ ((0xffL & (long)bytes[offset + 0]));
+
+ // Mix. This arithmetic must match the mix below.
+ a -= b; a -= c; a ^= (c>>>43);
+ b -= c; b -= a; b ^= (a<<9);
+ c -= a; c -= b; c ^= (b>>>8);
+ a -= b; a -= c; a ^= (c>>>38);
+ b -= c; b -= a; b ^= (a<<23);
+ c -= a; c -= b; c ^= (b>>>5);
+ a -= b; a -= c; a ^= (c>>>35);
+ b -= c; b -= a; b ^= (a<<49);
+ c -= a; c -= b; c ^= (b>>>11);
+ a -= b; a -= c; a ^= (c>>>12);
+ b -= c; b -= a; b ^= (a<<18);
+ c -= a; c -= b; c ^= (b>>>22);
+ // End mix.
+
+ offset += 24;
+ len -= 24;
+ }
+
+ c += bytes.length;
+ switch (len) {
+ case 23: c+= (0xffL & (long)bytes[offset + 22]) << 56;
+ case 22: c+= (0xffL & (long)bytes[offset + 21]) << 48;
+ case 21: c+= (0xffL & (long)bytes[offset + 20]) << 40;
+ case 20: c+= (0xffL & (long)bytes[offset + 19]) << 32;
+ case 19: c+= (0xffL & (long)bytes[offset + 18]) << 24;
+ case 18: c+= (0xffL & (long)bytes[offset + 17]) << 16;
+ case 17: c+= (0xffL & (long)bytes[offset + 16]) << 8;
+ // The first byte of c is reserved for the length
+ case 16: b+= (0xffL & (long)bytes[offset + 15]) << 56;
+ case 15: b+= (0xffL & (long)bytes[offset + 14]) << 48;
+ case 14: b+= (0xffL & (long)bytes[offset + 13]) << 40;
+ case 13: b+= (0xffL & (long)bytes[offset + 12]) << 32;
+ case 12: b+= (0xffL & (long)bytes[offset + 11]) << 24;
+ case 11: b+= (0xffL & (long)bytes[offset + 10]) << 16;
+ case 10: b+= (0xffL & (long)bytes[offset + 9]) << 8;
+ case 9: b+= (0xffL & (long)bytes[offset + 8]);
+ case 8: a+= (0xffL & (long)bytes[offset + 7]) << 56;
+ case 7: a+= (0xffL & (long)bytes[offset + 6]) << 48;
+ case 6: a+= (0xffL & (long)bytes[offset + 5]) << 40;
+ case 5: a+= (0xffL & (long)bytes[offset + 4]) << 32;
+ case 4: a+= (0xffL & (long)bytes[offset + 3]) << 24;
+ case 3: a+= (0xffL & (long)bytes[offset + 2]) << 16;
+ case 2: a+= (0xffL & (long)bytes[offset + 1]) << 8;
+ case 1: a+= (0xffL & (long)bytes[offset + 0]);
+ }
+ // Mix. This arithmetic must match the mix above.
+ a -= b; a -= c; a ^= (c>>>43);
+ b -= c; b -= a; b ^= (a<<9);
+ c -= a; c -= b; c ^= (b>>>8);
+ a -= b; a -= c; a ^= (c>>>38);
+ b -= c; b -= a; b ^= (a<<23);
+ c -= a; c -= b; c ^= (b>>>5);
+ a -= b; a -= c; a ^= (c>>>35);
+ b -= c; b -= a; b ^= (a<<49);
+ c -= a; c -= b; c ^= (b>>>11);
+ a -= b; a -= c; a ^= (c>>>12);
+ b -= c; b -= a; b ^= (a<<18);
+ c -= a; c -= b; c ^= (b>>>22);
+ // End mix.
+
+ return c;
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateOperator.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateOperator.java
new file mode 100644
index 00000000000..0c0af37a791
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateOperator.java
@@ -0,0 +1,12 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+abstract public class PredicateOperator extends Predicate {
+
+ public abstract List<Predicate> getOperands();
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateValue.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateValue.java
new file mode 100644
index 00000000000..f34a8f4b268
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/PredicateValue.java
@@ -0,0 +1,9 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+abstract class PredicateValue extends Predicate {
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/Predicates.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Predicates.java
new file mode 100644
index 00000000000..7362eb5ba6d
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/Predicates.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class Predicates {
+
+ public static Conjunction and(Predicate... operands) {
+ return new Conjunction(operands);
+ }
+
+ public static Disjunction or(Predicate... operands) {
+ return new Disjunction(operands);
+ }
+
+ public static Negation not(Predicate operand) {
+ return new Negation(operand);
+ }
+
+ public static BooleanPredicate value(boolean value) {
+ return new BooleanPredicate(value);
+ }
+
+ public static FeatureBuilder feature(String key) {
+ return new FeatureBuilder(key);
+ }
+
+ public static class FeatureBuilder {
+
+ private final String key;
+
+ public FeatureBuilder(String key) {
+ this.key = key;
+ }
+
+ public FeatureRange lessThan(long toExclusive) {
+ return new FeatureRange(key, null, toExclusive - 1);
+ }
+
+ public FeatureRange lessThanOrEqualTo(long toInclusive) {
+ return new FeatureRange(key, null, toInclusive);
+ }
+
+ public FeatureRange greaterThan(long fromExclusive) {
+ return new FeatureRange(key, fromExclusive + 1, null);
+ }
+
+ public FeatureRange greaterThanOrEqualTo(long fromInclusive) {
+ return new FeatureRange(key, fromInclusive, null);
+ }
+
+ public FeatureRange inRange(long fromInclusive, long toInclusive) {
+ return new FeatureRange(key, fromInclusive, toInclusive);
+ }
+
+ public Negation notInRange(long fromInclusive, long toInclusive) {
+ return new Negation(new FeatureRange(key, fromInclusive, toInclusive));
+ }
+
+ public FeatureSet inSet(String... values) {
+ return new FeatureSet(key, values);
+ }
+
+ public Negation notInSet(String... values) {
+ return new Negation(new FeatureSet(key, values));
+ }
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/RangeEdgePartition.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/RangeEdgePartition.java
new file mode 100644
index 00000000000..0771f0ec08e
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/RangeEdgePartition.java
@@ -0,0 +1,86 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+/**
+ * @author <a href="mailto:magnarn@yahoo-inc.com">Magnar Nedland</a>
+ */
+public class RangeEdgePartition extends RangePartition {
+ private final long value;
+ private final int lowerBound;
+ private final int upperBound;
+
+ public RangeEdgePartition(String label, long value, int lower, int upper) {
+ super(label);
+ this.value = value;
+ this.lowerBound = lower;
+ this.upperBound = upper;
+ }
+
+ public long getValue() {
+ return value;
+ }
+
+ public int getLowerBound() {
+ return lowerBound;
+ }
+
+ public int getUpperBound() {
+ return upperBound;
+ }
+
+ @Override
+ public RangeEdgePartition clone() throws CloneNotSupportedException {
+ return (RangeEdgePartition)super.clone();
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode() + new Long(value).hashCode() +
+ new Integer(lowerBound).hashCode() + new Integer(upperBound).hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof RangeEdgePartition)) {
+ return false;
+ }
+ RangeEdgePartition other = (RangeEdgePartition)obj;
+ return super.equals(other) &&
+ value == other.value &&
+ lowerBound == other.lowerBound &&
+ upperBound == other.upperBound;
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ super.appendTo(out);
+
+ out.append("+[");
+ if (lowerBound > 0)
+ out.append(lowerBound);
+ out.append("..");
+ if (upperBound >= 0)
+ out.append(upperBound);
+ out.append(']');
+ }
+
+ public long encodeBounds() {
+ if (lowerBound > 0) {
+ if (upperBound >= 0) {
+ return lowerBound << 16 | (upperBound + 1);
+ } else {
+ return lowerBound | 0x80000000L;
+ }
+ } else {
+ if (upperBound >= 0) {
+ return (upperBound + 1) | 0x40000000;
+ } else {
+ return 0x80000000L; // >0
+ }
+ }
+ }
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/RangePartition.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/RangePartition.java
new file mode 100644
index 00000000000..33cdd23c45c
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/RangePartition.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+/**
+ * @author <a href="mailto:magnarn@yahoo-inc.com">Magnar Nedland</a>
+ */
+public class RangePartition extends PredicateValue {
+
+ private String label;
+
+ public RangePartition(String label) {
+ this.label = label;
+ }
+
+ public RangePartition(String key, long fromInclusive, long toInclusive, boolean isNeg) {
+ this(makeLabel(key, fromInclusive, toInclusive, isNeg));
+ }
+
+ private static String makeLabel(String key, long fromInclusive, long toInclusive, boolean isNeg) {
+ if (isNeg) {
+ // special case for toInclusive==long_min: It will print its own hyphen.
+ return key + "=" + (toInclusive==0x8000000000000000L? "" : "-") + toInclusive + "-" + fromInclusive;
+ } else {
+ // special case for toInclusive==long_min: It will print its own hyphen.
+ return key + "=" + fromInclusive + (toInclusive==0x8000000000000000L? "" : "-") + toInclusive;
+ }
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ @Override
+ public RangePartition clone() throws CloneNotSupportedException {
+ return (RangePartition)super.clone();
+ }
+
+ @Override
+ public int hashCode() {
+ return label.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof RangePartition)) {
+ return false;
+ }
+ return label.equals(((RangePartition)obj).getLabel());
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ int i = label.lastIndexOf('=');
+ appendQuotedTo(label.substring(0, i), out);
+ if (out.charAt(out.length() - 1) == '\'') {
+ out.deleteCharAt(out.length() - 1);
+ out.append(label.substring(i));
+ out.append('\'');
+ } else {
+ out.append(label.substring(i));
+ }
+ }
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/SimplePredicates.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/SimplePredicates.java
new file mode 100644
index 00000000000..1b339767109
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/SimplePredicates.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.predicate;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class SimplePredicates {
+
+ public static Predicate newPredicate() {
+ return new Predicate() {
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ out.append("<anon>");
+ }
+
+ };
+ }
+
+ public static Predicate newString(String str) {
+ return new StringNode(str);
+ }
+
+ public static List<Predicate> newStrings(String... arr) {
+ List<Predicate> ret = new ArrayList<>(arr.length);
+ for (String str : arr) {
+ ret.add(newString(str));
+ }
+ return ret;
+ }
+
+ private static class StringNode extends Predicate {
+
+ final String str;
+
+ StringNode(String str) {
+ this.str = str;
+ }
+
+ @Override
+ public int hashCode() {
+ return str.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof StringNode)) {
+ return false;
+ }
+ StringNode rhs = (StringNode)obj;
+ if (!str.equals(rhs.str)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void appendTo(StringBuilder out) {
+ out.append(str);
+ }
+
+ }
+}
+
diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/package-info.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/package-info.java
new file mode 100644
index 00000000000..ef3b6954992
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@com.yahoo.osgi.annotation.ExportPackage
+@com.yahoo.api.annotations.PublicApi
+/*
+ The predicate package is exported by the document module (OSGi).
+ Do not remove unless the intention is to modify the public API of document.
+ */
+package com.yahoo.document.predicate;
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/PredicateQueryParser.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/PredicateQueryParser.java
new file mode 100644
index 00000000000..6f5fac54354
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/PredicateQueryParser.java
@@ -0,0 +1,132 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.predicate;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import org.apache.commons.lang.ArrayUtils;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Parses predicate queries from JSON.
+ *
+ * Input JSON is assumed to have the following format:
+ * {
+ * "features": [
+ * {"k": "key-name", "v":"value", "s":"0xDEADBEEFDEADBEEF"}
+ * ],
+ * "rangeFeatures": [
+ * {"k": "key-name", "v":42, "s":"0xDEADBEEFDEADBEEF"}
+ * ]
+ * }
+ *
+ * @author bjorncs
+ */
+public class PredicateQueryParser {
+ private final JsonFactory factory = new JsonFactory();
+
+ @FunctionalInterface
+ public interface FeatureHandler<V> {
+ void accept(String key, V value, long subqueryBitmap);
+ }
+
+ /**
+ * Parses predicate query from JSON.
+ * @param json JSON input.
+ * @param featureHandler The handler is invoked when a feature is parsed.
+ * @param rangeFeatureHandler The handler is invoked when a range feature is parsed.
+ * @throws IllegalArgumentException If JSON is invalid.
+ */
+ public void parseJsonQuery(
+ String json, FeatureHandler<String> featureHandler, FeatureHandler<Long> rangeFeatureHandler)
+ throws IllegalArgumentException {
+
+ try (JsonParser parser = factory.createParser(json)) {
+ skipToken(parser, JsonToken.START_OBJECT);
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ String fieldName = parser.getCurrentName();
+ switch (fieldName) {
+ case "features":
+ parseFeatures(parser, JsonParser::getText, featureHandler);
+ break;
+ case "rangeFeatures":
+ parseFeatures(parser, JsonParser::getLongValue, rangeFeatureHandler);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid field name: " + fieldName);
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (IOException e) {
+ throw new AssertionError("This should never happen when parsing from a String", e);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(String.format("Parsing query from JSON failed: '%s'", json), e);
+ }
+ }
+
+ private static <V> void parseFeatures(
+ JsonParser parser, ValueParser<V> valueParser, FeatureHandler<V> featureHandler) throws IOException {
+ skipToken(parser, JsonToken.START_ARRAY);
+ while (parser.nextToken() != JsonToken.END_ARRAY) {
+ parseFeature(parser, valueParser, featureHandler);
+ }
+ }
+
+ private static <V> void parseFeature(
+ JsonParser parser, ValueParser<V> valueParser, FeatureHandler<V> featureHandler) throws IOException {
+ String key = null;
+ V value = null;
+ long subqueryBitmap = SubqueryBitmap.DEFAULT_VALUE; // Specifying subquery bitmap is optional.
+
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ String fieldName = parser.getCurrentName();
+ skipToken(parser, JsonToken.VALUE_STRING, JsonToken.VALUE_NUMBER_INT);
+ switch (fieldName) {
+ case "k":
+ key = parser.getText();
+ break;
+ case "v":
+ value = valueParser.parse(parser);
+ break;
+ case "s":
+ subqueryBitmap = fromHexString(parser.getText());
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid field name: " + fieldName);
+ }
+ }
+ if (key == null) {
+ throw new IllegalArgumentException(
+ String.format("Feature key is missing! (%s)", parser.getCurrentLocation()));
+ }
+ if (value == null) {
+ throw new IllegalArgumentException(
+ String.format("Feature value is missing! (%s)", parser.getCurrentLocation()));
+ }
+ featureHandler.accept(key, value, subqueryBitmap);
+ }
+
+ private static void skipToken(JsonParser parser, JsonToken... expected) throws IOException {
+ JsonToken actual = parser.nextToken();
+ if (!ArrayUtils.contains(expected, actual)) {
+ throw new IllegalArgumentException(
+ String.format("Expected a token in %s, got %s (%s).",
+ Arrays.toString(expected), actual, parser.getTokenLocation()));
+ }
+ }
+
+ private static long fromHexString(String subqueryBitmap) {
+ if (!subqueryBitmap.startsWith("0x")) {
+ throw new IllegalArgumentException("Not a valid subquery bitmap ('0x' prefix missing): " + subqueryBitmap);
+ }
+ return Long.parseUnsignedLong(subqueryBitmap.substring(2), 16);
+ }
+
+ @FunctionalInterface
+ private interface ValueParser<V> {
+ V parse(JsonParser parser) throws IOException;
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/SubqueryBitmap.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/SubqueryBitmap.java
new file mode 100644
index 00000000000..84b342f930a
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/SubqueryBitmap.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.predicate;
+
+/**
+ * Constants related to the subquery bitmap in predicate/boolean search.
+ * @author bjorncs
+ */
+public interface SubqueryBitmap {
+
+ /**
+ * A feature annotated with the following bitmap will be added to all (64) subqueries.
+ */
+ long ALL_SUBQUERIES = 0xFFFF_FFFF_FFFF_FFFFl;
+
+ /**
+ * Default subquery bitmap.
+ */
+ long DEFAULT_VALUE = ALL_SUBQUERIES;
+
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/AndOrSimplifier.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/AndOrSimplifier.java
new file mode 100644
index 00000000000..b281f98a94a
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/AndOrSimplifier.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.predicate.optimization;
+
+import com.yahoo.document.predicate.Conjunction;
+import com.yahoo.document.predicate.Disjunction;
+import com.yahoo.document.predicate.Negation;
+import com.yahoo.document.predicate.Predicate;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Simplifies a predicate by:
+ * - Combining child-AND/OR nodes with direct parents of the same type
+ * - Pushing negations down to leaf nodes by using De Morgan's law.
+ *
+ * @author magnarn
+ * @author bjorncs
+ */
+public class AndOrSimplifier implements PredicateProcessor {
+
+ public Predicate simplifySubTree(Predicate predicate, boolean negated) {
+ if (predicate == null) {
+ return null;
+ }
+ if (predicate instanceof Negation) {
+ return simplifySubTree(((Negation) predicate).getOperand(), !negated);
+ } else if (predicate instanceof Conjunction) {
+ List<Predicate> in = ((Conjunction)predicate).getOperands();
+ List<Predicate> out = new ArrayList<>(in.size());
+ for (Predicate operand : in) {
+ operand = simplifySubTree(operand, negated);
+ if (operand instanceof Conjunction) {
+ out.addAll(((Conjunction)operand).getOperands());
+ } else {
+ out.add(operand);
+ }
+ }
+ if (negated) {
+ return new Disjunction(out);
+ }
+ ((Conjunction)predicate).setOperands(out);
+ } else if (predicate instanceof Disjunction) {
+ List<Predicate> in = ((Disjunction)predicate).getOperands();
+ List<Predicate> out = new ArrayList<>(in.size());
+ for (Predicate operand : in) {
+ operand = simplifySubTree(operand, negated);
+ if (operand instanceof Disjunction) {
+ out.addAll(((Disjunction)operand).getOperands());
+ } else {
+ out.add(operand);
+ }
+ }
+ if (negated) {
+ return new Conjunction(out);
+ }
+ ((Disjunction)predicate).setOperands(out);
+ } else {
+ if (negated) {
+ return new Negation(predicate);
+ }
+ }
+ return predicate;
+ }
+
+ @Override
+ public Predicate process(Predicate predicate, PredicateOptions options) {
+ return simplifySubTree(predicate, false);
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/BooleanSimplifier.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/BooleanSimplifier.java
new file mode 100644
index 00000000000..fd1fed8ae15
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/BooleanSimplifier.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.predicate.optimization;
+
+import com.yahoo.document.predicate.BooleanPredicate;
+import com.yahoo.document.predicate.Conjunction;
+import com.yahoo.document.predicate.Disjunction;
+import com.yahoo.document.predicate.Negation;
+import com.yahoo.document.predicate.Predicate;
+import com.yahoo.document.predicate.PredicateOperator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Simplifies a predicate by collapsing TRUE/FALSE-children into their parents.
+ * E.g. if an AND node has a FALSE child, the entire node is replaced by FALSE.
+ * Replaces single-child AND/OR-nodes with the child.
+ *
+ * @author magnarn
+ * @author bjorncs
+ */
+public class BooleanSimplifier implements PredicateProcessor {
+
+ public Predicate simplifySubTree(Predicate predicate) {
+ if (predicate == null) {
+ return null;
+ }
+ if (predicate instanceof Conjunction) {
+ List<Predicate> in = ((PredicateOperator)predicate).getOperands();
+ List<Predicate> out = new ArrayList<>(in.size());
+ for (Predicate operand : in) {
+ operand = simplifySubTree(operand);
+ if (isFalse(operand)) {
+ return new BooleanPredicate(false);
+ } else if (!isTrue(operand)) {
+ out.add(operand);
+ }
+ }
+ if (out.size() == 1) {
+ return out.get(0);
+ } else if (out.size() == 0) {
+ return new BooleanPredicate(true);
+ }
+ ((Conjunction)predicate).setOperands(out);
+ } else if (predicate instanceof Disjunction) {
+ List<Predicate> in = ((PredicateOperator)predicate).getOperands();
+ List<Predicate> out = new ArrayList<>(in.size());
+ for (Predicate operand : in) {
+ operand = simplifySubTree(operand);
+ if (isTrue(operand)) {
+ return new BooleanPredicate(true);
+ } else if (!isFalse(operand)) {
+ out.add(operand);
+ }
+ }
+ if (out.size() == 1) {
+ return out.get(0);
+ } else if (out.size() == 0) {
+ return new BooleanPredicate(false);
+ }
+ ((Disjunction)predicate).setOperands(out);
+ } else if (predicate instanceof Negation) {
+ Predicate operand = ((Negation)predicate).getOperand();
+ operand = simplifySubTree(operand);
+ if (isTrue(operand)) {
+ return new BooleanPredicate(false);
+ } else if (isFalse(operand)) {
+ return new BooleanPredicate(true);
+ }
+ ((Negation) predicate).setOperand(operand);
+ }
+ return predicate;
+ }
+
+ private boolean isFalse(Predicate predicate) {
+ if (predicate instanceof BooleanPredicate) {
+ return !((BooleanPredicate)predicate).getValue();
+ }
+ return false;
+ }
+
+ private boolean isTrue(Predicate predicate) {
+ if (predicate instanceof BooleanPredicate) {
+ return ((BooleanPredicate)predicate).getValue();
+ }
+ return false;
+ }
+
+ @Override
+ public Predicate process(Predicate predicate, PredicateOptions options) {
+ return simplifySubTree(predicate);
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/ComplexNodeTransformer.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/ComplexNodeTransformer.java
new file mode 100644
index 00000000000..4ff5d37f842
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/ComplexNodeTransformer.java
@@ -0,0 +1,159 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.predicate.optimization;
+
+import com.yahoo.document.predicate.FeatureRange;
+import com.yahoo.document.predicate.Predicate;
+import com.yahoo.document.predicate.PredicateOperator;
+import com.yahoo.document.predicate.RangeEdgePartition;
+import com.yahoo.document.predicate.RangePartition;
+
+import static java.lang.Math.abs;
+
+/**
+ * Partitions all the feature ranges according to the arity and bounds
+ * set in the PredicateOptions, and updates the ranges with a set of
+ * partitions and edge partitions.
+ * This is required to be able to store a range in the PredicateIndex.
+ *
+ * @author magnarn
+ * @author bjorncs
+ */
+public class ComplexNodeTransformer implements PredicateProcessor {
+
+ public void processPredicate(Predicate predicate, PredicateOptions options) {
+ if (predicate instanceof PredicateOperator) {
+ for (Predicate p : ((PredicateOperator) predicate).getOperands()) {
+ processPredicate(p, options);
+ }
+ } else if (predicate instanceof FeatureRange) {
+ processFeatureRange((FeatureRange) predicate, options);
+ }
+ }
+
+ private void processFeatureRange(FeatureRange range, PredicateOptions options) {
+ range.clearPartitions();
+ int arity = options.getArity();
+ RangePruner rangePruner = new RangePruner(range, options, arity);
+ long from = rangePruner.getFrom();
+ long to = rangePruner.getTo();
+
+ if (from < 0) {
+ if (to < 0) {
+ // Special case for to==-1. -X-0 means the same as -X-1, but is more efficient.
+ partitionRange(range, (to == -1 ? 0 : -to), -from, arity, true);
+ } else {
+ partitionRange(range, 0, -from, arity, true);
+ partitionRange(range, 0, to, arity, false);
+ }
+ } else {
+ partitionRange(range, from, to, arity, false);
+ }
+ }
+
+ private void partitionRange(FeatureRange range, long from, long to, int arity, boolean isNeg) {
+ int fromRemainder = abs((int) (from % arity)); // from is only negative when using LLONG_MIN.
+ // operate on exclusive upper bound.
+ int toRemainder = ((int) ((to - arity + 1) % arity) + arity) % arity; // avoid overflow of (to + 1)
+ long fromVal = from - fromRemainder;
+ long toVal = to - toRemainder; // use inclusive upper bound here to avoid overflow problems.
+ long fromValDividedByArity = abs(fromVal / arity);
+ if (fromVal - 1 == toVal) { // (toVal + 1) might cause overflow
+ addEdgePartition(range, fromVal, fromRemainder, toRemainder - 1, isNeg);
+ return;
+ } else {
+ if (fromRemainder != 0) {
+ addEdgePartition(range, fromVal, fromRemainder, -1, isNeg);
+ fromValDividedByArity += 1;
+ }
+ if (toRemainder != 0) {
+ addEdgePartition(range, toVal + 1, -1, toRemainder - 1, isNeg);
+ }
+ }
+ makePartitions(range, fromValDividedByArity, abs((toVal - (arity - 1)) / arity) + 1, arity, arity, isNeg);
+ }
+
+ private void addEdgePartition(FeatureRange range, long value, int from, int to, boolean isNeg) {
+ String label;
+ if (value == 0x8000000000000000L) // special case long_min.
+ label = range.getKey() + "=-9223372036854775808";
+ else
+ label = range.getKey() + (isNeg ? "=-" : "=") + Long.toString(value);
+ range.addPartition(new RangeEdgePartition(label, value, from, to));
+ }
+
+ private void makePartitions(FeatureRange range, long fromVal, long toVal, long stepSize, int arity, boolean isNeg) {
+ int fromRemainder = (int) (fromVal % arity);
+ int toRemainder = (int) (toVal % arity);
+ long nextFromVal = fromVal - fromRemainder;
+ long nextToVal = toVal - toRemainder;
+ if (nextFromVal == nextToVal) {
+ addPartitions(range, nextFromVal, stepSize, fromRemainder, toRemainder, isNeg);
+ } else {
+ if (fromRemainder > 0) {
+ addPartitions(range, nextFromVal, stepSize, fromRemainder, arity, isNeg);
+ fromVal = nextFromVal + arity;
+ }
+ addPartitions(range, nextToVal, stepSize, 0, toRemainder, isNeg);
+ makePartitions(range, fromVal / arity, toVal / arity, stepSize * arity, arity, isNeg);
+ }
+ }
+
+ private void addPartitions(FeatureRange range, long part, long partSize, int first, int last, boolean isNeg) {
+ for (int i = first; i < last; ++i) {
+ range.addPartition(new RangePartition(
+ range.getKey(), (part + i) * partSize, (part + i + 1) * partSize - 1, isNeg));
+ }
+ }
+
+ @Override
+ public Predicate process(Predicate predicate, PredicateOptions options) {
+ processPredicate(predicate, options);
+ return predicate;
+ }
+
+ /**
+ * Prunes ranges that lie partially or completely outside the configured upper/lower bounds.
+ */
+ private static class RangePruner {
+ private long from;
+ private long to;
+
+ public RangePruner(FeatureRange range, PredicateOptions options, int arity) {
+ from = range.getFromInclusive() != null ? range.getFromInclusive() : options.getAdjustedLowerBound();
+ to = range.getToInclusive() != null ? range.getToInclusive() : options.getAdjustedUpperBound();
+
+ if (from > options.getUpperBound()) { // Range completely beyond bounds
+ long upperRangeStart = Long.MAX_VALUE - (Long.MAX_VALUE % arity) - arity;
+ if (options.getUpperBound() < upperRangeStart) {
+ from = upperRangeStart;
+ to = upperRangeStart + arity - 1;
+ } else {
+ to = from;
+ }
+ } else if (to < options.getLowerBound()) { // Range completely before bounds
+ long lowerRangeEnd = Long.MIN_VALUE + (arity - (Long.MIN_VALUE % arity));
+ if (options.getLowerBound() > lowerRangeEnd) {
+ from = lowerRangeEnd - arity + 1;
+ to = lowerRangeEnd;
+ } else {
+ from = to;
+ }
+ } else { // Modify if range overlaps bounds
+ if (from < options.getLowerBound()) {
+ from = options.getAdjustedLowerBound();
+ }
+ if (to > options.getUpperBound()) {
+ to = options.getAdjustedUpperBound();
+ }
+ }
+ }
+
+ public long getFrom() {
+ return from;
+ }
+
+ public long getTo() {
+ return to;
+ }
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/NotNodeReorderer.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/NotNodeReorderer.java
new file mode 100644
index 00000000000..bd7339581f4
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/NotNodeReorderer.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.predicate.optimization;
+
+import com.yahoo.document.predicate.Conjunction;
+import com.yahoo.document.predicate.Disjunction;
+import com.yahoo.document.predicate.Negation;
+import com.yahoo.document.predicate.Predicate;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Reorders not nodes to improve the efficiency of the z-star posting list compression.
+ * It puts negative children first in AND-nodes, and last in OR-nodes.
+ *
+ * @author magnarn
+ * @author bjorncs
+ */
+public class NotNodeReorderer implements PredicateProcessor {
+ /**
+ * @return true if the predicate ends in a negation.
+ */
+ public boolean processSubTree(Predicate predicate) {
+ if (predicate == null) {
+ return false;
+ }
+ if (predicate instanceof Negation) {
+ // All negations are for leaf-nodes after AndOrSimplifier has run.
+ return true;
+ } else if (predicate instanceof Conjunction) {
+ List<Predicate> in = ((Conjunction)predicate).getOperands();
+ List<Predicate> out = new ArrayList<>(in.size());
+ List<Predicate> positiveChildren = new ArrayList<>(in.size());
+ for (Predicate operand : in) {
+ if (processSubTree(operand)) {
+ out.add(operand);
+ } else {
+ positiveChildren.add(operand);
+ }
+ }
+ out.addAll(positiveChildren);
+ ((Conjunction)predicate).setOperands(out);
+ return positiveChildren.isEmpty();
+ } else if (predicate instanceof Disjunction) {
+ List<Predicate> in = ((Disjunction)predicate).getOperands();
+ List<Predicate> out = new ArrayList<>(in.size());
+ List<Predicate> negativeChildren = new ArrayList<>(in.size());
+ for (Predicate operand : in) {
+ if (processSubTree(operand)) {
+ negativeChildren.add(operand);
+ } else {
+ out.add(operand);
+ }
+ }
+ out.addAll(negativeChildren);
+ ((Disjunction)predicate).setOperands(out);
+ return !negativeChildren.isEmpty();
+ }
+ return false;
+ }
+
+ @Override
+ public Predicate process(Predicate predicate, PredicateOptions options) {
+ processSubTree(predicate);
+ return predicate;
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/OrSimplifier.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/OrSimplifier.java
new file mode 100644
index 00000000000..768534b0068
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/OrSimplifier.java
@@ -0,0 +1,71 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.predicate.optimization;
+
+import com.yahoo.document.predicate.Conjunction;
+import com.yahoo.document.predicate.Disjunction;
+import com.yahoo.document.predicate.FeatureSet;
+import com.yahoo.document.predicate.Negation;
+import com.yahoo.document.predicate.Predicate;
+
+import java.util.List;
+import java.util.Optional;
+
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.reducing;
+import static java.util.stream.Collectors.toList;
+
+/**
+ * Simplifies Disjunction nodes where all children are of type FeatureSet. All FeatureSet that share the same key
+ * are merged into a single FeatureSet. The Disjunction is removed if all children merges into a single feature set.
+ * @author bjorncs
+ */
+public class OrSimplifier implements PredicateProcessor {
+
+ @Override
+ public Predicate process(Predicate predicate, PredicateOptions options) {
+ return simplifyTree(predicate);
+ }
+
+ public Predicate simplifyTree(Predicate predicate) {
+ if (predicate instanceof Disjunction) {
+ Disjunction disjunction = (Disjunction) predicate;
+ List<Predicate> newChildren =
+ disjunction.getOperands().stream().map(this::simplifyTree).collect(toList());
+ return compressFeatureSets(newChildren);
+ } else if (predicate instanceof Negation) {
+ Negation negation = (Negation) predicate;
+ negation.setOperand(simplifyTree(negation.getOperand()));
+ return negation;
+ } else if (predicate instanceof Conjunction) {
+ Conjunction conjunction = (Conjunction) predicate;
+ List<Predicate> newChildren =
+ conjunction.getOperands().stream().map(this::simplifyTree).collect(toList());
+ conjunction.setOperands(newChildren);
+ return conjunction;
+ } else {
+ return predicate;
+ }
+ }
+
+ // Groups and merges the feature sets based on key.
+ private static Predicate compressFeatureSets(List<Predicate> children) {
+ List<Predicate> newChildren = children.stream().filter(p -> !(p instanceof FeatureSet)).collect(toList());
+ children.stream()
+ .filter(FeatureSet.class::isInstance)
+ .map(FeatureSet.class::cast)
+ .collect(groupingBy(FeatureSet::getKey,
+ reducing((f1, f2) -> {
+ f1.addValues(f2.getValues());
+ return f1;
+ })))
+ .values()
+ .stream()
+ .map(Optional::get)
+ .forEach(newChildren::add);
+ if (newChildren.size() == 1) {
+ return newChildren.get(0);
+ } else {
+ return new Disjunction(newChildren);
+ }
+ }
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/PredicateOptions.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/PredicateOptions.java
new file mode 100644
index 00000000000..134d52dd3ab
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/PredicateOptions.java
@@ -0,0 +1,87 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.predicate.optimization;
+
+import java.util.OptionalLong;
+
+/**
+ * This class contains the configured options for predicate indexes.
+ * The adjusted bounds are extended to the nearest power of the arity (-1)
+ * and are used to generate more efficient indexes.
+ *
+ * @author magnarn
+ * @author bjorncs
+ */
+public class PredicateOptions {
+ public static final long DEFAULT_LOWER_BOUND = 0x8000000000000000L;
+ public static final long DEFAULT_UPPER_BOUND = 0x7fffffffffffffffL;
+
+ private final int arity;
+ private final long lowerBound;
+ private final long upperBound;
+ private OptionalLong adjustedLowerBound;
+ private OptionalLong adjustedUpperBound;
+
+ public PredicateOptions(int arity, Long lowerBound, Long upperBound) {
+ this.arity = arity;
+ this.lowerBound = lowerBound == null? DEFAULT_LOWER_BOUND : lowerBound;
+ this.upperBound = upperBound == null? DEFAULT_UPPER_BOUND : upperBound;
+ this.adjustedLowerBound = OptionalLong.empty();
+ this.adjustedUpperBound = OptionalLong.empty();
+ }
+
+ public PredicateOptions(int arity) {
+ this(arity, DEFAULT_LOWER_BOUND, DEFAULT_UPPER_BOUND);
+ }
+
+ public int getArity() {
+ return arity;
+ }
+
+ public long getLowerBound() {
+ return lowerBound;
+ }
+
+ public long getUpperBound() {
+ return upperBound;
+ }
+
+ public long getAdjustedLowerBound() {
+ if (!adjustedLowerBound.isPresent()) {
+ if (lowerBound == DEFAULT_LOWER_BOUND) {
+ adjustedLowerBound = OptionalLong.of(lowerBound);
+ } else if (lowerBound > 0) {
+ adjustedLowerBound = OptionalLong.of(0L);
+ } else {
+ adjustedLowerBound = OptionalLong.of(-adjustBound(arity, -lowerBound));
+ }
+ }
+ return adjustedLowerBound.getAsLong();
+ }
+
+ public long getAdjustedUpperBound() {
+ if (!adjustedUpperBound.isPresent()) {
+ if (upperBound == DEFAULT_UPPER_BOUND) {
+ adjustedUpperBound = OptionalLong.of(DEFAULT_UPPER_BOUND);
+ } else if (upperBound < 0) {
+ adjustedUpperBound = OptionalLong.of(-1L); // 0 belongs to the positive range.
+ } else {
+ adjustedUpperBound = OptionalLong.of(adjustBound(arity, upperBound));
+ }
+ }
+ return adjustedUpperBound.getAsLong();
+ }
+
+ private static long adjustBound(int arity, long bound) {
+ long adjusted = arity;
+ long value = bound;
+ long max = Long.MAX_VALUE / arity;
+ while ((value/=arity) > 0) {
+ if (adjusted > max) {
+ return bound;
+ }
+ adjusted *= arity;
+ }
+ return adjusted - 1;
+ }
+}
+
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/PredicateProcessor.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/PredicateProcessor.java
new file mode 100644
index 00000000000..f6559bb4044
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/PredicateProcessor.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.predicate.optimization;
+
+import com.yahoo.document.predicate.Predicate;
+
+/**
+ * A predicate processor takes a predicate, processes it and returns the result.
+ * Predicate optimisers typically implement this interface.
+ * Note that the interface does not give any guarantees if the processor will
+ * modify the predicate in-place or return a new instance.
+ *
+ * @author bjorncs
+ */
+@FunctionalInterface
+public interface PredicateProcessor {
+
+ /**
+ * Processes a predicate.
+ *
+ * @return the processed predicate.
+ */
+ Predicate process(Predicate predicate, PredicateOptions options);
+
+}
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/package-info.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/package-info.java
new file mode 100644
index 00000000000..77d8143d137
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/optimization/package-info.java
@@ -0,0 +1,4 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@com.yahoo.osgi.annotation.ExportPackage
+
+package com.yahoo.search.predicate.optimization;
diff --git a/predicate-search-core/src/main/java/com/yahoo/search/predicate/package-info.java b/predicate-search-core/src/main/java/com/yahoo/search/predicate/package-info.java
new file mode 100644
index 00000000000..8f2d929efcd
--- /dev/null
+++ b/predicate-search-core/src/main/java/com/yahoo/search/predicate/package-info.java
@@ -0,0 +1,4 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@com.yahoo.osgi.annotation.ExportPackage
+
+package com.yahoo.search.predicate;