| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| using System; |
| using Lucene.Net.Support; |
| using NUnit.Framework; |
| |
| using IndexReader = Lucene.Net.Index.IndexReader; |
| using Directory = Lucene.Net.Store.Directory; |
| using Single = Lucene.Net.Support.Single; |
| |
| namespace Lucene.Net.Search |
| { |
| |
| public class CheckHits |
| { |
| |
| /// <summary> Some explains methods calculate their values though a slightly |
| /// different order of operations from the actual scoring method ... |
| /// this allows for a small amount of variation |
| /// |
| /// mgarski - Java uses a value of 0.00005f, which causes tests in TestCustomScoreQuery |
| /// to fail when comparing the scorer and explanation scores with the wrapped |
| /// indexes and query boosts of greater than 2.0. A comparison of the Java and .Net |
| /// explanations reveal a floating point precision issue, not a hidden defect. |
| /// </summary> |
| public static float EXPLAIN_SCORE_TOLERANCE_DELTA = 0.00025f; // {{See: LUCENENET-288}} Intentional diversion from Java Lucene per above comment |
| |
| /// <summary> Tests that all documents up to maxDoc which are *not* in the |
| /// expected result set, have an explanation which indicates no match |
| /// (ie: Explanation value of 0.0f) |
| /// </summary> |
| public static void CheckNoMatchExplanations(Query q, System.String defaultFieldName, Searcher searcher, int[] results) |
| { |
| |
| System.String d = q.ToString(defaultFieldName); |
| System.Collections.Hashtable ignore = new System.Collections.Hashtable(); |
| for (int i = 0; i < results.Length; i++) |
| { |
| CollectionsHelper.AddIfNotContains(ignore, (System.Int32) results[i]); |
| } |
| |
| int maxDoc = searcher.MaxDoc; |
| for (int doc = 0; doc < maxDoc; doc++) |
| { |
| if (ignore.Contains((System.Int32) doc)) |
| continue; |
| |
| Explanation exp = searcher.Explain(q, doc); |
| Assert.IsNotNull(exp, "Explanation of [[" + d + "]] for #" + doc + " is null"); |
| Assert.AreEqual(0.0f, exp.Value, 0.0f, "Explanation of [[" + d + "]] for #" + doc + " doesn't indicate non-match: " + exp.ToString()); |
| } |
| } |
| |
| /// <summary> Tests that a query matches the an expected set of documents using a |
| /// HitCollector. |
| /// |
| /// <p/> |
| /// Note that when using the HitCollector API, documents will be collected |
| /// if they "match" regardless of what their score is. |
| /// <p/> |
| /// </summary> |
| /// <param name="query">the query to test |
| /// </param> |
| /// <param name="searcher">the searcher to test the query against |
| /// </param> |
| /// <param name="defaultFieldName">used for displaying the query in assertion messages |
| /// </param> |
| /// <param name="results">a list of documentIds that must match the query |
| /// </param> |
| /// <seealso cref="Searcher.Search(Query,HitCollector)"> |
| /// </seealso> |
| /// <seealso cref="checkHits"> |
| /// </seealso> |
| public static void CheckHitCollector(Query query, System.String defaultFieldName, Searcher searcher, int[] results) |
| { |
| |
| QueryUtils.Check(query, searcher); |
| |
| System.Collections.Hashtable correct = new System.Collections.Hashtable(); |
| for (int i = 0; i < results.Length; i++) |
| { |
| CollectionsHelper.AddIfNotContains(correct, (System.Int32) results[i]); |
| } |
| System.Collections.Hashtable actual = new System.Collections.Hashtable(); |
| Collector c = new SetCollector(actual); |
| |
| searcher.Search(query, c); |
| Assert.AreEqual(correct, actual, "Simple: " + query.ToString(defaultFieldName)); |
| |
| for (int i = - 1; i < 2; i++) |
| { |
| actual.Clear(); |
| QueryUtils.WrapSearcher(searcher, i).Search(query, c); |
| Assert.AreEqual(correct, actual, "Wrap Searcher " + i + ": " + query.ToString(defaultFieldName)); |
| } |
| |
| if (!(searcher is IndexSearcher)) |
| return ; |
| |
| for (int i = - 1; i < 2; i++) |
| { |
| actual.Clear(); |
| QueryUtils.WrapUnderlyingReader((IndexSearcher) searcher, i).Search(query, c); |
| Assert.AreEqual(correct, actual, "Wrap Reader " + i + ": " + query.ToString(defaultFieldName)); |
| } |
| } |
| |
| public class SetCollector:Collector |
| { |
| internal System.Collections.Hashtable bag; |
| public SetCollector(System.Collections.Hashtable bag) |
| { |
| this.bag = bag; |
| } |
| private int base_Renamed = 0; |
| public override void SetScorer(Scorer scorer) |
| { |
| } |
| public override void Collect(int doc) |
| { |
| CollectionsHelper.AddIfNotContains(bag, (System.Int32)(doc + base_Renamed)); |
| } |
| public override void SetNextReader(IndexReader reader, int docBase) |
| { |
| base_Renamed = docBase; |
| } |
| |
| public override bool AcceptsDocsOutOfOrder |
| { |
| get { return true; } |
| } |
| } |
| |
| /// <summary> Tests that a query matches the an expected set of documents using Hits. |
| /// |
| /// <p/> |
| /// Note that when using the Hits API, documents will only be returned |
| /// if they have a positive normalized score. |
| /// <p/> |
| /// </summary> |
| /// <param name="query">the query to test |
| /// </param> |
| /// <param name="searcher">the searcher to test the query against |
| /// </param> |
| /// <param name="defaultFieldName">used for displaing the query in assertion messages |
| /// </param> |
| /// <param name="results">a list of documentIds that must match the query |
| /// </param> |
| /// <seealso cref="Searcher.Search(Query)"> |
| /// </seealso> |
| /// <seealso cref="checkHitCollector"> |
| /// </seealso> |
| public static void CheckHits_Renamed_Method(Query query, System.String defaultFieldName, Searcher searcher, int[] results) |
| { |
| if (searcher is IndexSearcher) |
| { |
| QueryUtils.Check(query, searcher); |
| } |
| |
| ScoreDoc[] hits = searcher.Search(query, null, 1000).ScoreDocs; |
| |
| System.Collections.ArrayList correct = new System.Collections.ArrayList(); |
| for (int i = 0; i < results.Length; i++) |
| { |
| CollectionsHelper.AddIfNotContains(correct, results[i]); |
| } |
| correct.Sort(); |
| |
| System.Collections.ArrayList actual = new System.Collections.ArrayList(); |
| for (int i = 0; i < hits.Length; i++) |
| { |
| CollectionsHelper.AddIfNotContains(actual, hits[i].Doc); |
| } |
| actual.Sort(); |
| |
| Assert.AreEqual(correct, actual, query.ToString(defaultFieldName)); |
| |
| QueryUtils.Check(query, searcher); |
| } |
| |
| /// <summary>Tests that a Hits has an expected order of documents </summary> |
| public static void CheckDocIds(System.String mes, int[] results, ScoreDoc[] hits) |
| { |
| Assert.AreEqual(hits.Length, results.Length, mes + " nr of hits"); |
| for (int i = 0; i < results.Length; i++) |
| { |
| Assert.AreEqual(results[i], hits[i].Doc, mes + " doc nrs for hit " + i); |
| } |
| } |
| |
| /// <summary>Tests that two queries have an expected order of documents, |
| /// and that the two queries have the same score values. |
| /// </summary> |
| public static void CheckHitsQuery(Query query, ScoreDoc[] hits1, ScoreDoc[] hits2, int[] results) |
| { |
| |
| CheckDocIds("hits1", results, hits1); |
| CheckDocIds("hits2", results, hits2); |
| CheckEqual(query, hits1, hits2); |
| } |
| |
| public static void CheckEqual(Query query, ScoreDoc[] hits1, ScoreDoc[] hits2) |
| { |
| float scoreTolerance = 1.0e-6f; |
| Assert.IsTrue(hits1.Length == hits2.Length, "Unequal lengths: hits1=" + hits1.Length + ",hits2=" + hits2.Length); |
| |
| for (int i = 0; i < hits1.Length; i++) |
| { |
| Assert.IsTrue(hits1[i].Doc == hits2[i].Doc, |
| "Hit " + i + " docnumbers don't match\n" + Hits2str(hits1, hits2, 0, 0) + "for query:" + |
| query); |
| Assert.IsFalse( |
| (hits1[i].Doc != hits2[i].Doc) || System.Math.Abs(hits1[i].Score - hits2[i].Score) > scoreTolerance, |
| "Hit " + i + ", doc nrs " + hits1[i].Doc + " and " + hits2[i].Doc + "\nunequal : " + |
| hits1[i].Score + "\n and: " + hits2[i].Score + "\nfor query:" + query); |
| } |
| } |
| |
| public static System.String Hits2str(ScoreDoc[] hits1, ScoreDoc[] hits2, int start, int end) |
| { |
| System.Text.StringBuilder sb = new System.Text.StringBuilder(); |
| int len1 = hits1 == null?0:hits1.Length; |
| int len2 = hits2 == null?0:hits2.Length; |
| if (end <= 0) |
| { |
| end = System.Math.Max(len1, len2); |
| } |
| |
| sb.Append("Hits length1=").Append(len1).Append("\tlength2=").Append(len2); |
| |
| sb.Append('\n'); |
| for (int i = start; i < end; i++) |
| { |
| sb.Append("hit=").Append(i).Append(':'); |
| if (i < len1) |
| { |
| sb.Append(" doc").Append(hits1[i].Doc).Append('=').Append(hits1[i].Score); |
| } |
| else |
| { |
| sb.Append(" "); |
| } |
| sb.Append(",\t"); |
| if (i < len2) |
| { |
| sb.Append(" doc").Append(hits2[i].Doc).Append('=').Append(hits2[i].Score); |
| } |
| sb.Append('\n'); |
| } |
| return sb.ToString(); |
| } |
| |
| |
| public static System.String TopdocsString(TopDocs docs, int start, int end) |
| { |
| System.Text.StringBuilder sb = new System.Text.StringBuilder(); |
| sb.Append("TopDocs totalHits=").Append(docs.TotalHits).Append(" top=").Append(docs.ScoreDocs.Length).Append('\n'); |
| if (end <= 0) |
| end = docs.ScoreDocs.Length; |
| else |
| end = System.Math.Min(end, docs.ScoreDocs.Length); |
| for (int i = start; i < end; i++) |
| { |
| sb.Append('\t'); |
| sb.Append(i); |
| sb.Append(") doc="); |
| sb.Append(docs.ScoreDocs[i].Doc); |
| sb.Append("\tscore="); |
| sb.Append(docs.ScoreDocs[i].Score); |
| sb.Append('\n'); |
| } |
| return sb.ToString(); |
| } |
| |
| /// <summary> Asserts that the explanation value for every document matching a |
| /// query corresponds with the true score. |
| /// |
| /// </summary> |
| /// <seealso cref="ExplanationAsserter"> |
| /// </seealso> |
| /// <seealso cref="CheckExplanations(Query, String, Searcher, boolean) for a"> |
| /// "deep" testing of the explanation details. |
| /// |
| /// </seealso> |
| /// <param name="query">the query to test |
| /// </param> |
| /// <param name="searcher">the searcher to test the query against |
| /// </param> |
| /// <param name="defaultFieldName">used for displaing the query in assertion messages |
| /// </param> |
| public static void CheckExplanations(Query query, System.String defaultFieldName, Searcher searcher) |
| { |
| CheckExplanations(query, defaultFieldName, searcher, false); |
| } |
| |
| /// <summary> Asserts that the explanation value for every document matching a |
| /// query corresponds with the true score. Optionally does "deep" |
| /// testing of the explanation details. |
| /// |
| /// </summary> |
| /// <seealso cref="ExplanationAsserter"> |
| /// </seealso> |
| /// <param name="query">the query to test |
| /// </param> |
| /// <param name="searcher">the searcher to test the query against |
| /// </param> |
| /// <param name="defaultFieldName">used for displaing the query in assertion messages |
| /// </param> |
| /// <param name="deep">indicates whether a deep comparison of sub-Explanation details should be executed |
| /// </param> |
| public static void CheckExplanations(Query query, System.String defaultFieldName, Searcher searcher, bool deep) |
| { |
| |
| searcher.Search(query, new ExplanationAsserter(query, defaultFieldName, searcher, deep)); |
| } |
| |
| /// <summary> Assert that an explanation has the expected score, and optionally that its |
| /// sub-details max/sum/factor match to that score. |
| /// |
| /// </summary> |
| /// <param name="q">String representation of the query for assertion messages |
| /// </param> |
| /// <param name="doc">Document ID for assertion messages |
| /// </param> |
| /// <param name="score">Real score value of doc with query q |
| /// </param> |
| /// <param name="deep">indicates whether a deep comparison of sub-Explanation details should be executed |
| /// </param> |
| /// <param name="expl">The Explanation to match against score |
| /// </param> |
| public static void VerifyExplanation(System.String q, int doc, float score, bool deep, Explanation expl) |
| { |
| float value_Renamed = expl.Value; |
| Assert.AreEqual(score, value_Renamed, EXPLAIN_SCORE_TOLERANCE_DELTA, q + ": score(doc=" + doc + ")=" + score + " != explanationScore=" + value_Renamed + " Explanation: " + expl); |
| |
| if (!deep) |
| return ; |
| |
| Explanation[] detail = expl.GetDetails(); |
| if (detail != null) |
| { |
| if (detail.Length == 1) |
| { |
| // simple containment, no matter what the description says, |
| // just verify contained expl has same score |
| VerifyExplanation(q, doc, score, deep, detail[0]); |
| } |
| else |
| { |
| // explanation must either: |
| // - end with one of: "product of:", "sum of:", "max of:", or |
| // - have "max plus <x> times others" (where <x> is float). |
| float x = 0; |
| System.String descr = expl.Description.ToLower(); |
| bool productOf = descr.EndsWith("product of:"); |
| bool sumOf = descr.EndsWith("sum of:"); |
| bool maxOf = descr.EndsWith("max of:"); |
| bool maxTimesOthers = false; |
| if (!(productOf || sumOf || maxOf)) |
| { |
| // maybe 'max plus x times others' |
| int k1 = descr.IndexOf("max plus "); |
| if (k1 >= 0) |
| { |
| k1 += "max plus ".Length; |
| int k2 = descr.IndexOf(" ", k1); |
| try |
| { |
| x = Single.Parse(descr.Substring(k1, (k2) - (k1)).Trim()); |
| if (descr.Substring(k2).Trim().Equals("times others of:")) |
| { |
| maxTimesOthers = true; |
| } |
| } |
| catch (System.FormatException) |
| { |
| } |
| } |
| } |
| Assert.IsTrue(productOf || sumOf || maxOf || maxTimesOthers, q + ": multi valued explanation description=\"" + descr + "\" must be 'max of plus x times others' or end with 'product of'" + " or 'sum of:' or 'max of:' - " + expl); |
| float sum = 0; |
| float product = 1; |
| float max = 0; |
| for (int i = 0; i < detail.Length; i++) |
| { |
| float dval = detail[i].Value; |
| VerifyExplanation(q, doc, dval, deep, detail[i]); |
| product *= dval; |
| sum += dval; |
| max = System.Math.Max(max, dval); |
| } |
| float combined = 0; |
| if (productOf) |
| { |
| combined = product; |
| } |
| else if (sumOf) |
| { |
| combined = sum; |
| } |
| else if (maxOf) |
| { |
| combined = max; |
| } |
| else if (maxTimesOthers) |
| { |
| combined = max + x * (sum - max); |
| } |
| else |
| { |
| Assert.IsTrue(false, "should never get here!"); |
| } |
| Assert.AreEqual(combined, value_Renamed, EXPLAIN_SCORE_TOLERANCE_DELTA, q + ": actual subDetails combined==" + combined + " != value=" + value_Renamed + " Explanation: " + expl); |
| } |
| } |
| } |
| |
| /// <summary> an IndexSearcher that implicitly checks hte explanation of every match |
| /// whenever it executes a search. |
| /// |
| /// </summary> |
| /// <seealso cref="ExplanationAsserter"> |
| /// </seealso> |
| public class ExplanationAssertingSearcher:IndexSearcher |
| { |
| public ExplanationAssertingSearcher(Directory d):base(d, true) |
| { |
| } |
| public ExplanationAssertingSearcher(IndexReader r):base(r) |
| { |
| } |
| protected internal virtual void CheckExplanations(Query q) |
| { |
| base.Search(q, null, new ExplanationAsserter(q, null, this)); |
| } |
| public override TopFieldDocs Search(Query query, Filter filter, int n, Sort sort) |
| { |
| |
| CheckExplanations(query); |
| return base.Search(query, filter, n, sort); |
| } |
| public override void Search(Query query, Collector results) |
| { |
| CheckExplanations(query); |
| base.Search(query, results); |
| } |
| public override void Search(Query query, Filter filter, Collector results) |
| { |
| CheckExplanations(query); |
| base.Search(query, filter, results); |
| } |
| public override TopDocs Search(Query query, Filter filter, int n) |
| { |
| |
| CheckExplanations(query); |
| return base.Search(query, filter, n); |
| } |
| } |
| |
| /// <summary> Asserts that the score explanation for every document matching a |
| /// query corresponds with the true score. |
| /// |
| /// NOTE: this HitCollector should only be used with the Query and Searcher |
| /// specified at when it is constructed. |
| /// |
| /// </summary> |
| /// <seealso cref="CheckHits.verifyExplanation"> |
| /// </seealso> |
| public class ExplanationAsserter:Collector |
| { |
| |
| /// <deprecated> |
| /// </deprecated> |
| /// <seealso cref="CheckHits.EXPLAIN_SCORE_TOLERANCE_DELTA"> |
| /// </seealso> |
| public static float SCORE_TOLERANCE_DELTA = 0.00005f; |
| |
| internal Query q; |
| internal Searcher s; |
| internal System.String d; |
| internal bool deep; |
| |
| internal Scorer scorer; |
| private int base_Renamed = 0; |
| |
| /// <summary>Constructs an instance which does shallow tests on the Explanation </summary> |
| public ExplanationAsserter(Query q, System.String defaultFieldName, Searcher s):this(q, defaultFieldName, s, false) |
| { |
| } |
| public ExplanationAsserter(Query q, System.String defaultFieldName, Searcher s, bool deep) |
| { |
| this.q = q; |
| this.s = s; |
| this.d = q.ToString(defaultFieldName); |
| this.deep = deep; |
| } |
| |
| public override void SetScorer(Scorer scorer) |
| { |
| this.scorer = scorer; |
| } |
| |
| public override void Collect(int doc) |
| { |
| Explanation exp = null; |
| doc = doc + base_Renamed; |
| try |
| { |
| exp = s.Explain(q, doc); |
| } |
| catch (System.IO.IOException e) |
| { |
| throw new System.SystemException("exception in hitcollector of [[" + d + "]] for #" + doc, e); |
| } |
| |
| Assert.IsNotNull(exp, "Explanation of [[" + d + "]] for #" + doc + " is null"); |
| Lucene.Net.Search.CheckHits.VerifyExplanation(d, doc, scorer.Score(), deep, exp); |
| } |
| public override void SetNextReader(IndexReader reader, int docBase) |
| { |
| base_Renamed = docBase; |
| } |
| |
| public override bool AcceptsDocsOutOfOrder |
| { |
| get { return true; } |
| } |
| } |
| } |
| } |