// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search; import com.yahoo.collections.ListMap; import com.yahoo.net.URI; import com.yahoo.protect.Validator; import com.yahoo.search.query.context.QueryContext; import com.yahoo.search.result.Coverage; import com.yahoo.search.result.ErrorMessage; import com.yahoo.search.result.Hit; import com.yahoo.search.result.HitGroup; import com.yahoo.search.result.HitOrderer; import com.yahoo.search.result.HitSortOrderer; import com.yahoo.search.statistics.ElapsedTime; import java.util.Iterator; /** * The Result contains all the data produced by executing a Query: Some very limited global information, and * a single HitGroup containing hits of the result. The HitGroup may contain Hits, which are the individual * result items, as well as further HitGroups, making up a composite structure. This allows the hits of a result * to be hierarchically organized. A Hit is polymorphic and may contain any kind of information deemed * an approriate partial answer to the Query. *

* Do not cache this as it holds references to objects that should be garbage collected. * * @author bratseth */ public final class Result extends com.yahoo.processing.Response implements Cloneable { // Note to developers: If you think you should add something here you are probably wrong // To add some new kind of data, create a Hit subclass carrying the data and add that instead /** The top level hit group of this result */ private HitGroup hits; /** The estimated total number of hits which would in theory be displayed this result is a part of */ private long totalHitCount; /** * The estimated total number of deep hits, which includes every object which matches the query. * This is always at least the same as totalHitCount. A lower value will cause hitCount to be returned. */ private long deepHitCount; /** The time spent producing this result */ private ElapsedTime timeAccountant = new ElapsedTime(); /** Coverage information for this result. */ private Coverage coverage = null; /** * Headers containing "envelope" meta information to be returned with this result. * Used for HTTP getHeaders when the return protocol is HTTP. */ private ListMap headers = null; /** Creates a new Result where the top level hit group has id "toplevel" */ public Result(Query query) { this(query, new HitGroup("toplevel")); } /** * Create an empty result. * A source creating a result is required to call * {@link #setTotalHitCount} before releasing this result. * * @param query the query which produced this result * @param hits the hit container which this will return from {@link #hits()} */ public Result(Query query, HitGroup hits) { super(query); if (query == null) throw new NullPointerException("The query reference in a result cannot be null"); this.hits = hits; hits.setQuery(query); if (query.getRanking().getSorting() != null) { setHitOrderer(new HitSortOrderer(query.getRanking().getSorting())); } } /** Create a result containing an error */ public Result(Query query, ErrorMessage errorMessage) { this(query); hits.addError(errorMessage); } /** * Merges meta information from a result into this. * This does not merge hits, but the other information associated * with a result. It should always be called when adding * hits from a result, but there is no constraints on the order of the calls. */ public void mergeWith(Result result) { totalHitCount += result.getTotalHitCount(); deepHitCount += result.getDeepHitCount(); timeAccountant.merge(result.getElapsedTime()); boolean create = true; if (result.getCoverage(!create) != null || getCoverage(!create) != null) getCoverage(create).merge(result.getCoverage(create)); } /** * Merges meta information produced when a Hit already * contained in this result has been filled using another * result as an intermediary. @see mergeWith(Result) mergeWith. */ public void mergeWithAfterFill(Result result) { timeAccountant.merge(result.getElapsedTime()); } /** * Returns the number of hit objects available in the top level group of this result. * Note that this number is allowed to be higher than the requested number * of hits, because a searcher is allowed to add meta hits as well * as the requested number of concrete hits. */ public int getHitCount() { return hits.size(); } /** *

Returns the total number of concrete hits contained (directly or in subgroups) in this result. * This should equal the requested hits count if the query has that many matches.

*/ public int getConcreteHitCount() { return hits.getConcreteSize(); } /** * Returns the estimated total number of concrete hits which would be returned for this query. */ public long getTotalHitCount() { return totalHitCount; } /** * Returns the estimated total number of deep hits, which includes every object which matches the query. * This is always at least the same as totalHitCount. A lower value will cause hitCount to be returned. */ public long getDeepHitCount() { if (deepHitCountSets the hit orderer to be used for the top level hit group.

* * @param hitOrderer the new hit orderer, or null to use default relevancy ordering */ public void setHitOrderer(HitOrderer hitOrderer) { hits.setOrderer(hitOrderer); } /** Returns the orderer used by the top level group, or null if the default relevancy order is used */ public HitOrderer getHitOrderer() { return hits.getOrderer(); } public void setDeletionBreaksOrdering(boolean flag) { hits.setDeletionBreaksOrdering(flag); } public boolean getDeletionBreaksOrdering() { return hits.getDeletionBreaksOrdering(); } /** Update cached and filled by iterating through the hits of this result */ public void analyzeHits() { hits.analyze(); } /** Returns the top level hit group containing all the hits of this result */ public HitGroup hits() { return hits; } @Override public com.yahoo.processing.response.DataList data() { return hits; } /** Sets the top level hit group containing all the hits of this result */ public void setHits(HitGroup hits) { Validator.ensureNotNull("The top-level hit group of " + this,hits); this.hits=hits; } /** * Deep clones this result - copies are made of all hits and subgroups of hits, * but not of the query referenced by this. */ public Result clone() { Result resultClone = (Result) super.clone(); resultClone.hits = hits.clone(); resultClone.setElapsedTime(new ElapsedTime()); return resultClone; } public String toString() { if (hits.getError() != null) { return "Result: " + hits.getErrorHit().errors().iterator().next(); } else { return "Result (" + getConcreteHitCount() + " of total " + getTotalHitCount() + " hits)"; } } /** * Adds a context message to this query containing the entire content of this result, * if tracelevel is 5 or more. * * @param name the name of the searcher instance returning this result */ public void trace(String name) { if (hits().getQuery().getTrace().getLevel() < 5) { return; } StringBuilder hitBuffer = new StringBuilder(name); hitBuffer.append(" returns:\n"); int counter = 0; for (Iterator i = hits.unorderedIterator(); i.hasNext();) { Hit hit = i.next(); if (hit.isMeta()) continue; hitBuffer.append(" #: "); hitBuffer.append(counter); traceExtraHitProperties(hitBuffer, hit); hitBuffer.append(", relevancy: "); hitBuffer.append(hit.getRelevance()); hitBuffer.append(", source: "); hitBuffer.append(hit.getSource()); hitBuffer.append(", uri: "); URI uri = hit.getId(); if (uri != null) { hitBuffer.append(uri.getHost()); } else { hitBuffer.append("(no uri)"); } hitBuffer.append("\n"); counter++; } if (counter == 0) { hitBuffer.append("(no hits)\n"); } hits.getQuery().trace(hitBuffer.toString(), false, 5); } /** * For tracing custom properties of a hit, see trace(String). An example of * using this is in com.yahoo.prelude.Result. * * @param hitBuffer * the render target * @param hit * the hit to be analyzed */ protected void traceExtraHitProperties(StringBuilder hitBuffer, Hit hit) { } /** Returns the context of this result - this is equal to getQuery().getContext(create) */ public QueryContext getContext(boolean create) { return getQuery().getContext(create); } public void setCoverage(Coverage coverage) { this.coverage = coverage; } /** * Returns coverage information * * @param create if true the coverage information of this result is created if missing * @return the coverage information of this, or null if none and create is false */ public Coverage getCoverage(boolean create) { if (coverage == null && create) coverage = new Coverage(0L, 0, 0, (hits().size() == 0 ? 0 : 1)); return coverage; } /** * Returns the set of "envelope" headers to be returned with this result. * This returns the live map in modifiable form - modify this to change the * headers. Or null if none, and it should not be created. *

* Used for HTTP headers when the return protocol is HTTP, e.g *

result.getHeaders(true).put("Cache-Control","max-age=120")
* * @param create if true, create the header ListMap if it does not exist * @return returns the ListMap of current headers, or null if no headers are set and
create
is false */ public ListMap getHeaders(boolean create) { if (headers == null && create) headers = new ListMap<>(); return headers; } }