// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.searchchain; import com.yahoo.component.chain.Chain; import com.yahoo.language.Linguistics; import com.yahoo.prelude.IndexFacts; import com.yahoo.prelude.Ping; import com.yahoo.prelude.Pong; import com.yahoo.prelude.query.parser.SpecialTokenRegistry; import com.yahoo.processing.Processor; import com.yahoo.processing.Request; import com.yahoo.processing.Response; import com.yahoo.search.Query; import com.yahoo.search.Result; import com.yahoo.search.Searcher; import com.yahoo.search.cluster.PingableSearcher; import com.yahoo.search.rendering.RendererRegistry; import com.yahoo.search.statistics.TimeTracker; import java.util.logging.Logger; /** *

An execution of a search chain. This keeps track of the call state for an execution (in the calling thread) * of the searchers of a search chain.

* *

To execute a search chain, simply do *

 *     Result result = new Execution(mySearchChain, execution.context()).search(query)
 * 
* * *

See also {@link AsyncExecution}, which performs an execution in a separate thread than the caller.

* *

Execution instances should not be reused for multiple separate executions.

* * @author bratseth */ public class Execution extends com.yahoo.processing.execution.Execution { public static final String ATTRIBUTEPREFETCH = "attributeprefetch"; /** * The execution context is the search chain's current view of the indexes, * search chain registrys, etc. Searcher instances may set values here to * change the behavior of the rest of the search chain. *

* The Context class simply carries a set of objects which define the * environment for the search. Important: All objects available through context need to * be either truly immutable or support the freeze pattern. *

* If you are implementing a searcher where you need to create a new Context * instance to create an Execution, you should use the context from the * execution the searcher was invoked from. You can also copy * (Context.shallowCopy()) the incoming context if it is necessary to do * more. In other words, a minimal example would be:
* new Execution(searchChain, execution.context()) */ public static final class Context { /** Whether the search should perform detailed diagnostics. */ private boolean detailedDiagnostics = false; /** Whether the container was considered to be in a breakdown state when this query started. */ private boolean breakdown = false; /** * The search chain registry current when this execution was created, or * when the registry was first accessed, or null if it was not set on * creation or has been accessed yet. No setter method is intentional. */ private SearchChainRegistry searchChainRegistry = null; private IndexFacts indexFacts = null; /** The current set of special tokens */ private SpecialTokenRegistry tokenRegistry = null; /** The current template registry */ private RendererRegistry rendererRegistry = null; /** The current linguistics */ private Linguistics linguistics = null; /** Always set if this context belongs to an execution, never set if it does not. */ private final Execution owner; // Please don't add more constructors to the public interface of Context // unless the constructor is reasonably safe for an inexperienced user // in a production setting. Since queries blow up in a spectacular // fashion if Context is in a bad state, the Context() constructor is // package private. /** Create a context used to carry state into another context */ Context() { this.owner=null; } /** Create a context which belongs to an execution */ Context(Execution owner) { this.owner=owner; } /** * Creates a context from arguments, all of which may be null, though * this can be risky. If you are doing this outside a test, it is * usually better to do something like execution.context().shallowCopy() * instead, and then set the fields you need to change. It is also safe * to use the context from the incoming execution directly. In other * words, a plug-in writer should practically never construct a Context * instance directly. *

* This context is never attached to an execution but is used to carry state into * another context. */ public Context(SearchChainRegistry searchChainRegistry, IndexFacts indexFacts, SpecialTokenRegistry tokenRegistry, RendererRegistry rendererRegistry, Linguistics linguistics) { owner=null; // The next time something is added here, compose into wrapper objects. Many arguments... // Four methods need to be updated when adding something: // fill(Context), populateFrom(Context), equals(Context) and, // obviously, the most complete constructor. this.searchChainRegistry = searchChainRegistry; this.indexFacts = indexFacts; this.tokenRegistry = tokenRegistry; this.rendererRegistry = rendererRegistry; this.linguistics = linguistics; } /** Creates a context stub with no information. This is for unit testing. */ public static Context createContextStub() { return new Context(null, null, null, null, null); } /** * Create a Context instance where only the index related settings are * initialized. This is for unit testing. */ public static Context createContextStub(IndexFacts indexFacts) { return new Context(null, indexFacts, null, null, null); } /** * Create a Context instance where only the search chain registry and index facts are * initialized. This is for unit testing. */ public static Context createContextStub(SearchChainRegistry searchChainRegistry, IndexFacts indexFacts) { return new Context(searchChainRegistry, indexFacts, null, null, null); } /** * Create a Context instance where only the search chain registry, index facts and linguistics are * initialized. This is for unit testing. */ public static Context createContextStub(SearchChainRegistry searchChainRegistry, IndexFacts indexFacts, Linguistics linguistics) { return new Context(searchChainRegistry, indexFacts, null, null, linguistics); } /** * Populate missing values in this from the given context. * Values which are non-null in this will not be overwritten. * * @param sourceContext the context from which to get the parameters */ public void populateFrom(Context sourceContext) { // breakdown and detailedDiagnostics has no unset state, so they are always copied detailedDiagnostics = sourceContext.detailedDiagnostics; breakdown = sourceContext.breakdown; if (indexFacts == null) { indexFacts = sourceContext.indexFacts; } if (tokenRegistry == null) { tokenRegistry = sourceContext.tokenRegistry; } if (searchChainRegistry == null) { searchChainRegistry = sourceContext.searchChainRegistry; } if (rendererRegistry == null) { rendererRegistry = sourceContext.rendererRegistry; } if (linguistics == null) { linguistics = sourceContext.linguistics; } } /** * The brutal version of populateFrom(). * * @param other a Context instance this will copy all state from */ void fill(Context other) { searchChainRegistry = other.searchChainRegistry; indexFacts = other.indexFacts; tokenRegistry = other.tokenRegistry; rendererRegistry = other.rendererRegistry; detailedDiagnostics = other.detailedDiagnostics; breakdown = other.breakdown; linguistics = other.linguistics; } public boolean equals(Context other) { // equals() needs to be cheap, that's yet another reason we can only // allow immutables and frozen objects in the context return other.indexFacts == indexFacts && other.rendererRegistry == rendererRegistry && other.tokenRegistry == tokenRegistry && other.searchChainRegistry == searchChainRegistry && other.detailedDiagnostics == detailedDiagnostics && other.breakdown == breakdown && other.linguistics == linguistics; } @Override public int hashCode() { return java.util.Objects.hash(indexFacts, rendererRegistry, tokenRegistry, searchChainRegistry, detailedDiagnostics, breakdown, linguistics); } @Override public boolean equals(Object other) { if (other == null) { return false; } if (other.getClass() != Context.class) { return false; } else { return equals((Context) other); } } /** * Standard shallow copy, the new instance will carry the same * references as this. * * @return a new instance which is a shallow copy */ public Context shallowCopy() { Context c = new Context(); c.fill(this); return c; } /** * This is used when building the Context stack. If Context has been * changed since last time, build a new object. Otherwise simply return * the previous snapshot. * * @param previous another Context instance to compare with * @return a copy of this, or previous */ Context copyIfChanged(Context previous) { if (equals(previous)) { return previous; } else { return shallowCopy(); } } /** * Returns information about the indexes specified by the search definitions * used in this system, or null if not know. */ // TODO: Make a null default instance public IndexFacts getIndexFacts() { return indexFacts; } /** * Use this to override index settings for the searchers below * a given searcher, the easiest way to do this is to wrap the incoming * IndexFacts instance in a subclass. E.g. * execution.context().setIndexFacts(new WrapperClass(execution.context().getIndexFacts())). * * @param indexFacts * an instance to override the following searcher's view of * the indexes. */ public void setIndexFacts(IndexFacts indexFacts) { this.indexFacts = indexFacts; } /** * Returns the search chain registry to use with this execution. This is * a snapshot taken at creation of this execution, use * Context.shallowCopy() to get a correctly instantiated Context if * making a custom Context instance. */ public SearchChainRegistry searchChainRegistry() { return searchChainRegistry; } /** * Returns the template registry to use with this execution. This is * a snapshot taken at creation of this execution. */ public RendererRegistry rendererRegistry() { return rendererRegistry; } /** * @return the current set of special strings for the query tokenizer */ public SpecialTokenRegistry getTokenRegistry() { return tokenRegistry; } /** * Wrapping the incoming special token registry and then setting the * wrapper as the token registry, can be used for changing the set of * special tokens used by succeeding searchers. E.g. * execution.context().setTokenRegistry(new WrapperClass(execution.context().getTokenRegistry())). * * @param tokenRegistry a new registry for overriding behavior of following searchers */ public void setTokenRegistry(SpecialTokenRegistry tokenRegistry) { this.tokenRegistry = tokenRegistry; } public void setDetailedDiagnostics(boolean breakdown) { this.detailedDiagnostics = breakdown; } /** * The container has some internal diagnostics mechanisms which may be * costly, and therefore not active by default. Any general diagnostic * mechanism which should not be active be default, may inspect that * state here. If breakdown is assumed, a certain percentage of queries * will have this set automatically. * * @return whether components exposing different level of diagnostics * should go for the most detailed level */ public boolean getDetailedDiagnostics() { return detailedDiagnostics; } /** * If too many queries time out, the search handler will assume the * system is in a breakdown state. This state is propagated here. * * @return whether the system is assumed to be in a breakdown state */ public boolean getBreakdown() { return breakdown; } public void setBreakdown(boolean breakdown) { this.breakdown = breakdown; } /** * Returns the {@link Linguistics} object assigned to this Context. This object provides access to all the * linguistic-related APIs, and comes pre-configured with the Execution given. * * @return The current Linguistics. */ public Linguistics getLinguistics() { return linguistics; } public void setLinguistics(Linguistics linguistics) { this.linguistics = linguistics; } /** Creates a child trace if this has an owner, or a root trace otherwise */ private Trace createChildTrace() { return owner!=null ? owner.trace().createChild() : Trace.createRoot(0); } /** Creates a child environment if this has an owner, or a root environment otherwise */ private Environment createChildEnvironment() { return owner!=null ? owner.environment().nested() : Execution.Environment.createEmpty(); } } /** * The index of where in the chain this Execution has its initial entry point. * This is needed because executions can be started from the middle of other executions. */ private final int entryIndex; /** Time spent in each state of filling, searching or pinging. */ private final TimeTracker timer; /** A searcher's view of state external to the search chain. */ // Note that the context plays the same role as the Environment of the super.Execution // (although complicated by the need for stack-like behavior on changes). // We might want to unify those at some point. private final Context context = new Context(this); /** * Array for hiding context changes done in search by searcher following * another. */ private final Context[] contextCache; /** *

* Creates an execution from another. This execution will start at the * current next searcher in the given execution, rather than at the * start. *

* *

* The relevant state of the given execution is copied before this method * returns - the argument execution can then be reused for any other * purpose. *

*/ public Execution(Execution execution) { this(execution.chain(), execution.context, execution.nextIndex()); } /** Creates an which executes nothing */ public Execution(Context context) { this(new Chain<>(), context); } /** * The usually best way of creating a new execution for a search chain. This * is the one suitable for a production environment. It is safe to use the * incoming context from the search directly: * *
     * public Result search(Query query, Execution execution) {
     *     SearchChain searchChain = fancyChainSelectionRoutine(query);
     *     if (searchChain != null) {
     *         return new Execution(searchChain, execution.context());
     *     else {
     *         return execution.search(query);
     *     }
     * }
     * 
* * @param searchChain * the search chain to execute * @param context * the execution context from which this is populated (the given * context is not changed nor retained by this), or null to not * populate from a context * @throws IllegalArgumentException * if searchChain is null */ public Execution(Chain searchChain, Context context) { this(searchChain, context, 0); } /** Creates an execution from a single searcher */ public Execution(Searcher searcher, Context context) { this(new Chain<>(searcher), context, 0); } /** * Creates a new execution for a search chain or a single searcher. private * to ensure only searchChain or searcher is null (and because it's long and * cumbersome). * * @param searchChain * the search chain to execute, must be null if searcher is set * @param context * execution context for the search * @param searcherIndex * index of the first searcher to invoke, see * Execution(Execution) * @throws IllegalArgumentException * if searchChain is null */ @SuppressWarnings("unchecked") private Execution(Chain searchChain, Context context, int searcherIndex) { // Create a new Execution which is placed in the context of the execution of the given Context if any // "if any" because a context may, or may not, belong to an execution. // This is decided at the creation time of the Context - Context instances which do not belong // to an execution plays the role of data carriers between executions. super(searchChain,searcherIndex,context.createChildTrace(),context.createChildEnvironment()); this.context.fill(context); contextCache = new Context[searchChain.components().size()]; entryIndex=searcherIndex; timer = new TimeTracker(searchChain, searcherIndex); } /** Does return search(((Query)request) */ @Override public final Response process(Request request) { return search((Query)request); } /** Calls search on the next searcher in this chain. If there is no next, an empty result is returned. */ public Result search(Query query) { timer.sampleSearch(nextIndex(), context.getDetailedDiagnostics()); // Transfer state between query and execution as the execution constructors does not do that completely query.getModel().setExecution(this); trace().setTraceLevel(query.getTraceLevel()); return (Result)super.process(query); } @Override protected void onInvoking(Request request, Processor processor) { super.onInvoking(request,processor); final int traceDependencies = 6; Query query = (Query) request; if (query.getTraceLevel() >= traceDependencies) { query.trace(processor.getId() + " " + processor.getDependencies(), traceDependencies); } } /** * The default response returned from this kind of execution when there are not further processors * - an empty Result */ @Override protected Response defaultResponse(Request request) { return new Result((Query)request); } /** * Fill hit properties with values from all in-memory attributes. * This can be done with good performance on many more hits than * those for which fill is called with the final summary class, so * if filtering can be done using only in-memory attribute data, * this method should be preferred over {@link #fill} to get that data for filtering. *

* Calling this on already filled results has no cost. * * @param result the result to fill */ @SuppressWarnings("deprecation") public void fillAttributes(Result result) { fill(result, ATTRIBUTEPREFETCH); } /** * Fill hit properties with data using the default summary * class, possibly overridden with the 'summary' request parameter. *

* Fill must be called before any property (accessed by * getProperty/getField) is accessed on the hit. It should be done * as late as possible for performance reasons. *

* Calling this on already filled results has no cost. * * @param result the result to fill */ public void fill(Result result) { fill(result, result.getQuery().getPresentation().getSummary()); } /** Calls fill on the next searcher in this chain. If there is no next, nothing is done. */ public void fill(Result result, String summaryClass) { timer.sampleFill(nextIndex(), context.getDetailedDiagnostics()); Searcher current = (Searcher)next(); // TODO: Allow but skip processors which are not searchers if (current == null) return; try { nextProcessor(); onInvokingFill(current, summaryClass); current.ensureFilled(result, summaryClass, this); } finally { previousProcessor(); onReturningFill(current, result, summaryClass); timer.sampleFillReturn(nextIndex(), context.getDetailedDiagnostics(), result); } } private void onInvokingFill(Searcher searcher, String summaryClass) { int traceFillAt = 5; if (trace().getTraceLevel() < traceFillAt) return; trace().trace("Invoke fill(" + summaryClass + ") on " + searcher, traceFillAt); } private void onReturningFill(Searcher searcher, Result result, String summaryClass) { int traceFillAt = 5; if (trace().getTraceLevel() < traceFillAt) return; trace().trace("Return fill(" + summaryClass + ") on " + searcher, traceFillAt); } /** Calls ping on the next search in this chain. If there is no next, a Pong is created and returned. */ public Pong ping(Ping ping) { // return this reference, not directly. It's needed for adding time data Pong annotationReference = null; timer.samplePing(nextIndex(), context.getDetailedDiagnostics()); Searcher next = (Searcher)next(); // TODO: Allow but skip processors which are not searchers if (next == null) { annotationReference = new Pong(); return annotationReference; } try { nextProcessor(); annotationReference = invokePing(ping, next); return annotationReference; } finally { previousProcessor(); timer.samplePingReturn(nextIndex(), context.getDetailedDiagnostics(), annotationReference); } } @Override protected void onReturning(Request request, Processor processor,Response response) { super.onReturning(request, processor, response); timer.sampleSearchReturn(nextIndex(), context.getDetailedDiagnostics(), (Result)response); } @Override protected void previousProcessor() { super.previousProcessor(); popContext(); } @Override protected void nextProcessor() { pushContext(); super.nextProcessor(); } private void popContext() { context.fill(contextCache[nextIndex()]); contextCache[nextIndex()] = null; } private void pushContext() { final Context contextToPush; // Do note: Never put this.context in the cache. It would be totally // meaningless, since it's a final. if (nextIndex() == entryIndex) { contextToPush = context.shallowCopy(); } else { contextToPush = context.copyIfChanged(contextCache[nextIndex() - 1]); } contextCache[nextIndex()] = contextToPush; } private Pong invokePing(Ping ping, Searcher next) { Pong annotationReference; if (next instanceof PingableSearcher) { annotationReference = ((PingableSearcher) next).ping(ping, this); } else { annotationReference = ping(ping); } return annotationReference; } /** * Returns the search chain registry to use with this execution. This is a * snapshot taken at creation of this execution if available. */ public SearchChainRegistry searchChainRegistry() { return context.searchChainRegistry(); } /** * Returns the context of this execution, which contains various objects * which are looked up through a memory barrier at the point this is created * and which is guaranteed to be frozen during the execution of this query. *

* Note that the context itself can be changed. Such changes will be visible * to downstream searchers, but not after returning from the modifying * searcher. In other words, a change in the context will not be visible to * the preceding searchers when the result is returned from the searcher * which modified the context. */ public Context context() { return context; } /** * @return the TimeTracker instance associated with this Execution */ public TimeTracker timer() { return timer; } }