diff options
author | Henrik <henrik.hoiness@online.no> | 2018-08-02 10:18:50 +0200 |
---|---|---|
committer | Henrik <henrik.hoiness@online.no> | 2018-08-02 10:18:50 +0200 |
commit | 8c4383c1b1f0e06ea31a7b0284b57ca9c48f9d3e (patch) | |
tree | 2eef196206ac7c7965206d288ad29b0c2791d8de | |
parent | 0a5e370b59373d8147e2a8e682bc3296eec0d639 (diff) |
Created new parameter SELECT with subparameters WHERE and GROUPING. Started on a SelectParser which will parse the SELECT parameters to a QueryTree. Also added a new type for queryies 'SELECT', and will now use the SelectParser when getting the QueryTree. Started on testing the same testcases as in YQLParser, looks promising.
10 files changed, 1662 insertions, 2 deletions
diff --git a/container-search/src/main/java/com/yahoo/search/Query.java b/container-search/src/main/java/com/yahoo/search/Query.java index cbc8b15eff9..4ec0b6e426c 100644 --- a/container-search/src/main/java/com/yahoo/search/Query.java +++ b/container-search/src/main/java/com/yahoo/search/Query.java @@ -20,6 +20,7 @@ import com.yahoo.search.query.Presentation; import com.yahoo.search.query.Properties; import com.yahoo.search.query.QueryTree; import com.yahoo.search.query.Ranking; +import com.yahoo.search.query.Select; import com.yahoo.search.query.SessionId; import com.yahoo.search.query.Sorting; import com.yahoo.search.query.Sorting.AttributeSorter; @@ -100,7 +101,8 @@ public class Query extends com.yahoo.processing.Request implements Cloneable { ADVANCED(3,"adv"), WEB(4,"web"), PROGRAMMATIC(5, "prog"), - YQL(6, "yql"); + YQL(6, "yql"), + SELECT(7, "select");; private final int intValue; private final String stringValue; @@ -170,6 +172,9 @@ public class Query extends com.yahoo.processing.Request implements Cloneable { /** How results of this query should be presented */ private Presentation presentation = new Presentation(this); + /** The selection of where-clause and grouping */ + private Select select = new Select(this); + //---------------- Tracing ---------------------------------------------------- private static Logger log = Logger.getLogger(Query.class.getName()); @@ -206,6 +211,7 @@ public class Query extends com.yahoo.processing.Request implements Cloneable { argumentType.addField(new FieldDescription(Presentation.PRESENTATION, new QueryProfileFieldType(Presentation.getArgumentType()))); argumentType.addField(new FieldDescription(Ranking.RANKING, new QueryProfileFieldType(Ranking.getArgumentType()))); argumentType.addField(new FieldDescription(Model.MODEL, new QueryProfileFieldType(Model.getArgumentType()))); + argumentType.addField(new FieldDescription(Select.SELECT, new QueryProfileFieldType(Select.getArgumentType()))); argumentType.freeze(); } public static QueryProfileType getArgumentType() { return argumentType; } @@ -218,6 +224,7 @@ public class Query extends com.yahoo.processing.Request implements Cloneable { addAliases(Ranking.getArgumentType(), propertyAliasesBuilder); addAliases(Model.getArgumentType(), propertyAliasesBuilder); addAliases(Presentation.getArgumentType(), propertyAliasesBuilder); + addAliases(Select.getArgumentType(), propertyAliasesBuilder); propertyAliases = ImmutableMap.copyOf(propertyAliasesBuilder); } private static void addAliases(QueryProfileType arguments, Map<String, CompoundName> aliases) { @@ -237,6 +244,7 @@ public class Query extends com.yahoo.processing.Request implements Cloneable { registry.register(Query.getArgumentType().unfrozen()); registry.register(Ranking.getArgumentType().unfrozen()); registry.register(Model.getArgumentType().unfrozen()); + registry.register(Select.getArgumentType().unfrozen()); registry.register(Presentation.getArgumentType().unfrozen()); registry.register(DefaultProperties.argumentType.unfrozen()); } @@ -381,6 +389,7 @@ public class Query extends com.yahoo.processing.Request implements Cloneable { setFrom(properties,Model.getArgumentType(), context); setFrom(properties,Presentation.getArgumentType(), context); setFrom(properties,Ranking.getArgumentType(), context); + setFrom(properties, Select.getArgumentType(), context); } /** @@ -980,6 +989,9 @@ public class Query extends com.yahoo.processing.Request implements Cloneable { /** Returns the presentation to be used for this query, never null */ public Presentation getPresentation() { return presentation; } + /** Returns the select to be used for this query, never null */ + public Select getSelect() { return select; } + /** Returns the ranking to be used for this query, never null */ public Ranking getRanking() { return ranking; } diff --git a/container-search/src/main/java/com/yahoo/search/federation/vespa/VespaSearcher.java b/container-search/src/main/java/com/yahoo/search/federation/vespa/VespaSearcher.java index b59578ab6a3..fdbee9c8f11 100644 --- a/container-search/src/main/java/com/yahoo/search/federation/vespa/VespaSearcher.java +++ b/container-search/src/main/java/com/yahoo/search/federation/vespa/VespaSearcher.java @@ -140,6 +140,8 @@ public class VespaSearcher extends ConfiguredHTTPProviderSearcher { return Query.Type.PROGRAMMATIC; } else if (providerQueryType == ProviderConfig.QueryType.YQL) { return Query.Type.YQL; + } else if (providerQueryType == ProviderConfig.QueryType.SELECT) { + return Query.Type.SELECT; } else { throw new RuntimeException("Query type " + providerQueryType + " unsupported."); } diff --git a/container-search/src/main/java/com/yahoo/search/handler/SearchHandler.java b/container-search/src/main/java/com/yahoo/search/handler/SearchHandler.java index 3bfaee658f9..d9fa6d8097f 100644 --- a/container-search/src/main/java/com/yahoo/search/handler/SearchHandler.java +++ b/container-search/src/main/java/com/yahoo/search/handler/SearchHandler.java @@ -576,6 +576,18 @@ public class SearchHandler extends LoggingRequestHandler { // Create request-mapping Map<String, String> requestMap = new HashMap<>(); createRequestMapping(inspector, requestMap, ""); + + // Throws QueryException if query contains both yql- and select-parameter + if (requestMap.containsKey("yql") && (requestMap.containsKey("select.where") || requestMap.containsKey("select.grouping")) ) { + throw new QueryException("Illegal query: Query contains both yql- and select-parameter"); + } + + // The query-parameter overrides the select-parameter. Removing them here to prevent further processing of select-parameters. + if (requestMap.containsKey("query")) { + requestMap.remove("select.where"); + requestMap.remove("select.grouping"); + } + return new Query(request, requestMap, queryProfile); @@ -605,6 +617,10 @@ public class SearchHandler extends LoggingRequestHandler { map.put(qualifiedKey, value.asString()); break; case OBJECT: + if (qualifiedKey.equals("select.where") || qualifiedKey.equals("select.grouping")){ + map.put(qualifiedKey, value.toString()); + break; + } createRequestMapping(value, map, qualifiedKey+"."); break; } diff --git a/container-search/src/main/java/com/yahoo/search/query/Select.java b/container-search/src/main/java/com/yahoo/search/query/Select.java new file mode 100644 index 00000000000..ec747440246 --- /dev/null +++ b/container-search/src/main/java/com/yahoo/search/query/Select.java @@ -0,0 +1,135 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.search.query; + + +import com.yahoo.document.select.rule.AttributeNode; +import com.yahoo.processing.request.CompoundName; +import com.yahoo.search.Query; +import com.yahoo.search.Result; +import com.yahoo.search.query.parser.Parsable; +import com.yahoo.search.query.parser.ParserEnvironment; +import com.yahoo.search.query.parser.ParserFactory; +import com.yahoo.search.query.profile.types.FieldDescription; +import com.yahoo.search.query.profile.types.QueryProfileType; +import com.yahoo.search.result.ErrorMessage; +import com.yahoo.search.yql.YqlParser; + +import static com.yahoo.fs4.PacketDumper.PacketType.query; + + +/** + * The parameters defining the where-clause and groping of a query + * + * @author henrhoi + */ +public class Select implements Cloneable { + + /** The type representing the property arguments consumed by this */ + private static final QueryProfileType argumentType; + private static final CompoundName argumentTypeName; + + public static final String SELECT = "select"; + public static final String WHERE = "where"; + public static final String GROUPING = "grouping"; + + + private static Model model; + private Query parent; + + static { + argumentType = new QueryProfileType(SELECT); + argumentType.setStrict(true); + argumentType.setBuiltin(true); + argumentType.addField(new FieldDescription(WHERE, "string", "where")); + argumentType.addField(new FieldDescription(GROUPING, "string", "grouping")); + argumentType.freeze(); + argumentTypeName=new CompoundName(argumentType.getId().getName()); + } + + public static QueryProfileType getArgumentType() { return argumentType; } + + public Select(String where, String grouping){ + this.where = where; + this.grouping = grouping; + } + + public Select(Query query) { + setParent(query); + model = query.getModel(); + } + + private String where = ""; + private String grouping = ""; + + + /** returns the query owning this, never null */ + public Query getParent() { return parent; } + + /** Assigns the query owning this */ + public void setParent(Query parent) { + if (parent==null) throw new NullPointerException("A query models owner cannot be null"); + this.parent = parent; + } + + public static Select getFrom(Query q) { + return (Select) q.properties().get(argumentTypeName); + } + + + public void setQueryString(String queryString) { + model.setQueryString(queryString); + } + + /** Set the where-clause for the query. Must be a JSON-string. */ + public void setWhere(String where){ + this.where = where; + //QueryTree newTree = getParsedQueryTree(); + + //if (newTree != null){ + // parent.getModel().getQueryTree().setRoot(newTree.getRoot()); + //} + } + + public QueryTree getParsedQueryTree(){ + SelectParser parser = (SelectParser) ParserFactory.newInstance(Query.Type.SELECT, new ParserEnvironment()); + QueryTree newTree = null; + try { + System.out.println(parent.properties().getString(Select.WHERE)); + QueryTree ewTree = parser.parse(Parsable.fromQueryModel(parent.getModel()).setQuery(parent.properties().getString(Select.WHERE))); + System.out.println(ewTree.toString()); + + } catch (RuntimeException e) { + throw new RuntimeException("Could not instantiate query from YQL", e); + } + return newTree; + } + + + + + + + + + + /** Returns the where-clause in the query */ + public String getWhere(){ + return this.where; + } + + /** Set the grouping-string for the query. Must be a JSON-string. */ + public void setGrouping(String grouping){ + this.grouping = grouping; + } + + /** Returns the grouping in the query */ + public String getGrouping(){ + return this.grouping; + } + + @Override + public String toString() { + return "where: [" + where + "], grouping: [" + grouping+ "]"; + } + +} diff --git a/container-search/src/main/java/com/yahoo/search/query/SelectParser.java b/container-search/src/main/java/com/yahoo/search/query/SelectParser.java new file mode 100644 index 00000000000..559e0ab034e --- /dev/null +++ b/container-search/src/main/java/com/yahoo/search/query/SelectParser.java @@ -0,0 +1,1026 @@ +package com.yahoo.search.query; + + +import com.google.common.base.Preconditions; +import com.yahoo.collections.LazyMap; +import com.yahoo.collections.Tuple2; +import com.yahoo.component.Version; +import com.yahoo.language.Language; +import com.yahoo.language.Linguistics; +import com.yahoo.language.detect.Detector; +import com.yahoo.language.process.Normalizer; +import com.yahoo.language.process.Segmenter; +import com.yahoo.prelude.IndexFacts; +import com.yahoo.prelude.query.AndItem; +import com.yahoo.prelude.query.CompositeItem; +import com.yahoo.prelude.query.DotProductItem; +import com.yahoo.prelude.query.EquivItem; +import com.yahoo.prelude.query.ExactStringItem; +import com.yahoo.prelude.query.IntItem; +import com.yahoo.prelude.query.Item; +import com.yahoo.prelude.query.Limit; +import com.yahoo.prelude.query.NearItem; +import com.yahoo.prelude.query.NotItem; +import com.yahoo.prelude.query.ONearItem; +import com.yahoo.prelude.query.OrItem; +import com.yahoo.prelude.query.PhraseItem; +import com.yahoo.prelude.query.PredicateQueryItem; +import com.yahoo.prelude.query.PrefixItem; +import com.yahoo.prelude.query.QueryException; +import com.yahoo.prelude.query.RangeItem; +import com.yahoo.prelude.query.RankItem; +import com.yahoo.prelude.query.RegExpItem; +import com.yahoo.prelude.query.SameElementItem; +import com.yahoo.prelude.query.SegmentingRule; +import com.yahoo.prelude.query.Substring; +import com.yahoo.prelude.query.SubstringItem; +import com.yahoo.prelude.query.SuffixItem; +import com.yahoo.prelude.query.TaggableItem; +import com.yahoo.prelude.query.WandItem; +import com.yahoo.prelude.query.WeakAndItem; +import com.yahoo.prelude.query.WeightedSetItem; +import com.yahoo.prelude.query.WordAlternativesItem; +import com.yahoo.prelude.query.WordItem; +import com.yahoo.search.query.parser.Parsable; +import com.yahoo.search.query.parser.Parser; + +import com.yahoo.search.query.parser.ParserEnvironment; +import com.yahoo.slime.ArrayTraverser; +import com.yahoo.slime.Inspector; +import com.yahoo.slime.ObjectTraverser; +import com.yahoo.vespa.config.SlimeUtils; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.yahoo.slime.Type.ARRAY; +import static com.yahoo.slime.Type.DOUBLE; +import static com.yahoo.slime.Type.LONG; +import static com.yahoo.slime.Type.OBJECT; +import static com.yahoo.slime.Type.STRING; + +public class SelectParser implements Parser { + + Parsable query; + private final IndexFacts indexFacts; + private final Map<Integer, TaggableItem> identifiedItems = LazyMap.newHashMap(); + private final Normalizer normalizer; + private final Segmenter segmenter; + private final Detector detector; + private final String localSegmenterBackend; + private final Version localSegmenterVersion; + private final ParserEnvironment environment; + private IndexFacts.Session indexFactsSession; + + + + /** YQL parameters and functions */ + + private static final String DESCENDING_HITS_ORDER = "descending"; + private static final String ASCENDING_HITS_ORDER = "ascending"; + + private static final Integer DEFAULT_HITS = 10; + private static final Integer DEFAULT_OFFSET = 0; + private static final Integer DEFAULT_TARGET_NUM_HITS = 10; + static final String ORIGIN_LENGTH = "length"; + static final String ORIGIN_OFFSET = "offset"; + static final String ORIGIN = "origin"; + static final String ORIGIN_ORIGINAL = "original"; + + private static final String ANNOTATIONS = "annotations"; + private static final String NFKC = "nfkc"; + private static final String USER_INPUT_LANGUAGE = "language"; + private static final String ACCENT_DROP = "accentDrop"; + private static final String ALTERNATIVES = "alternatives"; + private static final String AND_SEGMENTING = "andSegmenting"; + private static final String DISTANCE = "distance"; + private static final String DOT_PRODUCT = "dotProduct"; + private static final String EQUIV = "equiv"; + private static final String FILTER = "filter"; + private static final String HIT_LIMIT = "hitLimit"; + private static final String IMPLICIT_TRANSFORMS = "implicitTransforms"; + private static final String LABEL = "label"; + private static final String NEAR = "near"; + private static final String NORMALIZE_CASE = "normalizeCase"; + private static final String ONEAR = "onear"; + private static final String PHRASE = "phrase"; + private static final String PREDICATE = "predicate"; + private static final String PREFIX = "prefix"; + private static final String RANKED = "ranked"; + private static final String RANK = "rank"; + private static final String SAME_ELEMENT = "sameElement"; + private static final String SCORE_THRESHOLD = "scoreThreshold"; + private static final String SIGNIFICANCE = "significance"; + private static final String STEM = "stem"; + private static final String SUBSTRING = "substring"; + private static final String SUFFIX = "suffix"; + private static final String TARGET_NUM_HITS = "targetNumHits"; + private static final String THRESHOLD_BOOST_FACTOR = "thresholdBoostFactor"; + private static final String UNIQUE_ID = "id"; + private static final String USE_POSITION_DATA = "usePositionData"; + private static final String WAND = "wand"; + private static final String WEAK_AND = "weakAnd"; + private static final String WEIGHTED_SET = "weightedSet"; + private static final String WEIGHT = "weight"; + + /**************************************/ + + public SelectParser(ParserEnvironment environment) { + indexFacts = environment.getIndexFacts(); + normalizer = environment.getLinguistics().getNormalizer(); + segmenter = environment.getLinguistics().getSegmenter(); + detector = environment.getLinguistics().getDetector(); + this.environment = environment; + + Tuple2<String, Version> version = environment.getLinguistics().getVersion(Linguistics.Component.SEGMENTER); + localSegmenterBackend = version.first; + localSegmenterVersion = version.second; + } + + @Override + public QueryTree parse(Parsable query) { + indexFactsSession = indexFacts.newSession(query.getSources(), query.getRestrict()); + this.query = query; + + return buildTree(); + + } + + public QueryTree buildTree() { + Inspector inspector = SlimeUtils.jsonToSlime(this.query.getSelect().getBytes()).get(); + if (inspector.field("error_message").valid()){ + throw new QueryException("Illegal query: "+inspector.field("error_message").asString() + ", at: "+ new String(inspector.field("offending_input").asData(), StandardCharsets.UTF_8)); + } + + Item root = walkJson(inspector); + QueryTree newTree = new QueryTree(root); + + return newTree; + } + private static final String AND = "and"; + private static final String AND_NOT = "and_not"; + private static final String OR = "or"; + private static final String EQ = "equals"; + private static final String RANGE = "range"; + private static final String CONTAINS = "contains"; + private static final String MATCHES = "matches"; + private static final String CALL = "call"; + private static final List<String> FUNCTION_CALLS = Arrays.asList(WAND, WEIGHTED_SET, DOT_PRODUCT, PREDICATE, RANK, WEAK_AND); + + + public Item walkJson(Inspector inspector){ + final Item[] item = {null}; + inspector.traverse((ObjectTraverser) (key, value) -> { + + + String type = (FUNCTION_CALLS.contains(key)) ? CALL : key; + + switch (type) { + case AND: + item[0] = buildAnd(key, value); + break; + case AND_NOT: + item[0] = buildNotAnd(key, value); + break; + case OR: + item[0] = buildOr(key, value); + break; + case EQ: + item[0] = buildEquals(key, value); + break; + case RANGE: + item[0] = buildRange(key, value); + break; + case CONTAINS: + item[0] = buildTermSearch(key, value); + break; + case MATCHES: + item[0] = buildRegExpSearch(key, value); + break; + case CALL: + item[0] = buildFunctionCall(key, value); + break; + default: + throw newUnexpectedArgumentException(key, AND, CALL, CONTAINS, EQ, OR, RANGE, AND_NOT); + } + + + }); + return item[0]; + } + + + @NonNull + private Item buildFunctionCall(String key, Inspector value) { + switch (key) { + case WAND: + return buildWand(key, value); + case WEIGHTED_SET: + return buildWeightedSet(key, value); + case DOT_PRODUCT: + return buildDotProduct(key, value); + case PREDICATE: + return buildPredicate(key, value); + case RANK: + return buildRank(key, value); + case WEAK_AND: + return buildWeakAnd(key, value); + default: + throw newUnexpectedArgumentException(key, DOT_PRODUCT, RANK, WAND, WEAK_AND, WEIGHTED_SET, PREDICATE); + } + } + + + + private void addItemsFromInspector(CompositeItem item, Inspector inspector){ + if (inspector.type() == ARRAY){ + inspector.traverse((ArrayTraverser) (index, new_value) -> { + item.addItem(walkJson(new_value)); + }); + + } else if (inspector.type() == OBJECT){ + if (inspector.field("children").valid()){ + inspector.field("children").traverse((ArrayTraverser) (index, new_value) -> { + item.addItem(walkJson(new_value)); + }); + } + + } + } + + private Inspector getChildren(Inspector inspector){ + HashMap<Integer, Inspector> children = new HashMap<>(); + if (inspector.type() == ARRAY){ + return inspector; + + } else if (inspector.type() == OBJECT){ + if (inspector.field("children").valid()){ + return inspector.field("children"); + } + } + return null; + } + + + private HashMap<Integer, Inspector> getChildrenMap(Inspector inspector){ + HashMap<Integer, Inspector> children = new HashMap<>(); + if (inspector.type() == ARRAY){ + inspector.traverse((ArrayTraverser) (index, new_value) -> { + children.put(index, new_value); + }); + + } else if (inspector.type() == OBJECT){ + if (inspector.field("children").valid()){ + inspector.field("children").traverse((ArrayTraverser) (index, new_value) -> { + children.put(index, new_value); + }); + } + } + return children; + } + + private Inspector getAnnotations(Inspector inspector){ + if (inspector.type() == OBJECT && inspector.field("attributes").valid()){ + return inspector.field("attributes"); + } + return null; + } + + private HashMap<String, Inspector> getAnnotationMapFromAnnotationInspector(Inspector annotation){ + HashMap<String, Inspector> attributes = new HashMap<>(); + if (annotation.type() == OBJECT){ + annotation.traverse((ObjectTraverser) (index, new_value) -> { + attributes.put(index, new_value); + }); + } + return attributes; + } + + private HashMap<String, Inspector> getAnnotationMap(Inspector inspector){ + HashMap<String, Inspector> attributes = new HashMap<>(); + if (inspector.type() == OBJECT && inspector.field("attributes").valid()){ + inspector.field("attributes").traverse((ObjectTraverser) (index, new_value) -> { + attributes.put(index, new_value); + }); + } + return attributes; + } + + private <T> T getAnnotation(String annotationName, HashMap<String, Inspector> annotations, Class<T> expectedClass, T defaultValue) { + return (annotations.get(annotationName) == null) ? defaultValue : expectedClass.cast(annotations.get(annotationName).asString()); + } + + private Inspector getAnnotationAsInspectorOrNull(String annotationName, HashMap<String, Inspector> annotations) { + return annotations.get(annotationName); + } + + @NonNull + private CompositeItem buildAnd(String key, Inspector value) { + AndItem andItem = new AndItem(); + addItemsFromInspector(andItem, value); + + return andItem; + } + + @NonNull + private CompositeItem buildNotAnd(String key, Inspector value) { + NotItem notItem = new NotItem(); + addItemsFromInspector(notItem, value); + + return notItem; + } + + + + @NonNull + private CompositeItem buildOr(String key, Inspector value) { + OrItem orItem = new OrItem(); + addItemsFromInspector(orItem, value); + return orItem; + } + + @NonNull + private CompositeItem buildWeakAnd(String key, Inspector value) { + WeakAndItem weakAnd = new WeakAndItem(); + addItemsFromInspector(weakAnd, value); + Inspector annotations = getAnnotations(value); + + if (annotations != null){ + annotations.traverse((ObjectTraverser) (annotation_name, annotation_value) -> { + if (TARGET_NUM_HITS.equals(annotation_name)){ + weakAnd.setN((Integer.class.cast(annotation_value.asDouble()))); + } + if (SCORE_THRESHOLD.equals(annotation_name)){ + weakAnd.setScoreThreshold((Integer.class.cast(annotation_value.asDouble()))); + } + }); + } + + return weakAnd; + } + + @NonNull + private <T extends TaggableItem> T leafStyleSettings(Inspector annotations, @NonNull T out) { + { + if (annotations != null) { + annotations.traverse((ObjectTraverser) (annotation_name, annotation_value) -> { + if (SIGNIFICANCE.equals(annotation_name)) { + if (annotation_value != null) { + out.setSignificance(annotation_value.asDouble()); + } + } + if (UNIQUE_ID.equals(annotation_name)) { + if (annotation_value != null) { + out.setUniqueID(Integer.class.cast(annotation_name)); + identifiedItems.put(Integer.class.cast(annotation_name), out); + } + } + }); + } + } + { + Item leaf = (Item) out; + if (annotations != null) { + Inspector itemAnnotations = getAnnotationAsInspectorOrNull(ANNOTATIONS, getAnnotationMapFromAnnotationInspector(annotations)); + if (itemAnnotations != null) { + itemAnnotations.traverse((ObjectTraverser) (key, value) -> { + leaf.addAnnotation(key, value.asString()); + }); + } + + annotations.traverse((ObjectTraverser) (annotation_name, annotation_value) -> { + if (FILTER.equals(annotation_name)) { + if (annotation_value != null) { + leaf.setFilter(annotation_value.asBool()); + } + } + if (RANKED.equals(annotation_name)) { + if (annotation_value != null) { + leaf.setRanked(annotation_value.asBool()); + } + } + if (LABEL.equals(annotation_name)) { + if (annotation_value != null) { + leaf.setLabel(annotation_value.asString()); + } + } + if (WEIGHT.equals(annotation_name)) { + if (annotation_value != null) { + leaf.setWeight((int)annotation_value.asDouble()); + } + } + }); + } + if (out instanceof IntItem && annotations != null) { + IntItem number = (IntItem) out; + Integer hitLimit = getCappedRangeSearchParameter(annotations); + if (hitLimit != null) { + number.setHitLimit(hitLimit); + } + + } + } + + return out; + } + + + private Integer getCappedRangeSearchParameter(Inspector annotations) { + final Integer[] hitLimit = {null}; + annotations.traverse((ObjectTraverser) (annotation_name, annotation_value) -> { + if (HIT_LIMIT.equals(annotation_name)) { + if (annotation_value != null) { + hitLimit[0] = Integer.class.cast(annotation_value); + } + } + }); + final Boolean[] ascending = {null}; + final Boolean[] descending = {null}; + + if (hitLimit[0] != null) { + annotations.traverse((ObjectTraverser) (annotation_name, annotation_value) -> { + if (ASCENDING_HITS_ORDER.equals(annotation_name)) { + ascending[0] = Boolean.class.cast(annotation_value); + } + if (DESCENDING_HITS_ORDER.equals(annotation_name)) { + descending[0] = Boolean.class.cast(annotation_value); + } + + }); + Preconditions.checkArgument(ascending[0] == null || descending[0] == null, + "Settings for both ascending and descending ordering set, only one of these expected."); + + if (Boolean.TRUE.equals(descending[0]) || Boolean.FALSE.equals(ascending[0])) { + hitLimit[0] = hitLimit[0] * -1; + } + } + return hitLimit[0]; + } + + + + @NonNull + private Item buildRange(String key, Inspector value) { + HashMap<Integer, Inspector> children = getChildrenMap(value); + Inspector annotations = getAnnotations(value); + + final boolean[] equals = {false}; + + String field; + Inspector boundInspector; + if (children.get(0).type() == STRING){ + field = children.get(0).asString(); + boundInspector = children.get(1); + } else { + field = children.get(1).asString(); + boundInspector = children.get(0); + } + + final Number[] bounds = {null, null}; + final String[] operators = {null, null}; + boundInspector.traverse((ObjectTraverser) (operator, bound) -> { + if (operator.equals("=")) { + bounds[0] = Number.class.cast(bound.asLong()); + operators[0] = operator; + equals[0] = true; + } + if (operator.equals(">=") || operator.equals(">")){ + bounds[0] = Number.class.cast(bound.asLong()); + operators[0] = operator; + } else if (operator.equals("<=") || operator.equals("<")){ + bounds[1] = Number.class.cast(bound.asLong()); + operators[1] = operator; + } + + }); + IntItem range = null; + if (equals[0]){ + range = new IntItem(bounds[0].toString(), field); + } else if (operators[0]==null || operators[1]==null){ + Integer index = (operators[0] == null) ? 1 : 0; + switch (operators[index]){ + case ">=": + range = buildGreaterThanOrEquals(field, bounds[index].toString()); + break; + case ">": + range = buildGreaterThan(field, bounds[index].toString()); + break; + case "<": + range = buildLessThan(field, bounds[index].toString()); + break; + case "<=": + range = buildLessThanOrEquals(field, bounds[index].toString()); + break; + } + } + else { + range = instantiateRangeItem(bounds[0], bounds[1], field, operators[0].equals(">"), operators[1].equals("<")); + } + + return leafStyleSettings(annotations, range); + } + + @NonNull + private IntItem buildGreaterThanOrEquals(String field, String bound) { + return new IntItem("[" + bound + ";]", field); + + } + + @NonNull + private IntItem buildLessThanOrEquals(String field, String bound) { + return new IntItem("[;" + bound + "]", field); + } + + @NonNull + private IntItem buildGreaterThan(String field, String bound) { + return new IntItem(">" + bound, field); + + } + + @NonNull + private IntItem buildLessThan(String field, String bound) { + return new IntItem("<" + bound, field); + } + + /* + @NonNull + private IntItem buildEquals(OperatorNode<ExpressionOperator> ast) { + IntItem number = new IntItem(fetchConditionWord(ast), fetchConditionIndex(ast)); + if (isIndexOnLeftHandSide(ast)) { + return leafStyleSettings(ast.getArgument(1, OperatorNode.class), number); + } else { + return leafStyleSettings(ast.getArgument(0, OperatorNode.class), number); + } + }*/ + + @NonNull + private IntItem instantiateRangeItem(Number lowerBound, Number upperBound, String field, boolean bounds_left_open, boolean bounds_right_open) { + Preconditions.checkArgument(lowerBound != null && upperBound != null && field != null, + "Expected 3 NonNull-arguments"); + + if (!bounds_left_open && !bounds_right_open) { + return new RangeItem(lowerBound, upperBound, field); + } else { + Limit from; + Limit to; + if (bounds_left_open && bounds_right_open) { + from = new Limit(lowerBound, false); + to = new Limit(upperBound, false); + } else if (bounds_left_open) { + from = new Limit(lowerBound, false); + to = new Limit(upperBound, true); + } else { + from = new Limit(lowerBound, true); + to = new Limit(upperBound, false); + } + return new IntItem(from, to, field); + } + } + + + @NonNull + private Item buildEquals(String key, Inspector value) { + return buildRange(key, value); + } + + + @NonNull + private Item buildWand(String key, Inspector value) { + HashMap<String, Inspector> annotations = getAnnotationMap(value); + HashMap<Integer, Inspector> children = getChildrenMap(value); + + Preconditions.checkArgument(children.size() == 2, "Expected 2 arguments, got %s.", children.size()); + Integer target_num_hits= getAnnotation(TARGET_NUM_HITS, annotations, Integer.class, DEFAULT_TARGET_NUM_HITS); + + WandItem out = new WandItem(children.get(0).asString(), target_num_hits); + + Double scoreThreshold = getAnnotation(SCORE_THRESHOLD, annotations, Double.class, null); + if (scoreThreshold != null) { + out.setScoreThreshold(scoreThreshold); + } + + Double thresholdBoostFactor = getAnnotation(THRESHOLD_BOOST_FACTOR, annotations, Double.class, null); + if (thresholdBoostFactor != null) { + out.setThresholdBoostFactor(thresholdBoostFactor); + } + return fillWeightedSet(value, children, out); + } + + @NonNull + private WeightedSetItem fillWeightedSet(Inspector value, HashMap<Integer, Inspector> children, @NonNull WeightedSetItem out) { + addItems(children, out); + + return leafStyleSettings(getAnnotations(value), out); + } + + + private static void addItems(HashMap<Integer, Inspector> children, WeightedSetItem out) { + switch (children.get(1).type()) { + case OBJECT: + addStringItems(children, out); + break; + case ARRAY: + addLongItems(children, out); + break; + default: + throw newUnexpectedArgumentException(children.get(1).type(), ARRAY, OBJECT); + } + } + + private static void addStringItems(HashMap<Integer, Inspector> children, WeightedSetItem out) { + //{"a":1, "b":2} + children.get(1).traverse((ObjectTraverser) (key, value) -> out.addToken(key, Integer.class.cast(value.asLong()))); + } + + private static void addLongItems(HashMap<Integer, Inspector> children, WeightedSetItem out) { + //[[11,1], [37,2]] + children.get(1).traverse((ArrayTraverser) (index, pair) -> { + List<Integer> pairValues = new ArrayList<>(); + pair.traverse((ArrayTraverser) (pairIndex, pairValue) -> { + pairValues.add(Integer.class.cast(pairValue.asLong())); + }); + Preconditions.checkArgument(pairValues.size() == 2, + "Expected item and weight, got %s.", pairValues); + out.addToken(pairValues.get(0).longValue(), pairValues.get(1)); + }); + } + + + @NonNull + private Item buildRegExpSearch(String key, Inspector value) { + assertHasOperator(key, MATCHES); + HashMap<Integer, Inspector> children = getChildrenMap(value); + String field = children.get(0).asString(); + String wordData = children.get(1).asString(); + RegExpItem regExp = new RegExpItem(field, true, wordData); + return leafStyleSettings(getAnnotations(value), regExp); + } + + @NonNull + private Item buildWeightedSet(String key, Inspector value) { + HashMap<Integer, Inspector> children = getChildrenMap(value); + String field = children.get(0).asString(); + Preconditions.checkArgument(children.size() == 2, "Expected 2 arguments, got %s.", children.size()); + return fillWeightedSet(value, children, new WeightedSetItem(field)); + } + + @NonNull + private Item buildDotProduct(String key, Inspector value) { + HashMap<Integer, Inspector> children = getChildrenMap(value); + String field = children.get(0).asString(); + Preconditions.checkArgument(children.size() == 2, "Expected 2 arguments, got %s.", children.size()); + return fillWeightedSet(value, children, new DotProductItem(field)); + } + + @NonNull + private Item buildPredicate(String key, Inspector value) { + HashMap<Integer, Inspector> children = getChildrenMap(value); + String field = children.get(0).asString(); + Inspector args = children.get(1); + + Preconditions.checkArgument(children.size() == 3, "Expected 3 arguments, got %s.", children.size()); + + PredicateQueryItem item = new PredicateQueryItem(); + item.setIndexName(field); + + List<Inspector> argumentList = valueListFromInspector(getChildren(value)); + + // Adding attributes + argumentList.get(1).traverse((ObjectTraverser) (attrKey, attrValue) -> item.addFeature(attrKey, attrValue.asString())); + + // Adding range attributes + argumentList.get(2).traverse((ObjectTraverser) (attrKey, attrValue) -> item.addRangeFeature(attrKey, Integer.class.cast(attrValue.asDouble()))); + + return leafStyleSettings(getAnnotations(value), item); + } + + + @NonNull + private CompositeItem buildRank(String key, Inspector value) { + RankItem rankItem = new RankItem(); + addItemsFromInspector(rankItem, value); + return rankItem; + } + + @NonNull + private Item buildTermSearch(String key, Inspector value) { + HashMap<Integer, Inspector> children = getChildrenMap(value); + String field = children.get(0).asString(); + + return instantiateLeafItem(field, key, value); + } + @NonNull + private Item instantiateLeafItem(String field, String key, Inspector value) { + if (CALL.contains(key)) { + return instantiateCompositeLeaf(field, key, value); + } else { + return instantiateWordItem(field, key, value); + } + } + + @NonNull + private Item instantiateCompositeLeaf(String field, String key, Inspector value) { + switch (key) { + case SAME_ELEMENT: + return instantiateSameElementItem(field, key, value); + case PHRASE: + return instantiatePhraseItem(field, key, value); + case NEAR: + return instantiateNearItem(field, key, value); + case ONEAR: + return instantiateONearItem(field, key, value); + case EQUIV: + return instantiateEquivItem(field, key, value); + case ALTERNATIVES: + return instantiateWordAlternativesItem(field, key, value); + default: + throw newUnexpectedArgumentException(key, EQUIV, NEAR, ONEAR, PHRASE, SAME_ELEMENT); + } + } + + + @NonNull + private Item instantiateWordItem(String field, String key, Inspector value) { + String wordData = getChildrenMap(value).get(1).asString(); + return instantiateWordItem(field, wordData, key, value, false, decideParsingLanguage(value, wordData)); + } + + @NonNull + private Item instantiateWordItem(String field, String rawWord, String key, Inspector value, boolean exactMatch, Language language) { + String wordData = rawWord; + HashMap<String, Inspector> annotations = getAnnotationMap(value); + + if (getAnnotation(NFKC, annotations, Boolean.class, Boolean.FALSE)) { + // NOTE: If this is set to FALSE (default), we will still NFKC normalize text data + // during tokenization/segmentation, as that is always turned on also on the indexing side. + wordData = normalizer.normalize(wordData); + } + boolean fromQuery = getAnnotation(IMPLICIT_TRANSFORMS, annotations, Boolean.class, Boolean.TRUE); + boolean prefixMatch = getAnnotation(PREFIX, annotations, Boolean.class, Boolean.FALSE); + boolean suffixMatch = getAnnotation(SUFFIX, annotations, Boolean.class, Boolean.FALSE); + boolean substrMatch = getAnnotation(SUBSTRING,annotations, Boolean.class, Boolean.FALSE); + + Preconditions.checkArgument((prefixMatch ? 1 : 0) + + (substrMatch ? 1 : 0) + (suffixMatch ? 1 : 0) < 2, + "Only one of prefix, substring and suffix can be set."); + @NonNull + final TaggableItem wordItem; + + if (exactMatch) { + wordItem = new ExactStringItem(wordData, fromQuery); + } else if (prefixMatch) { + wordItem = new PrefixItem(wordData, fromQuery); + } else if (suffixMatch) { + wordItem = new SuffixItem(wordData, fromQuery); + } else if (substrMatch) { + wordItem = new SubstringItem(wordData, fromQuery); + } else { + wordItem = new WordItem(wordData, fromQuery); + } + + if (wordItem instanceof WordItem) { + prepareWord(field, value, (WordItem) wordItem); + } + if (language != Language.ENGLISH) // mark the language used, unless it's the default + ((Item)wordItem).setLanguage(language); + + return (Item) leafStyleSettings(getAnnotations(value), wordItem); + } + + + private Language decideParsingLanguage(Inspector value, String wordData) { + String languageTag = getAnnotation(USER_INPUT_LANGUAGE, getAnnotationMap(value), String.class, null); + + Language language = Language.fromLanguageTag(languageTag); + if (language != Language.UNKNOWN) return language; + + Optional<Language> explicitLanguage = query.getExplicitLanguage(); + if (explicitLanguage.isPresent()) return explicitLanguage.get(); + + return Language.ENGLISH; + } + + + private void prepareWord(String field, Inspector value, WordItem wordItem) { + wordItem.setIndexName(field); + wordStyleSettings(value, wordItem); + } + + private void wordStyleSettings(Inspector value, WordItem out) { + HashMap<String, Inspector> annotations = getAnnotationMap(value); + + Substring origin = getOrigin(getAnnotations(value)); + if (origin != null) { + out.setOrigin(origin); + } + + Boolean usePositionData = Boolean.getBoolean(getAnnotation(USE_POSITION_DATA, annotations, String.class, null)); + if (usePositionData != null) { + out.setPositionData(usePositionData); + } + Boolean stem = Boolean.getBoolean(getAnnotation(STEM, annotations, String.class, null)); + if (stem != null) { + out.setStemmed(!stem); + } + Boolean normalizeCase = Boolean.getBoolean(getAnnotation(NORMALIZE_CASE, annotations, String.class, null)); + if (normalizeCase != null) { + out.setLowercased(!normalizeCase); + } + Boolean accentDrop = Boolean.getBoolean(getAnnotation(ACCENT_DROP, annotations, String.class, null)); + if (accentDrop != null) { + out.setNormalizable(accentDrop); + } + Boolean andSegmenting = Boolean.getBoolean(getAnnotation(AND_SEGMENTING, annotations, String.class, null)); + if (andSegmenting != null) { + if (andSegmenting) { + out.setSegmentingRule(SegmentingRule.BOOLEAN_AND); + } else { + out.setSegmentingRule(SegmentingRule.PHRASE); + } + } + } + + private Substring getOrigin(Inspector annotations) { + if (annotations != null) { + Inspector origin = getAnnotationAsInspectorOrNull(ORIGIN, getAnnotationMapFromAnnotationInspector(annotations)); + if (origin == null) { + return null; + } + final String[] original = {null}; + final Integer[] offset = {null}; + final Integer[] length = {null}; + + origin.traverse((ObjectTraverser) (key, value) -> { + switch (key) { + case (ORIGIN_ORIGINAL): + original[0] = value.asString(); + break; + case (ORIGIN_OFFSET): + offset[0] = (int) value.asDouble(); + break; + case (ORIGIN_LENGTH): + length[0] = (int) value.asDouble(); + break; + } + + + }); + return new Substring(offset[0], length[0] + offset[0], original[0]); + } + return null; + } + + @NonNull + private Item instantiateSameElementItem(String field, String key, Inspector value) { + assertHasOperator(key, SAME_ELEMENT); + + SameElementItem sameElement = new SameElementItem(field); + // All terms below sameElement are relative to this. + getChildren(value).traverse((ArrayTraverser) (index, term) -> { + sameElement.addItem(walkJson(term)); + }); + + return sameElement; + } + + @NonNull + private Item instantiatePhraseItem(String field, String key, Inspector value) { + assertHasOperator(key, PHRASE); + HashMap<String, Inspector> annotations = getAnnotationMap(value); + + PhraseItem phrase = new PhraseItem(); + phrase.setIndexName(field); + HashMap<Integer, Inspector> children = getChildrenMap(value); + + for (Inspector word : children.values()){ + phrase.addItem(instantiateWordItem(word.asString(), key, value)); + + } + return leafStyleSettings(getAnnotations(value), phrase); + } + + + + @NonNull + private Item instantiateNearItem(String field, String key, Inspector value) { + assertHasOperator(key, NEAR); + + NearItem near = new NearItem(); + near.setIndexName(field); + + HashMap<Integer, Inspector> children = getChildrenMap(value); + + for (Inspector word : children.values()){ + near.addItem(instantiateWordItem(word.asString(), key, value)); + } + + Integer distance = getAnnotation(DISTANCE, getAnnotationMap(value), Integer.class, null); + + if (distance != null) { + near.setDistance(distance); + } + return near; + } + + @NonNull + private Item instantiateONearItem(String field, String key, Inspector value) { + assertHasOperator(key, ONEAR); + + NearItem onear = new ONearItem(); + onear.setIndexName(field); + HashMap<Integer, Inspector> children = getChildrenMap(value); + + for (Inspector word : children.values()){ + onear.addItem(instantiateWordItem(word.asString(), key, value)); + } + + Integer distance = getAnnotation(DISTANCE, getAnnotationMap(value), Integer.class, null); + if (distance != null) { + onear.setDistance(distance); + } + return onear; + } + + + @NonNull + private Item instantiateEquivItem(String field, String key, Inspector value) { + + HashMap<Integer, Inspector> children = getChildrenMap(value); + Preconditions.checkArgument(children.size() >= 2, "Expected 2 or more arguments, got %s.", children.size()); + + EquivItem equiv = new EquivItem(); + equiv.setIndexName(field); + + for (Inspector word : children.values()){ + if (word.type() == STRING || word.type() == LONG || word.type() == DOUBLE){ + equiv.addItem(instantiateWordItem(word.asString(), key, value)); + } + if (word.type() == OBJECT){ + word.traverse((ObjectTraverser) (key2, value2) -> { + assertHasOperator(key2, PHRASE); + equiv.addItem(instantiatePhraseItem(word.asString(), key, value2)); + }); + } + } + + return leafStyleSettings(getAnnotations(value), equiv); + } + + private Item instantiateWordAlternativesItem(String field, String key, Inspector value) { + HashMap<Integer, Inspector> children = getChildrenMap(value); + Preconditions.checkArgument(children.size() >= 1, "Expected 1 or more arguments, got %s.", children.size()); + Preconditions.checkArgument(children.get(0).type() == OBJECT, "Expected OBJECT, got %s.", children.get(0).type()); + + List<WordAlternativesItem.Alternative> terms = new ArrayList<>(); + + children.get(0).traverse((ObjectTraverser) (keys, values) -> { + terms.add(new WordAlternativesItem.Alternative(keys, values.asDouble())); + }); + return leafStyleSettings(getAnnotations(value), new WordAlternativesItem(field, Boolean.TRUE, null, terms)); + } + + + + + + @NonNull + private String getIndex(String field) { + Preconditions.checkArgument(indexFactsSession.isIndex(field), "Field '%s' does not exist.", field); + //return indexFactsSession.getCanonicName(field); + return field; + } + + + + private static void assertHasOperator(String key, String expectedKey) { + Preconditions.checkArgument(key.equals(expectedKey), "Expected operator class %s, got %s.", expectedKey, key); + } + + + private static IllegalArgumentException newUnexpectedArgumentException(Object actual, Object... expected) { + StringBuilder out = new StringBuilder("Expected "); + for (int i = 0, len = expected.length; i < len; ++i) { + out.append(expected[i]); + if (i < len - 2) { + out.append(", "); + } else if (i < len - 1) { + out.append(" or "); + } + } + out.append(", got ").append(actual).append("."); + return new IllegalArgumentException(out.toString()); + } + + private List<Inspector> valueListFromInspector(Inspector inspector){ + List<Inspector> inspectorList = new ArrayList<>(); + inspector.traverse((ArrayTraverser) (key, value) -> inspectorList.add(value)); + return inspectorList; + } + + +} diff --git a/container-search/src/main/java/com/yahoo/search/query/parser/Parsable.java b/container-search/src/main/java/com/yahoo/search/query/parser/Parsable.java index e5941a90b83..4802e627f4d 100644 --- a/container-search/src/main/java/com/yahoo/search/query/parser/Parsable.java +++ b/container-search/src/main/java/com/yahoo/search/query/parser/Parsable.java @@ -3,6 +3,7 @@ package com.yahoo.search.query.parser; import com.yahoo.language.Language; import com.yahoo.search.query.Model; +import com.yahoo.search.query.Select; import java.util.Collection; import java.util.HashSet; @@ -36,6 +37,7 @@ public final class Parsable { private String defaultIndexName; private Language language; // TODO: Initialize to UNKNOWN private Optional<Language> explicitLanguage = Optional.empty(); + private String where; /** If this is set it will be used to determine the language, if not set explicitly */ private Optional<Model> model = Optional.empty(); @@ -133,6 +135,15 @@ public final class Parsable { return this; } + public Parsable setSelect(Select select){ + this.where = select.getWhere(); + return this; + } + + public String getSelect(){ + return this.where; + } + public static Parsable fromQueryModel(Model model) { return new Parsable() .setModel(model) @@ -141,7 +152,11 @@ public final class Parsable { .setExplicitLanguage(Optional.ofNullable(model.getLanguage())) .setDefaultIndexName(model.getDefaultIndex()) .addSources(model.getSources()) - .addRestricts(model.getRestrict()); + .addRestricts(model.getRestrict()) + .setSelect(model.getParent().getSelect()); } + + + } diff --git a/container-search/src/main/java/com/yahoo/search/query/parser/ParserFactory.java b/container-search/src/main/java/com/yahoo/search/query/parser/ParserFactory.java index 8d008abaac2..69d46527255 100644 --- a/container-search/src/main/java/com/yahoo/search/query/parser/ParserFactory.java +++ b/container-search/src/main/java/com/yahoo/search/query/parser/ParserFactory.java @@ -3,6 +3,7 @@ package com.yahoo.search.query.parser; import com.yahoo.prelude.query.parser.*; import com.yahoo.search.Query; +import com.yahoo.search.query.SelectParser; import com.yahoo.search.yql.YqlParser; /** @@ -40,6 +41,8 @@ public final class ParserFactory { return new ProgrammaticParser(); case YQL: return new YqlParser(environment); + case SELECT: + return new SelectParser(environment); default: throw new UnsupportedOperationException(type.toString()); } diff --git a/container-search/src/main/java/com/yahoo/search/query/properties/QueryProperties.java b/container-search/src/main/java/com/yahoo/search/query/properties/QueryProperties.java index 0aea5e96161..286062db755 100644 --- a/container-search/src/main/java/com/yahoo/search/query/properties/QueryProperties.java +++ b/container-search/src/main/java/com/yahoo/search/query/properties/QueryProperties.java @@ -113,6 +113,10 @@ public class QueryProperties extends Properties { if (key.get(1).equals(Ranking.PROPERTIES)) return ranking.getProperties().get(key.rest().rest().toString()); } } + else if (key.size()==2 && key.first().equals(Select.SELECT)) { + if (key.last().equals(Select.WHERE)) return query.getSelect().getWhere(); + if (key.last().equals(Select.GROUPING)) return query.getSelect().getGrouping(); + } else if (key.size()==2 && key.first().equals(Presentation.PRESENTATION)) { if (key.last().equals(Presentation.BOLDING)) return query.getPresentation().getBolding(); if (key.last().equals(Presentation.SUMMARY)) return query.getPresentation().getSummary(); @@ -247,6 +251,15 @@ public class QueryProperties extends Properties { else if ( ! key.last().equals(Presentation.REPORT_COVERAGE)) // TODO: Change this line to "else" on Vespa 7 throwIllegalParameter(key.last(), Presentation.PRESENTATION); } + else if (key.size()==2 && key.first().equals(Select.SELECT)) { + if (key.last().equals(Select.WHERE)){ + query.getSelect().setWhere(asString(value, "")); + query.getModel().setType(asString(value, "SELECT")); + } else if (key.last().equals(Select.GROUPING)) { + query.getSelect().setGrouping(asString(value, "")); + query.getModel().setType(asString(value, "SELECT")); + } + } else if (key.first().equals("rankfeature") || key.first().equals("featureoverride") ) { // featureoverride is deprecated setRankingFeature(query, key.rest().toString(), toSpecifiedType(key.rest().toString(), value, profileRegistry.getTypeRegistry().getComponent("features"))); } else if (key.first().equals("rankproperty")) { diff --git a/container-search/src/test/java/com/yahoo/search/handler/test/JSONSearchHandlerTestCase.java b/container-search/src/test/java/com/yahoo/search/handler/test/JSONSearchHandlerTestCase.java index eea58d5444e..e85a945cc67 100644 --- a/container-search/src/test/java/com/yahoo/search/handler/test/JSONSearchHandlerTestCase.java +++ b/container-search/src/test/java/com/yahoo/search/handler/test/JSONSearchHandlerTestCase.java @@ -11,7 +11,10 @@ import com.yahoo.io.IOUtils; import com.yahoo.net.HostName; import com.yahoo.search.handler.SearchHandler; import com.yahoo.search.searchchain.config.test.SearchChainConfigurerTestCase; +import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Inspector; +import com.yahoo.slime.ObjectTraverser; +import com.yahoo.slime.Type; import com.yahoo.vespa.config.SlimeUtils; import org.json.JSONObject; import org.junit.After; @@ -21,8 +24,10 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Map; import java.util.HashMap; +import java.util.stream.Collectors; import static com.yahoo.jdisc.http.HttpRequest.Method.GET; import static org.hamcrest.CoreMatchers.containsString; @@ -338,6 +343,35 @@ public class JSONSearchHandlerTestCase { } + @Test + public void testSelectParameter() throws Exception { + JSONObject json = new JSONObject(); + + JSONObject select = new JSONObject(); + + JSONObject where = new JSONObject(); + where.put("where", "where"); + + JSONObject grouping = new JSONObject(); + grouping.put("grouping", "grouping"); + + select.put("where", where); + select.put("grouping", grouping); + + json.put("select", select); + + + // Create mapping + Inspector inspector = SlimeUtils.jsonToSlime(json.toString().getBytes("utf-8")).get(); + Map<String, String> map = new HashMap<>(); + searchHandler.createRequestMapping(inspector, map, ""); + + JSONObject processedWhere = new JSONObject(map.get("select.where")); + assertEquals(where.toString(), processedWhere.toString()); + + JSONObject processedGrouping = new JSONObject(map.get("select.grouping")); + assertEquals(grouping.toString(), processedGrouping.toString()); + } @Test public void testRequestMapping() throws Exception { diff --git a/container-search/src/test/java/com/yahoo/select/SelectParserTestCase.java b/container-search/src/test/java/com/yahoo/select/SelectParserTestCase.java new file mode 100644 index 00000000000..324bd8e4387 --- /dev/null +++ b/container-search/src/test/java/com/yahoo/select/SelectParserTestCase.java @@ -0,0 +1,404 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.select; + +import com.yahoo.prelude.query.Item; +import com.yahoo.prelude.query.Substring; +import com.yahoo.prelude.query.WordItem; +import com.yahoo.search.query.QueryTree; +import com.yahoo.search.query.Select; +import com.yahoo.search.query.SelectParser; +import com.yahoo.search.query.parser.Parsable; +import com.yahoo.search.query.parser.ParserEnvironment; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +/** + * Specification for the conversion of Select expressions to Vespa search queries. + * + * @author henrhoi + */ + +public class SelectParserTestCase { + + private final SelectParser parser = new SelectParser(new ParserEnvironment()); + + @Test + public void test_contains() throws Exception { + JSONObject json = new JSONObject(); + List<String> contains = Arrays.asList("default", "henrik"); + json.put("contains", contains); + + System.out.println(json.toString()); + assertParse(json.toString(), "default:henrik"); + } + + @Test + public void test() { + assertParse("{'contains' : ['title', 'madonna']}", + "title:madonna"); + } + + + @Test + public void testDottedFieldNames() { + assertParse("{ 'contains' : ['my.nested.title', 'madonna']}", + "my.nested.title:madonna"); + } + + + + @Test + public void testOr() throws Exception { + JSONObject json_two_or = new JSONObject(); + JSONObject json_three_or = new JSONObject(); + List<String> contains1 = Arrays.asList("title", "madonna"); + List<String> contains2 = Arrays.asList("title", "saint"); + List<String> contains3 = Arrays.asList("title", "angel"); + + JSONObject contains_json1 = new JSONObject(); + JSONObject contains_json2 = new JSONObject(); + JSONObject contains_json3 = new JSONObject(); + contains_json1.put("contains", contains1); + contains_json2.put("contains", contains2); + contains_json3.put("contains", contains3); + + json_two_or.put("or", Arrays.asList(contains_json1, contains_json2)); + json_three_or.put("or", Arrays.asList(contains_json1, contains_json2, contains_json3)); + + assertParse(json_two_or.toString(), "OR title:madonna title:saint"); + assertParse(json_three_or.toString(), "OR title:madonna title:saint title:angel"); + } + + @Test + public void testAnd() throws Exception{ + JSONObject json_two_and = new JSONObject(); + JSONObject json_three_and = new JSONObject(); + List<String> contains1 = Arrays.asList("title", "madonna"); + List<String> contains2 = Arrays.asList("title", "saint"); + List<String> contains3 = Arrays.asList("title", "angel"); + + JSONObject contains_json1 = new JSONObject(); + JSONObject contains_json2 = new JSONObject(); + JSONObject contains_json3 = new JSONObject(); + contains_json1.put("contains", contains1); + contains_json2.put("contains", contains2); + contains_json3.put("contains", contains3); + + json_two_and.put("and", Arrays.asList(contains_json1, contains_json2)); + json_three_and.put("and", Arrays.asList(contains_json1, contains_json2, contains_json3)); + + assertParse(json_two_and.toString(), "AND title:madonna title:saint"); + assertParse(json_three_and.toString(), "AND title:madonna title:saint title:angel"); + } + + @Test + public void testAndNot() throws JSONException { + JSONObject json_and_not = new JSONObject(); + List<String> contains1 = Arrays.asList("title", "madonna"); + List<String> contains2 = Arrays.asList("title", "saint"); + + JSONObject contains_json1 = new JSONObject(); + JSONObject contains_json2 = new JSONObject(); + contains_json1.put("contains", contains1); + contains_json2.put("contains", contains2); + + json_and_not.put("and_not", Arrays.asList(contains_json1, contains_json2)); + + System.out.println(json_and_not.toString()); + assertParse(json_and_not.toString(), + "+title:madonna -title:saint"); + } + + + @Test + public void testLessThan() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put("<", 500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:<500"); + } + + @Test + public void testGreaterThan() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put(">", 500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:>500"); + } + + + @Test + public void testLessThanOrEqual() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put("<=", 500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:[;500]"); + } + + @Test + public void testGreaterThanOrEqual() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put(">=", 500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:[500;]"); + } + + @Test + public void testEquality() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put("=", 500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:500"); + } + + @Test + public void testNegativeLessThan() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put("<", -500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:<-500"); + } + + @Test + public void testNegativeGreaterThan() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put(">", -500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:>-500"); + } + + @Test + public void testNegativeLessThanOrEqual() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put("<=", -500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:[;-500]"); + } + + @Test + public void testNegativeGreaterThanOrEqual() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put(">=", -500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:[-500;]"); + } + + @Test + public void testNegativeEquality() throws JSONException { + JSONObject range_json = new JSONObject(); + JSONObject operators = new JSONObject(); + operators.put("=", -500); + + List<Object> range = Arrays.asList("price", operators); + + range_json.put("range", range); + + System.out.println(range_json.toString()); + assertParse(range_json.toString(), + "price:-500"); + } + + @Test + public void testAnnotatedLessThan() { + String jsonString = "{ \"range\": { \"children\" : [\"price\", {\"<\" : -500}], \"attributes\" : {\"filter\" : true} } }"; + System.out.println(jsonString); + + assertParse(jsonString, "|price:<-500"); + } + + @Test + public void testAnnotatedGreaterThan() { + String jsonString = "{ \"range\": { \"children\" : [\"price\", {\">\" : 500}], \"attributes\" : {\"filter\" : true} } }"; + System.out.println(jsonString); + + assertParse(jsonString, "|price:>500"); + } + + @Test + public void testAnnotatedLessThanOrEqual() { + String jsonString = "{ \"range\": { \"children\" : [\"price\", {\"<=\" : -500}], \"attributes\" : {\"filter\" : true} } }"; + System.out.println(jsonString); + + assertParse(jsonString, "|price:[;-500]"); + } + + @Test + public void testAnnotatedGreaterThanOrEqual() { + String jsonString = "{ \"range\": { \"children\" : [\"price\", {\">=\" : 500}], \"attributes\" : {\"filter\" : true} } }"; + System.out.println(jsonString); + + assertParse(jsonString, "|price:[500;]"); + } + + + @Test + public void testAnnotatedEquality() { + String jsonString = "{ \"range\": { \"children\" : [\"price\", {\"=\" : -500}], \"attributes\" : {\"filter\" : true} } }"; + System.out.println(jsonString); + + assertParse(jsonString, "|price:-500"); + } + + @Test + public void testTermAnnotations() { + assertEquals("merkelapp", + getRootWord("{ \"contains\": { \"children\" : [\"baz\", \"colors\"], \"attributes\" : {\"label\" : \"merkelapp\"} } }").getLabel()); + assertEquals("another", + getRootWord("{ \"contains\": { \"children\" : [\"baz\", \"colors\"], \"attributes\" : {\"annotations\" : {\"cox\" : \"another\"} } } }").getAnnotation("cox")); + assertEquals(23.0, getRootWord("{ \"contains\": { \"children\" : [\"baz\", \"colors\"], \"attributes\" : {\"significance\" : 23.0 } } }").getSignificance(), 1E-6); + assertEquals(150, getRootWord("{ \"contains\": { \"children\" : [\"baz\", \"colors\"], \"attributes\" : {\"weight\" : 150 } } }").getWeight()); + assertFalse(getRootWord("{ \"contains\": { \"children\" : [\"baz\", \"colors\"], \"attributes\" : {\"usePositionData\" : false } } }").usePositionData()); + assertTrue(getRootWord("{ \"contains\": { \"children\" : [\"baz\", \"colors\"], \"attributes\" : {\"filter\" : true } } }").isFilter()); + assertFalse(getRootWord("{ \"contains\": { \"children\" : [\"baz\", \"colors\"], \"attributes\" : {\"ranked\" : false } } }").isRanked()); + Substring origin = getRootWord("{ \"contains\": { \"children\" : [\"baz\", \"colors\"], \"attributes\" : {\"origin\": {\"original\": \"abc\", \"offset\": 1, \"length\": 2}} } }").getOrigin(); + assertEquals("abc", origin.string); + assertEquals(1, origin.start); + assertEquals(3, origin.end); + } + + + @Test + public void testSameElement() { + //System.out.println("{ \"contains\": [ \"baz\", {\"sameElement\" : [ { \"contains\" : [\"f1\", \"a\"] }, { \"contains\" : [\"f2\", \"b\"] } ]} ] }"); + // assertParse("{ \"contains\": [ \"baz\", {\"sameElement\" : [ { \"contains\" : [\"f1\", \"a\"] }, { \"contains\" : [\"f2\", \"b\"] } ]} ] }", + //"baz:{f1:a f2:b}"); + //assertParse("select foo from bar where baz contains sameElement(f1 contains \"a\", f2 = 10);", + // "baz:{f1:a f2:10}"); + //assertParse("select foo from bar where baz contains sameElement(key contains \"a\", value.f2 = 10);", + // "baz:{key:a value.f2:10}"); + } + + + + + + + + + + + + + /** Other methods */ + private WordItem getRootWord(String yqlQuery) { + Item root = parseWhere(yqlQuery).getRoot(); + assertTrue(root instanceof WordItem); + return (WordItem)root; + } + + + + + + + + + + + + + /** Assert-methods */ + private void assertParse(String where, String expectedQueryTree) { + String queryTree = parseWhere(where).toString(); + System.out.println(queryTree); + assertEquals(expectedQueryTree, queryTree); + } + + + /** Parse-methods*/ + + private QueryTree parseWhere(String where) { + Select select = new Select(where, ""); + + return parser.parse(new Parsable().setSelect(select)); + } + + private QueryTree parseGrouping(String grouping) { + Select select = new Select("", grouping); + + return parser.parse(new Parsable().setSelect(select)); + } + + private QueryTree parse(String where, String grouping) { + Select select = new Select(where, grouping); + + return parser.parse(new Parsable().setSelect(select)); + } + + + + + +} |