1 /** @file
2 * @brief tests of posting sources
3 */
4 /* Copyright 2008,2009,2011,2015,2016,2019 Olly Betts
5 * Copyright 2008,2009 Lemur Consulting Ltd
6 * Copyright 2010 Richard Boulton
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License as
10 * published by the Free Software Foundation; either version 2 of the
11 * License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
21 * USA
22 */
23
24 #include <config.h>
25
26 #include "api_postingsource.h"
27
28 #include <xapian.h>
29
30 #include <string>
31 #include "safeunistd.h"
32
33 #include "str.h"
34 #include "testutils.h"
35 #include "apitest.h"
36
37 using namespace std;
38
39 class MyOddPostingSource : public Xapian::PostingSource {
40 Xapian::doccount num_docs;
41
42 Xapian::doccount last_docid;
43
44 Xapian::docid did;
45
MyOddPostingSource(Xapian::doccount num_docs_,Xapian::doccount last_docid_)46 MyOddPostingSource(Xapian::doccount num_docs_,
47 Xapian::doccount last_docid_)
48 : num_docs(num_docs_), last_docid(last_docid_), did(0)
49 { }
50
51 public:
MyOddPostingSource(const Xapian::Database & db)52 MyOddPostingSource(const Xapian::Database &db)
53 : num_docs(db.get_doccount()), last_docid(db.get_lastdocid()), did(0)
54 { }
55
clone() const56 PostingSource * clone() const { return new MyOddPostingSource(num_docs, last_docid); }
57
init(const Xapian::Database &)58 void init(const Xapian::Database &) { did = 0; }
59
60 // These bounds could be better, but that's not important here.
get_termfreq_min() const61 Xapian::doccount get_termfreq_min() const { return 0; }
62
get_termfreq_est() const63 Xapian::doccount get_termfreq_est() const { return num_docs / 2; }
64
get_termfreq_max() const65 Xapian::doccount get_termfreq_max() const { return num_docs; }
66
next(double wt)67 void next(double wt) {
68 (void)wt;
69 ++did;
70 if (did % 2 == 0) ++did;
71 }
72
skip_to(Xapian::docid to_did,double wt)73 void skip_to(Xapian::docid to_did, double wt) {
74 (void)wt;
75 did = to_did;
76 if (did % 2 == 0) ++did;
77 }
78
at_end() const79 bool at_end() const {
80 // Doesn't work if last_docid is 2^32 - 1.
81 return did > last_docid;
82 }
83
get_docid() const84 Xapian::docid get_docid() const { return did; }
85
get_description() const86 string get_description() const { return "MyOddPostingSource"; }
87 };
88
89 DEFINE_TESTCASE(externalsource1, backend && !remote && !multi) {
90 // Doesn't work for remote without registering with the server.
91 // Doesn't work for multi because it checks the docid in the
92 // subdatabase.
93 Xapian::Database db(get_database("apitest_phrase"));
94 Xapian::Enquire enq(db);
95 MyOddPostingSource src(db);
96
97 // Check that passing NULL is rejected as intended.
98 Xapian::PostingSource * nullsrc = NULL;
99 TEST_EXCEPTION(Xapian::InvalidArgumentError, Xapian::Query bad(nullsrc));
100
101 enq.set_query(Xapian::Query(&src));
102
103 Xapian::MSet mset = enq.get_mset(0, 10);
104 mset_expect_order(mset, 1, 3, 5, 7, 9, 11, 13, 15, 17);
105
106 Xapian::Query q(Xapian::Query::OP_FILTER,
107 Xapian::Query("leav"),
108 Xapian::Query(&src));
109 enq.set_query(q);
110
111 mset = enq.get_mset(0, 10);
112 mset_expect_order(mset, 5, 7, 11, 13, 9);
113 }
114
115 // Test that trying to use PostingSource with the remote backend throws
116 // Xapian::UnimplementedError as expected (we need to register the class
117 // in xapian-tcpsrv/xapian-progsrv for this to work).
DEFINE_TESTCASE(externalsource2,remote)118 DEFINE_TESTCASE(externalsource2, remote) {
119 Xapian::Database db(get_database("apitest_phrase"));
120 Xapian::Enquire enq(db);
121 MyOddPostingSource src(db);
122
123 enq.set_query(Xapian::Query(&src));
124
125 TEST_EXCEPTION(Xapian::UnimplementedError,
126 Xapian::MSet mset = enq.get_mset(0, 10));
127
128 Xapian::Query q(Xapian::Query::OP_FILTER,
129 Xapian::Query("leav"),
130 Xapian::Query(&src));
131 enq.set_query(q);
132
133 TEST_EXCEPTION(Xapian::UnimplementedError,
134 Xapian::MSet mset = enq.get_mset(0, 10));
135 }
136
137 class MyOddWeightingPostingSource : public Xapian::PostingSource {
138 Xapian::doccount num_docs;
139
140 Xapian::doccount last_docid;
141
142 Xapian::docid did;
143
MyOddWeightingPostingSource(Xapian::doccount num_docs_,Xapian::doccount last_docid_)144 MyOddWeightingPostingSource(Xapian::doccount num_docs_,
145 Xapian::doccount last_docid_)
146 : num_docs(num_docs_), last_docid(last_docid_), did(0)
147 {
148 set_maxweight(1000);
149 }
150
151 public:
MyOddWeightingPostingSource(const Xapian::Database & db)152 MyOddWeightingPostingSource(const Xapian::Database &db)
153 : num_docs(db.get_doccount()), last_docid(db.get_lastdocid()), did(0)
154 { }
155
clone() const156 PostingSource * clone() const {
157 return new MyOddWeightingPostingSource(num_docs, last_docid);
158 }
159
init(const Xapian::Database &)160 void init(const Xapian::Database &) { did = 0; }
161
get_weight() const162 double get_weight() const {
163 return (did % 2) ? 1000 : 0.001;
164 }
165
166 // These bounds could be better, but that's not important here.
get_termfreq_min() const167 Xapian::doccount get_termfreq_min() const { return 0; }
168
get_termfreq_est() const169 Xapian::doccount get_termfreq_est() const { return num_docs / 2; }
170
get_termfreq_max() const171 Xapian::doccount get_termfreq_max() const { return num_docs; }
172
next(double wt)173 void next(double wt) {
174 (void)wt;
175 ++did;
176 }
177
skip_to(Xapian::docid to_did,double wt)178 void skip_to(Xapian::docid to_did, double wt) {
179 (void)wt;
180 did = to_did;
181 }
182
at_end() const183 bool at_end() const {
184 // Doesn't work if last_docid is 2^32 - 1.
185 return did > last_docid;
186 }
187
get_docid() const188 Xapian::docid get_docid() const { return did; }
189
get_description() const190 string get_description() const {
191 return "MyOddWeightingPostingSource";
192 }
193 };
194
195 // Like externalsource1, except we use the weight to favour odd documents.
196 DEFINE_TESTCASE(externalsource3, backend && !remote && !multi) {
197 // Doesn't work for remote without registering with the server.
198 // Doesn't work for multi because it checks the docid in the
199 // subdatabase.
200 Xapian::Database db(get_database("apitest_phrase"));
201 Xapian::Enquire enq(db);
202 MyOddWeightingPostingSource src(db);
203
204 enq.set_query(Xapian::Query(&src));
205
206 Xapian::MSet mset = enq.get_mset(0, 10);
207 mset_expect_order(mset, 1, 3, 5, 7, 9, 11, 13, 15, 17, 2);
208
209 Xapian::Query q(Xapian::Query::OP_OR,
210 Xapian::Query("leav"),
211 Xapian::Query(&src));
212 enq.set_query(q);
213
214 mset = enq.get_mset(0, 5);
215 mset_expect_order(mset, 5, 7, 11, 13, 9);
216
217 tout << "max possible weight = " << mset.get_max_possible() << endl;
218 TEST(mset.get_max_possible() > 1000);
219
220 enq.set_cutoff(0, 1000.001);
221 mset = enq.get_mset(0, 10);
222 mset_expect_order(mset, 5, 7, 11, 13, 9);
223
224 tout << "max possible weight = " << mset.get_max_possible() << endl;
225 TEST(mset.get_max_possible() > 1000);
226
227 enq.set_query(Xapian::Query(q.OP_SCALE_WEIGHT, Xapian::Query(&src), 0.5));
228 mset = enq.get_mset(0, 10);
229 TEST(mset.empty());
230
231 TEST_EQUAL(mset.get_max_possible(), 500);
232
233 enq.set_query(Xapian::Query(q.OP_SCALE_WEIGHT, Xapian::Query(&src), 2));
234 mset = enq.get_mset(0, 10);
235 mset_expect_order(mset, 1, 3, 5, 7, 9, 11, 13, 15, 17);
236
237 TEST_EQUAL(mset.get_max_possible(), 2000);
238 }
239
240 class MyDontAskWeightPostingSource : public Xapian::PostingSource {
241 Xapian::doccount num_docs;
242
243 Xapian::doccount last_docid;
244
245 Xapian::docid did;
246
MyDontAskWeightPostingSource(Xapian::doccount num_docs_,Xapian::doccount last_docid_)247 MyDontAskWeightPostingSource(Xapian::doccount num_docs_,
248 Xapian::doccount last_docid_)
249 : num_docs(num_docs_), last_docid(last_docid_), did(0)
250 { }
251
252 public:
MyDontAskWeightPostingSource()253 MyDontAskWeightPostingSource() : Xapian::PostingSource() {}
254
clone() const255 PostingSource * clone() const { return new MyDontAskWeightPostingSource(num_docs, last_docid); }
256
init(const Xapian::Database & db)257 void init(const Xapian::Database &db) {
258 num_docs = db.get_doccount();
259 last_docid = db.get_lastdocid();
260 did = 0;
261 }
262
get_weight() const263 double get_weight() const {
264 FAIL_TEST("MyDontAskWeightPostingSource::get_weight() called");
265 }
266
267 // These bounds could be better, but that's not important here.
get_termfreq_min() const268 Xapian::doccount get_termfreq_min() const { return num_docs; }
269
get_termfreq_est() const270 Xapian::doccount get_termfreq_est() const { return num_docs; }
271
get_termfreq_max() const272 Xapian::doccount get_termfreq_max() const { return num_docs; }
273
next(double wt)274 void next(double wt) {
275 (void)wt;
276 ++did;
277 }
278
skip_to(Xapian::docid to_did,double wt)279 void skip_to(Xapian::docid to_did, double wt) {
280 (void)wt;
281 did = to_did;
282 }
283
at_end() const284 bool at_end() const {
285 // Doesn't work if last_docid is 2^32 - 1.
286 return did > last_docid;
287 }
288
get_docid() const289 Xapian::docid get_docid() const { return did; }
290
get_description() const291 string get_description() const {
292 return "MyDontAskWeightPostingSource";
293 }
294 };
295
296 // Check that boolean use doesn't call get_weight().
297 DEFINE_TESTCASE(externalsource4, backend && !remote) {
298 Xapian::Database db(get_database("apitest_phrase"));
299 Xapian::Enquire enq(db);
300 MyDontAskWeightPostingSource src;
301
302 tout << "OP_SCALE_WEIGHT 0" << endl;
303 enq.set_query(Xapian::Query(Xapian::Query::OP_SCALE_WEIGHT, Xapian::Query(&src), 0));
304
305 Xapian::MSet mset = enq.get_mset(0, 5);
306 mset_expect_order(mset, 1, 2, 3, 4, 5);
307
308 tout << "OP_FILTER" << endl;
309 Xapian::Query q(Xapian::Query::OP_FILTER,
310 Xapian::Query("leav"),
311 Xapian::Query(&src));
312 enq.set_query(q);
313
314 mset = enq.get_mset(0, 5);
315 mset_expect_order(mset, 8, 6, 4, 5, 7);
316
317 tout << "BoolWeight" << endl;
318 enq.set_query(Xapian::Query(&src));
319 enq.set_weighting_scheme(Xapian::BoolWeight());
320
321 // mset = enq.get_mset(0, 5);
322 // mset_expect_order(mset, 1, 2, 3, 4, 5);
323 }
324
325 // Check that valueweightsource works correctly.
DEFINE_TESTCASE(valueweightsource1,backend)326 DEFINE_TESTCASE(valueweightsource1, backend) {
327 Xapian::Database db(get_database("apitest_phrase"));
328 Xapian::Enquire enq(db);
329 Xapian::ValueWeightPostingSource src(11);
330
331 // Should be in descending order of length
332 tout << "RAW" << endl;
333 enq.set_query(Xapian::Query(&src));
334 Xapian::MSet mset = enq.get_mset(0, 5);
335 mset_expect_order(mset, 3, 1, 2, 8, 14);
336
337 // In relevance order
338 tout << "OP_FILTER" << endl;
339 Xapian::Query q(Xapian::Query::OP_FILTER,
340 Xapian::Query("leav"),
341 Xapian::Query(&src));
342 enq.set_query(q);
343 mset = enq.get_mset(0, 5);
344 mset_expect_order(mset, 8, 6, 4, 5, 7);
345
346 // Should be in descending order of length
347 tout << "OP_FILTER other way" << endl;
348 q = Xapian::Query(Xapian::Query::OP_FILTER,
349 Xapian::Query(&src),
350 Xapian::Query("leav"));
351 enq.set_query(q);
352 mset = enq.get_mset(0, 5);
353 mset_expect_order(mset, 8, 14, 9, 13, 7);
354 }
355
356 // Check that valueweightsource gives the correct bounds for those databases
357 // which support value statistics.
DEFINE_TESTCASE(valueweightsource2,valuestats)358 DEFINE_TESTCASE(valueweightsource2, valuestats) {
359 Xapian::Database db(get_database("apitest_phrase"));
360 Xapian::ValueWeightPostingSource src(11);
361 src.init(db);
362 TEST_EQUAL(src.get_termfreq_min(), 17);
363 TEST_EQUAL(src.get_termfreq_est(), 17);
364 TEST_EQUAL(src.get_termfreq_max(), 17);
365 TEST_EQUAL(src.get_maxweight(), 135);
366 }
367
368 // Check that valueweightsource skip_to() can stay in the same position.
369 DEFINE_TESTCASE(valueweightsource3, valuestats && !multi) {
370 // FIXME: multi doesn't support iterating valuestreams yet.
371 Xapian::Database db(get_database("apitest_phrase"));
372 Xapian::ValueWeightPostingSource src(11);
373 src.init(db);
374 TEST(!src.at_end());
375 src.skip_to(8, 0.0);
376 TEST(!src.at_end());
377 TEST_EQUAL(src.get_docid(), 8);
378 src.skip_to(8, 0.0);
379 TEST(!src.at_end());
380 TEST_EQUAL(src.get_docid(), 8);
381 }
382
383 // Check that fixedweightsource works correctly.
DEFINE_TESTCASE(fixedweightsource1,backend)384 DEFINE_TESTCASE(fixedweightsource1, backend) {
385 Xapian::Database db(get_database("apitest_phrase"));
386 Xapian::Enquire enq(db);
387 double wt = 5.6;
388
389 {
390 Xapian::FixedWeightPostingSource src(wt);
391
392 // Should be in increasing order of docid.
393 enq.set_query(Xapian::Query(&src));
394 Xapian::MSet mset = enq.get_mset(0, 5);
395 mset_expect_order(mset, 1, 2, 3, 4, 5);
396
397 for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
398 TEST_EQUAL(i.get_weight(), wt);
399 }
400 }
401
402 // Do some direct tests, to check the skip_to() and check() methods work.
403 {
404 // Check next and skip_to().
405 Xapian::FixedWeightPostingSource src(wt);
406 src.init(db);
407
408 src.next(1.0);
409 TEST(!src.at_end());
410 TEST_EQUAL(src.get_docid(), 1);
411 src.next(1.0);
412 TEST(!src.at_end());
413 TEST_EQUAL(src.get_docid(), 2);
414 src.skip_to(5, 1.0);
415 TEST(!src.at_end());
416 TEST_EQUAL(src.get_docid(), 5);
417 src.next(wt * 2);
418 TEST(src.at_end());
419 }
420 {
421 // Check check() as the first operation, followed by next.
422 Xapian::FixedWeightPostingSource src(wt);
423 src.init(db);
424
425 TEST_EQUAL(src.check(5, 1.0), true);
426 TEST(!src.at_end());
427 TEST_EQUAL(src.get_docid(), 5);
428 src.next(1.0);
429 TEST(!src.at_end());
430 TEST_EQUAL(src.get_docid(), 6);
431 }
432 {
433 // Check check() as the first operation, followed by skip_to().
434 Xapian::FixedWeightPostingSource src(wt);
435 src.init(db);
436
437 TEST_EQUAL(src.check(5, 1.0), true);
438 TEST(!src.at_end());
439 TEST_EQUAL(src.get_docid(), 5);
440 src.skip_to(6, 1.0);
441 TEST(!src.at_end());
442 TEST_EQUAL(src.get_docid(), 6);
443 src.skip_to(7, wt * 2);
444 TEST(src.at_end());
445 }
446 }
447
448 // A posting source which changes the maximum weight.
449 class ChangeMaxweightPostingSource : public Xapian::PostingSource {
450 Xapian::docid did;
451
452 // Maximum docid that get_weight() should be called for.
453 Xapian::docid maxid_accessed;
454
455 public:
ChangeMaxweightPostingSource(Xapian::docid maxid_accessed_)456 ChangeMaxweightPostingSource(Xapian::docid maxid_accessed_)
457 : did(0), maxid_accessed(maxid_accessed_) { }
458
init(const Xapian::Database &)459 void init(const Xapian::Database &) { did = 0; }
460
get_weight() const461 double get_weight() const {
462 if (did > maxid_accessed) {
463 FAIL_TEST("ChangeMaxweightPostingSource::get_weight() called "
464 "for docid " + str(did) + ", max id accessed "
465 "should be " + str(maxid_accessed));
466 }
467 return 5 - did;
468 }
469
get_termfreq_min() const470 Xapian::doccount get_termfreq_min() const { return 4; }
get_termfreq_est() const471 Xapian::doccount get_termfreq_est() const { return 4; }
get_termfreq_max() const472 Xapian::doccount get_termfreq_max() const { return 4; }
473
next(double)474 void next(double) {
475 ++did;
476 set_maxweight(5 - did);
477 }
478
skip_to(Xapian::docid to_did,double)479 void skip_to(Xapian::docid to_did, double) {
480 did = to_did;
481 set_maxweight(5 - did);
482 }
483
at_end() const484 bool at_end() const { return did >= 5; }
get_docid() const485 Xapian::docid get_docid() const { return did; }
get_description() const486 string get_description() const { return "ChangeMaxweightPostingSource"; }
487 };
488
489 // Test a posting source with a variable maxweight.
490 DEFINE_TESTCASE(changemaxweightsource1, backend && !remote && !multi) {
491 // The ChangeMaxweightPostingSource doesn't work with multi or remote.
492 Xapian::Database db(get_database("apitest_phrase"));
493 Xapian::Enquire enq(db);
494
495 {
496 ChangeMaxweightPostingSource src1(5);
497 Xapian::FixedWeightPostingSource src2(2.5);
498
499 Xapian::Query q(Xapian::Query::OP_AND,
500 Xapian::Query(&src1), Xapian::Query(&src2));
501 enq.set_query(q);
502 // Set descending docid order so that the matcher isn't able to
503 // terminate early after 4 documents just because weight == maxweight.
504 enq.set_docid_order(enq.DESCENDING);
505
506 Xapian::MSet mset = enq.get_mset(0, 4);
507 TEST(src1.at_end());
508 mset_expect_order(mset, 1, 2, 3, 4);
509 for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
510 TEST_EQUAL_DOUBLE(i.get_weight(), 7.5 - *i);
511 }
512 }
513
514 {
515 ChangeMaxweightPostingSource src1(3);
516 Xapian::FixedWeightPostingSource src2(2.5);
517
518 Xapian::Query q(Xapian::Query::OP_AND,
519 Xapian::Query(&src1), Xapian::Query(&src2));
520 enq.set_query(q);
521
522 Xapian::MSet mset = enq.get_mset(0, 2);
523 TEST(!src1.at_end());
524 TEST_EQUAL(src1.get_docid(), 3);
525 TEST_EQUAL_DOUBLE(src1.get_maxweight(), 2.0);
526 mset_expect_order(mset, 1, 2);
527 for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
528 TEST_EQUAL_DOUBLE(i.get_weight(), 7.5 - *i);
529 }
530 }
531 }
532
533 // Test using a valueweightpostingsource which has no entries.
534 DEFINE_TESTCASE(emptyvalwtsource1, backend && !remote && !multi) {
535 Xapian::Database db(get_database("apitest_phrase"));
536 Xapian::Enquire enq(db);
537
538 Xapian::ValueWeightPostingSource src2(11); // A non-empty slot.
539 Xapian::ValueWeightPostingSource src3(100); // An empty slot.
540 Xapian::Query q1("leav");
541 Xapian::Query q2(&src2);
542 Xapian::Query q3(&src3);
543 Xapian::Query q(Xapian::Query::OP_OR, Xapian::Query(Xapian::Query::OP_AND_MAYBE, q1, q2), q3);
544
545 // Perform search without ORring with the posting source.
546 Xapian::doccount size1;
547 {
548 enq.set_query(q1);
549 Xapian::MSet mset = enq.get_mset(0, 10);
550 TEST_REL(mset.get_max_possible(), >, 0.0);
551 size1 = mset.size();
552 TEST_REL(size1, >, 0);
553 }
554
555 // Perform a search with just the non-empty posting source, checking it
556 // returns something.
557 {
558 enq.set_query(q2);
559 Xapian::MSet mset = enq.get_mset(0, 10);
560 TEST_REL(mset.get_max_possible(), >, 0.0);
561 TEST_REL(mset.size(), >, 0);
562 }
563
564 // Perform a search with just the empty posting source, checking it returns
565 // nothing.
566 {
567 enq.set_query(q3);
568 Xapian::MSet mset = enq.get_mset(0, 10);
569
570 // get_max_possible() returns 0 here for backends which track the upper
571 // bound on value slot entries, MAX_DBL for backends which don't.
572 // Either is valid.
573 TEST_REL(mset.get_max_possible(), >=, 0.0);
574
575 TEST_EQUAL(mset.size(), 0);
576 }
577
578 // Perform a search with the posting source ORred with the normal query.
579 // This is a regression test - it used to return nothing.
580 {
581 enq.set_query(q);
582 Xapian::MSet mset = enq.get_mset(0, 10);
583 TEST_REL(mset.get_max_possible(), >, 0.0);
584 TEST_REL(mset.size(), >, 0.0);
585 TEST_EQUAL(mset.size(), size1);
586 }
587 }
588
589 class SlowDecreasingValueWeightPostingSource
590 : public Xapian::DecreasingValueWeightPostingSource {
591 public:
592 int & count;
593
SlowDecreasingValueWeightPostingSource(int & count_)594 SlowDecreasingValueWeightPostingSource(int & count_)
595 : Xapian::DecreasingValueWeightPostingSource(0), count(count_) { }
596
clone() const597 SlowDecreasingValueWeightPostingSource * clone() const
598 {
599 return new SlowDecreasingValueWeightPostingSource(count);
600 }
601
next(double min_wt)602 void next(double min_wt) {
603 sleep(1);
604 ++count;
605 return Xapian::DecreasingValueWeightPostingSource::next(min_wt);
606 }
607 };
608
609 static void
make_matchtimelimit1_db(Xapian::WritableDatabase & db,const string &)610 make_matchtimelimit1_db(Xapian::WritableDatabase &db, const string &)
611 {
612 for (int wt = 20; wt > 0; --wt) {
613 Xapian::Document doc;
614 doc.add_value(0, Xapian::sortable_serialise(double(wt)));
615 db.add_document(doc);
616 }
617 }
618
619 // FIXME: This doesn't run for remote databases (we'd need to register
620 // SlowDecreasingValueWeightPostingSource on the remote).
621 DEFINE_TESTCASE(matchtimelimit1, generated && !remote)
622 {
623 #ifndef HAVE_TIMER_CREATE
624 SKIP_TEST("Enquire::set_time_limit() not implemented for this platform");
625 #endif
626 Xapian::Database db = get_database("matchtimelimit1",
627 make_matchtimelimit1_db);
628
629 int count = 0;
630 SlowDecreasingValueWeightPostingSource src(count);
631 src.init(db);
632 Xapian::Enquire enquire(db);
633 enquire.set_query(Xapian::Query(&src));
634
635 enquire.set_time_limit(1.5);
636
637 Xapian::MSet mset = enquire.get_mset(0, 1, 1000);
638 TEST_EQUAL(mset.size(), 1);
639 TEST_EQUAL(count, 2);
640 }
641
642 class CheckBoundsPostingSource
643 : public Xapian::DecreasingValueWeightPostingSource {
644 public:
645 Xapian::doccount& doclen_lb;
646
647 Xapian::doccount& doclen_ub;
648
CheckBoundsPostingSource(Xapian::doccount & doclen_lb_,Xapian::doccount & doclen_ub_)649 CheckBoundsPostingSource(Xapian::doccount& doclen_lb_,
650 Xapian::doccount& doclen_ub_)
651 : Xapian::DecreasingValueWeightPostingSource(0),
652 doclen_lb(doclen_lb_),
653 doclen_ub(doclen_ub_) { }
654
clone() const655 CheckBoundsPostingSource * clone() const
656 {
657 return new CheckBoundsPostingSource(doclen_lb, doclen_ub);
658 }
659
init(const Xapian::Database & database)660 void init(const Xapian::Database& database) {
661 doclen_lb = database.get_doclength_lower_bound();
662 doclen_ub = database.get_doclength_upper_bound();
663 Xapian::DecreasingValueWeightPostingSource::init(database);
664 }
665 };
666
667 // Test that doclength bounds are correct.
668 // Regression test for bug fixed in 1.2.25 and 1.4.1.
669 DEFINE_TESTCASE(postingsourcebounds1, backend && !remote)
670 {
671 Xapian::Database db = get_database("apitest_simpledata");
672
673 Xapian::doccount doclen_lb = 0, doclen_ub = 0;
674 CheckBoundsPostingSource ps(doclen_lb, doclen_ub);
675
676 Xapian::Enquire enquire(db);
677 enquire.set_query(Xapian::Query(&ps));
678
679 Xapian::MSet mset = enquire.get_mset(0, 1);
680
681 TEST_EQUAL(doclen_lb, db.get_doclength_lower_bound());
682 TEST_EQUAL(doclen_ub, db.get_doclength_upper_bound());
683 }
684
685 // PostingSource which really just counts the clone() calls.
686 // Never actually matches anything, but pretends it might.
687 class CloneTestPostingSource : public Xapian::PostingSource {
688 int& clone_count;
689
690 public:
CloneTestPostingSource(int & clone_count_)691 CloneTestPostingSource(int& clone_count_)
692 : clone_count(clone_count_)
693 { }
694
clone() const695 PostingSource * clone() const {
696 ++clone_count;
697 return new CloneTestPostingSource(clone_count);
698 }
699
init(const Xapian::Database &)700 void init(const Xapian::Database&) { }
701
get_termfreq_min() const702 Xapian::doccount get_termfreq_min() const { return 0; }
703
get_termfreq_est() const704 Xapian::doccount get_termfreq_est() const { return 1; }
705
get_termfreq_max() const706 Xapian::doccount get_termfreq_max() const { return 2; }
707
next(double)708 void next(double) { }
709
skip_to(Xapian::docid,double)710 void skip_to(Xapian::docid, double) { }
711
at_end() const712 bool at_end() const {
713 return true;
714 }
715
get_docid() const716 Xapian::docid get_docid() const { return 0; }
717
get_description() const718 string get_description() const { return "CloneTestPostingSource"; }
719 };
720
721 /// Test cloning of initial object, which regressed in 1.3.5.
722 DEFINE_TESTCASE(postingsourceclone1, !backend)
723 {
724 // This fails with 1.3.5-1.4.0 inclusive.
725 {
726 int clones = 0;
727 CloneTestPostingSource ps(clones);
728 TEST_EQUAL(clones, 0);
729 Xapian::Query q(&ps);
730 TEST_EQUAL(clones, 1);
731 }
732
733 // Check that clone() isn't needlessly called if reference counting has
734 // been turned on for the PostingSource.
735 {
736 int clones = 0;
737 CloneTestPostingSource* ps = new CloneTestPostingSource(clones);
738 TEST_EQUAL(clones, 0);
739 Xapian::Query q(ps->release());
740 TEST_EQUAL(clones, 0);
741 }
742 }
743
744 class OnlyTheFirstPostingSource : public Xapian::PostingSource {
745 Xapian::doccount last_docid;
746
747 Xapian::docid did;
748
749 bool allow_clone;
750
751 public:
752 static Xapian::doccount shard_index;
753
754 explicit
OnlyTheFirstPostingSource(bool allow_clone_)755 OnlyTheFirstPostingSource(bool allow_clone_) : allow_clone(allow_clone_) {}
756
clone() const757 PostingSource* clone() const {
758 return allow_clone ? new OnlyTheFirstPostingSource(true) : nullptr;
759 }
760
init(const Xapian::Database & db)761 void init(const Xapian::Database& db) {
762 did = 0;
763 if (shard_index == 0) {
764 last_docid = db.get_lastdocid();
765 } else {
766 last_docid = 0;
767 }
768 ++shard_index;
769 }
770
get_termfreq_min() const771 Xapian::doccount get_termfreq_min() const { return 0; }
772
get_termfreq_est() const773 Xapian::doccount get_termfreq_est() const { return last_docid / 2; }
774
get_termfreq_max() const775 Xapian::doccount get_termfreq_max() const { return last_docid; }
776
next(double wt)777 void next(double wt) {
778 (void)wt;
779 ++did;
780 if (did > last_docid) did = 0;
781 }
782
skip_to(Xapian::docid to_did,double wt)783 void skip_to(Xapian::docid to_did, double wt) {
784 (void)wt;
785 did = to_did;
786 if (did > last_docid) did = 0;
787 }
788
at_end() const789 bool at_end() const {
790 return did == 0;
791 }
792
get_docid() const793 Xapian::docid get_docid() const { return did; }
794
get_description() const795 string get_description() const { return "OnlyTheFirstPostingSource"; }
796 };
797
798 Xapian::doccount OnlyTheFirstPostingSource::shard_index;
799
800 DEFINE_TESTCASE(postingsourceshardindex1, multi && !remote) {
801 Xapian::Database db = get_database("apitest_simpledata");
802
803 OnlyTheFirstPostingSource::shard_index = 0;
804
805 Xapian::Enquire enquire(db);
806 {
807 auto ps = new OnlyTheFirstPostingSource(true);
808 enquire.set_query(Xapian::Query(ps->release()));
809
810 Xapian::MSet mset = enquire.get_mset(0, 10);
811 mset_expect_order(mset, 1, 3, 5);
812 }
813
814 {
815 /* Regression test for bug fixed in 1.4.12 - we should get an exception
816 * if we use a PostingSource that doesn't support clone() with a multi
817 * DB.
818 */
819 auto ps = new OnlyTheFirstPostingSource(false);
820 enquire.set_query(Xapian::Query(ps->release()));
821
822 TEST_EXCEPTION(Xapian::InvalidOperationError,
823 auto m = enquire.get_mset(0, 10));
824 }
825 }
826
827 /// PostingSource subclass for injecting tf bounds and estimate.
828 class EstimatePS : public Xapian::PostingSource {
829 Xapian::doccount lb, est, ub;
830
831 public:
EstimatePS(Xapian::doccount lb_,Xapian::doccount est_,Xapian::doccount ub_)832 EstimatePS(Xapian::doccount lb_,
833 Xapian::doccount est_,
834 Xapian::doccount ub_)
835 : lb(lb_), est(est_), ub(ub_)
836 { }
837
clone() const838 PostingSource * clone() const { return new EstimatePS(lb, est, ub); }
839
init(const Xapian::Database &)840 void init(const Xapian::Database &) { }
841
get_termfreq_min() const842 Xapian::doccount get_termfreq_min() const { return lb; }
843
get_termfreq_est() const844 Xapian::doccount get_termfreq_est() const { return est; }
845
get_termfreq_max() const846 Xapian::doccount get_termfreq_max() const { return ub; }
847
next(double)848 void next(double) {
849 FAIL_TEST("EstimatePS::next() shouldn't be called");
850 }
851
skip_to(Xapian::docid,double)852 void skip_to(Xapian::docid, double) {
853 FAIL_TEST("EstimatePS::skip_to() shouldn't be called");
854 }
855
at_end() const856 bool at_end() const {
857 return false;
858 }
859
get_docid() const860 Xapian::docid get_docid() const {
861 FAIL_TEST("EstimatePS::get_docid() shouldn't be called");
862 }
863
get_description() const864 string get_description() const { return "EstimatePS"; }
865 };
866
867 /// Check estimate is rounded to suitable number of S.F. - new in 1.4.3.
868 DEFINE_TESTCASE(estimaterounding1, backend && !multi && !remote) {
869 Xapian::Database db = get_database("etext");
870 Xapian::Enquire enquire(db);
871 static const struct { Xapian::doccount lb, est, ub, exp; } testcases[] = {
872 // Test rounding down.
873 {411, 424, 439, 420},
874 {1, 312, 439, 300},
875 // Test rounding up.
876 {411, 426, 439, 430},
877 {123, 351, 439, 400},
878 // Rounding based on estimate size if smaller than range size.
879 {1, 12, 439, 10},
880 // Round "5" away from the nearer bound.
881 {1, 15, 439, 20},
882 {1, 350, 439, 300},
883 // Check we round up if rounding down would be out of range.
884 {411, 416, 439, 420},
885 {411, 412, 439, 420},
886 // Check we round down if rounding up would be out of range.
887 {111, 133, 138, 130},
888 {111, 137, 138, 130},
889 // Check we don't round if either way would be out of range.
890 {411, 415, 419, 415},
891 // Leave small estimates alone.
892 {1, 6, 439, 6},
893 };
894 for (auto& t : testcases) {
895 EstimatePS ps(t.lb, t.est, t.ub);
896 enquire.set_query(Xapian::Query(&ps));
897 Xapian::MSet mset = enquire.get_mset(0, 0);
898 // MSet::get_description() includes bounds and raw estimate.
899 tout << mset.get_description() << endl;
900 TEST_EQUAL(mset.get_matches_estimated(), t.exp);
901 }
902 }
903