add --shared
[pylucene.git] / lucene-java-3.4.0 / lucene / backwards / src / test-framework / org / apache / lucene / search / CheckHits.java
1 package org.apache.lucene.search;
2
3 /**
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
10  *
11  *     http://www.apache.org/licenses/LICENSE-2.0
12  *
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.
18  */
19
20 import java.io.IOException;
21 import java.util.Set;
22 import java.util.TreeSet;
23 import java.util.Random;
24
25 import junit.framework.Assert;
26
27 import org.apache.lucene.index.IndexReader;
28 import org.apache.lucene.store.Directory;
29
30 public class CheckHits {
31   
32   /**
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
36    */
37   public static float EXPLAIN_SCORE_TOLERANCE_DELTA = 0.0002f;
38     
39   /**
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
43    */
44   public static void checkNoMatchExplanations(Query q, String defaultFieldName,
45                                               Searcher searcher, int[] results)
46     throws IOException {
47
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]));
52     }
53     
54     int maxDoc = searcher.maxDoc();
55     for (int doc = 0; doc < maxDoc; doc++) {
56       if (ignore.contains(Integer.valueOf(doc))) continue;
57
58       Explanation exp = searcher.explain(q, doc);
59       Assert.assertNotNull("Explanation of [["+d+"]] for #"+doc+" is null",
60                              exp);
61       Assert.assertFalse("Explanation of [["+d+"]] for #"+doc+
62                          " doesn't indicate non-match: " + exp.toString(),
63                          exp.isMatch());
64     }
65     
66   }
67   
68   /**
69    * Tests that a query matches the an expected set of documents using a
70    * HitCollector.
71    *
72    * <p>
73    * Note that when using the HitCollector API, documents will be collected
74    * if they "match" regardless of what their score is.
75    * </p>
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)
81    * @see #checkHits
82    */
83   public static void checkHitCollector(Random random, Query query, String defaultFieldName,
84                                        Searcher searcher, int[] results)
85     throws IOException {
86
87     QueryUtils.check(random,query,searcher);
88     
89     Set<Integer> correct = new TreeSet<Integer>();
90     for (int i = 0; i < results.length; i++) {
91       correct.add(Integer.valueOf(results[i]));
92     }
93     final Set<Integer> actual = new TreeSet<Integer>();
94     final Collector c = new SetCollector(actual);
95
96     searcher.search(query, c);
97     Assert.assertEquals("Simple: " + query.toString(defaultFieldName), 
98                         correct, actual);
99
100     for (int i = -1; i < 2; i++) {
101       actual.clear();
102       QueryUtils.wrapSearcher(random, searcher, i).search(query, c);
103       Assert.assertEquals("Wrap Searcher " + i + ": " +
104                           query.toString(defaultFieldName),
105                           correct, actual);
106     }
107                         
108     if ( ! ( searcher instanceof IndexSearcher ) ) return;
109
110     for (int i = -1; i < 2; i++) {
111       actual.clear();
112       QueryUtils.wrapUnderlyingReader
113         (random, (IndexSearcher)searcher, i).search(query, c);
114       Assert.assertEquals("Wrap Reader " + i + ": " +
115                           query.toString(defaultFieldName),
116                           correct, actual);
117     }
118   }
119
120   public static class SetCollector extends Collector {
121     final Set<Integer> bag;
122     public SetCollector(Set<Integer> bag) {
123       this.bag = bag;
124     }
125     private int base = 0;
126     @Override
127     public void setScorer(Scorer scorer) throws IOException {}
128     @Override
129     public void collect(int doc) {
130       bag.add(Integer.valueOf(doc + base));
131     }
132     @Override
133     public void setNextReader(IndexReader reader, int docBase) {
134       base = docBase;
135     }
136     @Override
137     public boolean acceptsDocsOutOfOrder() {
138       return true;
139     }
140   }
141
142   /**
143    * Tests that a query matches the an expected set of documents using Hits.
144    *
145    * <p>
146    * Note that when using the Hits API, documents will only be returned
147    * if they have a positive normalized score.
148    * </p>
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
155    */
156   public static void checkHits(
157         Random random,
158         Query query,
159         String defaultFieldName,
160         Searcher searcher,
161         int[] results)
162           throws IOException {
163
164     ScoreDoc[] hits = searcher.search(query, 1000).scoreDocs;
165
166     Set<Integer> correct = new TreeSet<Integer>();
167     for (int i = 0; i < results.length; i++) {
168       correct.add(Integer.valueOf(results[i]));
169     }
170
171     Set<Integer> actual = new TreeSet<Integer>();
172     for (int i = 0; i < hits.length; i++) {
173       actual.add(Integer.valueOf(hits[i].doc));
174     }
175
176     Assert.assertEquals(query.toString(defaultFieldName), correct, actual);
177
178     QueryUtils.check(random, query,searcher);
179   }
180
181   /** Tests that a Hits has an expected order of documents */
182   public static void checkDocIds(String mes, int[] results, ScoreDoc[] hits)
183   throws IOException {
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);
187     }
188   }
189
190   /** Tests that two queries have an expected order of documents,
191    * and that the two queries have the same score values.
192    */
193   public static void checkHitsQuery(
194         Query query,
195         ScoreDoc[] hits1,
196         ScoreDoc[] hits2,
197         int[] results)
198           throws IOException {
199
200     checkDocIds("hits1", results, hits1);
201     checkDocIds("hits2", results, hits2);
202     checkEqual(query, hits1, hits2);
203   }
204
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);
209      }
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());
215       }
216
217       if ((hits1[i].doc != hits2[i].doc)
218           || Math.abs(hits1[i].score -  hits2[i].score) > scoreTolerance)
219       {
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());
224       }
225     }
226   }
227
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;
232     if (end<=0) {
233       end = Math.max(len1,len2);
234     }
235
236       sb.append("Hits length1=").append(len1).append("\tlength2=").append(len2);
237
238     sb.append('\n');
239     for (int i=start; i<end; i++) {
240         sb.append("hit=").append(i).append(':');
241       if (i<len1) {
242           sb.append(" doc").append(hits1[i].doc).append('=').append(hits1[i].score);
243       } else {
244         sb.append("               ");
245       }
246       sb.append(",\t");
247       if (i<len2) {
248         sb.append(" doc").append(hits2[i].doc).append('=').append(hits2[i].score);
249       }
250       sb.append('\n');
251     }
252     return sb.toString();
253   }
254
255
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++) {
262       sb.append('\t');
263       sb.append(i);
264       sb.append(") doc=");
265       sb.append(docs.scoreDocs[i].doc);
266       sb.append("\tscore=");
267       sb.append(docs.scoreDocs[i].score);
268       sb.append('\n');
269     }
270     return sb.toString();
271   }
272
273   /**
274    * Asserts that the explanation value for every document matching a
275    * query corresponds with the true score. 
276    *
277    * @see ExplanationAsserter
278    * @see #checkExplanations(Query, String, Searcher, boolean) for a
279    * "deep" testing of the explanation details.
280    *   
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
284    */
285   public static void checkExplanations(Query query,
286                                        String defaultFieldName,
287                                        Searcher searcher) throws IOException {
288     checkExplanations(query, defaultFieldName, searcher, false);
289   }
290
291   /**
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.
295    *
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
301    */
302   public static void checkExplanations(Query query,
303                                        String defaultFieldName,
304                                        Searcher searcher, 
305                                        boolean deep) throws IOException {
306
307     searcher.search(query,
308                     new ExplanationAsserter
309                     (query, defaultFieldName, searcher, deep));
310
311   }
312
313   /** 
314    * Assert that an explanation has the expected score, and optionally that its
315    * sub-details max/sum/factor match to that score.
316    *
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
322    */
323   public static void verifyExplanation(String q, 
324                                        int doc, 
325                                        float score,
326                                        boolean deep,
327                                        Explanation expl) {
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);
332
333     if (!deep) return;
334
335     Explanation detail[] = expl.getDetails();
336     if (detail!=null) {
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]);
341       } else {
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).
345         float x = 0;
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 ");
354           if (k1>=0) {
355             k1 += "max plus ".length();
356             int k2 = descr.indexOf(" ",k1);
357             try {
358               x = Float.parseFloat(descr.substring(k1,k2).trim());
359               if (descr.substring(k2).trim().equals("times others of:")) {
360                 maxTimesOthers = true;
361               }
362             } catch (NumberFormatException e) {
363             }
364           }
365         }
366         Assert.assertTrue(
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);
371         float sum = 0;
372         float product = 1;
373         float max = 0;
374         for (int i=0; i<detail.length; i++) {
375           float dval = detail[i].getValue();
376           verifyExplanation(q,doc,dval,deep,detail[i]);
377           product *= dval;
378           sum += dval;
379           max = Math.max(max,dval);
380         }
381         float combined = 0;
382         if (productOf) {
383           combined = product;
384         } else if (sumOf) {
385           combined = sum;
386         } else if (maxOf) {
387           combined = max;
388         } else if (maxTimesOthers) {
389           combined = max + x * (sum - max);
390         } else {
391             Assert.assertTrue("should never get here!",false);
392         }
393         Assert.assertEquals(q+": actual subDetails combined=="+combined+
394             " != value="+value+" Explanation: "+expl,
395             combined,value,EXPLAIN_SCORE_TOLERANCE_DELTA);
396       }
397     }
398   }
399
400   /**
401    * an IndexSearcher that implicitly checks hte explanation of every match
402    * whenever it executes a search.
403    *
404    * @see ExplanationAsserter
405    */
406   public static class ExplanationAssertingSearcher extends IndexSearcher {
407     public ExplanationAssertingSearcher(Directory d) throws IOException {
408       super(d, true);
409     }
410     public ExplanationAssertingSearcher(IndexReader r) throws IOException {
411       super(r);
412     }
413     protected void checkExplanations(Query q) throws IOException {
414       super.search(q, null,
415                    new ExplanationAsserter
416                    (q, null, this));
417     }
418     @Override
419     public TopFieldDocs search(Query query,
420                                Filter filter,
421                                int n,
422                                Sort sort) throws IOException {
423       
424       checkExplanations(query);
425       return super.search(query,filter,n,sort);
426     }
427     @Override
428     public void search(Query query, Collector results) throws IOException {
429       checkExplanations(query);
430       super.search(query, results);
431     }
432     @Override
433     public void search(Query query, Filter filter, Collector results) throws IOException {
434       checkExplanations(query);
435       super.search(query, filter, results);
436     }
437     @Override
438     public TopDocs search(Query query, Filter filter,
439                           int n) throws IOException {
440
441       checkExplanations(query);
442       return super.search(query,filter, n);
443     }
444   }
445     
446   /**
447    * Asserts that the score explanation for every document matching a
448    * query corresponds with the true score.
449    *
450    * NOTE: this HitCollector should only be used with the Query and Searcher
451    * specified at when it is constructed.
452    *
453    * @see CheckHits#verifyExplanation
454    */
455   public static class ExplanationAsserter extends Collector {
456
457     /**
458      * @deprecated
459      * @see CheckHits#EXPLAIN_SCORE_TOLERANCE_DELTA
460      */
461     @Deprecated
462     public static float SCORE_TOLERANCE_DELTA = 0.00005f;
463
464     Query q;
465     Searcher s;
466     String d;
467     boolean deep;
468     
469     Scorer scorer;
470     private int base = 0;
471
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);
475     }      
476     public ExplanationAsserter(Query q, String defaultFieldName, Searcher s, boolean deep) {
477       this.q=q;
478       this.s=s;
479       this.d = q.toString(defaultFieldName);
480       this.deep=deep;
481     }      
482     
483     @Override
484     public void setScorer(Scorer scorer) throws IOException {
485       this.scorer = scorer;     
486     }
487     
488     @Override
489     public void collect(int doc) throws IOException {
490       Explanation exp = null;
491       doc = doc + base;
492       try {
493         exp = s.explain(q, doc);
494       } catch (IOException e) {
495         throw new RuntimeException
496           ("exception in hitcollector of [["+d+"]] for #"+doc, e);
497       }
498       
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(), 
503                         exp.isMatch());
504     }
505     @Override
506     public void setNextReader(IndexReader reader, int docBase) {
507       base = docBase;
508     }
509     @Override
510     public boolean acceptsDocsOutOfOrder() {
511       return true;
512     }
513   }
514
515 }
516
517