// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package ai.vespa.client.dsl; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; /** * Vespa query object */ public class Query extends QueryChain { final List queries = new ArrayList<>(); private Annotation annotation; Query(Sources sources, QueryChain queryChain) { this.sources = sources; this.queries.add(queryChain); this.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(); } /** * And. * https://docs.vespa.ai/en/reference/query-language-reference.html#and * * @param fieldName the field name * @return the field */ public Field and(String fieldName) { Field f = new Field(this, fieldName); f.setOp("and"); queries.add(f); nonEmpty = true; return f; } /** * Andnot. * https://docs.vespa.ai/en/reference/query-language-reference.html#andnot * * @param fieldName the field name * @return the field */ public Field andnot(String fieldName) { Field f = new Field(this, fieldName); f.setOp("andnot"); queries.add(f); nonEmpty = true; return f; } /** * Or. * https://docs.vespa.ai/en/reference/query-language-reference.html#or * * @param fieldName the field name * @return the field */ public Field or(String fieldName) { Field f = new Field(this, fieldName); f.setOp("or"); queries.add(f); nonEmpty = true; return f; } /** * And. * https://docs.vespa.ai/en/reference/query-language-reference.html#and * * @param query the query * @return the query */ public Query and(QueryChain query) { query.setOp("and"); queries.add(query); nonEmpty = nonEmpty || query.nonEmpty; return this; } /** * Andnot. * https://docs.vespa.ai/en/reference/query-language-reference.html#andnot * * @param query the query * @return the query */ public Query andnot(QueryChain query) { query.setOp("andnot"); queries.add(query); nonEmpty = nonEmpty || query.nonEmpty; return this; } /** * Or. * https://docs.vespa.ai/en/reference/query-language-reference.html#or * * @param query the query * @return the query */ public Query or(QueryChain query) { query.setOp("or"); queries.add(query); nonEmpty = nonEmpty || query.nonEmpty; return this; } /** * Annotate a query (sub-expression). * https://docs.vespa.ai/en/reference/query-language-reference.html#annotations-of-sub-expressions * * @param annotation the annotation * @return the query */ public Query annotate(Annotation annotation) { this.annotation = annotation; return this; } /** * Offset. * https://docs.vespa.ai/en/reference/query-language-reference.html#limit-offset * * @param offset the offset * @return the end query */ public EndQuery offset(int offset) { return new EndQuery(this).offset(offset); } /** * Limit. * https://docs.vespa.ai/en/reference/query-language-reference.html#limit-offset * * @param hits the hits * @return the end query */ public EndQuery limit(int hits) { return new EndQuery(this).limit(hits); } /** * Timeout. * https://docs.vespa.ai/en/reference/query-language-reference.html#timeout * * @param timeout the timeout * @return the end query */ public EndQuery timeout(int timeout) { return new EndQuery(this).timeout(timeout); } /** * Group. * https://docs.vespa.ai/en/reference/query-language-reference.html#grouping * * @param group the group * @return the end query */ public EndQuery group(Group group) { return new EndQuery(this).group(group); } /** * Group. * https://docs.vespa.ai/en/reference/query-language-reference.html#grouping * * @param groupStr the group str * @return the end query */ public EndQuery group(String groupStr) { return new EndQuery(this).group(groupStr); } /** * Order by asc. * https://docs.vespa.ai/en/reference/query-language-reference.html#order-by * * @param fieldName the field name * @return the end query */ public EndQuery orderByAsc(String fieldName) { return new EndQuery(this).orderByAsc(fieldName); } /** * Order by asc. * https://docs.vespa.ai/en/reference/query-language-reference.html#order-by * * @param annotation the annotation * @param fieldName the field name * @return the end query */ public EndQuery orderByAsc(Annotation annotation, String fieldName) { return new EndQuery(this).orderByAsc(annotation, fieldName); } /** * Order by desc. * https://docs.vespa.ai/en/reference/query-language-reference.html#order-by * * @param fieldName the field name * @return the end query */ public EndQuery orderByDesc(String fieldName) { return new EndQuery(this).orderByDesc(fieldName); } /** * Order by desc. * https://docs.vespa.ai/en/reference/query-language-reference.html#order-by * * @param annotation the annotation * @param fieldName the field name * @return the end query */ public EndQuery orderByDesc(Annotation annotation, String fieldName) { return new EndQuery(this).orderByDesc(annotation, fieldName); } /** * Calls fix() * * @return the fixed query * @deprecated use {@link #fix()}, {@link #end()} or {@link #build} instead */ @Deprecated // TODO: Remove on Vespa 9 public FixedQuery semicolon() { return fix(); } /** Returns this as an ended query. */ public EndQuery end() { return new EndQuery(this); } /** Calls end().fix(). */ public FixedQuery fix() { return end().fix(); } /** Calls fix().build(). */ public String build() { return fix().build(); } @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); } }