2
* Licensed to the Apache Software Foundation (ASF) under one or more
3
* contributor license agreements. See the NOTICE file distributed with
4
* this work for additional information regarding copyright ownership.
5
* The ASF licenses this file to You under the Apache License, Version 2.0
6
* (the "License"); you may not use this file except in compliance with
7
* the License. You may obtain a copy of the License at
9
* http://www.apache.org/licenses/LICENSE-2.0
11
* Unless required by applicable law or agreed to in writing, software
12
* distributed under the License is distributed on an "AS IS" BASIS,
13
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
* See the License for the specific language governing permissions and
15
* limitations under the License.
18
package org.apache.solr.search;
20
import org.apache.lucene.analysis.SimpleAnalyzer;
21
import org.apache.lucene.document.Document;
22
import org.apache.lucene.document.Field;
23
import org.apache.lucene.index.IndexReader;
24
import org.apache.lucene.index.IndexReader;
25
import org.apache.lucene.index.IndexWriter;
26
import org.apache.lucene.index.IndexWriterConfig;
27
import org.apache.lucene.search.*;
28
import org.apache.lucene.search.SortField;
29
import org.apache.lucene.store.Directory;
30
import org.apache.lucene.store.RAMDirectory;
31
import org.apache.lucene.util.OpenBitSet;
32
import org.apache.lucene.util._TestUtil;
34
import org.apache.solr.request.SolrQueryRequest;
36
import org.apache.solr.SolrTestCaseJ4;
38
import org.junit.BeforeClass;
40
import java.io.IOException;
43
public class TestSort extends SolrTestCaseJ4 {
45
public static void beforeClass() throws Exception {
46
initCore("solrconfig.xml","schema-minimal.xml");
49
final Random r = random;
57
public String toString() {
58
return "{id=" +doc + " val1="+val + " val2="+val2 + "}";
62
public void testRandomFieldNameSorts() throws Exception {
63
SolrQueryRequest req = lrf.makeRequest("q", "*:*");
65
final int iters = atLeast(5000);
66
int numberOfOddities = 0;
68
for (int i = 0; i < iters; i++) {
69
final StringBuilder input = new StringBuilder();
70
final String[] names = new String[_TestUtil.nextInt(r,1,10)];
71
final boolean[] reverse = new boolean[names.length];
72
for (int j = 0; j < names.length; j++) {
73
names[j] = _TestUtil.randomRealisticUnicodeString(r, 1, 20);
75
// reduce the likelyhood that the random str is a valid query or func
76
names[j] = names[j].replaceFirst("\\{","\\{\\{");
77
names[j] = names[j].replaceFirst("\\(","\\(\\(");
78
names[j] = names[j].replaceFirst("(\\\"|\\')","$1$1");
79
names[j] = names[j].replaceFirst("(\\d)","$1x");
81
// eliminate pesky problem chars
82
names[j] = names[j].replaceAll("\\p{Cntrl}|\\p{javaWhitespace}","");
84
if (0 == names[j].length()) {
86
// screw it, i'm taking my toys and going home
87
names[j] = "last_ditch_i_give_up";
89
reverse[j] = r.nextBoolean();
91
input.append(r.nextBoolean() ? " " : "");
92
input.append(names[j]);
94
input.append(reverse[j] ? "desc," : "asc,");
96
input.deleteCharAt(input.length()-1);
97
SortField[] sorts = null;
99
sorts = QueryParsing.parseSort(input.toString(), req).getSort();
100
} catch (RuntimeException e) {
101
throw new RuntimeException("Failed to parse sort: " + input, e);
103
assertEquals("parsed sorts had unexpected size",
104
names.length, sorts.length);
105
for (int j = 0; j < names.length; j++) {
106
assertEquals("sorts["+j+"] had unexpected reverse: " + input,
107
reverse[j], sorts[j].getReverse());
109
final int type = sorts[j].getType();
111
if (SortField.SCORE == type) {
113
assertEquals("sorts["+j+"] is (unexpectedly) type score : " + input,
115
} else if (SortField.DOC == type) {
117
assertEquals("sorts["+j+"] is (unexpectedly) type doc : " + input,
118
"_docid_", names[j]);
119
} else if (SortField.CUSTOM == type) {
122
// our orig string better be parsable as a func/query
124
QParser.getParser(names[j], FunctionQParserPlugin.NAME, req);
126
Query q = qp.getQuery();
127
assertNotNull("sorts["+j+"] had type " + type +
128
" but parsed to null func/query: " + input, q);
129
} catch (Exception e) {
130
assertNull("sorts["+j+"] had type " + type +
131
" but errored parsing as func/query: " + input, e);
134
assertEquals("sorts["+j+"] had unexpected field: " + input,
135
names[j], sorts[j].getField());
140
assertTrue("Over 0.2% oddities in test: " +
141
numberOfOddities + "/" + iters +
142
" have func/query parsing semenatics gotten broader?",
143
numberOfOddities < 0.002 * iters);
148
public void testSort() throws Exception {
152
Directory dir = new RAMDirectory();
153
Field f = new Field("f","0", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS);
154
Field f2 = new Field("f2","0", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS);
156
for (int iterCnt = 0; iterCnt<iter; iterCnt++) {
157
int ndocs = random.nextInt(100)+1;
158
int maxval = random.nextInt(ndocs)+random.nextInt(ndocs)+1;
160
IndexWriter iw = new IndexWriter(
162
new IndexWriterConfig(TEST_VERSION_CURRENT, new SimpleAnalyzer(TEST_VERSION_CURRENT)).
163
setOpenMode(IndexWriterConfig.OpenMode.CREATE));
165
int v1EmptyPercent = random.nextInt(40)+random.nextInt(40);
166
int v2EmptyPercent = random.nextInt(40)+random.nextInt(40);
168
for (int i=0; i< ndocs; i++) {
169
Document document = new Document();
170
if (r.nextInt(100) < v1EmptyPercent) {
171
String val = Integer.toString(r.nextInt(maxval));
175
if (r.nextInt(100) < v2EmptyPercent) {
176
String val2 = Integer.toString(r.nextInt(maxval));
181
iw.addDocument(document);
182
// duplicate a certain percent
183
if (r.nextInt(100) < 10) {
184
iw.addDocument(document);
186
if (random.nextInt(100) < 10) {
193
IndexReader reader = IndexReader.open(dir);
194
IndexSearcher searcher = new IndexSearcher(reader);
197
int maxDoc = searcher.maxDoc();
198
final MyDoc[] mydocs = new MyDoc[maxDoc];
199
for (int i=0; i<maxDoc; i++) {
200
if (searcher.getIndexReader().isDeleted(i)) continue;
201
Document doc = searcher.doc(i);
202
MyDoc mydoc = new MyDoc();
205
mydoc.val = doc.get("f");
206
mydoc.val2 = doc.get("f2");
210
for (int i=0; i<qiter; i++) {
211
Filter filt = new Filter() {
213
public DocIdSet getDocIdSet(IndexReader reader) throws IOException {
214
return randSet(reader.maxDoc());
218
int top = r.nextInt((ndocs>>3)+1)+1;
219
final boolean luceneSort = r.nextBoolean();
220
final boolean sortMissingLast = !luceneSort && r.nextBoolean();
221
final boolean sortMissingFirst = !luceneSort && !sortMissingLast;
222
final boolean reverse = r.nextBoolean();
223
List<SortField> sfields = new ArrayList<SortField>();
225
final boolean secondary = r.nextBoolean();
226
final boolean luceneSort2 = r.nextBoolean();
227
final boolean sortMissingLast2 = !luceneSort2 && r.nextBoolean();
228
final boolean sortMissingFirst2 = !luceneSort2 && !sortMissingLast2;
229
final boolean reverse2 = r.nextBoolean();
231
if (r.nextBoolean()) sfields.add( new SortField(null, SortField.SCORE));
232
// hit both use-cases of sort-missing-last
233
sfields.add( Sorting.getStringSortField("f", reverse, sortMissingLast, sortMissingFirst) );
235
sfields.add( Sorting.getStringSortField("f2", reverse2, sortMissingLast2, sortMissingFirst2) );
237
if (r.nextBoolean()) sfields.add( new SortField(null, SortField.SCORE));
239
Sort sort = new Sort(sfields.toArray(new SortField[sfields.size()]));
241
final String nullRep = luceneSort || sortMissingFirst && !reverse || sortMissingLast && reverse ? "" : "zzz";
242
final String nullRep2 = luceneSort2 || sortMissingFirst2 && !reverse2 || sortMissingLast2 && reverse2 ? "" : "zzz";
244
boolean trackScores = r.nextBoolean();
245
boolean trackMaxScores = r.nextBoolean();
246
boolean scoreInOrder = r.nextBoolean();
247
final TopFieldCollector topCollector = TopFieldCollector.create(sort, top, true, trackScores, trackMaxScores, scoreInOrder);
249
final List<MyDoc> collectedDocs = new ArrayList<MyDoc>();
250
// delegate and collect docs ourselves
251
Collector myCollector = new Collector() {
255
public void setScorer(Scorer scorer) throws IOException {
256
topCollector.setScorer(scorer);
260
public void collect(int doc) throws IOException {
261
topCollector.collect(doc);
262
collectedDocs.add(mydocs[doc + docBase]);
266
public void setNextReader(IndexReader reader, int docBase) throws IOException {
267
topCollector.setNextReader(reader,docBase);
268
this.docBase = docBase;
272
public boolean acceptsDocsOutOfOrder() {
273
return topCollector.acceptsDocsOutOfOrder();
277
searcher.search(new MatchAllDocsQuery(), filt, myCollector);
279
Collections.sort(collectedDocs, new Comparator<MyDoc>() {
280
public int compare(MyDoc o1, MyDoc o2) {
281
String v1 = o1.val==null ? nullRep : o1.val;
282
String v2 = o2.val==null ? nullRep : o2.val;
283
int cmp = v1.compareTo(v2);
284
if (reverse) cmp = -cmp;
285
if (cmp != 0) return cmp;
288
v1 = o1.val2==null ? nullRep2 : o1.val2;
289
v2 = o2.val2==null ? nullRep2 : o2.val2;
290
cmp = v1.compareTo(v2);
291
if (reverse2) cmp = -cmp;
294
cmp = cmp==0 ? o1.doc-o2.doc : cmp;
300
TopDocs topDocs = topCollector.topDocs();
301
ScoreDoc[] sdocs = topDocs.scoreDocs;
302
for (int j=0; j<sdocs.length; j++) {
303
int id = sdocs[j].doc;
304
if (id != collectedDocs.get(j).doc) {
305
log.error("Error at pos " + j
306
+ "\n\tsortMissingFirst=" + sortMissingFirst + " sortMissingLast=" + sortMissingLast + " reverse=" + reverse
307
+ "\n\tsecondary="+secondary + "sortMissingFirst=" + sortMissingFirst2 + " sortMissingLast=" + sortMissingLast2 + " reverse=" + reverse2
308
+ "\n\tEXPECTED=" + collectedDocs
311
assertEquals(id, collectedDocs.get(j).doc);
321
public DocIdSet randSet(int sz) {
322
OpenBitSet obs = new OpenBitSet(sz);
323
int n = r.nextInt(sz);
324
for (int i=0; i<n; i++) {
325
obs.fastSet(r.nextInt(sz));