aboutsummaryrefslogtreecommitdiffstats
path: root/container-search/src/main/java/com/yahoo/search/Result.java
blob: b1a0107c6d83e939422a8c5b00e56c2b6568073a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
// 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 <i>composite</i> 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.
 * <p>
 * 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 <i>deep</i> 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<String, String> 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 <b>required</b> 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 <b>meta information</b> from a result into this.
     * This does not merge hits, but the other information associated
     * with a result. It should <b>always</b> 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 <i>meta</i> hits as well
     * as the requested number of concrete hits.
     */
    public int getHitCount() {
        return hits.size();
    }

    /**
     * <p>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.</p>
     */
    public int getConcreteHitCount() {
        return hits.getConcreteSize();
    }

    /**
     * Returns the <b>estimated</b> total number of concrete hits which would be returned for this query.
     */
    public long getTotalHitCount() {
        return totalHitCount;
    }

    /**
     * Returns the estimated total number of <i>deep</i> 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 (deepHitCount<totalHitCount) return totalHitCount;
        return deepHitCount;
    }


    /** Sets the estimated total number of hits this result is a subset of */
    public void setTotalHitCount(long totalHitCount) {
        this.totalHitCount = totalHitCount;
    }

    /** Sets the estimated total number of deep hits this result is a subset of */
    public void setDeepHitCount(long deepHitCount) {
        this.deepHitCount = deepHitCount;
    }

    public ElapsedTime getElapsedTime() {
        return timeAccountant;
    }

    public void setElapsedTime(ElapsedTime t) {
        timeAccountant = t;
    }

    /**
     * Returns true only if _all_ hits in this result originates from a cache.
     */
    public boolean isCached() {
        return hits.isCached();
    }

    /**
     * Returns whether all hits in this result have been filled with
     * the properties contained in the given summary class. Note that
     * this method will also return true if no hits in this result are
     * fillable.
     */
    public boolean isFilled(String summaryClass) {
        return hits.isFilled(summaryClass);
    }

    /** Returns the query which produced this result */
    public Query getQuery() { return hits.getQuery(); }

    /** Sets a query for this result */
    public void setQuery(Query query) { hits.setQuery(query); }

    /**
     * <p>Sets the hit orderer to be used for the top level hit group.</p>
     *
     * @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,
     * <i>but not of the query referenced by this</i>.
     */
    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<Hit> 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.
     * <p>
     * Used for HTTP headers when the return protocol is HTTP, e.g
     * <pre>result.getHeaders(true).put("Cache-Control","max-age=120")</pre>
     *
     * @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 <pre>create</pre> is false
     */
    public ListMap<String, String> getHeaders(boolean create) {
        if (headers == null && create)
            headers = new ListMap<>();
        return headers;
    }
}