diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/DisjunctNode.java b/src/main/java/redis/clients/jedis/search/querybuilder/DisjunctNode.java new file mode 100644 index 0000000000..d982072416 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/DisjunctNode.java @@ -0,0 +1,24 @@ +package redis.clients.jedis.search.querybuilder; + +/** + * A disjunct node. evaluates to true if any of its children are false. Conversely, this node + * evaluates to false only iff all of its children are true, making it the exact inverse of + * {@link IntersectNode} + * + * In RS, it looks like: + * + * {@code -(@f1:v1 @f2:v2)} + * + * @see DisjunctUnionNode which evalutes to true if all its children are false. + */ +public class DisjunctNode extends IntersectNode { + @Override + public String toString(Parenthesize mode) { + String ret = super.toString(Parenthesize.NEVER); + if (shouldParenthesize(mode)) { + return "-(" + ret + ")"; + } else { + return "-" + ret; + } + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/DisjunctUnionNode.java b/src/main/java/redis/clients/jedis/search/querybuilder/DisjunctUnionNode.java new file mode 100644 index 0000000000..36be78bf31 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/DisjunctUnionNode.java @@ -0,0 +1,17 @@ +package redis.clients.jedis.search.querybuilder; + +/** + * A disjunct union node is the inverse of a {@link UnionNode}. It evaluates to true only iff + * all its children are false. Conversely, it evaluates to false if any of its + * children are true. + * + * As an RS query it looks like {@code -(@f1:v1|@f2:v2)} + * + * @see DisjunctNode which evaluates to true if any of its children are false. + */ +public class DisjunctUnionNode extends DisjunctNode { + @Override + protected String getJoinString() { + return "|"; + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/DoubleRangeValue.java b/src/main/java/redis/clients/jedis/search/querybuilder/DoubleRangeValue.java new file mode 100644 index 0000000000..75fc3843be --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/DoubleRangeValue.java @@ -0,0 +1,38 @@ +package redis.clients.jedis.search.querybuilder; + +/** + * @author mnunberg on 2/23/18. + */ +public class DoubleRangeValue extends RangeValue { + + private final double from; + private final double to; + + private static void appendNum(StringBuilder sb, double n, boolean inclusive) { + if (!inclusive) { + sb.append("("); + } + if (n == Double.NEGATIVE_INFINITY) { + sb.append("-inf"); + } else if (n == Double.POSITIVE_INFINITY) { + sb.append("inf"); + } else { + sb.append(n); + } + } + + public DoubleRangeValue(double from, double to) { + this.from = from; + this.to = to; + } + + @Override + protected void appendFrom(StringBuilder sb, boolean inclusive) { + appendNum(sb, from, inclusive); + } + + @Override + protected void appendTo(StringBuilder sb, boolean inclusive) { + appendNum(sb, to, inclusive); + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/GeoValue.java b/src/main/java/redis/clients/jedis/search/querybuilder/GeoValue.java new file mode 100644 index 0000000000..3c6727b217 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/GeoValue.java @@ -0,0 +1,33 @@ +package redis.clients.jedis.search.querybuilder; + +import java.util.Locale; +import redis.clients.jedis.args.GeoUnit; + +/** + * Created by mnunberg on 2/23/18. + */ +public class GeoValue extends Value { + + private final GeoUnit unit; + private final double lon; + private final double lat; + private final double radius; + + public GeoValue(double lon, double lat, double radius, GeoUnit unit) { + this.lon = lon; + this.lat = lat; + this.radius = radius; + this.unit = unit; + } + + @Override + public String toString() { + return "[" + lon + " " + lat + " " + radius + + " " + unit.name().toLowerCase(Locale.ENGLISH) + "]"; + } + + @Override + public boolean isCombinable() { + return false; + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/IntersectNode.java b/src/main/java/redis/clients/jedis/search/querybuilder/IntersectNode.java new file mode 100644 index 0000000000..979618647f --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/IntersectNode.java @@ -0,0 +1,13 @@ +package redis.clients.jedis.search.querybuilder; + +/** + * The intersection node evaluates to true if any of its children are true. + * + * In RS: {@code @f1:v1 @f2:v2} + */ +public class IntersectNode extends QueryNode { + @Override + protected String getJoinString() { + return " "; + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/LongRangeValue.java b/src/main/java/redis/clients/jedis/search/querybuilder/LongRangeValue.java new file mode 100644 index 0000000000..ffa072aa5a --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/LongRangeValue.java @@ -0,0 +1,40 @@ +package redis.clients.jedis.search.querybuilder; + +public class LongRangeValue extends RangeValue { + + private final long from; + private final long to; + + @Override + public boolean isCombinable() { + return false; + } + + private static void appendNum(StringBuilder sb, long n, boolean inclusive) { + if (!inclusive) { + sb.append("("); + } + if (n == Long.MIN_VALUE) { + sb.append("-inf"); + } else if (n == Long.MAX_VALUE) { + sb.append("inf"); + } else { + sb.append(Long.toString(n)); + } + } + + public LongRangeValue(long from, long to) { + this.from = from; + this.to = to; + } + + @Override + protected void appendFrom(StringBuilder sb, boolean inclusive) { + appendNum(sb, from, inclusive); + } + + @Override + protected void appendTo(StringBuilder sb, boolean inclusive) { + appendNum(sb, to, inclusive); + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/Node.java b/src/main/java/redis/clients/jedis/search/querybuilder/Node.java new file mode 100644 index 0000000000..013ac79b6f --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/Node.java @@ -0,0 +1,50 @@ +package redis.clients.jedis.search.querybuilder; + +import redis.clients.jedis.search.Query; + +/** + * Created by mnunberg on 2/23/18. + * + * Base node interface + */ +public interface Node { + + enum Parenthesize { + + /** + * Always encapsulate + */ + ALWAYS, + + /** + * Never encapsulate. Note that this may be ignored if parentheses are semantically required + * (e.g. {@code @foo:(val1|val2)}. However something like {@code @foo:v1 @bar:v2} need not be + * parenthesized. + */ + + NEVER, + /** + * Determine encapsulation based on number of children. If the node only has one child, it is + * not parenthesized, if it has more than one child, it is parenthesized + */ + + DEFAULT + } + + /** + * Returns the string form of this node. + * + * @param mode Whether the string should be encapsulated in parentheses {@code (...)} + * @return The string query. + */ + String toString(Parenthesize mode); + + /** + * Returns the string form of this node. This may be passed to + * {@link redis.clients.jedis.UnifiedJedis#ftSearch(String, Query)} + * + * @return The query string. + */ + @Override + String toString(); +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/OptionalNode.java b/src/main/java/redis/clients/jedis/search/querybuilder/OptionalNode.java new file mode 100644 index 0000000000..7a9c728a05 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/OptionalNode.java @@ -0,0 +1,22 @@ +package redis.clients.jedis.search.querybuilder; + +/** + * Created by mnunberg on 2/23/18. + * + * The optional node affects scoring and ordering. If it evaluates to true, the result is ranked + * higher. It is helpful to combine it with a {@link UnionNode} to rank a document higher if it + * meets one of several criteria. + * + * In RS: {@code ~(@lang:en @country:us)}. + */ +public class OptionalNode extends IntersectNode { + + @Override + public String toString(Parenthesize mode) { + String ret = super.toString(Parenthesize.NEVER); + if (shouldParenthesize(mode)) { + return "~(" + ret + ")"; + } + return "~" + ret; + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/QueryBuilders.java b/src/main/java/redis/clients/jedis/search/querybuilder/QueryBuilders.java new file mode 100644 index 0000000000..92576b80c5 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/QueryBuilders.java @@ -0,0 +1,155 @@ +package redis.clients.jedis.search.querybuilder; + +import java.util.Arrays; + +import static redis.clients.jedis.search.querybuilder.Values.value; + +/** + * Created by mnunberg on 2/23/18. + * + * This class contains methods to construct query nodes. These query nodes can be added to parent + * query nodes (building a chain) or used as the root query node. + */ +public class QueryBuilders { + private QueryBuilders() { + throw new InstantiationError("Must not instantiate this class"); + } + + /** + * Create a new intersection node with child nodes. An intersection node is true if all its + * children are also true + * + * @param n sub-condition to add + * @return The node + */ + public static QueryNode intersect(Node... n) { + return new IntersectNode().add(n); + } + + /** + * Create a new intersection node with a field-value pair. + * + * @param field The field that should contain this value. If this value is empty, then any field + * will be checked. + * @param values Value to check for. The node will be true only if the field (or any field) + * contains all of the values + * @return The node + */ + public static QueryNode intersect(String field, Value... values) { + return new IntersectNode().add(field, values); + } + + /** + * Helper method to create a new intersection node with a string value. + * + * @param field The field to check. If left null or empty, all fields will be checked. + * @param stringValue The value to check + * @return The node + */ + public static QueryNode intersect(String field, String stringValue) { + return intersect(field, value(stringValue)); + } + + /** + * Create a union node. Union nodes evaluate to true if any of its children are true + * + * @param n Child node + * @return The union node + */ + public static QueryNode union(Node... n) { + return new UnionNode().add(n); + } + + /** + * Create a union node which can match an one or more values + * + * @param field Field to check. If empty, all fields are checked + * @param values Values to search for. The node evaluates to true if {@code field} matches any of + * the values + * @return The union node + */ + public static QueryNode union(String field, Value... values) { + return new UnionNode().add(field, values); + } + + /** + * Convenience method to match one or more strings. This is equivalent to + * {@code union(field, value(v1), value(v2), value(v3)) ...} + * + * @param field Field to match + * @param values Strings to check for + * @return The union node + */ + public static QueryNode union(String field, String... values) { + return union(field, (Value[]) Arrays.stream(values).map(Values::value).toArray()); + } + + /** + * Create a disjunct node. Disjunct nodes are true iff any of its children are not + * true. Conversely, this node evaluates to false if all its children are true. + * + * @param n Child nodes to add + * @return The disjunct node + */ + public static QueryNode disjunct(Node... n) { + return new DisjunctNode().add(n); + } + + /** + * Create a disjunct node using one or more values. The node will evaluate to true iff the field + * does not match any of the values. + * + * @param field Field to check for (empty or null for any field) + * @param values The values to check for + * @return The node + */ + public static QueryNode disjunct(String field, Value... values) { + return new DisjunctNode().add(field, values); + } + + /** + * Create a disjunct node using one or more values. The node will evaluate to true iff the field + * does not match any of the values. + * + * @param field Field to check for (empty or null for any field) + * @param values The values to check for + * @return The node + */ + public static QueryNode disjunct(String field, String... values) { + return disjunct(field, (Value[]) Arrays.stream(values).map(Values::value).toArray()); + } + + /** + * Create a disjunct union node. This node evaluates to true if all of its children are not + * true. Conversely, this node evaluates as false if any of its children are true. + * + * @param n + * @return The node + */ + public static QueryNode disjunctUnion(Node... n) { + return new DisjunctUnionNode().add(n); + } + + public static QueryNode disjunctUnion(String field, Value... values) { + return new DisjunctUnionNode().add(field, values); + } + + public static QueryNode disjunctUnion(String field, String... values) { + return disjunctUnion(field, (Value[]) Arrays.stream(values).map(Values::value).toArray()); + } + + /** + * Create an optional node. Optional nodes do not affect which results are returned but they + * influence ordering and scoring. + * + * @param n The node to evaluate as optional + * @return The new node + */ + public static QueryNode optional(Node... n) { + return new OptionalNode().add(n); + } + + public static QueryNode optional(String field, Value... values) { + return new OptionalNode().add(field, values); + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/QueryNode.java b/src/main/java/redis/clients/jedis/search/querybuilder/QueryNode.java new file mode 100644 index 0000000000..bc64374a5a --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/QueryNode.java @@ -0,0 +1,92 @@ +package redis.clients.jedis.search.querybuilder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.StringJoiner; + +public abstract class QueryNode implements Node { + + private final List children = new ArrayList<>(); + + protected abstract String getJoinString(); + + /** + * Add a match criteria to this node + * + * @param field The field to check. If null or empty, then any field is checked + * @param values Values to check for. + * @return The current node, for chaining. + */ + public QueryNode add(String field, Value... values) { + children.add(new ValueNode(field, getJoinString(), values)); + return this; + } + + /** + * Convenience method to add a list of string values + * + * @param field Field to check for + * @param values One or more string values. + * @return The current node, for chaining. + */ + public QueryNode add(String field, String... values) { + children.add(new ValueNode(field, getJoinString(), values)); + return this; + } + + /** + * Add a list of values from a collection + * + * @param field The field to check + * @param values Collection of values to match + * @return The current node for chaining. + */ + public QueryNode add(String field, Collection values) { + return add(field, values.toArray(new Value[0])); + } + + /** + * Add children nodes to this node. + * + * @param nodes Children nodes to add + * @return The current node, for chaining. + */ + public QueryNode add(Node... nodes) { + children.addAll(Arrays.asList(nodes)); + return this; + } + + protected boolean shouldParenthesize(Parenthesize mode) { + if (mode == Parenthesize.ALWAYS) { + return true; + } + if (mode == Parenthesize.NEVER) { + return false; + } + return children.size() > 1; + } + + @Override + public String toString(Parenthesize parenMode) { + StringBuilder sb = new StringBuilder(); + StringJoiner sj = new StringJoiner(getJoinString()); + if (shouldParenthesize(parenMode)) { + sb.append('('); + } + for (Node n : children) { + sj.add(n.toString(parenMode)); + } + sb.append(sj.toString()); + if (shouldParenthesize(parenMode)) { + sb.append(')'); + } + return sb.toString(); + } + + @Override + public String toString() { + return toString(Parenthesize.DEFAULT); + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/RangeValue.java b/src/main/java/redis/clients/jedis/search/querybuilder/RangeValue.java new file mode 100644 index 0000000000..9d05657c33 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/RangeValue.java @@ -0,0 +1,40 @@ +package redis.clients.jedis.search.querybuilder; + +/** + * @author mnunberg on 2/23/18. + */ +public abstract class RangeValue extends Value { + + private boolean inclusiveMin = true; + private boolean inclusiveMax = true; + + @Override + public boolean isCombinable() { + return false; + } + + protected abstract void appendFrom(StringBuilder sb, boolean inclusive); + + protected abstract void appendTo(StringBuilder sb, boolean inclusive); + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('['); + appendFrom(sb, inclusiveMin); + sb.append(' '); + appendTo(sb, inclusiveMax); + sb.append(']'); + return sb.toString(); + } + + public RangeValue inclusiveMin(boolean val) { + inclusiveMin = val; + return this; + } + + public RangeValue inclusiveMax(boolean val) { + inclusiveMax = val; + return this; + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/UnionNode.java b/src/main/java/redis/clients/jedis/search/querybuilder/UnionNode.java new file mode 100644 index 0000000000..8066df1bc6 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/UnionNode.java @@ -0,0 +1,11 @@ +package redis.clients.jedis.search.querybuilder; + +/** + * Created by mnunberg on 2/23/18. + */ +public class UnionNode extends QueryNode { + @Override + protected String getJoinString() { + return "|"; + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/Value.java b/src/main/java/redis/clients/jedis/search/querybuilder/Value.java new file mode 100644 index 0000000000..54e7755fa9 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/Value.java @@ -0,0 +1,13 @@ +package redis.clients.jedis.search.querybuilder; + +/** + * Created by mnunberg on 2/23/18. + */ +public abstract class Value { + public boolean isCombinable() { + return false; + } + + @Override + public abstract String toString(); +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/ValueNode.java b/src/main/java/redis/clients/jedis/search/querybuilder/ValueNode.java new file mode 100644 index 0000000000..5bcb587001 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/ValueNode.java @@ -0,0 +1,82 @@ +package redis.clients.jedis.search.querybuilder; + +import java.util.StringJoiner; + +/** + * Created by mnunberg on 2/23/18. + */ +public class ValueNode implements Node { + + private final Value[] values; + private final String field; + private final String joinString; + + public ValueNode(String field, String joinstr, Value... values) { + this.field = field; + this.values = values; + this.joinString = joinstr; + } + + private static Value[] fromStrings(String[] values) { + Value[] objs = new Value[values.length]; + for (int i = 0; i < values.length; i++) { + objs[i] = Values.value(values[i]); + } + return objs; + } + + public ValueNode(String field, String joinstr, String... values) { + this(field, joinstr, fromStrings(values)); + } + + private String formatField() { + if (field == null || field.isEmpty()) { + return ""; + } + return '@' + field + ':'; + } + + private String toStringCombinable(Parenthesize mode) { + StringBuilder sb = new StringBuilder(formatField()); + if (values.length > 1 || mode == Parenthesize.ALWAYS) { + sb.append('('); + } + StringJoiner sj = new StringJoiner(joinString); + for (Value v : values) { + sj.add(v.toString()); + } + sb.append(sj.toString()); + if (values.length > 1 || mode == Parenthesize.ALWAYS) { + sb.append(')'); + } + return sb.toString(); + } + + private String toStringDefault(Parenthesize mode) { + boolean useParen = mode == Parenthesize.ALWAYS; + if (!useParen) { + useParen = mode != Parenthesize.NEVER && values.length > 1; + } + StringBuilder sb = new StringBuilder(); + if (useParen) { + sb.append('('); + } + StringJoiner sj = new StringJoiner(joinString); + for (Value v : values) { + sj.add(formatField() + v.toString()); + } + sb.append(sj.toString()); + if (useParen) { + sb.append(')'); + } + return sb.toString(); + } + + @Override + public String toString(Parenthesize mode) { + if (values[0].isCombinable()) { + return toStringCombinable(mode); + } + return toStringDefault(mode); + } +} diff --git a/src/main/java/redis/clients/jedis/search/querybuilder/Values.java b/src/main/java/redis/clients/jedis/search/querybuilder/Values.java new file mode 100644 index 0000000000..67256f2359 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/querybuilder/Values.java @@ -0,0 +1,99 @@ +package redis.clients.jedis.search.querybuilder; + +import redis.clients.jedis.GeoCoordinate; +import redis.clients.jedis.args.GeoUnit; + +import java.util.StringJoiner; + +/** + * Created by mnunberg on 2/23/18. + */ +public class Values { + private Values() { + throw new InstantiationError("Must not instantiate this class"); + } + + private abstract static class ScalableValue extends Value { + @Override + public boolean isCombinable() { + return true; + } + } + + public static Value value(String s) { + return new ScalableValue() { + @Override + public String toString() { + return s; + } + }; + } + + public static GeoValue geo(GeoCoordinate coord, double radius, GeoUnit unit) { + return new GeoValue(coord.getLongitude(), coord.getLatitude(), radius, unit); + } + + public static RangeValue between(double from, double to) { + return new DoubleRangeValue(from, to); + } + + public static RangeValue between(int from, int to) { + return new LongRangeValue(from, to); + } + + public static RangeValue eq(double d) { + return new DoubleRangeValue(d, d); + } + + public static RangeValue eq(int i) { + return new LongRangeValue(i, i); + } + + public static RangeValue lt(double d) { + return new DoubleRangeValue(Double.NEGATIVE_INFINITY, d).inclusiveMax(false); + } + + public static RangeValue lt(int d) { + return new LongRangeValue(Long.MIN_VALUE, d).inclusiveMax(false); + } + + public static RangeValue gt(double d) { + return new DoubleRangeValue(d, Double.POSITIVE_INFINITY).inclusiveMin(false); + } + + public static RangeValue gt(int d) { + return new LongRangeValue(d, Long.MAX_VALUE).inclusiveMin(false); + } + + public static RangeValue le(double d) { + return lt(d).inclusiveMax(true); + } + + public static RangeValue le(int d) { + return lt(d).inclusiveMax(true); + } + + public static RangeValue ge(double d) { + return gt(d).inclusiveMin(true); + } + + public static RangeValue ge(int d) { + return gt(d).inclusiveMin(true); + } + + public static Value tags(String... tags) { + if (tags.length == 0) { + throw new IllegalArgumentException("Must have at least one tag"); + } + StringJoiner sj = new StringJoiner(" | "); + for (String s : tags) { + sj.add(s); + } + return new Value() { + @Override + public String toString() { + return "{" + sj.toString() + "}"; + } + }; + } +} diff --git a/src/test/java/redis/clients/jedis/modules/search/QueryBuilderTest.java b/src/test/java/redis/clients/jedis/modules/search/QueryBuilderTest.java new file mode 100644 index 0000000000..a63b625f50 --- /dev/null +++ b/src/test/java/redis/clients/jedis/modules/search/QueryBuilderTest.java @@ -0,0 +1,109 @@ +package redis.clients.jedis.modules.search; + +import static org.junit.Assert.assertEquals; +import static redis.clients.jedis.search.querybuilder.QueryBuilders.*; +import static redis.clients.jedis.search.querybuilder.Values.*; + +import java.util.Arrays; +import org.junit.Test; + +import redis.clients.jedis.GeoCoordinate; +import redis.clients.jedis.args.GeoUnit; +import redis.clients.jedis.search.querybuilder.Node; +import redis.clients.jedis.search.querybuilder.Value; +import redis.clients.jedis.search.querybuilder.Values; + +/** + * Created by mnunberg on 2/23/18. + */ +public class QueryBuilderTest { + + @Test + public void testTag() { + Value v = tags("foo"); + assertEquals("{foo}", v.toString()); + v = tags("foo", "bar"); + assertEquals("{foo | bar}", v.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyTag() { + tags(); + } + + @Test + public void testRange() { + Value v = between(1, 10); + assertEquals("[1 10]", v.toString()); + v = between(1, 10).inclusiveMax(false); + assertEquals("[1 (10]", v.toString()); + v = between(1, 10).inclusiveMin(false); + assertEquals("[(1 10]", v.toString()); + + v = between(1.0, 10.1); + assertEquals("[1.0 10.1]", v.toString()); + v = between(-1.0, 10.1).inclusiveMax(false); + assertEquals("[-1.0 (10.1]", v.toString()); + v = between(-1.1, 150.61).inclusiveMin(false); + assertEquals("[(-1.1 150.61]", v.toString()); + + // le, gt, etc. + // le, gt, etc. + assertEquals("[42 42]", eq(42).toString()); + assertEquals("[-inf (42]", lt(42).toString()); + assertEquals("[-inf 42]", le(42).toString()); + assertEquals("[(-42 inf]", gt(-42).toString()); + assertEquals("[42 inf]", ge(42).toString()); + + assertEquals("[42.0 42.0]", eq(42.0).toString()); + assertEquals("[-inf (42.0]", lt(42.0).toString()); + assertEquals("[-inf 42.0]", le(42.0).toString()); + assertEquals("[(42.0 inf]", gt(42.0).toString()); + assertEquals("[42.0 inf]", ge(42.0).toString()); + + assertEquals("[(1587058030 inf]", gt(1587058030).toString()); + + // string value + assertEquals("s", value("s").toString()); + + // Geo value + assertEquals("[1.0 2.0 3.0 km]", + geo(new GeoCoordinate(1.0, 2.0), 3.0, GeoUnit.KM).toString()); + } + + @Test + public void testIntersectionBasic() { + Node n = intersect().add("name", "mark"); + assertEquals("@name:mark", n.toString()); + + n = intersect().add("name", "mark", "dvir"); + assertEquals("@name:(mark dvir)", n.toString()); + + n = intersect().add("name", Arrays.asList(Values.value("mark"), Values.value("shay"))); + assertEquals("@name:(mark shay)", n.toString()); + + n = intersect("name", "meir"); + assertEquals("@name:meir", n.toString()); + + n = intersect("name", Values.value("meir"), Values.value("rafi")); + assertEquals("@name:(meir rafi)", n.toString()); + } + + @Test + public void testIntersectionNested() { + Node n = intersect(). + add(union("name", value("mark"), value("dvir"))). + add("time", between(100, 200)). + add(disjunct("created", lt(1000))); + assertEquals("(@name:(mark|dvir) @time:[100 200] -@created:[-inf (1000])", n.toString()); + } + + @Test + public void testOptional() { + Node n = optional("name", tags("foo", "bar")); + assertEquals("~@name:{foo | bar}", n.toString()); + + n = optional(n, n); + assertEquals("~(~@name:{foo | bar} ~@name:{foo | bar})", n.toString()); + } +}