aboutsummaryrefslogtreecommitdiffstats
path: root/container-search/src/main/java/com/yahoo/search/query
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /container-search/src/main/java/com/yahoo/search/query
Publish
Diffstat (limited to 'container-search/src/main/java/com/yahoo/search/query')
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/Model.java521
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/ParameterParser.java88
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/Presentation.java211
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/Properties.java51
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/QueryHelper.java27
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/QueryTree.java159
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/Ranking.java246
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/SessionId.java36
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/Sorting.java407
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/context/QueryContext.java112
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/context/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/package-info.java10
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/parser/Parsable.java112
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/parser/Parser.java24
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/parser/ParserEnvironment.java76
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/parser/ParserFactory.java48
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/parser/package-info.java10
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/AllReferencesQueryProfileVisitor.java40
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/AllTypesQueryProfileVisitor.java51
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/AllUnoverridableQueryProfileVisitor.java45
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/AllValuesQueryProfileVisitor.java44
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/BackedOverridableQueryProfile.java139
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/CopyOnWriteContent.java159
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/DimensionBinding.java223
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/DimensionValues.java140
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/DumpTool.java89
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/FieldDescriptionQueryProfileVisitor.java70
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/ModelObjectMap.java26
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/OverridableQueryProfile.java51
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/PrefixQueryProfileVisitor.java63
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/QueryProfile.java835
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileCompiler.java140
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileProperties.java258
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileRegistry.java89
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVariant.java157
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVariants.java486
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVisitor.java87
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/SingleValueQueryProfileVisitor.java76
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/SubstituteString.java127
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/compiled/Binding.java128
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfile.java183
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfileRegistry.java76
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalMap.java68
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalValue.java159
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/config/QueryProfileConfigurer.java227
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/config/QueryProfileXMLReader.java366
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/config/package-info.java5
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/package-info.java12
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/types/FieldDescription.java148
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/types/FieldType.java94
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/types/PrimitiveFieldType.java86
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/types/QueryFieldType.java41
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileFieldType.java100
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileType.java355
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileTypeRegistry.java37
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/types/TensorFieldType.java59
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/types/package-info.java11
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/properties/DefaultProperties.java40
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/properties/PropertyAliases.java58
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/properties/PropertyMap.java132
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/properties/QueryProperties.java296
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/properties/QueryPropertyAliases.java33
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/properties/RequestContextProperties.java41
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/properties/SubProperties.java67
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/properties/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/ranking/Diversity.java127
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/ranking/MatchPhase.java153
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/ranking/RankFeatures.java130
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/ranking/RankProperties.java114
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/ranking/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/QueryRewriteSearcher.java423
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterConstants.java55
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterFeatures.java651
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterUtils.java334
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/SearchChainDispatcherSearcher.java74
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/GenericExpansionRewriter.java213
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/MisspellRewriter.java151
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/NameRewriter.java194
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/TextSerialize.java41
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/AndNotRestConverter.java54
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/CompositeConverter.java66
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/ExactStringConverter.java15
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/IntConverter.java20
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemArguments.java26
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemContext.java49
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemExecutorRegistry.java71
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemFormConverter.java14
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemFormHandler.java17
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemInitializer.java137
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/ListUtil.java33
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/NearConverter.java44
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/PrefixConverter.java14
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/SubStringConverter.java14
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/SuffixConverter.java14
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/TermConverter.java53
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/TypeCheck.java27
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/item/WordConverter.java20
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/parser/.gitignore7
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/parser/DispatchFormHandler.java11
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/DispatchForm.java56
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/ItemIdMapper.java33
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/QueryTreeSerializer.java16
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/Serializer.java79
106 files changed, 11947 insertions, 0 deletions
diff --git a/container-search/src/main/java/com/yahoo/search/query/Model.java b/container-search/src/main/java/com/yahoo/search/query/Model.java
new file mode 100644
index 00000000000..588580dda4d
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/Model.java
@@ -0,0 +1,521 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query;
+
+import com.yahoo.language.Language;
+import com.yahoo.language.Linguistics;
+import com.yahoo.language.LocaleFactory;
+import com.yahoo.prelude.query.CompositeItem;
+import com.yahoo.prelude.query.Item;
+import com.yahoo.prelude.query.TaggableItem;
+import com.yahoo.prelude.query.textualrepresentation.TextualQueryRepresentation;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.Query;
+import com.yahoo.search.query.parser.Parsable;
+import com.yahoo.search.query.parser.Parser;
+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.searchchain.Execution;
+
+import java.util.*;
+
+import static com.yahoo.text.Lowercase.toLowerCase;
+
+/**
+ * The parameters defining the recall of a query.
+ *
+ * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a>
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class Model 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 MODEL = "model";
+ public static final String PROGRAM = "program";
+ public static final String QUERY_STRING = "queryString";
+ public static final String TYPE = "type";
+ public static final String FILTER = "filter";
+ public static final String DEFAULT_INDEX = "defaultIndex";
+ public static final String LANGUAGE = "language";
+ public static final String ENCODING = "encoding";
+ public static final String SOURCES = "sources";
+ public static final String SEARCH_PATH = "searchPath";
+ public static final String RESTRICT = "restrict";
+
+ static {
+ argumentType =new QueryProfileType(MODEL);
+ argumentType.setStrict(true);
+ argumentType.setBuiltin(true);
+ //argumentType.addField(new FieldDescription(PROGRAM, "string", "yql")); // TODO: Custom type
+ argumentType.addField(new FieldDescription(QUERY_STRING, "string", "query"));
+ argumentType.addField(new FieldDescription(TYPE, "string", "type"));
+ argumentType.addField(new FieldDescription(FILTER, "string","filter"));
+ argumentType.addField(new FieldDescription(DEFAULT_INDEX, "string", "default-index def-idx defidx"));
+ argumentType.addField(new FieldDescription(LANGUAGE, "string", "language lang"));
+ argumentType.addField(new FieldDescription(ENCODING, "string", "encoding"));
+ argumentType.addField(new FieldDescription(SOURCES, "string", "sources search"));
+ argumentType.addField(new FieldDescription(SEARCH_PATH, "string", "searchpath"));
+ argumentType.addField(new FieldDescription(RESTRICT, "string", "restrict"));
+ argumentType.freeze();
+ argumentTypeName=new CompoundName(argumentType.getId().getName());
+ }
+
+ public static QueryProfileType getArgumentType() { return argumentType; }
+
+ /** The name of the query property used for generating hit count estimate queries. */
+ public static final CompoundName ESTIMATE = new CompoundName("hitcountestimate");
+
+ private String encoding = null;
+ private String queryString = "";
+ private String filter = null;
+ private Language language = null;
+ private Locale locale = null;
+ private QueryTree queryTree = null; // The actual query. This is lazily created from the program
+ private String defaultIndex = null;
+ private Query.Type type = Query.Type.ALL;
+ private Query parent;
+ private Set<String> sources=new LinkedHashSet<>();
+ private Set<String> restrict=new LinkedHashSet<>();
+ private String searchPath;
+ private String documentDbName = null;
+ private Execution execution=new Execution(new Execution.Context(null, null, null, null, null));
+
+ public Model(Query query) {
+ setParent(query);
+ }
+
+ /**
+ * Creates trace a message of language detection results into this Model
+ * instance's parent query. Do note this will give bogus results if the
+ * Execution instance is not set correctly. This is done automatically
+ * inside {@link Execution#search(Query)}. If tracing the same place as
+ * creating the query instance, {@link #setExecution(Execution)} has to be
+ * invoked first with the same Execution instance the query is intended to
+ * be run by.
+ */
+ public void traceLanguage() {
+ if (getParent().getTraceLevel()<2) return;
+ if (language != null) {
+ getParent().trace("Language " + getLanguage() + " specified directly as a parameter", false, 2);
+ }
+ else {
+ Language l = getParsingLanguage();
+ // Don't include the query, it will trigger query parsing
+ getParent().trace("Detected language: " + l, false, 2);
+ getParent().trace("Language " + l + " determined by " +
+ (Language.fromEncoding(encoding) != Language.UNKNOWN ? "query encoding" :
+ "the characters in the terms") + ".", false, 2);
+ }
+ }
+
+ /**
+ * Gets the language to use for parsing. If this is explicitly set, that language is returned, otherwise
+ * it is guessed from the query string. If this does not yield an actual language, English is
+ * returned as the default.
+ *
+ * @return the language determined, never null
+ */
+ public Language getParsingLanguage() {
+ Language language = getLanguage();
+ if (language != null) {
+ return language;
+ }
+ language = Language.fromEncoding(encoding);
+ if (language != Language.UNKNOWN) {
+ return language;
+ }
+ Linguistics linguistics = execution.context().getLinguistics();
+ if (linguistics != null) {
+ language = linguistics.getDetector().detect(queryString, null).getLanguage();
+ }
+ if (language != Language.UNKNOWN) {
+ return language;
+ }
+ return Language.ENGLISH;
+ }
+
+ /** Returns the explicitly set parsing language of this query model, or null if none */
+ public Language getLanguage() { return language; }
+
+ /** Explicitly sets the language to be used during parsing */
+ public void setLanguage(Language language) { this.language = language; }
+
+ /**
+ * <p>Explicitly sets the language to be used during parsing. The argument is first normalized by replacing
+ * underscores with hyphens (to support locale strings being used as RFC 5646 language tags), and then forwarded to
+ * {@link #setLocale(String)} so that the Locale information of the tag is preserved.</p>
+ *
+ * @param language The language string to parse.
+ * @see #getLanguage()
+ * @see #setLocale(String)
+ */
+ public void setLanguage(String language) {
+ setLocale(language.replace("_", "-"));
+ }
+
+ /**
+ * <p>Returns the explicitly set parsing locale of this query model, or null if none.</p>
+ *
+ * @return The locale of this.
+ * @see #setLocale(Locale)
+ */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ /**
+ * <p>Explicitly sets the locale to be used during parsing. This method also calls {@link #setLanguage(Language)}
+ * with the corresponding {@link Language} instance.</p>
+ *
+ * @param locale The locale to set.
+ * @see #getLocale()
+ * @see #setLanguage(Language)
+ */
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ setLanguage(Language.fromLocale(locale));
+ }
+
+ /**
+ * <p>Explicitly sets the locale to be used during parsing. This creates a Locale instance from the given language
+ * tag, and passes that to {@link #setLocale(Locale)}.</p>
+ *
+ * @param languageTag The language tag to parse.
+ * @see #setLocale(Locale)
+ */
+ public void setLocale(String languageTag) {
+ setLocale(LocaleFactory.fromLanguageTag(languageTag));
+ }
+
+ /** Returns the encoding used in the query as a lowercase string */
+ public String getEncoding() { return encoding; }
+
+ /** Sets the encoding which was used in the received query string */
+ public void setEncoding(String encoding) {
+ this.encoding = toLowerCase(encoding);
+ }
+
+ /** Set the path for which backend nodes to forward the search too. */
+ public void setSearchPath(String searchPath) { this.searchPath = searchPath; }
+
+ public String getSearchPath() { return searchPath; }
+
+ /**
+ * Set the query from a string. This will not be parsed into a query tree until that tree is attempted accessed.
+ * Note that setting this will clear the current query tree. Usually, this should <i>not</i> be modified -
+ * changes to the query should be implemented as modifications on the query tree structure.
+ * <p>
+ * Passing null causes this to be set to an empty string.
+ */
+ public void setQueryString(String queryString) {
+ if (queryString==null) queryString="";
+ this.queryString = queryString;
+ queryTree=null; // Cause parsing of the new query string next time the tree is accessed
+ }
+
+ /**
+ * Returns the query string which caused the original query tree of this model to come about.
+ * Note that changes to the query tree are <b>not</b> reflected in this query string.
+ *
+ * @return the original (or reassigned) query string - never null
+ */
+ public String getQueryString() { return queryString; }
+
+ /**
+ * Returns the query as an object structure.
+ * This causes parsing of the query string if it has changed since this was last called
+ * (i.e query parsing is lazy)
+ */
+ public QueryTree getQueryTree() {
+ if (queryTree == null) {
+ Parser parser = ParserFactory.newInstance(type, ParserEnvironment.fromExecutionContext(execution.context()));
+ queryTree = parser.parse(Parsable.fromQueryModel(this));
+ if (parent.getTraceLevel() >= 2) {
+ parent.trace("Query parsed to: " + parent.yqlRepresentation(), 2);
+ }
+ }
+ return queryTree;
+ }
+
+ /**
+ * Returns the filter string set for this query.
+ * The filter is included in the query tree at the time the query tree is parsed
+ */
+ public String getFilter() { return filter; }
+
+ /**
+ * Sets the filter string set for this query.
+ * The filter is included in the query tree at the time the query tree is parsed.
+ * Setting this does <i>not</i> cause the query to be reparsed.
+ */
+ public void setFilter(String filter) { this.filter = filter; }
+
+ /**
+ * Returns the default index for this query.
+ * The default index is taken into account at the time the query tree is parsed.
+ */
+ public String getDefaultIndex() { return defaultIndex; }
+
+ /**
+ * Sets the default index for this query.
+ * The default index is taken into account at the time the query tree is parsed.
+ * Setting this does <i>not</i> cause the query to be reparsed.
+ */
+ public void setDefaultIndex(String defaultIndex) { this.defaultIndex = defaultIndex; }
+
+ /**
+ * Sets the query type of for this query.
+ * The type is taken into account at the time the query tree is parsed.
+ */
+ public Query.Type getType() { return type; }
+
+ /**
+ * Sets the query type of for this query.
+ * The type is taken into account at the time the query tree is parsed.
+ * Setting this does <i>not</i> cause the query to be reparsed.
+ */
+ public void setType(Query.Type type) { this.type = type; }
+
+ /**
+ * Sets the query type of for this query.
+ * The type is taken into account at the time the query tree is parsed.
+ * Setting this does <i>not</i> cause the query to be reparsed.
+ */
+ public void setType(String typeString) { this.type = Query.Type.getType(typeString); }
+
+ public boolean equals(Object o) {
+ if ( ! (o instanceof Model)) return false;
+
+ Model other = (Model) o;
+ if ( ! (
+ QueryHelper.equals(other.encoding, this.encoding) &&
+ QueryHelper.equals(other.language, this.language) &&
+ QueryHelper.equals(other.searchPath, this.searchPath) &&
+ QueryHelper.equals(other.sources, this.sources) &&
+ QueryHelper.equals(other.restrict, this.restrict) &&
+ QueryHelper.equals(other.defaultIndex, this.defaultIndex) &&
+ QueryHelper.equals(other.type, this.type) ))
+ return false;
+
+ if (other.queryTree == null && this.queryTree == null) // don't cause query parsing
+ return QueryHelper.equals(other.queryString, this.queryString) &&
+ QueryHelper.equals(other.filter, this.filter);
+ else // make sure we compare a parsed variant of both
+ return QueryHelper.equals(other.getQueryTree(), this.getQueryTree());
+ }
+
+ @Override
+ public int hashCode() {
+ return getClass().hashCode() +
+ QueryHelper.combineHash(encoding,filter,language,getQueryTree(),sources,restrict,defaultIndex,type,searchPath);
+ }
+
+
+ public Object clone() {
+ try {
+ Model clone = (Model) super.clone();
+ if (queryTree != null)
+ clone.queryTree = this.queryTree.clone();
+ if (sources !=null)
+ clone.sources = new LinkedHashSet<>(this.sources);
+ if (restrict !=null)
+ clone.restrict = new LinkedHashSet<>(this.restrict);
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Someone inserted a noncloneable superclass",e);
+ }
+ }
+
+ public Model cloneFor(Query q) {
+ Model model = (Model) this.clone();
+ model.setParent(q);
+ return model;
+ }
+
+ /** 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;
+ }
+
+ /** Sets the set of sources this query will search from a comma-separated string of source names */
+ public void setSources(String sourceString) {
+ setFromString(sourceString,sources);
+ }
+
+ /**
+ * Returns the set of sources this query will search.
+ * This set can be modified to change the set of sources. If all sources are to be searched, this returns
+ * an empty set
+ *
+ * @return the set of sources to search, never null
+ */
+ public Set<String> getSources() { return sources; }
+
+ /**
+ * Sets the set of types (document type or search definition names) this query will search from a
+ * comma-separated string of type names. This is useful to narrow a search to just a subset of the types available
+ * from a sources
+ */
+ public void setRestrict(String restrictString) {
+ setFromString(restrictString,restrict);
+ }
+
+ /**
+ * Returns the set of types this query will search.
+ * This set can be modified to change the set of types. If all types are to be searched, this returns
+ * an empty set.
+ *
+ * @return the set of types to search, never null
+ */
+ public Set<String> getRestrict() { return restrict; }
+
+ /** Sets the execution working on this. For internal use. */
+ public void setExecution(Execution execution) {
+ if (execution==this.execution) return;
+
+ // If not already coupled, bind the trace of the new execution into the existing execution trace
+ if (execution.trace().traceNode().isRoot()
+ && execution.trace().traceNode() != this.execution.trace().traceNode().root()) {
+ this.execution.trace().traceNode().add(execution.trace().traceNode());
+ }
+
+ this.execution = execution;
+ }
+
+ /** Sets the document database this will search - a document type */
+ public void setDocumentDb(String documentDbName) {
+ this.documentDbName = documentDbName;
+ }
+
+ /** Returns the name of the document db this should search, or null if not set. */
+ public String getDocumentDb() { return documentDbName; }
+
+ /** Returns the Execution working on this, or a null execution if none. For internal use. */
+ public Execution getExecution() { return execution; }
+
+ private void setFromString(String string,Set<String> set) {
+ set.clear();
+ for (String item : string.split(","))
+ set.add(item.trim());
+ }
+
+ public static Model getFrom(Query q) {
+ return (Model)q.properties().get(argumentTypeName);
+ }
+
+ public @Override String toString() {
+ return "query representation [queryTree: " + queryTree + ", filter: " + filter + "]";
+ }
+
+ /** Prepares this for binary serialization. For internal use. */
+ public void prepare(Ranking ranking) {
+ prepareRankFeaturesFromModel(ranking);
+ }
+
+ private void prepareRankFeaturesFromModel(Ranking ranking) {
+ Item root = getQueryTree().getRoot();
+ if (root != null) {
+ List<Item> tagged = setUniqueIDs(root);
+ addLabels(tagged, ranking);
+ addConnectivityRankProperties(tagged, ranking);
+ addSignificances(tagged, ranking);
+ }
+ }
+
+ private List<Item> setUniqueIDs(Item root) {
+ List<Item> items = new ArrayList<>();
+ collectTaggableItems(root, items);
+ int id = 1;
+ for (Item i : items) {
+ TaggableItem t = (TaggableItem) i;
+ t.setUniqueID(id++);
+ }
+ return items;
+ }
+
+ private void addLabels(List<Item> candidates, Ranking ranking) {
+ for (Item candidate : candidates) {
+ String label = candidate.getLabel();
+ if (label != null) {
+ String name = "vespa.label." + label + ".id";
+ TaggableItem t = (TaggableItem) candidate;
+ ranking.getProperties().put(name, String.valueOf(t.getUniqueID()));
+ }
+ }
+ }
+
+ private void addConnectivityRankProperties(List<Item> connectedItems, Ranking ranking) {
+ for (Item link : connectedItems) {
+ TaggableItem t = (TaggableItem) link;
+ Item connectedTo = t.getConnectedItem();
+ if (connectedTo != null && strictContains(connectedTo, connectedItems)) {
+ TaggableItem t2 = (TaggableItem) connectedTo;
+ String name = "vespa.term." + t.getUniqueID() + ".connexity";
+ ranking.getProperties().put(name, String.valueOf(t2.getUniqueID()));
+ ranking.getProperties().put(name, String.valueOf(t.getConnectivity()));
+ }
+ }
+ }
+
+ private void addSignificances(List<Item> candidates, Ranking ranking) {
+ for (Item candidate : candidates) {
+ TaggableItem t = (TaggableItem) candidate;
+ if ( ! t.hasExplicitSignificance()) continue;
+ String name = "vespa.term." + t.getUniqueID() + ".significance";
+ ranking.getProperties().put(name, String.valueOf(t.getSignificance()));
+ }
+ }
+
+ private void collectTaggableItems(Item root, List<Item> terms) {
+ if (root == null) return;
+
+ if (root instanceof TaggableItem) {
+ // This is tested before descending, as phrases are viewed
+ // as leaf nodes in the ranking code in the backend
+ terms.add(root);
+ } else if (root instanceof CompositeItem) {
+ CompositeItem c = (CompositeItem) root;
+ for (Iterator<Item> i = c.getItemIterator(); i.hasNext();) {
+ collectTaggableItems(i.next(), terms);
+ }
+ } else {} // nop
+ }
+
+ private boolean strictContains(Object needle, Collection<?> haystack) {
+ for (Object pin : haystack)
+ if (pin == needle) return true;
+ return false;
+ }
+
+
+ /**
+ * Set the YTrace header value to use when transmitting this model to a
+ * search backend (of some kind).
+ *
+ * @param next string representation of header value
+ * @deprecated Not use, ytrace is done
+ */
+ @Deprecated
+ public void setYTraceHeaderToNext(String next) { }
+
+ /**
+ * Get the YTrace header value to use when transmitting this model to a
+ * search backend (of some kind). Returns null if no ytrace data is not
+ * turned on.
+ * @deprecated Not use, ytrace is done
+ */
+ @Deprecated
+ public String getYTraceHeaderToNext() {
+ return null;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/ParameterParser.java b/container-search/src/main/java/com/yahoo/search/query/ParameterParser.java
new file mode 100644
index 00000000000..a27e1bfde55
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/ParameterParser.java
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query;
+
+import static com.yahoo.container.util.Util.quote;
+
+/**
+ * Wrapper class to avoid code duplication of common parsing requirements.
+ *
+ * @author <a href="steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class ParameterParser {
+
+ /**
+ * Tries to return the given object as a Long. If it is a Number, treat it
+ * as a number of seconds, i.e. get a Long representation and multiply by
+ * 1000. If it has a String representation, try to parse this as a floating
+ * point number, followed by by an optional unit (seconds and an SI prefix,
+ * a couple of valid examples are "s" and "ms". Only a very small subset of
+ * SI prefixes are supported). If no unit is given, seconds are assumed.
+ *
+ * @param value
+ * some representation of a number of seconds
+ * @param defaultValue
+ * returned if value is null
+ * @return value as a number of milliseconds
+ * @throws NumberFormatException
+ * if value is not a Number instance and its String
+ * representation cannot be parsed as a number followed
+ * optionally by time unit
+ */
+ public static Long asMilliSeconds(Object value, Long defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof Number) {
+ Number n = (Number) value;
+ return Long.valueOf(n.longValue() * 1000L);
+ }
+ return parseTime(value.toString());
+ }
+
+ private static Long parseTime(String time) throws NumberFormatException {
+
+ time = time.trim();
+ try {
+ int unitOffset = findUnitOffset(time);
+ double measure = Double.valueOf(time.substring(0, unitOffset));
+ double multiplier = parseUnit(time.substring(unitOffset));
+ return Long.valueOf((long) (measure * multiplier));
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException("Error parsing " + quote(time), e);
+ }
+ }
+
+ private static int findUnitOffset(String time) {
+ int unitOffset = 0;
+ while (unitOffset < time.length()) {
+ char c = time.charAt(unitOffset);
+ if (c == '.' || (c >= '0' && c <= '9')) {
+ unitOffset += 1;
+ } else {
+ break;
+ }
+ }
+ if (unitOffset == 0) {
+ throw new NumberFormatException("Invalid number " + quote(time));
+ }
+ return unitOffset;
+ }
+
+ private static double parseUnit(String unit) {
+ unit = unit.trim();
+ final double multiplier;
+ if ("ks".equals(unit)) {
+ multiplier = 1e6d;
+ } else if ("s".equals(unit)) {
+ multiplier = 1000.0d;
+ } else if ("ms".equals(unit)) {
+ multiplier = 1.0d;
+ } else if ("\u00B5s".equals(unit)) {
+ // microseconds
+ multiplier = 1e-3d;
+ } else {
+ multiplier = 1000.0d;
+ }
+ return multiplier;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/Presentation.java b/container-search/src/main/java/com/yahoo/search/query/Presentation.java
new file mode 100644
index 00000000000..466ddf88299
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/Presentation.java
@@ -0,0 +1,211 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query;
+
+import com.google.common.base.Splitter;
+import com.yahoo.collections.LazySet;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.prelude.query.*;
+import com.yahoo.search.Query;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.rendering.RendererRegistry;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+
+/**
+ * Parameters deciding how the result of a query should be presented
+ *
+ * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a>
+ */
+public class Presentation implements Cloneable {
+
+ /** The type representing the property arguments consumed by this */
+ private static QueryProfileType argumentType;
+
+ public static final String PRESENTATION = "presentation";
+ public static final String BOLDING = "bolding";
+ public static final String TIMING = "timing";
+ public static final String SUMMARY = "summary";
+ public static final String REPORT_COVERAGE = "reportCoverage";
+ public static final String SUMMARY_FIELDS = "summaryFields";
+
+ /** The (short) name of the parameter holding the name of the return format to use */
+ public static final String FORMAT = "format";
+
+ static {
+ argumentType=new QueryProfileType(PRESENTATION);
+ argumentType.setStrict(true);
+ argumentType.setBuiltin(true);
+ argumentType.addField(new FieldDescription(BOLDING, "boolean", "bolding"));
+ argumentType.addField(new FieldDescription(TIMING, "boolean", "timing"));
+ argumentType.addField(new FieldDescription(SUMMARY, "string", "summary"));
+ argumentType.addField(new FieldDescription(REPORT_COVERAGE, "string", "reportcoverage"));
+ argumentType.addField(new FieldDescription(FORMAT, "string", "format template"));
+ argumentType.addField(new FieldDescription(SUMMARY_FIELDS, "string", "summaryFields"));
+ argumentType.freeze();
+ }
+ public static QueryProfileType getArgumentType() { return argumentType; }
+
+ /** How the result should be highlighted */
+ private Highlight highlight= null;
+
+ /** The terms to highlight in the result (only used by BoldingSearcher, may be removed later). */
+ private List<IndexedItem> boldingData = null;
+
+ /** Whether or not to do highlighting */
+ private boolean bolding = true;
+
+ /** The summary class to be shown */
+ private String summary = null;
+
+ /** Whether coverage information (how much of the indices was searched should be included in the result */
+ private boolean reportCoverage=false;
+
+ /** The name of the renderer to use for rendering the hits. */
+ private ComponentSpecification format = RendererRegistry.defaultRendererId.toSpecification();
+
+ /** Whether optional timing data should be rendered */
+ private boolean timing = false;
+
+ /** Set of explicitly requested summary fields, instead of summary classes */
+ @NonNull
+ private Set<String> summaryFields = LazySet.newHashSet();
+
+ private static final Splitter COMMA_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
+
+ public Presentation(Query parent) { }
+
+ /** Returns how terms in this result should be highlighted, or null if not set */
+ public Highlight getHighlight() { return highlight; }
+
+ /** Sets how terms in this result should be highlighted. Set to null to turn highlighting off */
+ public void setHighlight(Highlight highlight) { this.highlight = highlight; }
+
+ /** Returns the name of the summary class to be used to present hits from this query, or null if not set */
+ public String getSummary() { return summary; }
+
+ /** Sets the name of the summary class to be used to present hits from this query */
+ public void setSummary(String summary) { this.summary = summary; }
+
+ /** Returns whether matching query terms should be bolded in the result. Default is true. */
+ public boolean getBolding() { return bolding; }
+
+ /** Sets whether matching query terms should be bolded in the result */
+ public void setBolding(boolean bolding) { this.bolding = bolding; }
+
+ /** Returns whether coverage information should be returned in the result, if available. Default is false */
+ public boolean getReportCoverage() { return reportCoverage; }
+
+ /** Sets whether coverage information should be returned in the result, if available */
+ public void setReportCoverage(boolean reportCoverage) { this.reportCoverage=reportCoverage; }
+
+ /** Get the name of the format desired for result rendering. */
+ @NonNull
+ public ComponentSpecification getRenderer() { return format; }
+
+ /** Set the desired format for result rendering. If null, use the default renderer. */
+ public void setRenderer(@Nullable ComponentSpecification format) {
+ this.format = (format != null) ? format : RendererRegistry.defaultRendererId.toSpecification();
+ }
+
+ /**
+ * Get the name of the format desired for result rendering.
+ */
+ @NonNull
+ public String getFormat() { return format.getName(); }
+
+ /**
+ * Set the desired format for result rendering. If null, use the default renderer.
+ */
+ public void setFormat(@Nullable String format) {
+ setRenderer(ComponentSpecification.fromString(format));
+ }
+
+ @Override
+ public Object clone() {
+ try {
+ Presentation clone = (Presentation)super.clone();
+ if (boldingData != null)
+ clone.boldingData = new ArrayList<>(boldingData);
+
+ if (highlight != null)
+ clone.highlight = highlight.clone();
+
+ if (summaryFields != null) {
+ clone.summaryFields = LazySet.newHashSet();
+ clone.summaryFields.addAll(this.summaryFields);
+ }
+
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Someone inserted a noncloneable superclass",e);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || !(o instanceof Presentation)) return false;
+ Presentation p = (Presentation) o;
+ return QueryHelper.equals(bolding,p.bolding) && QueryHelper.equals(summary,p.summary);
+ }
+
+ @Override
+ public int hashCode() {
+ return QueryHelper.combineHash(bolding, summary);
+ }
+
+ /**
+ * @return whether to add optional timing data to the rendered result
+ */
+ public boolean getTiming() {
+ return timing;
+ }
+
+ public void setTiming(boolean timing) {
+ this.timing = timing;
+ }
+
+ /**
+ * Return the set of explicitly requested fields. Returns an empty set if no
+ * fields are specified outside of summary classes. The returned set is
+ * mutable and fields may be added or removed before passing on the query.
+ *
+ * @return the set of names of requested fields, never null
+ */
+ @NonNull
+ public Set<String> getSummaryFields() {
+ return summaryFields;
+ }
+
+ /** Prepares this for binary serialization. For internal use - see {@link Query#prepare} */
+ public void prepare() {
+ if (highlight != null)
+ highlight.prepare();
+ }
+
+ /**
+ * Parse the given string as a comma delimited set of field names and
+ * overwrite the set of summary fields. Whitespace will be trimmed. If you
+ * want to add or remove fields programmatically, use
+ * {@link #getSummaryFields()} and modify the returned set.
+ *
+ * @param asString
+ * the summary fields requested, e.g. "price,author,title"
+ */
+ public void setSummaryFields(String asString) {
+ summaryFields.clear();
+ for (String field : COMMA_SPLITTER.split(asString)) {
+ summaryFields.add(field);
+ }
+
+ }
+
+}
+
diff --git a/container-search/src/main/java/com/yahoo/search/query/Properties.java b/container-search/src/main/java/com/yahoo/search/query/Properties.java
new file mode 100644
index 00000000000..df3d120c337
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/Properties.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query;
+
+import com.yahoo.search.Query;
+
+/**
+ * Object properties keyed by name which can be looked up using default values and
+ * with conversion to various primitive wrapper types.
+ * <p>
+ * Multiple property implementations can be chained to provide unified access to properties
+ * backed by multiple sources as a Chain of Responsibility.
+ * <p>
+ * For better performance, prefer CompoundName argument constants over Strings.
+ * <p>
+ * Properties can be cloned. Cloning a properties instance returns a new instance
+ * which chains new instances of all chained instances. The content within each instance
+ * is cloned to the extent determined appropriate by that implementation.
+ * <p>
+ * This base class simply passes all access on to the next in chain.
+ *
+ * @author bratseth
+ */
+public abstract class Properties extends com.yahoo.processing.request.Properties {
+
+ @Override
+ public Properties chained() { return (Properties)super.chained(); }
+
+ @Override
+ public Properties clone() {
+ return (Properties)super.clone();
+ }
+
+ /** The query owning this property object.
+ * Only guaranteed to work if this instance is accessible as query.properties()
+ */
+ public Query getParentQuery() {
+ if (chained() == null) {
+ throw new RuntimeException("getParentQuery should only be called on a properties instance accessible as query.properties()");
+ } else {
+ return chained().getParentQuery();
+ }
+ }
+
+ /**
+ * Invoked during deep cloning of the parent query.
+ */
+ public void setParentQuery(Query query) {
+ if (chained() != null)
+ chained().setParentQuery(query);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/QueryHelper.java b/container-search/src/main/java/com/yahoo/search/query/QueryHelper.java
new file mode 100644
index 00000000000..d4b6f257c11
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/QueryHelper.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query;
+
+/**
+ * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a>
+ */
+class QueryHelper {
+
+ /** Compares two objects which may be null */
+ public static boolean equals(Object a,Object b) {
+ if (a == null) return b == null;
+ return a.equals(b);
+ }
+
+ /**
+ * Helper method that finds the hashcode for a group of objects.
+ * Inspired by java.util.List
+ */
+ public static int combineHash(Object... objs) {
+ int hash = 1;
+ for (Object o:objs) {
+ hash = 31*hash + (o == null ? 0 : o.hashCode());
+ }
+ return hash;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/QueryTree.java b/container-search/src/main/java/com/yahoo/search/query/QueryTree.java
new file mode 100644
index 00000000000..3a501853388
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/QueryTree.java
@@ -0,0 +1,159 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query;
+
+import com.yahoo.prelude.query.*;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * The root node of a query tree. This is always present above the actual semantic root to ease query manipulation,
+ * especially replacing the actual semantic root, but does not have any search semantics on its own.
+ *
+ * <p>To ease recursive manipulation of the query tree, this is a composite having one child, which is the actual root.
+ * <ul>
+ * <li>Setting the root item (at position 0, either directly or though the iterator of this, works as expected.
+ * Setting at any other position is disallowed.
+ * <li>Removing the root is allowed and causes this to be a null query.
+ * <li>Adding an item is only allowed if this is currently a null query (having no root)
+ * </ul>
+ *
+ * <p>This is also the home of accessor methods which eases querying into and manipulation of the query tree.</p>
+ *
+ * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a>
+ */
+public class QueryTree extends CompositeItem {
+
+ public QueryTree(Item root) {
+ setRoot(root);
+ }
+
+ public void setIndexName(String index) {
+ if (getRoot() != null)
+ getRoot().setIndexName(index);
+ }
+
+ public ItemType getItemType() {
+ throw new RuntimeException("Packet type access attempted. " +
+ "A query tree has no packet code. This is probably a misbehaving searcher.");
+ }
+
+ public String getName() { return "ROOT"; }
+
+ public int encode(ByteBuffer buffer) {
+ if (getRoot() == null) return 0;
+ return getRoot().encode(buffer);
+ }
+
+ //Lets not pollute toString() by adding "ROOT"
+ protected void appendHeadingString(StringBuilder sb) {
+ }
+
+ /** Returns the query root. This is null if this is a null query. */
+ public Item getRoot() {
+ if (getItemCount()==0) return null;
+ return getItem(0);
+ }
+
+ public final void setRoot(Item root) {
+ if (root==this) throw new IllegalArgumentException("Cannot make a root point at itself");
+ if (root == null) throw new IllegalArgumentException("Root must not be null, use NullItem instead.");
+ if (root instanceof QueryTree) throw new IllegalArgumentException("Do not use a new QueryTree instance as a root.");
+ if (this.getItemCount()==0) // initializing
+ super.addItem(root);
+ else
+ setItem(0,root); // replacing
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if( !(o instanceof QueryTree)) return false;
+ return super.equals(o);
+ }
+
+ /** Returns a deep copy of this */
+ @Override
+ public QueryTree clone() {
+ QueryTree clone = (QueryTree) super.clone();
+ fixClonedConnectivityReferences(clone);
+ return clone;
+ }
+
+ private void fixClonedConnectivityReferences(QueryTree clone) {
+ // TODO!
+ }
+
+ @Override
+ public void addItem(Item item) {
+ if (getItemCount()==0)
+ super.addItem(item);
+ else
+ throw new RuntimeException("Programming error: Cannot add multiple roots");
+ }
+
+ @Override
+ public void addItem(int index, Item item) {
+ if (getItemCount()==0 && index==0)
+ super.addItem(index,item);
+ else
+ throw new RuntimeException("Programming error: Cannot add multiple roots, have '" + getRoot() + "'");
+ }
+
+ /** Returns true if this represents the null query */
+ public boolean isEmpty() {
+ return getRoot() instanceof NullItem;
+ }
+
+ // -------------- Facade
+
+ /** Modifies this query to become the current query AND the given item */
+ // TODO: Make sure this is complete, unit test and make it public
+ private void and(Item item) {
+ if (isEmpty()) {
+ setRoot(item);
+ }
+ else if (getRoot() instanceof NotItem && item instanceof NotItem) {
+ throw new IllegalArgumentException("Can't AND two NOTs"); // TODO: Complete
+ }
+ else if (getRoot() instanceof NotItem){
+ NotItem notItem = (NotItem)getRoot();
+ notItem.addPositiveItem(item);
+ }
+ else if (item instanceof NotItem){
+ NotItem notItem = (NotItem)item;
+ notItem.addPositiveItem(getRoot());
+ setRoot(notItem);
+ }
+ else {
+ AndItem andItem = new AndItem();
+ andItem.addItem(getRoot());
+ andItem.addItem(item);
+ setRoot(andItem);
+ }
+ }
+
+ /** Returns a flattened list of all positive query terms under the given item */
+ public static List<IndexedItem> getPositiveTerms(Item item) {
+ List<IndexedItem> items = new ArrayList<>();
+ getPositiveTerms(item,items);
+ return items;
+ }
+
+ private static void getPositiveTerms(Item item, List<IndexedItem> terms) {
+ if (item instanceof NotItem) {
+ getPositiveTerms(((NotItem) item).getPositiveItem(), terms);
+ } else if (item instanceof PhraseItem) {
+ PhraseItem pItem = (PhraseItem)item;
+ terms.add(pItem);
+ } else if (item instanceof CompositeItem) {
+ for (Iterator<Item> i = ((CompositeItem) item).getItemIterator(); i.hasNext();) {
+ getPositiveTerms(i.next(), terms);
+ }
+ } else if (item instanceof TermItem) {
+ terms.add((TermItem)item);
+ }
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/Ranking.java b/container-search/src/main/java/com/yahoo/search/query/Ranking.java
new file mode 100644
index 00000000000..e543589f74d
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/Ranking.java
@@ -0,0 +1,246 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.prelude.Freshness;
+import com.yahoo.prelude.Location;
+import com.yahoo.search.Query;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.ranking.MatchPhase;
+import com.yahoo.search.query.ranking.RankFeatures;
+import com.yahoo.search.query.ranking.RankProperties;
+import com.yahoo.search.result.ErrorMessage;
+
+/**
+ * The ranking (hit ordering) settings of a query
+ *
+ * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a>
+ * @author bratseth
+ */
+public class Ranking implements Cloneable {
+
+ /** An alias for listing features */
+ public static final com.yahoo.processing.request.CompoundName RANKFEATURES =
+ new com.yahoo.processing.request.CompoundName("rankfeatures");
+
+ /** The type representing the property arguments consumed by this */
+ private static final QueryProfileType argumentType;
+ private static final CompoundName argumentTypeName;
+
+ public static final String RANKING = "ranking";
+ public static final String LOCATION = "location";
+ public static final String PROFILE = "profile";
+ public static final String SORTING = "sorting";
+ public static final String LIST_FEATURES = "listFeatures";
+ public static final String FRESHNESS = "freshness";
+ public static final String QUERYCACHE = "queryCache";
+ public static final String MATCH_PHASE = "matchPhase";
+ public static final String DIVERSITY = "diversity";
+ public static final String FEATURES = "features";
+ public static final String PROPERTIES = "properties";
+
+ static {
+ argumentType =new QueryProfileType(RANKING);
+ argumentType.setStrict(true);
+ argumentType.setBuiltin(true);
+ argumentType.addField(new FieldDescription(LOCATION, "string", "location"));
+ argumentType.addField(new FieldDescription(PROFILE, "string", "ranking"));
+ argumentType.addField(new FieldDescription(SORTING, "string", "sorting sortspec"));
+ argumentType.addField(new FieldDescription(LIST_FEATURES, "string", RANKFEATURES.toString()));
+ argumentType.addField(new FieldDescription(FRESHNESS, "string", "datetime"));
+ argumentType.addField(new FieldDescription(QUERYCACHE, "string"));
+ argumentType.addField(new FieldDescription(MATCH_PHASE, "query-profile", "matchPhase"));
+ argumentType.addField(new FieldDescription(FEATURES, "query-profile", "rankfeature"));
+ argumentType.addField(new FieldDescription(PROPERTIES, "query-profile", "rankproperty"));
+ argumentType.freeze();
+ argumentTypeName=new CompoundName(argumentType.getId().getName());
+ }
+ public static QueryProfileType getArgumentType() { return argumentType; }
+
+ private Query parent;
+
+ /** The location of the query is used for distance ranking */
+ private Location location = null;
+
+ /** The name of the rank profile to use */
+ private String profile = null;
+
+ /** How the query should be sorted */
+ private Sorting sorting = null;
+
+ /** Set to true to include the value of "all" rank features in the result */
+ private boolean listFeatures = false;
+
+ private Freshness freshness;
+
+ private boolean queryCache = false;
+
+ private RankProperties rankProperties = new RankProperties();
+
+ private RankFeatures rankFeatures = new RankFeatures();
+
+ private MatchPhase matchPhase = new MatchPhase();
+
+ public Ranking(Query parent) {
+ this.parent = parent;
+ }
+
+ /**
+ * Returns whether a rank profile has been explicitly set.
+ *
+ * This is only used in serializing the packet properly to FS4.
+ */
+ public boolean hasRankProfile() {
+ return profile != null;
+ }
+
+ /** Get the freshness search parameters associated with this query */
+ public Freshness getFreshness() {
+ return freshness;
+ }
+
+ /** Set the freshness search parameters for this query */
+ public void setFreshness(String dateTime) {
+ try {
+ Freshness freshness = new Freshness(dateTime);
+ setFreshness(freshness);
+ } catch (NumberFormatException e) {
+ parent.errors().add(ErrorMessage.createInvalidQueryParameter("Datetime reference could not be converted from '"
+ + dateTime + "' to long"));
+ }
+ }
+
+ public void setFreshness(Freshness freshness) {
+ this.freshness = freshness;
+ }
+
+ /**
+ * Returns whether feature caching is turned on in the backed.
+ * Feature caching allows us to avoid sending the query during document summary retrieval
+ * and recalculate feature scores, it is typically beneficial to turn it on if
+ * fan-out is low or queries are large.
+ * <p>
+ * Default is false (off).
+ */
+ public void setQueryCache(boolean queryCache) { this.queryCache = queryCache; }
+
+ public boolean getQueryCache() { return queryCache; }
+
+ /** Returns the location of this query, or null if none */
+ public Location getLocation() { return location; }
+
+ public void setLocation(Location location) { this.location = location; }
+
+ /** Sets the location from a string, see {@link Location} for syntax */
+ public void setLocation(String str) { this.location = new Location(str); }
+
+ /** Returns the name of the rank profile to be used. Returns "default" if nothing is set. */
+ public String getProfile() { return profile == null ? "default" : profile; }
+
+ /** Sets the name of the rank profile to use. This cannot be set to null. */
+ public void setProfile(String profile) {
+ if (profile==null) throw new NullPointerException("The ranking profile cannot be set to null");
+ this.profile = profile;
+ }
+
+ /**
+ * Returns the rank features of this, an empty container (never null) if none are set.
+ * The returned object can be modified directly to change the rank properties of this.
+ */
+ public RankFeatures getFeatures() {
+ return rankFeatures;
+ }
+
+ /**
+ * Returns the rank properties of this, an empty container (never null) if none are set.
+ * The returned object can be modified directly to change the rank properties of this.
+ */
+ public RankProperties getProperties() {
+ return rankProperties;
+ }
+
+ /** Set whether rank features should be included with the result of this query */
+ public void setListFeatures(boolean listFeatures) { this.listFeatures = listFeatures; }
+
+ /** Returns whether rank features should be dumped with the result of this query, default false */
+ public boolean getListFeatures() { return listFeatures; }
+
+ /** Returns the match phase rank settings of this. This is never null. */
+ public MatchPhase getMatchPhase() { return matchPhase; }
+
+ @Override
+ public Object clone() {
+ try {
+ Ranking clone = (Ranking) super.clone();
+
+ if (sorting != null) clone.sorting = this.sorting.clone();
+
+ clone.rankProperties = this.rankProperties.clone();
+ clone.rankFeatures = this.rankFeatures.clone();
+ clone.matchPhase = this.matchPhase.clone();
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Someone inserted a noncloneable superclass",e);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if( ! (o instanceof Ranking)) return false;
+
+ Ranking other = (Ranking) o;
+
+ if ( ! QueryHelper.equals(rankProperties, other.rankProperties)) return false;
+ if ( ! QueryHelper.equals(rankFeatures, other.rankFeatures)) return false;
+ if ( ! QueryHelper.equals(freshness, other.freshness)) return false;
+ if ( ! QueryHelper.equals(this.sorting, other.sorting)) return false;
+ if ( ! QueryHelper.equals(this.location, other.location)) return false;
+ if ( ! QueryHelper.equals(this.profile, other.profile)) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+ hash += 11 * rankFeatures.hashCode();
+ hash += 13 * rankProperties.hashCode();
+ hash += 17 * matchPhase.hashCode();
+ return Ranking.class.hashCode() + QueryHelper.combineHash(sorting,location,profile,hash);
+ }
+
+ /** Returns the sorting spec of this query, or null if none is set */
+ public Sorting getSorting() { return sorting; }
+
+ /** Sets how this query should be sorted. Set to null to turn off explicit sorting. */
+ public void setSorting(Sorting sorting) { this.sorting = sorting; }
+
+ /** Sets sorting from a string. See {@link Sorting} on syntax */
+ public void setSorting(String sortingString) {
+ if (sortingString==null)
+ setSorting((Sorting)null);
+ else
+ setSorting(new Sorting(sortingString));
+ }
+
+ public static Ranking getFrom(Query q) {
+ return (Ranking) q.properties().get(argumentTypeName);
+ }
+
+ public void prepare() {
+ rankFeatures.prepare(rankProperties);
+ matchPhase.prepare(rankProperties);
+ prepareNow(freshness);
+ }
+
+ private void prepareNow(Freshness freshness) {
+ if (freshness == null) return;
+ // TODO: See what freshness is doing with the internal props and simplify
+ if (rankProperties.get("vespa.now") == null || rankProperties.get("vespa.now").isEmpty()) {
+ rankProperties.put("vespa.now", "" + freshness.getRefTime());
+ }
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/SessionId.java b/container-search/src/main/java/com/yahoo/search/query/SessionId.java
new file mode 100644
index 00000000000..7f8ca6385e1
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/SessionId.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query;
+
+import com.yahoo.container.Server;
+import com.yahoo.text.Utf8String;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * A query id which is unique across this cluster - consisting of
+ * container runtime id + timestamp + serial.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class SessionId {
+
+ private static final String serverId = Server.get().getServerDiscriminator();
+ private static final AtomicLong sequenceCounter = new AtomicLong();
+
+ private final Utf8String id;
+
+ private SessionId(String serverId, long timestamp, long sequence) {
+ this.id = new Utf8String(serverId + "." + timestamp + "." + sequence);
+ }
+
+ public Utf8String asUtf8String() { return id; }
+
+ /**
+ * Creates a session id which is unique across the cluster this runtime is a member of each time this is called.
+ * Calling this causes synchronization.
+ */
+ public static SessionId next() {
+ return new SessionId(serverId, System.currentTimeMillis(), sequenceCounter.getAndIncrement());
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/Sorting.java b/container-search/src/main/java/com/yahoo/search/query/Sorting.java
new file mode 100644
index 00000000000..3af9bc34940
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/Sorting.java
@@ -0,0 +1,407 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query;
+
+import com.ibm.icu.text.Collator;
+import com.ibm.icu.util.ULocale;
+import com.yahoo.text.Utf8;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+
+/**
+ * Specifies how a query is sorted by a list of fields with a sort order
+ *
+ * @author Arne Bergene Fossaa
+ */
+public class Sorting implements Cloneable {
+
+ public static final String STRENGTH_IDENTICAL = "identical";
+ public static final String STRENGTH_QUATERNARY = "quaternary";
+ public static final String STRENGTH_TERTIARY = "tertiary";
+ public static final String STRENGTH_SECONDARY = "secondary";
+ public static final String STRENGTH_PRIMARY = "primary";
+ public static final String UCA = "uca";
+ public static final String RAW = "raw";
+ public static final String LOWERCASE = "lowercase";
+
+ private final List<FieldOrder> fieldOrders = new ArrayList<>(2);
+
+ /** Creates an empty sort spec */
+ public Sorting() { }
+
+ public Sorting(List<FieldOrder> fieldOrders) {
+ this.fieldOrders.addAll(fieldOrders);
+ }
+
+ /** Creates a sort spec from a string */
+ public Sorting(String sortSpec) {
+ setSpec(sortSpec);
+ }
+
+ /**
+ * Creates a new sorting from the given string and returns it, or returns null if the argument does not contain
+ * any sorting criteria (e.g it is null or the empty string)
+ */
+ public static Sorting fromString(String sortSpec) {
+ if (sortSpec==null) return null;
+ if ("".equals(sortSpec)) return null;
+ return new Sorting(sortSpec);
+ }
+
+ private void setSpec(String rawSortSpec) {
+ String[] vectors = rawSortSpec.split(" ");
+
+ for (String sortString:vectors) {
+ // A sortspec element must be at least two characters long,
+ // a sorting order and an attribute vector name
+ if (sortString.length() < 1) {
+ continue;
+ }
+ char orderMarker = sortString.charAt(0);
+ int funcAttrStart = 0;
+ if ((orderMarker == '+') || (orderMarker == '-')) {
+ funcAttrStart = 1;
+ }
+ AttributeSorter sorter = null;
+ int startPar = sortString.indexOf('(',funcAttrStart);
+ int endPar = sortString.lastIndexOf(')');
+ if ((startPar > 0) && (endPar > startPar)) {
+ String funcName = sortString.substring(funcAttrStart, startPar);
+ if (LOWERCASE.equalsIgnoreCase(funcName)) {
+ sorter = new LowerCaseSorter(sortString.substring(startPar+1, endPar));
+ } else if (RAW.equalsIgnoreCase(funcName)) {
+ sorter = new RawSorter(sortString.substring(startPar+1, endPar));
+ } else if (UCA.equalsIgnoreCase(funcName)) {
+ int commaPos = sortString.indexOf(',', startPar+1);
+ if ((startPar+1 < commaPos) && (commaPos < endPar)) {
+ int commaopt = sortString.indexOf(',', commaPos + 1);
+ UcaSorter.Strength strength = UcaSorter.Strength.UNDEFINED;
+ if (commaopt > 0) {
+ String s = sortString.substring(commaopt+1, endPar);
+ if (STRENGTH_PRIMARY.equalsIgnoreCase(s)) {
+ strength = UcaSorter.Strength.PRIMARY;
+ } else if (STRENGTH_SECONDARY.equalsIgnoreCase(s)) {
+ strength = UcaSorter.Strength.SECONDARY;
+ } else if (STRENGTH_TERTIARY.equalsIgnoreCase(s)) {
+ strength = UcaSorter.Strength.TERTIARY;
+ } else if (STRENGTH_QUATERNARY.equalsIgnoreCase(s)) {
+ strength = UcaSorter.Strength.QUATERNARY;
+ } else if (STRENGTH_IDENTICAL.equalsIgnoreCase(s)) {
+ strength = UcaSorter.Strength.IDENTICAL;
+ } else {
+ throw new IllegalArgumentException("Unknown collation strength: '" + s + "'");
+ }
+ sorter = new UcaSorter(sortString.substring(startPar+1, commaPos), sortString.substring(commaPos+1, commaopt), strength);
+ } else {
+ sorter = new UcaSorter(sortString.substring(startPar+1, commaPos), sortString.substring(commaPos+1, endPar), strength);
+ }
+ } else {
+ sorter = new UcaSorter(sortString.substring(startPar+1, endPar));
+ }
+ } else {
+ if (funcName.isEmpty()) {
+ throw new IllegalArgumentException("No sort function specified");
+ } else {
+ throw new IllegalArgumentException("Unknown sort function '" + funcName + "'");
+ }
+ }
+ } else {
+ sorter = new AttributeSorter(sortString.substring(funcAttrStart));
+ }
+ Order order = Order.UNDEFINED;
+ if (funcAttrStart != 0) {
+ // Override in sortspec
+ order = (orderMarker == '+') ? Order.ASCENDING : Order.DESCENDING;
+ }
+ fieldOrders.add(new FieldOrder(sorter, order));
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+
+ String space = "";
+ for (FieldOrder spec : fieldOrders) {
+ sb.append(space);
+ if (spec.getSortOrder() == Order.DESCENDING) {
+ sb.append("-");
+ } else {
+ sb.append("+");
+ }
+ sb.append(spec.getFieldName());
+ space = " ";
+ }
+ return sb.toString();
+ }
+
+
+ public enum Order {ASCENDING,DESCENDING,UNDEFINED}
+
+ /**
+ * Returns the field orders of this sort specification as list. This is never null but can be empty.
+ * This list can be modified to change this sort spec.
+ */
+ public List<FieldOrder> fieldOrders() { return fieldOrders; }
+
+ public Sorting clone() {
+ return new Sorting(this.fieldOrders);
+ }
+
+ public static class AttributeSorter implements Cloneable {
+ private static final Pattern legalAttributeName = Pattern.compile("[\\[]*[a-zA-Z_][\\.a-zA-Z0-9_-]*[\\]]*");
+
+ private String fieldName;
+ public AttributeSorter(String fieldName) {
+ if (legalAttributeName.matcher(fieldName).matches()) {
+ this.fieldName = fieldName;
+ } else {
+ throw new IllegalArgumentException("Illegal attribute name '" + fieldName + "' for sorting. Requires '" + legalAttributeName.pattern() + "'");
+ }
+ }
+ public String getName() { return fieldName; }
+ public void setName(String fieldName) { this.fieldName = fieldName; }
+ @Override
+ public String toString() { return fieldName; }
+ @Override
+ public int hashCode() { return fieldName.hashCode(); }
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof AttributeSorter)) {
+ return false;
+ }
+ return ((AttributeSorter) other).fieldName.equals(fieldName);
+ }
+ @Override
+ public AttributeSorter clone() {
+ try {
+ return (AttributeSorter)super.clone();
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public int compare(Comparable a, Comparable b) {
+ return a.compareTo(b);
+ }
+
+ }
+ public static class RawSorter extends AttributeSorter
+ {
+ public RawSorter(String fieldName) { super(fieldName); }
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof RawSorter)) {
+ return false;
+ }
+ return super.equals(other);
+ }
+ }
+ public static class LowerCaseSorter extends AttributeSorter
+ {
+ public LowerCaseSorter(String fieldName) { super(fieldName); }
+ @Override
+ public String toString() { return "lowercase(" + getName() + ')'; }
+ @Override
+ public int hashCode() { return 1 + 3*super.hashCode(); }
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof LowerCaseSorter)) {
+ return false;
+ }
+ return super.equals(other);
+ }
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public int compare(Comparable a, Comparable b) {
+ if ((a instanceof String) && (b instanceof String)) {
+ return ((String)a).compareToIgnoreCase((String) b);
+ }
+ return a.compareTo(b);
+ }
+ }
+ public static class UcaSorter extends AttributeSorter
+ {
+ public enum Strength { PRIMARY, SECONDARY, TERTIARY, QUATERNARY, IDENTICAL, UNDEFINED };
+ private String locale = null;
+ private Strength strength = Strength.UNDEFINED;
+ private Collator collator;
+ public UcaSorter(String fieldName, String locale, Strength strength) { super(fieldName); setLocale(locale, strength); }
+ public UcaSorter(String fieldName) { super(fieldName); }
+ static private int strength2Collator(Strength strength) {
+ switch (strength) {
+ case PRIMARY: return Collator.PRIMARY;
+ case SECONDARY: return Collator.SECONDARY;
+ case TERTIARY: return Collator.TERTIARY;
+ case QUATERNARY: return Collator.QUATERNARY;
+ case IDENTICAL: return Collator.IDENTICAL;
+ case UNDEFINED: return Collator.PRIMARY;
+ }
+ return Collator.PRIMARY;
+ }
+ public void setLocale(String locale, Strength strength) {
+ this.locale = locale;
+ this.strength = strength;
+ ULocale uloc;
+ try {
+ uloc = new ULocale(locale);
+ } catch (Throwable e) {
+ throw new RuntimeException("ULocale("+locale+") failed with exception " + e.toString());
+ }
+ try {
+ collator = Collator.getInstance(uloc);
+ if (collator == null) {
+ throw new RuntimeException("No collator available for: " + locale);
+ }
+ } catch (Throwable e) {
+ throw new RuntimeException("Collator.getInstance(ULocale("+locale+")) failed with exception " + e.toString());
+ }
+ collator.setStrength(strength2Collator(strength));
+ // collator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
+ }
+ public String getLocale() { return locale; }
+ public Strength getStrength() { return strength; }
+ public Collator getCollator() { return collator; }
+ public String getDecomposition() { return (collator.getDecomposition() == Collator.CANONICAL_DECOMPOSITION) ? "CANONICAL_DECOMPOSITION" : "NO_DECOMPOSITION"; }
+ @Override
+ public String toString() { return "uca(" + getName() + ',' + locale + ',' + ((strength != Strength.UNDEFINED) ? strength.toString() : "PRIMARY") + ')'; }
+ @Override
+ public int hashCode() { return 1 + 3*locale.hashCode() + 5*strength.hashCode() + 7*super.hashCode(); }
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof UcaSorter)) {
+ return false;
+ }
+ return super.equals(other) && locale.equals(((UcaSorter)other).locale) && (strength == ((UcaSorter)other).strength);
+ }
+ public UcaSorter clone() {
+ UcaSorter clone = (UcaSorter)super.clone();
+ if (locale != null) {
+ clone.setLocale(locale, strength);
+ }
+ return clone;
+ }
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public int compare(Comparable a, Comparable b) {
+ if ((a instanceof String) && (b instanceof String)) {
+ return collator.compare((String)a, (String) b);
+ }
+ return a.compareTo(b);
+ }
+ }
+ /**
+ * An attribute (field) and how it should be sorted
+ */
+ public static class FieldOrder implements Cloneable {
+
+ private AttributeSorter fieldSorter;
+ private Order sortOrder;
+
+ /**
+ * Creates an attribute vector
+ *
+ * @param fieldSorter the sorter of this attribute
+ * @param sortOrder whether to sort this ascending or descending
+ */
+ public FieldOrder(AttributeSorter fieldSorter, Order sortOrder) {
+ this.fieldSorter = fieldSorter;
+ this.sortOrder = sortOrder;
+ }
+
+ /**
+ * Returns the name of this attribute
+ */
+ public String getFieldName() {
+ return fieldSorter.getName();
+ }
+
+ /**
+ * Returns the sorter of this attribute
+ */
+ public AttributeSorter getSorter() { return fieldSorter; }
+ public void setSorter(AttributeSorter sorter) { fieldSorter = sorter; }
+
+ /**
+ * Returns the sorting order of this attribute
+ */
+ public Order getSortOrder() {
+ return sortOrder;
+ }
+
+ /**
+ * Decide if sortorder is ascending or not.
+ */
+ public void setAscending(boolean asc) {
+ sortOrder = asc ? Order.ASCENDING : Order.DESCENDING;
+ }
+
+ @Override
+ public String toString() {
+ return sortOrder.toString() + ":" + fieldSorter.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return sortOrder.hashCode() + 17 * fieldSorter.hashCode();
+ }
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof FieldOrder)) {
+ return false;
+ }
+ FieldOrder otherAttr = (FieldOrder) other;
+
+ return otherAttr.sortOrder.equals(sortOrder)
+ && otherAttr.fieldSorter.equals(fieldSorter);
+ }
+ @Override
+ public FieldOrder clone() {
+ return new FieldOrder(fieldSorter.clone(), sortOrder);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return fieldOrders.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if( ! (o instanceof Sorting)) return false;
+
+ Sorting ss = (Sorting) o;
+ return fieldOrders.equals(ss.fieldOrders);
+ }
+
+ public int encode(ByteBuffer buffer) {
+ int usedBytes = 0;
+ byte[] nameBuffer;
+ buffer.position();
+ byte space = '.';
+ for (FieldOrder fieldOrder : fieldOrders) {
+ if (space == ' ') {
+ buffer.put(space);
+ usedBytes++;
+ }
+ if (fieldOrder.getSortOrder() == Order.ASCENDING) {
+ buffer.put((byte) '+');
+ } else {
+ buffer.put((byte) '-');
+ }
+ usedBytes++;
+ nameBuffer = Utf8.toBytes(fieldOrder.getSorter().toString());
+ buffer.put(nameBuffer);
+ usedBytes += nameBuffer.length;
+ // If this isn't the last element, append a separating space
+ //if (i + 1 < sortSpec.size()) {
+ space = ' ';
+ }
+ return usedBytes;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/context/QueryContext.java b/container-search/src/main/java/com/yahoo/search/query/context/QueryContext.java
new file mode 100644
index 00000000000..e59f8589903
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/context/QueryContext.java
@@ -0,0 +1,112 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.context;
+
+import com.yahoo.processing.execution.Execution;
+import com.yahoo.search.Query;
+import com.yahoo.search.rendering.DefaultRenderer;
+import com.yahoo.text.XMLWriter;
+import com.yahoo.yolean.trace.TraceNode;
+
+import java.io.Writer;
+import java.util.Iterator;
+
+
+/**
+ * A proxy to the Execution.trace() which exists for legacy reasons.
+ * Calls to this is forwarded to owningQuery.getModel().getExecution().trace().
+ *
+ * @since 4.2
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class QueryContext implements Cloneable {
+
+ public static final String ID = "context";
+ private Query owner;
+
+ public QueryContext(int ignored,Query owner) {
+ this.owner=owner;
+ }
+
+ //---------------- Public API ---------------------------------------------------------------------------------
+
+ /** Adds a context message to this context */
+ public void trace(String message, int traceLevel) {
+ owner.getModel().getExecution().trace().trace(message,traceLevel);
+ }
+
+ /**
+ * Adds a key-value which will be logged to the access log for this query (by doing toString() on the value
+ * Multiple values may be set to the same key. A value cannot be removed once set.
+ */
+ public void logValue(String key,Object value) {
+ owner.getModel().getExecution().trace().logValue(key, value.toString());
+ }
+
+ /** Returns the values to be written to the access log for this */
+ public Iterator<Execution.Trace.LogValue> logValueIterator() {
+ return owner.getModel().getExecution().trace().logValueIterator();
+ }
+
+ /**
+ * Adds a property key-value to this context.
+ * If the same name is set multiple times, the behavior is thus:
+ * <ul>
+ * <li>Within a single context (thread/query clone), the last value set is used</li>
+ * <li>Across multiple traces, the <i>last</i> value from the <i>last</i> deepest nested thread/clone is used.
+ * In the case of multiple threads writing the value concurrently to their clone, it is of course undefined
+ * which one will be used.</li>
+ * </ul>
+ *
+ * @param name the name of the property
+ * @param value the value of the property, or null to set this property to null
+ */
+ public void setProperty(String name,Object value) {
+ owner.getModel().getExecution().trace().setProperty(name,value);
+ }
+
+ /**
+ * Returns a property set anywhere in this context.
+ * Note that even though this call is itself "thread robust", the object values returned
+ * may in some scenarios not be written behind a synchronization barrier, so when accessing
+ * objects which are not inherently thread safe, synchronization should be considered.
+ * <p>
+ * Note that this method have a time complexity which is proportional to
+ * the number of cloned/created queries times the average number of properties in each.
+ */
+ public Object getProperty(String name) {
+ return owner.getModel().getExecution().trace().getProperty(name);
+ }
+
+ /** Returns a short string description of this (includes the first few messages only, and no newlines) */
+ @Override
+ public String toString() {
+ return owner.getModel().getExecution().trace().toString();
+ }
+
+ public boolean render(Writer writer) throws java.io.IOException {
+ if (owner.getTraceLevel()!=0) {
+ XMLWriter xmlWriter=XMLWriter.from(writer);
+ xmlWriter.openTag("meta").attribute("type",ID);
+ TraceNode traceRoot=owner.getModel().getExecution().trace().traceNode().root();
+ traceRoot.accept(new DefaultRenderer.RenderingVisitor(xmlWriter,owner.getStartTime()));
+ xmlWriter.closeTag();
+ }
+ return true;
+ }
+
+ public QueryContext cloneFor(Query cloneOwner) {
+ try {
+ QueryContext clone=(QueryContext)super.clone();
+ clone.owner=cloneOwner;
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Returns the execution trace this delegates to */
+ public Execution.Trace getTrace() { return owner.getModel().getExecution().trace(); }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/context/package-info.java b/container-search/src/main/java/com/yahoo/search/query/context/package-info.java
new file mode 100644
index 00000000000..c19e5abedd0
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/context/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query.context;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/package-info.java b/container-search/src/main/java/com/yahoo/search/query/package-info.java
new file mode 100644
index 00000000000..2384169c52b
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/package-info.java
@@ -0,0 +1,10 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * The search query model
+ */
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
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
new file mode 100644
index 00000000000..92601a5464d
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/parser/Parsable.java
@@ -0,0 +1,112 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.parser;
+
+import com.yahoo.language.Language;
+import com.yahoo.search.query.Model;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * <p>This class encapsulates all the parameters required to call {@link Parser#parse(Parsable)}. Because all set-
+ * methods return a reference to self, you can write very compact calls to the parser:</p>
+ *
+ * <pre>
+ * parser.parse(new Parsable()
+ * .setQuery("foo")
+ * .setFilter("bar")
+ * .setDefaultIndexName("default")
+ * .setLanguage(Language.ENGLISH))
+ * </pre>
+ *
+ * <p>In case you are parsing the content of a {@link Model}, you can use the {@link #fromQueryModel(Model)} factory for
+ * convenience.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ * @since 5.1.4
+ */
+public final class Parsable {
+
+ private final Set<String> sourceList = new HashSet<>();
+ private final Set<String> restrictList = new HashSet<>();
+ private String query;
+ private String filter;
+ private String defaultIndexName;
+ private Language language;
+
+ public String getQuery() {
+ return query;
+ }
+
+ public Parsable setQuery(String query) {
+ this.query = query;
+ return this;
+ }
+
+ public String getFilter() {
+ return filter;
+ }
+
+ public Parsable setFilter(String filter) {
+ this.filter = filter;
+ return this;
+ }
+
+ public String getDefaultIndexName() {
+ return defaultIndexName;
+ }
+
+ public Parsable setDefaultIndexName(String defaultIndexName) {
+ this.defaultIndexName = defaultIndexName;
+ return this;
+ }
+
+ public Language getLanguage() {
+ return language;
+ }
+
+ public Parsable setLanguage(Language language) {
+ this.language = language;
+ return this;
+ }
+
+ public Set<String> getSources() {
+ return sourceList;
+ }
+
+ public Parsable addSource(String sourceName) {
+ sourceList.add(sourceName);
+ return this;
+ }
+
+ public Parsable addSources(Collection<String> sourceNames) {
+ sourceList.addAll(sourceNames);
+ return this;
+ }
+
+ public Set<String> getRestrict() {
+ return restrictList;
+ }
+
+ public Parsable addRestrict(String restrictName) {
+ restrictList.add(restrictName);
+ return this;
+ }
+
+ public Parsable addRestricts(Collection<String> restrictNames) {
+ restrictList.addAll(restrictNames);
+ return this;
+ }
+
+ public static Parsable fromQueryModel(Model model) {
+ return new Parsable()
+ .setQuery(model.getQueryString())
+ .setFilter(model.getFilter())
+ .setLanguage(model.getParsingLanguage())
+ .setDefaultIndexName(model.getDefaultIndex())
+ .addSources(model.getSources())
+ .addRestricts(model.getRestrict());
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/parser/Parser.java b/container-search/src/main/java/com/yahoo/search/query/parser/Parser.java
new file mode 100644
index 00000000000..3822b9b67d8
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/parser/Parser.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.parser;
+
+import com.yahoo.search.query.QueryTree;
+
+/**
+ * Defines the interface of a query parser. To construct an instance of this class, use the {@link ParserFactory}.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface Parser {
+
+ /**
+ * Parser the given {@link Parsable}, and returns a corresponding
+ * {@link QueryTree}. If parsing fails without an exception, the contained
+ * root will be an instance of {@link com.yahoo.prelude.query.NullItem}.
+ *
+ * @param query
+ * the Parsable to parse
+ * @return the parsed QueryTree, never null
+ */
+ QueryTree parse(Parsable query);
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/parser/ParserEnvironment.java b/container-search/src/main/java/com/yahoo/search/query/parser/ParserEnvironment.java
new file mode 100644
index 00000000000..b00afa27bf6
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/parser/ParserEnvironment.java
@@ -0,0 +1,76 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.parser;
+
+import com.yahoo.language.Linguistics;
+import com.yahoo.language.simple.SimpleLinguistics;
+import com.yahoo.prelude.IndexFacts;
+import com.yahoo.prelude.query.parser.SpecialTokenRegistry;
+import com.yahoo.prelude.query.parser.SpecialTokens;
+import com.yahoo.search.Searcher;
+import com.yahoo.search.searchchain.Execution;
+
+/**
+ * This class encapsulates the environment of a {@link Parser}. In case you are creating a parser from within a
+ * {@link Searcher}, you can use the {@link #fromExecutionContext(Execution.Context)} factory for convenience.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ * @since 5.1.4
+ */
+public final class ParserEnvironment {
+
+ private IndexFacts indexFacts = new IndexFacts();
+ private Linguistics linguistics = new SimpleLinguistics();
+ private SpecialTokens specialTokens = new SpecialTokens();
+
+ public IndexFacts getIndexFacts() {
+ return indexFacts;
+ }
+
+ public ParserEnvironment setIndexFacts(IndexFacts indexFacts) {
+ this.indexFacts = indexFacts;
+ return this;
+ }
+
+ public Linguistics getLinguistics() {
+ return linguistics;
+ }
+
+ public ParserEnvironment setLinguistics(Linguistics linguistics) {
+ this.linguistics = linguistics;
+ return this;
+ }
+
+ public SpecialTokens getSpecialTokens() {
+ return specialTokens;
+ }
+
+ public ParserEnvironment setSpecialTokens(SpecialTokens specialTokens) {
+ this.specialTokens = specialTokens;
+ return this;
+ }
+
+ public static ParserEnvironment fromExecutionContext(Execution.Context context) {
+ ParserEnvironment env = new ParserEnvironment();
+ if (context == null) {
+ return env;
+ }
+ if (context.getIndexFacts() != null) {
+ env.setIndexFacts(context.getIndexFacts());
+ }
+ if (context.getLinguistics() != null) {
+ env.setLinguistics(context.getLinguistics());
+ }
+ SpecialTokenRegistry registry = context.getTokenRegistry();
+ if (registry != null) {
+ env.setSpecialTokens(registry.getSpecialTokens("default"));
+ }
+ return env;
+ }
+
+ public static ParserEnvironment fromParserEnvironment(ParserEnvironment environment) {
+ return new ParserEnvironment()
+ .setIndexFacts(environment.indexFacts)
+ .setLinguistics(environment.linguistics)
+ .setSpecialTokens(environment.specialTokens);
+ }
+}
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
new file mode 100644
index 00000000000..e0a3338fec2
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/parser/ParserFactory.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.parser;
+
+import com.yahoo.prelude.query.parser.*;
+import com.yahoo.search.Query;
+import com.yahoo.search.yql.YqlParser;
+
+/**
+ * <p>Implements a factory for {@link Parser}.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ * @since 5.1.4
+ */
+public final class ParserFactory {
+
+ private ParserFactory() {
+ // hide
+ }
+
+ /**
+ * Creates a {@link Parser} appropriate for the given <tt>Query.Type</tt>, providing the Parser with access to
+ * the {@link ParserEnvironment} given.
+ *
+ * @param type the query type for which to create a Parser
+ * @param environment the environment settings to attach to the Parser
+ * @return the created Parser
+ */
+ public static Parser newInstance(Query.Type type, ParserEnvironment environment) {
+ switch (type) {
+ case ALL:
+ return new AllParser(environment);
+ case ANY:
+ return new AnyParser(environment);
+ case PHRASE:
+ return new PhraseParser(environment);
+ case ADVANCED:
+ return new AdvancedParser(environment);
+ case WEB:
+ return new WebParser(environment);
+ case PROGRAMMATIC:
+ return new ProgrammaticParser();
+ case YQL:
+ return new YqlParser(environment);
+ default:
+ throw new UnsupportedOperationException(type.toString());
+ }
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/parser/package-info.java b/container-search/src/main/java/com/yahoo/search/query/parser/package-info.java
new file mode 100644
index 00000000000..ddae3e83ddb
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/parser/package-info.java
@@ -0,0 +1,10 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * Provides access to parsing query strings into queries
+ */
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query.parser;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/AllReferencesQueryProfileVisitor.java b/container-search/src/main/java/com/yahoo/search/query/profile/AllReferencesQueryProfileVisitor.java
new file mode 100644
index 00000000000..393aba2b002
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/AllReferencesQueryProfileVisitor.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileFieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+final class AllReferencesQueryProfileVisitor extends PrefixQueryProfileVisitor {
+
+ /** A map of query profile types */
+ private Set<CompoundName> references = new HashSet<>();
+
+ public AllReferencesQueryProfileVisitor(CompoundName prefix) {
+ super(prefix);
+ }
+
+ @Override
+ public void onValue(String name, Object value, DimensionBinding binding, QueryProfile owner) {}
+
+ @Override
+ public void onQueryProfileInsidePrefix(QueryProfile profile, DimensionBinding binding, QueryProfile owner) {
+ references.add(currentPrefix);
+ }
+
+ /** Returns the values resulting from this visiting */
+ public Set<CompoundName> getResult() { return references; }
+
+ /** Returns false - we are not done until we have seen all */
+ public boolean isDone() { return false; }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/AllTypesQueryProfileVisitor.java b/container-search/src/main/java/com/yahoo/search/query/profile/AllTypesQueryProfileVisitor.java
new file mode 100644
index 00000000000..fb9638a958b
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/AllTypesQueryProfileVisitor.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileFieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+final class AllTypesQueryProfileVisitor extends PrefixQueryProfileVisitor {
+
+ /** A map of query profile types */
+ private Map<CompoundName, QueryProfileType> types = new HashMap<>();
+
+ public AllTypesQueryProfileVisitor(CompoundName prefix) {
+ super(prefix);
+ }
+
+ @Override
+ public void onValue(String name, Object value, DimensionBinding binding, QueryProfile owner) {}
+
+
+ @Override
+ public void onQueryProfileInsidePrefix(QueryProfile profile, DimensionBinding binding, QueryProfile owner) {
+ if (profile.getType() != null)
+ addReachableTypes(currentPrefix, profile.getType());
+ }
+
+ private void addReachableTypes(CompoundName name, QueryProfileType type) {
+ types.put(name, type);
+ for (FieldDescription fieldDescription : type.fields().values()) {
+ if ( ! (fieldDescription.getType() instanceof QueryProfileFieldType)) continue;
+ QueryProfileFieldType fieldType = (QueryProfileFieldType)fieldDescription.getType();
+ if (fieldType.getQueryProfileType() !=null) {
+ addReachableTypes(name.append(fieldDescription.getName()), fieldType.getQueryProfileType());
+ }
+ }
+ }
+
+ /** Returns the values resulting from this visiting */
+ public Map<CompoundName, QueryProfileType> getResult() { return types; }
+
+ /** Returns false - we are not done until we have seen all */
+ public boolean isDone() { return false; }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/AllUnoverridableQueryProfileVisitor.java b/container-search/src/main/java/com/yahoo/search/query/profile/AllUnoverridableQueryProfileVisitor.java
new file mode 100644
index 00000000000..65c3480272e
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/AllUnoverridableQueryProfileVisitor.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.processing.request.CompoundName;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+final class AllUnoverridableQueryProfileVisitor extends PrefixQueryProfileVisitor {
+
+ /** A map of query profile types */
+ private Set<CompoundName> unoverridables = new HashSet<>();
+
+ public AllUnoverridableQueryProfileVisitor(CompoundName prefix) {
+ super(prefix);
+ }
+
+ @Override
+ public void onValue(String name, Object value, DimensionBinding binding, QueryProfile owner) {
+ addUnoverridable(name, currentPrefix.append(name), binding, owner);
+ }
+
+ @Override
+ public void onQueryProfileInsidePrefix(QueryProfile profile, DimensionBinding binding, QueryProfile owner) {
+ addUnoverridable(currentPrefix.last(), currentPrefix, binding, owner);
+ }
+
+ private void addUnoverridable(String localName, CompoundName fullName, DimensionBinding binding, QueryProfile owner) {
+ if (owner == null) return;
+
+ Boolean isOverridable = owner.isLocalOverridable(localName, binding);
+ if (isOverridable != null && ! isOverridable)
+ unoverridables.add(fullName);
+ }
+
+ /** Returns the values resulting from this visiting */
+ public Set<CompoundName> getResult() { return unoverridables; }
+
+ /** Returns false - we are not done until we have seen all */
+ public boolean isDone() { return false; }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/AllValuesQueryProfileVisitor.java b/container-search/src/main/java/com/yahoo/search/query/profile/AllValuesQueryProfileVisitor.java
new file mode 100644
index 00000000000..bef5b00c51b
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/AllValuesQueryProfileVisitor.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.processing.request.CompoundName;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+final class AllValuesQueryProfileVisitor extends PrefixQueryProfileVisitor {
+
+ private Map<String,Object> values=new HashMap<>();
+
+ /* Lists all values starting at prefix */
+ public AllValuesQueryProfileVisitor(CompoundName prefix) {
+ super(prefix);
+ }
+
+ public @Override void onValue(String localName, Object value, DimensionBinding binding, QueryProfile owner) {
+ putValue(localName, value, values);
+ }
+
+ public @Override void onQueryProfileInsidePrefix(QueryProfile profile, DimensionBinding binding, QueryProfile owner) {
+ putValue("", profile.getValue(), values);
+ }
+
+ private final void putValue(String key, Object value, Map<String, Object> values) {
+ if (value == null) return;
+ CompoundName fullName = currentPrefix.append(key);
+ if (fullName.isEmpty()) return; // Avoid putting a non-leaf (subtree) root in the list
+ if (values.containsKey(fullName.toString())) return; // The first value encountered has priority
+ values.put(fullName.toString(), value);
+ }
+
+ /** Returns the values resulting from this visiting */
+ public Map<String, Object> getResult() { return values; }
+
+ /** Returns false - we are not done until we have seen all */
+ public boolean isDone() { return false; }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/BackedOverridableQueryProfile.java b/container-search/src/main/java/com/yahoo/search/query/profile/BackedOverridableQueryProfile.java
new file mode 100644
index 00000000000..71b27c6da63
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/BackedOverridableQueryProfile.java
@@ -0,0 +1,139 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.protect.Validator;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>A wrapper of a query profile where overrides to the values in the referenced
+ * profile can be set.</p>
+ *
+ * <p>This is used to allow configured overrides (in a particular referencing profile) of a referenced query profile.
+ *
+ * <p>Properties which are defined as not overridable in the type (if any) of the referenced query profile
+ * cannot be set.</p>
+ *
+ * @author bratseth
+ */
+public class BackedOverridableQueryProfile extends OverridableQueryProfile implements Cloneable {
+
+ /** The backing read only query profile, or null if this is not backed */
+ private QueryProfile backingProfile;
+
+ /**
+ * Creates an overridable profile from the given backing profile. The backing profile will never be
+ * written to.
+ *
+ * @param backingProfile the backing profile, which is assumed read only, never null
+ */
+ public BackedOverridableQueryProfile(QueryProfile backingProfile) {
+ Validator.ensureNotNull("An overridable query profile must be backed by a real query profile",backingProfile);
+ setType(backingProfile.getType());
+ this.backingProfile=backingProfile;
+ }
+
+ @Override
+ public synchronized void freeze() {
+ super.freeze();
+ backingProfile.freeze();
+ }
+
+ @Override
+ protected Object localLookup(String localName, DimensionBinding dimensionBinding) {
+ Object valueInThis=super.localLookup(localName,dimensionBinding);
+ if (valueInThis!=null) return valueInThis;
+ return backingProfile.localLookup(localName,dimensionBinding);
+ }
+
+ protected Boolean isLocalInstanceOverridable(String localName) {
+ Boolean valueInThis=super.isLocalInstanceOverridable(localName);
+ if (valueInThis!=null) return valueInThis;
+ return backingProfile.isLocalInstanceOverridable(localName);
+ }
+
+ @Override
+ protected QueryProfile createSubProfile(String name,DimensionBinding dimensionBinding) {
+ Object backing=backingProfile.lookup(new CompoundName(name),true,dimensionBinding.createFor(backingProfile.getDimensions()));
+ if (backing!=null && backing instanceof QueryProfile)
+ return new BackedOverridableQueryProfile((QueryProfile)backing);
+ else
+ return new OverridableQueryProfile(); // Nothing is set in this branch, so nothing to override, but need override checking
+ }
+
+ /** Returns a clone of this which can be independently overridden, but which refers to the same backing profile */
+ @Override
+ public BackedOverridableQueryProfile clone() {
+ BackedOverridableQueryProfile clone=(BackedOverridableQueryProfile)super.clone();
+ return clone;
+ }
+
+ /** Returns the query profile backing this */
+ public QueryProfile getBacking() { return backingProfile; }
+
+ @Override
+ public void addInherited(QueryProfile inherited) {
+ backingProfile.addInherited(inherited);
+ }
+
+ void addInheritedHere(QueryProfile inherited) {
+ super.addInherited(inherited);
+ }
+
+ @Override
+ protected void visitVariants(boolean allowContent,QueryProfileVisitor visitor,DimensionBinding dimensionBinding) {
+ super.visitVariants(allowContent, visitor, dimensionBinding);
+ if (visitor.isDone()) return;
+ backingProfile.visitVariants(allowContent, visitor, dimensionBinding);
+ }
+
+ @Override
+ protected void visitInherited(boolean allowContent,QueryProfileVisitor visitor,DimensionBinding dimensionBinding, QueryProfile owner) {
+ super.visitInherited(allowContent,visitor,dimensionBinding, owner);
+ if (visitor.isDone()) return;
+ backingProfile.visitInherited(allowContent,visitor,dimensionBinding,owner);
+ }
+
+ /** Returns a value from the content of this: The value in this, or the value from the backing if not set in this */
+ protected Object getContent(String localKey) {
+ Object value=super.getContent(localKey);
+ if (value!=null) return value;
+ return backingProfile.getContent(localKey);
+ }
+
+ /**
+ * Returns all the content from this:
+ * All the values in this, and all values in the backing where an overriding value is not set in this
+ */
+ @Override
+ protected Map<String,Object> getContent() {
+ Map<String,Object> thisContent=super.getContent();
+ Map<String,Object> backingContent=backingProfile.getContent();
+ if (thisContent.isEmpty()) return backingContent; // Shortcut
+ if (backingContent.isEmpty()) return thisContent; // Shortcut
+ Map<String,Object> content=new HashMap<>(backingContent);
+ content.putAll(thisContent);
+ return content;
+ }
+
+ @Override
+ public String toString() {
+ return "overridable wrapper of " + backingProfile.toString();
+ }
+
+ @Override
+ public boolean isExplicit() {
+ return backingProfile.isExplicit();
+ }
+
+ @Override
+ public List<String> getDimensions() {
+ List<String> dimensions=super.getDimensions();
+ if (dimensions!=null) return dimensions;
+ return backingProfile.getDimensions();
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/CopyOnWriteContent.java b/container-search/src/main/java/com/yahoo/search/query/profile/CopyOnWriteContent.java
new file mode 100644
index 00000000000..3c02677b676
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/CopyOnWriteContent.java
@@ -0,0 +1,159 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.component.provider.FreezableClass;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A HashMap wrapper which can be cloned without copying the wrapped map.
+ * Copying of the map is deferred until there is a write access to the wrapped map.
+ * This may be frozen, at which point no further modifications are allowed.
+ * Note that <b>until</b> this is cloned, the internal map may be both read and written.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class CopyOnWriteContent extends FreezableClass implements Cloneable {
+
+ // TODO: Now that we used CompiledQueryProfiles at runtime we can remove this
+
+ // Possible states:
+ // WRITABLE: The map can be freely modified - it is only used by this
+ // -> !isFrozen() && (map!=null || unmodifiableMap==null)
+ // COPYONWRITE: The map is referred by at least one clone - further modification must cause a copy
+ // -> !isFrozen() && (map==null && unmodifiableMap!=null)
+ // FROZEN: No further changes are allowed to the state of this, ever
+ // -> isFrozen()
+
+ // Possible start states:
+ // WRITABLE: When created using the public constructor
+ // COPYONWRITE: When created by cloning
+
+ // Possible state transitions:
+ // WRITABLE->COPYONWRITE: When this is cloned
+ // COPYONWRITE->WRITABLE: When a clone is written to
+ // (COPYONWRITE,WRITABLE)->FROZEN: When a profile is frozen
+
+ /** The modifiable content of this. Null if this is empty or if this is not in the WRITABLE state */
+ private Map<String,Object> map=null;
+ /**
+ * If map is non-null this is either null (not instantiated yet) or an unmodifiable wrapper of map,
+ * if map is null this is either null (this is empty) or a reference to the map of the content this was cloned from
+ */
+ private Map<String,Object> unmodifiableMap =null;
+
+ /** Create a WRITABLE, empty instance */
+ public CopyOnWriteContent() {
+ }
+
+ /** Create a COPYONWRITE instance with some initial state */
+ private static CopyOnWriteContent createInCopyOnWriteState(Map<String,Object> unmodifiableMap) {
+ CopyOnWriteContent content=new CopyOnWriteContent();
+ content.unmodifiableMap = unmodifiableMap;
+ return content;
+ }
+
+ /** Create a WRITABLE instance with some initial state */
+ private static CopyOnWriteContent createInWritableState(Map<String,Object> map) {
+ CopyOnWriteContent content=new CopyOnWriteContent();
+ content.map = map;
+ return content;
+ }
+
+ @Override
+ public void freeze() {
+ // Freeze this
+ if (unmodifiableMap==null)
+ unmodifiableMap= map!=null ? Collections.unmodifiableMap(map) : Collections.<String, Object>emptyMap();
+ map=null; // just to keep the states simpler
+
+ // Freeze content
+ for (Map.Entry<String,Object> entry : unmodifiableMap.entrySet()) {
+ if (entry.getValue() instanceof QueryProfile)
+ ((QueryProfile)entry.getValue()).freeze();
+ }
+ super.freeze();
+ }
+
+ private boolean isEmpty() {
+ return (map==null || map.isEmpty()) && (unmodifiableMap ==null || unmodifiableMap.isEmpty());
+ }
+
+ private boolean isWritable() {
+ return !isFrozen() && (map!=null || unmodifiableMap==null);
+ }
+
+ @Override
+ public CopyOnWriteContent clone() {
+ if (isEmpty()) return new CopyOnWriteContent(); // No referencing is necessary in this case
+ if (isDeepUnmodifiable(unmodifiableMap())) {
+ // Create an instance pointing to this and put both in the COPYONWRITE state
+ unmodifiableMap(); // Make sure we have an unmodifiable reference to the map below
+ map=null; // Put this into the COPYONWRITE state (unless it is already frozen, in which case this is a noop)
+ return createInCopyOnWriteState(unmodifiableMap());
+ }
+ else {
+ // This contains query profiles, don't try to defer copying
+ return createInWritableState(deepClone(map));
+ }
+ }
+
+ private boolean isDeepUnmodifiable(Map<String,Object> map) {
+ for (Object value : map.values())
+ if (value instanceof QueryProfile && !((QueryProfile)value).isFrozen()) return false;
+ return true; // all other values are primitives
+ }
+
+ /** Deep clones a map - this handles all value types which can be found in a query profile */
+ static Map<String,Object> deepClone(Map<String,Object> map) {
+ if (map==null) return null;
+ Map<String,Object> mapClone=new HashMap<>(map.size());
+ for (Map.Entry<String,Object> entry : map.entrySet())
+ mapClone.put(entry.getKey(),QueryProfile.cloneIfNecessary(entry.getValue()));
+ return mapClone;
+ }
+
+
+ //------- Content access -------------------------------------------------------
+
+ public Map<String,Object> unmodifiableMap() {
+ if (isEmpty()) return Collections.emptyMap();
+ if (map==null) // in COPYONWRITE or FROZEN state
+ return unmodifiableMap;
+ // In WRITABLE state: Create unmodifiable wrapper if necessary and return it
+ if (unmodifiableMap==null)
+ unmodifiableMap=Collections.unmodifiableMap(map);
+ return unmodifiableMap;
+ }
+
+ public Object get(String key) {
+ if (map!=null) return map.get(key);
+ if (unmodifiableMap!=null) return unmodifiableMap.get(key);
+ return null;
+ }
+
+ public void put(String key,Object value) {
+ ensureNotFrozen();
+ copyIfNotWritable();
+ if (map==null)
+ map=new HashMap<>();
+ map.put(key,value);
+ }
+
+ public void remove(String key) {
+ ensureNotFrozen();
+ copyIfNotWritable();
+ if (map!=null)
+ map.remove(key);
+ }
+
+ private void copyIfNotWritable() {
+ if (isWritable()) return;
+ // move from COPYONWRITE to WRITABLE state
+ map=new HashMap<>(unmodifiableMap); // deep clone is not necessary as this map is shallowly modifiable
+ unmodifiableMap=null; // will be created as needed
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/DimensionBinding.java b/container-search/src/main/java/com/yahoo/search/query/profile/DimensionBinding.java
new file mode 100644
index 00000000000..9adacee74af
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/DimensionBinding.java
@@ -0,0 +1,223 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An immutable, binding of a list of dimensions to dimension values
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class DimensionBinding {
+
+ /** The dimensions of this */
+ private List<String> dimensions=null;
+
+ /** The values matching those dimensions */
+ private DimensionValues values;
+
+ /** The binding from those dimensions to values, and possibly other values */
+ private Map<String,String> context;
+
+ public static final DimensionBinding nullBinding =
+ new DimensionBinding(Collections.<String>unmodifiableList(Collections.<String>emptyList()), DimensionValues.empty, null);
+
+ public static final DimensionBinding invalidBinding =
+ new DimensionBinding(Collections.<String>unmodifiableList(Collections.<String>emptyList()), DimensionValues.empty, null);
+
+ /** Whether the value array contains only nulls */
+ private boolean containsAllNulls;
+
+ /** Creates a binding from a variant and a context. Any of the arguments may be null. */
+ public static DimensionBinding createFrom(List<String> dimensions, Map<String,String> context) {
+ if (dimensions==null || dimensions.size()==0) {
+ if (context==null) return nullBinding;
+ if (dimensions==null) return new DimensionBinding(null,DimensionValues.empty,context); // Null, but must preserve context
+ }
+
+ return new DimensionBinding(dimensions,extractDimensionValues(dimensions,context),context);
+ }
+
+ /** Creates a binding from a variant and a context. Any of the arguments may be null. */
+ public static DimensionBinding createFrom(List<String> dimensions, DimensionValues dimensionValues) {
+ if (dimensionValues==null || dimensionValues==DimensionValues.empty) return nullBinding;
+ if (dimensions==null) return new DimensionBinding(null,dimensionValues,null); // Null, but preserve raw material for creating a context later (in createFor)
+
+ return new DimensionBinding(dimensions,dimensionValues,null);
+ }
+
+ /** Returns a binding for a (possibly) new set of variants. Variants may be null, but not bindings */
+ public DimensionBinding createFor(List<String> newDimensions) {
+ if (newDimensions==null) return this; // Note: Not necessarily null - if no new variants then keep the existing binding
+ // if (this.context==null && values.length==0) return nullBinding; // No data from which to create a non-null binding
+ if (this.dimensions==newDimensions) return this; // Avoid creating a new object if the dimensions are the same
+
+ Map<String,String> context=this.context;
+ if (context==null)
+ context=this.values.asContext(this.dimensions !=null ? this.dimensions : newDimensions);
+ return new DimensionBinding(newDimensions,extractDimensionValues(newDimensions,context),context);
+ }
+
+ /**
+ * Creates a dimension binding. The dimensions list given should be unmodifiable.
+ * The array will not be modified. The context is needed in order to convert this binding to another
+ * given another set of variant dimensions.
+ */
+ private DimensionBinding(List<String> dimensions, DimensionValues values, Map<String,String> context) {
+ this.dimensions=dimensions;
+ this.values=values;
+ this.context = context;
+ containsAllNulls=values.isEmpty();
+ }
+
+ /** Returns a read-only list of the dimensions of this. This value is undefined if this isNull() */
+ public List<String> getDimensions() { return dimensions; }
+
+ /** Returns a context created from the dimensions and values of this */
+ public Map<String,String> getContext() {
+ if (context !=null) return context;
+ context =values.asContext(dimensions);
+ return context;
+ }
+
+ /**
+ * Returns the values for the dimensions of this. This value is undefined if this isEmpty()
+ * This array is always of the same length as the
+ * length of the dimension list - missing elements are represented as nulls.
+ * This is never null but may be empty.
+ */
+ public DimensionValues getValues() { return values; }
+
+ /** Returns true only if this binding is null (contains no values for its dimensions (if any) */
+ public boolean isNull() { return dimensions==null || containsAllNulls; }
+
+ /**
+ * Returns an array of the dimension values corresponding to the dimensions of this from the given context,
+ * in the corresponding order. The array is always of the same length as the number of dimensions.
+ * Dimensions which are not set in this context get a null value.
+ */
+ private static DimensionValues extractDimensionValues(List<String> dimensions,Map<String,String> context) {
+ String[] dimensionValues=new String[dimensions.size()];
+ if (context==null || context.size()==0) return DimensionValues.createFrom(dimensionValues);
+ for (int i=0; i<dimensions.size(); i++)
+ dimensionValues[i]=context.get(dimensions.get(i));
+ return DimensionValues.createFrom(dimensionValues);
+ }
+
+ /**
+ * Combines this binding with another if compatible.
+ * Two bindings are incompatible if
+ * <ul>
+ * <li>They contain a different value for the same key, or</li>
+ * <li>They contain the same pair of dimensions in a different order</li>
+ * </ul>
+ *
+ * @return the combined binding, or the special invalidBinding if these two bindings are incompatible
+ */
+ public DimensionBinding combineWith(DimensionBinding binding) {
+ List<String> combinedDimensions = combineDimensions(getDimensions(), binding.getDimensions());
+ if (combinedDimensions == null) return invalidBinding;
+
+ // not runtime, so assume we don't need to preserve values outside the dimensions
+ Map<String, String> combinedValues = combineValues(getContext(), binding.getContext());
+ if (combinedValues == null) return invalidBinding;
+
+ return DimensionBinding.createFrom(combinedDimensions, combinedValues);
+ }
+
+ /**
+ * Returns a combined list of dimensions from two separate lists,
+ * or null if they are incompatible.
+ * This is to combine two lists to one such that the partial order in both is preserved
+ * (or return null if impossible).
+ */
+ private List<String> combineDimensions(List<String> d1, List<String> d2) {
+ List<String> combined = new ArrayList<>();
+ int d1Index = 0, d2Index=0;
+ while (d1Index < d1.size() && d2Index < d2.size()) {
+ if (d1.get(d1Index).equals(d2.get(d2Index))) { // agreement on next element
+ combined.add(d1.get(d1Index));
+ d1Index++;
+ d2Index++;
+ }
+ else if ( ! d2.contains(d1.get(d1Index))) { // next in d1 is independent from d2
+ combined.add(d1.get(d1Index++));
+ }
+ else if ( ! d1.contains(d2.get(d2Index))) { // next in d2 is independent from d1
+ combined.add(d2.get(d2Index++));
+ }
+ else {
+ return null; // no independent and no agreement
+ }
+ }
+ if (d1Index < d1.size())
+ combined.addAll(d1.subList(d1Index, d1.size()));
+ else if (d2Index < d2.size())
+ combined.addAll(d2.subList(d2Index, d2.size()));
+
+ return combined;
+ }
+
+ /**
+ * Returns a combined map of dimension values from two separate maps,
+ * or null if they are incompatible.
+ */
+ private Map<String, String> combineValues(Map<String, String> m1, Map<String, String> m2) {
+ Map<String, String> combinedValues = new HashMap<>(m1);
+ for (Map.Entry<String, String> m2Entry : m2.entrySet()) {
+ if (m2Entry.getValue() == null) continue;
+ String m1Value = m1.get(m2Entry.getKey());
+ if (m1Value != null && ! m1Value.equals(m2Entry.getValue()))
+ return null; // conflicting values of a key
+ combinedValues.put(m2Entry.getKey(), m2Entry.getValue());
+ }
+ return combinedValues;
+ }
+
+ private boolean intersects(List<String> l1, List<String> l2) {
+ for (String l1Item : l1)
+ if (l2.contains(l1Item))
+ return true;
+ return false;
+ }
+
+ /**
+ * Returns true if <code>this == invalidBinding</code>
+ */
+ public boolean isInvalid() { return this == invalidBinding; }
+
+ @Override
+ public String toString() {
+ if (isInvalid()) return "Invalid DimensionBinding";
+ if (dimensions==null) return "DimensionBinding []";
+ StringBuilder b=new StringBuilder("DimensionBinding [");
+ for (int i=0; i<dimensions.size(); i++) {
+ b.append(dimensions.get(i)).append("=").append(values.get(i));
+ if (i<dimensions.size()-1)
+ b.append(", ");
+ }
+ b.append("]");
+ return b.toString();
+ }
+
+ /** Two bindings are equal if they contain the same dimensions and the same non-null values */
+ @Override
+ public boolean equals(Object o) {
+ if (o==this) return true;
+ if (! (o instanceof DimensionBinding)) return false;
+ DimensionBinding other = (DimensionBinding)o;
+ if ( ! this.dimensions.equals(other.dimensions)) return false;
+ if ( ! this.values.equals(other.values)) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return dimensions.hashCode() + 17 * values.hashCode();
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/DimensionValues.java b/container-search/src/main/java/com/yahoo/search/query/profile/DimensionValues.java
new file mode 100644
index 00000000000..10435c4c6b5
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/DimensionValues.java
@@ -0,0 +1,140 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An immutable set of dimension values.
+ * Note that this may contain more or fewer values than needed given a set of dimensions.
+ * Any missing values are treated as null.
+ */
+public class DimensionValues implements Comparable<DimensionValues> {
+
+ private final String[] values;
+
+ public static final DimensionValues empty=new DimensionValues(new String[] {});
+
+ public static DimensionValues createFrom(String[] values) {
+ if (values==null || values.length==0 || containsAllNulls(values)) return empty;
+ return new DimensionValues(values);
+ }
+
+ /**
+ * Creates a set of dimension values, where the input array <b>must</b> be of
+ * the right size, and where no copying is done.
+ *
+ * @param values the dimension values. This need not be normalized to the right size.
+ * The input array is copied by this.
+ */
+ private DimensionValues(String[] values) {
+ if (values==null) throw new NullPointerException("Dimension values cannot be null");
+ this.values=Arrays.copyOf(values,values.length);
+ }
+
+ /** Returns true if this is has the same value every place it has a value as the givenValues. */
+ public boolean matches(DimensionValues givenValues) {
+ for (int i=0; i<this.size() || i<givenValues.size() ; i++)
+ if ( ! matches(this.get(i),givenValues.get(i)))
+ return false;
+ return true;
+ }
+
+ private final boolean matches(String conditionString,String checkString) {
+ if (conditionString==null) return true;
+ return conditionString.equals(checkString);
+ }
+
+ /**
+ * Implements the sort order of this which is based on specificity
+ * where dimensions to the left are more significant:
+ * -1 is returned if this is more specific than other,
+ * 1 is returned if other is more specific than this,
+ * 0 is returned if none is more specific than the other.
+ * <p>
+ * <b>Note:</b> This ordering is not consistent with equals - it returns 0 when the same dimensions
+ * are <i>set</i>, regardless of what they are set <i>to</i>.
+ */
+ @Override
+ public int compareTo(DimensionValues other) {
+ for (int i=0; i<this.size() || i<other.size(); i++) {
+ if (get(i)!=null && other.get(i)==null)
+ return -1;
+ if (get(i)==null && other.get(i)!=null)
+ return 1;
+ }
+ return 0;
+ }
+
+ /** Helper method which uses compareTo to return whether this is most specific */
+ public boolean isMoreSpecificThan(DimensionValues other) {
+ return this.compareTo(other)<0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this==o) return true;
+ if ( ! (o instanceof DimensionValues)) return false;
+ DimensionValues other=(DimensionValues)o;
+ for (int i=0; i<this.size() || i<other.size(); i++) {
+ if (get(i)==null) {
+ if (other.get(i)!=null) return false;
+ }
+ else {
+ if ( ! get(i).equals(other.get(i))) return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 0;
+ int i = 0;
+ for (String value : values) {
+ i++;
+ if (value != null)
+ hashCode += value.hashCode() * i;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public String toString() { return Arrays.toString(values); }
+
+ public boolean isEmpty() {
+ return this==empty;
+ }
+
+ private static boolean containsAllNulls(String[] values) {
+ for (String value : values)
+ if (value!=null) return false;
+ return true;
+ }
+
+ public Map<String,String> asContext(List<String> dimensions) {
+ Map<String,String> context=new HashMap<>();
+ if (dimensions==null) return context;
+ for (int i=0; i<dimensions.size(); i++) {
+ context.put(dimensions.get(i),get(i));
+ }
+ return context;
+ }
+
+ /** Returns the string at the given index, <b>or null if it has no value at this index.</b> */
+ public String get(int index) {
+ if (index>=values.length) return null;
+ return values[index];
+ }
+
+ /** Returns the number of values in this (some of which may be null) */
+ public int size() { return values.length; }
+
+ /** Returns copy of the values in this in an array */
+ public String[] getValues() {
+ return Arrays.copyOf(values,values.length);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/DumpTool.java b/container-search/src/main/java/com/yahoo/search/query/profile/DumpTool.java
new file mode 100644
index 00000000000..b9d631cdd10
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/DumpTool.java
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import java.io.File;
+import java.util.Map;
+
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.search.Query;
+import com.yahoo.search.query.profile.config.QueryProfileXMLReader;
+
+/**
+ * A standalone tool for dumping query profile properties
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class DumpTool {
+
+ /** Creates and returns a dump from some parameters */
+ public String resolveAndDump(String... args) {
+ if (args.length==0 || args[0].startsWith("-")) {
+ StringBuilder result=new StringBuilder();
+ result.append("Dumps all resolved query profile properties for a set of dimension values\n");
+ result.append("USAGE: dump [query-profile] [dir]? [parameters]?\n");
+ result.append(" and [query-profile] is the name of the query profile to dump the values of\n");
+ result.append(" and [dir] is a path to an application package or query profile directory. Default: current dir\n");
+ result.append(" and [parameters] is the http request encoded dimension keys used during resolving. Default: none\n");
+ result.append("Examples:\n");
+ result.append(" dump default\n");
+ result.append(" - dumps the 'default' profile non-variant values in the current dir\n");
+ result.append(" dump default x=x1&y=y1\n");
+ result.append(" - dumps the 'default' profile resolved with dimensions values x=x1 and y=y1 in the current dir\n");
+ result.append(" dump default myapppackage\n");
+ result.append(" - dumps the 'default' profile non-variant values in myapppackage/search/query-profiles\n");
+ result.append(" dump default dev/myprofiles x=x1&y=y1\n");
+ result.append(" - dumps the 'default' profile resolved with dimensions values x=x1 and y=y1 in dev/myprofiles\n");
+ return result.toString();
+ }
+
+ // Find what the arguments means
+ if (args.length>=3) {
+ return dump(args[0],args[1],args[2]);
+ }
+ else if (args.length==2) {
+ if (args[1].indexOf("=")>=0)
+ return dump(args[0],"",args[1]);
+ else
+ return dump(args[0],args[1],"");
+ }
+ else { // args.length=1
+ return dump(args[0],"","");
+ }
+ }
+
+ private String dump(String profileName,String dir,String parameters) {
+ // Import profiles
+ if (dir.isEmpty())
+ dir=".";
+ File dirInAppPackage=new File(dir,"search/query-profiles");
+ if (dirInAppPackage.exists())
+ dir=dirInAppPackage.getPath();
+ QueryProfileXMLReader reader = new QueryProfileXMLReader();
+ QueryProfileRegistry registry = reader.read(dir);
+ registry.freeze();
+
+ // Dump (through query to get wiring & parameter parsing done easily)
+ Query query = new Query("?" + parameters, registry.compile().findQueryProfile(profileName));
+ Map<String,Object> properties=query.properties().listProperties();
+
+ // Create result
+ StringBuilder b=new StringBuilder();
+ for (Map.Entry<String,Object> property : properties.entrySet()) {
+ b.append(property.getKey());
+ b.append("=");
+ b.append(property.getValue().toString());
+ b.append("\n");
+ }
+ return b.toString();
+ }
+
+ public static void main(String... args) {
+ try {
+ System.out.print(new DumpTool().resolveAndDump(args));
+ }
+ catch (Exception e) {
+ System.err.println(Exceptions.toMessageString(e));
+ }
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/FieldDescriptionQueryProfileVisitor.java b/container-search/src/main/java/com/yahoo/search/query/profile/FieldDescriptionQueryProfileVisitor.java
new file mode 100644
index 00000000000..73c0fcd2cb1
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/FieldDescriptionQueryProfileVisitor.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.search.query.profile.types.FieldDescription;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+final class FieldDescriptionQueryProfileVisitor extends QueryProfileVisitor {
+
+ /** The result, or null if none */
+ private FieldDescription result = null;
+
+ private final List<String> name;
+
+ private int nameIndex=-1;
+
+ private boolean enteringContent=false;
+
+ public FieldDescriptionQueryProfileVisitor(List<String> name) {
+ this.name=name;
+ }
+
+ @Override
+ public String getLocalKey() {
+ return name.get(nameIndex);
+ }
+
+ @Override
+ public boolean enter(String name) {
+ if (nameIndex+2<this.name.size()) {
+ nameIndex++;
+ enteringContent=true;
+ }
+ else {
+ enteringContent=false;
+ }
+ return enteringContent;
+ }
+
+ @Override
+ public void leave(String name) {
+ nameIndex--;
+ }
+
+ @Override
+ public void onValue(String name,Object value, DimensionBinding binding, QueryProfile owner) {
+ }
+
+ @Override
+ public void onQueryProfile(QueryProfile profile, DimensionBinding binding, QueryProfile owner) {
+ if (enteringContent) return; // not at leaf query profile
+ if (profile.getType() == null) return;
+ result = profile.getType().getField(name.get(name.size()-1));
+ }
+
+ @Override
+ public boolean isDone() {
+ return result != null;
+ }
+
+ public FieldDescription result() { return result; }
+
+ @Override
+ public String toString() {
+ return "a query profile type visitor (hash " + hashCode() + ") with current value " + result;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/ModelObjectMap.java b/container-search/src/main/java/com/yahoo/search/query/profile/ModelObjectMap.java
new file mode 100644
index 00000000000..242c551f876
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/ModelObjectMap.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.profile.types.FieldType;
+import com.yahoo.search.query.properties.PropertyMap;
+
+/**
+ * A map which stores all types which cannot be stored in a query profile
+ * that is rich model objects.
+ * <p>
+ * This map will deep copy not only the model object map, but also each
+ * clonable member in the map.
+ *
+ * @author bratseth
+ */
+public class ModelObjectMap extends PropertyMap {
+
+ /** Returns true if the class of the value is not acceptable as a query profile value */
+ @Override
+ protected boolean shouldSet(CompoundName name,Object value) {
+ if (value==null) return true;
+ return FieldType.fromClass(value.getClass())==null;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/OverridableQueryProfile.java b/container-search/src/main/java/com/yahoo/search/query/profile/OverridableQueryProfile.java
new file mode 100644
index 00000000000..5d0bffa1ea8
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/OverridableQueryProfile.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+/**
+ * A regular query profile which knows it is storing overrides (not configured profiles)
+ * and that implements override legality checking.
+ *
+ * @author bratseth
+ */
+public class OverridableQueryProfile extends QueryProfile {
+
+ private static final String simpleClassName = OverridableQueryProfile.class.getSimpleName();
+
+ /** Creates an unbacked overridable query profile */
+ protected OverridableQueryProfile() {
+ super(ComponentId.createAnonymousComponentId(simpleClassName));
+ }
+
+ @Override
+ protected Object checkAndConvertAssignment(String localName, Object inputValue, QueryProfileRegistry registry) {
+ Object value=super.checkAndConvertAssignment(localName, inputValue, registry);
+ if (value!=null && value.getClass() == QueryProfile.class) { // We are assigning a query profile - make it overridable
+ return new BackedOverridableQueryProfile((QueryProfile)value);
+ }
+ return value;
+ }
+
+ @Override
+ protected QueryProfile createSubProfile(String name,DimensionBinding binding) {
+ return new OverridableQueryProfile(); // Nothing is set in this branch, so nothing to override, but need override checking
+ }
+
+ /** Returns a clone of this which can be independently overridden */
+ @Override
+ public OverridableQueryProfile clone() {
+ if (isFrozen()) return this;
+ OverridableQueryProfile clone=(OverridableQueryProfile)super.clone();
+ clone.initId(ComponentId.createAnonymousComponentId(simpleClassName));
+ return clone;
+ }
+
+ @Override
+ public String toString() {
+ return "an overridable query profile with no backing";
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/PrefixQueryProfileVisitor.java b/container-search/src/main/java/com/yahoo/search/query/profile/PrefixQueryProfileVisitor.java
new file mode 100644
index 00000000000..2a22d58d8b7
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/PrefixQueryProfileVisitor.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.processing.request.CompoundName;
+
+/**
+ * A query profile visitor which keeps track of name prefixes and can skip values outside a given prefix
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+abstract class PrefixQueryProfileVisitor extends QueryProfileVisitor {
+
+ /** Only call onValue/onQueryProfile for nodes having this prefix */
+ private final CompoundName prefix;
+
+ /** The current prefix, relative to prefix. */
+ protected CompoundName currentPrefix = CompoundName.empty;
+
+ private int prefixComponentIndex = -1;
+
+ public PrefixQueryProfileVisitor(CompoundName prefix) {
+ if (prefix == null)
+ prefix = CompoundName.empty;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public final void onQueryProfile(QueryProfile profile, DimensionBinding binding, QueryProfile owner) {
+ if (prefixComponentIndex < prefix.size()) return; // Not in the prefix yet
+ onQueryProfileInsidePrefix(profile, binding, owner);
+ }
+
+ protected abstract void onQueryProfileInsidePrefix(QueryProfile profile, DimensionBinding binding, QueryProfile owner);
+
+ @Override
+ public final boolean enter(String name) {
+ prefixComponentIndex++;
+ if (prefixComponentIndex-1 < prefix.size()) return true; // we're in the given prefix, which should not be included in the name
+ currentPrefix = currentPrefix.append(name);
+ return true;
+ }
+
+ @Override
+ public final void leave(String name) {
+ prefixComponentIndex--;
+ if (prefixComponentIndex < prefix.size()) return; // we're in the given prefix, which should not be included in the name
+ if ( ! name.isEmpty() && ! currentPrefix.isEmpty())
+ currentPrefix = currentPrefix.first(currentPrefix.size() - 1);
+ }
+
+ /**
+ * Returns the correct prefix component if we are still going down the prefix path,
+ * or null to get all if we are inside the prefix
+ */
+ @Override
+ public String getLocalKey() {
+ if (prefixComponentIndex < prefix.size())
+ return prefix.get(prefixComponentIndex);
+ else
+ return null;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfile.java b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfile.java
new file mode 100644
index 00000000000..55210717305
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfile.java
@@ -0,0 +1,835 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.FreezableSimpleComponent;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.processing.request.Properties;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfile;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileFieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A query profile is a data container with an id and a class (type). More precisely, it contains
+ * <ul>
+ * <li>An id, on the form name:version, where the version is optional, and follows the same rules as for other search container components.
+ * <li>A class id referring to the class defining this profile (see Query Profile Classes below)
+ * <li>A (possibly empty) list of ids of inherited query profiles
+ * <li>A (possibly empty) list of declarative predicates over search request parameters which defines when this query profile is applicable (see Query Profile Selection below)
+ * <li>The data content, which consists of
+ * <ul>
+ * <li>named values
+ * <li>named references to other profiles
+ * </ul>
+ * </ul>
+ *
+ * This serves the purpose of an intermediate format between configuration and runtime structures - the runtime
+ * structure used is QueryProfileProperties.
+ *
+ * @author bratseth
+ */
+public class QueryProfile extends FreezableSimpleComponent implements Cloneable {
+
+ /** Defines the permissible content of this, or null if any content is permissible */
+ private QueryProfileType type=null;
+
+ /** The value at this query profile - allows non-fields to have values, e.g a=value1, a.b=value2 */
+ private Object value=null;
+
+ /** The variants of this, or null if none */
+ private QueryProfileVariants variants=null;
+
+ /** The resolved variant dimensions of this, or null if none or not resolved yet (is resolved at freeze) */
+ private List<String> resolvedDimensions=null;
+
+ /** The query profiles inherited by this, or null if none */
+ private List<QueryProfile> inherited=null;
+
+ /** The content of this profile. The values may be primitives, substitutable strings or other query profiles */
+ private CopyOnWriteContent content=new CopyOnWriteContent();
+
+ /**
+ * Field override settings: fieldName→OverrideValue. These overrides the override
+ * setting in the type (if any) of this field). If there are no query profile level settings, this is null.
+ */
+ private Map<String,Boolean> overridable=null;
+
+ /**
+ * Creates a new query profile from an id.
+ * The query profile can be modified freely (but not accessed) until it is {@link #freeze frozen}.
+ * At that point it becomes readable but unmodifiable, which it stays until it goes out of reference.
+ */
+ public QueryProfile(ComponentId id) {
+ super(id);
+ if ( ! id.isAnonymous())
+ validateName(id.getName());
+ }
+
+ /** Convenience shorthand for new QueryProfile(new ComponentId(idString)) */
+ public QueryProfile(String idString) {
+ this(new ComponentId(idString));
+ }
+
+ // ----------------- Public API -------------------------------------------------------------------------------
+
+ // ----------------- Setters and getters
+
+ /** Returns the type of this or null if it has no type */
+ public QueryProfileType getType() { return type; }
+
+ /** Sets the type of this, or set to null to not use any type checking in this profile */
+ public void setType(QueryProfileType type) { this.type=type; }
+
+ /** Returns the virtual variants of this, or null if none */
+ public QueryProfileVariants getVariants() { return variants; }
+
+ /**
+ * Returns the list of profiles inherited by this.
+ * Note that order matters for inherited profiles - variables are resolved depth first in the order found in
+ * the inherited list. This always returns an unmodifiable list - use addInherited to add.
+ */
+ public List<QueryProfile> inherited() {
+ if (isFrozen()) return inherited; // Frozen profiles always have an unmodifiable, non-null list
+ if (inherited==null) return Collections.emptyList();
+ return Collections.unmodifiableList(inherited);
+ }
+
+ /** Adds a profile to the end of the inherited list of this. Throws an exception if this is frozen. */
+ public void addInherited(QueryProfile profile) {
+ addInherited(profile,(DimensionValues)null);
+ }
+
+ public final void addInherited(QueryProfile profile,String[] dimensionValues) {
+ addInherited(profile,DimensionValues.createFrom(dimensionValues));
+ }
+
+ /** Adds a profile to the end of the inherited list of this for the given variant. Throws an exception if this is frozen. */
+ public void addInherited(QueryProfile profile, DimensionValues dimensionValues) {
+ ensureNotFrozen();
+
+ DimensionBinding dimensionBinding=DimensionBinding.createFrom(getDimensions(),dimensionValues);
+ if (dimensionBinding.isNull()) {
+ if (inherited==null)
+ inherited=new ArrayList<>();
+ inherited.add(profile);
+ }
+ else {
+ if (variants==null)
+ variants=new QueryProfileVariants(dimensionBinding.getDimensions(), this);
+ variants.inherit(profile,dimensionBinding.getValues());
+ }
+ }
+
+ /**
+ * Returns the content fields declared in this (i.e not including those inherited) as a read-only map.
+ * @throws IllegalStateException if this is frozen
+ */
+ public Map<String,Object> declaredContent() {
+ ensureNotFrozen();
+ return content.unmodifiableMap();
+ }
+
+ /**
+ * Returns if the given field is declared explicitly as overridable or not in this or any <i>nested</i> profiles
+ * (i.e not including overridable settings <i>inherited</i> and from <i>types</i>).
+ *
+ * @param name the (possibly dotted) field name to return
+ * @param context the context in which the name is resolved, or null if none
+ * @return true/false if this is declared overridable/not overridable in this instance, null if it is not
+ * given any value is <i>this</i> profile instance
+ * @throws IllegalStateException if this is frozen
+ */
+ public Boolean isDeclaredOverridable(String name, Map<String,String> context) {
+ return isDeclaredOverridable(new CompoundName(name),DimensionBinding.createFrom(getDimensions(),context));
+ }
+
+ /** Sets the dimensions over which this may vary. Note: This will erase any currently defined variants */
+ public void setDimensions(String[] dimensions) {
+ ensureNotFrozen();
+ variants=new QueryProfileVariants(dimensions, this);
+ }
+
+ /** Returns the value set at this node, to allow non-leafs to have values. Returns null if none. */
+ public Object getValue() { return value; }
+
+ public void setValue(Object value) {
+ ensureNotFrozen();
+ this.value=value;
+ }
+
+ /** Returns the variant dimensions to be used in this - an unmodifiable list of dimension names */
+ public List<String> getDimensions() {
+ if (isFrozen()) return resolvedDimensions;
+ if (variants!=null) return variants.getDimensions();
+ if (inherited==null) return null;
+ for (QueryProfile inheritedProfile : inherited) {
+ List<String> inheritedDimensions=inheritedProfile.getDimensions();
+ if (inheritedDimensions!=null) return inheritedDimensions;
+ }
+ return null;
+ }
+
+ // ----------------- Query profile facade API
+
+ /**
+ * Sets the overridability of a field in this profile,
+ * this overrides the corresponding setting in the type (if any)
+ */
+ public final void setOverridable(String fieldName, boolean overridable, Map<String,String> context) {
+ setOverridable(new CompoundName(fieldName), overridable,DimensionBinding.createFrom(getDimensions(), context));
+ }
+
+ /**
+ * Return all objects that start with the given prefix path using no context. Use "" to list all.
+ * <p>
+ * For example, if {a.d =&gt; "a.d-value" ,a.e =&gt; "a.e-value", b.d =&gt; "b.d-value", then calling listValues("a")
+ * will return {"d" =&gt; "a.d-value","e" =&gt; "a.e-value"}
+ */
+ public final Map<String, Object> listValues(String prefix) { return listValues(new CompoundName(prefix)); }
+
+ /**
+ * Return all objects that start with the given prefix path using no context. Use "" to list all.
+ * <p>
+ * For example, if {a.d =&gt; "a.d-value" ,a.e =&gt; "a.e-value", b.d =&gt; "b.d-value", then calling listValues("a")
+ * will return {"d" =&gt; "a.d-value","e" =&gt; "a.e-value"}
+ */
+ public final Map<String, Object> listValues(CompoundName prefix) { return listValues(prefix, null); }
+
+ /**
+ * Return all objects that start with the given prefix path. Use "" to list all.
+ * <p>
+ * For example, if {a.d =&gt; "a.d-value" ,a.e =&gt; "a.e-value", b.d =&gt; "b.d-value", then calling listValues("a")
+ * will return {"d" =&gt; "a.d-value","e" =&gt; "a.e-value"}
+ */
+ public final Map<String, Object> listValues(String prefix, Map<String,String> context) {
+ return listValues(new CompoundName(prefix), context);
+ }
+
+ /**
+ * Return all objects that start with the given prefix path. Use "" to list all.
+ * <p>
+ * For example, if {a.d =&gt; "a.d-value" ,a.e =&gt; "a.e-value", b.d =&gt; "b.d-value", then calling listValues("a")
+ * will return {"d" =&gt; "a.d-value","e" =&gt; "a.e-value"}
+ */
+ public final Map<String, Object> listValues(CompoundName prefix, Map<String,String> context) {
+ return listValues(prefix, context, null);
+ }
+
+ /**
+ * Adds all objects that start with the given path prefix to the given value map. Use "" to list all.
+ * <p>
+ * For example, if {a.d =&gt; "a.d-value" ,a.e =&gt; "a.e-value", b.d =&gt; "b.d-value", then calling listValues("a")
+ * will return {"d" =&gt; "a.d-value","e" =&gt; "a.e-value"}
+ */
+ public Map<String, Object> listValues(CompoundName prefix, Map<String, String> context, Properties substitution) {
+ DimensionBinding dimensionBinding=DimensionBinding.createFrom(getDimensions(),context);
+
+ AllValuesQueryProfileVisitor visitor=new AllValuesQueryProfileVisitor(prefix);
+ accept(visitor,dimensionBinding, null);
+ Map<String,Object> values=visitor.getResult();
+
+ if (substitution==null) return values;
+ for (Map.Entry<String,Object> entry : values.entrySet()) {
+ if (entry.getValue().getClass()==String.class) continue; // Shortcut
+ if (entry.getValue() instanceof SubstituteString)
+ entry.setValue(((SubstituteString)entry.getValue()).substitute(context,substitution));
+ }
+ return values;
+ }
+
+ /**
+ * Lists types reachable from this, indexed by the prefix having that type.
+ * If this is itself typed, this' type will be included with an empty prefix
+ */
+ Map<CompoundName, QueryProfileType> listTypes(CompoundName prefix, Map<String, String> context) {
+ DimensionBinding dimensionBinding = DimensionBinding.createFrom(getDimensions(), context);
+ AllTypesQueryProfileVisitor visitor = new AllTypesQueryProfileVisitor(prefix);
+ accept(visitor, dimensionBinding, null);
+ return visitor.getResult();
+ }
+
+ /**
+ * Lists references reachable from this.
+ */
+ Set<CompoundName> listReferences(CompoundName prefix, Map<String, String> context) {
+ DimensionBinding dimensionBinding=DimensionBinding.createFrom(getDimensions(),context);
+ AllReferencesQueryProfileVisitor visitor=new AllReferencesQueryProfileVisitor(prefix);
+ accept(visitor,dimensionBinding,null);
+ return visitor.getResult();
+ }
+
+ /**
+ * Lists every entry (value or reference) reachable from this which is not overridable
+ */
+ Set<CompoundName> listUnoverridable(CompoundName prefix, Map<String, String> context) {
+ DimensionBinding dimensionBinding = DimensionBinding.createFrom(getDimensions(),context);
+ AllUnoverridableQueryProfileVisitor visitor = new AllUnoverridableQueryProfileVisitor(prefix);
+ accept(visitor, dimensionBinding, null);
+ return visitor.getResult();
+ }
+
+ /**
+ * Returns a value from this query profile by resolving the given name:
+ * <ul>
+ * <li>The name up to the first dot is the value looked up in the value of this profile
+ * <li>The rest of the name (if any) is used as the name to look up in the referenced query profile
+ * </ul>
+ *
+ * If this name does not resolve <i>completely</i> into a value in this or any inherited profile, null is returned.
+ */
+ public final Object get(String name) { return get(name,(Map<String,String>)null); }
+
+ /** Returns a value from this using the given property context for resolution and using this for substitution */
+ public final Object get(String name, Map<String,String> context) {
+ return get(name,context,null);
+ }
+
+ /** Returns a value from this using the given dimensions for resolution */
+ public final Object get(String name, String[] dimensionValues) {
+ return get(name,dimensionValues,null);
+ }
+
+ public final Object get(String name, String[] dimensionValues, Properties substitution) {
+ return get(name,DimensionValues.createFrom(dimensionValues),substitution);
+ }
+
+ /** Returns a value from this using the given dimensions for resolution */
+ public final Object get(String name, DimensionValues dimensionValues, Properties substitution) {
+ return get(name,DimensionBinding.createFrom(getDimensions(),dimensionValues),substitution);
+ }
+
+ public final Object get(String name, Map<String,String> context, Properties substitution) {
+ return get(name,DimensionBinding.createFrom(getDimensions(),context),substitution);
+ }
+
+ public final Object get(CompoundName name, Map<String,String> context, Properties substitution) {
+ return get(name,DimensionBinding.createFrom(getDimensions(),context),substitution);
+ }
+
+ final Object get(String name, DimensionBinding binding,Properties substitution) {
+ return get(new CompoundName(name),binding,substitution);
+ }
+
+ final Object get(CompoundName name, DimensionBinding binding, Properties substitution) {
+ Object node=get(name,binding);
+ if (node!=null && node.getClass()==String.class) return node; // Shortcut
+ if (node instanceof SubstituteString) return ((SubstituteString)node).substitute(binding.getContext(),substitution);
+ return node;
+ }
+
+ final Object get(CompoundName name,DimensionBinding dimensionBinding) {
+ return lookup(name,false,dimensionBinding);
+ }
+
+ /**
+ * Returns the node at the position prescribed by the given name (without doing substitutions) -
+ * a primitive value, a substitutable string, a query profile, or null if not found.
+ */
+ public final Object lookup(String name, Map<String,String> context) {
+ return lookup(new CompoundName(name),true,DimensionBinding.createFrom(getDimensions(),context));
+ }
+
+ /** Sets a value in this or any nested profile using null as context */
+ public final void set(String name, Object value, QueryProfileRegistry registry) {
+ set(name,value,(Map<String,String>)null, registry);
+ }
+
+ /**
+ * Sets a value in this or any nested profile. Any missing structure needed to set this will be created.
+ * If this value is already set, this will overwrite the previous value.
+ *
+ * @param name the name of the field, possibly a dotted name which will cause setting of a variable in a subprofile
+ * @param value the value to assign to the name, a primitive wrapper, string or a query profile
+ * @param context the context used to resolve where this value should be set, or null if none
+ * @throws IllegalArgumentException if the given name is illegal given the types of this or any nested query profile
+ * @throws IllegalStateException if this query profile is frozen
+ */
+ public final void set(CompoundName name,Object value,Map<String,String> context, QueryProfileRegistry registry) {
+ set(name, value, DimensionBinding.createFrom(getDimensions(), context), registry);
+ }
+
+ public final void set(String name,Object value,Map<String,String> context, QueryProfileRegistry registry) {
+ set(new CompoundName(name), value, DimensionBinding.createFrom(getDimensions(), context), registry);
+ }
+
+ public final void set(String name,Object value,String[] dimensionValues, QueryProfileRegistry registry) {
+ set(name,value,DimensionValues.createFrom(dimensionValues), registry);
+ }
+
+ /**
+ * Sets a value in this or any nested profile. Any missing structure needed to set this will be created.
+ * If this value is already set, this will overwrite the previous value.
+ *
+ * @param name the name of the field, possibly a dotted name which will cause setting of a variable in a subprofile
+ * @param value the value to assign to the name, a primitive wrapper, string or a query profile
+ * @param dimensionValues the dimension values - will be matched by order to the dimensions set in this - if this is
+ * shorter or longer than the number of dimensions it will be adjusted as needed
+ * @param registry the registry used to resolve query profile references. If null is passed query profile references
+ * will cause an exception
+ * @throws IllegalArgumentException if the given name is illegal given the types of this or any nested query profile
+ * @throws IllegalStateException if this query profile is frozen
+ */
+ public final void set(String name,Object value,DimensionValues dimensionValues, QueryProfileRegistry registry) {
+ set(new CompoundName(name), value, DimensionBinding.createFrom(getDimensions(), dimensionValues), registry);
+ }
+
+ // ----------------- Misc
+
+ public boolean isExplicit() {
+ return !getId().isAnonymous();
+ }
+
+ /**
+ * Switches this from write-only to read-only mode.
+ * This profile can never be modified again after this method returns.
+ * Calling this on an already frozen profile has no effect.
+ * <p>
+ * Calling this will also freeze any profiles inherited and referenced by this.
+ */
+ // TODO: Remove/simplify as query profiles are not used at query time
+ public synchronized void freeze() {
+ if (isFrozen()) return;
+
+ resolvedDimensions=getDimensions();
+
+ if (variants !=null)
+ variants.freeze();
+
+ if (inherited!=null) {
+ for (QueryProfile inheritedProfile : inherited)
+ inheritedProfile.freeze();
+ }
+
+ content.freeze();
+
+ inherited= inherited==null ? ImmutableList.of() : ImmutableList.copyOf(inherited);
+
+ super.freeze();
+ }
+
+ @Override
+ public String toString() {
+ return "query profile '" + getId() + "'" + (type!=null ? " of type '" + type.getId() + "'" : "");
+ }
+
+ /**
+ * Returns a clone of this. The clone will not be frozen and will contain copied inherited and content collections
+ * pointing to the same values as this.
+ */
+ @Override
+ public QueryProfile clone() {
+ if (isFrozen()) return this;
+ QueryProfile clone=(QueryProfile)super.clone();
+ if (variants !=null)
+ clone.variants = variants.clone();
+ if (inherited!=null)
+ clone.inherited=new ArrayList<>(inherited);
+
+ if (this.content!=null)
+ clone.content=content.clone();
+
+ return clone;
+ }
+
+ /**
+ * Clones a value of a type which may appear in a query profile if cloning is necessary (i.e if it is
+ * not immutable). Returns the input type otherwise.
+ */
+ static Object cloneIfNecessary(Object object) {
+ if (object instanceof QueryProfile) return ((QueryProfile)object).clone();
+ return object; // Other types are immutable
+ }
+
+ /** Throws IllegalArgumentException if the given string is not a valid query profile name */
+ public static void validateName(String name) {
+ Matcher nameMatcher=namePattern.matcher(name);
+ if ( ! nameMatcher.matches())
+ throw new IllegalArgumentException("Illegal name '" + name + "'");
+ }
+
+ // ----------------- For subclass use --------------------------------------------------------------------
+
+ /** Override this to intercept all writes to this profile (or any nested profiles) */
+ protected void set(CompoundName name, Object value, DimensionBinding binding, QueryProfileRegistry registry) {
+ try {
+ setNode(name, value, null, binding, registry);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Could not set '" + name + "' to '" + value + "'",e);
+ }
+ }
+
+ /** Returns this value, or its corresponding substitution string if it contains substitutions */
+ protected Object convertToSubstitutionString(Object value) {
+ if (value==null) return value;
+ if (value.getClass()!=String.class) return value;
+ SubstituteString substituteString=SubstituteString.create((String)value);
+ if (substituteString==null) return value;
+ return substituteString;
+ }
+
+ /** Returns the field description of this field, or null if it is not typed */
+ protected FieldDescription getFieldDescription(CompoundName name, DimensionBinding binding) {
+ FieldDescriptionQueryProfileVisitor visitor=new FieldDescriptionQueryProfileVisitor(name.asList());
+ accept(visitor, binding,null);
+ return visitor.result();
+ }
+
+ /**
+ * Returns true if this value is definitely overridable in this (set and not unoverridable),
+ * false if it is declared unoverridable (in instance or type), and null if this profile has no
+ * opinion on the matter because the value is not set in this.
+ */
+ Boolean isLocalOverridable(String localName,DimensionBinding binding) {
+ if (localLookup(localName, binding)==null) return null; // Not set
+ Boolean isLocalInstanceOverridable=isLocalInstanceOverridable(localName);
+ if (isLocalInstanceOverridable!=null)
+ return isLocalInstanceOverridable.booleanValue();
+ if (type!=null) return type.isOverridable(localName);
+ return true;
+ }
+
+ protected Boolean isLocalInstanceOverridable(String localName) {
+ if (overridable==null) return null;
+ return overridable.get(localName);
+ }
+
+ protected Object lookup(CompoundName name,boolean allowQueryProfileResult, DimensionBinding dimensionBinding) {
+ SingleValueQueryProfileVisitor visitor=new SingleValueQueryProfileVisitor(name.asList(),allowQueryProfileResult);
+ accept(visitor,dimensionBinding,null);
+ return visitor.getResult();
+ }
+
+ protected final void accept(QueryProfileVisitor visitor,DimensionBinding dimensionBinding, QueryProfile owner) {
+ acceptAndEnter("", visitor, dimensionBinding, owner);
+ }
+
+ void acceptAndEnter(String key, QueryProfileVisitor visitor,DimensionBinding dimensionBinding, QueryProfile owner) {
+ boolean allowContent=visitor.enter(key);
+ accept(allowContent, visitor, dimensionBinding, owner);
+ if (allowContent)
+ visitor.leave(key);
+ }
+
+ /**
+ * Visit the profiles and values referenced from this in order of decreasing precedence
+ *
+ * @param allowContent whether content in this should be visited
+ * @param visitor the visitor
+ * @param dimensionBinding the dimension binding to use
+ */
+ final void accept(boolean allowContent,QueryProfileVisitor visitor, DimensionBinding dimensionBinding, QueryProfile owner) {
+ visitor.onQueryProfile(this, dimensionBinding, owner);
+ if (visitor.isDone()) return;
+
+ visitVariants(allowContent,visitor,dimensionBinding);
+ if (visitor.isDone()) return;
+
+ if (allowContent) {
+ visitContent(visitor,dimensionBinding);
+ if (visitor.isDone()) return;
+ }
+
+ if (visitor.visitInherited())
+ visitInherited(allowContent, visitor, dimensionBinding, owner);
+ }
+
+ protected void visitVariants(boolean allowContent,QueryProfileVisitor visitor,DimensionBinding dimensionBinding) {
+ if (getVariants()!=null)
+ getVariants().accept(allowContent, getType(), visitor, dimensionBinding);
+ }
+
+ protected void visitInherited(boolean allowContent,QueryProfileVisitor visitor,DimensionBinding dimensionBinding, QueryProfile owner) {
+ if (inherited==null) return;
+ for (QueryProfile inheritedProfile : inherited) {
+ inheritedProfile.accept(allowContent,visitor,dimensionBinding.createFor(inheritedProfile.getDimensions()), owner);
+ if (visitor.isDone()) return;
+ }
+ }
+
+ private void visitContent(QueryProfileVisitor visitor,DimensionBinding dimensionBinding) {
+ String contentKey=visitor.getLocalKey();
+
+ // Visit this' content
+ if (contentKey!=null) { // Get only the content of the current key
+ if (type!=null)
+ contentKey=type.unalias(contentKey);
+ visitor.acceptValue(contentKey, getContent(contentKey), dimensionBinding, this);
+ }
+ else { // get all content in this
+ for (Map.Entry<String,Object> entry : getContent().entrySet()) {
+ visitor.acceptValue(entry.getKey(), entry.getValue(), dimensionBinding, this);
+ if (visitor.isDone()) return;
+ }
+ }
+ }
+
+ /** Returns a value from the content of this, or null if not present */
+ protected Object getContent(String key) {
+ return content.get(key);
+ }
+
+ /** Returns all the content from this as an unmodifiable map */
+ protected Map<String,Object> getContent() {
+ return content.unmodifiableMap();
+ }
+
+ /** Sets the value of a node in <i>this</i> profile - the local name given must not be nested (contain dots) */
+ protected QueryProfile setLocalNode(String localName, Object value,QueryProfileType parentType,
+ DimensionBinding dimensionBinding, QueryProfileRegistry registry) {
+ if (parentType!=null && type==null && !isFrozen())
+ type=parentType;
+
+ value=checkAndConvertAssignment(localName, value, registry);
+ localPut(localName,value,dimensionBinding);
+ return this;
+ }
+
+ /**
+ * Combines an existing and a new value for a query property key.
+ * Return the new object to add to the state of the owning profile (/variant), or null if no new value needs to
+ * be added (usually because the new value was added to the existing).
+ */
+ static Object combineValues(Object newValue, Object existingValue) {
+ if (newValue instanceof QueryProfile) {
+ QueryProfile newProfile=(QueryProfile)newValue;
+ if ( existingValue==null || ! (existingValue instanceof QueryProfile)) {
+ if (!isModifiable(newProfile))
+ newProfile=new BackedOverridableQueryProfile(newProfile); // Make the query profile reference overridable
+ newProfile.value=existingValue;
+ return newProfile;
+ }
+
+ // if both are profiles:
+ return combineProfiles(newProfile,(QueryProfile)existingValue);
+ }
+ else {
+ if (existingValue instanceof QueryProfile) { // we need to set a non-leaf value on a query profile
+ QueryProfile existingProfile=(QueryProfile)existingValue;
+ if (isModifiable(existingProfile)) {
+ existingProfile.setValue(newValue);
+ return null;
+ }
+ else {
+ QueryProfile existingOverridable = new BackedOverridableQueryProfile((QueryProfile)existingValue);
+ existingOverridable.setValue(newValue);
+ return existingOverridable;
+ }
+ }
+ else {
+ return newValue;
+ }
+ }
+ }
+
+ private static QueryProfile combineProfiles(QueryProfile newProfile,QueryProfile existingProfile) {
+ QueryProfile returnValue=null;
+ QueryProfile existingModifiable;
+
+ // Ensure the existing profile is modifiable
+ if (existingProfile.getClass()==QueryProfile.class) {
+ existingModifiable = new BackedOverridableQueryProfile(existingProfile);
+ returnValue=existingModifiable;
+ }
+ else { // is an overridable wrapper
+ existingModifiable=existingProfile; // May be used as-is
+ }
+
+ // Make the existing profile inherit the new one
+ if (existingModifiable instanceof BackedOverridableQueryProfile)
+ ((BackedOverridableQueryProfile)existingModifiable).addInheritedHere(newProfile);
+ else
+ existingModifiable.addInherited(newProfile);
+
+ // Remove content from the existing which the new one does not allow overrides of
+ if (existingModifiable.content!=null) {
+ for (String key : existingModifiable.content.unmodifiableMap().keySet()) {
+ if ( ! newProfile.isLocalOverridable(key, null)) {
+ existingModifiable.content.remove(key);
+ }
+ }
+ }
+
+ return returnValue;
+ }
+
+ /** Returns whether the given profile may be modified from this profile */
+ private static boolean isModifiable(QueryProfile profile) {
+ if (profile.isFrozen()) return false;
+ if ( ! profile.isExplicit()) return true; // Implicitly defined from this - ok to modify then
+ if (! (profile instanceof BackedOverridableQueryProfile)) return false;
+ return true;
+ }
+
+ /**
+ * Converts to the type of the receiving field, if possible and necessary.
+ *
+ * @return the value to be assigned: the original or a converted value
+ * @throws IllegalArgumentException if the assignment is illegal
+ */
+ protected Object checkAndConvertAssignment(String localName, Object value, QueryProfileRegistry registry) {
+ if (type==null) return value; // no type checking
+
+ FieldDescription fieldDescription=type.getField(localName);
+ if (fieldDescription==null) {
+ if (type.isStrict())
+ throw new IllegalArgumentException("'" + localName + "' is not declared in " + type + ", and the type is strict");
+ return value;
+ }
+
+ if (registry == null && (fieldDescription.getType() instanceof QueryProfileFieldType))
+ throw new IllegalArgumentException("A registry was not passed: Query profile references is not supported");
+ Object convertedValue = fieldDescription.getType().convertFrom(value, registry);
+ if (convertedValue == null)
+ throw new IllegalArgumentException("'" + value + "' is not a " + fieldDescription.getType().toInstanceDescription());
+ return convertedValue;
+ }
+
+ /**
+ * Looks up all inherited profiles and adds any that matches this name.
+ * This default implementation returns an empty profile.
+ */
+ protected QueryProfile createSubProfile(String name,DimensionBinding dimensionBinding) {
+ QueryProfile queryProfile = new QueryProfile(ComponentId.createAnonymousComponentId(name));
+ return queryProfile;
+ }
+
+ /** Do a variant-aware content lookup in this */
+ protected Object localLookup(String name, DimensionBinding dimensionBinding) {
+ Object node=null;
+ if ( variants!=null && !dimensionBinding.isNull())
+ node=variants.get(name,type,true,dimensionBinding);
+ if (node==null)
+ node=content==null ? null : content.get(name);
+ return node;
+ }
+
+ // ----------------- Private ----------------------------------------------------------------------------------
+
+ private Boolean isDeclaredOverridable(CompoundName name,DimensionBinding dimensionBinding) {
+ QueryProfile parent= lookupParentExact(name, true, dimensionBinding);
+ if (parent.overridable==null) return null;
+ return parent.overridable.get(name.last());
+ }
+
+ /**
+ * Sets the overridability of a field in this profile,
+ * this overrides the corresponding setting in the type (if any)
+ */
+ private void setOverridable(CompoundName fieldName,boolean overridable,DimensionBinding dimensionBinding) {
+ QueryProfile parent= lookupParentExact(fieldName, true, dimensionBinding);
+ if (parent.overridable==null)
+ parent.overridable=new HashMap<>();
+ parent.overridable.put(fieldName.last(),overridable);
+ }
+
+ /** Sets a value to a (possibly non-local) node. The parent query profile holding the value is returned */
+ private void setNode(CompoundName name, Object value, QueryProfileType parentType,
+ DimensionBinding dimensionBinding, QueryProfileRegistry registry) {
+ ensureNotFrozen();
+ if (name.isCompound()) {
+ QueryProfile parent= getQueryProfileExact(name.first(), true, dimensionBinding);
+ parent.setNode(name.rest(), value,parentType, dimensionBinding.createFor(parent.getDimensions()), registry);
+ }
+ else {
+ setLocalNode(name.toString(), value,parentType, dimensionBinding, registry);
+ }
+ }
+
+ /**
+ * Looks up and, if necessary, creates, the query profile which should hold the given local name portion of the
+ * given name. If the name contains no dots, this is returned.
+ *
+ * @param name the name of the variable to lookup the parent of
+ * @param create whether or not to create the parent if it is not present
+ * @return the parent, or null if not present and created is false
+ */
+ private QueryProfile lookupParentExact(CompoundName name, boolean create, DimensionBinding dimensionBinding) {
+ CompoundName rest=name.rest();
+ if (rest.isEmpty()) return this;
+
+ QueryProfile topmostParent= getQueryProfileExact(name.first(), create, dimensionBinding);
+ if (topmostParent==null) return null;
+ return topmostParent.lookupParentExact(rest, create, dimensionBinding.createFor(topmostParent.getDimensions()));
+ }
+
+ /**
+ * Returns a query profile from this by name
+ *
+ * @param localName the local name of the profile in this, this is never a compound
+ * @param create whether the profile should be created if missing
+ * @return the created profile, or null if not present, and create is false
+ */
+ private QueryProfile getQueryProfileExact(String localName, boolean create, DimensionBinding dimensionBinding) {
+ Object node=localExactLookup(localName, dimensionBinding);
+ if (node!=null && node instanceof QueryProfile) {
+ return (QueryProfile)node;
+ }
+ if (!create) return null;
+
+ QueryProfile queryProfile=createSubProfile(localName,dimensionBinding);
+ if (type!=null) {
+ Class<?> legalClass=type.getValueClass(localName);
+ if (legalClass==null || ! legalClass.isInstance(queryProfile))
+ throw new RuntimeException("'" + localName + "' is not a legal query profile reference name in " + this);
+ queryProfile.setType(type.getType(localName));
+ }
+ localPut(localName,queryProfile,dimensionBinding);
+ return queryProfile;
+ }
+
+ /** Do a variant-aware content lookup in this - without looking in any wrapped content. But by matching variant bindings exactly only */
+ private Object localExactLookup(String name,DimensionBinding dimensionBinding) {
+ if (dimensionBinding.isNull()) return content==null ? null : content.get(name);
+ if (variants==null) return null;
+ QueryProfileVariant variant=variants.getVariant(dimensionBinding.getValues(),false);
+ if (variant==null) return null;
+ return variant.values().get(name);
+ }
+
+ /** Sets a value directly in this query profile (unless frozen) */
+ private void localPut(String localName,Object value,DimensionBinding dimensionBinding) {
+ ensureNotFrozen();
+
+ if (type!=null)
+ localName=type.unalias(localName);
+
+ validateName(localName);
+ value=convertToSubstitutionString(value);
+
+ if (dimensionBinding.isNull()) {
+ Object combinedValue;
+ if (value instanceof QueryProfile)
+ combinedValue = combineValues(value,content==null ? null : content.get(localName));
+ else
+ combinedValue = combineValues(value, localLookup(localName, dimensionBinding));
+
+ if (combinedValue!=null)
+ content.put(localName,combinedValue);
+ }
+ else {
+ if (variants==null)
+ variants=new QueryProfileVariants(dimensionBinding.getDimensions(), this);
+ variants.set(localName,dimensionBinding.getValues(),value);
+ }
+ }
+
+ private static final Pattern namePattern=Pattern.compile("[$a-zA-Z_/][-$a-zA-Z0-9_/()]*");
+
+ /**
+ * Returns a compiled version of this which produces faster lookup times
+ *
+ * @param registry the registry this will be added to by the caller, or null if none
+ */
+ public CompiledQueryProfile compile(CompiledQueryProfileRegistry registry) {
+ return QueryProfileCompiler.compile(this, registry);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileCompiler.java b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileCompiler.java
new file mode 100644
index 00000000000..795c7655dfb
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileCompiler.java
@@ -0,0 +1,140 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfile;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.search.query.profile.compiled.DimensionalMap;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Compile a set of query profiles into compiled profiles.
+ *
+ * @author bratseth
+ */
+public class QueryProfileCompiler {
+
+ private static final Logger log = Logger.getLogger(QueryProfileCompiler.class.getName());
+
+ public static CompiledQueryProfileRegistry compile(QueryProfileRegistry input) {
+ CompiledQueryProfileRegistry output = new CompiledQueryProfileRegistry(input.getTypeRegistry());
+ for (QueryProfile inputProfile : input.allComponents()) {
+ output.register(compile(inputProfile, output));
+ }
+ return output;
+ }
+
+ public static CompiledQueryProfile compile(QueryProfile in, CompiledQueryProfileRegistry registry) {
+ DimensionalMap.Builder<CompoundName, Object> values = new DimensionalMap.Builder<>();
+ DimensionalMap.Builder<CompoundName, QueryProfileType> types = new DimensionalMap.Builder<>();
+ DimensionalMap.Builder<CompoundName, Object> references = new DimensionalMap.Builder<>();
+ DimensionalMap.Builder<CompoundName, Object> unoverridables = new DimensionalMap.Builder<>();
+
+ // Resolve values for each existing variant and combine into a single data structure
+ Set<DimensionBindingForPath> variants = new HashSet<>();
+ collectVariants(CompoundName.empty, in, DimensionBinding.nullBinding, variants);
+ variants.add(new DimensionBindingForPath(DimensionBinding.nullBinding, CompoundName.empty)); // if this contains no variants
+ if (log.isLoggable(Level.FINE))
+ log.fine("Compiling " + in.toString() + " having " + variants.size() + " variants");
+ int i = 0;
+ for (DimensionBindingForPath variant : variants) {
+ if (log.isLoggable(Level.FINER))
+ log.finer(" Compiling variant " + i++ + ": " + variant);
+ for (Map.Entry<String, Object> entry : in.listValues(variant.path(), variant.binding().getContext(), null).entrySet())
+ values.put(variant.path().append(entry.getKey()), variant.binding(), entry.getValue());
+ for (Map.Entry<CompoundName, QueryProfileType> entry : in.listTypes(variant.path(), variant.binding().getContext()).entrySet())
+ types.put(variant.path().append(entry.getKey()), variant.binding(), entry.getValue());
+ for (CompoundName reference : in.listReferences(variant.path(), variant.binding().getContext()))
+ references.put(variant.path().append(reference), variant.binding(), Boolean.TRUE); // Used as a set; value is ignored
+ for (CompoundName name : in.listUnoverridable(variant.path(), variant.binding().getContext()))
+ unoverridables.put(variant.path().append(name), variant.binding(), Boolean.TRUE); // Used as a set; value is ignored
+ }
+
+ return new CompiledQueryProfile(in.getId(), in.getType(),
+ values.build(), types.build(), references.build(), unoverridables.build(),
+ registry);
+ }
+
+ /**
+ * Returns all the unique combinations of dimension values which have values set reachable from this profile.
+ *
+ * @param profile the profile we are collecting the variants of
+ * @param currentVariant the variant we must have to arrive at this point in the query profile graph
+ * @param allVariants the set of all variants accumulated so far
+ */
+ private static void collectVariants(CompoundName path, QueryProfile profile, DimensionBinding currentVariant, Set<DimensionBindingForPath> allVariants) {
+ for (QueryProfile inheritedProfile : profile.inherited())
+ collectVariants(path, inheritedProfile, currentVariant, allVariants);
+
+ collectVariantsFromValues(path, profile.getContent(), currentVariant, allVariants);
+
+ collectVariantsInThis(path, profile, currentVariant, allVariants);
+ if (profile instanceof BackedOverridableQueryProfile)
+ collectVariantsInThis(path, ((BackedOverridableQueryProfile) profile).getBacking(), currentVariant, allVariants);
+ }
+
+ private static void collectVariantsInThis(CompoundName path, QueryProfile profile, DimensionBinding currentVariant, Set<DimensionBindingForPath> allVariants) {
+ QueryProfileVariants profileVariants = profile.getVariants();
+ if (profileVariants != null) {
+ for (QueryProfileVariant variant : profile.getVariants().getVariants()) {
+ DimensionBinding combinedVariant =
+ DimensionBinding.createFrom(profile.getDimensions(), variant.getDimensionValues()).combineWith(currentVariant);
+ if (combinedVariant.isInvalid()) continue; // values at this point in the graph are unreachable
+ collectVariantsFromValues(path, variant.values(), combinedVariant, allVariants);
+ for (QueryProfile variantInheritedProfile : variant.inherited())
+ collectVariants(path, variantInheritedProfile, combinedVariant, allVariants);
+ }
+ }
+ }
+
+ private static void collectVariantsFromValues(CompoundName path, Map<String, Object> values, DimensionBinding currentVariant, Set<DimensionBindingForPath> allVariants) {
+ if ( ! values.isEmpty())
+ allVariants.add(new DimensionBindingForPath(currentVariant, path)); // there are actual values for this variant
+
+ for (Map.Entry<String, Object> entry : values.entrySet()) {
+ if (entry.getValue() instanceof QueryProfile)
+ collectVariants(path.append(entry.getKey()), (QueryProfile)entry.getValue(), currentVariant, allVariants);
+ }
+ }
+
+ private static class DimensionBindingForPath {
+
+ private final DimensionBinding binding;
+ private final CompoundName path;
+
+ public DimensionBindingForPath(DimensionBinding binding, CompoundName path) {
+ this.binding = binding;
+ this.path = path;
+ }
+
+ public DimensionBinding binding() { return binding; }
+ public CompoundName path() { return path; }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( o == this ) return true;
+ if ( ! (o instanceof DimensionBindingForPath)) return false;
+ DimensionBindingForPath other = (DimensionBindingForPath)o;
+ return other.binding.equals(this.binding) && other.path.equals(this.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return binding.hashCode() + 17*path.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return binding + " for path " + path;
+ }
+
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileProperties.java b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileProperties.java
new file mode 100644
index 00000000000..2432cb2ab33
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileProperties.java
@@ -0,0 +1,258 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.processing.request.properties.PropertyMap;
+import com.yahoo.protect.Validator;
+import com.yahoo.search.query.Properties;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfile;
+import com.yahoo.search.query.profile.compiled.DimensionalValue;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Properties backed by a query profile.
+ * This has the scope of one query and is not multithread safe.
+ *
+ * @author bratseth
+ */
+public class QueryProfileProperties extends Properties {
+
+ private final CompiledQueryProfile profile;
+
+ // Note: The priority order is: values has precedence over references
+
+ /** Values which has been overridden at runtime, or null if none */
+ private Map<CompoundName, Object> values = null;
+ /** Query profile references which has been overridden at runtime, or null if none. Earlier values has precedence */
+ private List<Pair<CompoundName, CompiledQueryProfile>> references = null;
+
+ /** Creates an instance from a profile, throws an exception if the given profile is null */
+ public QueryProfileProperties(CompiledQueryProfile profile) {
+ Validator.ensureNotNull("The profile wrapped by this cannot be null", profile);
+ this.profile = profile;
+ }
+
+ /** Returns the query profile backing this, or null if none */
+ public CompiledQueryProfile getQueryProfile() { return profile; }
+
+ /** Gets a value from the query profile, or from the nested profile if the value is null */
+ @Override
+ public Object get(CompoundName name, Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ name = unalias(name, context);
+ Object value = null;
+ if (values != null)
+ value = values.get(name);
+ if (value == null) {
+ Pair<CompoundName, CompiledQueryProfile> reference = findReference(name);
+ if (reference != null)
+ return reference.getSecond().get(name.rest(reference.getFirst().size()), context, substitution); // yes; even if null
+ }
+
+ if (value == null)
+ value = profile.get(name, context, substitution);
+ if (value == null)
+ value = super.get(name, context, substitution);
+ return value;
+ }
+
+ /**
+ * Sets a value in this query profile
+ *
+ * @throws IllegalArgumentException if this property cannot be set in the wrapped query profile
+ */
+ @Override
+ public void set(CompoundName name, Object value, Map<String,String> context) {
+ // TODO: Refactor
+ try {
+ name = unalias(name, context);
+
+ if (context == null)
+ context = Collections.emptyMap();
+
+ if ( ! profile.isOverridable(name, context)) return;
+
+ // Check runtime references
+ Pair<CompoundName, CompiledQueryProfile> runtimeReference = findReference(name);
+ if (runtimeReference != null && ! runtimeReference.getSecond().isOverridable(name.rest(runtimeReference.getFirst().size()), context))
+ return;
+
+ // Check types
+ if ( ! profile.getTypes().isEmpty()) {
+ for (int i = 0; i<name.size(); i++) {
+ QueryProfileType type = profile.getType(name.first(i), context);
+ if (type == null) continue;
+ String localName = name.get(i);
+ FieldDescription fieldDescription = type.getField(localName);
+ if (fieldDescription == null && type.isStrict())
+ throw new IllegalArgumentException("'" + localName + "' is not declared in " + type + ", and the type is strict");
+
+ // TODO: In addition to strictness, check legality along the way
+
+ if (i == name.size()-1 && fieldDescription != null) { // at the end of the path, check the assignment type
+ value = fieldDescription.getType().convertFrom(value, profile.getRegistry());
+ if (value == null)
+ throw new IllegalArgumentException("'" + value + "' is not a " + fieldDescription.getType().toInstanceDescription());
+ }
+ }
+ }
+
+ if (value instanceof String && value.toString().startsWith("ref:")) {
+ if (profile.getRegistry() == null)
+ throw new IllegalArgumentException("Runtime query profile references does not work when the " +
+ "QueryProfileProperties are constructed without a registry");
+ String queryProfileId = value.toString().substring(4);
+ value = profile.getRegistry().findQueryProfile(queryProfileId);
+ if (value == null)
+ throw new IllegalArgumentException("Query profile '" + queryProfileId + "' is not found");
+ }
+
+ if (value instanceof CompiledQueryProfile) { // this will be due to one of the two clauses above
+ if (references == null)
+ references = new ArrayList<>();
+ references.add(0, new Pair<>(name, (CompiledQueryProfile)value)); // references set later has precedence - put first
+ }
+ else {
+ if (values == null)
+ values = new HashMap<>();
+ values.put(name, value);
+ }
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Could not set '" + name + "' to '" + value + "': " + e.getMessage()); // TODO: Nest instead
+ }
+ }
+
+ @Override
+ public Map<String, Object> listProperties(CompoundName path, Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ path = unalias(path, context);
+ if (context == null) context = Collections.emptyMap();
+
+ Map<String, Object> properties = profile.listValues(path, context, substitution);
+
+ properties.putAll(super.listProperties(path, context, substitution));
+
+ if (references != null) {
+ for (Pair<CompoundName, CompiledQueryProfile> refEntry : references) {
+ if ( ! refEntry.getFirst().hasPrefix(path.first(Math.min(refEntry.getFirst().size(), path.size())))) continue;
+
+ CompoundName pathInReference;
+ CompoundName prefixToReferenceKeys;
+ if (refEntry.getFirst().size() > path.size()) {
+ pathInReference = CompoundName.empty;
+ prefixToReferenceKeys = refEntry.getFirst().rest(path.size());
+ }
+ else {
+ pathInReference = path.rest(refEntry.getFirst().size());
+ prefixToReferenceKeys = CompoundName.empty;
+ }
+ for (Map.Entry<String, Object> valueEntry : refEntry.getSecond().listValues(pathInReference, context, substitution).entrySet()) {
+ properties.put(prefixToReferenceKeys.append(new CompoundName(valueEntry.getKey())).toString(), valueEntry.getValue());
+ }
+ }
+
+ }
+
+ if (values != null) {
+ for (Map.Entry<CompoundName, Object> entry : values.entrySet()) {
+ if (entry.getKey().hasPrefix(path))
+ properties.put(entry.getKey().rest(path.size()).toString(), entry.getValue());
+ }
+ }
+
+ return properties;
+ }
+
+ public boolean isComplete(StringBuilder firstMissingName, Map<String,String> context) {
+ // Are all types reachable from this complete?
+ if ( ! reachableTypesAreComplete(CompoundName.empty, profile, firstMissingName, context))
+ return false;
+
+ // Are all runtime references in this complete?
+ if (references == null) return true;
+ for (Pair<CompoundName, CompiledQueryProfile> reference : references) {
+ if ( ! reachableTypesAreComplete(reference.getFirst(), reference.getSecond(), firstMissingName, context))
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean reachableTypesAreComplete(CompoundName prefix, CompiledQueryProfile profile, StringBuilder firstMissingName, Map<String,String> context) {
+ for (Map.Entry<CompoundName, DimensionalValue<QueryProfileType>> typeEntry : profile.getTypes().entrySet()) {
+ QueryProfileType type = typeEntry.getValue().get(context);
+ if (type == null) continue;
+ if ( ! typeIsComplete(prefix.append(typeEntry.getKey()), type, firstMissingName, context))
+ return false;
+ }
+ return true;
+ }
+
+ private boolean typeIsComplete(CompoundName prefix, QueryProfileType type, StringBuilder firstMissingName, Map<String,String> context) {
+ if (type == null) return true;
+ for (FieldDescription field : type.fields().values()) {
+ if ( ! field.isMandatory()) continue;
+
+ CompoundName fieldName = prefix.append(field.getName());
+ if ( get(fieldName, null) != null) continue;
+ if ( hasReference(fieldName)) continue;
+
+ if (profile.getReferences().get(fieldName, context) != null) continue;
+
+ if (firstMissingName != null)
+ firstMissingName.append(fieldName);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean hasReference(CompoundName name) {
+ if (references == null) return false;
+ for (Pair<CompoundName, CompiledQueryProfile> reference : references)
+ if (reference.getFirst().equals(name))
+ return true;
+ return false;
+ }
+
+ private Pair<CompoundName, CompiledQueryProfile> findReference(CompoundName name) {
+ if (references == null) return null;
+ for (Pair<CompoundName, CompiledQueryProfile> entry : references) {
+ if (name.hasPrefix(entry.getFirst())) return entry;
+ }
+ return null;
+ }
+
+ CompoundName unalias(CompoundName name, Map<String,String> context) {
+ if (profile.getTypes().isEmpty()) return name;
+
+ CompoundName unaliasedName = name;
+ for (int i = 0; i<name.size(); i++) {
+ QueryProfileType type = profile.getType(name.first(i), context);
+ if (type == null) continue;
+ if (type.aliases() == null) continue; // TODO: Make never null
+ if (type.aliases().isEmpty()) continue;
+ String localName = name.get(i);
+ String unaliasedLocalName = type.unalias(localName);
+ unaliasedName = unaliasedName.set(i, unaliasedLocalName);
+ }
+ return unaliasedName;
+ }
+
+ @Override
+ public QueryProfileProperties clone() {
+ QueryProfileProperties clone = (QueryProfileProperties)super.clone();
+ if (this.values != null)
+ clone.values = PropertyMap.cloneMap(this.values);
+ return clone;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileRegistry.java b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileRegistry.java
new file mode 100644
index 00000000000..a4bca752d18
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileRegistry.java
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.QueryProfileTypeRegistry;
+
+/**
+ * A set of query profiles. This also holds the query profile types as a dependent registry
+ *
+ * @author bratseth
+ */
+public class QueryProfileRegistry extends ComponentRegistry<QueryProfile> {
+
+ private QueryProfileTypeRegistry queryProfileTypeRegistry = new QueryProfileTypeRegistry();
+
+ /** The current default instance of this registry */
+ private static QueryProfileRegistry instance = new QueryProfileRegistry();
+
+ /** Register this type by its id */
+ public void register(QueryProfile profile) {
+ super.register(profile.getId(), profile);
+ }
+
+ /** Returns a query profile type by name, or null if not found */
+ public QueryProfileType getType(String type) {
+ return queryProfileTypeRegistry.getComponent(type);
+ }
+
+ /** Returns the type registry attached to this */
+ public QueryProfileTypeRegistry getTypeRegistry() { return queryProfileTypeRegistry; }
+
+ /**
+ * <p>Returns a query profile for the given request string, or null if a suitable one is not found.</p>
+ *
+ * The request string must be a valid {@link com.yahoo.component.ComponentId} or null.
+ *
+ * <p>
+ * If the string is null, the profile named "default" is returned, or null if that does not exists.
+ *
+ * <p>
+ * The version part (if any) is matched used the usual component version patching rules.
+ * If the name part matches a query profile name perfectly, that profile is returned.
+ * If not, and the name is a slash-separated path, the profile with the longest matching left sub-path
+ * which has a type which allows path mahting is used. If there is no such profile, null is returned.
+ */
+ public QueryProfile findQueryProfile(String idString) {
+ if (idString==null) return getComponent("default");
+ ComponentSpecification id=new ComponentSpecification(idString);
+ QueryProfile profile=getComponent(id);
+ if (profile!=null) return profile;
+
+ return findPathParentQueryProfile(new ComponentSpecification(idString));
+ }
+
+ private QueryProfile findPathParentQueryProfile(ComponentSpecification id) {
+ // Try the name with "/" appended - should have the same semantics with path matching
+ QueryProfile slashedProfile=getComponent(new ComponentSpecification(id.getName() + "/",id.getVersionSpecification()));
+ if (slashedProfile!=null && slashedProfile.getType()!=null && slashedProfile.getType().getMatchAsPath())
+ return slashedProfile;
+
+ // Extract the parent (if any)
+ int slashIndex=id.getName().lastIndexOf("/");
+ if (slashIndex<1) return null;
+ String parentName=id.getName().substring(0,slashIndex);
+ if (parentName.equals("")) return null;
+
+ ComponentSpecification parentId=new ComponentSpecification(parentName,id.getVersionSpecification());
+
+ QueryProfile pathParentProfile=getComponent(parentId);
+
+ if (pathParentProfile!=null && pathParentProfile.getType()!=null && pathParentProfile.getType().getMatchAsPath())
+ return pathParentProfile;
+ return findPathParentQueryProfile(parentId);
+ }
+
+ /** Freezes this, and all owned query profiles and query profile types */
+ public @Override void freeze() {
+ if (isFrozen()) return;
+ queryProfileTypeRegistry.freeze();
+ for (QueryProfile queryProfile : allComponents())
+ queryProfile.freeze();
+ }
+
+ public CompiledQueryProfileRegistry compile() { return QueryProfileCompiler.compile(this); }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVariant.java b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVariant.java
new file mode 100644
index 00000000000..42ea4a96d8f
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVariant.java
@@ -0,0 +1,157 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.*;
+
+/**
+ * A variant of a query profile
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+*/
+public class QueryProfileVariant implements Cloneable, Comparable<QueryProfileVariant> {
+
+ private List<QueryProfile> inherited=null;
+
+ private DimensionValues dimensionValues;
+
+ private Map<String,Object> values;
+
+ private boolean frozen=false;
+
+ private QueryProfile owner;
+
+ public QueryProfileVariant(DimensionValues dimensionValues, QueryProfile owner) {
+ this.dimensionValues=dimensionValues;
+ this.owner = owner;
+ }
+
+ public DimensionValues getDimensionValues() { return dimensionValues; }
+
+ /**
+ * Returns the live reference to the values of this. This may be modified
+ * if this is not frozen.
+ */
+ public Map<String,Object> values() {
+ if (values==null) {
+ if (frozen)
+ return Collections.emptyMap();
+ else
+ values=new HashMap<>();
+ }
+ return values;
+ }
+
+ /**
+ * Returns the live reference to the inherited profiles of this. This may be modified
+ * if this is not frozen.
+ */
+ public List<QueryProfile> inherited() {
+ if (inherited==null) {
+ if (frozen)
+ return Collections.emptyList();
+ else
+ inherited=new ArrayList<>();
+ }
+ return inherited;
+ }
+
+ public void set(String key, Object newValue) {
+ if (values==null)
+ values=new HashMap<>();
+
+ Object oldValue = values.get(key);
+
+ if (oldValue == null) {
+ values.put(key, newValue);
+ } else {
+ Object combinedOrNull = QueryProfile.combineValues(newValue, oldValue);
+ if (combinedOrNull != null) {
+ values.put(key, combinedOrNull);
+ }
+ }
+ }
+
+ public void inherit(QueryProfile profile) {
+ if (inherited==null)
+ inherited=new ArrayList<>(1);
+ inherited.add(profile);
+ }
+
+ /**
+ * Implements the sort order of this which is based on specificity
+ * where dimensions to the left are more significant.
+ * <p>
+ * <b>Note:</b> This ordering is not consistent with equals - it returns 0 when the same dimensions
+ * are <i>set</i>, regardless of what they are set <i>to</i>.
+ */
+ public @Override int compareTo(QueryProfileVariant other) {
+ return this.dimensionValues.compareTo(other.dimensionValues);
+ }
+
+ public boolean matches(DimensionValues givenDimensionValues) {
+ return this.dimensionValues.matches(givenDimensionValues);
+ }
+
+ /** Accepts a visitor to the values of this */
+ public void accept(boolean allowContent,QueryProfileType type,QueryProfileVisitor visitor, DimensionBinding dimensionBinding) {
+ // Visit this
+ if (allowContent) {
+ String key=visitor.getLocalKey();
+ if (key!=null) {
+ if (type!=null)
+ type.unalias(key);
+
+ visitor.acceptValue(key, values().get(key), dimensionBinding, owner);
+ if (visitor.isDone()) return;
+ }
+ else {
+ for (Map.Entry<String,Object> entry : values().entrySet()) {
+ visitor.acceptValue(entry.getKey(), entry.getValue(), dimensionBinding, owner);
+ if (visitor.isDone()) return;
+ }
+ }
+ }
+
+ // Visit inherited
+ for (QueryProfile profile : inherited()) {
+ if (visitor.visitInherited()) {
+ profile.accept(allowContent,visitor,dimensionBinding.createFor(profile.getDimensions()), owner);
+ }
+ if (visitor.isDone()) return;
+ }
+ }
+
+ public void freeze() {
+ if (frozen) return;
+ if (inherited != null)
+ inherited = ImmutableList.copyOf(inherited);
+ if (values != null)
+ values = ImmutableMap.copyOf(values);
+ frozen=true;
+ }
+
+ public QueryProfileVariant clone() {
+ if (frozen) return this;
+ try {
+ QueryProfileVariant clone=(QueryProfileVariant)super.clone();
+ if (this.inherited!=null)
+ clone.inherited=new ArrayList<>(this.inherited); // TODO: Deep clone is more correct, but probably does not matter in practice
+
+ clone.values=CopyOnWriteContent.deepClone(this.values);
+
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public @Override String toString() {
+ return "query profile variant for " + dimensionValues;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVariants.java b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVariants.java
new file mode 100644
index 00000000000..fde851bdc75
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVariants.java
@@ -0,0 +1,486 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.yahoo.component.provider.Freezable;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.*;
+
+/**
+ * This class represent a set of query profiles virtually - rather
+ * than storing and instantiating each profile this structure represents explicitly only
+ * the values set in the various virtual profiles. The set of virtual profiles are defined by a set of
+ * <i>dimensions</i>. Values may be set for any point in this multi-dimensional space, and may also be set for
+ * any regular hyper-region by setting values for any point in certain of these dimensions.
+ * The set of virtual profiles defined by this consists of all the combinations of dimension points for
+ * which one or more values is set in this, as well as any possible less specified regions.
+ * <p>
+ * A set of virtual profiles are always owned by a single profile, which is also their parent
+ * in the inheritance hierarchy.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class QueryProfileVariants implements Freezable, Cloneable {
+
+ private boolean frozen=false;
+
+ /** Properties indexed by name, to support fast lookup of single values */
+ private Map<String,FieldValues> fieldValuesByName=new HashMap<>();
+
+ /** The inherited profiles for various dimensions settings - a set of fieldvalues of List&lt;QueryProfile&gt; */
+ private FieldValues inheritedProfiles=new FieldValues();
+
+ /**
+ * Field and inherited profiles sorted by specificity used for all-value visiting.
+ * This is the same as how the source data looks (apart from the sorting).
+ */
+ private List<QueryProfileVariant> variants=new ArrayList<>();
+
+ /**
+ * The names of the dimensions (which are possible properties in the context given on lookup) of this.
+ * Order matters - more specific values to the left in this list are more significant than more specific values
+ * to the right
+ */
+ private List<String> dimensions;
+
+ /** The query profile this variants of */
+ private QueryProfile owner;
+
+ /**
+ * Creates a set of virtual query profiles which may return varying values over the set of dimensions given.
+ * Each dimension is a name for which a key-value may be supplied in the context properties
+ * on lookup time to influence the value returned.
+ */
+ public QueryProfileVariants(String[] dimensions, QueryProfile owner) {
+ this(Arrays.asList(dimensions), owner);
+ }
+
+ /**
+ * Creates a set of virtual query profiles which may return varying values over the set of dimensions given.
+ * Each dimension is a name for which a key-value may be supplied in the context properties
+ * on lookup time to influence the value returned.
+ *
+ * @param dimensions the dimension names this may vary over. The list gets owned by this, so it must not be further
+ * modified from outside). This will not modify the list.
+ */
+ public QueryProfileVariants(List<String> dimensions, QueryProfile owner) {
+ // Note: This is not made unmodifiable (here or in freeze) because we depend on map identity comparisons of this
+ // list (in dimensionBinding) for performance reasons.
+ this.dimensions = dimensions;
+ this.owner = owner;
+ }
+
+ /** Irreversibly prevents any further modifications to this */
+ public void freeze() {
+ if (frozen) return;
+ for (FieldValues fieldValues : fieldValuesByName.values())
+ fieldValues.freeze();
+ fieldValuesByName = ImmutableMap.copyOf(fieldValuesByName);
+ inheritedProfiles.freeze();
+
+ Collections.sort(variants);
+ for (QueryProfileVariant variant : variants)
+ variant.freeze();
+ variants = ImmutableList.copyOf(variants);
+
+ frozen=true;
+ }
+
+ @Override
+ public boolean isFrozen() {
+ return frozen;
+ }
+
+ /** Visits the most specific match to the dimension binding of each variable (or the one named by the visitor) */
+ void accept(boolean allowContent,QueryProfileType type,QueryProfileVisitor visitor,DimensionBinding dimensionBinding) {
+ String contentName=null;
+ if (allowContent)
+ contentName=visitor.getLocalKey();
+
+ if (contentName!=null) {
+ if (type!=null)
+ contentName=type.unalias(contentName);
+ acceptSingleValue(contentName,allowContent,visitor,dimensionBinding); // Special cased for performance
+ }
+ else {
+ acceptAllValues(allowContent,visitor,type,dimensionBinding);
+ }
+ }
+
+ // PERF: 90%
+ void acceptSingleValue(String name,boolean allowContent,QueryProfileVisitor visitor,DimensionBinding dimensionBinding) {
+ FieldValues fieldValues=fieldValuesByName.get(name);
+ if (fieldValues==null || !allowContent)
+ fieldValues=new FieldValues();
+
+ fieldValues.sort();
+ inheritedProfiles.sort();
+
+ int inheritedIndex=0;
+ int fieldIndex=0;
+ // Go through both the fields and the inherited profiles at the same time and try the single must specific pick
+ // from either of the lists at each step
+ while(fieldIndex<fieldValues.size() || inheritedIndex<inheritedProfiles.size()) { // PERF: 8% - fieldValues.size()
+ // Get the next most specific from field and inherited
+ FieldValue fieldValue=fieldValues.getIfExists(fieldIndex); // PERF: 11% - getIfExists
+ FieldValue inheritedProfileValue=inheritedProfiles.getIfExists(inheritedIndex); // PERF: 11% - getIfExists
+
+ // Try the most specific first, then the other
+ if (inheritedProfileValue==null || (fieldValue!=null && fieldValue.compareTo(inheritedProfileValue)<=0)) { // Field is most specific, or both are equally specific
+ if (fieldValue.matches(dimensionBinding.getValues())) { // PERF: 42% - matches, together with the other matches
+ visitor.acceptValue(name, fieldValue.getValue(), dimensionBinding, owner);
+ }
+ if (visitor.isDone()) return;
+ fieldIndex++;
+ }
+ else if (inheritedProfileValue!=null) { // Inherited is most specific at this point
+ if (inheritedProfileValue.matches(dimensionBinding.getValues())) { // PERF: 42% - matches, together with the other matches
+ @SuppressWarnings("unchecked")
+ List<QueryProfile> inheritedProfileList=(List<QueryProfile>)inheritedProfileValue.getValue();
+ for (QueryProfile inheritedProfile : inheritedProfileList) {
+ if (visitor.visitInherited()) {
+ inheritedProfile.accept(allowContent,visitor,dimensionBinding.createFor(inheritedProfile.getDimensions()), owner);
+ }
+ if (visitor.isDone()) return;
+ }
+ }
+ inheritedIndex++;
+ }
+ if (visitor.isDone()) return;
+ }
+ }
+
+ void acceptAllValues(boolean allowContent,QueryProfileVisitor visitor, QueryProfileType type,DimensionBinding dimensionBinding) {
+ if (!frozen)
+ Collections.sort(variants);
+ for (QueryProfileVariant variant : variants) {
+ if (variant.matches(dimensionBinding.getValues()))
+ variant.accept(allowContent,type,visitor,dimensionBinding);
+ if (visitor.isDone()) return;
+ }
+ }
+
+ /**
+ * Returns the most specific matching value of a name for a given set of <b>canonical</b> dimension values.
+ *
+ * @param name the name to return the best matching value of
+ * @param dimensionBinding the dimension bindings to use in this
+ */
+ public Object get(String name, QueryProfileType type, boolean allowQueryProfileResult, DimensionBinding dimensionBinding) {
+ SingleValueQueryProfileVisitor visitor=new SingleValueQueryProfileVisitor(Collections.singletonList(name),allowQueryProfileResult);
+ visitor.enter("");
+ accept(true,type,visitor,dimensionBinding);
+ visitor.leave("");
+ return visitor.getResult();
+ }
+
+ /** Inherits a particular profile in a variant of this */
+ public void inherit(QueryProfile profile,DimensionValues dimensionValues) {
+ ensureNotFrozen();
+
+ // Update variant
+ getVariant(dimensionValues,true).inherit(profile);
+
+ // Update per-variable optimized structure
+ @SuppressWarnings("unchecked")
+ List<QueryProfile> inheritedAtDimensionValues=(List<QueryProfile>)inheritedProfiles.getExact(dimensionValues);
+ if (inheritedAtDimensionValues==null) {
+ inheritedAtDimensionValues=new ArrayList<>();
+ inheritedProfiles.put(dimensionValues,inheritedAtDimensionValues);
+ }
+ inheritedAtDimensionValues.add(profile);
+ }
+
+ /**
+ * Sets a value to this
+ *
+ * @param fieldName the name of the field to set. This cannot be a compound (dotted) name
+ * @param binding the dimension values for which this value applies.
+ * The dimensions must be canonicalized, and ownership is transferred to this.
+ * @param value the value to set
+ */
+ /**
+ * Sets a value to this
+ *
+ * @param fieldName the name of the field to set. This cannot be a compound (dotted) name
+ * @param dimensionValues the dimension values for which this value applies
+ * @param value the value to set
+ */
+ public void set(String fieldName,DimensionValues dimensionValues,Object value) {
+ ensureNotFrozen();
+
+ // Update variant
+ getVariant(dimensionValues,true).set(fieldName,value);
+
+ // Update per-variable optimized structure
+ FieldValues fieldValues=fieldValuesByName.get(fieldName);
+ if (fieldValues==null) {
+ fieldValues=new FieldValues();
+ fieldValuesByName.put(fieldName,fieldValues);
+ }
+
+ Object combinedValue=QueryProfile.combineValues(value,fieldValues.getExact(dimensionValues));
+ if (combinedValue!=null)
+ fieldValues.put(dimensionValues,combinedValue);
+ }
+
+ /**
+ * Returns the dimensions over which the virtual profiles in this may return different values.
+ * Each dimension is a name for which a key-value may be supplied in the context properties
+ * on lookup time to influence the value returned.
+ * The dimensions may not be modified - the returned list is always read only.
+ */
+ // Note: A performance optimization in DimensionBinding depends on the identity of the list returned from this
+ public List<String> getDimensions() { return dimensions; }
+
+ /** Returns the map of field values of this indexed by field name. */
+ public Map<String,FieldValues> getFieldValues() { return fieldValuesByName; }
+
+ /** Returns the profiles inherited from various variants of this */
+ public FieldValues getInherited() { return inheritedProfiles; }
+
+ /**
+ * Returns all the variants of this, sorted by specificity. This is content as declared.
+ * The returned list is always unmodifiable.
+ */
+ public List<QueryProfileVariant> getVariants() {
+ if (frozen) return variants; // Already unmodifiable
+ return Collections.unmodifiableList(variants);
+ }
+
+ public QueryProfileVariants clone() {
+ try {
+ if (frozen) return this;
+ QueryProfileVariants clone=(QueryProfileVariants)super.clone();
+ clone.inheritedProfiles=inheritedProfiles.clone();
+
+ clone.variants=new ArrayList<>();
+ for (QueryProfileVariant variant : variants)
+ clone.variants.add(variant.clone());
+
+ clone.fieldValuesByName=new HashMap<>();
+ for (Map.Entry<String,FieldValues> entry : fieldValuesByName.entrySet())
+ clone.fieldValuesByName.put(entry.getKey(),entry.getValue().clone(entry.getKey(),clone.variants));
+
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Throws an IllegalStateException if this is frozen */
+ protected void ensureNotFrozen() {
+ if (frozen)
+ throw new IllegalStateException(this + " is frozen and cannot be modified");
+ }
+
+ /**
+ * Returns the query profile variant having exactly the given dimensions, and creates it if create is set and
+ * it is missing
+ *
+ * @param dimensionValues the dimension values
+ * @param create whether or not to create the variant if missing
+ * @return the profile variant, or null if not found and create is false
+ */
+ public QueryProfileVariant getVariant(DimensionValues dimensionValues,boolean create) {
+ for (QueryProfileVariant profileVariant : variants)
+ if (profileVariant.getDimensionValues().equals(dimensionValues))
+ return profileVariant;
+
+ // Not found
+ if (!create) return null;
+ QueryProfileVariant variant=new QueryProfileVariant(dimensionValues, owner);
+ variants.add(variant);
+ return variant;
+ }
+
+ public static class FieldValues implements Freezable, Cloneable {
+
+ private List<FieldValue> resolutionList=null;
+
+ private boolean frozen=false;
+
+ @Override
+ public void freeze() {
+ if (frozen) return;
+ sort();
+ if (resolutionList != null)
+ resolutionList = ImmutableList.copyOf(resolutionList);
+ frozen = true;
+ }
+
+ @Override
+ public boolean isFrozen() {
+ return frozen;
+ }
+
+ public void put(DimensionValues dimensionValues,Object value) {
+ ensureNotFrozen();
+ if (resolutionList==null) resolutionList=new ArrayList<>();
+ FieldValue fieldValue=getExactFieldValue(dimensionValues);
+ if (fieldValue!=null) // Replace
+ fieldValue.setValue(value);
+ else
+ resolutionList.add(new FieldValue(dimensionValues,value));
+ }
+
+ /** Returns the value having exactly the given dimensions, or null if none */
+ public Object getExact(DimensionValues dimensionValues) {
+ FieldValue value=getExactFieldValue(dimensionValues);
+ if (value==null) return null;
+ return value.getValue();
+ }
+
+ /** Returns the field value having exactly the given dimensions, or null if none */
+ private FieldValue getExactFieldValue(DimensionValues dimensionValues) {
+ for (FieldValue fieldValue : asList())
+ if (fieldValue.getDimensionValues().equals(dimensionValues))
+ return fieldValue;
+ return null;
+ }
+
+ /** Returns the field values (values for various dimensions) for this field as a read-only list (never null) */
+ public List<FieldValue> asList() {
+ if (resolutionList==null) return Collections.emptyList();
+ return resolutionList;
+ }
+
+ public FieldValue getIfExists(int index) {
+ if (index>=size()) return null;
+ return resolutionList.get(index);
+ }
+
+ public void sort() {
+ if (frozen) return ; // sorted already
+ if (resolutionList!=null)
+ Collections.sort(resolutionList);
+ }
+
+ /** Same as asList().size() */
+ public int size() {
+ if (resolutionList==null) return 0;
+ return resolutionList.size();
+ }
+
+ /** Throws an IllegalStateException if this is frozen */
+ protected void ensureNotFrozen() {
+ if (frozen)
+ throw new IllegalStateException(this + " is frozen and cannot be modified");
+ }
+
+ /** Clone by filling in values from the given variants */
+ public FieldValues clone(String fieldName,List<QueryProfileVariant> clonedVariants) {
+ try {
+ if (frozen) return this;
+ FieldValues clone=(FieldValues)super.clone();
+
+ if (resolutionList!=null) {
+ clone.resolutionList=new ArrayList<>(resolutionList.size());
+ for (FieldValue value : resolutionList)
+ clone.resolutionList.add(value.clone(fieldName,clonedVariants));
+ }
+
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public @Override FieldValues clone() {
+ try {
+ if (frozen) return this;
+ FieldValues clone=(FieldValues)super.clone();
+
+ if (resolutionList!=null) {
+ clone.resolutionList=new ArrayList<>(resolutionList.size());
+ for (FieldValue value : resolutionList)
+ clone.resolutionList.add(value.clone());
+ }
+
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ }
+
+ public static class FieldValue implements Comparable<FieldValue>, Cloneable {
+
+ private DimensionValues dimensionValues;
+ private Object value;
+
+ public FieldValue(DimensionValues dimensionValues,Object value) {
+ this.dimensionValues=dimensionValues;
+ this.value=value;
+ }
+
+ /**
+ * Returns the dimension values for which this value should be used.
+ * The dimension array is always of the exact size of the dimensions specified by the owning QueryProfileVariants,
+ * and the values appear in the order defined. "Wildcard" dimensions are represented by a null.
+ */
+ public DimensionValues getDimensionValues() { return dimensionValues; }
+
+ /** Returns the value to use for this set of dimension values */
+ public Object getValue() { return value; }
+
+ /** Sets the value to use for this set of dimension values */
+ public void setValue(Object value) { this.value=value; }
+
+ public boolean matches(DimensionValues givenDimensionValues) {
+ return dimensionValues.matches(givenDimensionValues);
+ }
+
+ /**
+ * Implements the sort order of this which is based on specificity
+ * where dimensions to the left are more significant.
+ * <p>
+ * <b>Note:</b> This ordering is not consistent with equals - it returns 0 when the same dimensions
+ * are <i>set</i>, regardless of what they are set <i>to</i>.
+ */
+ public @Override int compareTo(FieldValue other) {
+ return this.dimensionValues.compareTo(other.dimensionValues);
+ }
+
+ /** Clone by filling in the value from the given variants */
+ public FieldValue clone(String fieldName,List<QueryProfileVariant> clonedVariants) {
+ try {
+ FieldValue clone=(FieldValue)super.clone();
+ if (this.value instanceof QueryProfile)
+ clone.value=lookupInVariants(fieldName,dimensionValues,clonedVariants);
+ // Otherwise the value is immutable, so keep it as-is
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public FieldValue clone() {
+ try {
+ FieldValue clone=(FieldValue)super.clone();
+ clone.value=QueryProfile.cloneIfNecessary(this.value);
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Object lookupInVariants(String fieldName,DimensionValues dimensionValues,List<QueryProfileVariant> variants) {
+ for (QueryProfileVariant variant : variants) {
+ if ( ! variant.getDimensionValues().equals(dimensionValues)) continue;
+ return variant.values().get(fieldName);
+ }
+ return null;
+ }
+
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVisitor.java b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVisitor.java
new file mode 100644
index 00000000000..8cb6bf34021
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileVisitor.java
@@ -0,0 +1,87 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+/**
+ * Instances of this is used to visit nodes in a graph of query profiles
+ *
+ * <code>
+ * Visitor are called in the following sequence on each query profile:
+ * enter=enter(referenceName);
+ * onQueryProfile(this)
+ * if (enter) {
+ * getLocalKey()
+ * ...calls on nested content found in variants, this and inherited, in that order
+ * leave(referenceName)
+ * }
+ *
+ * The first enter call will be on the root node, which has an empt reference name.
+ * </code>
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+abstract class QueryProfileVisitor {
+
+ /**
+ * Called when a new <b>nested</b> profile in the graph is entered.
+ * This default implementation does nothing but returning true.
+ * If the node is entered (if true is returned from this), a corresponding {@link #leave(String)} call will happen
+ * later.
+ *
+ * @param name the name this profile is nested as, or the empty string if we are entering the root profile
+ * @return whether we should visit the content of this node or not
+ */
+ public boolean enter(String name) { return true; }
+
+ /**
+ * Called when the last {@link #enter(String) entered} nested profile is left.
+ * That is: One leave call is made for each enter call which returns true,
+ * but due to nesting those calls are not necessarily alternating.
+ * This default implementation does nothing.
+ */
+ public void leave(String name) { }
+
+ /**
+ * Called when a value (not a query profile) is encountered.
+ *
+ * @param localName the local name of this value (the full name, if needed, must be reconstructed
+ * by the information given by the history of {@link #enter(String)} and {@link #leave(String)} calls
+ * @param value the value
+ * @param binding the binding this holds for
+ * @param owner the query profile having this value, or null only when profile is the root profile
+ */
+ public abstract void onValue(String localName, Object value, DimensionBinding binding, QueryProfile owner);
+
+ /**
+ * Called when a query profile is encountered.
+ *
+ * @param profile the query profile reference encountered
+ * @param binding the binding this holds for
+ * @param owner the profile making this reference, or null only when profile is the root profile
+ */
+ public abstract void onQueryProfile(QueryProfile profile, DimensionBinding binding, QueryProfile owner);
+
+ /** Returns whether this visitor is done visiting what it needed to visit at this point */
+ public abstract boolean isDone();
+
+ /** Returns whether we should, at this point, visit inherited profiles. This default implementation returns true */
+ public boolean visitInherited() { return true; }
+
+ /**
+ * Returns the current local key which should be visited in the last {@link #enter(String) entered} sub-profile
+ * (or in the top level profile if none is entered), or null to visit all content
+ */
+ public abstract String getLocalKey();
+
+ /** Calls onValue or onQueryProfile on this and visits the content if it's a profile */
+ final void acceptValue(String key, Object value, DimensionBinding dimensionBinding, QueryProfile owner) {
+ if (value==null) return;
+ if (value instanceof QueryProfile) {
+ QueryProfile queryProfileValue=(QueryProfile)value;
+ queryProfileValue.acceptAndEnter(key, this, dimensionBinding.createFor(queryProfileValue.getDimensions()), owner);
+ }
+ else {
+ onValue(key, value, dimensionBinding, owner);
+ }
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/SingleValueQueryProfileVisitor.java b/container-search/src/main/java/com/yahoo/search/query/profile/SingleValueQueryProfileVisitor.java
new file mode 100644
index 00000000000..6d5d1b0686a
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/SingleValueQueryProfileVisitor.java
@@ -0,0 +1,76 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import java.util.List;
+
+/**
+ * Visitor which stores the first non-query-profile value encountered,
+ * or the first query profile encountered at a stop where we do not have any name components left which can be used to
+ * visit further subprofiles. Hence this may be used both to get the highest prioritized primitive
+ * value, or query profile, whichever is encountered first which matches the name.
+ * <p>
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+final class SingleValueQueryProfileVisitor extends QueryProfileVisitor {
+
+ /** The value found, or null if none */
+ private Object value=null;
+
+ private final List<String> name;
+
+ private int nameIndex=-1;
+
+ private final boolean allowQueryProfileResult;
+
+ private boolean enteringContent=true;
+
+ public SingleValueQueryProfileVisitor(List<String> name,boolean allowQueryProfileResult) {
+ this.name=name;
+ this.allowQueryProfileResult=allowQueryProfileResult;
+ }
+
+ public @Override String getLocalKey() {
+ return name.get(nameIndex);
+ }
+
+ public @Override boolean enter(String name) {
+ if (nameIndex+1<this.name.size()) {
+ nameIndex++;
+ enteringContent=true;
+ }
+ else {
+ enteringContent=false;
+ }
+ return enteringContent;
+ }
+
+ public @Override void leave(String name) {
+ nameIndex--;
+ }
+
+ public @Override void onValue(String key,Object value, DimensionBinding binding, QueryProfile owner) {
+ if (nameIndex==name.size()-1)
+ this.value=value;
+ }
+
+ public @Override void onQueryProfile(QueryProfile profile,DimensionBinding binding, QueryProfile owner) {
+ if (enteringContent) return; // still waiting for content
+ if (allowQueryProfileResult)
+ this.value = profile;
+ else
+ this.value = profile.getValue();
+ }
+
+ public @Override boolean isDone() {
+ return value!=null;
+ }
+
+ /** Returns the value found during visiting, or null if none */
+ public Object getResult() { return value; }
+
+ public @Override String toString() {
+ return "a single value visitor (hash " + hashCode() + ") with current value " + value;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/SubstituteString.java b/container-search/src/main/java/com/yahoo/search/query/profile/SubstituteString.java
new file mode 100644
index 00000000000..59401592378
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/SubstituteString.java
@@ -0,0 +1,127 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile;
+
+import com.yahoo.processing.request.Properties;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A string which contains one or more elements of the form %{name},
+ * where these occurrences are to be replaced by a query profile lookup on name.
+ * <p>
+ * This objects does the analysis on creation and provides a (reasonably) fast method of
+ * performing the actual substitution (at lookup time).
+ * <p>
+ * This is a value object. Lookups in this are thread safe.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class SubstituteString {
+
+ private final List<Component> components;
+ private final String stringValue;
+
+ /**
+ * Returns a new SubstituteString if the given string contains substitutions, null otherwise.
+ */
+ public static SubstituteString create(String value) {
+ int lastEnd=0;
+ int start=value.indexOf("%{");
+ if (start<0) return null; // Shortcut
+ List<Component> components=new ArrayList<>();
+ while (start>=0) {
+ int end=value.indexOf("}",start+2);
+ if (end<0)
+ throw new IllegalArgumentException("Unterminated value substitution '" + value.substring(start) + "'");
+ String propertyName=value.substring(start+2,end);
+ if (propertyName.indexOf("%{")>=0)
+ throw new IllegalArgumentException("Unterminated value substitution '" + value.substring(start) + "'");
+ components.add(new StringComponent(value.substring(lastEnd,start)));
+ components.add(new PropertyComponent(propertyName));
+ lastEnd=end+1;
+ start=value.indexOf("%{",lastEnd);
+ }
+ components.add(new StringComponent(value.substring(lastEnd,value.length())));
+ return new SubstituteString(components, value);
+ }
+
+ private SubstituteString(List<Component> components, String stringValue) {
+ this.components = components;
+ this.stringValue = stringValue;
+ }
+
+ /**
+ * Perform the substitution in this, by looking up in the given query profile,
+ * and returns the resulting string
+ */
+ public String substitute(Map<String,String> context,Properties substitution) {
+ StringBuilder b=new StringBuilder();
+ for (Component component : components)
+ b.append(component.getValue(context,substitution));
+ return b.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return stringValue.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) return true;
+ if ( ! (other instanceof SubstituteString)) return false;
+ return this.stringValue.equals(((SubstituteString)other).stringValue);
+ }
+
+ /** Returns this string in original (unsubstituted) form */
+ public @Override String toString() {
+ return stringValue;
+ }
+
+ private abstract static class Component {
+
+ protected abstract String getValue(Map<String,String> context,Properties substitution);
+
+ }
+
+ private final static class StringComponent extends Component {
+
+ private final String value;
+
+ public StringComponent(String value) {
+ this.value=value;
+ }
+
+ public @Override String getValue(Map<String,String> context,Properties substitution) {
+ return value;
+ }
+
+ public @Override String toString() {
+ return value;
+ }
+
+ }
+
+ private final static class PropertyComponent extends Component {
+
+ private final String propertyName;
+
+ public PropertyComponent(String propertyName) {
+ this.propertyName=propertyName;
+ }
+
+ public @Override String getValue(Map<String,String> context,Properties substitution) {
+ Object value=substitution.get(propertyName,context,substitution);
+ if (value==null) return "";
+ return String.valueOf(value);
+ }
+
+ public @Override String toString() {
+ return "%{" + propertyName + "}";
+ }
+
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/Binding.java b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/Binding.java
new file mode 100644
index 00000000000..a440365ceba
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/Binding.java
@@ -0,0 +1,128 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.compiled;
+
+import com.yahoo.search.query.profile.DimensionBinding;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An immutable binding of a set of dimensions to values.
+ * This binding is minimal in that it only includes dimensions which actually have values.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class Binding implements Comparable<Binding> {
+
+ private static final int maxDimensions = 31;
+
+ /**
+ * A higher number means this is more general. This accounts for both the number and position of the bindings
+ * in the dimensional space, such that bindings in earlier dimensions are matched before bindings in
+ * later dimensions
+ */
+ private final int generality;
+
+ /** The dimensions of this. Unenforced invariant: Content never changes. */
+ private final String[] dimensions;
+
+ /** The values of those dimensions. Unenforced invariant: Content never changes. */
+ private final String[] dimensionValues;
+
+ private final int hashCode;
+
+ @SuppressWarnings("unchecked")
+ public static final Binding nullBinding= new Binding(Integer.MAX_VALUE, Collections.<String,String>emptyMap());
+
+ public static Binding createFrom(DimensionBinding dimensionBinding) {
+ if (dimensionBinding.getDimensions().size() > maxDimensions)
+ throw new IllegalArgumentException("More than 31 dimensions is not supported");
+
+ int generality = 0;
+ Map<String, String> context = new HashMap<>();
+ if (dimensionBinding.getDimensions() == null || dimensionBinding.getDimensions().isEmpty()) { // TODO: Just have this return the nullBinding
+ generality = Integer.MAX_VALUE;
+ }
+ else {
+ for (int i = 0; i <= maxDimensions; i++) {
+ String value = i < dimensionBinding.getDimensions().size() ? dimensionBinding.getValues().get(i) : null;
+ if (value == null)
+ generality += Math.pow(2, maxDimensions - i-1);
+ else
+ context.put(dimensionBinding.getDimensions().get(i), value);
+ }
+ }
+ return new Binding(generality, context);
+ }
+
+ private Binding(int generality, Map<String, String> binding) {
+ this.generality = generality;
+
+ // Map -> arrays to limit memory consumption and speed up evaluation
+ dimensions = new String[binding.size()];
+ dimensionValues = new String[binding.size()];
+
+ int i = 0;
+ int bindingHash = 0;
+ for (Map.Entry<String,String> entry : binding.entrySet()) {
+ dimensions[i] = entry.getKey();
+ dimensionValues[i] = entry.getValue();
+ bindingHash += i * entry.getKey().hashCode() + 11 * i * entry.getValue().hashCode();
+ i++;
+ }
+ this.hashCode = bindingHash;
+ }
+
+ /** Returns true only if this binding is null (contains no values for its dimensions (if any) */
+ public boolean isNull() { return dimensions.length == 0; }
+
+ @Override
+ public String toString() {
+ StringBuilder b = new StringBuilder("Binding[");
+ for (int i = 0; i < dimensions.length; i++)
+ b.append(dimensions[i]).append("=").append(dimensionValues[i]).append(",");
+ if (dimensions.length > 0)
+ b.setLength(b.length()-1);
+ b.append("] (generality " + generality + ")");
+ return b.toString();
+ }
+
+ /** Returns whether the given binding has exactly the same values as this */
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (! (o instanceof Binding)) return false;
+ Binding other = (Binding)o;
+ return Arrays.equals(this.dimensions, other.dimensions)
+ && Arrays.equals(this.dimensionValues, other.dimensionValues);
+ }
+
+ @Override
+ public int hashCode() { return hashCode; }
+
+ /**
+ * Returns true if all the dimension values in this have the same values
+ * in the given context.
+ */
+ public boolean matches(Map<String,String> context) {
+ for (int i = 0; i < dimensions.length; i++) {
+ if ( ! dimensionValues[i].equals(context.get(dimensions[i]))) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Implements a partial ordering where more specific bindings come before less specific ones,
+ * taking both the number of bindings and their positions into account (earlier dimensions
+ * take precedence over later ones.
+ * <p>
+ * The order is not well defined for bindings in different dimensional spaces.
+ */
+ @Override
+ public int compareTo(Binding other) {
+ return Integer.compare(this.generality, other.generality);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfile.java b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfile.java
new file mode 100644
index 00000000000..a4056ee55a2
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfile.java
@@ -0,0 +1,183 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.compiled;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.processing.request.Properties;
+import com.yahoo.search.query.profile.QueryProfileProperties;
+import com.yahoo.search.query.profile.SubstituteString;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A query profile in a state where it is optimized for fast lookups.
+ *
+ * @author bratseth
+ */
+public class CompiledQueryProfile extends AbstractComponent implements Cloneable {
+
+ private static final Pattern namePattern=Pattern.compile("[$a-zA-Z_/][-$a-zA-Z0-9_/()]*");
+
+ private final CompiledQueryProfileRegistry registry;
+
+ /** The type of this, or null if none */
+ private final QueryProfileType type;
+
+ /** The values of this */
+ private final DimensionalMap<CompoundName, Object> entries;
+
+ /** Keys which have a type in this */
+ private final DimensionalMap<CompoundName, QueryProfileType> types;
+
+ /** Keys which are (typed or untyped) references to other query profiles in this. Used as a set. */
+ private final DimensionalMap<CompoundName, Object> references;
+
+ /** Values which are not overridable in this. Used as a set. */
+ private final DimensionalMap<CompoundName, Object> unoverridables;
+
+ /**
+ * Creates a new query profile from an id.
+ */
+ public CompiledQueryProfile(ComponentId id, QueryProfileType type,
+ DimensionalMap<CompoundName, Object> entries,
+ DimensionalMap<CompoundName, QueryProfileType> types,
+ DimensionalMap<CompoundName, Object> references,
+ DimensionalMap<CompoundName, Object> unoverridables,
+ CompiledQueryProfileRegistry registry) {
+ super(id);
+ this.registry = registry;
+ if (type != null)
+ type.freeze();
+ this.type = type;
+ this.entries = entries;
+ this.types = types;
+ this.references = references;
+ this.unoverridables = unoverridables;
+ if ( ! id.isAnonymous())
+ validateName(id.getName());
+ }
+
+ // ----------------- Public API -------------------------------------------------------------------------------
+
+ /** Returns the registry this belongs to, or null if none (in which case runtime profile reference assignment won't work) */
+ public CompiledQueryProfileRegistry getRegistry() { return registry; }
+
+ /** Returns the type of this or null if it has no type */
+ // TODO: Move into below
+ public QueryProfileType getType() { return type; }
+
+ /**
+ * Returns whether or not the given field name can be overridden at runtime.
+ * Attempts to override values which cannot be overridden will not fail but be ignored.
+ * Default: true.
+ *
+ * @param name the name of the field to check
+ * @param context the context in which to check, or null if none
+ */
+ public final boolean isOverridable(CompoundName name, Map<String, String> context) {
+ return unoverridables.get(name, context) == null;
+ }
+
+ /** Returns the type of a given prefix reachable from this profile, or null if none */
+ public final QueryProfileType getType(CompoundName name, Map<String, String> context) {
+ return types.get(name, context);
+ }
+
+ /** Returns the types reachable from this, or an empty map (never null) if none */
+ public DimensionalMap<CompoundName, QueryProfileType> getTypes() { return types; }
+
+ /** Returns the references reachable from this, or an empty map (never null) if none */
+ public DimensionalMap<CompoundName, Object> getReferences() { return references; }
+
+ /**
+ * Return all objects that start with the given prefix path using no context. Use "" to list all.
+ * <p>
+ * For example, if {a.d =&gt; "a.d-value" ,a.e =&gt; "a.e-value", b.d =&gt; "b.d-value", then calling listValues("a")
+ * will return {"d" =&gt; "a.d-value","e" =&gt; "a.e-value"}
+ */
+ public final Map<String, Object> listValues(final CompoundName prefix) { return listValues(prefix, Collections.<String,String>emptyMap()); }
+ public final Map<String, Object> listValues(final String prefix) { return listValues(new CompoundName(prefix)); }
+ /**
+ * Return all objects that start with the given prefix path. Use "" to list all.
+ * <p>
+ * For example, if {a.d =&gt; "a.d-value" ,a.e =&gt; "a.e-value", b.d =&gt; "b.d-value", then calling listValues("a")
+ * will return {"d" =&gt; "a.d-value","e" =&gt; "a.e-value"}
+ */
+ public final Map<String, Object> listValues(final String prefix,Map<String,String> context) {
+ return listValues(new CompoundName(prefix), context);
+ }
+ /**
+ * Return all objects that start with the given prefix path. Use "" to list all.
+ * <p>
+ * For example, if {a.d =&gt; "a.d-value" ,a.e =&gt; "a.e-value", b.d =&gt; "b.d-value", then calling listValues("a")
+ * will return {"d" =&gt; "a.d-value","e" =&gt; "a.e-value"}
+ */
+ public final Map<String, Object> listValues(final CompoundName prefix,Map<String,String> context) {
+ return listValues(prefix, context, null);
+ }
+ /**
+ * Adds all objects that start with the given path prefix to the given value map. Use "" to list all.
+ * <p>
+ * For example, if {a.d =&gt; "a.d-value" ,a.e =&gt; "a.e-value", b.d =&gt; "b.d-value", then calling listValues("a")
+ * will return {"d" =&gt; "a.d-value","e" =&gt; "a.e-value"}
+ */
+ public Map<String, Object> listValues(CompoundName prefix, Map<String,String> context, Properties substitution) {
+ Map<String, Object> values = new HashMap<>();
+ for (Map.Entry<CompoundName, DimensionalValue<Object>> entry : entries.entrySet()) {
+ if ( entry.getKey().size() <= prefix.size()) continue;
+ if ( ! entry.getKey().hasPrefix(prefix)) continue;
+
+ Object value = entry.getValue().get(context);
+ if (value == null) continue;
+
+ value = substitute(value, context, substitution);
+ CompoundName suffixName = entry.getKey().rest(prefix.size());
+ values.put(suffixName.toString(), value);
+ }
+ return values;
+ }
+
+ public final Object get(String name) {
+ return get(name, Collections.<String,String>emptyMap());
+ }
+ public final Object get(String name, Map<String,String> context) {
+ return get(name, context, new QueryProfileProperties(this));
+ }
+ public final Object get(String name, Map<String,String> context, Properties substitution) {
+ return get(new CompoundName(name), context, substitution);
+ }
+ public final Object get(CompoundName name, Map<String, String> context, Properties substitution) {
+ return substitute(entries.get(name, context), context, substitution);
+ }
+
+ private Object substitute(Object value, Map<String,String> context, Properties substitution) {
+ if (value == null) return value;
+ if (substitution == null) return value;
+ if (value.getClass() != SubstituteString.class) return value;
+ return ((SubstituteString)value).substitute(context, substitution);
+ }
+
+ /** Throws IllegalArgumentException if the given string is not a valid query profile name */
+ private static void validateName(String name) {
+ Matcher nameMatcher=namePattern.matcher(name);
+ if ( ! nameMatcher.matches())
+ throw new IllegalArgumentException("Illegal name '" + name + "'");
+ }
+
+ @Override
+ public CompiledQueryProfile clone() {
+ return this; // immutable
+ }
+
+ @Override
+ public String toString() {
+ return "query profile '" + getId() + "'" + (type!=null ? " of type '" + type.getId() + "'" : "");
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfileRegistry.java b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfileRegistry.java
new file mode 100644
index 00000000000..91a81888267
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfileRegistry.java
@@ -0,0 +1,76 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.compiled;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.QueryProfileTypeRegistry;
+
+/**
+ * A set of compiled query profiles.
+ *
+ * @author bratseth
+ */
+public class CompiledQueryProfileRegistry extends ComponentRegistry<CompiledQueryProfile> {
+
+ private final QueryProfileTypeRegistry typeRegistry;
+
+ /** Creates a compiled query profile registry with no types */
+ public CompiledQueryProfileRegistry() {
+ this(QueryProfileTypeRegistry.emptyFrozen());
+ }
+
+ public CompiledQueryProfileRegistry(QueryProfileTypeRegistry typeRegistry) {
+ this.typeRegistry = typeRegistry;
+ }
+
+ /** Registers a type by its id */
+ public void register(CompiledQueryProfile profile) {
+ super.register(profile.getId(), profile);
+ }
+
+ public QueryProfileTypeRegistry getTypeRegistry() { return typeRegistry; }
+
+ /**
+ * <p>Returns a query profile for the given request string, or null if a suitable one is not found.</p>
+ *
+ * The request string must be a valid {@link com.yahoo.component.ComponentId} or null.<br>
+ * If the string is null, the profile named "default" is returned, or null if that does not exists.
+ *
+ * <p>
+ * The version part (if any) is matched used the usual component version patching rules.
+ * If the name part matches a query profile name perfectly, that profile is returned.
+ * If not, and the name is a slash-separated path, the profile with the longest matching left sub-path
+ * which has a type which allows path matching is used. If there is no such profile, null is returned.
+ */
+ public CompiledQueryProfile findQueryProfile(String idString) {
+ if (idString==null || idString.isEmpty()) return getComponent("default");
+ ComponentSpecification id=new ComponentSpecification(idString);
+ CompiledQueryProfile profile=getComponent(id);
+ if (profile!=null) return profile;
+
+ return findPathParentQueryProfile(new ComponentSpecification(idString));
+ }
+
+ private CompiledQueryProfile findPathParentQueryProfile(ComponentSpecification id) {
+ // Try the name with "/" appended - should have the same semantics with path matching
+ CompiledQueryProfile slashedProfile=getComponent(new ComponentSpecification(id.getName() + "/",id.getVersionSpecification()));
+ if (slashedProfile!=null && slashedProfile.getType()!=null && slashedProfile.getType().getMatchAsPath())
+ return slashedProfile;
+
+ // Extract the parent (if any)
+ int slashIndex=id.getName().lastIndexOf("/");
+ if (slashIndex<1) return null;
+ String parentName=id.getName().substring(0,slashIndex);
+ if (parentName.equals("")) return null;
+
+ ComponentSpecification parentId=new ComponentSpecification(parentName,id.getVersionSpecification());
+
+ CompiledQueryProfile pathParentProfile=getComponent(parentId);
+
+ if (pathParentProfile!=null && pathParentProfile.getType()!=null && pathParentProfile.getType().getMatchAsPath())
+ return pathParentProfile;
+ return findPathParentQueryProfile(parentId);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalMap.java b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalMap.java
new file mode 100644
index 00000000000..b82939fa4ac
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalMap.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.compiled;
+
+import com.google.common.collect.ImmutableMap;
+import com.yahoo.search.query.profile.DimensionBinding;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A map which may return different values depending on the values given in a context
+ * supplied with the key on all operations.
+ * <p>
+ * Dimensional maps are immutable and created through a DimensionalMap.Builder
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class DimensionalMap<KEY, VALUE> {
+
+ private final Map<KEY, DimensionalValue<VALUE>> values;
+
+ private DimensionalMap(Map<KEY, DimensionalValue<VALUE>> values) {
+ this.values = ImmutableMap.copyOf(values);
+ }
+
+ /** Returns the value for this key matching a context, or null if none */
+ public VALUE get(KEY key, Map<String, String> context) {
+ DimensionalValue<VALUE> variants = values.get(key);
+ if (variants == null) return null;
+ return variants.get(context);
+ }
+
+ /** Returns the set of dimensional entries across all contexts. */
+ public Set<Map.Entry<KEY, DimensionalValue<VALUE>>> entrySet() {
+ return values.entrySet();
+ }
+
+ /** Returns true if this is empty for all contexts. */
+ public boolean isEmpty() {
+ return values.isEmpty();
+ }
+
+ public static class Builder<KEY, VALUE> {
+
+ private Map<KEY, DimensionalValue.Builder<VALUE>> entries = new HashMap<>();
+
+ // TODO: DimensionBinding -> Binding?
+ public void put(KEY key, DimensionBinding binding, VALUE value) {
+ DimensionalValue.Builder<VALUE> entry = entries.get(key);
+ if (entry == null) {
+ entry = new DimensionalValue.Builder<>();
+ entries.put(key, entry);
+ }
+ entry.add(value, binding);
+ }
+
+ public DimensionalMap<KEY, VALUE> build() {
+ Map<KEY, DimensionalValue<VALUE>> map = new HashMap<>();
+ for (Map.Entry<KEY, DimensionalValue.Builder<VALUE>> entry : entries.entrySet()) {
+ map.put(entry.getKey(), entry.getValue().build());
+ }
+ return new DimensionalMap<>(map);
+ }
+
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalValue.java b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalValue.java
new file mode 100644
index 00000000000..0112928ada6
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalValue.java
@@ -0,0 +1,159 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.compiled;
+
+import com.yahoo.search.query.profile.DimensionBinding;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Contains the values a given key in a DimensionalMap may take for different dimensional contexts.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class DimensionalValue<VALUE> {
+
+ private final List<Value<VALUE>> values;
+
+ /** Create a set of variants which is a single value regardless of dimensions */
+ public DimensionalValue(Value<VALUE> value) {
+ this.values = Collections.singletonList(value);
+ }
+
+ public DimensionalValue(List<Value<VALUE>> valueVariants) {
+ if (valueVariants.size() == 1) { // special cased for efficiency
+ this.values = Collections.singletonList(valueVariants.get(0));
+ }
+ else {
+ this.values = new ArrayList<>(valueVariants);
+ Collections.sort(this.values);
+ }
+ }
+
+ /** Returns the value matching this context, or null if none */
+ public VALUE get(Map<String, String> context) {
+ if (context == null)
+ context = Collections.emptyMap();
+ for (Value<VALUE> value : values) {
+ if (value.matches(context))
+ return value.value();
+ }
+ return null;
+ }
+
+ public boolean isEmpty() { return values.isEmpty(); }
+
+ @Override
+ public String toString() {
+ return values.toString();
+ }
+
+ public static class Builder<VALUE> {
+
+ /** The minimal set of variants needed to capture all values at this key */
+ private Map<VALUE, Value.Builder<VALUE>> buildableVariants = new HashMap<>();
+
+ public void add(VALUE value, DimensionBinding variantBinding) {
+ // Note: We know we can index by the value because its possible types are constrained
+ // to what query profiles allow: String, primitives and query profiles
+ Value.Builder variant = buildableVariants.get(value);
+ if (variant == null) {
+ variant = new Value.Builder<>(value);
+ buildableVariants.put(value, variant);
+ }
+ variant.addVariant(variantBinding);
+ }
+
+ public DimensionalValue<VALUE> build() {
+ List<Value> variants = new ArrayList<>();
+ for (Value.Builder buildableVariant : buildableVariants.values()) {
+ variants.addAll(buildableVariant.build());
+ }
+ return new DimensionalValue(variants);
+ }
+
+ }
+
+ /** A value for a particular binding */
+ private static class Value<VALUE> implements Comparable<Value> {
+
+ private VALUE value = null;
+
+ /** The minimal binding this holds for */
+ private Binding binding = null;
+
+ public Value(VALUE value, Binding binding) {
+ this.value = value;
+ this.binding = binding;
+ }
+
+ /** Returns the value at this entry or null if none */
+ public VALUE value() { return value; }
+
+ /** Returns the binding that must match for this to be a valid entry, or Binding.nullBinding if none */
+ public Binding binding() {
+ if (binding == null) return Binding.nullBinding;
+ return binding;
+ }
+
+ public boolean matches(Map<String, String> context) {
+ return binding.matches(context);
+ }
+
+ @Override
+ public int compareTo(Value other) {
+ return this.binding.compareTo(other.binding);
+ }
+
+ @Override
+ public String toString() {
+ return " value '" + value + "' for " + binding;
+ }
+
+ /**
+ * A single value with the minimal set of dimension combinations it holds for.
+ */
+ private static class Builder<VALUE> {
+
+ private final VALUE value;
+
+ /**
+ * The set of bindings this value is for.
+ * Some of these are more general versions of others.
+ * We need to keep both to allow interleaving a different value with medium generality.
+ */
+ private Set<DimensionBinding> variants = new HashSet<>();
+
+ public Builder(VALUE value) {
+ this.value = value;
+ }
+
+ /** Add a binding this holds for */
+ public void addVariant(DimensionBinding binding) {
+ variants.add(binding);
+ }
+
+ /** Build a separate value object for each dimension combination which has this value */
+ public List<Value<VALUE>> build() {
+ // Shortcut for efficiency of the normal case
+ if (variants.size()==1)
+ return Collections.singletonList(new Value<>(value, Binding.createFrom(variants.iterator().next())));
+
+ List<Value<VALUE>> values = new ArrayList<>(variants.size());
+ for (DimensionBinding variant : variants)
+ values.add(new Value<>(value, Binding.createFrom(variant)));
+ return values;
+ }
+
+ public Object value() {
+ return value;
+ }
+
+ }
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/config/QueryProfileConfigurer.java b/container-search/src/main/java/com/yahoo/search/query/profile/config/QueryProfileConfigurer.java
new file mode 100644
index 00000000000..5770665e3a1
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/config/QueryProfileConfigurer.java
@@ -0,0 +1,227 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.config;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.subscription.ConfigSubscriber;
+import com.yahoo.search.query.profile.DimensionValues;
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.FieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.QueryProfileTypeRegistry;
+import com.yahoo.text.BooleanParser;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author bratseth
+ */
+public class QueryProfileConfigurer implements ConfigSubscriber.SingleSubscriber<QueryProfilesConfig> {
+
+ private final ConfigSubscriber subscriber = new ConfigSubscriber();
+
+ private volatile QueryProfileRegistry currentRegistry;
+
+ public QueryProfileConfigurer(String configId) {
+ subscriber.subscribe(this, QueryProfilesConfig.class, configId);
+ }
+
+ /** Returns the registry created by the last occurring call to configure */
+ public QueryProfileRegistry getCurrentRegistry() { return currentRegistry; }
+
+ private void setCurrentRegistry(QueryProfileRegistry registry) {
+ this.currentRegistry=registry;
+ }
+
+ public void configure(QueryProfilesConfig config) {
+ QueryProfileRegistry registry = createFromConfig(config);
+ setCurrentRegistry(registry);
+ }
+
+ public static QueryProfileRegistry createFromConfig(QueryProfilesConfig config) {
+ QueryProfileRegistry registry=new QueryProfileRegistry();
+
+ // Pass 1: Create all profiles and profile types
+ for (QueryProfilesConfig.Queryprofiletype profileTypeConfig : config.queryprofiletype()) {
+ createProfileType(profileTypeConfig,registry.getTypeRegistry());
+ }
+ for (QueryProfilesConfig.Queryprofile profileConfig : config.queryprofile()) {
+ createProfile(profileConfig,registry);
+ }
+
+ // Pass 2: Resolve references and add content
+ for (QueryProfilesConfig.Queryprofiletype profileTypeConfig : config.queryprofiletype()) {
+ fillProfileType(profileTypeConfig,registry.getTypeRegistry());
+ }
+
+ // To ensure topological sorting, using DPS. This will _NOT_ detect cycles (but it will not fail if they
+ // exist either)
+ Set<ComponentId> filled = new HashSet<>();
+ for (QueryProfilesConfig.Queryprofile profileConfig : config.queryprofile()) {
+ fillProfile(profileConfig, config, registry, filled);
+ }
+
+ registry.freeze();
+ return registry;
+ }
+
+ /** Stop subscribing from this configurer */
+ public void shutdown() {
+ subscriber.close();
+ }
+
+ private static void createProfile(QueryProfilesConfig.Queryprofile config,QueryProfileRegistry registry) {
+ QueryProfile profile=new QueryProfile(config.id());
+ try {
+ String typeId=config.type();
+ if (typeId!=null && !typeId.isEmpty())
+ profile.setType(registry.getType(typeId));
+
+ if (config.dimensions().size()>0) {
+ String[] dimensions=new String[config.dimensions().size()];
+ for (int i=0; i<config.dimensions().size(); i++)
+ dimensions[i]=config.dimensions().get(i);
+ profile.setDimensions(dimensions);
+ }
+
+ registry.register(profile);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid " + profile,e);
+ }
+ }
+
+ private static void createProfileType(QueryProfilesConfig.Queryprofiletype config, QueryProfileTypeRegistry registry) {
+ QueryProfileType type=new QueryProfileType(config.id());
+ type.setStrict(config.strict());
+ type.setMatchAsPath(config.matchaspath());
+ registry.register(type);
+ }
+
+ private static void fillProfile(QueryProfilesConfig.Queryprofile config,
+ QueryProfilesConfig queryProfilesConfig,
+ QueryProfileRegistry registry,
+ Set<ComponentId> filled) {
+ QueryProfile profile=registry.getComponent(new ComponentSpecification(config.id()).toId());
+ if (filled.contains(profile.getId())) return;
+ filled.add(profile.getId());
+ try {
+ for (String inheritedId : config.inherit()) {
+ QueryProfile inherited=registry.getComponent(inheritedId);
+ if (inherited==null)
+ throw new IllegalArgumentException("Inherited query profile '" + inheritedId + "' in " + profile + " was not found");
+ fillProfile(inherited, queryProfilesConfig, registry, filled);
+ profile.addInherited(inherited);
+ }
+
+ for (QueryProfilesConfig.Queryprofile.Reference referenceConfig : config.reference()) {
+ QueryProfile referenced=registry.getComponent(referenceConfig.value());
+ if (referenced==null)
+ throw new IllegalArgumentException("Query profile '" + referenceConfig.value() + "' referenced as '" +
+ referenceConfig.name() + "' in " + profile + " was not found");
+ profile.set(referenceConfig.name(),referenced, registry);
+ if (referenceConfig.overridable()!=null && !referenceConfig.overridable().isEmpty())
+ profile.setOverridable(referenceConfig.name(),BooleanParser.parseBoolean(referenceConfig.overridable()),null);
+ }
+
+ for (QueryProfilesConfig.Queryprofile.Property propertyConfig : config.property()) {
+ profile.set(propertyConfig.name(),propertyConfig.value(), registry);
+ if (propertyConfig.overridable()!=null && !propertyConfig.overridable().isEmpty())
+ profile.setOverridable(propertyConfig.name(),BooleanParser.parseBoolean(propertyConfig.overridable()),null);
+ }
+
+ for (QueryProfilesConfig.Queryprofile.Queryprofilevariant variantConfig : config.queryprofilevariant()) {
+ String[] forDimensionValueArray=new String[variantConfig.fordimensionvalues().size()];
+ for (int i=0; i<variantConfig.fordimensionvalues().size(); i++) {
+ forDimensionValueArray[i]=variantConfig.fordimensionvalues().get(i).trim();
+ if ("*".equals(forDimensionValueArray[i]))
+ forDimensionValueArray[i]=null;
+ }
+ DimensionValues forDimensionValues=DimensionValues.createFrom(forDimensionValueArray);
+
+ for (String inheritedId : variantConfig.inherit()) {
+ QueryProfile inherited=registry.getComponent(inheritedId);
+ if (inherited==null)
+ throw new IllegalArgumentException("Inherited query profile '" + inheritedId + "' in " + profile +
+ " for '" + forDimensionValues + "' was not found");
+ fillProfile(inherited, queryProfilesConfig, registry, filled);
+ profile.addInherited(inherited, forDimensionValues);
+ }
+
+ for (QueryProfilesConfig.Queryprofile.Queryprofilevariant.Reference referenceConfig : variantConfig.reference()) {
+ QueryProfile referenced=registry.getComponent(referenceConfig.value());
+ if (referenced==null)
+ throw new IllegalArgumentException("Query profile '" + referenceConfig.value() + "' referenced as '" +
+ referenceConfig.name() + "' in " + profile + " for '" + forDimensionValues + "' was not found");
+ profile.set(referenceConfig.name(), referenced, forDimensionValues, registry);
+ }
+
+ for (QueryProfilesConfig.Queryprofile.Queryprofilevariant.Property propertyConfig : variantConfig.property()) {
+ profile.set(propertyConfig.name(), propertyConfig.value(), forDimensionValues, registry);
+ }
+
+ }
+
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid " + profile,e);
+ }
+ }
+
+ /** Fill a given profile by locating its config */
+ private static void fillProfile(QueryProfile inherited,
+ QueryProfilesConfig queryProfilesConfig,
+ QueryProfileRegistry registry,
+ Set<ComponentId> visited) {
+ for (QueryProfilesConfig.Queryprofile inheritedConfig : queryProfilesConfig.queryprofile()) {
+ if (inherited.getId().stringValue().equals(inheritedConfig.id())) {
+ fillProfile(inheritedConfig, queryProfilesConfig, registry, visited);
+ }
+ }
+ }
+
+ private static void fillProfileType(QueryProfilesConfig.Queryprofiletype config,QueryProfileTypeRegistry registry) {
+ QueryProfileType type=registry.getComponent(new ComponentSpecification(config.id()).toId());
+ try {
+
+ for (String inheritedId : config.inherit()) {
+ QueryProfileType inherited=registry.getComponent(inheritedId);
+ if (inherited==null)
+ throw new IllegalArgumentException("Inherited query profile type '" + inheritedId + "' in " + type + " was not found");
+ else
+ type.inherited().add(inherited);
+
+ }
+
+ for (QueryProfilesConfig.Queryprofiletype.Field fieldConfig : config.field())
+ instantiateFieldDescription(fieldConfig,type,registry);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid " + type,e);
+ }
+ }
+
+ private static void instantiateFieldDescription(QueryProfilesConfig.Queryprofiletype.Field fieldConfig,
+ QueryProfileType type,
+ QueryProfileTypeRegistry registry) {
+ try {
+ FieldType fieldType=FieldType.fromString(fieldConfig.type(),registry);
+ FieldDescription field=new FieldDescription(
+ fieldConfig.name(),
+ fieldType,
+ fieldConfig.alias(),
+ fieldConfig.mandatory(),
+ fieldConfig.overridable()
+ );
+ type.addField(field, registry);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid field '" + fieldConfig.name() + "' in " + type,e);
+ }
+ }
+
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/config/QueryProfileXMLReader.java b/container-search/src/main/java/com/yahoo/search/query/profile/config/QueryProfileXMLReader.java
new file mode 100644
index 00000000000..97e3fb90dc9
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/config/QueryProfileXMLReader.java
@@ -0,0 +1,366 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.config;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.search.query.profile.DimensionValues;
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.FieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.QueryProfileTypeRegistry;
+import com.yahoo.text.XML;
+import org.w3c.dom.Element;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * A class which imports query profiles and types from XML files
+ *
+ * @author bratseth
+ */
+public class QueryProfileXMLReader {
+
+ private static Logger logger=Logger.getLogger(QueryProfileXMLReader.class.getName());
+
+ /**
+ * Reads all query profile xml files in a given directory,
+ * and all type xml files from the immediate subdirectory "types/" (if any)
+ *
+ * @throws RuntimeException if <code>directory</code> is not a readable directory, or if there is some error in the XML
+ */
+ public QueryProfileRegistry read(String directory) {
+ List<NamedReader> queryProfileReaders=new ArrayList<>();
+ List<NamedReader> queryProfileTypeReaders=new ArrayList<>();
+ try {
+ File dir=new File(directory);
+ if ( !dir.isDirectory() ) throw new IllegalArgumentException("Could not read query profiles: '" +
+ directory + "' is not a valid directory.");
+
+ for (File file : sortFiles(dir)) {
+ if ( ! file.getName().endsWith(".xml")) continue;
+ queryProfileReaders.add(new NamedReader(file.getName(),new FileReader(file)));
+ }
+ File typeDir=new File(dir,"types");
+ if (typeDir.isDirectory()) {
+ for (File file : sortFiles(typeDir)) {
+ if ( ! file.getName().endsWith(".xml")) continue;
+ queryProfileTypeReaders.add(new NamedReader(file.getName(),new FileReader(file)));
+ }
+ }
+
+ return read(queryProfileTypeReaders,queryProfileReaders);
+ }
+ catch (IOException e) {
+ throw new IllegalArgumentException("Could not read query profiles from '" + directory + "'",e);
+ }
+ finally {
+ closeAll(queryProfileReaders);
+ closeAll(queryProfileTypeReaders);
+ }
+ }
+
+ private List<File> sortFiles(File dir) {
+ ArrayList<File> files = new ArrayList<>();
+ files.addAll(Arrays.asList(dir.listFiles()));
+ Collections.sort(files);
+ return files;
+ }
+
+ private void closeAll(List<NamedReader> readers) {
+ for (NamedReader reader : readers) {
+ try { reader.close(); } catch (IOException e) { }
+ }
+ }
+
+ /**
+ * Read the XML file readers into a registry. This does not close the readers.
+ * This method is used directly from the admin system.
+ */
+ public QueryProfileRegistry read(List<NamedReader> queryProfileTypeReaders,List<NamedReader> queryProfileReaders) {
+ QueryProfileRegistry registry=new QueryProfileRegistry();
+
+ // Phase 1
+ List<Element> queryProfileTypeElements=createQueryProfileTypes(queryProfileTypeReaders,registry.getTypeRegistry());
+ List<Element> queryProfileElements=createQueryProfiles(queryProfileReaders,registry);
+
+ // Phase 2
+ fillQueryProfileTypes(queryProfileTypeElements,registry.getTypeRegistry());
+ fillQueryProfiles(queryProfileElements,registry);
+ return registry;
+ }
+
+ public List<Element> createQueryProfileTypes(List<NamedReader> queryProfileTypeReaders, QueryProfileTypeRegistry registry) {
+ List<Element> queryProfileTypeElements=new ArrayList<>(queryProfileTypeReaders.size());
+ for (NamedReader reader : queryProfileTypeReaders) {
+ Element root=XML.getDocument(reader).getDocumentElement();
+ if ( ! root.getNodeName().equals("query-profile-type")) {
+ logger.info("Ignoring '" + reader.getName() +
+ "': Expected XML root element 'query-profile-type' but was '" + root.getNodeName() + "'");
+ continue;
+ }
+
+ String idString=root.getAttribute("id");
+ if (idString==null || idString.equals(""))
+ throw new IllegalArgumentException("'" + reader.getName() + "' has no 'id' attribute in the root element");
+ ComponentId id=new ComponentId(idString);
+ validateFileNameToId(reader.getName(),id,"query profile type");
+ QueryProfileType type=new QueryProfileType(id);
+ type.setMatchAsPath(XML.getChild(root,"match") != null);
+ type.setStrict(XML.getChild(root,"strict") != null);
+ registry.register(type);
+ queryProfileTypeElements.add(root);
+ }
+ return queryProfileTypeElements;
+ }
+
+ public List<Element> createQueryProfiles(List<NamedReader> queryProfileReaders, QueryProfileRegistry registry) {
+ List<Element> queryProfileElements=new ArrayList<>(queryProfileReaders.size());
+ for (NamedReader reader : queryProfileReaders) {
+ Element root=XML.getDocument(reader).getDocumentElement();
+ if ( ! root.getNodeName().equals("query-profile")) {
+ logger.info("Ignoring '" + reader.getName() +
+ "': Expected XML root element 'query-profile' but was '" + root.getNodeName() + "'");
+ continue;
+ }
+
+ String idString=root.getAttribute("id");
+ if (idString==null || idString.equals(""))
+ throw new IllegalArgumentException("Query profile '" + reader.getName() + "' has no 'id' attribute in the root element");
+ ComponentId id=new ComponentId(idString);
+ validateFileNameToId(reader.getName(),id,"query profile");
+
+ QueryProfile queryProfile=new QueryProfile(id);
+ String typeId=root.getAttribute("type");
+ if (typeId!=null && ! typeId.equals("")) {
+ QueryProfileType type=registry.getType(typeId);
+ if (type==null)
+ throw new IllegalArgumentException("Query profile '" + reader.getName() + "': Type id '" + typeId + "' can not be resolved");
+ queryProfile.setType(type);
+ }
+
+ Element dimensions=XML.getChild(root,"dimensions");
+ if (dimensions!=null)
+ queryProfile.setDimensions(toArray(XML.getValue(dimensions)));
+
+ registry.register(queryProfile);
+ queryProfileElements.add(root);
+ }
+ return queryProfileElements;
+ }
+
+ /** Throws an exception if the name is not corresponding to the id */
+ private void validateFileNameToId(final String actualName,ComponentId id,String artifactName) {
+ String expectedCanonicalFileName=id.toFileName();
+ String expectedAlternativeFileName=id.stringValue().replace(":","-").replace("/","_"); // legacy
+ String fileName=new File(actualName).getName();
+ fileName=stripXmlEnding(fileName);
+ String canonicalFileName=ComponentId.fromFileName(fileName).toFileName();
+ if ( ! canonicalFileName.equals(expectedCanonicalFileName) && ! canonicalFileName.equals(expectedAlternativeFileName))
+ throw new IllegalArgumentException("The file name of " + artifactName + " '" + id +
+ "' must be '" + expectedCanonicalFileName + ".xml' but was '" + actualName + "'");
+ }
+
+ private String stripXmlEnding(String fileName) {
+ if (!fileName.endsWith(".xml"))
+ throw new IllegalArgumentException("'" + fileName + "' should have a .xml ending");
+ else
+ return fileName.substring(0,fileName.length()-4);
+ }
+
+ private String[] toArray(String csv) {
+ String[] array=csv.split(",");
+ for (int i=0; i<array.length; i++)
+ array[i]=array[i].trim();
+ return array;
+ }
+
+ public void fillQueryProfileTypes(List<Element> queryProfileTypeElements, QueryProfileTypeRegistry registry) {
+ for (Element element : queryProfileTypeElements) {
+ QueryProfileType type=registry.getComponent(new ComponentSpecification(element.getAttribute("id")).toId());
+ try {
+ readInheritedTypes(element,type,registry);
+ readFieldDefinitions(element,type,registry);
+ }
+ catch (RuntimeException e) {
+ throw new IllegalArgumentException("Error reading " + type,e);
+ }
+ }
+ }
+
+ private void readInheritedTypes(Element element,QueryProfileType type,QueryProfileTypeRegistry registry) {
+ String inheritedString=element.getAttribute("inherits");
+ if (inheritedString==null || inheritedString.equals("")) return;
+ for (String inheritedId : inheritedString.split(" ")) {
+ inheritedId=inheritedId.trim();
+ if (inheritedId.equals("")) continue;
+ QueryProfileType inheritedType=registry.getComponent(inheritedId);
+ if (inheritedType==null) throw new IllegalArgumentException("Could not resolve inherited query profile type '" + inheritedId);
+ type.inherited().add(inheritedType);
+ }
+ }
+
+ private void readFieldDefinitions(Element element,QueryProfileType type,QueryProfileTypeRegistry registry) {
+ for (Element field : XML.getChildren(element,"field")) {
+ String name=field.getAttribute("name");
+ if (name==null || name.equals("")) throw new IllegalArgumentException("A field has no 'name' attribute");
+ try {
+ String fieldTypeName=field.getAttribute("type");
+ if (fieldTypeName==null) throw new IllegalArgumentException("Field '" + field + "' has no 'type' attribute");
+ FieldType fieldType=FieldType.fromString(fieldTypeName,registry);
+ type.addField(new FieldDescription(name,fieldType,field.getAttribute("alias"),
+ getBooleanAttribute("mandatory",false,field),getBooleanAttribute("overridable",true,field)), registry);
+ }
+ catch(RuntimeException e) {
+ throw new IllegalArgumentException("Invalid field '" + name + "'",e);
+ }
+ }
+ }
+
+ public void fillQueryProfiles(List<Element> queryProfileElements, QueryProfileRegistry registry) {
+ for (Element element : queryProfileElements) {
+ // Lookup by exact id
+ QueryProfile profile=registry.getComponent(new ComponentSpecification(element.getAttribute("id")).toId());
+ try {
+ readInherited(element,profile,registry,null,profile.toString());
+ readFields(element,profile,registry,null,profile.toString());
+ readVariants(element,profile,registry);
+ }
+ catch (RuntimeException e) {
+ throw new IllegalArgumentException("Error reading " + profile,e);
+ }
+ }
+ }
+
+ private void readInherited(Element element,QueryProfile profile,QueryProfileRegistry registry,DimensionValues dimensionValues,String sourceDescription) {
+ String inheritedString=element.getAttribute("inherits");
+ if (inheritedString==null || inheritedString.equals("")) return;
+ for (String inheritedId : inheritedString.split(" ")) {
+ inheritedId=inheritedId.trim();
+ if (inheritedId.equals("")) continue;
+ QueryProfile inheritedProfile=registry.getComponent(inheritedId);
+ if (inheritedProfile==null) throw new IllegalArgumentException("Could not resolve inherited query profile '" + inheritedId + "' in " + sourceDescription);
+ profile.addInherited(inheritedProfile,dimensionValues);
+ }
+ }
+
+ private void readFields(Element element,QueryProfile profile,QueryProfileRegistry registry,DimensionValues dimensionValues,String sourceDescription) {
+ List<KeyValue> references=new ArrayList<>();
+ List<KeyValue> properties=new ArrayList<>();
+ for (Element field : XML.getChildren(element,"field")) {
+ String name=field.getAttribute("name");
+ if (name==null || name.equals("")) throw new IllegalArgumentException("A field in " + sourceDescription + " has no 'name' attribute");
+ try {
+ Boolean overridable=getBooleanAttribute("overridable",null,field);
+ if (overridable!=null)
+ profile.setOverridable(name,overridable,null);
+
+ Object fieldValue=readFieldValue(field,name,sourceDescription,registry);
+ if (fieldValue instanceof QueryProfile)
+ references.add(new KeyValue(name,fieldValue));
+ else
+ properties.add(new KeyValue(name,fieldValue));
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid field '" + name + "' in " + sourceDescription,e);
+ }
+ }
+ // Must set references before properties
+ for (KeyValue keyValue : references)
+ profile.set(keyValue.getKey() ,keyValue.getValue(), dimensionValues, registry);
+ for (KeyValue keyValue : properties)
+ profile.set(keyValue.getKey(), keyValue.getValue(), dimensionValues, registry);
+
+ }
+
+ private Object readFieldValue(Element field,String name,String targetDescription,QueryProfileRegistry registry) {
+ Element ref=XML.getChild(field,"ref");
+ if (ref!=null) {
+ String referencedName=XML.getValue(ref);
+ QueryProfile referenced=registry.getComponent(referencedName);
+ if (referenced==null)
+ throw new IllegalArgumentException("Could not find query profile '" + referencedName + "' referenced as '" +
+ name + "' in " + targetDescription);
+ return referenced;
+ }
+ else {
+ return XML.getValue(field);
+ }
+ }
+
+ private void readVariants(Element element,QueryProfile profile,QueryProfileRegistry registry) {
+ for (Element queryProfileVariantElement : XML.getChildren(element,"query-profile")) { // A "virtual" query profile contained inside another
+ List<String> dimensions=profile.getDimensions();
+ if (dimensions==null)
+ throw new IllegalArgumentException("Cannot create a query profile variant in " + profile +
+ ", as it has not declared any variable dimensions");
+ String dimensionString=queryProfileVariantElement.getAttribute("for");
+ String[] dimensionValueArray=makeStarsNull(toArray(dimensionString));
+ if (dimensions.size()<dimensionValueArray.length)
+ throw new IllegalArgumentException("Cannot create a query profile variant for '" + dimensionString +
+ "' as only " + dimensions.size() + " dimensions has been defined");
+ DimensionValues dimensionValues=DimensionValues.createFrom(dimensionValueArray);
+
+ String description="variant '" + dimensionString + "' in " + profile.toString();
+ readInherited(queryProfileVariantElement,profile,registry,dimensionValues,description);
+ readFields(queryProfileVariantElement,profile,registry,dimensionValues,description);
+ }
+ }
+
+ private String[] makeStarsNull(String[] strings) {
+ for (int i=0; i<strings.length; i++)
+ if (strings[i].equals("*"))
+ strings[i]=null;
+ return strings;
+ }
+
+ /**
+ * Returns true if the string is "true".<br>
+ * Returns false if the string is "false".<br>
+ * Returns <code>default</code> if the string is null or empty (this parameter may be null)<br>
+ * @throws IllegalArgumentException if the string has any other value
+ */
+ private Boolean asBoolean(String s,Boolean defaultValue) {
+ if (s==null) return defaultValue;
+ if (s.isEmpty()) return defaultValue;
+ if ("true".equals(s)) return true;
+ if ("false".equals(s)) return false;
+ throw new IllegalArgumentException("Expected 'true' or 'false' but was'" + s + "'");
+ }
+
+ /** Returns the given attribute as a boolean, using the semantics of {@link #asBoolean} */
+ private Boolean getBooleanAttribute(String attributeName,Boolean defaultValue,Element from) {
+ try {
+ return asBoolean(from.getAttribute(attributeName),defaultValue);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Attribute '" + attributeName,e);
+ }
+ }
+
+ private static class KeyValue {
+
+ private String key;
+ private Object value;
+
+ public KeyValue(String key,Object value) {
+ this.key=key;
+ this.value=value;
+ }
+
+ public String getKey() { return key; }
+
+ public Object getValue() { return value; }
+
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/config/package-info.java b/container-search/src/main/java/com/yahoo/search/query/profile/config/package-info.java
new file mode 100644
index 00000000000..8ea4e887661
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/config/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.search.query.profile.config;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/package-info.java b/container-search/src/main/java/com/yahoo/search/query/profile/package-info.java
new file mode 100644
index 00000000000..df3f4ac45ab
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/package-info.java
@@ -0,0 +1,12 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * Query Profiles provide nested sets of named (and optionally typed) key-values which can be referenced in a Query
+ * to proviode initial values of Query properties. Values in nested query profiles can be looked up from
+ * the query properties by dotting the names. Query profiles supports inheritance to allow variations
+ * for, e.g different buckets, client types, markets etc. */
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query.profile;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/types/FieldDescription.java b/container-search/src/main/java/com/yahoo/search/query/profile/types/FieldDescription.java
new file mode 100644
index 00000000000..c522ec04023
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/types/FieldDescription.java
@@ -0,0 +1,148 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.types;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.profile.QueryProfile;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A field description of a query profile type. Immutable.
+ * Field descriptions can be sorted by name.
+ *
+ * @author bratseth
+ */
+public class FieldDescription implements Comparable<FieldDescription> {
+
+ private final CompoundName name;
+ private final FieldType type;
+ private final List<String> aliases;
+
+ /** If true, this value must be provided either in the query profile or in the search request */
+ private final boolean mandatory;
+
+ /** If true, assignments to this value from outside will be ignored */
+ private final boolean overridable;
+
+ public FieldDescription(String name, FieldType type) {
+ this(name,type,false);
+ }
+
+ public FieldDescription(String name, String type) {
+ this(name,FieldType.fromString(type,null));
+ }
+
+ public FieldDescription(String name, FieldType type, boolean mandatory) {
+ this(name, type, mandatory, true);
+ }
+
+ public FieldDescription(String name, String type, String aliases) {
+ this(name,type,aliases,false,true);
+ }
+
+ public FieldDescription(String name, FieldType type, String aliases) {
+ this(name, type, aliases, false, true);
+ }
+
+ /**
+ * Creates a field description
+ *
+ * @param name the name of the field
+ * @param typeString the type of the field represented as a string - see {@link com.yahoo.search.query.profile.types.FieldType}
+ * @param aliases a space-separated list of alias names of this field name. Aliases are not following dotted
+ * (meaning they are global, not that they cannot contain dots) and are case insensitive. Null is permissible
+ * if there are no aliases
+ * @param mandatory whether it is mandatory to provide a value for this field. default: false
+ * @param overridable whether this can be overridden when first set in a profile. Default: true
+ */
+ public FieldDescription(String name, String typeString, String aliases, boolean mandatory, boolean overridable) {
+ this(name,FieldType.fromString(typeString,null),aliases,mandatory,overridable);
+ }
+
+ public FieldDescription(String name, FieldType type, boolean mandatory, boolean overridable) {
+ this(name, type, null, mandatory, overridable);
+ }
+
+ public FieldDescription(String name, FieldType type, String aliases, boolean mandatory, boolean overridable) {
+ this(new CompoundName(name), type, aliases, mandatory, overridable);
+ }
+
+ /**
+ * Creates a field description from a list where the aliases are represented as a comma-separated string
+ */
+ public FieldDescription(CompoundName name, FieldType type, String aliases, boolean mandatory, boolean overridable) {
+ this(name, type, toList(aliases), mandatory, overridable);
+ }
+
+ /**
+ * Creates a field description
+ *
+ * @param name the name of the field
+ * @param type the type of the field represented as a string - see {@link com.yahoo.search.query.profile.types.FieldType}
+ * @param aliases a list of aliases, never null. Aliases are not following dotted
+ * (meaning they are global, not that they cannot contain dots) and are case insensitive.
+ * @param mandatory whether it is mandatory to provide a value for this field. default: false
+ * @param overridable whether this can be overridden when first set in a profile. Default: true
+ */
+ public FieldDescription(CompoundName name, FieldType type, List<String> aliases, boolean mandatory, boolean overridable) {
+ if (name.isEmpty())
+ throw new IllegalArgumentException("Illegal name ''");
+ for (String nameComponent : name.asList())
+ QueryProfile.validateName(nameComponent);
+ this.name = name;
+ this.type = type;
+
+ // Forbidden until we can figure out the right semantics
+ if (name.isCompound() && ! aliases.isEmpty()) throw new IllegalArgumentException("Aliases is not allowed with compound names");
+
+ this.aliases = ImmutableList.copyOf(aliases);
+ this.mandatory = mandatory;
+ this.overridable = overridable;
+ }
+
+ private static List<String> toList(String string) {
+ if (string == null || string.isEmpty()) return ImmutableList.of();
+ return ImmutableList.copyOf(Arrays.asList(string.split(" ")));
+ }
+
+ /** Returns the full name of this as a string */
+ public String getName() { return name.toString(); }
+
+ /** Returns the full name of this as a compound name */
+ public CompoundName getCompoundName() { return name; }
+
+ public FieldType getType() { return type; }
+
+ /** Returns a unmodifiable list of the aliases of this. An empty list (never null) if there are none. */
+ public List<String> getAliases() { return aliases; }
+
+ /** Returns whether this field must be provided in the query profile or the search definition. Default: false */
+ public boolean isMandatory() { return mandatory; }
+
+ /** Returns false if overrides to values for this field from the outside should be ignored. Default: true */
+ public boolean isOverridable() { return overridable; }
+
+ public int compareTo(FieldDescription other) {
+ return name.toString().compareTo(other.name.toString());
+ }
+
+ /** Returns a copy of this with the name set to the argument name */
+ public FieldDescription withName(CompoundName name) {
+ return new FieldDescription(name, type, aliases, mandatory, overridable);
+ }
+
+ /** Returns a copy of this with the type set to the argument type */
+ public FieldDescription withType(FieldType type) {
+ return new FieldDescription(name, type, aliases, mandatory, overridable);
+ }
+
+ @Override
+ public String toString() {
+ return "field '" + name + "' type " + type.stringValue() + "" +
+ (mandatory?" (mandatory)":"") + (!overridable?" (not overridable)":"");
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/types/FieldType.java b/container-search/src/main/java/com/yahoo/search/query/profile/types/FieldType.java
new file mode 100644
index 00000000000..abe3c4425ae
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/types/FieldType.java
@@ -0,0 +1,94 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.types;
+
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.search.yql.YqlQuery;
+import com.yahoo.tensor.Tensor;
+
+import java.util.Optional;
+
+/**
+ * Superclass of query type field types.
+ * Field types are immutable.
+ *
+ * @author bratseth
+ */
+@SuppressWarnings("rawtypes")
+public abstract class FieldType {
+
+ public static final PrimitiveFieldType stringType = new PrimitiveFieldType(String.class);
+ public static final PrimitiveFieldType integerType = new PrimitiveFieldType(Integer.class);
+ public static final PrimitiveFieldType longType = new PrimitiveFieldType(Long.class);
+ public static final PrimitiveFieldType floatType = new PrimitiveFieldType(Float.class);
+ public static final PrimitiveFieldType doubleType = new PrimitiveFieldType(Double.class);
+ public static final PrimitiveFieldType booleanType = new PrimitiveFieldType(Boolean.class);
+ public static final TensorFieldType genericTensorType = new TensorFieldType(Optional.empty());
+ public static final QueryFieldType queryType = new QueryFieldType();
+ public static final QueryProfileFieldType genericQueryProfileType = new QueryProfileFieldType();
+
+ /** Returns the class of instance values of this field type */
+ public abstract Class getValueClass();
+
+ /** Returns a string representation of this type which can be converted back to a type class by {@link #fromString} */
+ public abstract String stringValue();
+
+ public abstract String toString();
+
+ /** Returns a string describing possible instances of this type, suitable for user error messages */
+ public abstract String toInstanceDescription();
+
+ /** Converts the given type to an instance of this type, if possible. Returns null if not possible. */
+ public abstract Object convertFrom(Object o, QueryProfileRegistry registry);
+
+ /** Converts the given type to an instance of this type, if possible. Returns null if not possible. */
+ public abstract Object convertFrom(Object o, CompiledQueryProfileRegistry registry);
+
+ /**
+ * Returns the field type for a given string name.
+ *
+ * @param typeString a type string - a primitive name, "query-profile" or "query-profile:profile-name"
+ * @param registry the registry in which query profile references are resolved when the last form above is used,
+ * or null in which case that form cannot be used
+ * @throws IllegalArgumentException if the string does not resolve to a type
+ */
+ public static FieldType fromString(String typeString, QueryProfileTypeRegistry registry) {
+ if ("string".equals(typeString))
+ return stringType;
+ if ("integer".equals(typeString))
+ return integerType;
+ if ("long".equals(typeString))
+ return longType;
+ if ("float".equals(typeString))
+ return floatType;
+ if ("double".equals(typeString))
+ return doubleType;
+ if ("boolean".equals(typeString))
+ return booleanType;
+ if ("query".equals(typeString))
+ return queryType;
+ if (typeString.startsWith("tensor"))
+ return TensorFieldType.fromTypeString(typeString);
+ if ("query-profile".equals(typeString))
+ return genericQueryProfileType;
+ if (typeString.startsWith("query-profile:"))
+ return QueryProfileFieldType.fromString(typeString.substring("query-profile:".length()),registry);
+ throw new IllegalArgumentException("Unknown type '" + typeString + "'");
+ }
+
+ /** Returns the field type from a value class, or null if there is no type for it */
+ public static FieldType fromClass(Class clazz) {
+ if (clazz == String.class) return stringType;
+ if (clazz == Integer.class) return integerType;
+ if (clazz == Long.class) return longType;
+ if (clazz == Float.class) return floatType;
+ if (clazz == Double.class) return doubleType;
+ if (clazz == Boolean.class) return booleanType;
+ if (clazz == Tensor.class) return genericTensorType;
+ if (clazz == YqlQuery.class) return queryType;
+ if (clazz == QueryProfile.class) return genericQueryProfileType;
+ return null;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/types/PrimitiveFieldType.java b/container-search/src/main/java/com/yahoo/search/query/profile/types/PrimitiveFieldType.java
new file mode 100644
index 00000000000..76b3f78ac2f
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/types/PrimitiveFieldType.java
@@ -0,0 +1,86 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.types;
+
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+
+import static com.yahoo.text.Lowercase.toLowerCase;
+
+/**
+ * Represents a query field type which is a primitive - String, Integer, Float, Double or Long.
+ *
+ * @author bratseth
+ */
+@SuppressWarnings("rawtypes")
+public class PrimitiveFieldType extends FieldType {
+
+ private Class primitiveClass;
+
+ PrimitiveFieldType(Class primitiveClass) {
+ this.primitiveClass=primitiveClass;
+ }
+
+ public @Override Class getValueClass() { return primitiveClass; }
+
+ public @Override String stringValue() {
+ return toLowerCase(primitiveClass.getSimpleName());
+ }
+
+ public @Override String toString() { return "field type " + stringValue(); }
+
+ public @Override String toInstanceDescription() {
+ return toLowerCase(primitiveClass.getSimpleName());
+ }
+
+ @Override
+ public Object convertFrom(Object object, CompiledQueryProfileRegistry registry) {
+ return convertFrom(object, (QueryProfileRegistry)null);
+ }
+
+ public @Override Object convertFrom(Object object, QueryProfileRegistry registry) {
+ if (primitiveClass == object.getClass()) return object;
+
+ if (object.getClass() == String.class) return convertFromString((String)object);
+ if (object instanceof Number) return convertFromNumber((Number)object);
+
+ return null;
+ }
+
+ private Object convertFromString(String string) {
+ try {
+ if (primitiveClass==Integer.class) return Integer.valueOf(string);
+ if (primitiveClass==Double.class) return Double.valueOf(string);
+ if (primitiveClass==Float.class) return Float.valueOf(string);
+ if (primitiveClass==Long.class) return Long.valueOf(string);
+ if (primitiveClass==Boolean.class) return Boolean.valueOf(string);
+ }
+ catch (NumberFormatException e) {
+ return null; // Handled in caller
+ }
+ throw new RuntimeException("Programming error");
+ }
+
+ private Object convertFromNumber(Number number) {
+ if (primitiveClass==Integer.class) return number.intValue();
+ if (primitiveClass==Double.class) return number.doubleValue();
+ if (primitiveClass==Float.class) return number.floatValue();
+ if (primitiveClass==Long.class) return number.longValue();
+ if (primitiveClass==String.class) return String.valueOf(number);
+ throw new RuntimeException("Programming error: Input type is " + number.getClass() +
+ " primitiveClass is " + primitiveClass);
+ }
+
+ @Override
+ public int hashCode() {
+ return primitiveClass.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if ( ! (o instanceof PrimitiveFieldType)) return false;
+ PrimitiveFieldType other = (PrimitiveFieldType)o;
+ return other.primitiveClass.equals(this.primitiveClass);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryFieldType.java b/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryFieldType.java
new file mode 100644
index 00000000000..a0982fdf0f6
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryFieldType.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.types;
+
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.search.yql.YqlQuery;
+import com.yahoo.tensor.MapTensor;
+import com.yahoo.tensor.Tensor;
+
+/**
+ * A YQL query template field type in a query profile
+ *
+ * @author bratseth
+ */
+public class QueryFieldType extends FieldType {
+
+ @Override
+ public Class getValueClass() { return YqlQuery.class; }
+
+ @Override
+ public String stringValue() { return "query"; }
+
+ @Override
+ public String toString() { return "field type " + stringValue(); }
+
+ @Override
+ public String toInstanceDescription() { return "a YQL query template"; }
+
+ @Override
+ public Object convertFrom(Object o, QueryProfileRegistry registry) {
+ if (o instanceof YqlQuery) return o;
+ if (o instanceof String) return YqlQuery.from((String)o);
+ return null;
+ }
+
+ @Override
+ public Object convertFrom(Object o, CompiledQueryProfileRegistry registry) {
+ return convertFrom(o, (QueryProfileRegistry)null);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileFieldType.java b/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileFieldType.java
new file mode 100644
index 00000000000..df52e78c6ef
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileFieldType.java
@@ -0,0 +1,100 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.types;
+
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfile;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+
+/**
+ * Represents a query profile field type which is a reference to a query profile.
+ * The reference may optionally specify the type of the referred query profile.
+ *
+ * @author bratseth
+ */
+public class QueryProfileFieldType extends FieldType {
+
+ private final QueryProfileType type;
+
+ public static QueryProfileFieldType fromString(String queryProfileName, QueryProfileTypeRegistry registry) {
+ if (queryProfileName==null || queryProfileName.equals(""))
+ return new QueryProfileFieldType(null);
+
+ if (registry==null)
+ throw new IllegalArgumentException("Can not resolve query profile type '" + queryProfileName +
+ "' because no registry is provided");
+ QueryProfileType queryProfileType=registry.getComponent(queryProfileName);
+ if (queryProfileType==null)
+ throw new IllegalArgumentException("Could not resolve query profile type '" + queryProfileName + "'");
+ return new QueryProfileFieldType(registry.getComponent(queryProfileName));
+ }
+
+ public QueryProfileFieldType() { this(null); }
+
+ public QueryProfileFieldType(QueryProfileType type) {
+ this.type = type;
+ }
+
+ /** Returns the query profile type of this, or null if any type works */
+ public QueryProfileType getQueryProfileType() { return type; }
+
+ public @Override Class<?> getValueClass() { return QueryProfile.class; }
+
+ public @Override String stringValue() {
+ return "query-profile" + (type!=null ? ":" + type.getId().getName() : "");
+ }
+
+ public @Override String toString() {
+ return "field type " + stringValue();
+ }
+
+ public @Override String toInstanceDescription() {
+ return "reference to a query profile" + (type!=null ? " of type '" + type.getId().getName() + "'" : "");
+ }
+
+ @Override
+ public CompiledQueryProfile convertFrom(Object object, CompiledQueryProfileRegistry registry) {
+ String profileId = object.toString();
+ if (profileId.startsWith("ref:"))
+ profileId = profileId.substring("ref:".length());
+ CompiledQueryProfile profile = registry.getComponent(profileId);
+ if (profile == null) return null;
+ if (type != null && ! type.equals(profile.getType())) return null;
+ return profile;
+ }
+
+ @Override
+ public QueryProfile convertFrom(Object object, QueryProfileRegistry registry) {
+ QueryProfile profile;
+ if (object instanceof String)
+ profile = registry.getComponent((String)object);
+ else if (object instanceof QueryProfile)
+ profile = (QueryProfile)object;
+ else
+ return null;
+
+ // Verify its type as well
+ if (type!=null && type!=profile.getType()) return null;
+ return profile;
+ }
+
+ @Override
+ public int hashCode() {
+ if (type == null) return 17;
+ return type.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if ( ! (o instanceof QueryProfileFieldType)) return false;
+ QueryProfileFieldType other = (QueryProfileFieldType)o;
+ return equals(this.type.getId(), other.type.getId());
+ }
+
+ private boolean equals(Object o1, Object o2) {
+ if (o1 == null) return o2 == null;
+ return o1.equals(o2);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileType.java b/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileType.java
new file mode 100644
index 00000000000..ecf60f8723d
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileType.java
@@ -0,0 +1,355 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.types;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.FreezableSimpleComponent;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.profile.QueryProfile;
+
+import java.util.*;
+
+import static com.yahoo.text.Lowercase.toLowerCase;
+
+/**
+ * Defines a kind of query profiles
+ *
+ * @author bratseth
+ */
+public class QueryProfileType extends FreezableSimpleComponent {
+
+ /** The fields of this query profile type */
+ private Map<String, FieldDescription> fields = new HashMap<>();
+
+ /** The query profile types this inherits */
+ private List<QueryProfileType> inherited = new ArrayList<>();
+
+ /** If this is true, keys which are not declared in this type cannot be set in instances */
+ private boolean strict = false;
+
+ /** True if the name of instances of this profile should be matched as path names, see QueryProfileRegistry */
+ private boolean matchAsPath = false;
+
+ private boolean builtin = false;
+
+ /** Aliases *from* any strings *to* field names. Aliases are case insensitive */
+ private Map<String, String> aliases = null;
+
+ public QueryProfileType(String idString) {
+ this(new ComponentId(idString));
+ }
+
+ public QueryProfileType(ComponentId id) {
+ super(id);
+ QueryProfile.validateName(id.getName());
+ }
+
+ private QueryProfileType(ComponentId id, Map<String, FieldDescription> fields, List<QueryProfileType> inherited,
+ boolean strict, boolean matchAsPath, boolean builtin, Map<String,String> aliases) {
+ super(id);
+ this.fields = new HashMap<>(fields);
+ this.inherited = new ArrayList<>(inherited);
+ this.strict = strict;
+ this.matchAsPath = matchAsPath;
+ this.builtin = builtin;
+ this.aliases = aliases == null ? null : new HashMap<>(aliases);
+ }
+
+ /** Return this is it is not frozen, returns a modifiable deeply unfrozen copy otherwise */
+ public QueryProfileType unfrozen() {
+ if ( ! isFrozen()) return this;
+
+ // Unfreeze inherited query profile references
+ List<QueryProfileType> unfrozenInherited = new ArrayList<>();
+ for (QueryProfileType inheritedType : inherited) {
+ unfrozenInherited.add(inheritedType.unfrozen());
+ }
+
+ // Unfreeze nested query profile references
+ Map<String, FieldDescription> unfrozenFields = new HashMap<>();
+ for (Map.Entry<String, FieldDescription> field : fields.entrySet()) {
+ FieldDescription unfrozenFieldValue = field.getValue();
+ if (field.getValue().getType() instanceof QueryProfileFieldType) {
+ QueryProfileFieldType queryProfileFieldType = (QueryProfileFieldType)field.getValue().getType();
+ if (queryProfileFieldType.getQueryProfileType() != null) {
+ QueryProfileFieldType unfrozenType =
+ new QueryProfileFieldType(queryProfileFieldType.getQueryProfileType().unfrozen());
+ unfrozenFieldValue = field.getValue().withType(unfrozenType);
+ }
+ }
+ unfrozenFields.put(field.getKey(), unfrozenFieldValue);
+ }
+
+ return new QueryProfileType(getId(), unfrozenFields, unfrozenInherited, strict, matchAsPath, builtin, aliases);
+ }
+
+ /** Mark this type as built into the system. Do not use */
+ public void setBuiltin(boolean builtin) { this.builtin=builtin; }
+
+ /** Returns whether this type is built into the system */
+ public boolean isBuiltin() { return builtin; }
+
+ /**
+ * Returns the query profile types inherited from this (never null).
+ * If this profile type is not frozen, this list can be modified to change the set of inherited types.
+ * If it is frozen, the returned list is immutable.
+ */
+ public List<QueryProfileType> inherited() { return inherited; }
+
+ /**
+ * Returns the fields declared in this (i.e not including those inherited) as an immutable map.
+ *
+ * @throws IllegalStateException if this is frozen
+ */
+ public Map<String,FieldDescription> declaredFields() {
+ ensureNotFrozen();
+ return Collections.unmodifiableMap(fields);
+ }
+
+ /**
+ * Returns true if <i>this</i> is declared strict.
+ * @throws IllegalStateException if this is frozen
+ */
+ public boolean isDeclaredStrict() {
+ ensureNotFrozen();
+ return strict;
+ }
+
+ /**
+ * Returns true if <i>this</i> is declared as match as path.
+ * @throws IllegalStateException if this is frozen
+ */
+ public boolean getDeclaredMatchAsPath() {
+ ensureNotFrozen();
+ return matchAsPath;
+ }
+
+ /** Set whether nondeclared fields are permissible. Throws an exception if this is frozen. */
+ public void setStrict(boolean strict) {
+ ensureNotFrozen();
+ this.strict=strict;
+ }
+
+ /** Returns whether field not declared in this type is permissible in instances. Default is false: Additional values are allowed */
+ public boolean isStrict() {
+ if (isFrozen()) return strict;
+
+ // Check if any of this or an inherited is true
+ if (strict) return true;
+ for (QueryProfileType inheritedType : inherited)
+ if (inheritedType.isStrict()) return true;
+ return false;
+ }
+
+ /** Returns whether instances of this should be matched as path names. Throws if this is frozen. */
+ public void setMatchAsPath(boolean matchAsPath) {
+ ensureNotFrozen();
+ this.matchAsPath=matchAsPath;
+ }
+
+ /** Returns whether instances of this should be matched as path names. Default is false: Use exact name matching. */
+ public boolean getMatchAsPath() {
+ if (isFrozen()) return matchAsPath;
+
+ // Check if any of this or an inherited is true
+ if (matchAsPath) return true;
+ for (QueryProfileType inheritedType : inherited)
+ if (inheritedType.getMatchAsPath()) return true;
+ return false;
+ }
+
+ public void freeze() {
+ if (isFrozen()) return;
+ // Flatten the inheritance hierarchy into this to facilitate faster lookup
+ for (QueryProfileType inheritedType : inherited) {
+ for (FieldDescription field : inheritedType.fields().values())
+ if ( ! fields.containsKey(field.getName()))
+ fields.put(field.getName(),field);
+ }
+ fields = ImmutableMap.copyOf(fields);
+ inherited = ImmutableList.copyOf(inherited);
+ strict = isStrict();
+ matchAsPath = getMatchAsPath();
+ super.freeze();
+ }
+
+ /**
+ * Returns whether the given field name is overridable in this type.
+ * Default: true (so all non-declared fields returns true)
+ */
+ public boolean isOverridable(String fieldName) {
+ FieldDescription field=getField(fieldName);
+ if (field==null) return true;
+ return field.isOverridable();
+ }
+
+ /**
+ * Returns the permissible class for the value of the given name in this type
+ *
+ * @return the permissible class for a value, <code>Object</code> if all types are legal,
+ * null if no types are legal (i.e if the name is not legal)
+ */
+ public Class<?> getValueClass(String name) {
+ FieldDescription fieldDescription=getField(name);
+ if (fieldDescription==null) {
+ if (strict)
+ return null; // Undefined -> Not legal
+ else
+ return Object.class; // Undefined -> Anything is legal
+ }
+ return fieldDescription.getType().getValueClass();
+ }
+
+ /** Returns the type of the given query profile type declared as a field in this */
+ public QueryProfileType getType(String localName) {
+ FieldDescription fieldDescription=getField(localName);
+ if (fieldDescription ==null) return null;
+ if ( ! (fieldDescription.getType() instanceof QueryProfileFieldType)) return null;
+ return ((QueryProfileFieldType) fieldDescription.getType()).getQueryProfileType();
+ }
+
+ /**
+ * Returns the description of the field with the given name in this type or an inherited type
+ * (depth first left to right search). Returns null if the field is not defined in this or an inherited profile.
+ */
+ public FieldDescription getField(String name) {
+ FieldDescription field=fields.get(name);
+ if ( field!=null ) return field;
+
+ if ( isFrozen() ) return null; // Inherited are collapsed into this
+
+ for (QueryProfileType inheritedType : this.inherited() ) {
+ field=inheritedType.getField(name);
+ if (field!=null) return field;
+ }
+
+ return null;
+ }
+
+ /**
+ * Removes a field from this (not from any inherited profile)
+ *
+ * @return the removed field or null if none
+ * @throws IllegalStateException if this is frozen
+ */
+ public FieldDescription removeField(String fieldName) {
+ ensureNotFrozen();
+ return fields.remove(fieldName);
+ }
+
+ /**
+ * Adds a field to this, without associating with a type registry; field descriptions with compound
+ * is not be supported.
+ *
+ * @throws IllegalStateException if this is frozen
+ */
+ public void addField(FieldDescription fieldDescription) {
+ // Compound names translates to new types, which must be added to a supplied registry
+ if (fieldDescription.getCompoundName().isCompound())
+ throw new IllegalArgumentException("Adding compound names is only legal when supplying a registry");
+ addField(fieldDescription, null);
+ }
+
+ /**
+ * Adds a field to this
+ *
+ * @throws IllegalStateException if this is frozen
+ */
+ public void addField(FieldDescription fieldDescription, QueryProfileTypeRegistry registry) {
+ CompoundName name = fieldDescription.getCompoundName();
+ if (name.isCompound()) {
+ // Add (/to) a query profile type containing the rest of the name.
+ // (we do not need the field description settings for intermediate query profile types
+ // as the leaf entry will enforce them)
+ QueryProfileType type = getOrCreateQueryProfileType(name.first(), registry);
+ type.addField(fieldDescription.withName(name.rest()), registry);
+ }
+ else {
+ ensureNotFrozen();
+ fields.put(fieldDescription.getName(), fieldDescription);
+ }
+
+ for (String alias : fieldDescription.getAliases())
+ addAlias(alias, fieldDescription.getName());
+ }
+
+ private QueryProfileType getOrCreateQueryProfileType(String name, QueryProfileTypeRegistry registry) {
+ FieldDescription fieldDescription = getField(name);
+ if (fieldDescription != null) {
+ if ( ! ( fieldDescription.getType() instanceof QueryProfileFieldType))
+ throw new IllegalArgumentException("Cannot use name '" + name + "' as a prefix because it is " +
+ "already a " + fieldDescription.getType());
+ QueryProfileFieldType fieldType = (QueryProfileFieldType) fieldDescription.getType();
+ QueryProfileType type = fieldType.getQueryProfileType();
+ if (type == null) { // an as-yet untyped reference; add type
+ type = new QueryProfileType(name);
+ registry.register(type.getId(), type);
+ fields.put(name, fieldDescription.withType(new QueryProfileFieldType(type)));
+ }
+ return type;
+ }
+ else {
+ QueryProfileType type = new QueryProfileType(name);
+ registry.register(type.getId(), type);
+ fields.put(name, new FieldDescription(name, new QueryProfileFieldType(type)));
+ return type;
+ }
+ }
+
+ private void addAlias(String alias,String field) {
+ ensureNotFrozen();
+ if (aliases==null)
+ aliases=new HashMap<>();
+ aliases.put(toLowerCase(alias),field);
+ }
+
+ /** Returns all the fields of this profile type and all types it inherits as a read-only map */
+ public Map<String,FieldDescription> fields() {
+ if (isFrozen()) return fields;
+ if (inherited().size()==0) return Collections.unmodifiableMap(fields);
+
+ // Collapse inherited
+ Map<String,FieldDescription> allFields=new HashMap<>(fields);
+ for (QueryProfileType inheritedType : inherited)
+ allFields.putAll(inheritedType.fields());
+ return Collections.unmodifiableMap(allFields);
+ }
+
+ /**
+ * Returns the alias to field mapping of this type as a read-only map. This is never null.
+ * Note that all keys are lower-cased because aliases are case-insensitive
+ */
+ public Map<String,String> aliases() {
+ if (isFrozen()) return aliases;
+ if (aliases == null) return Collections.emptyMap();
+ return Collections.unmodifiableMap(aliases);
+ }
+
+ /** Returns the field name of an alias or field name */
+ public String unalias(String aliasOrField) {
+ if (aliases==null || aliases.isEmpty()) return aliasOrField;
+ String field=aliases.get(toLowerCase(aliasOrField));
+ if (field!=null) return field;
+ return aliasOrField;
+ }
+
+ @Override
+ public int hashCode() {
+ return getId().hashCode();
+ }
+
+ /** Two types are equal if they have the same id */
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if ( ! (o instanceof QueryProfileType)) return false;
+ QueryProfileType other = (QueryProfileType)o;
+ return other.getId().equals(this.getId());
+ }
+
+ public String toString() {
+ return "query profile type '" + getId() + "'";
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileTypeRegistry.java b/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileTypeRegistry.java
new file mode 100644
index 00000000000..3f64caa7ab1
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/types/QueryProfileTypeRegistry.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.types;
+
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.search.Query;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+
+/**
+ * A registry of query profile types
+ *
+ * @author bratseth
+ */
+public class QueryProfileTypeRegistry extends ComponentRegistry<QueryProfileType> {
+
+ public QueryProfileTypeRegistry() {
+ Query.addNativeQueryProfileTypesTo(this);
+ }
+
+ /** Register this type by its id */
+ public void register(QueryProfileType type) {
+ super.register(type.getId(), type);
+ }
+
+ @Override
+ public void freeze() {
+ if (isFrozen()) return;
+ for (QueryProfileType queryProfileType : allComponents())
+ queryProfileType.freeze();
+ }
+
+ public static QueryProfileTypeRegistry emptyFrozen() {
+ QueryProfileTypeRegistry registry = new QueryProfileTypeRegistry();
+ registry.freeze();
+ return registry;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/types/TensorFieldType.java b/container-search/src/main/java/com/yahoo/search/query/profile/types/TensorFieldType.java
new file mode 100644
index 00000000000..747cf73acb3
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/types/TensorFieldType.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.profile.types;
+
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.tensor.MapTensor;
+import com.yahoo.tensor.Tensor;
+import com.yahoo.tensor.TensorType;
+
+import java.util.Optional;
+
+/**
+ * A tensor field type in a query profile
+ *
+ * @author bratseth
+ */
+public class TensorFieldType extends FieldType {
+
+ private final Optional<TensorType> type;
+
+ /** Creates a tensor field type with optional information about the kind of tensor this will hold */
+ public TensorFieldType(Optional<TensorType> type) {
+ this.type = type;
+ }
+
+ /** Returns information about the type of tensor this will hold, or empty to allow any kind of tensor */
+ public Optional<TensorType> type() { return type; }
+
+ @Override
+ public Class getValueClass() { return Tensor.class; }
+
+ @Override
+ public String stringValue() { return "tensor"; }
+
+ @Override
+ public String toString() { return "field type " + stringValue(); }
+
+ @Override
+ public String toInstanceDescription() { return "a tensor"; }
+
+ @Override
+ public Object convertFrom(Object o, QueryProfileRegistry registry) {
+ if (o instanceof Tensor) return o;
+ if (o instanceof String) return MapTensor.from((String)o);
+ return null;
+ }
+
+ @Override
+ public Object convertFrom(Object o, CompiledQueryProfileRegistry registry) {
+ return convertFrom(o, (QueryProfileRegistry)null);
+ }
+
+ public static TensorFieldType fromTypeString(String s) {
+ if (s.equals("tensor")) return genericTensorType;
+ return new TensorFieldType(Optional.of(TensorType.fromSpec(s)));
+ }
+
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/types/package-info.java b/container-search/src/main/java/com/yahoo/search/query/profile/types/package-info.java
new file mode 100644
index 00000000000..1f9fa7a1fb4
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/types/package-info.java
@@ -0,0 +1,11 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * Query profile types defines the set of fields a query profile may, can or must have. Query profile
+ * types may be inherited in a type hierarchy.
+ */
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query.profile.types;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/properties/DefaultProperties.java b/container-search/src/main/java/com/yahoo/search/query/properties/DefaultProperties.java
new file mode 100644
index 00000000000..01c861b879e
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/properties/DefaultProperties.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.properties;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.Properties;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.Map;
+
+/**
+ * Default values for properties that are meant to be customized in query profiles.
+ * @author tonytv
+ */
+public final class DefaultProperties extends Properties {
+ public static final CompoundName MAX_OFFSET = new CompoundName("maxOffset");
+ public static final CompoundName MAX_HITS = new CompoundName("maxHits");
+
+
+ public static final QueryProfileType argumentType = new QueryProfileType("DefaultProperties");
+ static {
+ argumentType.setBuiltin(true);
+
+ argumentType.addField(new FieldDescription(MAX_OFFSET.toString(), "integer"));
+ argumentType.addField(new FieldDescription(MAX_HITS.toString(), "integer"));
+
+ argumentType.freeze();
+ }
+
+ @Override
+ public Object get(CompoundName name, Map<String, String> context, com.yahoo.processing.request.Properties substitution) {
+ if (MAX_OFFSET.equals(name)) {
+ return 1000;
+ } else if (MAX_HITS.equals(name)) {
+ return 400;
+ } else {
+ return super.get(name, context, substitution);
+ }
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/properties/PropertyAliases.java b/container-search/src/main/java/com/yahoo/search/query/properties/PropertyAliases.java
new file mode 100644
index 00000000000..cc2c08c5504
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/properties/PropertyAliases.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.properties;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.Properties;
+
+import java.util.Map;
+
+/**
+ * A properties implementation which translates the incoming name to its standard name
+ * if it is a registered alias.
+ * <p>
+ * Aliases are case insensitive. One standard name may have multiple aliases.
+ * <p>
+ * This is multithread safe or not depending on the status of the passed map of aliases.
+ * Cloning will not deep copy the set of aliases.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class PropertyAliases extends Properties {
+
+ /** A map from aliases to standard names */
+ private final Map<String,CompoundName> aliases;
+
+ /**
+ * Creates an instance with a set of aliases. The given aliases will be used directly by this class.
+ * To make this class immutable and thread safe, relinquish ownership of the parameter map.
+ */
+ public PropertyAliases(Map<String,CompoundName> aliases) {
+ this.aliases=aliases;
+ }
+
+ /**
+ * Returns the standard name for an alias, or the given name if it is not a registered alias
+ *
+ * @param nameOrAlias the name to check if is an alias
+ * @return the real name if an alias or the input name itself
+ */
+ protected CompoundName unalias(CompoundName nameOrAlias) {
+ CompoundName properName = aliases.get(nameOrAlias.getLowerCasedName());
+ return (properName != null) ? properName : nameOrAlias;
+ }
+
+ public @Override Map<String, Object> listProperties(CompoundName property,Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ return super.listProperties(unalias(property),context,substitution);
+ }
+
+ public @Override Object get(CompoundName name,Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ return super.get(unalias(name),context,substitution);
+ }
+
+ public @Override void set(CompoundName name,Object value,Map<String,String> context) {
+ super.set(unalias(name),value,context);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/properties/PropertyMap.java b/container-search/src/main/java/com/yahoo/search/query/properties/PropertyMap.java
new file mode 100644
index 00000000000..820c4fc8ea3
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/properties/PropertyMap.java
@@ -0,0 +1,132 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.properties;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.Properties;
+import com.yahoo.search.result.Hit;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * A Map backing of Properties.
+ * <p>
+ * When this is cloned it will deep copy not only the model object map, but also each
+ * clonable member inside the map.
+ * <p>
+ * Subclassing is supported, a hook can be implemented to provide conditional inclusion in the map.
+ * By default - all properties are accepted, so set is never propagated.
+ * <p>
+ * This class is not multithread safe.
+ *
+ * @author bratseth
+ */
+public class PropertyMap extends Properties {
+
+ private static Logger log=Logger.getLogger(PropertyMap.class.getName());
+
+ /** The properties of this */
+ private Map<CompoundName, Object> properties = new LinkedHashMap<>();
+
+ public void set(CompoundName name, Object value, Map<String,String> context) {
+ if (shouldSet(name, value))
+ properties.put(name, value);
+ else
+ super.set(name, value, context);
+ }
+
+ /**
+ * Return true if this value should be set in this map, false if the set should be propagated instead
+ * This default implementation always returns true.
+ */
+ protected boolean shouldSet(CompoundName name,Object value) { return true; }
+
+ public @Override Object get(CompoundName name, Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ if ( ! properties.containsKey(name)) return super.get(name,context,substitution);
+ return properties.get(name);
+ }
+
+ /**
+ * Returns a direct reference to the map containing the properties set in this instance.
+ */
+ public Map<CompoundName, Object> propertyMap() {
+ return properties;
+ }
+
+ public @Override PropertyMap clone() {
+ PropertyMap clone = (PropertyMap)super.clone();
+ clone.properties = new HashMap<>();
+ for (Map.Entry<CompoundName, Object> entry : this.properties.entrySet()) {
+ Object cloneValue = clone(entry.getValue());
+ if (cloneValue == null)
+ cloneValue = entry.getValue(); // Shallow copy objects which does not support cloning
+ clone.properties.put(entry.getKey(), cloneValue);
+ }
+ return clone;
+ }
+
+ /** Clones this object if it is clonable, and the clone is public. Returns null if not */
+ public static Object clone(Object object) {
+ if (object==null) return null;
+ if (! ( object instanceof Cloneable) ) return null;
+ if (object instanceof Object[])
+ return arrayClone((Object[])object);
+ else
+ return objectClone(object);
+ }
+
+ private static Object arrayClone(Object[] object) {
+ Object[] arrayClone= Arrays.copyOf(object, object.length);
+ // deep clone
+ for (int i=0; i<arrayClone.length; i++) {
+ Object elementClone=clone(arrayClone[i]);
+ if (elementClone!=null)
+ arrayClone[i]=elementClone;
+ }
+ return arrayClone;
+ }
+
+ private static Object objectClone(Object object) {
+ if (object instanceof Hit) {
+ return ((Hit) object).clone();
+ } else if (object instanceof LinkedList) {
+ return ((LinkedList) object).clone();
+ }
+ try {
+ Method cloneMethod=object.getClass().getMethod("clone");
+ return cloneMethod.invoke(object);
+ }
+ catch (NoSuchMethodException e) {
+ log.warning("'" + object + "' is Cloneable, but has no clone method - will use the same instance in all requests");
+ return null;
+ }
+ catch (IllegalAccessException e) {
+ log.warning("'" + object + "' is Cloneable, but clone method cannot be accessed - will use the same instance in all requests");
+ return null;
+ }
+ catch (InvocationTargetException e) {
+ throw new RuntimeException("Exception cloning '" + object + "'",e);
+ }
+ }
+
+ @Override
+ public Map<String, Object> listProperties(CompoundName path, Map<String, String> context, com.yahoo.processing.request.Properties substitution) {
+ Map<String, Object> map = super.listProperties(path, context, substitution);
+
+ for (Map.Entry<CompoundName, Object> entry : properties.entrySet()) {
+ if ( ! entry.getKey().hasPrefix(path)) continue;
+ CompoundName propertyName = entry.getKey().rest(path.size());
+ if (propertyName.isEmpty()) continue;
+ map.put(propertyName.toString(), entry.getValue());
+ }
+ return map;
+ }
+
+}
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
new file mode 100644
index 00000000000..cd4e02dc768
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/properties/QueryProperties.java
@@ -0,0 +1,296 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.properties;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.Query;
+import com.yahoo.search.query.*;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.QueryProfileTypeRegistry;
+import com.yahoo.search.query.ranking.Diversity;
+import com.yahoo.search.query.ranking.MatchPhase;
+import com.yahoo.tensor.Tensor;
+
+import java.util.Map;
+
+/**
+ * Maps between the query model and text properties.
+ * This can be done simpler by using reflection but the performance penalty was not worth it,
+ * especially since we should be conservative in adding things to the query model.
+ *
+ * @author bratseth
+ */
+public class QueryProperties extends Properties {
+
+ private static final String MODEL_PREFIX = Model.MODEL + ".";
+ private static final String RANKING_PREFIX = Ranking.RANKING + ".";
+ private static final String PRESENTATION_PREFIX = Presentation.PRESENTATION + ".";
+
+ public static final CompoundName[] PER_SOURCE_QUERY_PROPERTIES = new CompoundName[] {
+ new CompoundName(MODEL_PREFIX + Model.QUERY_STRING),
+ new CompoundName(MODEL_PREFIX + Model.TYPE),
+ new CompoundName(MODEL_PREFIX + Model.FILTER),
+ new CompoundName(MODEL_PREFIX + Model.DEFAULT_INDEX),
+ new CompoundName(MODEL_PREFIX + Model.LANGUAGE),
+ new CompoundName(MODEL_PREFIX + Model.ENCODING),
+ new CompoundName(MODEL_PREFIX + Model.SOURCES),
+ new CompoundName(MODEL_PREFIX + Model.SEARCH_PATH),
+ new CompoundName(MODEL_PREFIX + Model.RESTRICT),
+ new CompoundName(RANKING_PREFIX + Ranking.LOCATION),
+ new CompoundName(RANKING_PREFIX + Ranking.PROFILE),
+ new CompoundName(RANKING_PREFIX + Ranking.SORTING),
+ new CompoundName(RANKING_PREFIX + Ranking.FRESHNESS),
+ new CompoundName(RANKING_PREFIX + Ranking.QUERYCACHE),
+ new CompoundName(RANKING_PREFIX + Ranking.LIST_FEATURES),
+ new CompoundName(PRESENTATION_PREFIX + Presentation.BOLDING),
+ new CompoundName(PRESENTATION_PREFIX + Presentation.SUMMARY),
+ new CompoundName(PRESENTATION_PREFIX + Presentation.REPORT_COVERAGE),
+ new CompoundName(PRESENTATION_PREFIX + Presentation.FORMAT),
+ new CompoundName(PRESENTATION_PREFIX + Presentation.SUMMARY_FIELDS),
+ Query.HITS,
+ Query.OFFSET,
+ Query.TRACE_LEVEL,
+ Query.TIMEOUT,
+ Query.NO_CACHE,
+ Query.GROUPING_SESSION_CACHE };
+
+ private Query query;
+ private final CompiledQueryProfileRegistry profileRegistry;
+
+ public QueryProperties(Query query, CompiledQueryProfileRegistry profileRegistry) {
+ this.query = query;
+ this.profileRegistry = profileRegistry;
+ }
+
+ public void setParentQuery(Query query) {
+ this.query=query;
+ super.setParentQuery(query);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public Object get(final CompoundName key, Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ if (key.size()==2 && key.first().equals(Model.MODEL)) {
+ if (key.last().equals(Model.QUERY_STRING)) return query.getModel().getQueryString();
+ if (key.last().equals(Model.TYPE)) return query.getModel().getType();
+ if (key.last().equals(Model.FILTER)) return query.getModel().getFilter();
+ if (key.last().equals(Model.DEFAULT_INDEX)) return query.getModel().getDefaultIndex();
+ if (key.last().equals(Model.LANGUAGE)) return query.getModel().getLanguage();
+ if (key.last().equals(Model.ENCODING)) return query.getModel().getEncoding();
+ if (key.last().equals(Model.SOURCES)) return query.getModel().getSources();
+ if (key.last().equals(Model.SEARCH_PATH)) return query.getModel().getSearchPath();
+ if (key.last().equals(Model.RESTRICT)) return query.getModel().getRestrict();
+ }
+ else if (key.first().equals(Ranking.RANKING)) {
+ if (key.size()==2) {
+ if (key.last().equals(Ranking.LOCATION)) return query.getRanking().getLocation();
+ if (key.last().equals(Ranking.PROFILE)) return query.getRanking().getProfile();
+ if (key.last().equals(Ranking.SORTING)) return query.getRanking().getSorting();
+ if (key.last().equals(Ranking.FRESHNESS)) return query.getRanking().getFreshness();
+ if (key.last().equals(Ranking.QUERYCACHE)) return query.getRanking().getQueryCache();
+ if (key.last().equals(Ranking.LIST_FEATURES)) return query.getRanking().getListFeatures();
+ }
+ else if (key.size()>=3 && key.get(1).equals(Ranking.MATCH_PHASE)) {
+ if (key.size() == 3) {
+ MatchPhase matchPhase = query.getRanking().getMatchPhase();
+ if (key.last().equals(MatchPhase.ATTRIBUTE)) return matchPhase.getAttribute();
+ if (key.last().equals(MatchPhase.ASCENDING)) return matchPhase.getAscending();
+ if (key.last().equals(MatchPhase.MAX_HITS)) return matchPhase.getMaxHits();
+ if (key.last().equals(MatchPhase.MAX_FILTER_COVERAGE)) return matchPhase.getMaxFilterCoverage();
+ } else if (key.size() >= 4 && key.get(2).equals(Ranking.DIVERSITY)) {
+ Diversity diversity = query.getRanking().getMatchPhase().getDiversity();
+ if (key.size() == 4) {
+ if (key.last().equals(Diversity.ATTRIBUTE)) return diversity.getAttribute();
+ if (key.last().equals(Diversity.MINGROUPS)) return diversity.getMinGroups();
+ } else if ((key.size() == 5) && key.get(3).equals(Diversity.CUTOFF)) {
+ if (key.last().equals(Diversity.FACTOR)) return diversity.getCutoffFactor();
+ if (key.last().equals(Diversity.STRATEGY)) return diversity.getCutoffStrategy();
+ }
+ }
+ }
+ else if (key.size()>2) {
+ // pass the portion after "ranking.features/properties" down
+ if (key.get(1).equals(Ranking.FEATURES)) return query.getRanking().getFeatures().getObject(key.rest().rest().toString());
+ if (key.get(1).equals(Ranking.PROPERTIES)) return query.getRanking().getProperties().get(key.rest().rest().toString());
+ }
+ }
+ 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();
+ if (key.last().equals(Presentation.REPORT_COVERAGE)) return query.getPresentation().getReportCoverage();
+ if (key.last().equals(Presentation.FORMAT)) return query.getPresentation().getFormat();
+ if (key.last().equals(Presentation.TIMING)) return query.getPresentation().getTiming();
+ if (key.last().equals(Presentation.SUMMARY_FIELDS)) return query.getPresentation().getSummaryFields();
+ }
+ else if (key.first().equals("rankfeature") || key.first().equals("featureoverride")) { // featureoverride is deprecated
+ return query.getRanking().getFeatures().getObject(key.rest().toString());
+ } else if (key.first().equals("rankproperty")) {
+ return query.getRanking().getProperties().get(key.rest().toString());
+ } else if (key.size()==1) {
+ if (key.equals(Query.HITS)) return query.getHits();
+ if (key.equals(Query.OFFSET)) return query.getOffset();
+ if (key.equals(Query.TRACE_LEVEL)) return query.getTraceLevel();
+ if (key.equals(Query.TIMEOUT)) return query.getTimeout();
+ if (key.equals(Query.NO_CACHE)) return query.getNoCache();
+ if (key.equals(Query.GROUPING_SESSION_CACHE)) return query.getGroupingSessionCache();
+ if (key.toString().equals(Model.MODEL)) return query.getModel();
+ if (key.toString().equals(Ranking.RANKING)) return query.getRanking();
+ if (key.toString().equals(Presentation.PRESENTATION)) return query.getPresentation();
+ }
+ return super.get(key,context,substitution);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void set(final CompoundName key,Object value,Map<String,String> context) {
+ // Note: The defaults here are never used
+ try {
+ if (key.size()==2 && key.first().equals(Model.MODEL)) {
+ if (key.last().equals(Model.QUERY_STRING))
+ query.getModel().setQueryString(asString(value, ""));
+ else if (key.last().equals(Model.TYPE))
+ query.getModel().setType(asString(value, "ANY"));
+ else if (key.last().equals(Model.FILTER))
+ query.getModel().setFilter(asString(value, ""));
+ else if (key.last().equals(Model.DEFAULT_INDEX))
+ query.getModel().setDefaultIndex(asString(value, ""));
+ else if (key.last().equals(Model.LANGUAGE))
+ query.getModel().setLanguage(asString(value, ""));
+ else if (key.last().equals(Model.ENCODING))
+ query.getModel().setEncoding(asString(value,""));
+ else if (key.last().equals(Model.SEARCH_PATH))
+ query.getModel().setSearchPath(asString(value,""));
+ else if (key.last().equals(Model.SOURCES))
+ query.getModel().setSources(asString(value,""));
+ else if (key.last().equals(Model.RESTRICT))
+ query.getModel().setRestrict(asString(value,""));
+ else
+ throwIllegalParameter(key.last(),Model.MODEL);
+ }
+ else if (key.first().equals(Ranking.RANKING)) {
+ if (key.size()==2) {
+ if (key.last().equals(Ranking.LOCATION))
+ query.getRanking().setLocation(asString(value,""));
+ else if (key.last().equals(Ranking.PROFILE))
+ query.getRanking().setProfile(asString(value,""));
+ else if (key.last().equals(Ranking.SORTING))
+ query.getRanking().setSorting(asString(value,""));
+ else if (key.last().equals(Ranking.FRESHNESS))
+ query.getRanking().setFreshness(asString(value, ""));
+ else if (key.last().equals(Ranking.QUERYCACHE))
+ query.getRanking().setQueryCache(asBoolean(value, false));
+ else if (key.last().equals(Ranking.LIST_FEATURES))
+ query.getRanking().setListFeatures(asBoolean(value,false));
+ }
+ else if (key.size()>=3 && key.get(1).equals(Ranking.MATCH_PHASE)) {
+ if (key.size() == 3) {
+ MatchPhase matchPhase = query.getRanking().getMatchPhase();
+ if (key.last().equals(MatchPhase.ATTRIBUTE)) {
+ matchPhase.setAttribute(asString(value, null));
+ } else if (key.last().equals(MatchPhase.ASCENDING)) {
+ matchPhase.setAscending(asBoolean(value, false));
+ } else if (key.last().equals(MatchPhase.MAX_HITS)) {
+ matchPhase.setMaxHits(asLong(value, null));
+ } else if (key.last().equals(MatchPhase.MAX_FILTER_COVERAGE)) {
+ matchPhase.setMaxFilterCoverage(asDouble(value, 1.0));
+ }
+ } else if (key.size() > 3 && key.get(2).equals(Ranking.DIVERSITY)) {
+ Diversity diversity = query.getRanking().getMatchPhase().getDiversity();
+ if (key.last().equals(Diversity.ATTRIBUTE)) {
+ diversity.setAttribute(asString(value, null));
+ } else if (key.last().equals(Diversity.MINGROUPS)) {
+ diversity.setMinGroups(asLong(value, null));
+ } else if ((key.size() > 4) && key.get(3).equals(Diversity.CUTOFF)) {
+ if (key.last().equals(Diversity.FACTOR)) {
+ diversity.setCutoffFactor(asDouble(value, 10.0));
+ } else if (key.last().equals(Diversity.STRATEGY)) {
+ diversity.setCutoffStrategy(asString(value, "loose"));
+ }
+ }
+ }
+ }
+ else if (key.size()>2) {
+ String restKey = key.rest().rest().toString();
+ if (key.get(1).equals(Ranking.FEATURES))
+ setRankingFeature(query, restKey, toSpecifiedType(restKey, value, profileRegistry.getTypeRegistry().getComponent("features")));
+ else if (key.get(1).equals(Ranking.PROPERTIES))
+ query.getRanking().getProperties().put(restKey, toSpecifiedType(restKey, value, profileRegistry.getTypeRegistry().getComponent("properties")));
+ else
+ throwIllegalParameter(key.rest().toString(),Ranking.RANKING);
+ }
+ }
+ else if (key.size()==2 && key.first().equals(Presentation.PRESENTATION)) {
+ if (key.last().equals(Presentation.BOLDING))
+ query.getPresentation().setBolding(asBoolean(value, true));
+ else if (key.last().equals(Presentation.SUMMARY))
+ query.getPresentation().setSummary(asString(value, ""));
+ else if (key.last().equals(Presentation.REPORT_COVERAGE))
+ query.getPresentation().setReportCoverage(asBoolean(value,true));
+ else if (key.last().equals(Presentation.FORMAT))
+ query.getPresentation().setFormat(asString(value,""));
+ else if (key.last().equals(Presentation.TIMING))
+ query.getPresentation().setTiming(asBoolean(value, true));
+ else if (key.last().equals(Presentation.SUMMARY_FIELDS))
+ query.getPresentation().setSummaryFields(asString(value,""));
+ else
+ throwIllegalParameter(key.last(), Presentation.PRESENTATION);
+ }
+ 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")) {
+ query.getRanking().getProperties().put(key.rest().toString(), toSpecifiedType(key.rest().toString(), value, profileRegistry.getTypeRegistry().getComponent("properties")));
+ } else if (key.size()==1) {
+ if (key.equals(Query.HITS))
+ query.setHits(asInteger(value,10));
+ else if (key.equals(Query.OFFSET))
+ query.setOffset(asInteger(value,0));
+ else if (key.equals(Query.TRACE_LEVEL))
+ query.setTraceLevel(asInteger(value,0));
+ else if (key.equals(Query.TIMEOUT))
+ query.setTimeout(value.toString());
+ else if (key.equals(Query.NO_CACHE))
+ query.setNoCache(asBoolean(value,false));
+ else if (key.equals(Query.GROUPING_SESSION_CACHE))
+ query.setGroupingSessionCache(asBoolean(value, false));
+ else
+ super.set(key,value,context);
+ }
+ else
+ super.set(key,value,context);
+ }
+ catch (Exception e) { // Make sure error messages are informative. This should be moved out of this properties implementation
+ if (e.getMessage().startsWith("Could not set"))
+ throw e;
+ else
+ throw new IllegalArgumentException("Could not set '" + key + "' to '" + value + "'", e);
+ }
+ }
+
+ private void setRankingFeature(Query query, String key, Object value) {
+ if (value instanceof Tensor)
+ query.getRanking().getFeatures().put(key, (Tensor)value);
+ else
+ query.getRanking().getFeatures().put(key, asString(value, ""));
+ }
+
+ private Object toSpecifiedType(String key, Object value, QueryProfileType type) {
+ if ( ! ( value instanceof String)) return value; // already typed
+ if (type == null) return value; // no type info -> keep as string
+ FieldDescription field = type.getField(key);
+ if (field == null) return value; // ditto
+ return field.getType().convertFrom(value, profileRegistry);
+ }
+
+ private void throwIllegalParameter(String key,String namespace) {
+ throw new IllegalArgumentException("'" + key + "' is not a valid property in '" + namespace +
+ "'. See the search api for valid keys starting by '" + namespace + "'.");
+ }
+
+ @Override
+ public final Query getParentQuery() {
+ return query;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/properties/QueryPropertyAliases.java b/container-search/src/main/java/com/yahoo/search/query/properties/QueryPropertyAliases.java
new file mode 100644
index 00000000000..15544e8ff4c
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/properties/QueryPropertyAliases.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.properties;
+
+import com.yahoo.processing.request.CompoundName;
+
+import java.util.Map;
+
+/**
+ * Property aliases which contains some hardcoded unaliasing of prefixes of
+ * rankfeature and rankproperty maps.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class QueryPropertyAliases extends PropertyAliases {
+
+ /**
+ * Creates an instance with a set of aliases. The given aliases will be used directly by this class.
+ * To make this class immutable and thread safe, relinquish ownership of the parameter map.
+ */
+ public QueryPropertyAliases(Map<String,CompoundName> aliases) {
+ super(aliases);
+ }
+
+ @Override
+ protected CompoundName unalias(CompoundName nameOrAlias) {
+ if (nameOrAlias.first().equalsIgnoreCase("rankfeature"))
+ return nameOrAlias.rest().prepend("ranking", "features");
+ else if (nameOrAlias.first().equalsIgnoreCase("rankproperty"))
+ return nameOrAlias.rest().prepend("ranking", "properties");
+ return super.unalias(nameOrAlias);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/properties/RequestContextProperties.java b/container-search/src/main/java/com/yahoo/search/query/properties/RequestContextProperties.java
new file mode 100644
index 00000000000..c97f4daf6d4
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/properties/RequestContextProperties.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.properties;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.Properties;
+
+import java.util.Map;
+
+/**
+ * Turns get(name) into get(name,request) using the request given at construction time.
+ * This is used to allow the query's request to be supplied to all property requests
+ * without forcing users of the query.properties() to supply this explicitly.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class RequestContextProperties extends Properties {
+
+ private final Map<String,String> requestMap;
+
+ public RequestContextProperties(Map<String, String> properties) {
+ this.requestMap=properties;
+ }
+
+ @Override
+ public Object get(CompoundName name,Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ return super.get(name,context==null ? requestMap : context,substitution);
+ }
+
+ @Override
+ public void set(CompoundName name,Object value,Map<String,String> context) {
+ super.set(name,value,context==null ? requestMap : context);
+ }
+
+ @Override
+ public Map<String, Object> listProperties(CompoundName path,Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ return super.listProperties(path,context==null ? requestMap : context,substitution);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/properties/SubProperties.java b/container-search/src/main/java/com/yahoo/search/query/properties/SubProperties.java
new file mode 100644
index 00000000000..7f5c2ec2558
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/properties/SubProperties.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.properties;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.processing.request.Properties;
+
+import java.util.Map;
+
+/**
+ * A wrapper around a chain of property objects that prefixes all gets/sets with a given path
+ *
+ * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a>
+ */
+public class SubProperties extends com.yahoo.search.query.Properties {
+
+ final private CompoundName pathPrefix;
+ final private Properties parent;
+
+ public SubProperties(String pathPrefix, Properties properties) {
+ this(new CompoundName(pathPrefix),properties);
+ }
+
+ public SubProperties(CompoundName pathPrefix, Properties properties) {
+ this.pathPrefix = pathPrefix;
+ this.parent = properties;
+ }
+
+ @Override
+ public Object get(CompoundName key, Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ if(key == null) return null;
+ Object result = parent.get(getPathPrefix() + "." + key,context,substitution);
+ if(result == null) {
+ return super.get(key,context,substitution);
+ } else {
+ return result;
+ }
+ }
+
+ @Override
+ public void set(CompoundName key, Object obj, Map<String,String> context) {
+ if(key == null) return;
+ parent.set(getPathPrefix() + "." + key, obj, context);
+ }
+
+ @Override
+ public Map<String, Object> listProperties(CompoundName path,Map<String,String> context,
+ com.yahoo.processing.request.Properties substitution) {
+ Map<String, Object> map = super.listProperties(path,context,substitution);
+ if(path.isEmpty()) {
+ map.putAll(parent.listProperties(getPathPrefix(),context,substitution));
+ } else {
+ map.putAll(parent.listProperties(getPathPrefix() + "." + path,context,substitution));
+ }
+ return map;
+ }
+
+ public CompoundName getPathPrefixCompound() {
+ return pathPrefix;
+ }
+
+ /** Returns getPatchPrefixCompound.toString() */
+ public String getPathPrefix() {
+ return getPathPrefixCompound().toString();
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/properties/package-info.java b/container-search/src/main/java/com/yahoo/search/query/properties/package-info.java
new file mode 100644
index 00000000000..047a5494e53
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/properties/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query.properties;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/ranking/Diversity.java b/container-search/src/main/java/com/yahoo/search/query/ranking/Diversity.java
new file mode 100644
index 00000000000..b1865ad9d75
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/ranking/Diversity.java
@@ -0,0 +1,127 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.ranking;
+
+import com.yahoo.search.query.Ranking;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.Objects;
+
+/**
+ * <p>The diversity settings during match phase of a query.
+ * These are the same settings for diversity during match phase that can be set in a rank profile
+ * and is used for achieving guaranteed diversity at the cost of slightly higher cost as more hits must be
+ * considered compared to plain match-phase.</p>
+ *
+ * <p>You specify an additional attribute to be the diversifier and also min diversity needed.</p>
+ *
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public class Diversity implements Cloneable {
+
+ /** The type representing the property arguments consumed by this */
+ private static final QueryProfileType argumentType;
+
+ public static final String ATTRIBUTE = "attribute";
+ public static final String MINGROUPS = "minGroups";
+ public static final String CUTOFF = "cutoff";
+ public static final String FACTOR = "factor";
+ public static final String STRATEGY = "strategy";
+
+
+ static {
+ argumentType =new QueryProfileType(Ranking.DIVERSITY);
+ argumentType.setStrict(true);
+ argumentType.setBuiltin(true);
+ argumentType.addField(new FieldDescription(ATTRIBUTE, "string"));
+ argumentType.addField(new FieldDescription(MINGROUPS, "long"));
+ argumentType.freeze();
+ }
+ public static QueryProfileType getArgumentType() { return argumentType; }
+
+ public enum CutoffStrategy {loose, strict};
+ private String attribute = null;
+ private Long minGroups = null;
+ private Double cutoffFactor = null;
+ private CutoffStrategy cutoffStrategy= null;
+
+ /**
+ * Sets the attribute field which will be used to guarantee diversity.
+ * Set to null (default) to disable diversification.
+ * <p>
+ * If this is set, make sure to also set the maxGroups value.
+ * <p>
+ * This attribute must be singlevalue.
+ */
+ public void setAttribute(String attribute) { this.attribute = attribute; }
+
+ /** Returns the attribute to use for diversity, or null if none */
+ public String getAttribute() { return attribute; }
+
+ /**
+ * Sets the max hits to aim for producing in the match phase.
+ * This must be set if an attribute value is set.
+ * It should be set to a reasonable fraction of the total documents on each partition.
+ */
+ public void setMinGroups(long minGroups) { this.minGroups = minGroups; }
+
+ /** Returns the max hits to aim for producing in the match phase on each content node, or null if not set */
+ public Long getMinGroups() { return minGroups; }
+
+ public void setCutoffFactor(double cutoffFactor) { this.cutoffFactor = cutoffFactor; }
+ public Double getCutoffFactor() { return cutoffFactor; }
+ public void setCutoffStrategy(String cutoffStrategy) { this.cutoffStrategy = CutoffStrategy.valueOf(cutoffStrategy); }
+ public CutoffStrategy getCutoffStrategy() { return cutoffStrategy; }
+
+ /** Internal operation - DO NOT USE */
+ public void prepare(RankProperties rankProperties) {
+ if (attribute == null && minGroups == null) return;
+
+ if (attribute != null && !attribute.isEmpty()) {
+ rankProperties.put("vespa.matchphase.diversity.attribute", attribute);
+ }
+ if (minGroups != null) {
+ rankProperties.put("vespa.matchphase.diversity.mingroups", String.valueOf(minGroups));
+ }
+ if (cutoffFactor != null) {
+ rankProperties.put("vespa.matchphase.diversity.cutoff.factor", String.valueOf(cutoffFactor));
+ }
+ if (cutoffStrategy != null) {
+ rankProperties.put("vespa.matchphase.diversity.cutoff.strategy", cutoffStrategy);
+ }
+ }
+
+ @Override
+ public Diversity clone() {
+ try {
+ return (Diversity)super.clone();
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Won't happen", e);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+ if (attribute != null) hash += 11 * attribute.hashCode();
+ if (minGroups != null) hash += 13 * minGroups.hashCode();
+ if (cutoffFactor != null) hash += 17 * cutoffFactor.hashCode();
+ if (cutoffStrategy != null) hash += 19 * cutoffStrategy.hashCode();
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if ( ! (o instanceof Diversity)) return false;
+
+ Diversity other = (Diversity)o;
+ if ( ! Objects.equals(this.attribute, other.attribute)) return false;
+ if ( ! Objects.equals(this.minGroups, other.minGroups)) return false;
+ if ( ! Objects.equals(this.cutoffFactor, other.cutoffFactor)) return false;
+ if ( ! Objects.equals(this.cutoffStrategy, other.cutoffStrategy)) return false;
+ return true;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/ranking/MatchPhase.java b/container-search/src/main/java/com/yahoo/search/query/ranking/MatchPhase.java
new file mode 100644
index 00000000000..ba25ddbe7e6
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/ranking/MatchPhase.java
@@ -0,0 +1,153 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.ranking;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.query.Ranking;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+
+import java.util.Objects;
+
+/**
+ * The match phase ranking settings of this query.
+ * These are the same settings for match phase that can be set in a rank profile
+ * and is used for achieving reasonable query behavior given a query which causes too many matches:
+ * The engine will fall back to retrieving the best values according to the attribute given here
+ * during matching.
+ * <p>
+ * For this feature to work well, the order given by the attribute should correlate reasonably with the order
+ * of results produced if full evaluation is performed.
+ *
+ * @author bratseth
+ */
+public class MatchPhase implements Cloneable {
+
+ /** The type representing the property arguments consumed by this */
+ private static final QueryProfileType argumentType;
+
+ public static final String ATTRIBUTE = "attribute";
+ public static final String ASCENDING = "ascending";
+ public static final String MAX_HITS = "maxHits";
+ public static final String MAX_FILTER_COVERAGE = "maxFilterCoverage";
+
+ static {
+ argumentType =new QueryProfileType(Ranking.MATCH_PHASE);
+ argumentType.setStrict(true);
+ argumentType.setBuiltin(true);
+ argumentType.addField(new FieldDescription(ATTRIBUTE, "string"));
+ argumentType.addField(new FieldDescription(ASCENDING, "boolean"));
+ argumentType.addField(new FieldDescription(MAX_HITS, "long"));
+ argumentType.addField(new FieldDescription(MAX_FILTER_COVERAGE, "double"));
+ argumentType.addField(new FieldDescription(Ranking.DIVERSITY, "query-profile", "diversity"));
+ argumentType.freeze();
+ }
+ public static QueryProfileType getArgumentType() { return argumentType; }
+
+ private String attribute = null;
+ private boolean ascending = false;
+ private Long maxHits = null;
+ private Double maxFilterCoverage = 1.0;
+ private Diversity diversity = new Diversity();
+
+ /**
+ * Sets the attribute field which will be used to decide the best matches after it has been determined
+ * during matching that this query is going to cause too many matches.
+ * Set to null (default) to disable degradation.
+ * <p>
+ * If this is set, make sure to also set the maxHits value.
+ * Otherwise, the attribute setting is ignored.
+ * <p>
+ * This attribute should have fast-search turned on.
+ */
+ public void setAttribute(String attribute) { this.attribute = attribute; }
+
+ /** Returns the attribute to use for degradation, or null if none */
+ public String getAttribute() { return attribute; }
+
+ /**
+ * Set to true to sort by the attribute in ascending order when this is in use during the match phase,
+ * false (default) to use descending order.
+ */
+ public void setAscending(boolean ascending) { this.ascending = ascending; }
+
+ /**
+ * Returns the order to sort the attribute during the path phase when this takes effect.
+ */
+ public boolean getAscending() { return ascending; }
+
+ /**
+ * Sets the max hits to aim for producing in the match phase.
+ * This must be set if an attribute value is set.
+ * It should be set to a reasonable fraction of the total documents on each partition.
+ */
+ public void setMaxHits(long maxHits) { this.maxHits = maxHits; }
+
+ public void setMaxFilterCoverage(double maxFilterCoverage) {
+ if ((maxFilterCoverage < 0.0) || (maxFilterCoverage > 1.0)) {
+ throw new IllegalArgumentException("maxFilterCoverage must be in the range [0.0, 1.0]. It is " + maxFilterCoverage);
+ }
+ this.maxFilterCoverage = maxFilterCoverage;
+ }
+
+ /** Returns the max hits to aim for producing in the match phase on each content node, or null if not set */
+ public Long getMaxHits() { return maxHits; }
+
+ public Double getMaxFilterCoverage() { return maxFilterCoverage; }
+
+ public Diversity getDiversity() { return diversity; }
+
+ public void setDiversity(Diversity diversity) {
+ this.diversity = diversity;
+ }
+
+ /** Internal operation - DO NOT USE */
+ public void prepare(RankProperties rankProperties) {
+ if (attribute == null || maxHits == null) return;
+
+ rankProperties.put("vespa.matchphase.degradation.attribute", attribute);
+ if (ascending) { // backend default is descending
+ rankProperties.put("vespa.matchphase.degradation.ascendingorder", "true");
+ }
+ rankProperties.put("vespa.matchphase.degradation.maxhits", String.valueOf(maxHits));
+ rankProperties.put("vespa.matchphase.degradation.maxfiltercoverage", String.valueOf(maxFilterCoverage));
+ diversity.prepare(rankProperties);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+ hash += 13 * Boolean.hashCode(ascending);
+ hash += 19 * diversity.hashCode();
+ if (attribute != null) hash += 11 * attribute.hashCode();
+ if (maxHits != null) hash += 17 * maxHits.hashCode();
+ hash += 23 * maxFilterCoverage.hashCode();
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if ( ! (o instanceof MatchPhase)) return false;
+
+ MatchPhase other = (MatchPhase)o;
+ if ( this.ascending != other.ascending) return false;
+ if ( ! Objects.equals(this.attribute, other.attribute)) return false;
+ if ( ! Objects.equals(this.maxHits, other.maxHits)) return false;
+ if ( ! Objects.equals(this.diversity, other.diversity)) return false;
+ if ( ! Objects.equals(this.maxFilterCoverage, other.maxFilterCoverage)) return false;
+ return true;
+ }
+
+ @Override
+ public MatchPhase clone() {
+ try {
+ MatchPhase clone = (MatchPhase)super.clone();
+ clone.diversity = diversity.clone();
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Won't happen", e);
+ }
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/ranking/RankFeatures.java b/container-search/src/main/java/com/yahoo/search/query/ranking/RankFeatures.java
new file mode 100644
index 00000000000..1bcd548882c
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/ranking/RankFeatures.java
@@ -0,0 +1,130 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.ranking;
+
+import com.yahoo.fs4.MapEncoder;
+import com.yahoo.tensor.Tensor;
+import com.yahoo.text.JSON;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Contains the rank features of a query.
+ *
+ * @author bratseth
+ */
+public class RankFeatures implements Cloneable {
+
+ private final Map<String, Object> features;
+
+ public RankFeatures() {
+ this(new LinkedHashMap<>());
+ }
+
+ private RankFeatures(Map<String, Object> features) {
+ this.features = features;
+ }
+
+ /** Sets a rank feature by full name to a value */
+ public void put(String name, String value) {
+ features.put(name, value);
+ }
+
+ /** Sets a tensor rank feature */
+ public void put(String name, Tensor value) {
+ features.put(name, value);
+ }
+
+ /** Returns a rank feature as a string by full name or null if not set */
+ public String get(String name) {
+ Object value = features.get(name);
+ if (value == null) return null;
+ return value.toString();
+ }
+
+ /** Returns this value as whatever type it was stored as. Returns null if the value is not set. */
+ public Object getObject(String name) {
+ return features.get(name);
+ }
+
+ /**
+ * Returns a tensor rank feature, or empty if there is no value with this name.
+ *
+ * @throws IllegalArgumentException if the value is set but is not a tensor
+ */
+ public Optional<Tensor> getTensor(String name) {
+ Object feature = features.get(name);
+ if (feature == null) return Optional.empty();
+ if (feature instanceof Tensor) return Optional.of((Tensor)feature);
+ throw new IllegalArgumentException("Expected a tensor value of '" + name + "' but has " + feature);
+ }
+
+ /**
+ * Returns the map holding the features of this.
+ * This map may be modified to change the rank features of the query.
+ */
+ public Map<String, Object> asMap() { return features; }
+
+ public boolean isEmpty() {
+ return features.isEmpty();
+ }
+
+ /**
+ * Prepares this for encoding, not for external use. See encode on Query for details.
+ * <p>
+ * If the query feature is found in the rank feature set,
+ * remove all these entries and insert them into the rank property set instead.
+ * We want to hide from the user that the query feature value is sent down as a rank property
+ * and picked up by the query feature executor in the backend.
+ */
+ public void prepare(RankProperties rankProperties) {
+ if (isEmpty()) return;
+
+ List<String> featuresToRemove = new ArrayList<>();
+ List<String> propertiesToInsert = new ArrayList<>();
+ for (String key : features.keySet()) {
+ if (key.startsWith("query(") && key.endsWith(")")) {
+ featuresToRemove.add(key);
+ propertiesToInsert.add(key.substring("query(".length(), key.length() - 1));
+ } else if (key.startsWith("$")) {
+ featuresToRemove.add(key);
+ propertiesToInsert.add(key.substring(1));
+ }
+ }
+ for (int i = 0; i < featuresToRemove.size(); ++i) {
+ rankProperties.put(propertiesToInsert.get(i), features.remove(featuresToRemove.get(i)));
+ }
+ }
+
+ public int encode(ByteBuffer buffer) {
+ return MapEncoder.encodeMap("feature", features, buffer);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) return true;
+ if ( ! (other instanceof RankFeatures)) return false;
+
+ return this.features.equals(((RankFeatures)other).features);
+ }
+
+ @Override
+ public int hashCode() {
+ return features.hashCode();
+ }
+
+ @Override
+ public RankFeatures clone() {
+ return new RankFeatures(new LinkedHashMap<>(features));
+ }
+
+ @Override
+ public String toString() {
+ return JSON.encode(features);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/ranking/RankProperties.java b/container-search/src/main/java/com/yahoo/search/query/ranking/RankProperties.java
new file mode 100644
index 00000000000..eccb8bac2d4
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/ranking/RankProperties.java
@@ -0,0 +1,114 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.ranking;
+
+import com.yahoo.fs4.GetDocSumsPacket;
+import com.yahoo.fs4.MapEncoder;
+import com.yahoo.text.JSON;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Contains the properties properties of a query.
+ * This is a multimap: Multiple properties may be set for the same key.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class RankProperties implements Cloneable {
+
+ private Map<String, List<Object>> properties = new LinkedHashMap<>();
+
+ public RankProperties() {
+ this(new LinkedHashMap<>());
+ }
+
+ private RankProperties(Map<String, List<Object>> properties) {
+ this.properties = properties;
+ }
+
+ public void put(String name, String value) {
+ put(name, (Object)value);
+ }
+
+ /** Adds a property by full name to a value */
+ public void put(String name, Object value) {
+ List<Object> list = properties.get(name);
+ if (list == null) {
+ list = new ArrayList<>();
+ properties.put(name, list);
+ }
+ list.add(value);
+ }
+
+ /**
+ * Returns a read-only list of properties properties by full name.
+ * If this is not set, null is returned. If this is explicitly set to
+ * have no values, and empty list is returned.
+ */
+ public List<String> get(String name) {
+ List<Object> values = properties.get(name);
+ if (values == null) return null;
+ if (values.isEmpty()) return Collections.<String>emptyList();
+
+ // Compatibility ...
+ List<String> stringValues = new ArrayList<>(values.size());
+ for (Object value : values)
+ stringValues.add(value.toString());
+ return Collections.unmodifiableList(stringValues);
+ }
+
+ /** Removes all properties properties for a given name */
+ public void remove(String name) {
+ properties.remove(name);
+ }
+
+ public boolean isEmpty() {
+ return properties.isEmpty();
+ }
+
+ /** Returns a modifiable map of the properties of this */
+ public Map<String, List<Object>> asMap() { return properties; }
+
+ /** Encodes this in a binary internal representation and returns the number of property maps encoded (0 or 1) */
+ public int encode(ByteBuffer buffer, boolean encodeQueryData) {
+ if (encodeQueryData) {
+ return MapEncoder.encodeObjectMultiMap("rank", properties, buffer);
+ }
+ else {
+ List<Object> sessionId = properties.get(GetDocSumsPacket.sessionIdKey);
+ if (sessionId == null) return 0;
+ return MapEncoder.encodeSingleValue("rank", GetDocSumsPacket.sessionIdKey, sessionId.get(0), buffer);
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) return true;
+ if ( ! (other instanceof RankProperties)) return false;
+
+ return this.properties.equals(((RankProperties)other).properties);
+ }
+
+ @Override
+ public int hashCode() {
+ return properties.hashCode();
+ }
+
+ @Override
+ public RankProperties clone() {
+ Map<String, List<Object>> clone = new LinkedHashMap<>();
+ for (Map.Entry<String, List<Object>> entry : properties.entrySet())
+ clone.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+ return new RankProperties(clone);
+ }
+
+ @Override
+ public String toString() {
+ return JSON.encode(properties);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/ranking/package-info.java b/container-search/src/main/java/com/yahoo/search/query/ranking/package-info.java
new file mode 100644
index 00000000000..f254b327f96
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/ranking/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query.ranking;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/QueryRewriteSearcher.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/QueryRewriteSearcher.java
new file mode 100644
index 00000000000..bb76c1006f2
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/QueryRewriteSearcher.java
@@ -0,0 +1,423 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.rewrite;
+
+import com.google.inject.Inject;
+import com.yahoo.search.*;
+import com.yahoo.config.*;
+import com.yahoo.search.query.rewrite.RewritesConfig.FsaDict;
+import com.yahoo.search.searchchain.Execution;
+import com.yahoo.fsa.FSA;
+import com.yahoo.filedistribution.fileacquirer.FileAcquirer;
+import com.yahoo.component.ComponentId;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * <p>A template class for all rewriters</p>
+ *
+ * <p>All rewriters extending this class would need to implement the
+ * rewrite method which contains the rewriter's main logic,
+ * getSkipRewriterIfRewritten method which indicates whether this
+ * rewriter should be skipped if the query has been rewritten,
+ * getRewriterName method which returns the name of the rewriter used
+ * in query profile, configure method which contains any instance
+ * creation time configuration besides the default FSA loading, and
+ * getDefaultDicts method which return the pair of dictionary name
+ * and filename.</p>
+ *
+ * <p>Common rewrite features are in RewriterFeatures.java.
+ * Common rewriter utils are in RewriterUtils.java.</p>
+ *
+ * @author Karen Sze Wing Lee
+ */
+public abstract class QueryRewriteSearcher extends Searcher {
+
+ // Indicate whether rewriter is properly initiated
+ private boolean isOk = false;
+
+ protected final Logger logger = Logger.getLogger(QueryRewriteSearcher.class.getName());
+
+ // HashMap which store the rewriter dicts
+ // It has the following format:
+ // HashMap<String(e.g. dictionary name, etc),
+ // Object(e.g. FSA, etc)>>
+ protected HashMap<String, Object> rewriterDicts = new HashMap<>();
+
+ /**
+ * Constructor for this rewriter.
+ * Prepare the data needed by the rewriter
+ * @param id Component ID (see vespa's search container doc for more detail)
+ * @param fileAcquirer Required param for retrieving file type config
+ * (see vespa's search container doc for more detail)
+ * @param config Config from vespa-services.xml (see vespa's search
+ * container doc for more detail)
+ */
+ @Inject
+ protected QueryRewriteSearcher(ComponentId id,
+ FileAcquirer fileAcquirer,
+ RewritesConfig config) {
+ super(id);
+ RewriterUtils.log(logger, "In QueryRewriteSearcher(ComponentId id, " +
+ "FileAcquirer fileAcquirer, " +
+ "RewritesConfig config)");
+ isOk = loadFSADicts(fileAcquirer, config, null);
+ isOk = isOk && configure(fileAcquirer, config, null);
+ if(isOk) {
+ RewriterUtils.log(logger, "Rewriter is configured properly");
+ } else {
+ RewriterUtils.log(logger, "Rewriter is not configured properly");
+ }
+ }
+
+ /**
+ * Constructor for unit test.
+ * Prepare the data needed by the rewriter
+ * @param config Config from vespa-services.xml (see vespa's search
+ * container doc for more detail)
+ * @param fileList pairs of file name and file handler for unit tests
+ */
+ protected QueryRewriteSearcher(RewritesConfig config,
+ HashMap<String, File> fileList) {
+ RewriterUtils.log(logger, "In QueryRewriteSearcher(RewritesConfig config, " +
+ "HashMap<String, File> fileList)");
+ isOk = loadFSADicts(null, config, fileList);
+ isOk = isOk && configure(null, config, fileList);
+ if(isOk) {
+ RewriterUtils.log(logger, "Rewriter is configured properly");
+ } else {
+ RewriterUtils.log(logger, "Rewriter is not configured properly");
+ }
+ }
+
+ /**
+ * Empty constructor.
+ * Do nothing at instance creation time
+ */
+ protected QueryRewriteSearcher(ComponentId id) {
+ super(id);
+ RewriterUtils.log(logger, "In QueryRewriteSearcher(Component id)");
+ RewriterUtils.log(logger, "Configuring rewriter: " + getRewriterName());
+ isOk = true;
+ RewriterUtils.log(logger, "Rewriter is configured properly");
+ }
+
+ /**
+ * Empty constructor for unit test.
+ * Do nothing at instance creation time
+ */
+ protected QueryRewriteSearcher() {
+ RewriterUtils.log(logger, "In QueryRewriteSearcher()");
+ RewriterUtils.log(logger, "Configuring rewriter: " + getRewriterName());
+ isOk = true;
+ RewriterUtils.log(logger, "Rewriter is configured properly");
+ }
+
+ /**
+ * Load the dicts specified in vespa-services.xml
+ *
+ * @param fileAcquirer Required param for retrieving file type config
+ * (see vespa's search container doc for more detail)
+ * @param config Config from vespa-services.xml (see vespa's search
+ * container doc for more detail)
+ * @param fileList pairs of file name and file handler for unit tests
+ * @return boolean true if loaded successfully, false otherwise
+ */
+ private boolean loadFSADicts(FileAcquirer fileAcquirer,
+ RewritesConfig config,
+ HashMap<String, File> fileList)
+ throws RuntimeException {
+
+ // Check if getRewriterName method is properly implemented
+ String rewriterName = getRewriterName();
+ if(rewriterName==null) {
+ RewriterUtils.error(logger, "Rewriter required method is not properly implemented: ");
+ return false;
+ }
+
+ RewriterUtils.log(logger, "Configuring rewriter: " + rewriterName);
+
+ // Check if there's no config need to be loaded
+ if(config==null || (fileAcquirer==null && fileList==null)) {
+ RewriterUtils.log(logger, "No FSA dictionary file need to be loaded");
+ return true;
+ }
+
+ // Check if config contains the FSADict param
+ if(config.fsaDict()==null) {
+ RewriterUtils.error(logger, "FSADict is not properly set in config");
+ return false;
+ }
+
+ RewriterUtils.log(logger, "Loading rewriter dictionaries");
+
+ // Retrieve FSA names and paths
+ ListIterator<FsaDict> fsaList = config.fsaDict().listIterator();
+
+ // Load default dictionaries if no user dictionaries is configured
+ if(!fsaList.hasNext()) {
+ RewriterUtils.log(logger, "Loading default dictionaries");
+ HashMap<String, String> defaultFSAs = getDefaultFSAs();
+
+ if(defaultFSAs==null) {
+ RewriterUtils.log(logger, "No default FSA dictionary is configured");
+ return true;
+ }
+ Iterator<Map.Entry<String, String>> defaultFSAList = defaultFSAs.entrySet().iterator();
+ while(defaultFSAList.hasNext()) {
+ try{
+ Map.Entry<String, String> currFSA = defaultFSAList.next();
+ String fsaName = currFSA.getKey();
+ String fsaPath = currFSA.getValue();
+
+ RewriterUtils.log(logger,
+ "FSA file location for " + fsaName + ": " + fsaPath);
+
+ // Load FSA
+ FSA fsa = RewriterUtils.loadFSA(RewriterConstants.DEFAULT_DICT_DIR + fsaPath, null);
+
+ // Store FSA into dictionary map
+ rewriterDicts.put(fsaName, fsa);
+ } catch (IOException e) {
+ RewriterUtils.error(logger, "Error loading FSA dictionary: " +
+ e.getMessage());
+ return false;
+ }
+ }
+ } else {
+ // Load user configured dictionaries
+ while(fsaList.hasNext()) {
+ try{
+ FsaDict currFSA = fsaList.next();
+ // fsaName and fsaPath are not null
+ // or else vespa config server would not have been
+ // able to start up
+ String fsaName = currFSA.name();
+ FileReference fsaPath = currFSA.path();
+
+ RewriterUtils.log(logger,
+ "FSA file location for " + fsaName + ": " + fsaPath);
+
+ // Retrieve FSA File handler
+ File fsaFile = null;
+ if(fileAcquirer!=null) {
+ fsaFile = fileAcquirer.waitFor(fsaPath, 5, TimeUnit.MINUTES);
+ } else if(fileList!=null) {
+ fsaFile = fileList.get(fsaName);
+ }
+
+ if(fsaFile==null) {
+ RewriterUtils.error(logger, "Error loading FSA dictionary file handler");
+ return false;
+ }
+
+ // Load FSA
+ FSA fsa = RewriterUtils.loadFSA(fsaFile, null);
+
+ // Store FSA into dictionary map
+ rewriterDicts.put(fsaName, fsa);
+ } catch (InterruptedException e1) {
+ RewriterUtils.error(logger, "Error loading FSA dictionary file handler: " +
+ e1.getMessage());
+ return false;
+ } catch (IOException e2) {
+ RewriterUtils.error(logger, "Error loading FSA dictionary: " +
+ e2.getMessage());
+ return false;
+ }
+ }
+ }
+ RewriterUtils.log(logger, "Successfully loaded rewriter dictionaries");
+ return true;
+ }
+
+ /**
+ * Perform instance creation time configuration besides the
+ * default FSA loading
+ *
+ * @param fileAcquirer Required param for retrieving file type config
+ * (see vespa's search container doc for more detail)
+ * @param config Config from vespa-services.xml (see vespa's search
+ * container doc for more detail)
+ * @param fileList pairs of file name and file handler for unit tests
+ * @return boolean true if loaded successfully, false otherwise
+ */
+ public abstract boolean configure(FileAcquirer fileAcquirer,
+ RewritesConfig config,
+ HashMap<String, File> fileList)
+ throws RuntimeException;
+
+ /**
+ * Perform main rewrite logics for this searcher<br>
+ * - Skip to next rewriter if query is previously
+ * rewritten and getSkipRewriterIfRewritten() is
+ * true for this rewriter<br>
+ * - Execute rewriter's main rewrite logic<br>
+ * - Pass to the next rewriter the query to be used
+ * for dictionary retrieval<br>
+ */
+ public @Override Result search(Query query, Execution execution) {
+ RewriterUtils.log(logger, query, "Executing " + getRewriterName());
+
+ // Check if rewriter is properly initialized
+ if(!isOk) {
+ RewriterUtils.error(logger, query, "Rewriter is not properly initialized");
+ return execution.search(query);
+ }
+
+ RewriterUtils.log(logger, query, "Original query: " + query.toDetailString());
+
+ // Retrieve metadata passed by previous rewriter
+ HashMap<String, Object> rewriteMeta = RewriterUtils.getRewriteMeta(query);
+
+ // This key would be updated by each rewriter to specify
+ // the key to be used for dict retrieval in next
+ // rewriter downstream. This controls whether the
+ // next rewriter should use the rewritten query or the
+ // original query for dict retrieval. e.g. rewriters
+ // following misspell rewriter should use the rewritten
+ // query by misspell rewriter for dict retrieval
+ String prevDictKey = (String)rewriteMeta.get(RewriterConstants.DICT_KEY);
+
+ // Whether the query has been rewritten
+ Boolean prevRewritten = (Boolean)rewriteMeta.get(RewriterConstants.REWRITTEN);
+
+ // Check if rewriter should be skipped if the query
+ // has been rewritten
+ if(prevRewritten && getSkipRewriterIfRewritten()) {
+ RewriterUtils.log(logger, query, "Skipping rewriter since the " +
+ "query has been rewritten");
+ return execution.search(query);
+ }
+
+ // Store rewriter result
+ HashMap<String, Object> rewriterResult = null;
+ Query originalQueryObj = query.clone();
+
+ try {
+ // Execute rewriter's main rewrite logic
+ rewriterResult = rewrite(query, prevDictKey);
+
+ } catch (RuntimeException e) {
+ RewriterUtils.error(logger, originalQueryObj, "Error executing this rewriter, " +
+ "skipping to next rewriter: " + e.getMessage());
+ return execution.search(originalQueryObj);
+ }
+
+ // Check if rewriter result is set properly
+ if(rewriterResult==null) {
+ RewriterUtils.error(logger, originalQueryObj, "Rewriter result are not set properly, " +
+ "skipping to next rewriter");
+ return execution.search(originalQueryObj);
+ }
+
+ // Retrieve results from rewriter
+ Boolean rewritten = (Boolean)rewriterResult.get(RewriterConstants.REWRITTEN);
+ String dictKey = (String)rewriterResult.get(RewriterConstants.DICT_KEY);
+
+ if(rewritten==null || dictKey==null) {
+ RewriterUtils.error(logger, originalQueryObj, "Rewriter result are not set properly, " +
+ "skipping to next rewriter");
+ return execution.search(originalQueryObj);
+ }
+
+ // Retrieve results from rewriter
+ rewriteMeta.put(RewriterConstants.REWRITTEN, (rewritten || prevRewritten));
+ rewriteMeta.put(RewriterConstants.DICT_KEY, dictKey);
+
+ // Pass metadata to the next rewriter
+ RewriterUtils.setRewriteMeta(query, rewriteMeta);
+
+ RewriterUtils.log(logger, query, "Final query: " + query.toDetailString());
+
+ return execution.search(query);
+ }
+
+ /**
+ * Perform the main rewrite logic
+ *
+ * @param query Query object from searcher
+ * @param dictKey the key passed from previous rewriter
+ * to be treated as "original query from user"
+ * For example, if previous is misspell rewriter,
+ * it would pass the corrected query as the
+ * "original query from user". For other rewriters which
+ * add variants, abbr, etc to the query, the original
+ * query should be passed as a key. This rewriter could
+ * still choose to ignore this key. This key
+ * is not the rewritten query itself. For example,
+ * if original query is (willl smith) and the
+ * rewritten query is (willl smith) OR (will smith)
+ * the key to be passed could be (will smith)
+ * @return HashMap which contains the key value pairs:<br>
+ * - whether this query has been rewritten by this
+ * rewriter<br>
+ * key: rewritten<br>
+ * value: true or false<br>
+ * - the key to be treated as "original query from user" in next
+ * rewriter downstream, for example, misspell rewriter
+ * would pass the corrected query as the "original query from
+ * user" to the next rewriter. For other rewriters which
+ * add variants, abbr, etc to the query, the original
+ * query should be passed as a key. This key is not necessarily
+ * consumed by the next rewriter. The next rewriter
+ * can still choose to ignore this key.<br>
+ * key: newDictKey<br>
+ * value: new dict key<br>
+ */
+ protected abstract HashMap<String, Object> rewrite(Query query,
+ String dictKey) throws RuntimeException;
+
+ /**
+ * Check whether rewriter should be skipped if
+ * the query has been rewritten by other rewriter
+ *
+ * @return boolean Whether rewriter should be skipped
+ */
+ protected abstract boolean getSkipRewriterIfRewritten();
+
+ /**
+ * Retrieve rewriter name
+ * It should match the name used in query profile
+ *
+ * @return Name of the rewriter
+ */
+ public abstract String getRewriterName();
+
+ /**
+ * Get default FSA dictionary names
+ *
+ * @return Pair of FSA dictionary name and filename
+ */
+ public abstract HashMap<String, String> getDefaultFSAs();
+
+ /**
+ * Get config parameter value set in query profile
+ *
+ * @param query Query object from the searcher
+ * @param paramName parameter to be retrieved
+ * @return parameter value or null if not found
+ */
+ protected String getQPConfig(Query query,
+ String paramName) {
+ return RewriterUtils.getQPConfig(query, getRewriterName(), paramName);
+ }
+
+ /**
+ * Retrieve rewrite from FSA given the original query
+ *
+ * @param query Query object from searcher
+ * @param dictName FSA dictionary name
+ * @param key The original query used to retrieve rewrite
+ * from the dictionary
+ * @return String The retrieved rewrites, null if query
+ * doesn't exist
+ */
+ protected String getRewriteFromFSA(Query query,
+ String dictName,
+ String key) throws RuntimeException {
+ return RewriterUtils.getRewriteFromFSA(query, rewriterDicts, dictName, key);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterConstants.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterConstants.java
new file mode 100644
index 00000000000..45ce08de9d5
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterConstants.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.rewrite;
+
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.vespa.defaults.Defaults;
+
+/**
+ * Contains common constant strings used by rewriters
+ *
+ * @author Karen Sze Wing Lee
+ */
+public class RewriterConstants {
+
+ /** Config flag for addUnitToOriginalQuery */
+ public static final String ORIGINAL_AS_UNIT = "OriginalAsUnit";
+
+ /** Config flag for addUnitEquivToOriginalQuery */
+ public static final String ORIGINAL_AS_UNIT_EQUIV = "OriginalAsUnitEquiv";
+
+ /** Config flag for addRewritesAsEquiv(false) */
+ public static final String REWRITES_AS_EQUIV = "RewritesAsEquiv";
+
+ /** Config flag for addRewritesAsEquiv(true) */
+ public static final String REWRITES_AS_UNIT_EQUIV = "RewritesAsUnitEquiv";
+
+ /** Config flag for addExpansions */
+ public static final String PARTIAL_PHRASE_MATCH = "PartialPhraseMatch";
+
+ /** Config flag for max number of rewrites added per rewriter */
+ public static final String MAX_REWRITES = "MaxRewrites";
+
+ /** Config flag for considering QSS Rewrite in spell correction */
+ public static final String QSS_RW = "QSSRewrite";
+
+ /** Config flag for considering QSS Suggest in spell correction */
+ public static final String QSS_SUGG = "QSSSuggest";
+
+ /** Config flag for expansion index name */
+ public static final String EXPANSION_INDEX = "ExpansionIndex";
+
+ /** Name for market chain retrieval from user param */
+ public static final String REWRITER_CHAIN = "QRWChain";
+
+ /** Name for rewrite metadata retrieval from query properties */
+ public static final CompoundName REWRITE_META = new CompoundName("RewriteMeta");
+
+ /** Name for rewritten field retrieval from query properties */
+ public static final String REWRITTEN = "Rewritten";
+
+ /** Name for new dictionary key field retrieval from query properties */
+ public static final String DICT_KEY = "DictKey";
+
+ /** Default dictionaries dir */
+ public static final String DEFAULT_DICT_DIR = Defaults.getDefaults().vespaHome() + "share/qrw_data/";
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterFeatures.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterFeatures.java
new file mode 100644
index 00000000000..0a5110dbd7e
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterFeatures.java
@@ -0,0 +1,651 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.rewrite;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+import com.yahoo.prelude.IndexFacts;
+import com.yahoo.prelude.query.parser.CustomParser;
+import com.yahoo.search.*;
+import com.yahoo.search.query.*;
+import com.yahoo.prelude.query.*;
+import com.yahoo.prelude.querytransform.PhraseMatcher;
+import com.yahoo.prelude.querytransform.PhraseMatcher.Phrase;
+import com.yahoo.search.query.parser.ParserEnvironment;
+import com.yahoo.search.query.parser.ParserFactory;
+
+/**
+ * Contains commonly used rewriter features
+ *
+ * @author Karen Sze Wing Lee
+ */
+public class RewriterFeatures {
+
+ private static final Logger logger = Logger.getLogger(RewriterFeatures.class.getName());
+
+ /**
+ * <p>Add proximity boosting to original query by modifying
+ * the query tree directly</p>
+ * e.g. original Query Tree: (AND aa bb)<br>
+ * if keepOriginalQuery: true<br>
+ * new Query tree: (OR (AND aa bb) "aa bb")<br>
+ * if keepOriginalQuery: false<br>
+ * new Query Tree: "aa bb"<br><br>
+ *
+ * original Query Tree: (OR (AND aa bb) (AND cc dd))<br>
+ * boostingQuery: cc dd<br>
+ * if keepOriginalQuery: true<br>
+ * new Query Tree: (OR (AND aa bb) (AND cc dd) "cc dd")<br>
+ * if keepOriginalQuery: false<br>
+ * new Query Tree: (OR (AND aa bb) "cc dd") <br>
+ *
+ * @param query Query object from searcher
+ * @param boostingQuery query to be boosted
+ * @param keepOriginalQuery whether to keep original unboosted query as equiv
+ * @return Modified Query object, return original query object
+ * on error
+ */
+ public static Query addUnitToOriginalQuery(Query query, String boostingQuery,
+ boolean keepOriginalQuery)
+ throws RuntimeException {
+ RewriterUtils.log(logger, query, "Adding proximity boosting to [" + boostingQuery + "]");
+
+ Model queryModel = query.getModel();
+ QueryTree qTree = queryModel.getQueryTree();
+ Item oldRoot = qTree.getRoot();
+
+ if (oldRoot == null) {
+ RewriterUtils.error(logger, query, "Error retrieving query tree root");
+ throw new RuntimeException("Error retrieving query tree root");
+ }
+
+ // Convert original query to query tree item
+ Item origQueryItem = convertStringToQTree(query, boostingQuery);
+
+ // Boost proximity by phrasing the original query
+ // query tree structure: (AND aa bb)
+ if (oldRoot instanceof AndItem &&
+ oldRoot.equals(origQueryItem)) {
+ PhraseItem phrase = convertAndToPhrase((AndItem)oldRoot);
+
+ if(!keepOriginalQuery) {
+ qTree.setRoot(phrase);
+ } else {
+ OrItem newRoot = new OrItem();
+ newRoot.addItem(oldRoot);
+ newRoot.addItem(phrase);
+ qTree.setRoot(newRoot);
+ queryModel.setType(Query.Type.ADVANCED); //set type=adv
+ }
+ RewriterUtils.log(logger, query, "Added proximity boosting successfully");
+ return query;
+
+ // query tree structure: (OR (AND aa bb) (AND cc dd))
+ } else if (oldRoot instanceof OrItem &&
+ ((OrItem)oldRoot).getItemIndex(origQueryItem)!=-1 &&
+ origQueryItem instanceof AndItem) {
+
+ // Remove original unboosted query
+ if(!keepOriginalQuery)
+ ((OrItem)oldRoot).removeItem(origQueryItem);
+
+ // Check if the tree already contained the phrase item
+ PhraseItem pI = convertAndToPhrase((AndItem)origQueryItem);
+ if(((OrItem)oldRoot).getItemIndex(pI)==-1) {
+ ((OrItem)oldRoot).addItem(convertAndToPhrase((AndItem)origQueryItem));
+ RewriterUtils.log(logger, query, "Added proximity boosting successfully");
+ return query;
+ }
+ }
+ RewriterUtils.log(logger, query, "No proximity boosting added");
+ return query;
+ }
+
+ /**
+ * <p>Add query expansion to the query tree</p>
+ * e.g. origQuery: aa bb<br>
+ * matchingStr: aa bb<br>
+ * rewrite: cc dd, ee ff<br>
+ * if addUnitToRewrites: false<br>
+ * new query tree: (OR (AND aa bb) (AND cc dd) (AND ee ff))<br>
+ * if addUnitToRewrites: true<br>
+ * new query tree: (OR (AND aa bb) "cc dd" "ee ff") <br>
+ *
+ * @param query Query object from searcher
+ * @param matchingStr string used to retrieve the rewrite
+ * @param rewrites The rewrite string retrieved from
+ * dictionary
+ * @param addUnitToRewrites Whether to add unit to rewrites
+ * @param maxNumRewrites Max number of rewrites to be added,
+ * 0 if no limit
+ * @return Modified Query object, return original query object
+ * on error
+ */
+ public static Query addRewritesAsEquiv(Query query, String matchingStr,
+ String rewrites,
+ boolean addUnitToRewrites,
+ int maxNumRewrites) throws RuntimeException {
+ String normalizedQuery = RewriterUtils.getNormalizedOriginalQuery(query);
+
+ RewriterUtils.log(logger, query,
+ "Adding rewrites [" + rewrites +
+ "] to the query [" + normalizedQuery + "]");
+ if (rewrites.equalsIgnoreCase(normalizedQuery) || rewrites.equalsIgnoreCase("n/a")) {
+ RewriterUtils.log(logger, query, "No rewrite added");
+ return query;
+ }
+
+ Model queryModel = query.getModel();
+ QueryTree qTree = queryModel.getQueryTree();
+ Item oldRoot = qTree.getRoot();
+
+ if (oldRoot == null) {
+ RewriterUtils.error(logger, query, "Error retrieving query tree root");
+ throw new RuntimeException("Error retrieving query tree root");
+ }
+
+ StringTokenizer rewrite_list = new StringTokenizer(rewrites, "\t");
+ Item rI = null;
+
+ // Convert matching string to query tree item
+ Item matchingStrItem = convertStringToQTree(query, matchingStr);
+ PhraseItem matchingStrPhraseItem = null;
+ if(matchingStrItem instanceof AndItem) {
+ matchingStrPhraseItem = convertAndToPhrase(((AndItem)matchingStrItem));
+ }
+
+ // Add rewrites as OR item to the query tree
+ // Only should rewrite in this case:
+ // - origQuery: (OR (AND aa bb) (AND cc dd))
+ // - matchingStr: (AND aa bb)
+ // Or in this case:
+ // - origQuery: (AND aa bb)
+ // - matching Str: (AND aa bb)
+ // Should not rewrite in this case:
+ // - origQuery: (OR (AND cc (OR dd (AND aa bb)) ee)
+ // - matchingStr: (AND aa bb)
+ // - for this case, should use getNonOverlappingMatches instead
+ OrItem newRoot;
+ if(oldRoot instanceof OrItem) {
+ if(((OrItem)oldRoot).getItemIndex(matchingStrItem)==-1) {
+ RewriterUtils.log(logger, query, "Whole query matching is used, skipping rewrite");
+ return query;
+ }
+ newRoot = (OrItem)oldRoot;
+ } else if(oldRoot.equals(matchingStrItem) || oldRoot.equals(matchingStrPhraseItem)) {
+ newRoot = new OrItem();
+ newRoot.addItem(oldRoot);
+ } else {
+ RewriterUtils.log(logger, query, "Whole query matching is used, skipping rewrite");
+ return query;
+ }
+ int numRewrites = 0;
+ while(rewrite_list.hasMoreTokens() &&
+ (maxNumRewrites==0 || numRewrites < maxNumRewrites)) {
+ rI = convertStringToQTree(query, rewrite_list.nextToken());
+ if(addUnitToRewrites && rI instanceof AndItem) {
+ rI = convertAndToPhrase((AndItem)rI);
+ }
+ if(newRoot.getItemIndex(rI)==-1) {
+ newRoot.addItem(rI);
+ numRewrites++;
+ } else {
+ RewriterUtils.log(logger, query, "Rewrite already exist, skipping");
+ }
+ }
+ qTree.setRoot(newRoot);
+ queryModel.setType(Query.Type.ADVANCED); //set type=adv
+ RewriterUtils.log(logger, query, "Added rewrite successfully");
+
+ return query;
+ }
+
+ /**
+ * <p>Retrieve the longest, from left to right non overlapping full
+ * phrase substrings in query based on FSA dictionary</p>
+ *
+ * e.g. query: ((modern AND new AND york AND city AND travel) OR travel) AND
+ * ((sunny AND travel AND agency) OR nyc)<br>
+ * dictionary: <br>
+ * mny\tmodern new york<br>
+ * mo\tmodern<br>
+ * modern\tn/a<br>
+ * modern\tnew york\tn/a<br>
+ * new york\tn/a<br>
+ * new york city\tn/a<br>
+ * new york city travel\tn/a<br>
+ * new york company\tn/a<br>
+ * ny\tnew york<br>
+ * nyc\tnew york city\tnew york company<br>
+ * nyct\tnew york city travel<br>
+ * ta\ttravel agency<br>
+ * travel agency\tn/a<br>
+ * return: nyc
+ * @param phraseMatcher PhraseMatcher object loaded with FSA dict
+ * @param query Query object from the searcher
+ * @return Matching phrases
+ */
+ public static Set<PhraseMatcher.Phrase> getNonOverlappingFullPhraseMatches(PhraseMatcher phraseMatcher,
+ Query query)
+ throws RuntimeException {
+ RewriterUtils.log(logger, query, "Retrieving longest non-overlapping full phrase matches");
+ if(phraseMatcher==null)
+ return null;
+
+ Item root = query.getModel().getQueryTree().getRoot();
+ List<PhraseMatcher.Phrase> matches = phraseMatcher.matchPhrases(root);
+ if (matches==null || matches.isEmpty())
+ return null;
+
+ Set<PhraseMatcher.Phrase> resultMatches = new HashSet<>();
+ ListIterator<Phrase> matchesIter = matches.listIterator();
+
+ // Iterate through all matches
+ while(matchesIter.hasNext()) {
+ PhraseMatcher.Phrase phrase = matchesIter.next();
+ RewriterUtils.log(logger, query, "Working on phrase: " + phrase);
+ CompositeItem currOwner = phrase.getOwner();
+
+ // Check if this is full phrase
+ // If phrase is not an AND item, only keep those that are single word
+ // in order to eliminate cases such as (new RANK york) from being treated
+ // as match if only new york but not new or york is in the dictionary
+ if((currOwner!=null &&
+ ((phrase.isComplete() && currOwner instanceof AndItem) ||
+ (phrase.getLength()==1 && currOwner instanceof OrItem) ||
+ (phrase.getLength()==1 && currOwner instanceof RankItem && phrase.getStartIndex()==0))) ||
+ (currOwner==null && phrase.getLength()==1)) {
+ resultMatches.add(phrase);
+ RewriterUtils.log(logger, query, "Keeping phrase: " + phrase);
+ }
+ }
+
+ RewriterUtils.log(logger, query, "Successfully Retrieved longest non-overlapping full phrase matches");
+ return resultMatches;
+ }
+
+
+ /**
+ * <p>Retrieve the longest, from left to right non overlapping partial
+ * phrase substrings in query based on FSA dictionary</p>
+ *
+ * e.g. query: ((modern AND new AND york AND city AND travel) OR travel) AND
+ * ((sunny AND travel AND agency) OR nyc)<br>
+ * dictionary: <br>
+ * mny\tmodern new york<br>
+ * mo\tmodern<br>
+ * modern\tn/a<br>
+ * modern new york\tn/a<br>
+ * new york\tn/a<br>
+ * new york city\tn/a<br>
+ * new york city travel\tn/a<br>
+ * new york company\tn/a<br>
+ * ny\tnew york<br>
+ * nyc\tnew york city\tnew york company<br>
+ * nyct\tnew york city travel<br>
+ * ta\ttravel agency<br>
+ * travel agency\tn/a<br>
+ * return: <br>
+ * modern<br>
+ * new york city travel<br>
+ * travel agency<br>
+ * nyc<br>
+ * @param phraseMatcher PhraseMatcher object loaded with FSA dict
+ * @param query Query object from the searcher
+ * @return Matching phrases
+ */
+ public static Set<PhraseMatcher.Phrase> getNonOverlappingPartialPhraseMatches(PhraseMatcher phraseMatcher,
+ Query query)
+ throws RuntimeException {
+ RewriterUtils.log(logger, query, "Retrieving longest non-overlapping partial phrase matches");
+ if(phraseMatcher==null)
+ return null;
+
+ Item root = query.getModel().getQueryTree().getRoot();
+ List<PhraseMatcher.Phrase> matches = phraseMatcher.matchPhrases(root);
+ if (matches==null || matches.isEmpty())
+ return null;
+
+ Set<PhraseMatcher.Phrase> resultMatches = new HashSet<>();
+ ArrayList<PhraseMatcher.Phrase> phrasesInSubTree = new ArrayList<>();
+ CompositeItem prevOwner = null;
+ ListIterator<PhraseMatcher.Phrase> matchesIter = matches.listIterator();
+
+ // Iterate through all matches
+ while(matchesIter.hasNext()) {
+ PhraseMatcher.Phrase phrase = matchesIter.next();
+ RewriterUtils.log(logger, query, "Working on phrase: " + phrase);
+ CompositeItem currOwner = phrase.getOwner();
+
+ // Check if previous is AND item and this phrase is in a different item
+ // If so, work on the previous set to eliminate overlapping matches
+ if(!phrasesInSubTree.isEmpty() && currOwner!=null &&
+ prevOwner!=null && !currOwner.equals(prevOwner)) {
+ RewriterUtils.log(logger, query, "Previous phrase is in different AND item");
+ List<PhraseMatcher.Phrase> subTreeMatches
+ = getNonOverlappingMatchesInAndItem(phrasesInSubTree, query);
+ if(subTreeMatches==null) {
+ RewriterUtils.error(logger, query, "Error retrieving matches from subtree");
+ throw new RuntimeException("Error retrieving matches from subtree");
+ }
+ resultMatches.addAll(subTreeMatches);
+ phrasesInSubTree.clear();
+ }
+
+ // Check if this is an AND item
+ if(currOwner!=null && currOwner instanceof AndItem) {
+ phrasesInSubTree.add(phrase);
+ // If phrase is not an AND item, only keep those that are single word
+ // in order to eliminate cases such as (new RANK york) from being treated
+ // as match if only new york but not new or york is in the dictionary
+ } else if (phrase.getLength()==1 &&
+ !(currOwner!=null && currOwner instanceof RankItem && phrase.getStartIndex()!=0)) {
+ resultMatches.add(phrase);
+ }
+
+ prevOwner = currOwner;
+ }
+
+ // Check if last item is AND item
+ // If so, work on the previous set to elimate overlapping matches
+ if(!phrasesInSubTree.isEmpty()) {
+ RewriterUtils.log(logger, query, "Last phrase is in AND item");
+ List<PhraseMatcher.Phrase> subTreeMatches
+ = getNonOverlappingMatchesInAndItem(phrasesInSubTree, query);
+ if(subTreeMatches==null) {
+ RewriterUtils.error(logger, query, "Error retrieving matches from subtree");
+ throw new RuntimeException("Error retrieving matches from subtree");
+ }
+ resultMatches.addAll(subTreeMatches);
+ }
+ RewriterUtils.log(logger, query, "Successfully Retrieved longest non-overlapping partial phrase matches");
+ return resultMatches;
+ }
+
+ /**
+ * <p>Retrieve the longest, from left to right non overlapping substrings in
+ * AndItem based on FSA dictionary</p>
+ *
+ * e.g. subtree: (modern AND new AND york AND city AND travel)<br>
+ * dictionary:<br>
+ * mny\tmodern new york<br>
+ * mo\tmodern<br>
+ * modern\tn/a<br>
+ * modern new york\tn/a<br>
+ * new york\tn/a<br>
+ * new york city\tn/a<br>
+ * new york city travel\tn/a<br>
+ * new york company\tn/a<br>
+ * ny\tnew york<br>
+ * nyc\tnew york city\tnew york company<br>
+ * nyct\tnew york city travel<br>
+ * allMatches:<br>
+ * modern<br>
+ * modern new york<br>
+ * new york<br>
+ * new york city<br>
+ * new york city travel<br>
+ * return: <br>
+ * modern<br>
+ * new york city travel<br>
+ * @param allMatches All matches within the subtree
+ * @param query Query object from the searcher
+ * @return Matching phrases
+ */
+ public static List<PhraseMatcher.Phrase> getNonOverlappingMatchesInAndItem(
+ List<PhraseMatcher.Phrase> allMatches,
+ Query query)
+ throws RuntimeException {
+ RewriterUtils.log(logger, query, "Retrieving longest non-overlapping matches in subtree");
+
+ if (allMatches==null || allMatches.isEmpty())
+ return null;
+
+ if(allMatches.size()==1) {
+ RewriterUtils.log(logger, query, "Only one match in subtree");
+ return allMatches;
+ }
+
+ // Phrase are sorted based on length, if both have the
+ // same length, the lefter one ranks higher
+ RewriterUtils.log(logger, query, "Sorting the phrases");
+ PhraseLength phraseLength = new PhraseLength();
+ Collections.sort(allMatches, phraseLength);
+
+ // Create a bitset with length equal to the number of
+ // items in the subtree
+ int numWords = allMatches.get(0).getOwner().getItemCount();
+ BitSet matchPos = new BitSet(numWords);
+
+ // Removing matches that are overlapping with previously selected ones
+ RewriterUtils.log(logger, query, "Removing matches that are overlapping " +
+ "with previously selected ones");
+ ListIterator<Phrase> allMatchesIter = allMatches.listIterator();
+ while(allMatchesIter.hasNext()) {
+ PhraseMatcher.Phrase currMatch = allMatchesIter.next();
+ PhraseMatcher.Phrase.MatchIterator matchIter = currMatch.itemIterator();
+ if(matchIter.hasNext() && matchIter.next().isFilter()) {
+ RewriterUtils.log(logger, query, "Removing filter item" + currMatch);
+ allMatchesIter.remove();
+ continue;
+ }
+
+ BitSet currMatchPos = new BitSet(numWords);
+ currMatchPos.set(currMatch.getStartIndex(),
+ currMatch.getLength()+currMatch.getStartIndex());
+ if(currMatchPos.intersects(matchPos)) {
+ RewriterUtils.log(logger, query, "Removing " + currMatch);
+ allMatchesIter.remove();
+ } else {
+ RewriterUtils.log(logger, query, "Keeping " + currMatch);
+ matchPos.or(currMatchPos);
+ }
+ }
+ return allMatches;
+ }
+
+ /**
+ * <p>Add Expansions to the matching phrases</p>
+ *
+ * e.g. Query: nyc travel agency<br>
+ * matching phrase: nyc\tnew york city\tnew york company
+ * travel agency\tn/a<br>
+ * if expandIndex is not null and removeOriginal is true<br>
+ * New Query: ((new york city) OR ([expandIndex]:new york city)
+ * OR (new york company) OR
+ * ([expandIndex]:new york company)) AND
+ * ((travel agency) OR ([expandIndex]:travel agency))<br>
+ * if expandIndex is null and removeOriginal is true<br>
+ * New Query: ((new york city) OR (new york company)) AND
+ * travel agency<br>
+ * if expandIndex is null and removeOriginal is false<br>
+ * New Query: (nyc OR (new york city) OR (new york company)) AND
+ * travel agency<br>
+ *
+ * @param query Query object from searcher
+ * @param matches Set of longest non-overlapping matches
+ * @param expandIndex Name of expansion index or null if
+ * default index
+ * @param maxNumRewrites Max number of rewrites to be added,
+ * 0 if no limit
+ * @param removeOriginal Whether to remove the original matching phrase
+ * @param addUnitToRewrites Whether to add rewrite as phrase
+ */
+ public static Query addExpansions(Query query, Set<PhraseMatcher.Phrase> matches,
+ String expandIndex, int maxNumRewrites,
+ boolean removeOriginal, boolean addUnitToRewrites)
+ throws RuntimeException {
+
+ if(matches==null) {
+ RewriterUtils.log(logger, query, "No expansions to be added");
+ return query;
+ }
+
+ RewriterUtils.log(logger, query, "Adding expansions to matching phrases");
+ Model queryModel = query.getModel();
+ QueryTree qTree = queryModel.getQueryTree();
+ Iterator<Phrase> matchesIter = matches.iterator();
+ CompositeItem parent = null;
+
+ // Iterate through all matches
+ while(matchesIter.hasNext()) {
+ PhraseMatcher.Phrase match = matchesIter.next();
+ RewriterUtils.log(logger, query, "Working on phrase: " + match);
+
+ // Retrieve expansion phrases
+ String expansionStr = match.getData();
+ if(expansionStr.equalsIgnoreCase("n/a") && expandIndex==null) {
+ continue;
+ }
+ StringTokenizer expansions = new StringTokenizer(expansionStr,"\t");
+
+ // Create this structure for all expansions of this match
+ // (OR (AND expandsion1) indexName:expansion1
+ // (AND expansion2) indexName:expansion2..)
+ OrItem expansionGrp = new OrItem();
+ int numRewrites = 0;
+ String matchStr = convertMatchToString(match);
+ while(expansions.hasMoreTokens() &&
+ (maxNumRewrites==0 || numRewrites < maxNumRewrites)) {
+ String expansion = expansions.nextToken();
+ RewriterUtils.log(logger, query, "Working on expansion: " + expansion);
+ if(expansion.equalsIgnoreCase("n/a")) {
+ expansion = matchStr;
+ }
+ // (AND expansion) or "expansion"
+ Item expansionItem = convertStringToQTree(query, expansion);
+ if(addUnitToRewrites && expansionItem instanceof AndItem) {
+ expansionItem = convertAndToPhrase((AndItem)expansionItem);
+ }
+ expansionGrp.addItem(expansionItem);
+
+ if(expandIndex!=null) {
+ // indexName:expansion
+ WordItem expansionIndexItem = new WordItem(expansion, expandIndex);
+ expansionGrp.addItem(expansionIndexItem);
+ }
+ numRewrites++;
+ RewriterUtils.log(logger, query, "Adding expansion: " + expansion);
+ }
+
+ if(!removeOriginal) {
+ //(AND original)
+ Item matchItem = convertStringToQTree(query, matchStr);
+ if(expansionGrp.getItemIndex(matchItem)==-1) {
+ expansionGrp.addItem(matchItem);
+ }
+ }
+
+ parent = match.getOwner();
+ int matchIndex = match.getStartIndex();
+ if(parent!=null) {
+ // Remove matching phrase from original query
+ for(int i=0; i<match.getLength(); i++) {
+ parent.removeItem(matchIndex);
+ }
+ // Adding back expansions
+ parent.addItem(matchIndex, expansionGrp);
+ } else {
+ RewriterUtils.log(logger, query, "Single root item");
+ // If there's no parent, i.e. single root item
+ qTree.setRoot(expansionGrp);
+ break;
+ }
+ }
+
+ // Not root single item
+ if(parent!=null) {
+ // Cleaning up the query after rewrite to remove redundant tags
+ // e.g. (AND (OR (AND a b) c)) => (OR (AND a b) c)
+ String cleanupError = QueryCanonicalizer.canonicalize(qTree);
+ if(cleanupError!=null) {
+ RewriterUtils.error(logger, query, "Error canonicalizing query tree");
+ throw new RuntimeException("Error canonicalizing query tree");
+ }
+ }
+ queryModel.setType(Query.Type.ADVANCED); //set type=adv
+ RewriterUtils.log(logger, query, "Successfully added expansions to matching phrases");
+ return query;
+ }
+
+ /**
+ * Convert Match to String
+ *
+ * @param phrase Match from PhraseMatcher
+ * @return String format of the phrase
+ */
+ public static String convertMatchToString(PhraseMatcher.Phrase phrase) {
+ StringBuilder buffer = new StringBuilder();
+ for (Iterator<Item> i = phrase.itemIterator(); i.hasNext();) {
+ buffer.append(i.next().toString());
+ if (i.hasNext()) {
+ buffer.append(" ");
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Convert String to query tree
+ *
+ * @param stringToParse The string to be converted to a
+ * query tree
+ * @param query Query object from searcher
+ * @return Item The resulting query tree
+ */
+ static Item convertStringToQTree(Query query, String stringToParse) {
+ RewriterUtils.log(logger, query, "Converting string [" + stringToParse + "] to query tree");
+ if(stringToParse==null) {
+ return new NullItem();
+ }
+ Model model = query.getModel();
+ CustomParser parser = (CustomParser) ParserFactory.newInstance(model.getType(),
+ ParserEnvironment.fromExecutionContext(query.getModel().getExecution().context()));
+ IndexFacts indexFacts = new IndexFacts();
+ Item item = parser.parse(stringToParse, null, model.getParsingLanguage(),
+ indexFacts.newSession(model.getSources(), model.getRestrict()),
+ model.getDefaultIndex());
+ RewriterUtils.log(logger, query, "Converted string: [" + item.toString() + "]");
+ return item;
+ }
+
+ /**
+ * Convert AndItem to PhraseItem<br>
+ *
+ * e.g. (AND a b) to "a b"
+ * @param andItem query tree to be converted
+ * @return converted PhraseItem
+ */
+ private static PhraseItem convertAndToPhrase(AndItem andItem) {
+ PhraseItem result = new PhraseItem();
+ Iterator<Item> subItems = andItem.getItemIterator();
+ while(subItems.hasNext()) {
+ Item curr = (subItems.next());
+ if(curr instanceof IntItem) {
+ WordItem numItem = new WordItem(((IntItem)curr).stringValue());
+ result.addItem(numItem);
+ } else {
+ result.addItem(curr);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Class for comparing phrase.
+ * A phrase is larger if its length is longer.
+ * If both phrases are of the same length, the lefter one
+ * is considered larger
+ */
+ private static class PhraseLength implements Comparator<PhraseMatcher.Phrase> {
+ public int compare(PhraseMatcher.Phrase phrase1, PhraseMatcher.Phrase phrase2) {
+ if((phrase2.getLength()>phrase1.getLength()) ||
+ (phrase2.getLength()==phrase1.getLength() &&
+ phrase2.getStartIndex()<=phrase1.getStartIndex())) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterUtils.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterUtils.java
new file mode 100644
index 00000000000..26ead8de5e5
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/RewriterUtils.java
@@ -0,0 +1,334 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.rewrite;
+
+import com.yahoo.fsa.FSA;
+import com.yahoo.log.LogLevel;
+import com.yahoo.search.Query;
+import com.yahoo.search.intent.model.IntentModel;
+import com.yahoo.search.intent.model.InterpretationNode;
+import com.yahoo.text.interpretation.Annotations;
+import com.yahoo.text.interpretation.Modification;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.logging.Logger;
+
+import static com.yahoo.language.LinguisticsCase.toLowerCase;
+
+/**
+ * Contains common utilities used by rewriters
+ *
+ * @author Karen Sze Wing Lee
+ */
+public class RewriterUtils {
+
+ private static final Logger utilsLogger = Logger.getLogger(RewriterUtils.class.getName());
+
+ // Tracelevel for debug log of this rewriter
+ private static final int TRACELEVEL = 3;
+
+ /**
+ * Load FSA from file
+ *
+ * @param file FSA dictionary file object
+ * @param query Query object from the searcher, could be null if not available
+ * @return FSA The FSA object for the input file path
+ */
+ public static FSA loadFSA(File file, Query query) throws IOException {
+ log(utilsLogger, query, "Loading FSA file");
+ String filePath = null;
+
+ try {
+ filePath = file.getAbsolutePath();
+ } catch (SecurityException e1) {
+ error(utilsLogger, query, "No read access for the FSA file");
+ throw new IOException("No read access for the FSA file");
+ }
+
+ FSA fsa = loadFSA(filePath, query);
+
+ return fsa;
+ }
+
+ /**
+ * Load FSA from file
+ *
+ * @param filename FSA dictionary file path
+ * @param query Query object from the searcher, could be null if not available
+ * @return FSA The FSA object for the input file path
+ */
+ public static FSA loadFSA(String filename, Query query) throws IOException {
+ log(utilsLogger, query, "Loading FSA file from: " + filename);
+
+ if(!new File(filename).exists()) {
+ error(utilsLogger, query, "File does not exist : " + filename);
+ throw new IOException("File does not exist : " + filename);
+ }
+
+ FSA fsa;
+ try {
+ fsa = new FSA(filename);
+ } catch (RuntimeException e) {
+ error(utilsLogger, query, "Invalid FSA file");
+ throw new IOException("Invalid FSA file");
+ }
+
+ if (!fsa.isOk()) {
+ error(utilsLogger, query, "Unable to load FSA file from : " + filename);
+ throw new IOException("Not able to load FSA file from : " + filename);
+ }
+ log(utilsLogger, query, "Loaded FSA successfully from file : " + filename);
+ return fsa;
+ }
+
+ /**
+ * Retrieve rewrite from FSA given the original query
+ *
+ * @param query Query object from searcher
+ * @param dictName FSA dictionary name
+ * @param rewriterDicts list of rewriter dictionaries
+ * It has the following format:
+ * HashMap&lt;dictionary name, FSA&gt;
+ * @param key The original query used to retrieve rewrite
+ * from the dictionary
+ * @return String The retrieved rewrites, null if query
+ * doesn't exist
+ */
+ public static String getRewriteFromFSA(Query query,
+ HashMap<String, Object> rewriterDicts,
+ String dictName,
+ String key) throws RuntimeException {
+ if(rewriterDicts==null) {
+ error(utilsLogger, query, "HashMap containing rewriter dicts is null");
+ throw new RuntimeException("HashMap containing rewriter dicts is null");
+ }
+
+ FSA fsa = (FSA)rewriterDicts.get(dictName);
+
+ if(fsa==null) {
+ error(utilsLogger, query, "Error retrieving FSA dictionary: " + dictName);
+ throw new RuntimeException("Error retrieving FSA dictionary: " + dictName);
+ }
+
+ String result = null;
+ result = fsa.lookup(key);
+ log(utilsLogger, query, "Retrieved rewrite: " + result);
+
+ return result;
+ }
+
+ /**
+ * Get config parameter value set in query profile
+ *
+ * @param query Query object from the searcher
+ * @param rewriterName Name of the rewriter
+ * @param paramName parameter to be retrieved
+ * @return parameter value or null if not found
+ */
+ public static String getQPConfig(Query query,
+ String rewriterName,
+ String paramName) {
+ log(utilsLogger, query, "Retrieving config parameter value of: " +
+ rewriterName + "." + paramName);
+
+ return getUserParam(query, rewriterName + "." + paramName);
+ }
+
+ /**
+ * Get rewriter chain value
+ *
+ * @param query Query object from the searcher
+ * @return parameter value or null if not found
+ */
+ public static String getRewriterChain(Query query) {
+ log(utilsLogger, query, "Retrieving rewriter chain value: " +
+ RewriterConstants.REWRITER_CHAIN);
+
+ return getUserParam(query, RewriterConstants.REWRITER_CHAIN);
+ }
+
+ /**
+ * Get user param value
+ *
+ * @param query Query object from the searcher
+ * @param paramName parameter to be retrieved
+ *
+ * @return parameter value or null if not found
+ */
+ public static String getUserParam(Query query, String paramName) {
+ log(utilsLogger, query, "Retrieving user param value: " + paramName);
+
+ if(paramName==null) {
+ error(utilsLogger, query, "Parameter name is null");
+ return null;
+ }
+
+ String paramValue = null;
+ paramValue = query.properties().getString(paramName);
+ log(utilsLogger, query, "Param value retrieved is: " + paramValue);
+
+ return paramValue;
+ }
+
+ /**
+ * Retrieve metadata passed by previous rewriter
+ * from query properties
+ * Initialize values if this is the first rewriter
+ *
+ * @param query Query object from the searcher
+ * @return hashmap containing the metadata
+ */
+ public static HashMap<String, Object> getRewriteMeta(Query query) {
+ log(utilsLogger, query, "Retrieving metadata passed by previous rewriter");
+
+ @SuppressWarnings("unchecked")
+ HashMap<String, Object> rewriteMeta = (HashMap<String, Object>) query
+ .properties().get(RewriterConstants.REWRITE_META);
+
+ if(rewriteMeta==null) {
+ log(utilsLogger, query, "No metadata available from previous rewriter");
+ rewriteMeta = new HashMap<>();
+ rewriteMeta.put(RewriterConstants.REWRITTEN, false);
+ rewriteMeta.put(RewriterConstants.DICT_KEY, getNormalizedOriginalQuery(query));
+ } else {
+ if((Boolean)rewriteMeta.get(RewriterConstants.REWRITTEN)) {
+ log(utilsLogger, query, "Query has been rewritten by previous rewriters");
+ } else {
+ log(utilsLogger, query, "Query has not been rewritten by previous rewriters");
+ }
+ log(utilsLogger, query, "Dict key passed by previous rewriter: " +
+ rewriteMeta.get(RewriterConstants.DICT_KEY));
+ }
+
+ return rewriteMeta;
+ }
+
+ /**
+ * Pass metadata to the next rewriter through query properties
+ *
+ * @param query Query object from the searcher
+ * @param metadata HashMap containing the metadata
+ */
+ public static void setRewriteMeta(Query query, HashMap<String, Object> metadata) {
+ log(utilsLogger, query, "Passing metadata to the next rewriter");
+
+ query.properties().set(RewriterConstants.REWRITE_META, metadata);
+ log(utilsLogger, query, "Successfully passed metadata to the next rewriter");
+ }
+
+
+ /**
+ * Retrieve spell corrected query with highest score from QLAS
+ *
+ * @param query Query object from the searcher
+ * @param qss_rw Whether to consider qss_rw modification
+ * @param qss_sugg Whether ot consider qss_sugg modification
+ * @return Spell corrected query or null if not found
+ */
+ public static String getSpellCorrected(Query query,
+ boolean qss_rw,
+ boolean qss_sugg)
+ throws RuntimeException {
+ log(utilsLogger, query, "Retrieving spell corrected query");
+
+ // Retrieve Intent Model
+ IntentModel intentModel = IntentModel.getFrom(query);
+ if(intentModel==null) {
+ error(utilsLogger, query, "Unable to retrieve intent model");
+ throw new RuntimeException("Not able to retrieve intent model");
+ }
+
+ double max_score = 0;
+ String spellCorrected = null;
+
+ // Iterate through all interpretations to get a spell corrected
+ // query with highest score
+ for (InterpretationNode interpretationNode : intentModel.children()) {
+ Modification modification = interpretationNode.getInterpretation()
+ .getModification();
+ Annotations annotations = modification.getAnnotation();
+ Double score = annotations.getDouble("score");
+
+ // Check if it's higher than the max score
+ if(score!=null && score>max_score) {
+ Boolean isQSSRewrite = annotations.getBoolean("qss_rw");
+ Boolean isQSSSuggest = annotations.getBoolean("qss_sugg");
+
+ // Check if it's qss_rw or qss_sugg
+ if((qss_rw && isQSSRewrite!=null && isQSSRewrite) ||
+ (qss_sugg && isQSSSuggest!=null && isQSSSuggest)) {
+ max_score = score;
+ spellCorrected = modification.getText();
+ }
+ }
+ }
+
+ if(spellCorrected!=null) {
+ log(utilsLogger, query, "Successfully retrieved spell corrected query: " +
+ spellCorrected);
+ } else {
+ log(utilsLogger, query, "No spell corrected query is retrieved");
+ }
+
+ return spellCorrected;
+ }
+
+ /**
+ * Retrieve normalized original query from query object
+ *
+ * @param query Query object from searcher
+ * @return normalized query
+ */
+ public static String getNormalizedOriginalQuery(Query query) {
+ return toLowerCase(query.getModel().getQueryString()).trim();
+ }
+
+ /**
+ * Log message
+ *
+ * @param logger Logger used for this msg
+ * @param msg Log message
+ */
+ public static void log(Logger logger, String msg) {
+ logger.log(LogLevel.DEBUG, logger.getName() + ": " + msg);
+ }
+
+ /**
+ * Log message
+ *
+ * @param logger Logger used for this msg
+ * @param query Query object from searcher
+ * @param msg Log message
+ */
+ public static void log(Logger logger, Query query, String msg) {
+ if(query!=null) {
+ query.trace(logger.getName() + ": " + msg, true, TRACELEVEL);
+ }
+ logger.log(LogLevel.DEBUG, logger.getName() + ": " + msg);
+ }
+
+ /**
+ * Print error message
+ *
+ * @param logger Logger used for this msg
+ * @param msg Error message
+ */
+ public static void error(Logger logger, String msg) {
+ logger.severe(logger.getName() + ": " + msg);
+ }
+
+ /**
+ * Print error message
+ *
+ * @param logger Logger used for this msg
+ * @param query Query object from searcher
+ * @param msg Error message
+ */
+ public static void error(Logger logger, Query query, String msg) {
+ if(query!=null) {
+ query.trace(logger.getName() + ": " + msg, true, TRACELEVEL);
+ }
+ logger.severe(logger.getName() + ": " + msg);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/SearchChainDispatcherSearcher.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/SearchChainDispatcherSearcher.java
new file mode 100644
index 00000000000..589696c4e77
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/SearchChainDispatcherSearcher.java
@@ -0,0 +1,74 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.rewrite;
+
+import com.yahoo.component.chain.Chain;
+import com.yahoo.component.chain.dependencies.After;
+import com.yahoo.component.chain.dependencies.Provides;
+import com.yahoo.search.*;
+import com.yahoo.search.searchchain.Execution;
+import com.yahoo.component.ComponentId;
+
+import java.util.logging.Logger;
+
+/**
+ * Execute rewriter search chain specified by the user.
+ * It's inteneded to be used for executing rewriter search chains
+ * for different markets.
+ *
+ * @author Karen Sze Wing Lee
+ */
+@Provides("SearchChainDispatcher")
+@After("QLAS")
+public class SearchChainDispatcherSearcher extends Searcher {
+
+ protected final Logger logger = Logger.getLogger(SearchChainDispatcherSearcher.class.getName());
+
+ /**
+ * Constructor for this searcher
+ * @param id Component ID (see vespa's search container doc for more detail)
+ */
+ public SearchChainDispatcherSearcher(ComponentId id) {
+ super(id);
+ }
+
+ /**
+ * Constructor for unit test
+ */
+ public SearchChainDispatcherSearcher() {
+ }
+
+ /**
+ * Execute another search chain specified by the user<br>
+ * - Retrieve search chain specified by the user through
+ * param<br>
+ * - Execute specified search chain if exist
+ */
+ public @Override Result search(Query query, Execution execution) {
+ RewriterUtils.log(logger, query, "Entering SearchChainDispatcherSearcher");
+
+ // Retrieve search chain specified by user through REWRITER_CHAIN
+ String rewriterChain = RewriterUtils.getRewriterChain(query);
+
+ // Skipping to next searcher if no rewriter chain is specified
+ if(rewriterChain==null || rewriterChain.equals("")) {
+ RewriterUtils.log(logger, query, "No rewriter chain is specified, " +
+ "skipping to the next searcher");
+ return execution.search(query);
+ }
+
+ // Execute rewriter search chain
+ RewriterUtils.log(logger, query, "Redirecting to chain " + rewriterChain);
+ Chain<Searcher> myChain = execution.searchChainRegistry().getChain(rewriterChain);
+ if(myChain==null) {
+ RewriterUtils.log(logger, query, "Invalid search chain specified, " +
+ "skipping to the next searcher");
+ return execution.search(query);
+ }
+ new Execution(myChain, execution.context()).search(query);
+ RewriterUtils.log(logger, query, "Finish executing search chain " + rewriterChain);
+
+ // Continue down the chain ignoring the result from REWRITER_CHAIN
+ // since the rewriters only modify the query but not the result
+ return execution.search(query);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/package-info.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/package-info.java
new file mode 100644
index 00000000000..c435ed45623
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query.rewrite;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/GenericExpansionRewriter.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/GenericExpansionRewriter.java
new file mode 100644
index 00000000000..3d57675c4ab
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/GenericExpansionRewriter.java
@@ -0,0 +1,213 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.rewrite.rewriters;
+
+import java.io.*;
+import java.util.*;
+import java.util.logging.Logger;
+
+import com.google.inject.Inject;
+import com.yahoo.component.chain.dependencies.Provides;
+import com.yahoo.fsa.FSA;
+import com.yahoo.search.query.rewrite.*;
+import com.yahoo.search.*;
+import com.yahoo.component.ComponentId;
+import com.yahoo.filedistribution.fileacquirer.FileAcquirer;
+import com.yahoo.search.query.rewrite.RewritesConfig;
+import com.yahoo.prelude.querytransform.PhraseMatcher;
+
+/**
+ * This rewriter would add rewrites to entities (e.g abbreviation, synonym, etc)<br>
+ * to boost precision
+ * - FSA dict: [normalized original query]\t[rewrite 1]\t[rewrite 2]\t[etc]<br>
+ * - Features:<br>
+ * RewritesAsUnitEquiv flag: add proximity boosted rewrites<br>
+ * PartialPhraseMatch flag: whether to match whole phrase or partial phrase<br>
+ * MaxRewrites flag: the maximum number of rewrites to be added<br>
+ *
+ * @author Karen Sze Wing Lee
+ */
+@Provides("GenericExpansionRewriter")
+public class GenericExpansionRewriter extends QueryRewriteSearcher {
+
+ // Flag for skipping this rewriter if the query has been rewritten
+ private final boolean SKIP_REWRITER_IF_REWRITTEN = false;
+
+ // Name of the rewriter
+ public static final String REWRITER_NAME = "GenericExpansionRewriter";
+
+ // Generic expansion dictionary name
+ public static final String GENERIC_EXPAND_DICT = "GenericExpansion";
+
+ // Default generic expansion dictionary file name
+ public static final String GENERIC_EXPAND_DICT_FILENAME = "GenericExpansionRewriter.fsa";
+
+ // PhraseMatcher created from FSA dict
+ private PhraseMatcher phraseMatcher;
+
+ private Logger logger;
+
+
+ /**
+ * Constructor for GenericExpansionRewriter.
+ * Load configs using default format
+ */
+ @Inject
+ public GenericExpansionRewriter(ComponentId id,
+ FileAcquirer fileAcquirer,
+ RewritesConfig config) {
+ super(id, fileAcquirer, config);
+ }
+
+ /**
+ * Constructor for GenericExpansionRewriter unit test.
+ * Load configs using default format
+ */
+ public GenericExpansionRewriter(RewritesConfig config,
+ HashMap<String, File> fileList) {
+ super(config, fileList);
+ }
+
+ /**
+ * Instance creation time config loading besides FSA.
+ * Create PhraseMatcher from FSA dict
+ */
+ public boolean configure(FileAcquirer fileAcquirer,
+ RewritesConfig config,
+ HashMap<String, File> fileList) {
+ logger = Logger.getLogger(GenericExpansionRewriter.class.getName());
+ FSA fsa = (FSA)rewriterDicts.get(GENERIC_EXPAND_DICT);
+ if(fsa==null) {
+ RewriterUtils.error(logger, "Error retrieving FSA dictionary: " +
+ GENERIC_EXPAND_DICT);
+ return false;
+ }
+ // Create Phrase Matcher
+ RewriterUtils.log(logger, "Creating PhraseMatcher");
+ try {
+ phraseMatcher = new PhraseMatcher(fsa, false);
+ } catch (IllegalArgumentException e) {
+ RewriterUtils.error(logger, "Error creating phrase matcher");
+ return false;
+ }
+
+ // Match single word as well
+ phraseMatcher.setMatchSingleItems(true);
+
+ // Return all matches instead of only the longest match
+ phraseMatcher.setMatchAll(true);
+
+ return true;
+ }
+
+ /**
+ * Main logic of rewriter<br>
+ * - Retrieve rewrites from FSA dict<br>
+ * - rewrite query using features that are enabled by user
+ */
+ public HashMap<String, Object> rewrite(Query query,
+ String dictKey) throws RuntimeException {
+
+ Boolean rewritten = false;
+
+ // Pass the original dict key to the next rewriter
+ HashMap<String, Object> result = new HashMap<>();
+ result.put(RewriterConstants.REWRITTEN, rewritten);
+ result.put(RewriterConstants.DICT_KEY, dictKey);
+
+ RewriterUtils.log(logger, query,
+ "In GenericExpansionRewriter, query used for dict retrieval=[" + dictKey + "]");
+
+ // Retrieve flags for choosing between whole query match
+ // or partial query match
+ String partialPhraseMatch = getQPConfig(query, RewriterConstants.PARTIAL_PHRASE_MATCH);
+
+ if(partialPhraseMatch==null) {
+ RewriterUtils.error(logger, query, "Required param " + RewriterConstants.PARTIAL_PHRASE_MATCH +
+ " is not set, skipping rewriter");
+ throw new RuntimeException("Required param " + RewriterConstants.PARTIAL_PHRASE_MATCH +
+ " is not set, skipping rewriter");
+ }
+
+ // Retrieve max number of rewrites allowed
+ int maxNumRewrites = 0;
+ String maxNumRewritesStr = getQPConfig(query, RewriterConstants.MAX_REWRITES);
+ if(maxNumRewritesStr!=null) {
+ maxNumRewrites = Integer.parseInt(maxNumRewritesStr);
+ RewriterUtils.log(logger, query,
+ "Limiting max number of rewrites to: " + maxNumRewrites);
+ } else {
+ RewriterUtils.log(logger, query, "No limit on number of rewrites");
+ }
+
+ // Retrieve flags for choosing whether to add
+ // the rewrites as phrase, default to false
+ String rewritesAsUnitEquiv = getQPConfig(query, RewriterConstants.REWRITES_AS_UNIT_EQUIV);
+ if(rewritesAsUnitEquiv==null) {
+ rewritesAsUnitEquiv = "false";
+ }
+
+ Set<PhraseMatcher.Phrase> matches;
+
+ // Partial Phrase Matching
+ if(partialPhraseMatch.equalsIgnoreCase("true")) {
+ RewriterUtils.log(logger, query, "Partial phrase matching");
+
+ // Retrieve longest non overlapping matches
+ matches = RewriterFeatures.getNonOverlappingPartialPhraseMatches(phraseMatcher, query);
+
+ // Full Phrase Matching if set to anything else
+ } else {
+ RewriterUtils.log(logger, query, "Full phrase matching");
+
+ // Retrieve longest non overlapping matches
+ matches = RewriterFeatures.getNonOverlappingFullPhraseMatches(phraseMatcher, query);
+ }
+
+ if(matches==null) {
+ return result;
+ }
+
+ // Add expansions to the query
+ query = RewriterFeatures.addExpansions(query, matches, null, maxNumRewrites, false,
+ rewritesAsUnitEquiv.equalsIgnoreCase("true"));
+
+ rewritten = true;
+
+ RewriterUtils.log(logger, query, "GenericExpansionRewriter final query: " + query.toDetailString());
+
+ result.put(RewriterConstants.REWRITTEN, rewritten);
+
+ return result;
+ }
+
+ /**
+ * Get the flag which specifies whether this rewriter
+ * should be skipped if the query has been rewritten
+ *
+ * @return true if rewriter should be skipped, false
+ * otherwise
+ */
+ public boolean getSkipRewriterIfRewritten() {
+ return SKIP_REWRITER_IF_REWRITTEN;
+ }
+
+ /**
+ * Get the name of the rewriter
+ *
+ * @return Name of the rewriter
+ */
+ public String getRewriterName() {
+ return REWRITER_NAME;
+ }
+
+ /**
+ * Get default FSA dictionary names
+ *
+ * @return Pair of FSA dictionary name and filename
+ */
+ public HashMap<String, String> getDefaultFSAs() {
+ HashMap<String, String> defaultDicts = new HashMap<>();
+ defaultDicts.put(GENERIC_EXPAND_DICT, GENERIC_EXPAND_DICT_FILENAME);
+ return defaultDicts;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/MisspellRewriter.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/MisspellRewriter.java
new file mode 100644
index 00000000000..a1b46926cbd
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/MisspellRewriter.java
@@ -0,0 +1,151 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.rewrite.rewriters;
+
+import java.io.*;
+import java.util.*;
+import java.util.logging.Logger;
+
+import com.google.inject.Inject;
+import com.yahoo.component.chain.dependencies.After;
+import com.yahoo.component.chain.dependencies.Provides;
+import com.yahoo.search.query.rewrite.*;
+import com.yahoo.search.*;
+import com.yahoo.component.ComponentId;
+import com.yahoo.filedistribution.fileacquirer.FileAcquirer;
+import com.yahoo.search.query.rewrite.RewritesConfig;
+
+/**
+ * This rewriter would retrieve spell corrected query from QLAS and
+ * add it to the original query tree as equiv<br>
+ * - Features:<br>
+ * RewritesAsEquiv flag: add rewrites to original query as equiv
+ *
+ * @author Karen Sze Wing Lee
+ */
+@After("QLAS")
+@Provides("MisspellRewriter")
+public class MisspellRewriter extends QueryRewriteSearcher {
+
+ // Flag for skipping this rewriter if the query has been rewritten
+ private final boolean SKIP_REWRITER_IF_REWRITTEN = false;
+
+ // Name of the rewriter
+ public static final String REWRITER_NAME = "MisspellRewriter";
+
+ private Logger logger = Logger.getLogger(MisspellRewriter.class.getName());
+
+ /**
+ * Constructor for MisspellRewriter
+ */
+ @Inject
+ public MisspellRewriter(ComponentId id) {
+ super(id);
+ }
+
+ /**
+ * Constructor for MisspellRewriter unit test
+ */
+ public MisspellRewriter() {
+ super();
+ }
+
+ /**
+ * Instance creation time config loading besides FSA.
+ * Empty for this rewriter
+ */
+ public boolean configure(FileAcquirer fileAcquirer,
+ RewritesConfig config,
+ HashMap<String, File> fileList) {
+ return true;
+ }
+
+ /**
+ * Main logic of rewriter<br>
+ * - Retrieve spell corrected query from QLAS<br>
+ * - Add spell corrected query as equiv
+ */
+ public HashMap<String, Object> rewrite(Query query,
+ String dictKey) throws RuntimeException {
+
+ Boolean rewritten = false;
+
+ HashMap<String, Object> result = new HashMap<>();
+ result.put(RewriterConstants.REWRITTEN, rewritten);
+ result.put(RewriterConstants.DICT_KEY, dictKey);
+
+ RewriterUtils.log(logger, query,
+ "In MisspellRewriter");
+
+ // Retrieve flags for enabling the features
+ String qssRw = getQPConfig(query, RewriterConstants.QSS_RW);
+ String qssSugg = getQPConfig(query, RewriterConstants.QSS_SUGG);
+
+ boolean isQSSRw = false;
+ boolean isQSSSugg = false;
+
+ if(qssRw!=null) {
+ isQSSRw = qssRw.equalsIgnoreCase("true");
+ }
+ if(qssSugg!=null) {
+ isQSSSugg = qssSugg.equalsIgnoreCase("true");
+ }
+
+ // Rewrite is not enabled
+ if(!isQSSRw && !isQSSSugg) {
+ return result;
+ }
+
+ // Retrieve spell corrected query from QLAS
+ String rewrites = RewriterUtils.getSpellCorrected(query, isQSSRw, isQSSSugg);
+
+ // No rewrites
+ if(rewrites==null) {
+ RewriterUtils.log(logger, query, "No rewrite is retrieved");
+ return result;
+ } else {
+ RewriterUtils.log(logger, query, "Retrieved spell corrected query: " +
+ rewrites);
+ }
+
+ // Adding rewrite to the query tree
+ query = RewriterFeatures.addRewritesAsEquiv(query, dictKey, rewrites, false, 0);
+
+ rewritten = true;
+ RewriterUtils.log(logger, query, "MisspellRewriter final query: " +
+ query.toDetailString());
+
+ result.put(RewriterConstants.REWRITTEN, rewritten);
+ result.put(RewriterConstants.DICT_KEY, rewrites);
+
+ return result;
+ }
+
+ /**
+ * Get the flag which specifies whether this rewriter
+ * should be skipped if the query has been rewritten
+ *
+ * @return true if rewriter should be skipped, false
+ * otherwise
+ */
+ public boolean getSkipRewriterIfRewritten() {
+ return SKIP_REWRITER_IF_REWRITTEN;
+ }
+
+ /**
+ * Get the name of the rewriter
+ *
+ * @return Name of the rewriter
+ */
+ public String getRewriterName() {
+ return REWRITER_NAME;
+ }
+
+ /**
+ * Get default FSA dictionary names
+ *
+ * @return Pair of FSA dictionary name and filename
+ */
+ public HashMap<String, String> getDefaultFSAs() {
+ return null;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/NameRewriter.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/NameRewriter.java
new file mode 100644
index 00000000000..5ecf7893c63
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/NameRewriter.java
@@ -0,0 +1,194 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.rewrite.rewriters;
+
+import java.io.*;
+import java.util.*;
+import java.util.logging.Logger;
+
+import com.google.inject.Inject;
+import com.yahoo.component.chain.dependencies.Provides;
+import com.yahoo.search.query.rewrite.*;
+import com.yahoo.search.*;
+import com.yahoo.component.ComponentId;
+import com.yahoo.filedistribution.fileacquirer.FileAcquirer;
+import com.yahoo.search.query.rewrite.RewritesConfig;
+
+/**
+ * This rewriter would add rewrites to name entities to boost precision<br>
+ * - FSA dict: [normalized original query]\t[rewrite 1]\t[rewrite 2]\t[etc]<br>
+ * - Features:<br>
+ * OriginalAsUnit flag: add proximity boosting to original query<br>
+ * RewritesAsUnitEquiv flag: add proximity boosted rewrites to original query<br>
+ * RewritesAsEquiv flag: add rewrites to original query<br>
+ *
+ * @author Karen Sze Wing Lee
+ */
+@Provides("NameRewriter")
+public class NameRewriter extends QueryRewriteSearcher {
+
+ // Flag for skipping this rewriter if the query has been rewritten
+ private final boolean SKIP_REWRITER_IF_REWRITTEN = false;
+
+ // Name of the rewriter
+ public static final String REWRITER_NAME = "NameRewriter";
+
+ // Name entity expansion dictionary name
+ public static final String NAME_ENTITY_EXPAND_DICT = "NameEntityExpansion";
+
+ // Default Name entity expansion dictionary file name
+ public static final String NAME_ENTITY_EXPAND_DICT_FILENAME = "NameRewriter.fsa";
+
+ private Logger logger;
+
+ /**
+ * Constructor for NameRewriter<br>
+ * Load configs using default format
+ */
+ @Inject
+ public NameRewriter(ComponentId id,
+ FileAcquirer fileAcquirer,
+ RewritesConfig config) {
+ super(id, fileAcquirer, config);
+ }
+
+ /**
+ * Constructor for NameRewriter unit test<br>
+ * Load configs using default format
+ */
+ public NameRewriter(RewritesConfig config,
+ HashMap<String, File> fileList) {
+ super(config, fileList);
+ }
+
+ /**
+ * Instance creation time config loading besides FSA<br>
+ * Empty for this rewriter
+ */
+ public boolean configure(FileAcquirer fileAcquirer,
+ RewritesConfig config,
+ HashMap<String, File> fileList) {
+ logger = Logger.getLogger(NameRewriter.class.getName());
+ return true;
+ }
+
+ /**
+ * Main logic of rewriter<br>
+ * - Retrieve rewrites from FSA dict<br>
+ * - rewrite query using features that are enabled by user
+ */
+ public HashMap<String, Object> rewrite(Query query,
+ String dictKey) throws RuntimeException {
+
+ Boolean rewritten = false;
+
+ // Pass the original dict key to the next rewriter
+ HashMap<String, Object> result = new HashMap<>();
+ result.put(RewriterConstants.REWRITTEN, rewritten);
+ result.put(RewriterConstants.DICT_KEY, dictKey);
+
+ RewriterUtils.log(logger, query,
+ "In NameRewriter, query used for dict retrieval=[" + dictKey + "]");
+
+ // Retrieve rewrite from FSA dict using normalized query
+ String rewrites = super.getRewriteFromFSA(query, NAME_ENTITY_EXPAND_DICT, dictKey);
+ RewriterUtils.log(logger, query, "Retrieved rewrites: " + rewrites);
+
+ // No rewrites
+ if(rewrites==null) {
+ RewriterUtils.log(logger, query, "No rewrite is retrieved");
+ return result;
+ }
+
+ // Retrieve max number of rewrites allowed
+ int maxNumRewrites = 0;
+ String maxNumRewritesStr = getQPConfig(query, RewriterConstants.MAX_REWRITES);
+ if(maxNumRewritesStr!=null) {
+ maxNumRewrites = Integer.parseInt(maxNumRewritesStr);
+ RewriterUtils.log(logger, query,
+ "Limiting max number of rewrites to: " + maxNumRewrites);
+ } else {
+ RewriterUtils.log(logger, query, "No limit on number of rewrites");
+ }
+
+ // Retrieve flags for enabling the features
+ String originalAsUnit = getQPConfig(query, RewriterConstants.ORIGINAL_AS_UNIT);
+ String originalAsUnitEquiv = getQPConfig(query, RewriterConstants.ORIGINAL_AS_UNIT_EQUIV);
+ String rewritesAsUnitEquiv = getQPConfig(query, RewriterConstants.REWRITES_AS_UNIT_EQUIV);
+ String rewritesAsEquiv = getQPConfig(query, RewriterConstants.REWRITES_AS_EQUIV);
+
+ // Add proximity boosting to original query and keeping
+ // the original query if it's enabled
+ if(originalAsUnitEquiv!=null && originalAsUnitEquiv.equalsIgnoreCase("true")) {
+ RewriterUtils.log(logger, query, "OriginalAsUnitEquiv is enabled");
+ query = RewriterFeatures.addUnitToOriginalQuery(query, dictKey, true);
+ RewriterUtils.log(logger, query,
+ "Query after OriginalAsUnitEquiv: " + query.toDetailString());
+ rewritten = true;
+
+ // Add proximity boosting to original query
+ // if it's enabled
+ } else if(originalAsUnit!=null && originalAsUnit.equalsIgnoreCase("true")) {
+ RewriterUtils.log(logger, query, "OriginalAsUnit is enabled");
+ query = RewriterFeatures.addUnitToOriginalQuery(query, dictKey, false);
+ RewriterUtils.log(logger, query,
+ "Query after OriginalAsUnit: " + query.toDetailString());
+ rewritten = true;
+ }
+
+ // Add rewrites as unit equiv if it's enabled
+ if(rewritesAsUnitEquiv!=null && rewritesAsUnitEquiv.equalsIgnoreCase("true")) {
+ RewriterUtils.log(logger, query, "RewritesAsUnitEquiv is enabled");
+ //query = RewriterFeatures.addRewritesAsEquiv(query, dictKey, rewrites, true, maxNumRewrites);
+ query = RewriterFeatures.addRewritesAsEquiv(query, dictKey, rewrites, true, maxNumRewrites);
+ RewriterUtils.log(logger, query,
+ "Query after RewritesAsUnitEquiv: " + query.toDetailString());
+ rewritten = true;
+
+ // Add rewrites as equiv if it's enabled
+ } else if(rewritesAsEquiv!=null && rewritesAsEquiv.equalsIgnoreCase("true")) {
+ RewriterUtils.log(logger, query, "RewritesAsEquiv is enabled");
+ //query = RewriterFeatures.addRewritesAsEquiv(query, dictKey, rewrites, false, maxNumRewrites);
+ query = RewriterFeatures.addRewritesAsEquiv(query, dictKey, rewrites, false, maxNumRewrites);
+ RewriterUtils.log(logger, query,
+ "Query after RewritesAsEquiv: " + query.toDetailString());
+ rewritten = true;
+ }
+
+ RewriterUtils.log(logger, query, "NameRewriter final query: " + query.toDetailString());
+
+ result.put(RewriterConstants.REWRITTEN, rewritten);
+
+ return result;
+ }
+
+ /**
+ * Get the flag which specifies whether this rewriter.
+ * should be skipped if the query has been rewritten
+ *
+ * @return true if rewriter should be skipped, false
+ * otherwise
+ */
+ public boolean getSkipRewriterIfRewritten() {
+ return SKIP_REWRITER_IF_REWRITTEN;
+ }
+
+ /**
+ * Get the name of the rewriter
+ *
+ * @return Name of the rewriter
+ */
+ public String getRewriterName() {
+ return REWRITER_NAME;
+ }
+
+ /**
+ * Get default FSA dictionary names
+ *
+ * @return Pair of FSA dictionary name and filename
+ */
+ public HashMap<String, String> getDefaultFSAs() {
+ HashMap<String, String> defaultDicts = new HashMap<>();
+ defaultDicts.put(NAME_ENTITY_EXPAND_DICT, NAME_ENTITY_EXPAND_DICT_FILENAME);
+ return defaultDicts;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/package-info.java b/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/package-info.java
new file mode 100644
index 00000000000..bfbb73f661e
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/rewrite/rewriters/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query.rewrite.rewriters;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/TextSerialize.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/TextSerialize.java
new file mode 100644
index 00000000000..bac9f2af237
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/TextSerialize.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize;
+
+import com.yahoo.prelude.query.Item;
+import com.yahoo.search.query.textserialize.item.ItemContext;
+import com.yahoo.search.query.textserialize.item.ItemFormHandler;
+import com.yahoo.search.query.textserialize.parser.ParseException;
+import com.yahoo.search.query.textserialize.parser.Parser;
+import com.yahoo.search.query.textserialize.parser.TokenMgrError;
+import com.yahoo.search.query.textserialize.serializer.QueryTreeSerializer;
+
+import java.io.StringReader;
+
+/**
+ * @author tonytv
+ * Facade
+ * Allows serializing/deserializing a query to the programmatic format.
+ */
+public class TextSerialize {
+ public static Item parse(String serializedQuery) {
+ try {
+ ItemContext context = new ItemContext();
+ Object result = new Parser(new StringReader(serializedQuery.replace("'", "\"")), new ItemFormHandler(), context).start();
+ context.connectItems();
+
+ if (!(result instanceof Item)) {
+ throw new RuntimeException("The serialized query '" + serializedQuery + "' did not evaluate to an Item" +
+ "(type = " + result.getClass() + ")");
+ }
+ return (Item) result;
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ } catch (TokenMgrError e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String serialize(Item item) {
+ return new QueryTreeSerializer().serialize(item);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/AndNotRestConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/AndNotRestConverter.java
new file mode 100644
index 00000000000..c4e54ca748d
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/AndNotRestConverter.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.Item;
+import com.yahoo.prelude.query.NotItem;
+
+import java.util.List;
+
+import static com.yahoo.search.query.textserialize.item.ListUtil.butFirst;
+import static com.yahoo.search.query.textserialize.item.ListUtil.first;
+
+/**
+ * @author tonytv
+ */
+public class AndNotRestConverter extends CompositeConverter<NotItem> {
+ static final String andNotRest = "AND-NOT-REST";
+
+ public AndNotRestConverter() {
+ super(NotItem.class);
+ }
+
+ @Override
+ protected void addChildren(NotItem item, ItemArguments arguments, ItemContext context) {
+ if (firstIsNull(arguments.children)) {
+ addNegativeItems(item, arguments.children);
+ } else {
+ addItems(item, arguments.children);
+ }
+ }
+
+ private void addNegativeItems(NotItem notItem, List<Object> children) {
+ for (Object child: butFirst(children)) {
+ TypeCheck.ensureInstanceOf(child, Item.class);
+ notItem.addNegativeItem((Item) child);
+ }
+ }
+
+ private void addItems(NotItem notItem, List<Object> children) {
+ for (Object child : children) {
+ TypeCheck.ensureInstanceOf(child, Item.class);
+ notItem.addItem((Item) child);
+ }
+ }
+
+
+ private boolean firstIsNull(List<Object> children) {
+ return !children.isEmpty() && first(children) == null;
+ }
+
+ @Override
+ protected String getFormName(Item item) {
+ return andNotRest;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/CompositeConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/CompositeConverter.java
new file mode 100644
index 00000000000..7f7c5e48d0a
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/CompositeConverter.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.CompositeItem;
+import com.yahoo.prelude.query.Item;
+import com.yahoo.search.query.textserialize.serializer.DispatchForm;
+import com.yahoo.search.query.textserialize.serializer.ItemIdMapper;
+
+import java.util.ListIterator;
+
+/**
+ * @author tonytv
+ */
+public class CompositeConverter<T extends CompositeItem> implements ItemFormConverter {
+ private final Class<T> itemClass;
+
+ public CompositeConverter(Class<T> itemClass) {
+ this.itemClass = itemClass;
+ }
+
+ @Override
+ public Object formToItem(String name, ItemArguments arguments, ItemContext itemContext) {
+ T item = newInstance();
+ addChildren(item, arguments, itemContext);
+ return item;
+ }
+
+ protected void addChildren(T item, ItemArguments arguments, ItemContext itemContext) {
+ for (Object child : arguments.children) {
+ item.addItem(asItem(child));
+ }
+ ItemInitializer.initialize(item, arguments, itemContext);
+ }
+
+ private static Item asItem(Object child) {
+ if (!(child instanceof Item) && child != null) {
+ throw new RuntimeException("Expected query item, but got '" + child.toString() +
+ "' [" + child.getClass().getName() + "]");
+ }
+ return (Item) child;
+ }
+
+ private T newInstance() {
+ try {
+ return itemClass.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public DispatchForm itemToForm(Item item, ItemIdMapper itemIdMapper) {
+ CompositeItem compositeItem = (CompositeItem) item;
+
+ DispatchForm form = new DispatchForm(getFormName(item));
+ for (ListIterator<Item> i = compositeItem.getItemIterator(); i.hasNext() ;) {
+ form.addChild(i.next());
+ }
+ ItemInitializer.initializeForm(form, item, itemIdMapper);
+ return form;
+ }
+
+ protected String getFormName(Item item) {
+ return item.getItemType().name();
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ExactStringConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ExactStringConverter.java
new file mode 100644
index 00000000000..4b68ecfe5a9
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ExactStringConverter.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.ExactstringItem;
+
+/**
+ * @author balder
+ */
+// TODO: balder to fix javadoc
+public class ExactStringConverter extends WordConverter {
+ @Override
+ ExactstringItem newTermItem(String word) {
+ return new ExactstringItem(word);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/IntConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/IntConverter.java
new file mode 100644
index 00000000000..43b96d17773
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/IntConverter.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.IntItem;
+import com.yahoo.prelude.query.TermItem;
+
+/**
+ * @author tonytv
+ */
+public class IntConverter extends TermConverter {
+ @Override
+ IntItem newTermItem(String word) {
+ return new IntItem(word);
+ }
+
+ @Override
+ protected String getValue(TermItem item) {
+ return ((IntItem)item).getNumber();
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemArguments.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemArguments.java
new file mode 100644
index 00000000000..50cc9c42773
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemArguments.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static com.yahoo.search.query.textserialize.item.ListUtil.firstInstanceOf;
+
+/**
+ * @author tonytv
+ */
+public class ItemArguments {
+ public final Map<?, ?> properties;
+ public final List<Object> children;
+
+ public ItemArguments(List<Object> arguments) {
+ if (firstInstanceOf(arguments, Map.class)) {
+ properties = (Map<?, ?>) ListUtil.first(arguments);
+ children = ListUtil.rest(arguments);
+ } else {
+ properties = Collections.emptyMap();
+ children = arguments;
+ }
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemContext.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemContext.java
new file mode 100644
index 00000000000..fd21b4e02e1
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemContext.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.Item;
+import com.yahoo.prelude.query.TaggableItem;
+
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+/**
+ * @author tonytv
+ */
+public class ItemContext {
+ private class Connectivity {
+ final String id;
+ final double strength;
+
+ public Connectivity(String id, double strength) {
+ this.id = id;
+ this.strength = strength;
+ }
+ }
+
+ private final Map<String, Item> itemById = new HashMap<>();
+ private final Map<TaggableItem, Connectivity> connectivityByItem = new IdentityHashMap<>();
+
+
+ public void setItemId(String id, Item item) {
+ itemById.put(id, item);
+ }
+
+ public void setConnectivity(TaggableItem item, String id, Double strength) {
+ connectivityByItem.put(item, new Connectivity(id, strength));
+ }
+
+ public void connectItems() {
+ for (Map.Entry<TaggableItem, Connectivity> entry : connectivityByItem.entrySet()) {
+ entry.getKey().setConnectivity(getItem(entry.getValue().id), entry.getValue().strength);
+ }
+ }
+
+ private Item getItem(String id) {
+ Item item = itemById.get(id);
+ if (item == null)
+ throw new IllegalArgumentException("No item with id '" + id + "'.");
+ return item;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemExecutorRegistry.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemExecutorRegistry.java
new file mode 100644
index 00000000000..20ef9f4e5cc
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemExecutorRegistry.java
@@ -0,0 +1,71 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.AndItem;
+import com.yahoo.prelude.query.CompositeItem;
+import com.yahoo.prelude.query.EquivItem;
+import com.yahoo.prelude.query.Item;
+import com.yahoo.prelude.query.NearItem;
+import com.yahoo.prelude.query.ONearItem;
+import com.yahoo.prelude.query.OrItem;
+import com.yahoo.prelude.query.PhraseItem;
+import com.yahoo.prelude.query.RankItem;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author tonytv
+ */
+public class ItemExecutorRegistry {
+
+ private static final Map<String, ItemFormConverter> executorsByName = new HashMap<>();
+ static {
+ register(Item.ItemType.AND, createCompositeConverter(AndItem.class));
+ register(Item.ItemType.OR, createCompositeConverter(OrItem.class));
+ register(Item.ItemType.RANK, createCompositeConverter(RankItem.class));
+ register(Item.ItemType.PHRASE, createCompositeConverter(PhraseItem.class));
+ register(Item.ItemType.EQUIV, createCompositeConverter(EquivItem.class));
+
+ register(AndNotRestConverter.andNotRest, new AndNotRestConverter());
+
+ register(Item.ItemType.NEAR, new NearConverter(NearItem.class));
+ register(Item.ItemType.ONEAR, new NearConverter(ONearItem.class));
+
+ register(Item.ItemType.WORD, new WordConverter());
+ register(Item.ItemType.INT, new IntConverter());
+ register(Item.ItemType.PREFIX, new PrefixConverter());
+ register(Item.ItemType.SUBSTRING, new SubStringConverter());
+ register(Item.ItemType.EXACT, new ExactStringConverter());
+ register(Item.ItemType.SUFFIX, new SuffixConverter());
+ }
+
+ private static <T extends CompositeItem> ItemFormConverter createCompositeConverter(Class<T> itemClass) {
+ return new CompositeConverter<>(itemClass);
+ }
+
+ private static void register(Item.ItemType type, ItemFormConverter executor) {
+ register(type.toString(), executor);
+ }
+
+ private static void register(String type, ItemFormConverter executor) {
+ executorsByName.put(type, executor);
+ }
+
+ public static ItemFormConverter getByName(String name) {
+ ItemFormConverter executor = executorsByName.get(name);
+ ensureNotNull(executor, name);
+ return executor;
+ }
+
+ private static void ensureNotNull(ItemFormConverter executor, String name) {
+ if (executor == null) {
+ throw new RuntimeException("No item type named '" + name + "'.");
+ }
+ }
+
+ public static ItemFormConverter getByType(Item.ItemType itemType) {
+ String name = (itemType == Item.ItemType.NOT) ? AndNotRestConverter.andNotRest : itemType.name();
+ return getByName(name);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemFormConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemFormConverter.java
new file mode 100644
index 00000000000..256ad569686
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemFormConverter.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.Item;
+import com.yahoo.search.query.textserialize.serializer.DispatchForm;
+import com.yahoo.search.query.textserialize.serializer.ItemIdMapper;
+
+/**
+ * @author tonytv
+ */
+public interface ItemFormConverter {
+ Object formToItem(String name, ItemArguments arguments, ItemContext context);
+ DispatchForm itemToForm(Item item, ItemIdMapper itemIdMapper);
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemFormHandler.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemFormHandler.java
new file mode 100644
index 00000000000..81b13a107c8
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemFormHandler.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.search.query.textserialize.parser.DispatchFormHandler;
+
+import java.util.List;
+
+/**
+ * @author tonytv
+ */
+public class ItemFormHandler implements DispatchFormHandler{
+ @Override
+ public Object dispatch(String name, List<Object> arguments, Object dispatchContext) {
+ ItemFormConverter executor = ItemExecutorRegistry.getByName(name);
+ return executor.formToItem(name, new ItemArguments(arguments), (ItemContext)dispatchContext);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemInitializer.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemInitializer.java
new file mode 100644
index 00000000000..ae54165abef
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ItemInitializer.java
@@ -0,0 +1,137 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.IndexedItem;
+import com.yahoo.prelude.query.Item;
+import com.yahoo.prelude.query.TaggableItem;
+import com.yahoo.search.query.textserialize.serializer.DispatchForm;
+import com.yahoo.search.query.textserialize.serializer.ItemIdMapper;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author tonytv
+ */
+public class ItemInitializer {
+ private static final String indexProperty = "index";
+ private static final String idProperty = "id";
+ private static final String significanceProperty = "significance";
+ private static final String uniqueIdProperty = "uniqueId";
+ private static final String weightProperty = "weight";
+
+ public static void initialize(Item item, ItemArguments arguments, ItemContext itemContext) {
+ storeIdInContext(item, arguments.properties, itemContext);
+
+ Object weight = arguments.properties.get(weightProperty);
+ if (weight != null) {
+ TypeCheck.ensureInstanceOf(weight, Number.class);
+ item.setWeight(((Number)weight).intValue());
+ }
+
+ if (item instanceof TaggableItem) {
+ initializeTaggableItem((TaggableItem)item, arguments, itemContext);
+ }
+
+ if (item instanceof IndexedItem) {
+ initializeIndexedItem((IndexedItem)item, arguments, itemContext);
+ }
+ }
+
+ private static void storeIdInContext(Item item, Map<?, ?> properties, ItemContext itemContext) {
+ Object id = properties.get("id");
+ if (id != null) {
+ TypeCheck.ensureInstanceOf(id, String.class);
+ itemContext.setItemId((String) id, item);
+ }
+ }
+
+ private static void initializeTaggableItem(TaggableItem item, ItemArguments arguments, ItemContext itemContext) {
+ Object connectivity = arguments.properties.get("connectivity");
+ if (connectivity != null) {
+ storeConnectivityInContext(item, connectivity, itemContext);
+ }
+
+ Object significance = arguments.properties.get(significanceProperty);
+ if (significance != null) {
+ TypeCheck.ensureInstanceOf(significance, Number.class);
+ item.setSignificance(((Number)significance).doubleValue());
+ }
+
+ Object uniqueId = arguments.properties.get(uniqueIdProperty);
+ if (uniqueId != null) {
+ TypeCheck.ensureInstanceOf(uniqueId, Number.class);
+ item.setUniqueID(((Number)uniqueId).intValue());
+ }
+ }
+
+ private static void initializeIndexedItem(IndexedItem indexedItem, ItemArguments arguments, ItemContext itemContext) {
+ Object index = arguments.properties.get(indexProperty);
+ if (index != null) {
+ TypeCheck.ensureInstanceOf(index, String.class);
+ indexedItem.setIndexName((String) index);
+ }
+ }
+
+ private static void storeConnectivityInContext(TaggableItem item, Object connectivity, ItemContext itemContext) {
+ TypeCheck.ensureInstanceOf(connectivity, List.class);
+ List<?> connectivityList = (List<?>) connectivity;
+ if (connectivityList.size() != 2) {
+ throw new IllegalArgumentException("Expected two elements for connectivity, got " + connectivityList.size());
+ }
+
+ Object id = connectivityList.get(0);
+ Object strength = connectivityList.get(1);
+
+ TypeCheck.ensureInstanceOf(id, String.class);
+ TypeCheck.ensureInstanceOf(strength, Number.class);
+
+ itemContext.setConnectivity(item, (String)id, ((Number)strength).doubleValue());
+ }
+
+ public static void initializeForm(DispatchForm form, Item item, ItemIdMapper itemIdMapper) {
+ if (item.getWeight() != Item.DEFAULT_WEIGHT) {
+ form.setProperty(weightProperty, item.getWeight());
+ }
+
+ if (item instanceof IndexedItem) {
+ initializeIndexedForm(form, (IndexedItem) item);
+ }
+ if (item instanceof TaggableItem) {
+ initializeTaggableForm(form, (TaggableItem) item, itemIdMapper);
+ }
+ initializeFormWithIdIfConnected(form, item, itemIdMapper);
+ }
+
+ private static void initializeFormWithIdIfConnected(DispatchForm form, Item item, ItemIdMapper itemIdMapper) {
+ if (item.hasConnectivityBackLink()) {
+ form.setProperty(idProperty, itemIdMapper.getId(item));
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void initializeTaggableForm(DispatchForm form, TaggableItem taggableItem, ItemIdMapper itemIdMapper) {
+ Item connectedItem = taggableItem.getConnectedItem();
+ if (connectedItem != null) {
+ form.setProperty("connectivity",
+ Arrays.asList(itemIdMapper.getId(connectedItem), taggableItem.getConnectivity()));
+ }
+
+ if (taggableItem.hasExplicitSignificance()) {
+ form.setProperty(significanceProperty, taggableItem.getSignificance());
+ }
+
+ if (taggableItem.hasUniqueID()) {
+ form.setProperty(uniqueIdProperty, taggableItem.getUniqueID());
+ }
+ }
+
+ private static void initializeIndexedForm(DispatchForm form, IndexedItem item) {
+ String index = item.getIndexName();
+ if (!index.isEmpty()) {
+ form.setProperty(indexProperty, index);
+ }
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ListUtil.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ListUtil.java
new file mode 100644
index 00000000000..9349b01a3bc
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/ListUtil.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import java.util.*;
+
+/**
+ * @author tonytv
+ */
+public class ListUtil {
+ public static <T> List<T> rest(List<T> list) {
+ return list.subList(1, list.size());
+ }
+
+ public static <T> T first(Collection<T> collection) {
+ return collection.iterator().next();
+ }
+
+ public static boolean firstInstanceOf(Collection<?> collection, @SuppressWarnings("rawtypes") Class c) {
+ return !collection.isEmpty() && c.isInstance(first(collection));
+ }
+
+ public static <T> List<T> butFirst(List<T> list) {
+ return list.subList(1, list.size());
+ }
+
+ public static <T> Iterable<T> butFirst(final Collection<T> collection) {
+ return () -> {
+ Iterator<T> i = collection.iterator();
+ i.next();
+ return i;
+ };
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/NearConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/NearConverter.java
new file mode 100644
index 00000000000..3be8d3d1c65
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/NearConverter.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.Item;
+import com.yahoo.prelude.query.NearItem;
+import com.yahoo.search.query.textserialize.serializer.DispatchForm;
+import com.yahoo.search.query.textserialize.serializer.ItemIdMapper;
+
+/**
+ * @author tonytv
+ */
+@SuppressWarnings("rawtypes")
+public class NearConverter extends CompositeConverter {
+ final private String distanceProperty = "distance";;
+
+ @SuppressWarnings("unchecked")
+ public NearConverter(Class<? extends NearItem> nearItemClass) {
+ super(nearItemClass);
+ }
+
+ @Override
+ public Object formToItem(String name, ItemArguments arguments, ItemContext itemContext) {
+ NearItem nearItem = (NearItem) super.formToItem(name, arguments, itemContext);
+ setDistance(nearItem, arguments);
+ return nearItem;
+ }
+
+ private void setDistance(NearItem nearItem, ItemArguments arguments) {
+ Object distance = arguments.properties.get(distanceProperty);
+ if (distance != null) {
+ TypeCheck.ensureInteger(distance);
+ nearItem.setDistance(((Number)distance).intValue());
+ }
+ }
+
+ @Override
+ public DispatchForm itemToForm(Item item, ItemIdMapper itemIdMapper) {
+ DispatchForm dispatchForm = super.itemToForm(item, itemIdMapper);
+
+ NearItem nearItem = (NearItem)item;
+ dispatchForm.setProperty(distanceProperty, nearItem.getDistance());
+ return dispatchForm;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/PrefixConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/PrefixConverter.java
new file mode 100644
index 00000000000..cb3a6c1943c
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/PrefixConverter.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.PrefixItem;
+
+/**
+ * @author tonytv
+ */
+public class PrefixConverter extends WordConverter {
+ @Override
+ PrefixItem newTermItem(String word) {
+ return new PrefixItem(word);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/SubStringConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/SubStringConverter.java
new file mode 100644
index 00000000000..e61a189684f
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/SubStringConverter.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.SubstringItem;
+
+/**
+ * @author tonytv
+ */
+public class SubStringConverter extends WordConverter {
+ @Override
+ SubstringItem newTermItem(String word) {
+ return new SubstringItem(word);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/SuffixConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/SuffixConverter.java
new file mode 100644
index 00000000000..4390e3464d2
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/SuffixConverter.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.SuffixItem;
+
+/**
+ * @author tonytv
+ */
+public class SuffixConverter extends WordConverter {
+ @Override
+ SuffixItem newTermItem(String word) {
+ return new SuffixItem(word);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/TermConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/TermConverter.java
new file mode 100644
index 00000000000..8bc6cba7f67
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/TermConverter.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.Item;
+import com.yahoo.prelude.query.TermItem;
+import com.yahoo.search.query.textserialize.serializer.DispatchForm;
+import com.yahoo.search.query.textserialize.serializer.ItemIdMapper;
+
+/**
+ * @author tonytv
+ */
+public abstract class TermConverter implements ItemFormConverter {
+ @Override
+ public Object formToItem(String name, ItemArguments arguments, ItemContext context) {
+ ensureOnlyOneChild(arguments);
+ String word = getWord(arguments);
+
+ TermItem item = newTermItem(word);
+ ItemInitializer.initialize(item, arguments, context);
+ return item;
+ }
+
+ abstract TermItem newTermItem(String word);
+
+
+ private void ensureOnlyOneChild(ItemArguments arguments) {
+ if (arguments.children.size() != 1) {
+ throw new IllegalArgumentException("Expected exactly one argument, got '" +
+ arguments.children.toString() + "'");
+ }
+ }
+
+ private String getWord(ItemArguments arguments) {
+ Object word = arguments.children.get(0);
+
+ if (!(word instanceof String)) {
+ throw new RuntimeException("Expected string, got '" + word + "' [" + word.getClass().getName() + "].");
+ }
+ return (String)word;
+ }
+
+ @Override
+ public DispatchForm itemToForm(Item item, ItemIdMapper itemIdMapper) {
+ TermItem termItem = (TermItem)item;
+
+ DispatchForm form = new DispatchForm(termItem.getItemType().name());
+ ItemInitializer.initializeForm(form, item, itemIdMapper);
+ form.addChild(getValue(termItem));
+ return form;
+ }
+
+ protected abstract String getValue(TermItem item);
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/TypeCheck.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/TypeCheck.java
new file mode 100644
index 00000000000..a6e38d288a4
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/TypeCheck.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.protect.Validator;
+
+/**
+ * @author tonytv
+ */
+public class TypeCheck {
+ public static void ensureInstanceOf(Object object, Class<?> c) {
+ Validator.ensureInstanceOf(expectationString(c.getName(), object.getClass().getSimpleName()),
+ object, c);
+ }
+
+ public static void ensureInteger(Object value) {
+ ensureInstanceOf(value, Number.class);
+ Number number = (Number)value;
+
+ int intValue = number.intValue();
+ if (intValue != number.doubleValue())
+ throw new IllegalArgumentException("Invalid integer '" + number + "'");
+ }
+
+ private static String expectationString(String expected, String got) {
+ return "Expected " + expected + ", but got " + got;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/item/WordConverter.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/WordConverter.java
new file mode 100644
index 00000000000..dce33e392ae
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/item/WordConverter.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.item;
+
+import com.yahoo.prelude.query.TermItem;
+import com.yahoo.prelude.query.WordItem;
+
+/**
+ * @author tonytv
+ */
+public class WordConverter extends TermConverter {
+ @Override
+ WordItem newTermItem(String word) {
+ return new WordItem(word);
+ }
+
+ @Override
+ protected String getValue(TermItem item) {
+ return ((WordItem)item).getWord();
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/package-info.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/package-info.java
new file mode 100644
index 00000000000..1e1d3052731
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.query.textserialize;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/parser/.gitignore b/container-search/src/main/java/com/yahoo/search/query/textserialize/parser/.gitignore
new file mode 100644
index 00000000000..add88bd6807
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/parser/.gitignore
@@ -0,0 +1,7 @@
+/TokenMgrError.java
+/Token.java
+/SimpleCharStream.java
+/ParserTokenManager.java
+/ParserConstants.java
+/ParseException.java
+/Parser.java
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/parser/DispatchFormHandler.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/parser/DispatchFormHandler.java
new file mode 100644
index 00000000000..33c8e36bd57
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/parser/DispatchFormHandler.java
@@ -0,0 +1,11 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.parser;
+
+import java.util.List;
+
+/**
+ * @author tonytv
+ */
+public interface DispatchFormHandler {
+ Object dispatch(String name, List<Object> arguments, Object dispatchContext);
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/DispatchForm.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/DispatchForm.java
new file mode 100644
index 00000000000..091efa0a01b
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/DispatchForm.java
@@ -0,0 +1,56 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.serializer;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author tonytv
+ */
+public class DispatchForm {
+ private final String name;
+ public final Map<Object, Object> properties = new LinkedHashMap<>();
+ public final List<Object> children = new ArrayList<>();
+
+ public DispatchForm(String name) {
+ this.name = name;
+ }
+
+ public void addChild(Object child) {
+ children.add(child);
+ }
+
+ /**
+ * Only public for the purpose of testing.
+ */
+ public String serialize(ItemIdMapper itemIdMapper) {
+ StringBuilder builder = new StringBuilder();
+ builder.append('(').append(name);
+
+ serializeProperties(builder, itemIdMapper);
+ serializeChildren(builder, itemIdMapper);
+
+ builder.append(')');
+ return builder.toString();
+ }
+
+ private void serializeProperties(StringBuilder builder, ItemIdMapper itemIdMapper) {
+ if (properties.isEmpty())
+ return;
+
+ builder.append(' ').append(Serializer.serializeMap(properties, itemIdMapper));
+ }
+
+
+ private void serializeChildren(StringBuilder builder, ItemIdMapper itemIdMapper) {
+ for (Object child : children) {
+ builder.append(' ').append(Serializer.serialize(child, itemIdMapper));
+ }
+ }
+
+ public void setProperty(Object key, Object value) {
+ properties.put(key, value);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/ItemIdMapper.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/ItemIdMapper.java
new file mode 100644
index 00000000000..c32a7f52c0a
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/ItemIdMapper.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.serializer;
+
+import com.yahoo.prelude.query.Item;
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+/**
+ * @author tonytv
+ */
+public class ItemIdMapper {
+ private final Map<Item, String> idByItem = new IdentityHashMap<>();
+ private int idCounter = 0;
+
+ public String getId(Item item) {
+ String id = idByItem.get(item);
+ if (id != null) {
+ return id;
+ } else {
+ idByItem.put(item, generateId(item));
+ return getId(item);
+ }
+ }
+
+ private String generateId(Item item) {
+ return item.getName() + "_" + nextCount();
+ }
+
+ private int nextCount() {
+ return idCounter++;
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/QueryTreeSerializer.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/QueryTreeSerializer.java
new file mode 100644
index 00000000000..e3090930369
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/QueryTreeSerializer.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.serializer;
+
+import com.yahoo.prelude.query.Item;
+import com.yahoo.search.query.textserialize.item.ItemExecutorRegistry;
+
+
+/**
+ * @author tonytv
+ */
+public class QueryTreeSerializer {
+ public String serialize(Item root) {
+ ItemIdMapper itemIdMapper = new ItemIdMapper();
+ return ItemExecutorRegistry.getByType(root.getItemType()).itemToForm(root, itemIdMapper).serialize(itemIdMapper);
+ }
+}
diff --git a/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/Serializer.java b/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/Serializer.java
new file mode 100644
index 00000000000..e8352254551
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/query/textserialize/serializer/Serializer.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.query.textserialize.serializer;
+
+import com.yahoo.prelude.query.Item;
+import com.yahoo.search.query.textserialize.item.ItemExecutorRegistry;
+
+import java.util.List;
+import java.util.Map;
+
+import static com.yahoo.search.query.textserialize.item.ListUtil.butFirst;
+import static com.yahoo.search.query.textserialize.item.ListUtil.first;
+
+/**
+ * @author tonytv
+ */
+class Serializer {
+ static String serialize(Object child, ItemIdMapper itemIdMapper) {
+ if (child instanceof DispatchForm) {
+ return ((DispatchForm) child).serialize(itemIdMapper);
+ } else if (child instanceof Item) {
+ return serializeItem((Item) child, itemIdMapper);
+ } else if (child instanceof String) {
+ return serializeString((String) child);
+ } else if (child instanceof Number) {
+ return child.toString();
+ } else if (child instanceof Map) {
+ return serializeMap((Map<?, ?>)child, itemIdMapper);
+ } else if (child instanceof List) {
+ return serializeList((List<?>)child, itemIdMapper);
+ } else {
+ throw new IllegalArgumentException("Can't serialize type " + child.getClass());
+ }
+ }
+
+ private static String serializeString(String string) {
+ return '"' + string.replace("\\", "\\\\").replace("\"", "\\\"") + '"';
+ }
+
+ static String serializeList(List<?> list, ItemIdMapper itemIdMapper) {
+ StringBuilder builder = new StringBuilder();
+ builder.append('[');
+
+ if (!list.isEmpty()) {
+ builder.append(serialize(first(list), itemIdMapper));
+
+ for (Object element : butFirst(list)) {
+ builder.append(", ").append(serialize(element, itemIdMapper));
+ }
+ }
+
+ builder.append(']');
+ return builder.toString();
+ }
+
+ static String serializeMap(Map<?, ?> map, ItemIdMapper itemIdMapper) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("{");
+
+ if (!map.isEmpty()) {
+ serializeEntry(builder, first(map.entrySet()), itemIdMapper);
+ for (Map.Entry<?, ?> entry : butFirst(map.entrySet())) {
+ builder.append(", ");
+ serializeEntry(builder, entry, itemIdMapper);
+ }
+ }
+
+ builder.append('}');
+ return builder.toString();
+ }
+
+ static void serializeEntry(StringBuilder builder, Map.Entry<?, ?> entry, ItemIdMapper itemIdMapper) {
+ builder.append(serialize(entry.getKey(), itemIdMapper)).append(' ').
+ append(serialize(entry.getValue(), itemIdMapper));
+ }
+
+ static String serializeItem(Item item, ItemIdMapper itemIdMapper) {
+ return ItemExecutorRegistry.getByType(item.getItemType()).itemToForm(item, itemIdMapper).serialize(itemIdMapper);
+ }
+}