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());
+ }
+}