xapian-core  2.1.0
honey_spelling.cc
Go to the documentation of this file.
1 
4 /* Copyright (C) 2004-2026 Olly Betts
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, see
18  * <https://www.gnu.org/licenses/>.
19  */
20 
21 #include <config.h>
22 
23 #include <xapian/error.h>
24 #include <xapian/types.h>
25 
26 #include "clamp_cast.h"
27 #include "expand/expandweight.h"
28 #include "expand/termlistmerger.h"
29 #include "honey_spelling.h"
30 #include "omassert.h"
31 #include "pack.h"
32 
33 #include "../prefix_compressed_strings.h"
34 
35 #include <algorithm>
36 #include <map>
37 #include <queue>
38 #include <vector>
39 #include <set>
40 #include <string>
41 #include <string_view>
42 
43 using namespace Honey;
44 using namespace std;
45 
46 void
48 {
49  for (auto i : termlist_deltas) {
50  const string& key = i.first;
51  const set<string>& changes = i.second;
52 
53  auto d = changes.begin();
54  if (d == changes.end()) continue;
55 
56  string updated;
57  string current;
58  PrefixCompressedStringWriter out(updated);
59  if (get_exact_entry(key, current)) {
60  PrefixCompressedStringItor in(current, key);
61  updated.reserve(current.size()); // FIXME plus some?
62  while (!in.at_end() && d != changes.end()) {
63  const string& word = *in;
64  Assert(d != changes.end());
65  int cmp = word.compare(*d);
66  if (cmp < 0) {
67  out.append(word);
68  ++in;
69  } else if (cmp > 0) {
70  out.append(*d);
71  ++d;
72  } else {
73  // If an existing entry is in the changes list, that means
74  // we should remove it.
75  ++in;
76  ++d;
77  }
78  }
79  if (!in.at_end()) {
80  // FIXME : easy to optimise this to a fix-up and substring copy.
81  while (!in.at_end()) {
82  out.append(*in++);
83  }
84  }
85  }
86  while (d != changes.end()) {
87  out.append(*d++);
88  }
89  if (!updated.empty()) {
90  add(key, updated);
91  } else {
92  del(key);
93  }
94  }
95  termlist_deltas.clear();
96 
97  for (auto j = wordfreq_changes.begin(); j != wordfreq_changes.end(); ++j) {
98  const string& key = make_spelling_wordlist_key(j->first);
99  Xapian::termcount wordfreq = j->second;
100  if (wordfreq) {
101  string tag;
102  pack_uint_last(tag, wordfreq);
103  add(key, tag);
104  if (wordfreq > wordfreq_upper_bound)
105  wordfreq_upper_bound = wordfreq;
106  } else {
107  del(key);
108  }
109  }
110  wordfreq_changes.clear();
111 }
112 
113 void
115 {
116  auto i = termlist_deltas.find(frag);
117  if (i == termlist_deltas.end()) {
118  i = termlist_deltas.insert(make_pair(frag, set<string>())).first;
119  }
120  // The commonest case is that we're adding lots of words, so try insert
121  // first and if that reports that the word already exists, remove it.
122  auto res = i->second.insert(word);
123  if (!res.second) {
124  // word is already in the set, so remove it.
125  i->second.erase(res.first);
126  }
127 }
128 
129 void
131 {
132  if (word.size() <= 1) return;
133 
134  auto i = wordfreq_changes.find(word);
135  if (i != wordfreq_changes.end()) {
136  // Word "word" already exists and has been modified.
137  if (i->second) {
138  i->second += freqinc;
139  return;
140  }
141  // If "word" is currently modified such that it no longer exists, so
142  // we need to execute the code below to re-add trigrams for it.
143  i->second = freqinc;
144  } else {
145  string data;
146  if (get_exact_entry(make_spelling_wordlist_key(word), data)) {
147  // Word "word" already exists, so increment its count.
148  Xapian::termcount freq;
149  const char* p = data.data();
150  if (!unpack_uint_last(&p, p + data.size(), &freq) || freq == 0) {
151  throw Xapian::DatabaseCorruptError("Bad spelling word freq");
152  }
153  wordfreq_changes[word] = freq + freqinc;
154  return;
155  }
156  wordfreq_changes[word] = freqinc;
157  }
158 
159  // Add trigrams for word.
160  toggle_word(word);
161 }
162 
165 {
166  if (word.size() <= 1) return freqdec;
167 
168  auto i = wordfreq_changes.find(word);
169  if (i != wordfreq_changes.end()) {
170  if (i->second == 0) {
171  // Word has already been deleted.
172  return freqdec;
173  }
174  // Word "word" exists and has been modified.
175  if (freqdec < i->second) {
176  i->second -= freqdec;
177  return 0;
178  }
179  freqdec -= i->second;
180 
181  // Mark word as deleted.
182  i->second = 0;
183  } else {
184  string data;
185  if (!get_exact_entry(make_spelling_wordlist_key(word), data)) {
186  // This word doesn't exist.
187  return freqdec;
188  }
189 
190  Xapian::termcount freq;
191  const char* p = data.data();
192  if (!unpack_uint_last(&p, p + data.size(), &freq)) {
193  throw Xapian::DatabaseCorruptError("Bad spelling word freq");
194  }
195  if (freqdec < freq) {
196  wordfreq_changes[word] = freq - freqdec;
197  return 0;
198  }
199  freqdec -= freq;
200 
201  // Mark word as deleted.
202  wordfreq_changes[word] = 0;
203  }
204 
205  // Remove trigrams for word.
206  toggle_word(word);
207 
208  return freqdec;
209 }
210 
211 void
213 {
214  fragment buf(0);
215 
216  if (word.size() <= 4) {
217  // We also generate 'bookends' for two, three, and four character
218  // terms so we can handle transposition of the middle two characters
219  // of a four character word, substitution or deletion of the middle
220  // character of a three character word, or insertion in the middle of a
221  // two character word.
222  // 'Bookends':
223  buf[0] = KEY_PREFIX_BOOKEND;
224  buf[1] = word[0];
225  buf[2] = word[word.size() - 1];
226  toggle_fragment(buf, word);
227  }
228 
229  // Head:
230  buf[0] = KEY_PREFIX_HEAD;
231  buf[1] = word[0];
232  buf[2] = word[1];
233  toggle_fragment(buf, word);
234 
235  // Tail:
236  buf[0] = KEY_PREFIX_TAIL;
237  buf[1] = word[word.size() - 2];
238  buf[2] = word[word.size() - 1];
239  toggle_fragment(buf, word);
240 
241  if (word.size() > 2) {
242  set<fragment> done;
243  // Middles:
244  buf[0] = KEY_PREFIX_MIDDLE;
245  for (size_t start = 0; start <= word.size() - 3; ++start) {
246  memcpy(buf.data + 1, word.data() + start, 3);
247  // Don't toggle the same fragment twice or it will cancel out.
248  // Bug fixed in 1.2.6.
249  if (done.insert(buf).second)
250  toggle_fragment(buf, word);
251  }
252  }
253 }
254 
256  bool operator()(const TermList* a, const TermList* b) const {
257  return a->get_approx_size() > b->get_approx_size();
258  }
259 };
260 
261 TermList*
263 {
264  // This should have been handled by Database::get_spelling_suggestion().
265  AssertRel(word.size(),>,1);
266 
267  // Merge any pending changes to disk, but don't call commit() so they
268  // won't be switched live.
269  if (!wordfreq_changes.empty()) merge_changes();
270 
271  vector<TermList*> termlists;
272  try {
273  string data;
274  fragment buf(0);
275 
276  if (word.size() <= 4) {
277  // We also generate 'bookends' for two, three, and four character
278  // terms so we can handle transposition of the middle two
279  // characters of a four character word, substitution or deletion of
280  // the middle character of a three character word, or insertion in
281  // the middle of a two character word.
282  buf[0] = KEY_PREFIX_BOOKEND;
283  buf[1] = word[0];
284  buf[2] = word[word.size() - 1];
285  if (get_exact_entry(string(buf), data))
286  termlists.push_back(new HoneySpellingTermList(data, buf.data));
287  }
288 
289  // Head:
290  buf[0] = KEY_PREFIX_HEAD;
291  buf[1] = word[0];
292  buf[2] = word[1];
293  if (get_exact_entry(string(buf), data))
294  termlists.push_back(new HoneySpellingTermList(data, buf.data));
295 
296  if (word.size() == 2) {
297  // For two letter words, we generate H and T terms for the
298  // transposed form so that we can produce good spelling
299  // suggestions.
300  // AB -> BA
301  buf[1] = word[1];
302  buf[2] = word[0];
303  if (get_exact_entry(string(buf), data))
304  termlists.push_back(new HoneySpellingTermList(data, buf.data));
305  buf[0] = KEY_PREFIX_TAIL;
306  if (get_exact_entry(string(buf), data))
307  termlists.push_back(new HoneySpellingTermList(data, buf.data));
308  }
309 
310  // Tail:
311  buf[0] = KEY_PREFIX_TAIL;
312  buf[1] = word[word.size() - 2];
313  buf[2] = word[word.size() - 1];
314  if (get_exact_entry(string(buf), data))
315  termlists.push_back(new HoneySpellingTermList(data, buf.data));
316 
317  if (word.size() > 2) {
318  // Middles:
319  buf[0] = KEY_PREFIX_MIDDLE;
320  for (size_t start = 0; start <= word.size() - 3; ++start) {
321  memcpy(buf.data + 1, word.data() + start, 3);
322  if (get_exact_entry(string(buf), data))
323  termlists.push_back(new HoneySpellingTermList(data));
324  }
325 
326  if (word.size() == 3) {
327  // For three letter words, we generate the two "single
328  // transposition" forms too, so that we can produce good
329  // spelling suggestions.
330  // ABC -> BAC
331  buf[1] = word[1];
332  buf[2] = word[0];
333  if (get_exact_entry(string(buf), data))
334  termlists.push_back(new HoneySpellingTermList(data));
335  // ABC -> ACB
336  buf[1] = word[0];
337  buf[2] = word[2];
338  buf[3] = word[1];
339  if (get_exact_entry(string(buf), data))
340  termlists.push_back(new HoneySpellingTermList(data));
341  }
342  }
343 
344  return make_termlist_merger(termlists);
345  } catch (...) {
346  // Make sure we delete all the TermList objects to avoid leaking
347  // memory.
348  for (auto& t : termlists) {
349  delete t;
350  }
351  throw;
352  }
353 }
354 
357 {
358  auto i = wordfreq_changes.find(word);
359  if (i != wordfreq_changes.end()) {
360  // Modified frequency for word:
361  return i->second;
362  }
363 
364  string data;
365  if (get_exact_entry(make_spelling_wordlist_key(word), data)) {
366  // Word "word" already exists.
367  Xapian::termcount freq;
368  const char* p = data.data();
369  if (!unpack_uint_last(&p, p + data.size(), &freq)) {
370  throw Xapian::DatabaseCorruptError("Bad spelling word freq");
371  }
372  return freq;
373  }
374 
375  return 0;
376 }
377 
379 
382 {
383  // This is only used to decide how to build a OR-tree of TermList objects
384  // so we just need to return "sizes" which are ordered roughly correctly.
385  return clamp_cast<Xapian::termcount>(data.size());
386 }
387 
390 {
391  return 1;
392 }
393 
396 {
397  return 1;
398 }
399 
400 TermList*
402 {
403  if (p == data.size()) {
404  return this;
405  }
406 
407  size_t keep = 0;
408  if (rare(tail < 0)) {
409  tail += 2;
410  keep = current_term.size() - tail;
411  } else if (usual(!current_term.empty())) {
412  keep = data[p++] ^ MAGIC_XOR_VALUE;
413  }
414  size_t add;
415  if (p == data.size() ||
416  (add = data[p] ^ MAGIC_XOR_VALUE) >= data.size() - p) {
417  throw Xapian::DatabaseCorruptError("Bad spelling data (too little "
418  "left)");
419  }
420  if (rare(keep + tail > current_term.size())) {
421  // The initial part to keep overlaps with the tail part which is an
422  // unusual case requiring special handling.
423  string tail_string(current_term, current_term.size() - tail);
424  current_term.replace(keep, string::npos, data.data() + p + 1, add);
425  current_term += tail_string;
426  } else {
427  current_term.replace(keep, current_term.size() - tail - keep,
428  data.data() + p + 1, add);
429  }
430  p += add + 1;
431 
432  return NULL;
433 }
434 
435 TermList*
437 {
438  while (current_term < term) {
440  return this;
441  }
442  return NULL;
443 }
444 
447 {
448  throw
449  Xapian::UnimplementedError("HoneySpellingTermList::"
450  "positionlist_count() "
451  "not implemented");
452 }
453 
456 {
457  throw
458  Xapian::UnimplementedError("HoneySpellingTermList::"
459  "positionlist_begin() "
460  "not implemented");
461 }
#define MAGIC_XOR_VALUE
Cast a value to a type, clamping out of range values.
void merge_changes()
Merge in batched-up changes.
void toggle_word(const std::string &word)
Xapian::termcount remove_word(const std::string &word, Xapian::termcount freqdec)
void toggle_fragment(Honey::fragment frag, const std::string &word)
TermList * open_termlist(std::string_view word)
Xapian::doccount get_word_frequency(std::string_view word) const
void add_word(const std::string &word, Xapian::termcount freqinc)
The list of words containing a particular trigram.
TermList * next()
Advance the current position to the next term in the termlist.
Xapian::termcount positionlist_count() const
Return the length of the position list for the current position.
Xapian::termcount get_wdf() const
Return the wdf for the term at the current position.
PositionList * positionlist_begin() const
Return PositionList for the current position.
TermList * skip_to(std::string_view term)
Skip forward to the specified term.
Xapian::doccount get_termfreq() const
Return the term frequency for the term at the current position.
Xapian::termcount get_approx_size() const
Return approximate size of this termlist.
void append(const std::string &word)
DatabaseCorruptError indicates database corruption was detected.
Definition: error.h:397
Abstract base class for iterating term positions in a document.
Definition: positionlist.h:32
Abstract base class for termlists.
Definition: termlist.h:42
virtual Xapian::termcount get_approx_size() const =0
Return approximate size of this termlist.
UnimplementedError indicates an attempt to use an unimplemented feature.
Definition: error.h:313
#define usual(COND)
Definition: config.h:617
#define rare(COND)
Definition: config.h:616
string term
PositionList * p
Hierarchy of classes which Xapian can throw as exceptions.
Collate statistics and calculate the term weights for the ESet.
Spelling correction data for a honey database.
const unsigned KEY_PREFIX_MIDDLE
const unsigned KEY_PREFIX_TAIL
std::string make_spelling_wordlist_key(std::string_view word)
const unsigned KEY_PREFIX_BOOKEND
const unsigned KEY_PREFIX_HEAD
unsigned XAPIAN_TERMCOUNT_BASE_TYPE termcount
A counts of terms.
Definition: types.h:64
unsigned XAPIAN_DOCID_BASE_TYPE doccount
A count of documents.
Definition: types.h:37
Various assertion macros.
#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_uint_last(const char **p, const char *end, U *result)
Decode an unsigned integer as the last item in a string.
Definition: pack.h:118
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 operator()(const TermList *a, const TermList *b) const
Build tree to merge TermList objects.
TermList * make_termlist_merger(std::vector< TermList * > &termlists)
typedefs for Xapian