1 package org.apache.lucene.search;
4 * Licensed to the Apache Software Foundation (ASF) under one or more
5 * contributor license agreements. See the NOTICE file distributed with
6 * this work for additional information regarding copyright ownership.
7 * The ASF licenses this file to You under the Apache License, Version 2.0
8 * (the "License"); you may not use this file except in compliance with
9 * the License. You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
20 import java.io.IOException;
22 import java.util.TreeSet;
23 import java.util.Random;
25 import junit.framework.Assert;
27 import org.apache.lucene.index.IndexReader;
28 import org.apache.lucene.store.Directory;
30 public class CheckHits {
33 * Some explains methods calculate their values though a slightly
34 * different order of operations from the actual scoring method ...
35 * this allows for a small amount of variation
37 public static float EXPLAIN_SCORE_TOLERANCE_DELTA = 0.0002f;
40 * Tests that all documents up to maxDoc which are *not* in the
41 * expected result set, have an explanation which indicates that
42 * the document does not match
44 public static void checkNoMatchExplanations(Query q, String defaultFieldName,
45 Searcher searcher, int[] results)
48 String d = q.toString(defaultFieldName);
49 Set<Integer> ignore = new TreeSet<Integer>();
50 for (int i = 0; i < results.length; i++) {
51 ignore.add(Integer.valueOf(results[i]));
54 int maxDoc = searcher.maxDoc();
55 for (int doc = 0; doc < maxDoc; doc++) {
56 if (ignore.contains(Integer.valueOf(doc))) continue;
58 Explanation exp = searcher.explain(q, doc);
59 Assert.assertNotNull("Explanation of [["+d+"]] for #"+doc+" is null",
61 Assert.assertFalse("Explanation of [["+d+"]] for #"+doc+
62 " doesn't indicate non-match: " + exp.toString(),
69 * Tests that a query matches the an expected set of documents using a
73 * Note that when using the HitCollector API, documents will be collected
74 * if they "match" regardless of what their score is.
76 * @param query the query to test
77 * @param searcher the searcher to test the query against
78 * @param defaultFieldName used for displaying the query in assertion messages
79 * @param results a list of documentIds that must match the query
80 * @see Searcher#search(Query,Collector)
83 public static void checkHitCollector(Random random, Query query, String defaultFieldName,
84 Searcher searcher, int[] results)
87 QueryUtils.check(random,query,searcher);
89 Set<Integer> correct = new TreeSet<Integer>();
90 for (int i = 0; i < results.length; i++) {
91 correct.add(Integer.valueOf(results[i]));
93 final Set<Integer> actual = new TreeSet<Integer>();
94 final Collector c = new SetCollector(actual);
96 searcher.search(query, c);
97 Assert.assertEquals("Simple: " + query.toString(defaultFieldName),
100 for (int i = -1; i < 2; i++) {
102 QueryUtils.wrapSearcher(random, searcher, i).search(query, c);
103 Assert.assertEquals("Wrap Searcher " + i + ": " +
104 query.toString(defaultFieldName),
108 if ( ! ( searcher instanceof IndexSearcher ) ) return;
110 for (int i = -1; i < 2; i++) {
112 QueryUtils.wrapUnderlyingReader
113 (random, (IndexSearcher)searcher, i).search(query, c);
114 Assert.assertEquals("Wrap Reader " + i + ": " +
115 query.toString(defaultFieldName),
120 public static class SetCollector extends Collector {
121 final Set<Integer> bag;
122 public SetCollector(Set<Integer> bag) {
125 private int base = 0;
127 public void setScorer(Scorer scorer) throws IOException {}
129 public void collect(int doc) {
130 bag.add(Integer.valueOf(doc + base));
133 public void setNextReader(IndexReader reader, int docBase) {
137 public boolean acceptsDocsOutOfOrder() {
143 * Tests that a query matches the an expected set of documents using Hits.
146 * Note that when using the Hits API, documents will only be returned
147 * if they have a positive normalized score.
149 * @param query the query to test
150 * @param searcher the searcher to test the query against
151 * @param defaultFieldName used for displaing the query in assertion messages
152 * @param results a list of documentIds that must match the query
153 * @see Searcher#search(Query, int)
154 * @see #checkHitCollector
156 public static void checkHits(
159 String defaultFieldName,
164 ScoreDoc[] hits = searcher.search(query, 1000).scoreDocs;
166 Set<Integer> correct = new TreeSet<Integer>();
167 for (int i = 0; i < results.length; i++) {
168 correct.add(Integer.valueOf(results[i]));
171 Set<Integer> actual = new TreeSet<Integer>();
172 for (int i = 0; i < hits.length; i++) {
173 actual.add(Integer.valueOf(hits[i].doc));
176 Assert.assertEquals(query.toString(defaultFieldName), correct, actual);
178 QueryUtils.check(random, query,searcher);
181 /** Tests that a Hits has an expected order of documents */
182 public static void checkDocIds(String mes, int[] results, ScoreDoc[] hits)
184 Assert.assertEquals(mes + " nr of hits", hits.length, results.length);
185 for (int i = 0; i < results.length; i++) {
186 Assert.assertEquals(mes + " doc nrs for hit " + i, results[i], hits[i].doc);
190 /** Tests that two queries have an expected order of documents,
191 * and that the two queries have the same score values.
193 public static void checkHitsQuery(
200 checkDocIds("hits1", results, hits1);
201 checkDocIds("hits2", results, hits2);
202 checkEqual(query, hits1, hits2);
205 public static void checkEqual(Query query, ScoreDoc[] hits1, ScoreDoc[] hits2) throws IOException {
206 final float scoreTolerance = 1.0e-6f;
207 if (hits1.length != hits2.length) {
208 Assert.fail("Unequal lengths: hits1="+hits1.length+",hits2="+hits2.length);
210 for (int i = 0; i < hits1.length; i++) {
211 if (hits1[i].doc != hits2[i].doc) {
212 Assert.fail("Hit " + i + " docnumbers don't match\n"
213 + hits2str(hits1, hits2,0,0)
214 + "for query:" + query.toString());
217 if ((hits1[i].doc != hits2[i].doc)
218 || Math.abs(hits1[i].score - hits2[i].score) > scoreTolerance)
220 Assert.fail("Hit " + i + ", doc nrs " + hits1[i].doc + " and " + hits2[i].doc
221 + "\nunequal : " + hits1[i].score
222 + "\n and: " + hits2[i].score
223 + "\nfor query:" + query.toString());
228 public static String hits2str(ScoreDoc[] hits1, ScoreDoc[] hits2, int start, int end) throws IOException {
229 StringBuilder sb = new StringBuilder();
230 int len1=hits1==null ? 0 : hits1.length;
231 int len2=hits2==null ? 0 : hits2.length;
233 end = Math.max(len1,len2);
236 sb.append("Hits length1=").append(len1).append("\tlength2=").append(len2);
239 for (int i=start; i<end; i++) {
240 sb.append("hit=").append(i).append(':');
242 sb.append(" doc").append(hits1[i].doc).append('=').append(hits1[i].score);
248 sb.append(" doc").append(hits2[i].doc).append('=').append(hits2[i].score);
252 return sb.toString();
256 public static String topdocsString(TopDocs docs, int start, int end) {
257 StringBuilder sb = new StringBuilder();
258 sb.append("TopDocs totalHits=").append(docs.totalHits).append(" top=").append(docs.scoreDocs.length).append('\n');
259 if (end<=0) end=docs.scoreDocs.length;
260 else end=Math.min(end,docs.scoreDocs.length);
261 for (int i=start; i<end; i++) {
265 sb.append(docs.scoreDocs[i].doc);
266 sb.append("\tscore=");
267 sb.append(docs.scoreDocs[i].score);
270 return sb.toString();
274 * Asserts that the explanation value for every document matching a
275 * query corresponds with the true score.
277 * @see ExplanationAsserter
278 * @see #checkExplanations(Query, String, Searcher, boolean) for a
279 * "deep" testing of the explanation details.
281 * @param query the query to test
282 * @param searcher the searcher to test the query against
283 * @param defaultFieldName used for displaing the query in assertion messages
285 public static void checkExplanations(Query query,
286 String defaultFieldName,
287 Searcher searcher) throws IOException {
288 checkExplanations(query, defaultFieldName, searcher, false);
292 * Asserts that the explanation value for every document matching a
293 * query corresponds with the true score. Optionally does "deep"
294 * testing of the explanation details.
296 * @see ExplanationAsserter
297 * @param query the query to test
298 * @param searcher the searcher to test the query against
299 * @param defaultFieldName used for displaing the query in assertion messages
300 * @param deep indicates whether a deep comparison of sub-Explanation details should be executed
302 public static void checkExplanations(Query query,
303 String defaultFieldName,
305 boolean deep) throws IOException {
307 searcher.search(query,
308 new ExplanationAsserter
309 (query, defaultFieldName, searcher, deep));
314 * Assert that an explanation has the expected score, and optionally that its
315 * sub-details max/sum/factor match to that score.
317 * @param q String representation of the query for assertion messages
318 * @param doc Document ID for assertion messages
319 * @param score Real score value of doc with query q
320 * @param deep indicates whether a deep comparison of sub-Explanation details should be executed
321 * @param expl The Explanation to match against score
323 public static void verifyExplanation(String q,
328 float value = expl.getValue();
329 Assert.assertEquals(q+": score(doc="+doc+")="+score+
330 " != explanationScore="+value+" Explanation: "+expl,
331 score,value,EXPLAIN_SCORE_TOLERANCE_DELTA);
335 Explanation detail[] = expl.getDetails();
337 if (detail.length==1) {
338 // simple containment, no matter what the description says,
339 // just verify contained expl has same score
340 verifyExplanation(q,doc,score,deep,detail[0]);
342 // explanation must either:
343 // - end with one of: "product of:", "sum of:", "max of:", or
344 // - have "max plus <x> times others" (where <x> is float).
346 String descr = expl.getDescription().toLowerCase();
347 boolean productOf = descr.endsWith("product of:");
348 boolean sumOf = descr.endsWith("sum of:");
349 boolean maxOf = descr.endsWith("max of:");
350 boolean maxTimesOthers = false;
351 if (!(productOf || sumOf || maxOf)) {
352 // maybe 'max plus x times others'
353 int k1 = descr.indexOf("max plus ");
355 k1 += "max plus ".length();
356 int k2 = descr.indexOf(" ",k1);
358 x = Float.parseFloat(descr.substring(k1,k2).trim());
359 if (descr.substring(k2).trim().equals("times others of:")) {
360 maxTimesOthers = true;
362 } catch (NumberFormatException e) {
367 q+": multi valued explanation description=\""+descr
368 +"\" must be 'max of plus x times others' or end with 'product of'"
369 +" or 'sum of:' or 'max of:' - "+expl,
370 productOf || sumOf || maxOf || maxTimesOthers);
374 for (int i=0; i<detail.length; i++) {
375 float dval = detail[i].getValue();
376 verifyExplanation(q,doc,dval,deep,detail[i]);
379 max = Math.max(max,dval);
388 } else if (maxTimesOthers) {
389 combined = max + x * (sum - max);
391 Assert.assertTrue("should never get here!",false);
393 Assert.assertEquals(q+": actual subDetails combined=="+combined+
394 " != value="+value+" Explanation: "+expl,
395 combined,value,EXPLAIN_SCORE_TOLERANCE_DELTA);
401 * an IndexSearcher that implicitly checks hte explanation of every match
402 * whenever it executes a search.
404 * @see ExplanationAsserter
406 public static class ExplanationAssertingSearcher extends IndexSearcher {
407 public ExplanationAssertingSearcher(Directory d) throws IOException {
410 public ExplanationAssertingSearcher(IndexReader r) throws IOException {
413 protected void checkExplanations(Query q) throws IOException {
414 super.search(q, null,
415 new ExplanationAsserter
419 public TopFieldDocs search(Query query,
422 Sort sort) throws IOException {
424 checkExplanations(query);
425 return super.search(query,filter,n,sort);
428 public void search(Query query, Collector results) throws IOException {
429 checkExplanations(query);
430 super.search(query, results);
433 public void search(Query query, Filter filter, Collector results) throws IOException {
434 checkExplanations(query);
435 super.search(query, filter, results);
438 public TopDocs search(Query query, Filter filter,
439 int n) throws IOException {
441 checkExplanations(query);
442 return super.search(query,filter, n);
447 * Asserts that the score explanation for every document matching a
448 * query corresponds with the true score.
450 * NOTE: this HitCollector should only be used with the Query and Searcher
451 * specified at when it is constructed.
453 * @see CheckHits#verifyExplanation
455 public static class ExplanationAsserter extends Collector {
459 * @see CheckHits#EXPLAIN_SCORE_TOLERANCE_DELTA
462 public static float SCORE_TOLERANCE_DELTA = 0.00005f;
470 private int base = 0;
472 /** Constructs an instance which does shallow tests on the Explanation */
473 public ExplanationAsserter(Query q, String defaultFieldName, Searcher s) {
474 this(q,defaultFieldName,s,false);
476 public ExplanationAsserter(Query q, String defaultFieldName, Searcher s, boolean deep) {
479 this.d = q.toString(defaultFieldName);
484 public void setScorer(Scorer scorer) throws IOException {
485 this.scorer = scorer;
489 public void collect(int doc) throws IOException {
490 Explanation exp = null;
493 exp = s.explain(q, doc);
494 } catch (IOException e) {
495 throw new RuntimeException
496 ("exception in hitcollector of [["+d+"]] for #"+doc, e);
499 Assert.assertNotNull("Explanation of [["+d+"]] for #"+doc+" is null", exp);
500 verifyExplanation(d,doc,scorer.score(),deep,exp);
501 Assert.assertTrue("Explanation of [["+d+"]] for #"+ doc +
502 " does not indicate match: " + exp.toString(),
506 public void setNextReader(IndexReader reader, int docBase) {
510 public boolean acceptsDocsOutOfOrder() {