Parent: [60cb39] (diff)

Download this file

rclabstract.cpp    665 lines (596 with data), 23.9 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
/* Copyright (C) 2004-2017 J.F.Dockes
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the
* Free Software Foundation, Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include "autoconfig.h"
#include <math.h>
#include <map>
#include <unordered_map>
#include <deque>
#include <algorithm>
#include "log.h"
#include "rcldb.h"
#include "rcldb_p.h"
#include "rclquery.h"
#include "rclquery_p.h"
#include "textsplit.h"
#include "searchdata.h"
#include "utf8iter.h"
#include "hldata.h"
#include "chrono.h"
using namespace std;
namespace Rcl {
// This is used as a marker inside the abstract frag lists, but
// normally doesn't remain in final output (which is built with a
// custom sep. by our caller).
static const string cstr_ellipsis("...");
static const string emptys;
// This is used to mark positions overlapped by a multi-word match term
static const string occupiedmarker("?");
#define DEBUGABSTRACT
#ifdef DEBUGABSTRACT
#define LOGABS LOGDEB
#else
#define LOGABS LOGDEB2
#endif
// Unprefix terms. Actually it's not completely clear if we should
// remove prefixes and keep all terms or prune the prefixed
// ones. There is no good way to be sure what will provide the best
// result in general.
static const bool prune_prefixed_terms = true;
static void noPrefixList(const vector<string>& in, vector<string>& out)
{
for (const auto& term : in) {
if (prune_prefixed_terms) {
if (has_prefix(term))
continue;
}
out.push_back(strip_prefix(term));
}
sort(out.begin(), out.end());
vector<string>::iterator it = unique(out.begin(), out.end());
out.resize(it - out.begin());
}
bool Query::Native::getMatchTerms(unsigned long xdocid, vector<string>& terms)
{
if (!xenquire) {
LOGERR("Query::getMatchTerms: no query opened\n");
return false;
}
terms.clear();
Xapian::TermIterator it;
Xapian::docid id = Xapian::docid(xdocid);
vector<string> iterms;
XAPTRY(iterms.insert(iterms.begin(),
xenquire->get_matching_terms_begin(id),
xenquire->get_matching_terms_end(id)),
m_q->m_db->m_ndb->xrdb, m_q->m_reason);
if (!m_q->m_reason.empty()) {
LOGERR("getMatchTerms: xapian error: " << m_q->m_reason << "\n");
return false;
}
noPrefixList(iterms, terms);
return true;
}
// Retrieve db-wide frequencies for the query terms and store them in
// the query object. This is done at most once for a query, and the data is used
// while computing abstracts for the different result documents.
void Query::Native::setDbWideQTermsFreqs()
{
// Do it once only for a given query.
if (!termfreqs.empty())
return;
vector<string> qterms;
{
vector<string> iqterms;
m_q->getQueryTerms(iqterms);
noPrefixList(iqterms, qterms);
}
LOGDEB("Query terms: " << stringsToString(qterms) << endl);
Xapian::Database &xrdb = m_q->m_db->m_ndb->xrdb;
double doccnt = xrdb.get_doccount();
if (doccnt == 0)
doccnt = 1;
for (const auto& term : qterms) {
termfreqs[term] = xrdb.get_termfreq(term) / doccnt;
LOGABS("setDbWideQTermFreqs: [" << term << "] db freq " <<
termfreqs[term] << "\n");
}
}
// Compute matched terms quality coefficients for a matched document by
// retrieving the Within Document Frequencies and multiplying by
// overal term frequency, then using log-based thresholds.
// 2012: it's not too clear to me why exactly we do the log thresholds thing.
// Preferring terms wich are rare either or both in the db and the document
// seems reasonable though
// To avoid setting a high quality for a low frequency expansion of a
// common stem, which seems wrong, we group the terms by
// root, compute a frequency for the group from the sum of member
// occurrences, and let the frequency for each group member be the
// aggregated frequency.
double Query::Native::qualityTerms(Xapian::docid docid,
const vector<string>& terms,
multimap<double, vector<string> >& byQ)
{
LOGABS("qualityTerms\n");
setDbWideQTermsFreqs();
map<string, double> termQcoefs;
double totalweight = 0;
Xapian::Database &xrdb = m_q->m_db->m_ndb->xrdb;
double doclen = xrdb.get_doclength(docid);
if (doclen == 0)
doclen = 1;
HighlightData hld;
if (m_q->m_sd) {
m_q->m_sd->getTerms(hld);
}
// Group the input terms by the user term they were possibly
// expanded from (by stemming)
map<string, vector<string> > byRoot;
for (const auto& term: terms) {
map<string, string>::const_iterator eit = hld.terms.find(term);
if (eit != hld.terms.end()) {
byRoot[eit->second].push_back(term);
} else {
LOGDEB0("qualityTerms: [" << term << "] not found in hld\n");
byRoot[term].push_back(term);
}
}
#ifdef DEBUGABSTRACT
{
string deb;
hld.toString(deb);
LOGABS("qualityTerms: hld: " << deb << "\n");
string byRootstr;
for (const auto& entry : byRoot) {
byRootstr.append("[").append(entry.first).append("]->");
for (const auto& term : entry.second) {
byRootstr.append("[").append(term).append("] ");
}
byRootstr.append("\n");
}
LOGABS("\nqualityTerms: uterms to terms: " << byRootstr << "\n");
}
#endif
// Compute in-document and global frequencies for the groups.
map<string, double> grpwdfs;
map<string, double> grptfreqs;
for (const auto& group : byRoot) {
for (const auto& term : group.second) {
Xapian::TermIterator xtermit = xrdb.termlist_begin(docid);
xtermit.skip_to(term);
if (xtermit != xrdb.termlist_end(docid) && *xtermit == term) {
if (grpwdfs.find(group.first) != grpwdfs.end()) {
grpwdfs[group.first] = xtermit.get_wdf() / doclen;
grptfreqs[group.first] = termfreqs[term];
} else {
grpwdfs[group.first] += xtermit.get_wdf() / doclen;
grptfreqs[group.first] += termfreqs[term];
}
}
}
}
// Build a sorted by quality container for the groups
for (const auto& group : byRoot) {
double q = (grpwdfs[group.first]) * grptfreqs[group.first];
q = -log10(q);
if (q < 3) {
q = 0.05;
} else if (q < 4) {
q = 0.3;
} else if (q < 5) {
q = 0.7;
} else if (q < 6) {
q = 0.8;
} else {
q = 1;
}
totalweight += q;
byQ.insert(pair<double, vector<string> >(q, group.second));
}
#ifdef DEBUGABSTRACT
for (auto mit= byQ.rbegin(); mit != byQ.rend(); mit++) {
LOGABS("qualityTerms: coef: " << mit->first << " group: " <<
stringsToString(mit->second) << endl);
}
#endif
return totalweight;
}
// Return page number for first match of "significant" term.
int Query::Native::getFirstMatchPage(Xapian::docid docid, string& term)
{
LOGDEB("Query::Native::getFirstMatchPage\n");
if (!m_q|| !m_q->m_db || !m_q->m_db->m_ndb || !m_q->m_db->m_ndb->m_isopen) {
LOGERR("Query::getFirstMatchPage: no db\n");
return -1;
}
Rcl::Db::Native *ndb(m_q->m_db->m_ndb);
Xapian::Database& xrdb(ndb->xrdb);
vector<string> terms;
getMatchTerms(docid, terms);
if (terms.empty()) {
LOGDEB("getFirstMatchPage: empty match term list (field match?)\n");
return -1;
}
vector<int> pagepos;
ndb->getPagePositions(docid, pagepos);
if (pagepos.empty())
return -1;
setDbWideQTermsFreqs();
// We try to use a page which matches the "best" term. Get a sorted list
multimap<double, vector<string> > byQ;
qualityTerms(docid, terms, byQ);
for (auto mit = byQ.rbegin(); mit != byQ.rend(); mit++) {
for (vector<string>::const_iterator qit = mit->second.begin();
qit != mit->second.end(); qit++) {
string qterm = *qit;
Xapian::PositionIterator pos;
string emptys;
try {
for (pos = xrdb.positionlist_begin(docid, qterm);
pos != xrdb.positionlist_end(docid, qterm); pos++) {
int pagenum = ndb->getPageNumberForPosition(pagepos, *pos);
if (pagenum > 0) {
term = qterm;
return pagenum;
}
}
} catch (...) {
// Term does not occur. No problem.
}
}
}
return -1;
}
// Creating the abstract from index position data: populate the sparse
// array with the positions for a given query term, and mark the
// neighboring positions.
void Query::Native::abstractPopulateQTerm(
Xapian::Database& xrdb,
Xapian::docid docid,
const string& qterm,
int qtrmwrdcnt,
int ctxwords,
unsigned int maxgrpoccs,
unsigned int maxtotaloccs,
map<unsigned int, string>& sparseDoc,
unordered_set<unsigned int>& searchTermPositions,
unsigned int& maxpos,
unsigned int& totaloccs,
unsigned int& grpoccs,
int& ret
)
{
Xapian::PositionIterator pos;
// Walk the position list for this term.
for (pos = xrdb.positionlist_begin(docid, qterm);
pos != xrdb.positionlist_end(docid, qterm); pos++) {
int ipos = *pos;
if (ipos < int(baseTextPosition)) // Not in text body
continue;
LOGABS("makeAbstract: [" << qterm << "] at pos " <<
ipos << " grpoccs " << grpoccs << " maxgrpoccs " <<
maxgrpoccs << "\n");
totaloccs++;
grpoccs++;
// Add adjacent slots to the set to populate at next
// step by inserting empty strings. Special provisions
// for adding ellipsis and for positions overlapped by
// the match term.
unsigned int sta = MAX(int(baseTextPosition),
ipos - ctxwords);
unsigned int sto = ipos + qtrmwrdcnt-1 +
m_q->m_db->getAbsCtxLen();
for (unsigned int ii = sta; ii <= sto; ii++) {
if (ii == (unsigned int)ipos) {
sparseDoc[ii] = qterm;
searchTermPositions.insert(ii);
if (ii > maxpos)
maxpos = ii;
} else if (ii > (unsigned int)ipos &&
ii < (unsigned int)ipos + qtrmwrdcnt) {
// Position for another word of the multi-word term
sparseDoc[ii] = occupiedmarker;
} else if (!sparseDoc[ii].compare(cstr_ellipsis)) {
// For an empty slot, the test above has a side
// effect of inserting an empty string which
// is what we want. Do it also if it was an ellipsis
sparseDoc[ii] = emptys;
}
}
// Add ellipsis at the end. This may be replaced later by
// an overlapping extract. Take care not to replace an
// empty string here, we really want an empty slot,
// use find()
if (sparseDoc.find(sto+1) == sparseDoc.end()) {
sparseDoc[sto+1] = cstr_ellipsis;
}
// Group done ?
if (grpoccs >= maxgrpoccs) {
ret |= ABSRES_TRUNC;
LOGABS("Db::makeAbstract: max group occs cutoff\n");
break;
}
// Global done ?
if (totaloccs >= maxtotaloccs) {
ret |= ABSRES_TRUNC;
LOGABS("Db::makeAbstract: max occurrences cutoff\n");
break;
}
}
}
// Creating the abstract from index position data: after the query
// terms have been inserted at their place in the sparse array, and
// the neighboring positions marked, populate the neighbours: for each
// term in the document, walk its position list and populate slots
// around the query terms. We arbitrarily truncate the list to avoid
// taking forever. If we do cutoff, the abstract may be inconsistant
// (missing words, potentially altering meaning), which is bad.
void Query::Native::abstractPopulateContextTerms(
Xapian::Database& xrdb,
Xapian::docid docid,
unsigned int maxpos,
map<unsigned int, string>& sparseDoc,
int& ret
)
{
Xapian::TermIterator term;
int cutoff = m_q->m_snipMaxPosWalk;
for (term = xrdb.termlist_begin(docid);
term != xrdb.termlist_end(docid); term++) {
// Ignore prefixed terms
if (has_prefix(*term))
continue;
if (m_q->m_snipMaxPosWalk > 0 && cutoff-- < 0) {
ret |= ABSRES_TERMMISS;
LOGDEB0("makeAbstract: max term count cutoff " <<
m_q->m_snipMaxPosWalk << "\n");
break;
}
map<unsigned int, string>::iterator vit;
Xapian::PositionIterator pos;
for (pos = xrdb.positionlist_begin(docid, *term);
pos != xrdb.positionlist_end(docid, *term); pos++) {
if (m_q->m_snipMaxPosWalk > 0 && cutoff-- < 0) {
ret |= ABSRES_TERMMISS;
LOGDEB0("makeAbstract: max term count cutoff " <<
m_q->m_snipMaxPosWalk << "\n");
break;
}
// If we are beyond the max possible position, stop
// for this term
if (*pos > maxpos) {
break;
}
if ((vit = sparseDoc.find(*pos)) != sparseDoc.end()) {
// Don't replace a term: the terms list is in
// alphabetic order, and we may have several terms
// at the same position, we want to keep only the
// first one (ie: dockes and dockes@wanadoo.fr)
if (vit->second.empty()) {
LOGDEB2("makeAbstract: populating: [" << *term <<
"] at " << *pos << "\n");
sparseDoc[*pos] = *term;
}
}
}
}
}
// Creating the abstract from position data: final phase: extract the
// snippets from the sparse array.
void Query::Native::abstractCreateSnippetsVector(
Rcl::Db::Native *ndb,
map<unsigned int, string>& sparseDoc,
unordered_set<unsigned int>& searchTermPositions,
vector<int>& vpbreaks,
vector<Snippet>& vabs)
{
vabs.clear();
string chunk;
bool incjk = false;
int page = 0;
string term;
for (const auto& ent : sparseDoc) {
LOGDEB2("Abtract:output "<< ent.first <<" -> [" <<ent.second <<"]\n");
if (!occupiedmarker.compare(ent.second)) {
LOGDEB("Abstract: qtrm position not filled ??\n");
continue;
}
if (chunk.empty() && !vpbreaks.empty()) {
page = ndb->getPageNumberForPosition(vpbreaks, ent.first);
if (page < 0)
page = 0;
term.clear();
}
Utf8Iter uit(ent.second);
bool newcjk = false;
if (TextSplit::isCJK(*uit))
newcjk = true;
if (!incjk || (incjk && !newcjk))
chunk += " ";
incjk = newcjk;
if (searchTermPositions.find(ent.first) != searchTermPositions.end())
term = ent.second;
if (ent.second == cstr_ellipsis) {
vabs.push_back(Snippet(page, chunk).setTerm(term));
chunk.clear();
} else {
if (ent.second.compare(end_of_field_term) &&
ent.second.compare(start_of_field_term))
chunk += ent.second;
}
}
if (!chunk.empty())
vabs.push_back(Snippet(page, chunk).setTerm(term));
}
// Creating the abstract from index position data: top level routine
int Query::Native::abstractFromIndex(
Rcl::Db::Native *ndb,
Xapian::docid docid,
const vector<string>& matchTerms,
const multimap<double, vector<string>> byQ,
double totalweight,
int ctxwords,
unsigned int maxtotaloccs,
vector<Snippet>& vabs,
Chrono& chron
)
{
Xapian::Database& xrdb(ndb->xrdb);
int ret = ABSRES_OK;
// The terms 'array' that we partially populate with the document
// terms, at their positions around the search terms positions:
map<unsigned int, string> sparseDoc;
// Also remember apart the search term positions so that we can list
// them with their snippets.
std::unordered_set<unsigned int> searchTermPositions;
// Remember max position. Used to stop walking positions lists while
// populating the adjacent slots.
unsigned int maxpos = 0;
// Total number of occurences for all terms. We stop when we have too much
unsigned int totaloccs = 0;
// First pass to populate the sparse document: we walk the term
// groups, beginning with the better ones, and insert each term at
// its position. We also insert empty strings at the surrounding
// positions. These are markers showing where we should insert
// data during the next pass.
for (auto mit = byQ.rbegin(); mit != byQ.rend(); mit++) {
unsigned int maxgrpoccs;
double q;
if (byQ.size() == 1) {
maxgrpoccs = maxtotaloccs;
q = 1.0;
} else {
// We give more slots to the better term groups
q = mit->first / totalweight;
maxgrpoccs = int(ceil(maxtotaloccs * q));
}
unsigned int grpoccs = 0;
// For each term in user term expansion group
for (const auto& qterm : mit->second) {
// Enough for this group ?
if (grpoccs >= maxgrpoccs)
break;
LOGABS("makeAbstract: [" << qterm << "] " << maxgrpoccs <<
" max grp occs (coef " << q << ")\n");
// The match term may span several words (more than one position)
int qtrmwrdcnt =
TextSplit::countWords(qterm, TextSplit::TXTS_NOSPANS);
// Populate positions for this query term.
// There may be query terms not in this doc. This raises an
// exception when requesting the position list, we catch it ??
// Not clear how this can happen because we are walking the
// match list returned by Xapian. Maybe something with the
// fields?
try {
abstractPopulateQTerm(xrdb, docid, qterm, qtrmwrdcnt, ctxwords,
maxgrpoccs,maxtotaloccs, sparseDoc,
searchTermPositions, maxpos, totaloccs,
grpoccs, ret);
} catch (...) {
// Term does not occur. No problem.
}
if (totaloccs >= maxtotaloccs) {
ret |= ABSRES_TRUNC;
LOGABS("Db::makeAbstract: max1 occurrences cutoff\n");
break;
}
}
}
maxpos += ctxwords + 1;
LOGABS("makeAbstract:" << chron.millis() <<
"mS:chosen number of positions " << totaloccs << "\n");
// This can happen if there are term occurences in the keywords
// etc. but not elsewhere ?
if (totaloccs == 0) {
LOGDEB("makeAbstract: no occurrences\n");
return ABSRES_OK;
}
abstractPopulateContextTerms(xrdb, docid, maxpos, sparseDoc, ret);
LOGABS("makeAbstract:" << chron.millis() << "mS: all term poslist read\n");
vector<int> vpbreaks;
ndb->getPagePositions(docid, vpbreaks);
LOGABS("makeAbstract:" << chron.millis() << "mS: extracting. Got " <<
vpbreaks.size() << " pages\n");
// Finally build the abstract by walking the map (in order of position)
abstractCreateSnippetsVector(ndb, sparseDoc, searchTermPositions,
vpbreaks, vabs);
LOGABS("makeAbtract: done in " << chron.millis() << " mS\n");
return ret;
}
// Build a document abstract by extracting text chunks around the
// query terms. This can either uses the index position lists, or the
// stored document text, with very different implementations.
//
// DatabaseModified and other general exceptions are catched and
// possibly retried by our caller.
//
// @param[out] vabs the abstract is returned as a vector of snippets.
int Query::Native::makeAbstract(Xapian::docid docid,
vector<Snippet>& vabs,
int imaxoccs, int ictxwords)
{
Chrono chron;
LOGABS("makeAbstract: docid " << docid << " imaxoccs " <<
imaxoccs << " ictxwords " << ictxwords << "\n");
// The (unprefixed) terms matched by this document
vector<string> matchedTerms;
getMatchTerms(docid, matchedTerms);
if (matchedTerms.empty()) {
LOGDEB("makeAbstract:" << chron.millis() << "mS:Empty term list\n");
return ABSRES_ERROR;
}
LOGDEB("Match terms: " << stringsToString(matchedTerms) << endl);
// Retrieve the term frequencies for the query terms. This is
// actually computed only once for a query, and for all terms in
// the query (not only the matches for this doc)
setDbWideQTermsFreqs();
// Build a sorted by quality container for the match terms We are
// going to try and show text around the less common search terms.
// Terms issued from an original one by stem expansion are
// aggregated by the qualityTerms() routine (this is what we call
// 'term groups' in the following: index terms expanded from the
// same user term).
multimap<double, vector<string>> byQ;
double totalweight = qualityTerms(docid, matchedTerms, byQ);
LOGABS("makeAbstract:" << chron.millis() << "mS: computed Qcoefs.\n");
// This can't happen, but would crash us
if (totalweight == 0.0) {
LOGERR("makeAbstract:"<<chron.millis()<<"mS: totalweight == 0.0 !\n");
return ABSRES_ERROR;
}
Rcl::Db::Native *ndb(m_q->m_db->m_ndb);
Xapian::Database& xrdb(ndb->xrdb);
// Total number of slots we populate. The 7 is taken as
// average word size. It was a mistake to have the user max
// abstract size parameter in characters, we basically only deal
// with words. We used to limit the character size at the end, but
// this damaged our careful selection of terms
const unsigned int maxtotaloccs = imaxoccs > 0 ? imaxoccs :
m_q->m_db->getAbsLen() /(7 * (m_q->m_db->getAbsCtxLen() + 1));
int ctxwords = ictxwords == -1 ? m_q->m_db->getAbsCtxLen() : ictxwords;
LOGABS("makeAbstract:" << chron.millis() << "mS: mxttloccs " <<
maxtotaloccs << " ctxwords " << ctxwords << "\n");
if (ndb->m_storetext) {
return abstractFromText(ndb, docid, matchedTerms, byQ,
totalweight, ctxwords, maxtotaloccs, vabs,
chron);
} else {
return abstractFromIndex(ndb, docid, matchedTerms, byQ,
totalweight, ctxwords, maxtotaloccs, vabs,
chron);
}
}
}