aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/README.md28
-rw-r--r--client/pom.xml66
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/A.java67
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Aggregator.java22
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Annotation.java29
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/DotProduct.java50
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/EndQuery.java114
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Field.java274
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/FixedQuery.java414
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/G.java46
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Group.java24
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/GroupOperation.java34
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/IGroup.java10
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/IGroupOperation.java10
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/NonEmpty.java46
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Q.java73
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Query.java227
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/QueryChain.java52
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Rank.java50
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Select.java38
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Sources.java64
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/UserInput.java80
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/Wand.java71
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/WeakAnd.java63
-rw-r--r--client/src/main/java/com/yahoo/vespa/client/dsl/WeightedSet.java50
-rw-r--r--client/src/test/groovy/com/yahoo/vespa/client/dsl/QTest.groovy583
-rw-r--r--pom.xml1
27 files changed, 2586 insertions, 0 deletions
diff --git a/client/README.md b/client/README.md
new file mode 100644
index 00000000000..ddea3591e38
--- /dev/null
+++ b/client/README.md
@@ -0,0 +1,28 @@
+# vespa_query_dsl
+This lib is used for composing vespa YQL queries
+
+referece: https://docs.vespa.ai/documentation/reference/query-language-reference.html
+
+# usage
+please refer the unit test:
+
+https://github.com/vespa-engine/vespa/blob/master/client/src/test/groovy/com/yahoo/vespa/client/dsl/QTest.groovy
+
+# todos
+- [ ] support `predicate` (https://docs.vespa.ai/documentation/predicate-fields.html)
+- [ ] support methods for checking positive/negative conditions for specific field
+- [X] support order by annotation
+- [X] support order by
+- [X] support sub operators in contains (sameElement, phrase, near, onear, equiv)
+- [X] support group syntax
+- [X] support `nonEmpty`
+- [X] support `dotProduct`
+- [X] support `weightedSet`
+- [X] support `wand`
+- [X] support `weakAnd`
+- [x] support `userInput`
+- [x] support `rank`
+- [x] support filter annotation
+- [X] unit tests
+- [X] support other annotations
+- [X] handle edge cases (e.g. `Q.b("test").contains("a").build()`)
diff --git a/client/pom.xml b/client/pom.xml
new file mode 100644
index 00000000000..ded8a6a3ae0
--- /dev/null
+++ b/client/pom.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>7-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+
+ <artifactId>client</artifactId>
+ <packaging>jar</packaging>
+ <version>7-SNAPSHOT</version>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>2.8.5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-text</artifactId>
+ <version>1.6</version>
+ </dependency>
+ <dependency>
+ <groupId>org.spockframework</groupId>
+ <artifactId>spock-core</artifactId>
+ <version>1.3-groovy-2.5</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.groovy</groupId>
+ <artifactId>groovy-all</artifactId>
+ <!-- any version of Groovy \>= 1.8.2 should work here -->
+ <version>2.5.6</version>
+ <type>pom</type>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.gmavenplus</groupId>
+ <artifactId>gmavenplus-plugin</artifactId>
+ <version>1.7.0</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>addTestSources</goal>
+ <goal>compileTests</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/A.java b/client/src/main/java/com/yahoo/vespa/client/dsl/A.java
new file mode 100644
index 00000000000..86c722c98fe
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/A.java
@@ -0,0 +1,67 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public final class A {
+
+ private final static Annotation EMPTY = new Annotation();
+
+ static public Annotation empty() {
+ return EMPTY;
+ }
+
+ static public Annotation filter() {
+ return a("filter", true);
+ }
+
+ static public Annotation defaultIndex(String index) {
+ return a("defaultIndex", index);
+ }
+
+ public static Annotation a(String name, Object value) {
+ Map<String, Object> map = new HashMap<>();
+ map.put(name, value);
+ return new Annotation(map);
+ }
+
+ public static Annotation a(Map<String, Object> annotation) {
+ if (annotation.isEmpty()) {
+ return empty();
+ }
+ return new Annotation(annotation);
+ }
+
+ public static Annotation a(String n1, Object v1, String n2, Object v2) {
+ return a(new Object[][]{{n1, v1}, {n2, v2}});
+ }
+
+ public static Annotation a(String n1, Object v1, String n2, Object v2, String n3, Object v3) {
+ return a(new Object[][]{{n1, v1}, {n2, v2}, {n3, v3}});
+ }
+
+ public static Annotation a(String n1, Object v1, String n2, Object v2, String n3, Object v3, String n4, Object v4) {
+ return a(new Object[][]{{n1, v1}, {n2, v2}, {n3, v3}, {n4, v4}});
+ }
+
+ public static Annotation a(String n1, Object v1, String n2, Object v2, String n3, Object v3, String n4, Object v4,
+ String n5, Object v5) {
+ return a(new Object[][]{{n1, v1}, {n2, v2}, {n3, v3}, {n4, v4}, {n5, v5}});
+ }
+
+ public static Annotation a(String n1, Object v1, String n2, Object v2, String n3, Object v3, String n4, Object v4,
+ String n5, Object v5, String n6, Object v6) {
+ return a(new Object[][]{{n1, v1}, {n2, v2}, {n3, v3}, {n4, v4}, {n5, v5}, {n6, v6}});
+ }
+
+ private static Annotation a(Object[][] kvpairs) {
+ return new Annotation(Stream.of(kvpairs).collect(Collectors.toMap(o -> o[0].toString(), o -> o[1])));
+ }
+
+ static boolean hasAnnotation(Annotation annotation) {
+ return annotation != null && !EMPTY.equals(annotation);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Aggregator.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Aggregator.java
new file mode 100644
index 00000000000..dc27c764236
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Aggregator.java
@@ -0,0 +1,22 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+public class Aggregator {
+
+ String type;
+ Object value = "";
+
+ Aggregator(String type) {
+ this.type = type;
+ }
+
+ Aggregator(String type, Object value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s(%s)", type, value);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Annotation.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Annotation.java
new file mode 100644
index 00000000000..bc70f2d366d
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Annotation.java
@@ -0,0 +1,29 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.Collections;
+import java.util.Map;
+
+public class Annotation {
+
+ Map<String, Object> annotations = Collections.emptyMap();
+
+ Annotation() {
+ }
+
+ Annotation(Map<String, Object> annotations) {
+ this.annotations = annotations;
+ }
+
+ public Annotation append(Annotation a) {
+ this.annotations.putAll(a.annotations);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return annotations == null || annotations.isEmpty()
+ ? ""
+ : Q.gson.toJson(annotations);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/DotProduct.java b/client/src/main/java/com/yahoo/vespa/client/dsl/DotProduct.java
new file mode 100644
index 00000000000..b288f9a4afd
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/DotProduct.java
@@ -0,0 +1,50 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.Map;
+
+public class DotProduct extends QueryChain {
+
+ private String fieldName;
+ private Map<String, Integer> weightedSet;
+
+ DotProduct(String fieldName, Map<String, Integer> weightedSet) {
+ this.fieldName = fieldName;
+ this.weightedSet = weightedSet;
+ this.nonEmpty = true;
+ }
+
+ @Override
+ public Select getSelect() {
+ return sources.select;
+ }
+
+ @Override
+ public String toString() {
+ return "dotProduct(" + fieldName + ", " + Q.gson.toJson(weightedSet) + ")";
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/EndQuery.java b/client/src/main/java/com/yahoo/vespa/client/dsl/EndQuery.java
new file mode 100644
index 00000000000..80e8d893a8e
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/EndQuery.java
@@ -0,0 +1,114 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class EndQuery {
+
+ QueryChain queryChain;
+ Map<String, Integer> map = new LinkedHashMap<>();
+ List<Object[]> order = new ArrayList<>();
+ String groupQueryStr;
+
+ EndQuery(QueryChain queryChain) {
+ this.queryChain = queryChain;
+
+ // make sure the order of limit, offset and timeout
+ this.map.put("limit", null);
+ this.map.put("offset", null);
+ this.map.put("timeout", null);
+ }
+
+ EndQuery setTimeout(Integer timeout) {
+ map.put("timeout", timeout);
+ return this;
+ }
+
+ EndQuery setOffset(int offset) {
+ map.put("offset", offset);
+ return this;
+ }
+
+ EndQuery setLimit(int limit) {
+ map.put("limit", limit);
+ return this;
+ }
+
+ public EndQuery offset(int offset) {
+ return this.setOffset(offset);
+ }
+
+ public EndQuery timeout(int timeout) {
+ return this.setTimeout(timeout);
+ }
+
+ public EndQuery limit(int limit) {
+ return this.setLimit(limit);
+ }
+
+ public FixedQuery semicolon() {
+ return new FixedQuery(this);
+ }
+
+ public EndQuery group(Group group) {
+ this.groupQueryStr = group.toString();
+ return this;
+ }
+
+ public EndQuery group(String groupQueryStr) {
+ this.groupQueryStr = groupQueryStr;
+ return this;
+ }
+
+ public EndQuery orderByAsc(Annotation annotation, String fieldName) {
+ order.add(new Object[]{annotation, fieldName, "asc"});
+ return this;
+ }
+
+ public EndQuery orderByAsc(String fieldName) {
+ order.add(new Object[]{A.empty(), fieldName, "asc"});
+ return this;
+ }
+
+ public EndQuery orderByDesc(Annotation annotation, String fieldName) {
+ order.add(new Object[]{annotation, fieldName, "desc"});
+ return this;
+ }
+
+ public EndQuery orderByDesc(String fieldName) {
+ order.add(new Object[]{A.empty(), fieldName, "desc"});
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ String orderStr = order.stream().map(array -> A.empty().equals(array[0])
+ ? String.format("%s %s", array[1], array[2])
+ : String.format("[%s]%s %s", array[0], array[1], array[2]))
+ .collect(Collectors.joining(", "));
+
+ String others = map.entrySet().stream()
+ .filter(entry -> entry.getValue() != null)
+ .map(entry -> entry.getKey() + " " + entry.getValue())
+ .collect(Collectors.joining(" "));
+
+ if (orderStr.isEmpty()) {
+ sb.append(others.isEmpty() ? "" : others);
+ } else if (others.isEmpty()) {
+ sb.append("order by ").append(orderStr);
+ } else {
+ sb.append("order by ").append(orderStr).append(", ").append(others);
+ }
+
+ if (groupQueryStr != null) {
+ sb.append("| ").append(groupQueryStr);
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Field.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Field.java
new file mode 100644
index 00000000000..fc8d0ea7f86
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Field.java
@@ -0,0 +1,274 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import org.apache.commons.text.StringEscapeUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class Field extends QueryChain {
+
+ private String fieldName;
+ private List<Object> values = new ArrayList<>();
+ private Annotation annotation = A.empty();
+ private String relation;
+
+ Field(Sources sources, String fieldName) {
+ this.sources = sources;
+ this.fieldName = fieldName;
+ }
+
+ Field(Query query, String fieldName) {
+ this.query = query;
+ this.fieldName = fieldName;
+ }
+
+ public Query contains(String value) {
+ return contains(A.empty(), value);
+ }
+
+ public Query contains(Annotation annotation, String value) {
+ return common("contains", annotation, value);
+ }
+
+ public Query containsPhrase(String value, String... others) {
+ return common("phrase", annotation, value, others);
+ }
+
+ public Query containsPhrase(List<String> values) {
+ if (values.isEmpty()) {
+ throw new IllegalArgumentException("value of \"contains phrase\" should not be empty");
+ }
+
+ return common("phrase", annotation, values);
+ }
+
+ public Query containsNear(String value, String... others) {
+ return common("near", annotation, value, others);
+ }
+
+ public Query containsNear(List<String> values) {
+ if (values.isEmpty()) {
+ throw new IllegalArgumentException("value of \"contains near\" should not be empty");
+ }
+
+ return common("near", annotation, values);
+ }
+
+ public Query containsNear(Annotation annotation, String value, String... others) {
+ return common("near", annotation, value, others);
+ }
+
+ public Query containsNear(Annotation annotation, List<String> values) {
+ if (values.isEmpty()) {
+ throw new IllegalArgumentException("value of \"contains near\" should not be empty");
+ }
+
+ return common("near", annotation, values);
+ }
+
+ public Query containsOnear(String value, String... others) {
+ return common("onear", annotation, value, others);
+ }
+
+ public Query containsOnear(List<String> values) {
+ if (values.isEmpty()) {
+ throw new IllegalArgumentException("value of \"contains onear\" should not be empty");
+ }
+
+ return common("onear", annotation, values);
+ }
+
+ public Query containsOnear(Annotation annotation, String value, String... others) {
+ return common("onear", annotation, value, others);
+ }
+
+ public Query containsOnear(Annotation annotation, List<String> values) {
+ if (values.isEmpty()) {
+ throw new IllegalArgumentException("value of \"contains onear\" should not be empty");
+ }
+
+ return common("onear", annotation, values);
+ }
+
+ public Query containsSameElement(Query andQuery) {
+ return common("sameElement", annotation, andQuery);
+ }
+
+ public Query containsEquiv(String value, String... others) {
+ return containsEquiv(Stream.concat(Stream.of(value), Stream.of(others)).collect(Collectors.toList()));
+ }
+
+ public Query containsEquiv(List<String> values) {
+ if (values.isEmpty()) {
+ throw new IllegalArgumentException("value of \"contains equiv\" should not be empty");
+ } else if (values.size() == 1) {
+ // Vespa does not support one element equiv syntax, use contains instead
+ return contains(values.get(0));
+ } else {
+ return common("equiv", annotation, values);
+ }
+ }
+
+ public Query containsUri(String value) {
+ return common("uri", annotation, value) ;
+ }
+
+ public Query containsUri(Annotation annotation, String value) {
+ return common("uri", annotation, value) ;
+ }
+
+ public Query matches(String str) {
+ return common("matches", annotation, str);
+ }
+
+ public Query eq(int t) {
+ return common("=", annotation, t);
+ }
+
+ public Query ge(int t) {
+ return common(">=", annotation, t);
+ }
+
+ public Query gt(int t) {
+ return common(">", annotation, t);
+ }
+
+ public Query le(int t) {
+ return common("<=", annotation, t);
+ }
+
+ public Query lt(int t) {
+ return common("<", annotation, t);
+ }
+
+ public Query inRange(int l, int m) {
+ return common("range", annotation, l, new Integer[]{m});
+ }
+
+ public Query eq(long t) {
+ return common("=", annotation, t);
+ }
+
+ public Query ge(long t) {
+ return common(">=", annotation, t);
+ }
+
+ public Query gt(long t) {
+ return common(">", annotation, t);
+ }
+
+ public Query le(long t) {
+ return common("<=", annotation, t);
+ }
+
+ public Query lt(long t) {
+ return common("<", annotation, t);
+ }
+
+ public Query inRange(long l, long m) {
+ return common("range", annotation, l, new Long[]{m});
+ }
+
+
+ public Query isTrue() {
+ return common("=", annotation, true);
+ }
+
+ public Query isFalse() {
+ return common("=", annotation, false);
+ }
+
+ private Query common(String relation, Annotation annotation, Object value) {
+ return common(relation, annotation, value, values.toArray());
+ }
+
+ private Query common(String relation, Annotation annotation, String value) {
+ Object v = "\"" + StringEscapeUtils.escapeJava(value) + "\"";
+ return common(relation, annotation, v, values.toArray());
+ }
+
+ private Query common(String relation, Annotation annotation, List<String> values) {
+ return common(relation, annotation, values.get(0), values.subList(1, values.size()).toArray(new String[0]));
+ }
+
+ private Query common(String relation, Annotation annotation, String value, String[] others) {
+ Object v = "\"" + StringEscapeUtils.escapeJava(value) + "\"";
+ Object[] o = Stream.of(others).map(s -> "\"" + StringEscapeUtils.escapeJava(s) + "\"").toArray();
+ return common(relation, annotation, v, o);
+ }
+
+ private Query common(String relation, Annotation annotation, Object value, Object[] others) {
+ this.annotation = annotation;
+ this.relation = relation;
+ this.values = Stream.concat(Stream.of(value), Stream.of(others)).collect(Collectors.toList());
+ this.nonEmpty = true;
+ return query != null
+ ? query
+ : new Query(sources, this);
+ }
+
+ @Override
+ public String toString() {
+ boolean hasAnnotation = !A.empty().equals(annotation);
+ String valuesStr;
+ switch (relation) {
+ case "range":
+ valuesStr = values.stream()
+ .map(i -> i instanceof Long ? i.toString() + "L" : i.toString())
+ .collect(Collectors.joining(", "));
+
+ return hasAnnotation
+ ? String.format("([%s]range(%s, %s))", annotation, fieldName, valuesStr)
+ : String.format("range(%s, %s)", fieldName, valuesStr);
+ case "near":
+ case "onear":
+ case "phrase":
+ case "equiv":
+ case "uri":
+ valuesStr = values.stream().map(Object::toString).collect(Collectors.joining(", "));
+ return hasAnnotation
+ ? String.format("%s contains ([%s]%s(%s))", fieldName, annotation, relation, valuesStr)
+ : String.format("%s contains %s(%s)", fieldName, relation, valuesStr);
+ case "sameElement":
+ return String.format("%s contains %s(%s)", fieldName, relation,
+ ((Query) values.get(0)).toCommaSeparatedAndQueries());
+ default:
+ Object value = values.get(0);
+ valuesStr = value instanceof Long ? value.toString() + "L" : value.toString();
+ return hasAnnotation
+ ? String.format("%s %s ([%s]%s)", fieldName, relation, annotation, valuesStr)
+ : String.format("%s %s %s", fieldName, relation, valuesStr);
+ }
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName) {
+ return !"andnot".equals(this.op) && this.fieldName.equals(fieldName);
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName, Object value) {
+ return hasPositiveSearchField(fieldName) && valuesContains(value);
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName) {
+ return "andnot".equals(this.op) && this.fieldName.equals(fieldName);
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName, Object value) {
+ return hasNegativeSearchField(fieldName) && valuesContains(value);
+ }
+
+ boolean valuesContains(Object value) {
+ if (value instanceof String) {
+ value = "\"" + value + "\"";
+ }
+ return values.contains(value);
+ }
+
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/FixedQuery.java b/client/src/main/java/com/yahoo/vespa/client/dsl/FixedQuery.java
new file mode 100644
index 00000000000..76c3756abc3
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/FixedQuery.java
@@ -0,0 +1,414 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import com.google.gson.internal.LinkedHashTreeMap;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+public class FixedQuery {
+
+ final EndQuery endQuery;
+ Map<String, String> others = new HashMap<>();
+ Map<String, String> queryMap;
+
+
+ public FixedQuery(EndQuery endQuery) {
+ this.endQuery = endQuery;
+ }
+
+ public FixedQuery hits(int hits) {
+ this.param("hits", hits);
+ return this;
+ }
+
+ public FixedQuery offset(int offset) {
+ this.param("offset", offset);
+ return this;
+ }
+
+ public FixedQuery queryProfile(String queryProfile) {
+ this.param("queryProfile", queryProfile);
+ return this;
+ }
+
+ public FixedQuery groupingSessionCache(boolean enable) {
+ this.param("groupingSessionCache", enable);
+ return this;
+ }
+
+ public FixedQuery searchChain(String searchChain) {
+ this.param("searchChain", searchChain);
+ return this;
+ }
+
+ public FixedQuery timeout(int second) {
+ this.param("timeout", second);
+ return this;
+ }
+
+ public FixedQuery timeoutInMs(int milli) {
+ this.param("timeout", milli + "ms");
+ return this;
+ }
+
+ public FixedQuery tracelevel(int level) {
+ this.param("tracelevel", level);
+ return this;
+ }
+
+ public FixedQuery traceTimestamps(boolean enable) {
+ this.param("trace.timestamps", enable);
+ return this;
+ }
+
+ public FixedQuery defaultIndex(String indexName) {
+ this.param("default-index", indexName);
+ return this;
+ }
+
+ public FixedQuery encoding(String encoding) {
+ this.param("encoding", encoding);
+ return this;
+ }
+
+ public FixedQuery filter(String filter) {
+ this.param("filter", filter);
+ return this;
+ }
+
+ public FixedQuery locale(String locale) {
+ this.param("locale", locale);
+ return this;
+ }
+
+ public FixedQuery language(String language) {
+ this.param("language", language);
+ return this;
+ }
+
+ @Deprecated
+ public FixedQuery query(String query) {
+ this.param("query", query);
+ return this;
+ }
+
+ public FixedQuery restrict(String commaDelimitedDocTypeNames) {
+ this.param("restrict", commaDelimitedDocTypeNames);
+ return this;
+ }
+
+ public FixedQuery path(String searchPath) {
+ this.param("path", searchPath);
+ return this;
+ }
+
+ public FixedQuery sources(String commaDelimitedSourceNames) {
+ this.param("sources", commaDelimitedSourceNames);
+ return this;
+ }
+
+ public FixedQuery type(String type) {
+ // web, all, any, phrase, yql, adv (deprecated)
+ this.param("type", type);
+ return this;
+ }
+
+ public FixedQuery location(String location) {
+ this.param("location", location);
+ return this;
+ }
+
+ public FixedQuery rankfeature(String featureName, String featureValue) {
+ this.param("rankfeature." + featureName, featureValue);
+ return this;
+ }
+
+ public FixedQuery rankfeatures(boolean enable) {
+ this.param("rankfeatures", enable);
+ return this;
+ }
+
+ public FixedQuery ranking(String rankProfileName) {
+ this.param("ranking", rankProfileName);
+ return this;
+ }
+
+ public FixedQuery rankproperty(String propertyName, String propertyValue) {
+ this.param("rankproperty." + propertyName, propertyValue);
+ return this;
+ }
+
+ public FixedQuery rankingSoftTimeout(boolean enable) {
+ this.param("ranking.softtimeout.enable", enable);
+ return this;
+ }
+
+ public FixedQuery rankingSoftTimeout(boolean enable, double factor) {
+ this.param("ranking.softtimeout.enable", enable);
+ this.param("ranking.softtimeout.factor", factor);
+ return this;
+ }
+
+ public FixedQuery sorting(String sorting) {
+ this.param("sorting", sorting);
+ return this;
+ }
+
+ public FixedQuery rankingFreshness(String freshness) {
+ this.param("ranking.freshness", freshness);
+ return this;
+ }
+
+ public FixedQuery rankingQueryCache(boolean enable) {
+ this.param("ranking.queryCache", enable);
+ return this;
+ }
+
+ public FixedQuery bolding(boolean enable) {
+ this.param("bolding", enable);
+ return this;
+ }
+
+ public FixedQuery format(String format) {
+ this.param("format", format);
+ return this;
+ }
+
+ public FixedQuery summary(String summaryClass) {
+ this.param("summary", summaryClass);
+ return this;
+ }
+
+ public FixedQuery presentationTemplate(String template) {
+ this.param("presentation.template", template);
+ return this;
+ }
+
+ public FixedQuery presentationTiming(boolean enable) {
+ this.param("presentation.timing", enable);
+ return this;
+ }
+
+ public FixedQuery select(String groupSyntax) {
+ this.param("select", groupSyntax);
+ return this;
+ }
+
+ public FixedQuery select(Group group) {
+ this.param("select", group.toString());
+ return this;
+ }
+
+ public FixedQuery collapseField(String summaryFieldName) {
+ this.param("collapsefield", summaryFieldName);
+ return this;
+ }
+
+ public FixedQuery collapseSummary(String summaryClass) {
+ this.param("collapse.summary", summaryClass);
+ return this;
+ }
+
+ public FixedQuery collapseSize(int size) {
+ this.param("collapsesize", size);
+ return this;
+ }
+
+ public FixedQuery posLatLong(String vespaLatLong) {
+ this.param("pos.ll", vespaLatLong);
+ return this;
+ }
+
+ public FixedQuery posLatLong(double lat, double lon) {
+ String latlong = toVespaLatLong(lat, lon);
+ return posLatLong(latlong);
+ }
+
+ private String toVespaLatLong(double lat, double lon) {
+ double absLat = Math.abs(lat);
+ double absLon = Math.abs(lon);
+ if (absLat > 90 || absLon > 180) {
+ throw new IllegalArgumentException(String.format("invalid lat long value, lat=%f, long=%f", lat, lon));
+ }
+
+ return String.format("%s%f;%s%f",
+ lat > 0 ? "N" : "S", absLat,
+ lon > 0 ? "E" : "W", absLon);
+ }
+
+ public FixedQuery posRadiusInKilometer(int km) {
+ this.param("pos.radius", km + "km");
+ return this;
+ }
+
+ public FixedQuery posRadiusInMeter(int m) {
+ this.param("pos.radius", m + "m");
+ return this;
+ }
+
+ public FixedQuery posRadiusInMile(int mi) {
+ this.param("pos.radius", mi + "mi");
+ return this;
+ }
+
+ public FixedQuery posBoundingBox(double n, double s, double e, double w) {
+ this.param("pos.bb", String.format("n=%f,s=%f,e=%f,w=%f", n, s, e, w));
+ return this;
+ }
+
+ public FixedQuery streamingUserId(BigDecimal id) {
+ this.param("streaming.userid", id);
+ return this;
+ }
+
+ public FixedQuery streamingGroupName(String groupName) {
+ this.param("streaming.groupname", groupName);
+ return this;
+ }
+
+ public FixedQuery streamingSelection(String selection) {
+ this.param("streaming.selection", selection);
+ return this;
+ }
+
+ public FixedQuery streamingPriority(String priority) {
+ this.param("streaming.priority", priority);
+ return this;
+ }
+
+ public FixedQuery streamingMaxBucketsPerVisitor(int max) {
+ this.param("streaming.maxbucketspervisitor", max);
+ return this;
+ }
+
+ public FixedQuery rulesOff(boolean bool) {
+ this.param("rules.off", bool);
+ return this;
+ }
+
+ public FixedQuery rulesRulebase(String rulebase) {
+ this.param("rules.rulebase", rulebase);
+ return this;
+ }
+
+ public FixedQuery recall(String recall) {
+ this.param("recall", recall);
+ return this;
+ }
+
+ public FixedQuery user(String user) {
+ this.param("user", user);
+ return this;
+ }
+
+ public FixedQuery hitCountEstimate(boolean enable) {
+ this.param("hitcountestimate", enable);
+ return this;
+ }
+
+ public FixedQuery metricsIgnore(boolean bool) {
+ this.param("metrics.ignore", bool);
+ return this;
+ }
+
+ public FixedQuery param(String key, String value) {
+ others.put(key, value);
+ return this;
+ }
+
+ private FixedQuery param(String key, Object value) {
+ this.param(key, value.toString());
+ return this;
+ }
+
+ public FixedQuery params(Map<String, String> params) {
+ others.putAll(params);
+ return this;
+ }
+
+ public Map<String, String> buildQueryMap() {
+ if (queryMap != null) {
+ return queryMap;
+ }
+
+ assignIndex();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("select ")
+ .append(endQuery.queryChain.getSelect())
+ .append(" from ")
+ .append(endQuery.queryChain.getSources())
+ .append(" where ")
+ .append(endQuery.queryChain);
+
+ if (!"".equals(endQuery.toString())) {
+ sb.append(' ').append(endQuery);
+ }
+ sb.append(";");
+
+ queryMap = new LinkedHashTreeMap<>(); // for the order
+ queryMap.put("yql", sb.toString());
+ queryMap.putAll(others);
+ queryMap.putAll(getUserInputs());
+ return queryMap;
+ }
+
+
+ public String build() {
+ return buildQueryMap().entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue())
+ .collect(Collectors.joining("&"));
+ }
+
+ private void assignIndex() {
+ assignIndex(endQuery.queryChain.getQuery(), new AtomicInteger());
+ }
+
+ private void assignIndex(QueryChain q, AtomicInteger ai) {
+ q.setIndex(ai.incrementAndGet());
+ if (q instanceof Query) {
+ assignIndex((Query) q, ai);
+ }
+ }
+
+ private void assignIndex(Query q, AtomicInteger ai) {
+ q.queries.stream()
+ .filter(QueryChain::nonEmpty)
+ .forEach(qu -> assignIndex(qu, ai));
+ }
+
+ private Map<String, String> getUserInputs() {
+ return getUserInputs(endQuery.queryChain.getQuery());
+ }
+
+ private Map<String, String> getUserInputs(Query q) {
+ Map<String, String> param = new HashMap<>();
+ q.queries.forEach(qu -> {
+ if (qu instanceof UserInput) {
+ param.putAll(((UserInput) qu).getParam());
+ } else if (qu instanceof Query) {
+ param.putAll(getUserInputs((Query) qu));
+ }
+ });
+ return param;
+ }
+
+ public boolean hasPositiveSearchField(String fieldName) {
+ return endQuery.queryChain.hasPositiveSearchField(fieldName);
+ }
+
+ public boolean hasPositiveSearchField(String fieldName, Object value) {
+ return endQuery.queryChain.hasPositiveSearchField(fieldName, value);
+ }
+
+ public boolean hasNegativeSearchField(String fieldName) {
+ return endQuery.queryChain.hasNegativeSearchField(fieldName);
+ }
+
+ public boolean hasNegativeSearchField(String fieldName, Object value) {
+ return endQuery.queryChain.hasNegativeSearchField(fieldName, value);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/G.java b/client/src/main/java/com/yahoo/vespa/client/dsl/G.java
new file mode 100644
index 00000000000..23499dfb7e6
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/G.java
@@ -0,0 +1,46 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+
+public final class G {
+
+ public static Group all(IGroupOperation... ops) {
+ return new Group("all", ops);
+ }
+
+ public static Group each(IGroupOperation... ops) {
+ return new Group("each", ops);
+ }
+
+ public static GroupOperation group(String expr) {
+ return new GroupOperation("group", expr);
+ }
+
+ public static GroupOperation maxRtn(int max) {
+ return new GroupOperation("max", max);
+ }
+
+ public static GroupOperation order(String expr) {
+ return new GroupOperation("order", expr);
+ }
+
+ public static GroupOperation output(Aggregator... aggrs) {
+ return new GroupOperation("output", aggrs);
+ }
+
+ public static Aggregator max(int max) {
+ return new Aggregator("max", max);
+ }
+
+ public static Aggregator summary() {
+ return new Aggregator("summary");
+ }
+
+ public static Aggregator count() {
+ return new Aggregator("count");
+ }
+
+ public static Aggregator summary(String summaryClass) {
+ return new Aggregator("summary", summaryClass);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Group.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Group.java
new file mode 100644
index 00000000000..db27e7bc2ee
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Group.java
@@ -0,0 +1,24 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class Group implements IGroup, IGroupOperation {
+
+ String type;
+ IGroupOperation[] operations;
+
+ Group(String type, IGroupOperation[] operations) {
+ this.type = type;
+ this.operations = operations;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s(%s)",
+ type,
+ Stream.of(operations).map(Objects::toString).collect(Collectors.joining(" ")));
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/GroupOperation.java b/client/src/main/java/com/yahoo/vespa/client/dsl/GroupOperation.java
new file mode 100644
index 00000000000..a40b304f5da
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/GroupOperation.java
@@ -0,0 +1,34 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class GroupOperation implements IGroupOperation {
+
+ String type;
+ Object value;
+ Aggregator[] aggregators;
+
+ public GroupOperation(String type, Object value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ public GroupOperation(String type, Aggregator[] aggregators) {
+ this.type = type;
+ this.aggregators = aggregators;
+ }
+
+ @Override
+ public String toString() {
+ if (value != null) {
+ return String.format("%s(%s)", type, value);
+ }
+
+ return String.format("%s(%s)",
+ type,
+ Stream.of(aggregators).map(Objects::toString).collect(Collectors.joining(" ")));
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/IGroup.java b/client/src/main/java/com/yahoo/vespa/client/dsl/IGroup.java
new file mode 100644
index 00000000000..0b3a09914bc
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/IGroup.java
@@ -0,0 +1,10 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+/*
+ * interface for group syntax
+ */
+
+public interface IGroup {
+
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/IGroupOperation.java b/client/src/main/java/com/yahoo/vespa/client/dsl/IGroupOperation.java
new file mode 100644
index 00000000000..d60e76fd3bd
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/IGroupOperation.java
@@ -0,0 +1,10 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+/*
+ * interface for group operation
+ */
+
+public interface IGroupOperation {
+
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/NonEmpty.java b/client/src/main/java/com/yahoo/vespa/client/dsl/NonEmpty.java
new file mode 100644
index 00000000000..385797943a5
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/NonEmpty.java
@@ -0,0 +1,46 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+public class NonEmpty extends QueryChain {
+
+ private Query query;
+
+ NonEmpty(Query query) {
+ this.query = query;
+ this.nonEmpty = true;
+ }
+
+ @Override
+ public Select getSelect() {
+ return sources.select;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("nonEmpty(%s)", query);
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Q.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Q.java
new file mode 100644
index 00000000000..f6439302f6b
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Q.java
@@ -0,0 +1,73 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import com.google.gson.Gson;
+
+import java.util.List;
+import java.util.Map;
+
+public final class Q {
+
+ static Gson gson = new Gson();
+ private static Sources SELECT_ALL_FROM_SOURCES_ALL = new Sources(new Select("*"), "*");
+
+ public static Select select(String fieldName) {
+ return new Select(fieldName);
+ }
+
+ public static Select select(String fieldName, String... others) {
+ return new Select(fieldName, others);
+ }
+
+ public static Field p(String fieldName) {
+ return SELECT_ALL_FROM_SOURCES_ALL.where(fieldName);
+ }
+
+ public static Query p(QueryChain query) {
+ return new Query(SELECT_ALL_FROM_SOURCES_ALL, query);
+ }
+
+ public static Query p() {
+ return new Query(SELECT_ALL_FROM_SOURCES_ALL);
+ }
+
+ public static Rank rank(Query query, Query... ranks) {
+ return new Rank(query, ranks);
+ }
+
+ public static UserInput ui(String value) {
+ return new UserInput(value);
+ }
+
+ public static UserInput ui(Annotation a, String value) {
+ return new UserInput(a, value);
+ }
+
+ public static UserInput ui(String index, String value) {
+ return ui(A.defaultIndex(index), value);
+ }
+
+ public static DotProduct dotPdt(String field, Map<String, Integer> weightedSet) {
+ return new DotProduct(field, weightedSet);
+ }
+
+ public static WeightedSet wtdSet(String field, Map<String, Integer> weightedSet) {
+ return new WeightedSet(field, weightedSet);
+ }
+
+ public static NonEmpty nonEmpty(Query query) {
+ return new NonEmpty(query);
+ }
+
+ public static Wand wand(String field, Map<String, Integer> weightedSet) {
+ return new Wand(field, weightedSet);
+ }
+
+ public static Wand wand(String field, List<List<Integer>> numericRange) {
+ return new Wand(field, numericRange);
+ }
+
+ public static WeakAnd weakand(String field, Query query) {
+ return new WeakAnd(field, query);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Query.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Query.java
new file mode 100644
index 00000000000..1b220a936cd
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Query.java
@@ -0,0 +1,227 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class Query extends QueryChain {
+
+ Annotation annotation;
+ Sources sources;
+
+ List<QueryChain> queries = new ArrayList<>();
+
+ Query(Sources sources, QueryChain queryChain) {
+ this.sources = sources;
+ queries.add(queryChain);
+ nonEmpty = queryChain.nonEmpty;
+ }
+
+ Query(Sources sources) {
+ this.sources = sources;
+ }
+
+ String toCommaSeparatedAndQueries() {
+ return queries.stream()
+ .filter(qc -> "and".equals(qc.getOp()))
+ .map(Objects::toString)
+ .collect(Collectors.joining(", "));
+ }
+
+ @Override
+ public String toString() {
+ // TODO: need to refactor
+ if (!nonEmpty) {
+ return "";
+ }
+
+ boolean hasAnnotation = A.hasAnnotation(annotation);
+
+ StringBuilder sb = new StringBuilder();
+
+ if (hasAnnotation) {
+ sb.append("([").append(annotation).append("](");
+ }
+
+ boolean firstQuery = true;
+ for (int i = 0; i < queries.size(); i++) {
+ QueryChain qc = queries.get(i);
+ if (!qc.nonEmpty) {
+ continue;
+ }
+
+ boolean isNotAnd = "andnot".equals(qc.getOp());
+
+ if (!firstQuery) {
+ sb.append(" ");
+ if (isNotAnd) {
+ sb.append("and !");
+ } else {
+ sb.append(qc.getOp()).append(' ');
+ }
+ } else {
+ firstQuery = false;
+ }
+
+ boolean appendBrackets =
+ (qc instanceof Query && ((Query) qc).queries.size() > 1 && !A.hasAnnotation(((Query) qc).annotation))
+ || isNotAnd;
+ if (appendBrackets) {
+ sb.append("(");
+ }
+
+ sb.append(qc);
+
+ if (appendBrackets) {
+ sb.append(")");
+ }
+
+ }
+
+ if (hasAnnotation) {
+ sb.append("))");
+ }
+ return sb.toString().trim();
+ }
+
+ public Field and(String fieldName) {
+ Field f = new Field(this, fieldName);
+ f.setOp("and");
+ queries.add(f);
+ nonEmpty = true;
+ return f;
+ }
+
+ public Field andnot(String fieldName) {
+ Field f = new Field(this, fieldName);
+ f.setOp("andnot");
+ queries.add(f);
+ nonEmpty = true;
+ return f;
+ }
+
+ public Field or(String fieldName) {
+ Field f = new Field(this, fieldName);
+ f.setOp("or");
+ queries.add(f);
+ nonEmpty = true;
+ return f;
+ }
+
+ public Query and(QueryChain query) {
+ query.setOp("and");
+ queries.add(query);
+ nonEmpty = nonEmpty || query.nonEmpty;
+ return this;
+ }
+
+ public Query andnot(QueryChain query) {
+ query.setOp("andnot");
+ queries.add(query);
+ nonEmpty = nonEmpty || query.nonEmpty;
+ return this;
+ }
+
+ public Query or(QueryChain query) {
+ query.setOp("or");
+ queries.add(query);
+ nonEmpty = nonEmpty || query.nonEmpty;
+ return this;
+ }
+
+ public Query annotate(Annotation annotation) {
+ this.annotation = annotation;
+ return this;
+ }
+
+ public EndQuery offset(int offset) {
+ return new EndQuery(this).offset(offset);
+ }
+
+ public EndQuery limit(int hits) {
+ return new EndQuery(this).limit(hits);
+ }
+
+ public EndQuery timeout(int timeout) {
+ return new EndQuery(this).timeout(timeout);
+ }
+
+ public EndQuery group(Group group) {
+ return new EndQuery(this).group(group);
+ }
+
+ public EndQuery group(String groupStr) {
+ return new EndQuery(this).group(groupStr);
+ }
+
+ public EndQuery orderByAsc(String fieldName) {
+ return new EndQuery(this).orderByAsc(fieldName);
+ }
+
+ public EndQuery orderByDesc(String fieldName) {
+ return new EndQuery(this).orderByDesc(fieldName);
+ }
+
+ public FixedQuery semicolon() {
+ return new FixedQuery(new EndQuery(this));
+ }
+
+ @Override
+ public Sources getSources() {
+ return sources;
+ }
+
+ @Override
+ public void setSources(Sources sources) {
+ this.sources = sources;
+ }
+
+ @Override
+ public Query getQuery() {
+ return this;
+ }
+
+ @Override
+ public Select getSelect() {
+ return sources.select;
+ }
+
+ @Override
+ public boolean hasPositiveSearchField(String fieldName) {
+ boolean hasPositiveInSubqueries = queries.stream().anyMatch(q -> q.hasPositiveSearchField(fieldName));
+ boolean hasNegativeInSubqueries = queries.stream().anyMatch(q -> q.hasNegativeSearchField(fieldName));
+ return nonEmpty
+ && ((!"andnot".equals(this.op) && hasPositiveInSubqueries)
+ || ("andnot".equals(this.op) && hasNegativeInSubqueries));
+ }
+
+
+ @Override
+ public boolean hasPositiveSearchField(String fieldName, Object value) {
+ boolean hasPositiveInSubqueries = queries.stream().anyMatch(q -> q.hasPositiveSearchField(fieldName, value));
+ boolean hasNegativeInSubqueries = queries.stream().anyMatch(q -> q.hasNegativeSearchField(fieldName, value));
+ return nonEmpty &&
+ (!"andnot".equals(this.op) && hasPositiveInSubqueries)
+ || ("andnot".equals(this.op) && hasNegativeInSubqueries);
+ }
+
+ @Override
+ public boolean hasNegativeSearchField(String fieldName) {
+ boolean hasPositiveInSubqueries = queries.stream().anyMatch(q -> q.hasPositiveSearchField(fieldName));
+ boolean hasNegativeInSubqueries = queries.stream().anyMatch(q -> q.hasNegativeSearchField(fieldName));
+ return nonEmpty
+ && (!"andnot".equals(this.op) && hasNegativeInSubqueries)
+ || ("andnot".equals(this.op) && hasPositiveInSubqueries);
+ }
+
+ @Override
+ public boolean hasNegativeSearchField(String fieldName, Object value) {
+ boolean hasPositiveInSubqueries = queries.stream().anyMatch(q -> q.hasPositiveSearchField(fieldName, value));
+ boolean hasNegativeInSubqueries = queries.stream().anyMatch(q -> q.hasNegativeSearchField(fieldName, value));
+ return nonEmpty
+ && (!"andnot".equals(this.op) && hasNegativeInSubqueries)
+ || ("andnot".equals(this.op) && hasPositiveInSubqueries);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/QueryChain.java b/client/src/main/java/com/yahoo/vespa/client/dsl/QueryChain.java
new file mode 100644
index 00000000000..02341ee997d
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/QueryChain.java
@@ -0,0 +1,52 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+public abstract class QueryChain {
+
+ String op;
+ int index; // for distinct each query chain
+ Sources sources;
+ Select select;
+ Query query;
+ boolean nonEmpty;
+
+ void setOp(String op) {
+ this.op = op;
+ }
+
+ String getOp() {
+ return op;
+ }
+
+ void setIndex(int index) {
+ this.index = index;
+ }
+
+ Sources getSources() {
+ return sources;
+ }
+
+ void setSources(Sources sources) {
+ this.sources = sources;
+ }
+
+ Select getSelect() {
+ return select;
+ }
+
+ Query getQuery() {
+ return query;
+ }
+
+ boolean nonEmpty() {
+ return nonEmpty;
+ }
+
+ abstract boolean hasPositiveSearchField(String fieldName);
+
+ abstract boolean hasPositiveSearchField(String fieldName, Object value);
+
+ abstract boolean hasNegativeSearchField(String fieldName);
+
+ abstract boolean hasNegativeSearchField(String fieldName, Object value);
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Rank.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Rank.java
new file mode 100644
index 00000000000..4eca6264f98
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Rank.java
@@ -0,0 +1,50 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class Rank extends QueryChain {
+
+ private List<Query> queries = new ArrayList<>();
+
+ Rank(Query query, Query... ranks) {
+ this.query = query;
+ this.nonEmpty = query.nonEmpty();
+ queries.add(query);
+ queries.addAll(Stream.of(ranks).collect(Collectors.toList()));
+ }
+
+ @Override
+ public Select getSelect() {
+ return sources.select;
+ }
+
+ @Override
+ public String toString() {
+ return "rank(" + queries.stream().map(Objects::toString).collect(Collectors.joining(", ")) + ")";
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName) {
+ return queries.get(0).hasPositiveSearchField(fieldName);
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName, Object value) {
+ return queries.get(0).hasPositiveSearchField(fieldName, value);
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName) {
+ return queries.get(0).hasNegativeSearchField(fieldName);
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName, Object value) {
+ return queries.get(0).hasNegativeSearchField(fieldName, value);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Select.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Select.java
new file mode 100644
index 00000000000..9f699f2c7fa
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Select.java
@@ -0,0 +1,38 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class Select {
+
+ private List<String> selectedFields = new ArrayList<>();
+
+ Select(String fieldName) {
+ selectedFields.add(fieldName);
+ }
+
+ public Select(String fieldName, String... others) {
+ selectedFields.add(fieldName);
+ selectedFields.addAll(Stream.of(others).collect(Collectors.toList()));
+ }
+
+ Select(List<String> fieldNames) {
+ selectedFields = new ArrayList<>(fieldNames);
+ }
+
+ public Sources from(String sd) {
+ return new Sources(this, sd);
+ }
+
+ public Sources from(String sd, String... sds) {
+ return new Sources(this, sd, sds);
+ }
+
+ @Override
+ public String toString() {
+ return selectedFields.isEmpty() ? "*" : String.join(", ", selectedFields);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Sources.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Sources.java
new file mode 100644
index 00000000000..bab5ad6b4a6
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Sources.java
@@ -0,0 +1,64 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class Sources {
+
+ final Select select;
+ final List<String> targetDocTypes;
+
+ Sources(Select select, List<String> searchDefinitions) {
+ this.select = select;
+ targetDocTypes = new ArrayList<>(searchDefinitions);
+ }
+
+ Sources(Select select, String searchDefinition) {
+ this(select, Collections.singletonList(searchDefinition));
+ }
+
+ Sources(Select select, String searchDefinition, String... others) {
+ this(select, Stream.concat(Stream.of(searchDefinition), Stream.of(others)).collect(Collectors.toList()));
+ }
+
+ @Override
+ public String toString() {
+ if (targetDocTypes.isEmpty() || targetDocTypes.size() == 1 && "*".equals(targetDocTypes.get(0))) {
+ return "sources *";
+ }
+
+ if (targetDocTypes.size() == 1) {
+ return targetDocTypes.get(0);
+ }
+
+ return "sources " + String.join(", ", targetDocTypes);
+ }
+
+ public Field where(String fieldName) {
+ Field f = new Field(this, fieldName);
+ f.setOp("and");
+ return f;
+ }
+
+ public Query where(QueryChain userinput) {
+ return whereReturnQuery(userinput);
+ }
+
+ public EndQuery where(Rank rank) {
+ return whereReturnEndQuery(rank);
+ }
+
+ private Query whereReturnQuery(QueryChain qc) {
+ return new Query(this, qc);
+ }
+
+ private EndQuery whereReturnEndQuery(Rank rank) {
+ rank.setSources(this);
+ return new EndQuery(rank);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/UserInput.java b/client/src/main/java/com/yahoo/vespa/client/dsl/UserInput.java
new file mode 100644
index 00000000000..8fad664b712
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/UserInput.java
@@ -0,0 +1,80 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+
+public class UserInput extends QueryChain {
+
+ Annotation annotation; // accept only defaultIndex annotation
+ String value;
+ String indexField;
+ String placeholder; // for generating unique param
+ boolean setDefaultIndex;
+
+ UserInput(Sources sources, String value) {
+ this(sources, A.empty(), value);
+ }
+
+ UserInput(Sources sources, Annotation annotation, String value) {
+ this.sources = sources;
+ this.annotation = annotation;
+ this.value = value;
+ this.nonEmpty = true;
+
+ if (annotation.annotations.containsKey("defaultIndex")) {
+ setDefaultIndex = true;
+ indexField = (String) annotation.annotations.get("defaultIndex");
+ } else {
+ indexField = UUID.randomUUID().toString().substring(0, 5);
+ }
+ }
+
+ UserInput(String value) {
+ this(A.empty(), value);
+ }
+
+ UserInput(Annotation annotation, String value) {
+ this(null, annotation, value);
+ }
+
+ public void setIndex(int index) {
+ placeholder = setDefaultIndex
+ ? "_" + index + "_" + indexField
+ : "_" + index;
+ }
+
+ @Override
+ public String toString() {
+ //([{"defaultIndex": "shpdescfree"}](userInput(@_shpdescfree_1)))
+ return setDefaultIndex
+ ? String.format("([%s]userInput(@%s))", annotation, placeholder)
+ : String.format("userInput(@%s)", placeholder);
+ }
+
+
+ Map<String, String> getParam() {
+ return Collections.singletonMap(placeholder, value);
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName) {
+ return !"andnot".equals(this.op) && this.indexField.equals(fieldName);
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName, Object value) {
+ return hasPositiveSearchField(fieldName) && this.value.equals(value);
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName) {
+ return "andnot".equals(this.op) && this.indexField.equals(fieldName);
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName, Object value) {
+ return hasNegativeSearchField(fieldName) && this.value.equals(value);
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/Wand.java b/client/src/main/java/com/yahoo/vespa/client/dsl/Wand.java
new file mode 100644
index 00000000000..f79e07eb656
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/Wand.java
@@ -0,0 +1,71 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.List;
+import java.util.Map;
+
+public class Wand extends QueryChain {
+
+ private String fieldName;
+ private Annotation annotation;
+ private Object value;
+
+
+ Wand(String fieldName, Map<String, Integer> weightedSet) {
+ this.fieldName = fieldName;
+ this.value = weightedSet;
+ this.nonEmpty = true;
+ }
+
+ Wand(String fieldName, List<List<Integer>> numeric) {
+ boolean invalid = numeric.stream().anyMatch(range -> range.size() != 2);
+ if (invalid) {
+ throw new IllegalArgumentException("incorrect range format");
+ }
+
+ this.fieldName = fieldName;
+ this.value = numeric;
+ this.nonEmpty = true;
+ }
+
+ public Wand annotate(Annotation annotation) {
+ this.annotation = annotation;
+ return this;
+ }
+
+ @Override
+ public Select getSelect() {
+ return sources.select;
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ boolean hasAnnotation = A.hasAnnotation(annotation);
+ String s = String.format("wand(%s, %s)", fieldName, Q.gson.toJson(value));
+ return hasAnnotation ? String.format("([%s]%s)", annotation, s) : s;
+ }
+}
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/WeakAnd.java b/client/src/main/java/com/yahoo/vespa/client/dsl/WeakAnd.java
new file mode 100644
index 00000000000..43e7dd10ce2
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/WeakAnd.java
@@ -0,0 +1,63 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.stream.Collectors;
+
+public class WeakAnd extends QueryChain {
+
+ private String fieldName;
+ private Annotation annotation;
+ private Query value;
+
+
+ WeakAnd(String fieldName, Query value) {
+ this.fieldName = fieldName;
+ this.value = value;
+ this.nonEmpty = true;
+ }
+
+ public WeakAnd annotate(Annotation annotation) {
+ this.annotation = annotation;
+ return this;
+ }
+
+ @Override
+ public Select getSelect() {
+ return sources.select;
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ boolean hasAnnotation = A.hasAnnotation(annotation);
+ String
+ s =
+ String.format("weakAnd(%s, %s)", fieldName,
+ value.queries.stream().map(Object::toString).collect(Collectors.joining(", ")));
+ return hasAnnotation ? String.format("([%s]%s)", annotation, s) : s;
+ }
+}
+
diff --git a/client/src/main/java/com/yahoo/vespa/client/dsl/WeightedSet.java b/client/src/main/java/com/yahoo/vespa/client/dsl/WeightedSet.java
new file mode 100644
index 00000000000..31d435e22bb
--- /dev/null
+++ b/client/src/main/java/com/yahoo/vespa/client/dsl/WeightedSet.java
@@ -0,0 +1,50 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl;
+
+import java.util.Map;
+
+public class WeightedSet extends QueryChain {
+
+ private String fieldName;
+ private Map<String, Integer> weightedSet;
+
+ WeightedSet(String fieldName, Map<String, Integer> weightedSet) {
+ this.fieldName = fieldName;
+ this.weightedSet = weightedSet;
+ this.nonEmpty = true;
+ }
+
+ @Override
+ public Select getSelect() {
+ return sources.select;
+ }
+
+ @Override
+ public String toString() {
+ return "weightedSet(" + fieldName + ", " + Q.gson.toJson(weightedSet) + ")";
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasPositiveSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName) {
+ // TODO: implementation
+ return false;
+ }
+
+ @Override
+ boolean hasNegativeSearchField(String fieldName, Object value) {
+ // TODO: implementation
+ return false;
+ }
+}
diff --git a/client/src/test/groovy/com/yahoo/vespa/client/dsl/QTest.groovy b/client/src/test/groovy/com/yahoo/vespa/client/dsl/QTest.groovy
new file mode 100644
index 00000000000..ef87bdd3688
--- /dev/null
+++ b/client/src/test/groovy/com/yahoo/vespa/client/dsl/QTest.groovy
@@ -0,0 +1,583 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.client.dsl
+
+import spock.lang.Specification
+
+class QTest extends Specification {
+
+ def "select specific fields"() {
+ given:
+ def q = Q.select("f1", "f2")
+ .from("sd1")
+ .where("f1").contains("v1")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select f1, f2 from sd1 where f1 contains "v1";"""
+ }
+
+ def "select from specific sources"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where("f1").contains("v1")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where f1 contains "v1";"""
+ }
+
+ def "select from multiples sources"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1", "sd2")
+ .where("f1").contains("v1")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources sd1, sd2 where f1 contains "v1";"""
+ }
+
+ def "basic 'and', 'andnot', 'or', 'offset', 'limit', 'param', 'order by', and 'contains'"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where("f1").contains("v1")
+ .and("f2").contains("v2")
+ .or("f3").contains("v3")
+ .andnot("f4").contains("v4")
+ .offset(1)
+ .limit(2)
+ .timeout(3)
+ .orderByDesc("f1")
+ .orderByAsc("f2")
+ .semicolon()
+ .param("paramk1", "paramv1")
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where f1 contains "v1" and f2 contains "v2" or f3 contains "v3" and !(f4 contains "v4") order by f1 desc, f2 asc, limit 2 offset 1 timeout 3;&paramk1=paramv1"""
+ }
+
+ def "matches"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where("f1").matches("v1")
+ .and("f2").matches("v2")
+ .or("f3").matches("v3")
+ .andnot("f4").matches("v4")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where f1 matches "v1" and f2 matches "v2" or f3 matches "v3" and !(f4 matches "v4");"""
+ }
+
+ def "numeric operations"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where("f1").le(1)
+ .and("f2").lt(2)
+ .and("f3").ge(3)
+ .and("f4").gt(4)
+ .and("f5").eq(5)
+ .and("f6").inRange(6, 7)
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where f1 <= 1 and f2 < 2 and f3 >= 3 and f4 > 4 and f5 = 5 and range(f6, 6, 7);"""
+ }
+
+ def "long numeric operations"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where("f1").le(1L)
+ .and("f2").lt(2L)
+ .and("f3").ge(3L)
+ .and("f4").gt(4L)
+ .and("f5").eq(5L)
+ .and("f6").inRange(6L, 7L)
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where f1 <= 1L and f2 < 2L and f3 >= 3L and f4 > 4L and f5 = 5L and range(f6, 6L, 7L);"""
+ }
+
+ def "nested queries"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where("f1").contains("1")
+ .andnot(Q.p(Q.p("f2").contains("2").and("f3").contains("3"))
+ .or(Q.p("f2").contains("4").andnot("f3").contains("5")))
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where f1 contains "1" and !((f2 contains "2" and f3 contains "3") or (f2 contains "4" and !(f3 contains "5")));"""
+ }
+
+ def "userInput (with and with out defaultIndex)"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.ui("value"))
+ .and(Q.ui("index", "value2"))
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where userInput(@_1) and ([{"defaultIndex":"index"}]userInput(@_2_index));&_2_index=value2&_1=value"""
+ }
+
+ def "dot product"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.dotPdt("f1", [a: 1, b: 2, c: 3]))
+ .and("f2").contains("1")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where dotProduct(f1, {"a":1,"b":2,"c":3}) and f2 contains "1";"""
+ }
+
+ def "weighted set"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.wtdSet("f1", [a: 1, b: 2, c: 3]))
+ .and("f2").contains("1")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where weightedSet(f1, {"a":1,"b":2,"c":3}) and f2 contains "1";"""
+ }
+
+ def "non empty"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.nonEmpty(Q.p("f1").contains("v1")))
+ .and("f2").contains("v2")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where nonEmpty(f1 contains "v1") and f2 contains "v2";"""
+ }
+
+
+ def "wand (with and without annotation)"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.wand("f1", [a: 1, b: 2, c: 3]))
+ .and(Q.wand("f2", [[1, 1], [2, 2]]))
+ .and(
+ Q.wand("f3", [[1, 1], [2, 2]])
+ .annotate(A.a("scoreThreshold", 0.13))
+ )
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where wand(f1, {"a":1,"b":2,"c":3}) and wand(f2, [[1,1],[2,2]]) and ([{"scoreThreshold":0.13}]wand(f3, [[1,1],[2,2]]));"""
+ }
+
+ def "weak and (with and without annotation)"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.weakand("f1", Q.p("f1").contains("v1").and("f2").contains("v2")))
+ .and(Q.weakand("f3", Q.p("f1").contains("v1").and("f2").contains("v2"))
+ .annotate(A.a("scoreThreshold", 0.13))
+ )
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where weakAnd(f1, f1 contains "v1", f2 contains "v2") and ([{"scoreThreshold":0.13}]weakAnd(f3, f1 contains "v1", f2 contains "v2"));"""
+ }
+
+ def "rank with only query"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.rank(
+ Q.p("f1").contains("v1")
+ )
+ )
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where rank(f1 contains "v1");"""
+ }
+
+ def "rank"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.rank(
+ Q.p("f1").contains("v1"),
+ Q.p("f2").contains("v2"),
+ Q.p("f3").eq(3))
+ )
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where rank(f1 contains "v1", f2 contains "v2", f3 = 3);"""
+ }
+
+ def "rank with rank query array"() {
+ given:
+ Query[] ranks = [Q.p("f2").contains("v2"), Q.p("f3").eq(3)].toArray()
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.rank(
+ Q.p("f1").contains("v1"),
+ ranks)
+ )
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where rank(f1 contains "v1", f2 contains "v2", f3 = 3);"""
+ }
+
+ def "string/function annotations"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where("f1").contains(annotation, "v1")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where f1 contains (${expected}"v1");"""
+
+ where:
+ annotation | expected
+ A.filter() | """[{"filter":true}]"""
+ A.defaultIndex("idx") | """[{"defaultIndex":"idx"}]"""
+ A.a([a1: [k1: "v1", k2: 2]]) | """[{"a1":{"k1":"v1","k2":2}}]"""
+ }
+
+ def "sub-expression annotations"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where("f1").contains("v1").annotate(A.a("ak1", "av1"))
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where ([{"ak1":"av1"}](f1 contains "v1"));"""
+ }
+
+ def "sub-expressions annotations (annotate in the middle of query)"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.p("f1").contains("v1").annotate(A.a("ak1", "av1")).and("f2").contains("v2"))
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where ([{"ak1":"av1"}](f1 contains "v1" and f2 contains "v2"));"""
+ }
+
+ def "sub-expressions annotations (annotate in nested queries)"() {
+ given:
+ def q = Q.select("*")
+ .from("sd1")
+ .where(Q.p(
+ Q.p("f1").contains("v1").annotate(A.a("ak1", "av1")))
+ .and("f2").contains("v2")
+ )
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sd1 where (([{"ak1":"av1"}](f1 contains "v1")) and f2 contains "v2");"""
+ }
+
+ def "build query which created from Q.b without select and sources"() {
+ given:
+ def q = Q.p("f1").contains("v1")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources * where f1 contains "v1";"""
+ }
+
+ def "order by"() {
+ given:
+ def q = Q.p("f1").contains("v1")
+ .orderByAsc("f2")
+ .orderByAsc(A.a([function: "uca", locale: "en_US", strength: "IDENTICAL"]), "f3")
+ .orderByDesc("f4")
+ .orderByDesc(A.a([function: "lowercase"]), "f5")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources * where f1 contains "v1" order by f2 asc, [{"function":"uca","locale":"en_US","strength":"IDENTICAL"}]f3 asc, f4 desc, [{"function":"lowercase"}]f5 desc;"""
+ }
+
+ def "contains sameElement"() {
+ given:
+ def q = Q.p("f1").containsSameElement(Q.p("stime").le(1).and("etime").gt(2))
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources * where f1 contains sameElement(stime <= 1, etime > 2);"""
+ }
+
+ def "contains phrase/near/onear/equiv"() {
+ given:
+ def funcName = "contains${operator.capitalize()}"
+ def q1 = Q.p("f1")."$funcName"("p1", "p2", "p3")
+ .semicolon()
+ .build()
+ def q2 = Q.p("f1")."$funcName"(["p1", "p2", "p3"])
+ .semicolon()
+ .build()
+
+ expect:
+ q1 == """yql=select * from sources * where f1 contains ${operator}("p1", "p2", "p3");"""
+ q2 == """yql=select * from sources * where f1 contains ${operator}("p1", "p2", "p3");"""
+
+ where:
+ operator | _
+ "phrase" | _
+ "near" | _
+ "onear" | _
+ "equiv" | _
+ }
+
+ def "contains uri"() {
+ given:
+ def q = Q.p("f1").containsUri("https://test.uri")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources * where f1 contains uri("https://test.uri");"""
+ }
+
+ def "contains uri with annotation"() {
+ given:
+ def q = Q.p("f1").containsUri(A.a("key", "value"), "https://test.uri")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources * where f1 contains ([{"key":"value"}]uri("https://test.uri"));"""
+ }
+
+ def "use contains instead of contains equiv when input size is 1"() {
+ def q = Q.p("f1").containsEquiv(["p1"])
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources * where f1 contains "p1";"""
+ }
+
+ def "contains phrase/near/onear/equiv empty list should throw illegal argument exception"() {
+ given:
+ def funcName = "contains${operator.capitalize()}"
+
+ when:
+ def q = Q.p("f1")."$funcName"([])
+ .semicolon()
+ .build()
+
+ then:
+ thrown(IllegalArgumentException)
+
+ where:
+ operator | _
+ "phrase" | _
+ "near" | _
+ "onear" | _
+ "equiv" | _
+ }
+
+
+ def "contains near/onear with annotation"() {
+ given:
+ def funcName = "contains${operator.capitalize()}"
+ def q = Q.p("f1")."$funcName"(A.a("distance", 5), "p1", "p2", "p3")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources * where f1 contains ([{"distance":5}]${operator}("p1", "p2", "p3"));"""
+
+ where:
+ operator | _
+ "near" | _
+ "onear" | _
+ }
+
+ def "basic group syntax"() {
+ /*
+ example from vespa document:
+ https://docs.vespa.ai/documentation/grouping.html
+ all( group(a) max(5) each(output(count())
+ all(max(1) each(output(summary())))
+ all(group(b) each(output(count())
+ all(max(1) each(output(summary())))
+ all(group(c) each(output(count())
+ all(max(1) each(output(summary())))))))) );
+ */
+ given:
+ def q = Q.p("f1").contains("v1")
+ .group(
+ G.all(G.group("a"), G.maxRtn(5), G.each(G.output(G.count()),
+ G.all(G.maxRtn(1), G.each(G.output(G.summary()))),
+ G.all(G.group("b"), G.each(G.output(G.count()),
+ G.all(G.maxRtn(1), G.each(G.output(G.summary()))),
+ G.all(G.group("c"), G.each(G.output(G.count()),
+ G.all(G.maxRtn(1), G.each(G.output(G.summary())))
+ ))
+ ))
+ ))
+ )
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources * where f1 contains "v1" | all(group(a) max(5) each(output(count()) all(max(1) each(output(summary()))) all(group(b) each(output(count()) all(max(1) each(output(summary()))) all(group(c) each(output(count()) all(max(1) each(output(summary())))))))));"""
+ }
+
+ def "set group syntax string directly"() {
+ /*
+ example from vespa document:
+ https://docs.vespa.ai/documentation/grouping.html
+ all( group(a) max(5) each(output(count())
+ all(max(1) each(output(summary())))
+ all(group(b) each(output(count())
+ all(max(1) each(output(summary())))
+ all(group(c) each(output(count())
+ all(max(1) each(output(summary())))))))) );
+ */
+ given:
+ def q = Q.p("f1").contains("v1")
+ .group("all(group(a) max(5) each(output(count()) all(max(1) each(output(summary()))) all(group(b) each(output(count()) all(max(1) each(output(summary()))) all(group(c) each(output(count()) all(max(1) each(output(summary())))))))))")
+ .semicolon()
+ .build()
+
+ expect:
+ q == """yql=select * from sources * where f1 contains "v1" | all(group(a) max(5) each(output(count()) all(max(1) each(output(summary()))) all(group(b) each(output(count()) all(max(1) each(output(summary()))) all(group(c) each(output(count()) all(max(1) each(output(summary())))))))));"""
+ }
+
+ def "arbitrary annotations"() {
+ given:
+ def a = A.a("a1", "v1", "a2", 2, "a3", [k: "v", k2: 1], "a4", 4D, "a5", [1, 2, 3])
+ expect:
+ a.toString() == """{"a1":"v1","a2":2,"a3":{"k":"v","k2":1},"a4":4.0,"a5":[1,2,3]}"""
+ }
+
+ def "test programmability"() {
+ given:
+ def map = [a: "1", b: "2", c: "3"]
+
+ when:
+ Query q = map
+ .entrySet()
+ .stream()
+ .map { entry -> Q.p(entry.key).contains(entry.value) }
+ .reduce { q1, q2 -> q1.and(q2) }
+ .get()
+
+ then:
+ q.semicolon().build() == """yql=select * from sources * where a contains "1" and b contains "2" and c contains "3";"""
+ }
+
+ def "test programmability 2"() {
+ given:
+ def map = [a: "1", b: "2", c: "3"]
+ def q = Q.p()
+
+ when:
+ map.each { k, v ->
+ q.and(Q.p(k).contains(v))
+ }
+
+ then:
+ q.semicolon().build() == """yql=select * from sources * where a contains "1" and b contains "2" and c contains "3";"""
+ }
+
+ def "empty queries should not print out"() {
+ given:
+ def q = Q.p(Q.p(Q.p().andnot(Q.p()).and(Q.p()))).and("a").contains("1").semicolon().build()
+
+ expect:
+ q == """yql=select * from sources * where a contains "1";"""
+ }
+
+ def "validate positive search term of strings"() {
+ given:
+ def q = Q.p(Q.p("k1").contains("v1").and("k2").contains("v2").andnot("k3").contains("v3"))
+ .andnot(Q.p("nk1").contains("nv1").and("nk2").contains("nv2").andnot("nk3").contains("nv3"))
+ .and(Q.p("k4").contains("v4")
+ .andnot(Q.p("k5").contains("v5").andnot("k6").contains("v6"))
+ )
+
+ expect:
+ q.hasPositiveSearchField("k1")
+ q.hasPositiveSearchField("k2")
+ q.hasPositiveSearchField("nk3")
+ q.hasPositiveSearchField("k6")
+ q.hasPositiveSearchField("k6", "v6")
+ !q.hasPositiveSearchField("k6", "v5")
+
+ q.hasNegativeSearchField("k3")
+ q.hasNegativeSearchField("nk1")
+ q.hasNegativeSearchField("nk2")
+ q.hasNegativeSearchField("k5")
+ q.hasNegativeSearchField("k5", "v5")
+ !q.hasNegativeSearchField("k5", "v4")
+ }
+
+ def "validate positive search term of user input"() {
+ given:
+ def q = Q.p(Q.ui("k1", "v1")).and(Q.ui("k2", "v2")).andnot(Q.ui("k3", "v3"))
+ .andnot(Q.p(Q.ui("nk1", "nv1")).and(Q.ui("nk2", "nv2")).andnot(Q.ui("nk3", "nv3")))
+ .and(Q.p(Q.ui("k4", "v4"))
+ .andnot(Q.p(Q.ui("k5", "v5")).andnot(Q.ui("k6", "v6")))
+ )
+
+ expect:
+ q.hasPositiveSearchField("k1")
+ q.hasPositiveSearchField("k2")
+ q.hasPositiveSearchField("nk3")
+ q.hasPositiveSearchField("k6")
+ q.hasPositiveSearchField("k6", "v6")
+ !q.hasPositiveSearchField("k6", "v5")
+
+ q.hasNegativeSearchField("k3")
+ q.hasNegativeSearchField("nk1")
+ q.hasNegativeSearchField("nk2")
+ q.hasNegativeSearchField("k5")
+ q.hasNegativeSearchField("k5", "v5")
+ !q.hasNegativeSearchField("k5", "v4")
+ }
+}
diff --git a/pom.xml b/pom.xml
index b42f34062b4..a98d9887fcf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,7 @@
<module>athenz-identity-provider-service</module>
<module>bundle-plugin-test</module>
<module>chain</module>
+ <module>client</module>
<module>clustercontroller-apps</module>
<module>clustercontroller-apputil</module>
<module>clustercontroller-core</module>