blob: 58b190152bd1d4fc72c0aee01a36b14ae7e92a6b [file] [log] [blame]
/*
* 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.
*/
package org.apache.solr.search;
import java.io.IOException;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.Rescorer;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopDocsCollector;
import org.apache.lucene.search.TotalHits;
import org.apache.lucene.search.Weight;
import org.apache.solr.handler.component.MergeStrategy;
import org.apache.solr.SolrTestCaseJ4;
import org.junit.Before;
import org.junit.BeforeClass;
public class SolrIndexSearcherTest extends SolrTestCaseJ4 {
private final static int NUM_DOCS = 200;
@BeforeClass
public static void setUpClass() throws Exception {
initCore("solrconfig.xml", "schema.xml");
for (int i = 0 ; i < NUM_DOCS ; i ++) {
assertU(adoc("id", String.valueOf(i),
"field1_s", "foo",
"field2_s", String.valueOf(i % 2),
"field3_i_dvo", String.valueOf(i),
"field4_t", numbersTo(i)));
}
assertU(commit());
}
private static String numbersTo(int i) {
StringBuilder numbers = new StringBuilder();
for (int j = 0; j <= i ; j++) {
numbers.append(String.valueOf(j) + " ");
}
return numbers.toString();
}
@Before
public void setUp() throws Exception {
assertU(adoc("id", "1",
"field1_s", "foo",
"field2_s", "1",
"field3_i_dvo", "1",
"field4_t", numbersTo(1)));
assertU(commit());
super.setUp();
}
public void testMinExactCountLongValue() {
assertQ("test query on empty index",
req("q", "field1_s:foo",
"minExactCount", Long.toString(10L * Integer.MAX_VALUE),
"rows", "2")
,"//*[@numFoundExact='true']"
,"//*[@numFound='" + NUM_DOCS + "']"
);
}
public void testMinExactCount() {
assertQ("minExactCount is lower than numFound,should produce approximated results",
req("q", "field1_s:foo",
"minExactCount", "2",
"rows", "2")
,"//*[@numFoundExact='false']"
,"//*[@numFound<='" + NUM_DOCS + "']"
);
assertQ("minExactCount is higher than numFound,should produce exact results",
req("q", "field1_s:foo",
"minExactCount", "200",
"rows", "2")
,"//*[@numFoundExact='true']"
,"//*[@numFound='" + NUM_DOCS + "']"
);
}
private void assertMatchesEqual(int expectedCount, SolrIndexSearcher searcher, QueryCommand cmd) throws IOException {
QueryResult qr = new QueryResult();
searcher.search(qr, cmd);
assertEquals(expectedCount, qr.getDocList().matches());
assertEquals(TotalHits.Relation.EQUAL_TO, qr.getDocList().hitCountRelation());
}
private QueryResult assertMatchesGreaterThan(int expectedCount, SolrIndexSearcher searcher, QueryCommand cmd) throws IOException {
QueryResult qr = new QueryResult();
searcher.search(qr, cmd);
assertTrue("Expecting returned matches to be greater than " + expectedCount + " but got " + qr.getDocList().matches(),
expectedCount >= qr.getDocList().matches());
assertEquals(TotalHits.Relation.GREATER_THAN_OR_EQUAL_TO, qr.getDocList().hitCountRelation());
return qr;
}
public void testLowMinExactCountGeneratesApproximation() throws IOException {
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(NUM_DOCS / 2, 10, "field1_s", "foo");
assertMatchesGreaterThan(NUM_DOCS, searcher, cmd);
return null;
});
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(1, 1, "field2_s", "1");
assertMatchesGreaterThan(NUM_DOCS/2, searcher, cmd);
return null;
});
}
public void testHighMinExactCountGeneratesExactCount() throws IOException {
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(NUM_DOCS, 10, "field1_s", "foo");
assertMatchesEqual(NUM_DOCS, searcher, cmd);
return null;
});
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(NUM_DOCS, 10, "field2_s", "1");
assertMatchesEqual(NUM_DOCS/2, searcher, cmd);
return null;
});
}
public void testLowMinExactCountWithQueryResultCache() throws IOException {
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(NUM_DOCS / 2, 10, "field1_s", "foo");
cmd.clearFlags(SolrIndexSearcher.NO_CHECK_QCACHE | SolrIndexSearcher.NO_SET_QCACHE);
searcher.search(new QueryResult(), cmd);
assertMatchesGreaterThan(NUM_DOCS, searcher, cmd);
return null;
});
}
public void testHighMinExactCountWithQueryResultCache() throws IOException {
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(NUM_DOCS, 2, "field1_s", "foo");
cmd.clearFlags(SolrIndexSearcher.NO_CHECK_QCACHE | SolrIndexSearcher.NO_SET_QCACHE);
searcher.search(new QueryResult(), cmd);
assertMatchesEqual(NUM_DOCS, searcher, cmd);
return null;
});
}
public void testMinExactCountMoreRows() throws IOException {
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(2, NUM_DOCS, "field1_s", "foo");
assertMatchesEqual(NUM_DOCS, searcher, cmd);
return null;
});
}
public void testMinExactCountMatchWithDocSet() throws IOException {
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(2, 2, "field1_s", "foo");
assertMatchesGreaterThan(NUM_DOCS, searcher, cmd);
cmd.setNeedDocSet(true);
assertMatchesEqual(NUM_DOCS, searcher, cmd);
return null;
});
}
public void testMinExactCountWithMaxScoreRequested() throws IOException {
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(2, 2, "field1_s", "foo");
cmd.setFlags(SolrIndexSearcher.GET_SCORES);
QueryResult qr = assertMatchesGreaterThan(NUM_DOCS, searcher, cmd);
assertNotEquals(Float.NaN, qr.getDocList().maxScore());
return null;
});
}
public void testReranking() throws Exception {
float fixedScore = 1.23f;
for (boolean doFilter : new boolean[]{ false, true }) {
for (boolean doSort : new boolean[]{ false, true }) {
for (int getDocSetFlag : new int[]{ 0, SolrIndexSearcher.GET_DOCSET }) {
implTestReranking(doFilter, doSort, getDocSetFlag, null); // don't fix score i.e. no re-ranking
implTestReranking(doFilter, doSort, getDocSetFlag, fixedScore); // fix score to be non-zero and non-one
fixedScore *= 2;
}
}
}
}
private void implTestReranking(boolean doFilter, boolean doSort, int getDocSetFlag, Float fixedScore) throws Exception {
h.getCore().withSearcher(searcher -> {
final QueryCommand cmd = new QueryCommand();
cmd.setFlags(SolrIndexSearcher.GET_SCORES | getDocSetFlag);
if (doSort) {
cmd.setSort(new Sort(SortField.FIELD_SCORE, new SortField("id", SortField.Type.STRING)));
}
if (doFilter) {
cmd.setFilterList(new TermQuery(new Term("field4_t", Integer.toString(NUM_DOCS - 1))));
}
cmd.setQuery(new TermQuery(new Term("field1_s", "foo")));
final float expectedScore;
if (fixedScore == null) {
expectedScore = 1f;
} else {
expectedScore = fixedScore.floatValue();
cmd.setQuery(new FixedScoreReRankQuery(cmd.getQuery(), expectedScore));
}
final QueryResult qr = new QueryResult();
searcher.search(qr, cmd);
// check score for the first document
final DocIterator iter = qr.getDocList().iterator();
iter.next();
assertEquals(expectedScore, iter.score(), 0);
return null;
});
}
private static final class FixedScoreReRankQuery extends RankQuery {
private Query q;
final private float fixedScore;
public FixedScoreReRankQuery(Query q, float fixedScore) {
this.q = q;
this.fixedScore = fixedScore;
}
public Weight createWeight(IndexSearcher indexSearcher, ScoreMode scoreMode, float boost) throws IOException {
return q.createWeight(indexSearcher, scoreMode, boost);
}
@Override
public void visit(QueryVisitor visitor) {
q.visit(visitor);
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return q.hashCode();
}
@Override
public String toString(String field) {
return q.toString(field);
}
@Override
@SuppressWarnings({"rawtypes"})
public TopDocsCollector getTopDocsCollector(int len, QueryCommand cmd, IndexSearcher searcher) throws IOException {
return new ReRankCollector(len, len, new Rescorer() {
@Override
public TopDocs rescore(IndexSearcher searcher, TopDocs firstPassTopDocs, int topN) {
for (ScoreDoc scoreDoc : firstPassTopDocs.scoreDocs) {
scoreDoc.score = fixedScore;
}
return firstPassTopDocs;
}
@Override
public Explanation explain(IndexSearcher searcher, Explanation firstPassExplanation, int docID) {
return firstPassExplanation;
}
}, cmd, searcher, null);
}
@Override
public MergeStrategy getMergeStrategy() {
return null;
}
public RankQuery wrap(Query q) {
this.q = q;
return this;
}
}
public void testMinExactWithFilters() throws Exception {
h.getCore().withSearcher(searcher -> {
//Sanity Check - No Filter
QueryCommand cmd = createBasicQueryCommand(1, 1, "field4_t", "0");
assertMatchesGreaterThan(NUM_DOCS, searcher, cmd);
return null;
});
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(1, 1, "field4_t", "0");
Query filterQuery = new TermQuery(new Term("field4_t", Integer.toString(NUM_DOCS - 1)));
cmd.setFilterList(filterQuery);
assertNull(searcher.getProcessedFilter(null, cmd.getFilterList()).postFilter);
assertMatchesEqual(1, searcher, cmd);
return null;
});
}
public void testMinExactWithPostFilters() throws Exception {
h.getCore().withSearcher(searcher -> {
//Sanity Check - No Filter
QueryCommand cmd = createBasicQueryCommand(1, 1, "field4_t", "0");
assertMatchesGreaterThan(NUM_DOCS, searcher, cmd);
return null;
});
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(1, 1, "field4_t", "0");
MockPostFilter filterQuery = new MockPostFilter(1, 101);
cmd.setFilterList(filterQuery);
assertNotNull(searcher.getProcessedFilter(null, cmd.getFilterList()).postFilter);
assertMatchesEqual(1, searcher, cmd);
return null;
});
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(1, 1, "field4_t", "0");
MockPostFilter filterQuery = new MockPostFilter(100, 101);
cmd.setFilterList(filterQuery);
assertNotNull(searcher.getProcessedFilter(null, cmd.getFilterList()).postFilter);
assertMatchesGreaterThan(NUM_DOCS, searcher, cmd);
return null;
});
}
public void testMinExactWithPostFilterThatChangesScoreMode() throws Exception {
h.getCore().withSearcher(searcher -> {
QueryCommand cmd = createBasicQueryCommand(1, 1, "field4_t", "0");
// Use ScoreMode.COMPLETE for the PostFilter
MockPostFilter filterQuery = new MockPostFilter(NUM_DOCS * 10, 101, ScoreMode.COMPLETE);
cmd.setFilterList(filterQuery);
assertNotNull(searcher.getProcessedFilter(null, cmd.getFilterList()).postFilter);
assertMatchesEqual(NUM_DOCS, searcher, cmd);
return null;
});
}
private QueryCommand createBasicQueryCommand(int minExactCount, int length, String field, String q) {
QueryCommand cmd = new QueryCommand();
cmd.setMinExactCount(minExactCount);
cmd.setLen(length);
cmd.setFlags(SolrIndexSearcher.NO_CHECK_QCACHE | SolrIndexSearcher.NO_SET_QCACHE);
cmd.setQuery(new TermQuery(new Term(field, q)));
return cmd;
}
private final static class MockPostFilter extends TermQuery implements PostFilter {
private final int cost;
private final int maxDocsToCollect;
private final ScoreMode scoreMode;
public MockPostFilter(int maxDocsToCollect, int cost, ScoreMode scoreMode) {
super(new Term("foo", "bar"));//The term won't really be used. just the collector
assert cost > 100;
this.cost = cost;
this.maxDocsToCollect = maxDocsToCollect;
this.scoreMode = scoreMode;
}
public MockPostFilter(int maxDocsToCollect, int cost) {
this(maxDocsToCollect, cost, null);
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
throw new UnsupportedOperationException("This class is only intended to be used as a PostFilter");
}
@Override
public boolean getCache() {
return false;
}
@Override
public void setCache(boolean cache) {}
@Override
public int getCost() {
return cost;
}
@Override
public void setCost(int cost) {}
@Override
public boolean getCacheSep() {
return false;
}
@Override
public void setCacheSep(boolean cacheSep) {
}
@Override
public DelegatingCollector getFilterCollector(IndexSearcher searcher) {
return new DelegatingCollector() {
private int collected = 0;
@Override
public void collect(int doc) throws IOException {
if (++collected <= maxDocsToCollect) {
super.collect(doc);
}
}
@Override
public ScoreMode scoreMode() {
if (scoreMode != null) {
return scoreMode;
}
return super.scoreMode();
}
};
}
}
}