From d915a9ec3c90d6b72eac9dfd79c64d6b7ce90391 Mon Sep 17 00:00:00 2001 From: yehzu Date: Thu, 12 Dec 2019 13:32:06 +0800 Subject: dsl library for vespa search queries --- .../main/java/com/yahoo/vespa/client/dsl/A.java | 67 +++ .../com/yahoo/vespa/client/dsl/Aggregator.java | 22 + .../com/yahoo/vespa/client/dsl/Annotation.java | 29 + .../com/yahoo/vespa/client/dsl/DotProduct.java | 50 ++ .../java/com/yahoo/vespa/client/dsl/EndQuery.java | 114 ++++ .../java/com/yahoo/vespa/client/dsl/Field.java | 274 ++++++++++ .../com/yahoo/vespa/client/dsl/FixedQuery.java | 414 +++++++++++++++ .../main/java/com/yahoo/vespa/client/dsl/G.java | 46 ++ .../java/com/yahoo/vespa/client/dsl/Group.java | 24 + .../com/yahoo/vespa/client/dsl/GroupOperation.java | 34 ++ .../java/com/yahoo/vespa/client/dsl/IGroup.java | 10 + .../yahoo/vespa/client/dsl/IGroupOperation.java | 10 + .../java/com/yahoo/vespa/client/dsl/NonEmpty.java | 46 ++ .../main/java/com/yahoo/vespa/client/dsl/Q.java | 73 +++ .../java/com/yahoo/vespa/client/dsl/Query.java | 227 ++++++++ .../com/yahoo/vespa/client/dsl/QueryChain.java | 52 ++ .../main/java/com/yahoo/vespa/client/dsl/Rank.java | 50 ++ .../java/com/yahoo/vespa/client/dsl/Select.java | 38 ++ .../java/com/yahoo/vespa/client/dsl/Sources.java | 64 +++ .../java/com/yahoo/vespa/client/dsl/UserInput.java | 80 +++ .../main/java/com/yahoo/vespa/client/dsl/Wand.java | 71 +++ .../java/com/yahoo/vespa/client/dsl/WeakAnd.java | 63 +++ .../com/yahoo/vespa/client/dsl/WeightedSet.java | 50 ++ .../groovy/com/yahoo/vespa/client/dsl/QTest.groovy | 583 +++++++++++++++++++++ 24 files changed, 2491 insertions(+) create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/A.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Aggregator.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Annotation.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/DotProduct.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/EndQuery.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Field.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/FixedQuery.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/G.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Group.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/GroupOperation.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/IGroup.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/IGroupOperation.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/NonEmpty.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Q.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Query.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/QueryChain.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Rank.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Select.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Sources.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/UserInput.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/Wand.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/WeakAnd.java create mode 100644 client/src/main/java/com/yahoo/vespa/client/dsl/WeightedSet.java create mode 100644 client/src/test/groovy/com/yahoo/vespa/client/dsl/QTest.groovy (limited to 'client/src') 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 map = new HashMap<>(); + map.put(name, value); + return new Annotation(map); + } + + public static Annotation a(Map 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 annotations = Collections.emptyMap(); + + Annotation() { + } + + Annotation(Map 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 weightedSet; + + DotProduct(String fieldName, Map 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 map = new LinkedHashMap<>(); + List 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 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 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 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 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 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 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 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 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 others = new HashMap<>(); + Map 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 params) { + others.putAll(params); + return this; + } + + public Map 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 getUserInputs() { + return getUserInputs(endQuery.queryChain.getQuery()); + } + + private Map getUserInputs(Query q) { + Map 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 weightedSet) { + return new DotProduct(field, weightedSet); + } + + public static WeightedSet wtdSet(String field, Map weightedSet) { + return new WeightedSet(field, weightedSet); + } + + public static NonEmpty nonEmpty(Query query) { + return new NonEmpty(query); + } + + public static Wand wand(String field, Map weightedSet) { + return new Wand(field, weightedSet); + } + + public static Wand wand(String field, List> 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 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 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 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 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 targetDocTypes; + + Sources(Select select, List 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 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 weightedSet) { + this.fieldName = fieldName; + this.value = weightedSet; + this.nonEmpty = true; + } + + Wand(String fieldName, List> 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 weightedSet; + + WeightedSet(String fieldName, Map 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;¶mk1=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") + } +} -- cgit v1.2.3