xapian-core  2.0.0
glass_values.cc
Go to the documentation of this file.
1 
4 /* Copyright (C) 2008,2009,2010,2011,2012,2016,2017 Olly Betts
5  * Copyright (C) 2008,2009 Lemur Consulting Ltd
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, see
19  * <https://www.gnu.org/licenses/>.
20  */
21 
22 #include <config.h>
23 
24 #include "glass_values.h"
25 
26 #include "glass_cursor.h"
27 #include "glass_postlist.h"
28 #include "glass_termlist.h"
29 #include "debuglog.h"
31 #include "pack.h"
32 
33 #include "xapian/error.h"
34 #include "xapian/valueiterator.h"
35 
36 #include <algorithm>
37 #include <memory>
38 
39 using namespace Glass;
40 using namespace std;
41 
42 // FIXME:
43 // * put the "used slots" entry in the same termlist tag as the terms?
44 // * multi-values?
45 // * values named instead of numbered?
46 
48 static inline string
50 {
51  LOGCALL_STATIC(DB, string, "make_slot_key", did);
52  // Add an extra character so that it can't clash with a termlist entry key
53  // and will sort just after the corresponding termlist entry key.
54  // FIXME: should we store this in the *same entry* as the list of terms?
55  string key;
56  pack_uint_preserving_sort(key, did);
57  key += '\0';
58  RETURN(key);
59 }
60 
62 static inline string
64 {
65  LOGCALL_STATIC(DB, string, "make_valuestats_key", slot);
66  string key("\0\xd0", 2);
67  pack_uint_last(key, slot);
68  RETURN(key);
69 }
70 
71 void
72 ValueChunkReader::assign(const char * p_, size_t len, Xapian::docid did_)
73 {
74  p = p_;
75  end = p_ + len;
76  did = did_;
77  if (!unpack_string(&p, end, value))
78  throw Xapian::DatabaseCorruptError("Failed to unpack first value");
79 }
80 
81 void
83 {
84  if (p == end) {
85  p = NULL;
86  return;
87  }
88 
89  Xapian::docid delta;
90  if (!unpack_uint(&p, end, &delta))
91  throw Xapian::DatabaseCorruptError("Failed to unpack streamed value docid");
92  did += delta + 1;
93  if (!unpack_string(&p, end, value))
94  throw Xapian::DatabaseCorruptError("Failed to unpack streamed value");
95 }
96 
97 void
99 {
100  if (p == NULL || target <= did)
101  return;
102 
103  size_t value_len;
104  while (p != end) {
105  // Get the next docid
106  Xapian::docid delta;
107  if (rare(!unpack_uint(&p, end, &delta)))
108  throw Xapian::DatabaseCorruptError("Failed to unpack streamed value docid");
109  did += delta + 1;
110 
111  // Get the length of the string
112  if (rare(!unpack_uint(&p, end, &value_len))) {
113  throw Xapian::DatabaseCorruptError("Failed to unpack streamed value length");
114  }
115 
116  // Check that it's not too long
117  if (rare(value_len > size_t(end - p))) {
118  throw Xapian::DatabaseCorruptError("Failed to unpack streamed value");
119  }
120 
121  // Assign the value and return only if we've reached the target
122  if (did >= target) {
123  value.assign(p, value_len);
124  p += value_len;
125  return;
126  }
127  p += value_len;
128  }
129  p = NULL;
130 }
131 
132 void
134  const string & val)
135 {
136  auto i = changes.find(slot);
137  if (i == changes.end()) {
138  i = changes.insert(make_pair(slot, map<Xapian::docid, string>())).first;
139  }
140  i->second[did] = val;
141 }
142 
143 void
145 {
146  auto i = changes.find(slot);
147  if (i == changes.end()) {
148  i = changes.insert(make_pair(slot, map<Xapian::docid, string>())).first;
149  }
150  i->second[did] = string();
151 }
152 
155  Xapian::docid did,
156  string &chunk) const
157 {
158  LOGCALL(DB, Xapian::docid, "GlassValueManager::get_chunk_containing_did", slot | did | chunk);
159  if (!cursor)
160  cursor.reset(postlist_table->cursor_get());
161  if (!cursor) RETURN(0);
162 
163  bool exact = cursor->find_entry(make_valuechunk_key(slot, did));
164  if (!exact) {
165  // If we didn't find a chunk starting with docid did, then we need
166  // to check if the chunk contains did.
167  const char * p = cursor->current_key.data();
168  const char * end = p + cursor->current_key.size();
169 
170  // Check that it is a value stream chunk.
171  if (end - p < 2 || *p++ != '\0' || *p++ != '\xd8') RETURN(0);
172 
173  // Check that it's for the right value slot.
174  Xapian::valueno v;
175  if (!unpack_uint(&p, end, &v)) {
176  throw Xapian::DatabaseCorruptError("Bad value key");
177  }
178  if (v != slot) RETURN(0);
179 
180  // And get the first docid for the chunk so we can return it.
181  if (!unpack_uint_preserving_sort(&p, end, &did) || p != end) {
182  throw Xapian::DatabaseCorruptError("Bad value key");
183  }
184  }
185 
186  cursor->read_tag();
187  swap(chunk, cursor->current_tag);
188 
189  RETURN(did);
190 }
191 
192 static const size_t CHUNK_SIZE_THRESHOLD = 2000;
193 
194 namespace Glass {
195 
198 
200 
201  string ctag;
202 
204 
205  string tag;
206 
208 
210 
212 
214 
215  void append_to_stream(Xapian::docid did, const string & value) {
216  Assert(did);
217  if (tag.empty()) {
218  new_first_did = did;
219  } else {
220  AssertRel(did,>,prev_did);
221  pack_uint(tag, did - prev_did - 1);
222  }
223  prev_did = did;
224  pack_string(tag, value);
225  if (tag.size() >= CHUNK_SIZE_THRESHOLD) write_tag();
226  }
227 
228  void write_tag() {
229  // If the first docid has changed, delete the old entry.
230  if (first_did && new_first_did != first_did) {
231  table->del(make_valuechunk_key(slot, first_did));
232  }
233  if (!tag.empty()) {
234  table->add(make_valuechunk_key(slot, new_first_did), tag);
235  }
236  first_did = 0;
237  tag.resize(0);
238  }
239 
240  public:
242  : table(table_), slot(slot_), first_did(0), last_allowed_did(0) { }
243 
245  while (!reader.at_end()) {
246  // FIXME: use skip_to and some splicing magic instead?
247  append_to_stream(reader.get_docid(), reader.get_value());
248  reader.next();
249  }
250  write_tag();
251  }
252 
253  void update(Xapian::docid did, const string & value) {
254  if (last_allowed_did && did > last_allowed_did) {
255  // The next change needs to go in a later existing chunk than the
256  // one we're currently updating, so we copy over the rest of the
257  // entries from the current chunk, write out the updated chunk and
258  // drop through to the case below will read in that later chunk.
259  // FIXME: use some string splicing magic instead of this loop.
260  while (!reader.at_end()) {
261  // last_allowed_did should be an upper bound for this chunk.
262  AssertRel(reader.get_docid(),<=,last_allowed_did);
263  append_to_stream(reader.get_docid(), reader.get_value());
264  reader.next();
265  }
266  write_tag();
267  last_allowed_did = 0;
268  }
269  if (last_allowed_did == 0) {
270  last_allowed_did = GLASS_MAX_DOCID;
271  Assert(tag.empty());
272  new_first_did = 0;
273  unique_ptr<GlassCursor> cursor(table->cursor_get());
274  if (cursor->find_entry(make_valuechunk_key(slot, did))) {
275  // We found an exact match, so the first docid is the one
276  // we looked for.
277  first_did = did;
278  } else {
279  Assert(!cursor->after_end());
280  // Otherwise we need to unpack it from the key we found.
281  // We may have found a non-value-chunk entry in which case
282  // docid_from_key() returns 0.
283  first_did = docid_from_key(slot, cursor->current_key);
284  }
285 
286  // If there are no further chunks, then the last docid that can go
287  // in this chunk is the highest valid docid. If there are further
288  // chunks then it's one less than the first docid of the next
289  // chunk.
290  if (first_did) {
291  // We found a value chunk.
292  cursor->read_tag();
293  // FIXME:swap(cursor->current_tag, ctag);
294  ctag = cursor->current_tag;
295  reader.assign(ctag.data(), ctag.size(), first_did);
296  }
297  if (cursor->next()) {
298  const string & key = cursor->current_key;
299  Xapian::docid next_first_did = docid_from_key(slot, key);
300  if (next_first_did) last_allowed_did = next_first_did - 1;
301  Assert(last_allowed_did);
302  AssertRel(last_allowed_did,>=,first_did);
303  }
304  }
305 
306  // Copy over entries until we get to the one we want to
307  // add/modify/delete.
308  // FIXME: use skip_to and some splicing magic instead?
309  while (!reader.at_end() && reader.get_docid() < did) {
310  append_to_stream(reader.get_docid(), reader.get_value());
311  reader.next();
312  }
313  if (!reader.at_end() && reader.get_docid() == did) reader.next();
314  if (!value.empty()) {
315  // Add/update entry for did.
316  append_to_stream(did, value);
317  }
318  }
319 };
320 
321 }
322 
323 void
325 {
326  if (termlist_table->is_open()) {
327  for (auto i : slots) {
328  string key = make_slot_key(i.first);
329  const string& enc = i.second;
330  if (!enc.empty()) {
331  termlist_table->add(key, enc);
332  } else {
333  termlist_table->del(key);
334  }
335  }
336  slots.clear();
337  }
338 
339  for (auto i : changes) {
340  Xapian::valueno slot = i.first;
341  Glass::ValueUpdater updater(postlist_table, slot);
342  const map<Xapian::docid, string>& slot_changes = i.second;
343  for (auto j : slot_changes) {
344  updater.update(j.first, j.second);
345  }
346  }
347  changes.clear();
348 }
349 
350 void
352  map<Xapian::valueno, ValueStats> & value_stats)
353 {
354  // FIXME: Use BitWriter and interpolative coding? Or is it not worthwhile
355  // for this?
356  string slots_used;
357  Xapian::valueno prev_slot = static_cast<Xapian::valueno>(-1);
359  while (it != doc.values_end()) {
360  Xapian::valueno slot = it.get_valueno();
361  string value = *it;
362  Assert(!value.empty());
363 
364  // Update the statistics.
365  std::pair<map<Xapian::valueno, ValueStats>::iterator, bool> i;
366  i = value_stats.insert(make_pair(slot, ValueStats()));
367  ValueStats & stats = i.first->second;
368  if (i.second) {
369  // There were no statistics stored already, so read them.
370  get_value_stats(slot, stats);
371  }
372 
373  // Now, modify the stored statistics.
374  if ((stats.freq)++ == 0) {
375  // If the value count was previously zero, set the upper and lower
376  // bounds to the newly added value.
377  stats.lower_bound = value;
378  stats.upper_bound = value;
379  } else {
380  // Otherwise, simply make sure they reflect the new value.
381  if (value < stats.lower_bound) {
382  stats.lower_bound = value;
383  } else if (value > stats.upper_bound) {
384  stats.upper_bound = value;
385  }
386  }
387 
388  add_value(did, slot, value);
389  if (termlist_table->is_open()) {
390  // prev_slot starts as Xapian::valueno(-1).
391  pack_uint(slots_used, slot - UNSIGNED_OVERFLOW_OK(prev_slot + 1));
392  prev_slot = slot;
393  }
394  ++it;
395  }
396  if (slots_used.empty() && slots.find(did) == slots.end()) {
397  // Adding a new document with no values which we didn't just remove.
398  } else {
399  swap(slots[did], slots_used);
400  }
401 }
402 
403 void
405  map<Xapian::valueno, ValueStats> & value_stats)
406 {
407  Assert(termlist_table->is_open());
408  map<Xapian::docid, string>::iterator it = slots.find(did);
409  string s;
410  if (it != slots.end()) {
411  swap(s, it->second);
412  } else {
413  // Get from table, making a swift exit if this document has no values.
414  if (!termlist_table->get_exact_entry(make_slot_key(did), s)) return;
415  slots.insert(make_pair(did, string()));
416  }
417  const char * p = s.data();
418  const char * end = p + s.size();
419  Xapian::valueno prev_slot = static_cast<Xapian::valueno>(-1);
420  while (p != end) {
421  Xapian::valueno slot;
422  if (!unpack_uint(&p, end, &slot)) {
423  throw Xapian::DatabaseCorruptError("Value slot encoding corrupt");
424  }
425  // prev_slot starts as Xapian::valueno(-1).
426  slot += UNSIGNED_OVERFLOW_OK(prev_slot + 1);
427  prev_slot = slot;
428 
429  std::pair<map<Xapian::valueno, ValueStats>::iterator, bool> i;
430  i = value_stats.insert(make_pair(slot, ValueStats()));
431  ValueStats & stats = i.first->second;
432  if (i.second) {
433  // There were no statistics stored already, so read them.
434  get_value_stats(slot, stats);
435  }
436 
437  // Now, modify the stored statistics.
438  AssertRelParanoid(stats.freq, >, 0);
439  if (--(stats.freq) == 0) {
440  stats.lower_bound.resize(0);
441  stats.upper_bound.resize(0);
442  }
443 
444  remove_value(did, slot);
445  }
446 }
447 
448 void
450  const Xapian::Document &doc,
451  map<Xapian::valueno, ValueStats> & value_stats)
452 {
453  if (doc.get_docid() == did) {
454  // If we're replacing a document with itself, but the optimisation for
455  // this higher up hasn't kicked in (e.g. because we've added/replaced
456  // a document since this one was read) and the values haven't changed,
457  // then the call to delete_document() below will remove the values
458  // before the subsequent add_document() can read them.
459  //
460  // The simplest way to handle this is to force the document to read its
461  // values, which we only need to do this is the docid matches. Note
462  // that this check can give false positives as we don't also check the
463  // database, so for example replacing document 4 in one database with
464  // document 4 from another will unnecessarily trigger this, but forcing
465  // the values to be read is fairly harmless, and this is unlikely to be
466  // a common case.
467  doc.internal->ensure_values_fetched();
468  }
469  delete_document(did, value_stats);
470  add_document(did, doc, value_stats);
471 }
472 
473 string
475 {
476  auto i = changes.find(slot);
477  if (i != changes.end()) {
478  auto j = i->second.find(did);
479  if (j != i->second.end()) return j->second;
480  }
481 
482  // Read it from the table.
483  string chunk;
484  Xapian::docid first_did;
485  first_did = get_chunk_containing_did(slot, did, chunk);
486  if (first_did == 0) return string();
487 
488  ValueChunkReader reader(chunk.data(), chunk.size(), first_did);
489  reader.skip_to(did);
490  if (reader.at_end() || reader.get_docid() != did) return string();
491  return reader.get_value();
492 }
493 
494 void
495 GlassValueManager::get_all_values(map<Xapian::valueno, string> & values,
496  Xapian::docid did) const
497 {
498  Assert(values.empty());
499  if (!termlist_table->is_open()) {
500  // Either the database has been closed, or else there's no termlist
501  // table. Check if the postlist table is open to determine which is
502  // the case.
503  if (!postlist_table->is_open())
505  throw Xapian::FeatureUnavailableError("Database has no termlist");
506  }
507  map<Xapian::docid, string>::const_iterator i = slots.find(did);
508  string s;
509  if (i != slots.end()) {
510  s = i->second;
511  } else {
512  // Get from table.
513  if (!termlist_table->get_exact_entry(make_slot_key(did), s)) return;
514  }
515  const char * p = s.data();
516  const char * end = p + s.size();
517  Xapian::valueno prev_slot = static_cast<Xapian::valueno>(-1);
518  while (p != end) {
519  Xapian::valueno slot;
520  if (!unpack_uint(&p, end, &slot)) {
521  throw Xapian::DatabaseCorruptError("Value slot encoding corrupt");
522  }
523  // prev_slot starts as Xapian::valueno(-1).
524  slot += UNSIGNED_OVERFLOW_OK(prev_slot + 1);
525  prev_slot = slot;
526  values.insert(make_pair(slot, get_value(did, slot)));
527  }
528 }
529 
530 void
532 {
533  LOGCALL_VOID(DB, "GlassValueManager::get_value_stats", slot);
534  // Invalidate the cache first in case an exception is thrown.
535  mru_slot = Xapian::BAD_VALUENO;
536  get_value_stats(slot, mru_valstats);
537  mru_slot = slot;
538 }
539 
540 void
542 {
543  LOGCALL_VOID(DB, "GlassValueManager::get_value_stats", slot | Literal("[stats]"));
544 
545  string tag;
546  if (postlist_table->get_exact_entry(make_valuestats_key(slot), tag)) {
547  const char * pos = tag.data();
548  const char * end = pos + tag.size();
549 
550  if (!unpack_uint(&pos, end, &(stats.freq))) {
551  if (pos == 0) throw Xapian::DatabaseCorruptError("Incomplete stats item in value table");
552  throw Xapian::RangeError("Frequency statistic in value table is too large");
553  }
554  if (!unpack_string(&pos, end, stats.lower_bound)) {
555  if (pos == 0) throw Xapian::DatabaseCorruptError("Incomplete stats item in value table");
556  throw Xapian::RangeError("Lower bound in value table is too large");
557  }
558  if (stats.lower_bound.empty() && stats.freq != 0) {
559  Assert(false);
560  // This shouldn't happen, but let's code defensively and set the
561  // smallest valid lower bound (a single zero byte) if it somehow
562  // does (it's been reported by a notmuch user).
563  stats.lower_bound.assign(1, '\0');
564  }
565  size_t len = end - pos;
566  if (len == 0) {
567  stats.upper_bound = stats.lower_bound;
568  } else {
569  stats.upper_bound.assign(pos, len);
570  }
571  } else {
572  stats.clear();
573  }
574 }
575 
576 void
577 GlassValueManager::set_value_stats(map<Xapian::valueno, ValueStats> & value_stats)
578 {
579  LOGCALL_VOID(DB, "GlassValueManager::set_value_stats", value_stats);
580  map<Xapian::valueno, ValueStats>::const_iterator i;
581  for (i = value_stats.begin(); i != value_stats.end(); ++i) {
582  string key = make_valuestats_key(i->first);
583  const ValueStats & stats = i->second;
584  if (stats.freq != 0) {
585  string new_value;
586  pack_uint(new_value, stats.freq);
587  pack_string(new_value, stats.lower_bound);
588  // We don't store or count empty values, so neither of the bounds
589  // can be empty. So we can safely store an empty upper bound when
590  // the bounds are equal.
591  if (stats.lower_bound != stats.upper_bound)
592  new_value += stats.upper_bound;
593  postlist_table->add(key, new_value);
594  } else {
595  postlist_table->del(key);
596  }
597  }
598  value_stats.clear();
599  mru_slot = Xapian::BAD_VALUENO;
600 }
GlassCursor * cursor_get() const
Get a cursor for reading from the table.
bool del(std::string_view key)
Delete an entry from the table.
static void throw_database_closed()
Throw an exception indicating that the database is closed.
void add(std::string_view key, std::string_view tag, bool already_compressed=false)
Add a key/tag pair to the table, replacing any existing pair with the same key.
void get_value_stats(Xapian::valueno slot) const
Get the statistics for value slot slot.
void add_document(Xapian::docid did, const Xapian::Document &doc, std::map< Xapian::valueno, ValueStats > &value_stats)
std::string get_value(Xapian::docid did, Xapian::valueno slot) const
void set_value_stats(std::map< Xapian::valueno, ValueStats > &value_stats)
Write the updated statistics to the table.
void remove_value(Xapian::docid did, Xapian::valueno slot)
void get_all_values(std::map< Xapian::valueno, std::string > &values, Xapian::docid did) const
void add_value(Xapian::docid did, Xapian::valueno slot, const std::string &val)
void replace_document(Xapian::docid did, const Xapian::Document &doc, std::map< Xapian::valueno, ValueStats > &value_stats)
Xapian::docid get_chunk_containing_did(Xapian::valueno slot, Xapian::docid did, std::string &chunk) const
void delete_document(Xapian::docid did, std::map< Xapian::valueno, ValueStats > &value_stats)
const std::string & get_value() const
Definition: glass_values.h:202
void assign(const char *p_, size_t len, Xapian::docid did_)
Definition: glass_values.cc:72
Xapian::docid get_docid() const
Definition: glass_values.h:200
void skip_to(Xapian::docid target)
Definition: glass_values.cc:98
Xapian::valueno slot
ValueUpdater(GlassPostListTable *table_, Xapian::valueno slot_)
void update(Xapian::docid did, const string &value)
GlassPostListTable * table
Xapian::docid last_allowed_did
Xapian::docid prev_did
Xapian::docid first_did
Xapian::docid new_first_did
ValueChunkReader reader
void append_to_stream(Xapian::docid did, const string &value)
DatabaseCorruptError indicates database corruption was detected.
Definition: error.h:397
Class representing a document.
Definition: document.h:64
Xapian::docid get_docid() const
Get the document ID this document came from.
Definition: document.cc:69
Xapian::Internal::intrusive_ptr_nonnull< Internal > internal
Definition: document.h:67
ValueIterator values_begin() const
Start iterating the values in this document.
Definition: document.cc:208
ValueIterator values_end() const noexcept
End iterator corresponding to values_begin().
Definition: document.h:259
Indicates an attempt to use a feature which is unavailable.
Definition: error.h:707
RangeError indicates an attempt to access outside the bounds of a container.
Definition: error.h:959
Class for iterating over document values.
Definition: valueiterator.h:39
Xapian::valueno get_valueno() const
Return the value slot number for the current position.
#define UNSIGNED_OVERFLOW_OK(X)
Definition: config.h:626
#define rare(COND)
Definition: config.h:607
PositionList * p
Xapian::termpos pos
Debug logging macros.
#define RETURN(...)
Definition: debuglog.h:484
#define LOGCALL(CATEGORY, TYPE, FUNC, PARAMS)
Definition: debuglog.h:478
#define LOGCALL_STATIC(CATEGORY, TYPE, FUNC, PARAMS)
Definition: debuglog.h:482
#define LOGCALL_VOID(CATEGORY, FUNC, PARAMS)
Definition: debuglog.h:479
Abstract base class for a document.
Hierarchy of classes which Xapian can throw as exceptions.
Interface to Btree cursors.
#define GLASS_MAX_DOCID
The largest docid value supported by glass.
Definition: glass_defs.h:50
Postlists in glass databases.
A TermList in a glass database.
static string make_valuestats_key(Xapian::valueno slot)
Generate a key for a value statistics item.
Definition: glass_values.cc:63
static const size_t CHUNK_SIZE_THRESHOLD
static string make_slot_key(Xapian::docid did)
Generate a key for the "used slots" data.
Definition: glass_values.cc:49
GlassValueManager class.
enc
Definition: header.h:76
std::string make_valuechunk_key(Xapian::valueno slot, Xapian::docid did)
Generate a key for a value stream chunk.
Definition: glass_values.h:41
Xapian::docid docid_from_key(Xapian::valueno required_slot, const std::string &key)
Definition: glass_values.h:50
const valueno BAD_VALUENO
Reserved value to indicate "no valueno".
Definition: types.h:100
unsigned valueno
The number for a value slot in a document.
Definition: types.h:90
unsigned XAPIAN_DOCID_BASE_TYPE docid
A unique identifier for a document.
Definition: types.h:51
#define AssertRelParanoid(A, REL, B)
Definition: omassert.h:130
#define AssertRel(A, REL, B)
Definition: omassert.h:123
#define Assert(COND)
Definition: omassert.h:122
Pack types into strings and unpack them again.
bool unpack_string(const char **p, const char *end, std::string &result)
Decode a std::string from a string.
Definition: pack.h:468
void pack_uint_last(std::string &s, U value)
Append an encoded unsigned integer to a string as the last item.
Definition: pack.h:100
bool unpack_uint(const char **p, const char *end, U *result)
Decode an unsigned integer from a string.
Definition: pack.h:346
void pack_uint(std::string &s, U value)
Append an encoded unsigned integer to a string.
Definition: pack.h:315
void pack_string(std::string &s, std::string_view value)
Append an encoded std::string to a string.
Definition: pack.h:442
bool unpack_uint_preserving_sort(const char **p, const char *end, U *result)
Decode a "sort preserved" unsigned integer from a string.
Definition: pack.h:251
void pack_uint_preserving_sort(std::string &s, U value)
Append an encoded unsigned integer to a string, preserving the sort order.
Definition: pack.h:204
Definition: pretty.h:48
Class to hold statistics for a given slot.
Definition: valuestats.h:28
std::string lower_bound
A lower bound on the values stored in the given value slot.
Definition: valuestats.h:36
std::string upper_bound
An upper bound on the values stored in the given value slot.
Definition: valuestats.h:40
void clear()
Clear the statistics.
Definition: valuestats.h:47
Xapian::doccount freq
The number of documents which have a (non-empty) value stored in the slot.
Definition: valuestats.h:32
Class for iterating over document values.