blob: 6eda410273acf5b0855333167be30884e1527f5b [file] [log] [blame]
Marc Kupietz4b799e92018-01-02 11:04:56 +01001#define EXPORT __attribute__((visibility("visible")))
2#define IMPORT
Marc Kupietz12af0192021-03-13 18:05:14 +01003
Marc Kupietz39887082024-11-22 18:06:20 +01004#include "config.h"
5#include "export.h"
6#include "merge_operators.h"
Marc Kupietz28cc53e2017-12-23 17:24:55 +01007#include "rocksdb/db.h"
8#include "rocksdb/env.h"
Marc Kupietzc8ddf452018-01-07 21:33:12 +01009#include "rocksdb/table.h"
Marc Kupietze889cec2024-11-23 12:08:42 +010010#include "rocksdb/slice.h"
Marc Kupietz39887082024-11-22 18:06:20 +010011#include <algorithm>
Marc Kupietz2f65bcf2026-08-02 14:04:37 +020012#include <atomic>
Marc Kupietz39887082024-11-22 18:06:20 +010013#include <cassert>
14#include <cmath>
15#include <cstdint>
16#include <iostream>
17#include <memory>
Marc Kupietz122ba3c2026-08-09 17:13:24 +020018#include <mutex>
Marc Kupietz28cc53e2017-12-23 17:24:55 +010019#include <rocksdb/merge_operator.h>
Marc Kupietzc8ddf452018-01-07 21:33:12 +010020#include <rocksdb/slice_transform.h>
Marc Kupietz65d44792026-07-31 09:46:34 +090021#include <rocksdb/version.h>
Marc Kupietz122ba3c2026-08-09 17:13:24 +020022#include "rocksdb/write_batch.h"
Marc Kupietz39887082024-11-22 18:06:20 +010023#include <sstream> // for ostringstream
24#include <string>
Marc Kupietzc630c152025-01-23 11:17:47 +010025#include <thread>
Marc Kupietz65d44792026-07-31 09:46:34 +090026#include <utility>
Marc Kupietz39887082024-11-22 18:06:20 +010027#include <vector>
Marc Kupietz28cc53e2017-12-23 17:24:55 +010028
Marc Kupietz65d44792026-07-31 09:46:34 +090029/* Since rocksdb 11 DB::Open() and DB::OpenForReadOnly() hand the database back
30 as a unique_ptr instead of a raw pointer. */
31#if ROCKSDB_MAJOR >= 11
32#define ROCKSDB_DB_HANDLE std::unique_ptr<rocksdb::DB>
33#define ROCKSDB_DB_RELEASE(handle) (handle).release()
34#else
35#define ROCKSDB_DB_HANDLE rocksdb::DB *
36#define ROCKSDB_DB_RELEASE(handle) (handle)
37#endif
38
Marc Kupietz75af60f2019-01-22 22:34:29 +010039#define WINDOW_SIZE 5
Marc Kupietz98cbcdc2019-01-21 17:11:27 +010040#define FREQUENCY_THRESHOLD 5
Marc Kupietz28cc53e2017-12-23 17:24:55 +010041#define IS_BIG_ENDIAN (*(uint16_t *)"\0\xff" < 0x100)
Marc Kupietz39887082024-11-22 18:06:20 +010042#define encodeCollocation(w1, w2, dist) \
43 (((uint64_t)dist << 56) | ((uint64_t)w2 << 24) | w1)
Marc Kupietz18375e12017-12-24 10:11:18 +010044#define W1(key) (uint64_t)(key & 0xffffff)
45#define W2(key) (uint64_t)((key >> 24) & 0xffffff)
46#define DIST(key) (int8_t)((uint64_t)((key >> 56) & 0xff))
Marc Kupietzc8ddf452018-01-07 21:33:12 +010047
48typedef struct {
49 uint64_t freq;
50 char *word;
Marc Kupietz12af0192021-03-13 18:05:14 +010051} vocab_entry;
Marc Kupietzc8ddf452018-01-07 21:33:12 +010052
53// typedef struct Collocator {
54// uint64_t w2;
55// uint64_t sum;
56// };
57
Marc Kupietz28cc53e2017-12-23 17:24:55 +010058using namespace rocksdb;
Marc Kupietzc8ddf452018-01-07 21:33:12 +010059using namespace std;
Marc Kupietz28cc53e2017-12-23 17:24:55 +010060
Marc Kupietz4b799e92018-01-02 11:04:56 +010061namespace rocksdb {
Marc Kupietz39887082024-11-22 18:06:20 +010062class Collocator {
63public:
64 uint32_t w2;
65 uint64_t f2;
66 uint64_t raw;
67 double pmi;
68 double npmi;
69 double llr;
70 double lfmd;
71 double md;
Marc Kupietze889cec2024-11-23 12:08:42 +010072 double md_nws;
Marc Kupietz39887082024-11-22 18:06:20 +010073 uint64_t left_raw;
74 uint64_t right_raw;
75 double left_pmi;
76 double right_pmi;
77 double dice;
78 double logdice;
79 double ldaf;
80 int window;
81 int af_window;
Marc Kupietz28cc53e2017-12-23 17:24:55 +010082};
Marc Kupietz06c9a9f2018-01-02 16:56:43 +010083
Marc Kupietz39887082024-11-22 18:06:20 +010084size_t num_merge_operator_calls;
85
86void resetNumMergeOperatorCalls() { num_merge_operator_calls = 0; }
87
88size_t num_partial_merge_calls;
89
90void resetNumPartialMergeCalls() { num_partial_merge_calls = 0; }
91
92inline void EncodeFixed64(char *buf, uint64_t value) {
93 if (!IS_BIG_ENDIAN) {
94 memcpy(buf, &value, sizeof(value));
95 } else {
96 buf[0] = value & 0xff;
97 buf[1] = (value >> 8) & 0xff;
98 buf[2] = (value >> 16) & 0xff;
99 buf[3] = (value >> 24) & 0xff;
100 buf[4] = (value >> 32) & 0xff;
101 buf[5] = (value >> 40) & 0xff;
102 buf[6] = (value >> 48) & 0xff;
103 buf[7] = (value >> 56) & 0xff;
104 }
Marc Kupietz4a5e08a2018-06-05 11:07:11 +0200105}
106
Marc Kupietz39887082024-11-22 18:06:20 +0100107inline uint32_t DecodeFixed32(const char *ptr) {
108 if (!IS_BIG_ENDIAN) {
109 // Load the raw bytes
110 uint32_t result;
111 memcpy(&result, ptr, sizeof(result)); // gcc optimizes this to a plain load
112 return result;
113 } else {
114 return ((static_cast<uint32_t>(static_cast<unsigned char>(ptr[0]))) |
115 (static_cast<uint32_t>(static_cast<unsigned char>(ptr[1])) << 8) |
116 (static_cast<uint32_t>(static_cast<unsigned char>(ptr[2])) << 16) |
117 (static_cast<uint32_t>(static_cast<unsigned char>(ptr[3])) << 24));
118 }
119}
120
121inline uint64_t DecodeFixed64(const char *ptr) {
122 if (!IS_BIG_ENDIAN) {
123 // Load the raw bytes
124 uint64_t result;
125 memcpy(&result, ptr, sizeof(result)); // gcc optimizes this to a plain load
126 return result;
127 } else {
128 uint64_t lo = DecodeFixed32(ptr);
129 uint64_t hi = DecodeFixed32(ptr + 4);
130 return (hi << 32) | lo;
131 }
132}
133
134static inline double ca_pmi(uint64_t f1, uint64_t f2, uint64_t f12,
135 uint64_t total, double window_size) {
136 double r1 = f1 * window_size, c1 = f2, e = r1 * c1 / total, o = f12;
137 if (f12 < FREQUENCY_THRESHOLD)
138 return -1.0;
139 else
140 return log2(o / e);
141}
142
143// Bouma, Gerlof (2009): <a
144// href="https://svn.spraakdata.gu.se/repos/gerlof/pub/www/Docs/npmi-pfd.pdf">
145// Normalized (pointwise) mutual information in collocation extraction</a>. In
146// Proceedings of GSCL.
147static double ca_npmi(uint64_t f1, uint64_t f2, uint64_t f12,
148 uint64_t total, double window_size) {
149 double r1 = f1 * window_size, c1 = f2, e = r1 * c1 / total, o = f12;
150 if (f12 < FREQUENCY_THRESHOLD)
151 return -1.0;
152 else
153 return log2(o / e) / (-log2(o / total / window_size));
154}
155
156// Thanopoulos, A., Fakotakis, N., Kokkinakis, G.: Comparative evaluation of
157// collocation extraction metrics. In: International Conference on Language
158// Resources and Evaluation (LREC-2002). (2002) 620–625 double md =
159// log2(pow((double)max * window_size / total, 2) / (window_size *
160// ((double)_vocab[w1].freq/total) * ((double)_vocab[last_w2].freq/total)));
161static double ca_md(uint64_t f1, uint64_t f2, uint64_t f12,
162 uint64_t total, double window_size) {
163 const double r1 = f1 * window_size;
164 const double c1 = f2;
165 const double e = r1 * c1 / total;
166 const double o = f12;
167 return log2(o * o / e);
168}
169
170static double ca_lfmd(uint64_t f1, uint64_t f2, uint64_t f12,
171 uint64_t total, double window_size) {
172 double r1 = f1 * window_size, c1 = f2, e = r1 * c1 / total, o = f12;
173 if (f12 == 0)
174 return 0;
175 return log2(o * o * o / e);
176}
177
178// Evert, Stefan (2004): The Statistics of Word Cooccurrences: Word Pairs and
179// Collocations. PhD dissertation, IMS, University of Stuttgart. Published in
180// 2005, URN urn:nbn:de:bsz:93-opus-23714. Free PDF available from
181// http://purl.org/stefan.evert/PUB/Evert2004phd.pdf
182static double ca_ll(uint64_t w1, uint64_t w2, uint64_t w12, uint64_t n,
183 uint64_t window_size) {
184 double r1 = (double)w1 * window_size, r2 = (double)n - r1, c1 = w2,
185 c2 = n - c1, o11 = w12, o12 = r1 - o11, o21 = c1 - w12, o22 = r2 - o21,
186 e11 = r1 * c1 / n, e12 = r1 * c2 / n, e21 = r2 * c1 / n,
187 e22 = r2 * c2 / n;
188 return (2 * ((o11 > 0 ? o11 * log(o11 / e11) : 0) +
189 (o12 > 0 ? o12 * log(o12 / e12) : 0) +
190 (o21 > 0 ? o21 * log(o21 / e21) : 0) +
191 (o22 > 0 ? o22 * log(o22 / e22) : 0)));
192}
193
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200194// The Dice coefficient relates the co-occurrence frequency to how often the two
195// words occur at all, so its denominator sums marginal word frequencies. It
196// therefore takes no window size factor, unlike the scores above, which need one
197// in their expected frequency: w1 * window_size would count window positions
198// rather than word tokens, and adding that to w2 would also make the coefficient
199// asymmetric.
Marc Kupietz39887082024-11-22 18:06:20 +0100200static double ca_dice(uint64_t w1, uint64_t w2, uint64_t w12, uint64_t n,
201 uint64_t window_size) {
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200202 double r1 = (double)w1, c1 = w2;
Marc Kupietz39887082024-11-22 18:06:20 +0100203 return 2 * w12 / (c1 + r1);
204}
205
206// Rychlý, Pavel (2008): <a
207// href="http://www.fi.muni.cz/usr/sojka/download/raslan2008/13.pdf">A
208// lexicographer-friendly association score.</a> In Proceedings of Recent
209// Advances in Slavonic Natural Language Processing, RASLAN, 6–9.
210static double ca_logdice(uint64_t w1, uint64_t w2, uint64_t w12,
211 uint64_t n, uint64_t window_size) {
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200212 double r1 = (double)w1, c1 = w2;
213 return 14 + log2(2 * w12 / (c1 + r1));
214}
215
216// The auto focus score, reported as LDaf: its maximum over all selections of
217// positions both picks the auto focus window and is the value shown for it. It
218// used to be computed by ca_logdice(), which is why that one carried a window
219// size factor.
220//
221// It is Dice-like, but it is not logDice and the two are not on a common scale:
222// multiplying f1 by the number of positions penalizes wide windows. That
223// penalty is the point. It makes the score sensitive to how concentrated a pair
224// is, which is what tells actual collocations from words that merely share
225// contexts, and is why LDaf orders collocates more usefully than the other
226// measures. logDice itself cannot serve here: its denominator does not depend
227// on the window while the co-occurrence count only grows as positions are
228// added, so the widest window would always win.
229static double ca_focus_score(uint64_t w1, uint64_t w2, uint64_t w12, uint64_t n,
230 uint64_t window_size) {
Marc Kupietz39887082024-11-22 18:06:20 +0100231 double r1 = (double)w1 * window_size, c1 = w2;
232 return 14 + log2(2 * w12 / (c1 + r1));
233}
234
235class CountMergeOperator : public AssociativeMergeOperator {
236public:
237 CountMergeOperator() {
238 mergeOperator_ = MergeOperators::CreateUInt64AddOperator();
239 }
240
241 bool Merge(const Slice &key, const Slice *existing_value,
242 const Slice &value, std::string *new_value,
243 Logger *logger) const override {
244 assert(new_value->empty());
245 ++num_merge_operator_calls;
246 if (existing_value == nullptr) {
247 new_value->assign(value.data(), value.size());
248 return true;
249 }
250
251 return mergeOperator_->PartialMerge(key, *existing_value, value, new_value,
252 logger);
253 }
254
255 const char *Name() const override { return "UInt64AddOperator"; }
256
257private:
258 std::shared_ptr<MergeOperator> mergeOperator_;
259};
260
261class CollocatorIterator : public Iterator {
262 char prefixc[sizeof(uint64_t)]{};
263 Iterator *base_iterator_;
264
265public:
266 explicit CollocatorIterator(Iterator *base_iterator) : base_iterator_(base_iterator) {}
267
Marc Kupietzaa354d82026-07-31 09:17:57 +0900268 /* Takes ownership of the iterator handed in by SeekIterator(). Without this
269 every seek leaked a rocksdb iterator and the resources it pins. */
270 ~CollocatorIterator() override { delete base_iterator_; }
271
Marc Kupietz39887082024-11-22 18:06:20 +0100272 void setPrefix(char *prefix) { memcpy(prefixc, prefix, sizeof(uint64_t)); }
273
274 void SeekToFirst() override { base_iterator_->SeekToFirst(); }
275
276 void SeekToLast() override { base_iterator_->SeekToLast(); }
277
278 void Seek(const rocksdb::Slice &s) override { base_iterator_->Seek(s); }
279
280 void SeekForPrev(const rocksdb::Slice &s) override {
281 base_iterator_->SeekForPrev(s);
282 }
283
284 void Prev() override { base_iterator_->Prev(); }
285
286 void Next() override { base_iterator_->Next(); }
287
288 Slice key() const override;
289
290 Slice value() const override;
291
292 Status status() const override;
293
294 bool Valid() const override;
295
296 bool isValid();
297
298 uint64_t intValue();
299
300 uint64_t intKey();
301};
302
303// rocksdb::CollocatorIterator::CollocatorIterator(Iterator* base_iterator) {}
304
305bool CollocatorIterator::Valid() const {
306 return base_iterator_->Valid() && key().starts_with(std::string(prefixc, 3));
307}
308
309bool CollocatorIterator::isValid() {
310 return base_iterator_->Valid() && key().starts_with(std::string(prefixc, 3));
311 // return key().starts_with(std::string(prefixc,3));
312}
313
314uint64_t CollocatorIterator::intKey() {
315 return DecodeFixed64(base_iterator_->key().data());
316}
317
318uint64_t CollocatorIterator::intValue() {
319 return DecodeFixed64(base_iterator_->value().data());
320}
321
322class VocabEntry {
323public:
324 string word;
325 uint64_t freq;
326};
327
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200328static uint64_t env_size(const char *name, uint64_t fallback);
329
Marc Kupietz39887082024-11-22 18:06:20 +0100330class CollocatorDB {
331 WriteOptions merge_option_; // for merge
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200332 // to repeat a write that rocksdb cancelled, rather than lose the count
333 WriteOptions blocking_merge_option_;
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200334 mutable std::atomic<uint64_t> stalled_writes_{0};
335 mutable std::atomic<uint64_t> failed_writes_{0};
Marc Kupietz39887082024-11-22 18:06:20 +0100336 char _one[sizeof(uint64_t)]{};
337 Slice _one_slice;
338 vector<VocabEntry> _vocab;
339 uint64_t total = 0;
340 uint64_t sentences = 0;
341 float avg_window_size = 8.0;
342
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200343 /* Single-key merges are collected in a batch, so that a long indexing run
344 does one rocksdb write per batch instead of one per collocation pair.
345 rocksdb inserts the merge operands of a key in order anyway, which is why
346 the collection point has a mutex: it is the same serialization rocksdb
347 would impose on the writes.
348
349 They are mutable so that the const read methods can write out what is
350 still buffered: a read has to see every increment that came before it. */
351 mutable std::mutex batch_mutex_;
352 mutable WriteBatch batch_;
353 size_t batch_target_ = 65536;
354
355 /* Writes out a batch of merge operands, retrying a cancelled write the same
356 way merge_one() used to, before the merges were batched.
357
358 Takes a batch that merge_one() or flush() took out of the shared batch,
359 and is called without batch_mutex_: the write is the slow part, and the
360 other threads must be able to keep accumulating while it is in flight.
361 Holding the mutex across the write made the indexer run slower with many
362 threads than with one, because the threads queued up on the mutex and
363 rocksdb never saw concurrent writers to batch into one. */
364 void write_batch(WriteBatch &to_write) const {
365 if (to_write.Count() == 0)
366 return;
367 Status s = db_->Write(merge_option_, &to_write);
368 if (s.ok())
369 return;
370 if (s.IsIncomplete()) {
371 ++stalled_writes_;
372 s = db_->Write(blocking_merge_option_, &to_write);
373 if (s.ok())
374 return;
375 }
376 if (failed_writes_++ == 0)
377 std::cerr << "collocatordb: cannot write, counts are lost: "
378 << s.ToString() << std::endl;
379 }
380
381 /* A read has to see the increments that are still in the batch, so the read
382 methods call this first. */
383 void flush() const {
384 WriteBatch to_write;
385 {
386 std::lock_guard<std::mutex> lock(batch_mutex_);
387 std::swap(batch_, to_write);
388 }
389 write_batch(to_write);
390 }
391
Marc Kupietz39887082024-11-22 18:06:20 +0100392protected:
393 std::shared_ptr<DB> db_;
394
395 WriteOptions put_option_;
396 ReadOptions get_option_;
397 WriteOptions delete_option_;
398
399 uint64_t default_{};
400
401 std::shared_ptr<DB> OpenDb(const char *dbname);
402
403 std::shared_ptr<DB> OpenDbForRead(const char *dbname);
404
405public:
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200406 virtual ~CollocatorDB(); // flushes what is still in memory, see close()
Marc Kupietz39887082024-11-22 18:06:20 +0100407 void readVocab(const string& fname);
408 string getWord(uint32_t w1);
409
410 uint64_t getWordId(const char *word) const;
411
Marc Kupietzd26b1052024-12-10 16:56:39 +0100412 uint64_t getCorpusSize() const;
413
Marc Kupietz21b964c2024-12-10 17:10:50 +0100414 uint64_t getWordFrequency(uint64_t w1);
415
Marc Kupietz39887082024-11-22 18:06:20 +0100416 CollocatorDB(const char *db_name, bool read_only);
417
418 // public interface of CollocatorDB.
419 // All four functions return false
420 // if the underlying level db operation failed.
421
422 // mapped to a levedb Put
423 bool set(const std::string &key, uint64_t value) {
424 // just treat the internal rep of int64 as the string
425 char buf[sizeof(value)];
426 EncodeFixed64(buf, value);
427 Slice slice(buf, sizeof(value));
428 auto s = db_->Put(put_option_, key, slice);
429
430 if (s.ok()) {
431 return true;
432 } else {
433 std::cerr << s.ToString() << std::endl;
434 return false;
435 }
436 }
437
438 DB *getDb() { return db_.get(); }
439
440 // mapped to a rocksdb Delete
441 bool remove(const std::string &key) {
442 auto s = db_->Delete(delete_option_, key);
443
444 if (s.ok()) {
445 return true;
446 } else {
447 std::cerr << s.ToString() << std::endl;
448 return false;
449 }
450 }
451
452 // mapped to a rocksdb Get
453 bool get(const std::string &key, uint64_t *value) {
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200454 flush();
Marc Kupietz39887082024-11-22 18:06:20 +0100455 std::string str;
456 auto s = db_->Get(get_option_, key, &str);
457
458 if (s.IsNotFound()) {
459 // return default value if not found;
460 *value = default_;
461 return true;
462 } else if (s.ok()) {
463 // deserialization
464 if (str.size() != sizeof(uint64_t)) {
465 std::cerr << "value corruption\n";
466 return false;
467 }
468 *value = DecodeFixed64(&str[0]);
469 return true;
470 } else {
471 std::cerr << s.ToString() << std::endl;
472 return false;
473 }
474 }
475
476 uint64_t get(const uint32_t w1, const uint32_t w2, const int8_t dist) {
477 char encoded_key[sizeof(uint64_t)];
478 EncodeFixed64(encoded_key, encodeCollocation(w1, w2, dist));
479 uint64_t value = default_;
480 get(std::string(encoded_key, 8), &value);
481 return value;
482 }
483
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200484 /* A merge that does not lose the count. rocksdb cancels a write with
485 Status::Incomplete instead of waiting when it is asked not to slow down,
486 and then the increment is simply gone. Such a write was not applied, so
487 repeating it blocking cannot count twice. */
488 void merge_one(const Slice &key) {
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200489 WriteBatch to_write;
490 {
491 std::lock_guard<std::mutex> lock(batch_mutex_);
492 batch_.Merge(key, _one_slice);
493 if (batch_.Count() >= (int)batch_target_)
494 std::swap(batch_, to_write);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200495 }
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200496 write_batch(to_write);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200497 }
498
Marc Kupietz39887082024-11-22 18:06:20 +0100499 virtual void inc(const std::string &key) {
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200500 merge_one(Slice(key));
Marc Kupietz39887082024-11-22 18:06:20 +0100501 }
502
503 void inc(const uint64_t key) {
504 char encoded_key[sizeof(uint64_t)];
505 EncodeFixed64(encoded_key, key);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200506 merge_one(Slice(encoded_key, sizeof(uint64_t)));
Marc Kupietz39887082024-11-22 18:06:20 +0100507 }
508
509 virtual void inc(uint32_t w1, uint32_t w2, uint8_t dist);
510
511 void dump(uint32_t w1, uint32_t w2, int8_t dist) const;
512
513 vector<Collocator> get_collocators(uint32_t w1);
514
515 vector<Collocator> get_collocators(uint32_t w1, uint32_t max_w2);
516
517 vector<Collocator> get_collocation_scores(uint32_t w1, uint32_t w2);
518
519 vector<Collocator> get_collocators(uint32_t w1, uint32_t min_w2,
520 uint32_t max_w2);
521
522 void applyCAMeasures(uint32_t w1, uint32_t w2,
523 uint64_t *sumWindow, uint64_t sum,
524 int usedPositions, int true_window_size,
525 Collocator *result) const;
526
527 void dumpSparseLlr(uint32_t w1, uint32_t min_cooccur);
528
529 string collocators2json(uint32_t w1, const vector<Collocator>& collocators);
530
531 // mapped to a rocksdb Merge operation
532 virtual bool add(const std::string &key, uint64_t value) {
533 char encoded[sizeof(uint64_t)];
534 EncodeFixed64(encoded, value);
535 Slice slice(encoded, sizeof(uint64_t));
536 auto s = db_->Merge(merge_option_, key, slice);
537
538 if (s.ok()) {
539 return true;
540 } else {
541 std::cerr << s.ToString() << std::endl;
542 return false;
543 }
544 }
545
546 CollocatorIterator *SeekIterator(uint64_t w1, uint64_t w2, int8_t dist) const;
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200547
548 /* Writes what is still in memory and closes the database. Without this
549 everything that has not been flushed yet is lost when the process ends,
550 because the write ahead log is switched off for speed. */
551 void close() {
552 if (!db_)
553 return;
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200554 flush();
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200555 if (stalled_writes_ > 0)
556 std::cerr << "collocatordb: repeated " << stalled_writes_
557 << " writes that rocksdb had cancelled" << std::endl;
558 if (failed_writes_ > 0)
559 std::cerr << "collocatordb: " << failed_writes_
560 << " writes failed, the database is missing counts" << std::endl;
561 FlushOptions flush_options;
562 flush_options.wait = true;
563 Status s = db_->Flush(flush_options);
564 if (!s.ok() && !s.IsNotSupported())
565 std::cerr << "collocatordb: cannot write what is still in memory: "
566 << s.ToString() << std::endl;
567 db_.reset();
568 }
569
Marc Kupietz39887082024-11-22 18:06:20 +0100570};
571
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200572CollocatorDB::~CollocatorDB() { close(); }
573
Marc Kupietz39887082024-11-22 18:06:20 +0100574CollocatorDB::CollocatorDB(const char *db_name,
575 bool read_only = false) {
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200576 batch_target_ = (size_t)env_size("COLLOCATORDB_BATCH_SIZE", 65536);
Marc Kupietz39887082024-11-22 18:06:20 +0100577 // merge_option_.sync = true;
578 if (read_only)
579 db_ = OpenDbForRead(strdup(db_name));
580 else
581 db_ = OpenDb(db_name);
582 assert(db_);
583 uint64_t one = 1;
584 EncodeFixed64(_one, one);
585 _one_slice = Slice(_one, sizeof(uint64_t));
586}
587
588void CollocatorDB::inc(const uint32_t w1, const uint32_t w2,
589 const uint8_t dist) {
590 inc(encodeCollocation(w1, w2, dist));
591}
592
593void CollocatorDB::readVocab(const string& fname) {
594 char strbuf[2048];
595 uint64_t freq;
596 FILE *fin = fopen(fname.c_str(), "rb");
597 if (fin == nullptr) {
598 cout << "Vocabulary file " << fname << " not found\n";
599 exit(1);
600 }
601 uint64_t i = 0;
602 while (fscanf(fin, "%s %lu", strbuf, &freq) == 2) {
603 _vocab.push_back({strbuf, freq});
604 total += freq;
605 i++;
606 }
607 fclose(fin);
608
609 char size_fname[256];
610 strcpy(size_fname, fname.c_str());
611 char *pos = strstr(size_fname, ".vocab");
612 if (pos) {
613 *pos = 0;
614 strcat(size_fname, ".size");
615 FILE *fp = fopen(size_fname, "r");
616 if (fp != nullptr) {
617 fscanf(fp, "%lu", &sentences);
618 fscanf(fp, "%lu", &total);
619 float sl = (float)total / (float)sentences;
620 float w = WINDOW_SIZE;
621 avg_window_size =
622 ((sl > 2 * w ? (sl - 2 * w) * 2 * w : 0) + (double)w * (3 * w - 1)) /
623 sl;
624 fprintf(stdout,
625 "Size corrections found: corpus size: %lu tokens in %lu "
626 "sentences, avg. sentence size: %f, avg. window size: %f\n",
627 total, sentences, sl, avg_window_size);
628 fclose(fp);
629 } else {
630 // std::cout << "size file " << size_fname << " not found\n";
631 }
632 } else {
633 std::cout << "cannot determine size file " << size_fname << "\n";
634 }
635}
636
637std::shared_ptr<DB> CollocatorDB::OpenDbForRead(const char *name) {
Marc Kupietz65d44792026-07-31 09:46:34 +0900638 ROCKSDB_DB_HANDLE db;
Marc Kupietz39887082024-11-22 18:06:20 +0100639 Options options;
640 options.env->SetBackgroundThreads(4);
641 options.create_if_missing = true;
642 options.merge_operator = std::make_shared<CountMergeOperator>();
643 options.max_successive_merges = 0;
644 // options.prefix_extractor.reset(NewFixedPrefixTransform(8));
645 options.IncreaseParallelism();
646 options.OptimizeLevelStyleCompaction();
647 options.prefix_extractor.reset(NewFixedPrefixTransform(3));
648 ostringstream dbname, vocabname;
649 dbname << name << ".rocksdb";
650 auto s = DB::OpenForReadOnly(options, dbname.str(), &db);
651 if (!s.ok()) {
652 std::cerr << s.ToString() << std::endl;
653 assert(false);
654 }
655 vocabname << name << ".vocab";
656 readVocab(vocabname.str());
Marc Kupietz65d44792026-07-31 09:46:34 +0900657 return std::shared_ptr<DB>(ROCKSDB_DB_RELEASE(db));
Marc Kupietz39887082024-11-22 18:06:20 +0100658}
659
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200660 /* Reads a size from the environment, so that a long indexing run can be
661 tuned without recompiling. Returns the default when unset or unusable. */
662 static uint64_t env_size(const char *name, uint64_t fallback) {
663 const char *value = getenv(name);
664 if (value == nullptr)
665 return fallback;
666 char *end = nullptr;
667 unsigned long long parsed = strtoull(value, &end, 10);
668 if (end == value || parsed == 0) {
669 std::cerr << "collocatordb: ignoring " << name << "=" << value << std::endl;
670 return fallback;
671 }
672 return (uint64_t)parsed;
673 }
674
Marc Kupietzc630c152025-01-23 11:17:47 +0100675 std::shared_ptr<DB> CollocatorDB::OpenDb(const char *dbname) {
Marc Kupietz65d44792026-07-31 09:46:34 +0900676 ROCKSDB_DB_HANDLE db;
Marc Kupietzc630c152025-01-23 11:17:47 +0100677 Options options;
Marc Kupietz39887082024-11-22 18:06:20 +0100678
Marc Kupietzc630c152025-01-23 11:17:47 +0100679 int max_cores = static_cast<int>(std::thread::hardware_concurrency());
680
Marc Kupietzc630c152025-01-23 11:17:47 +0100681 options.create_if_missing = true;
682 options.merge_operator = std::make_shared<CountMergeOperator>();
Marc Kupietzc630c152025-01-23 11:17:47 +0100683
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200684 /* Indexing a corpus is one long stream of merge operands for the same
685 keys. rocksdb only collapses them when memtables are merged and when
686 files are compacted, so the settings aim at doing that early and often -
687 every collapsed operand is one less to write, to read and to merge
688 again later.
Marc Kupietzc630c152025-01-23 11:17:47 +0100689
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200690 All of it can be overridden from the environment, to be able to tune a
691 run that takes days without recompiling. */
692 options.write_buffer_size = env_size("COLLOCATORDB_WRITE_BUFFER_MB", 256) << 20;
693 options.max_write_buffer_number = (int)env_size("COLLOCATORDB_WRITE_BUFFERS", 8);
694 /* collapses the operands of several memtables before they are written */
695 options.min_write_buffer_number_to_merge =
696 (int)env_size("COLLOCATORDB_WRITE_BUFFERS_TO_MERGE", 4);
697
698 /* Compaction has to keep up with the writer, otherwise the level 0 files
699 pile up and every read has to merge through all of them. */
700 options.max_background_jobs =
701 (int)env_size("COLLOCATORDB_BACKGROUND_JOBS",
702 max_cores > 4 ? (max_cores < 32 ? max_cores : 32) : 4);
703 options.max_subcompactions = (int)env_size("COLLOCATORDB_SUBCOMPACTIONS", 4);
704 options.level0_file_num_compaction_trigger = 4;
705 options.level0_slowdown_writes_trigger = 20;
706 options.level0_stop_writes_trigger = 36;
707
708 /* Merge operands are inserted one writer at a time, rocksdb does not
709 support concurrent memtable writes for them, which is why more threads
710 in the indexer do not help beyond a certain point. */
711 options.allow_concurrent_memtable_write = false;
Marc Kupietzc630c152025-01-23 11:17:47 +0100712 options.enable_write_thread_adaptive_yield = true;
Marc Kupietzc630c152025-01-23 11:17:47 +0100713 options.allow_mmap_reads = true;
714
Marc Kupietzc630c152025-01-23 11:17:47 +0100715 BlockBasedTableOptions table_options;
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200716 table_options.block_cache =
717 NewLRUCache(env_size("COLLOCATORDB_BLOCK_CACHE_MB", 512) << 20);
Marc Kupietzc630c152025-01-23 11:17:47 +0100718 options.table_factory.reset(NewBlockBasedTableFactory(table_options));
719
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200720 /* No write ahead log: an interrupted indexing run is repeated, and the log
721 would double the amount written. Everything that has not been flushed is
722 lost when the process dies, which is what close_collocatordb() is for. */
723 merge_option_.disableWAL = true;
724 merge_option_.sync = false;
725 /* Let rocksdb slow the writer down when compaction falls behind, instead
726 of cancelling the write. It used to be told to do neither, which threw
727 counts away. merge_one() repeats a cancelled write, whatever the
728 settings are. */
729 merge_option_.low_pri = false;
730 merge_option_.no_slowdown = false;
731 blocking_merge_option_ = merge_option_;
732 blocking_merge_option_.low_pri = false;
733 blocking_merge_option_.no_slowdown = false;
Marc Kupietzc630c152025-01-23 11:17:47 +0100734
735 Status s = DB::Open(options, dbname, &db);
736 if (!s.ok()) {
737 std::cerr << s.ToString() << std::endl;
738 assert(false);
739 }
740 total = 1000;
Marc Kupietz65d44792026-07-31 09:46:34 +0900741 return std::shared_ptr<DB>(ROCKSDB_DB_RELEASE(db));
Marc Kupietz39887082024-11-22 18:06:20 +0100742 }
Marc Kupietz39887082024-11-22 18:06:20 +0100743
744CollocatorIterator *
745CollocatorDB::SeekIterator(uint64_t w1, uint64_t w2, int8_t dist) const {
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200746 flush();
Marc Kupietz39887082024-11-22 18:06:20 +0100747 ReadOptions options;
748 options.prefix_same_as_start = true;
749 char prefixc[sizeof(uint64_t)];
750 EncodeFixed64(prefixc, encodeCollocation(w1, w2, dist));
751 Iterator *it = db_->NewIterator(options);
752 auto *cit = new CollocatorIterator(it);
753 if (w2 > 0)
754 cit->Seek(std::string(prefixc, 6));
755 else
756 cit->Seek(std::string(prefixc, 3));
757 cit->setPrefix(prefixc);
758 return cit;
759}
760
761void CollocatorDB::dump(uint32_t w1, uint32_t w2, int8_t dist) const {
762 auto it = std::unique_ptr<CollocatorIterator>(SeekIterator(w1, w2, dist));
763 for (; it->isValid(); it->Next()) {
764 uint64_t value = it->intValue();
765 uint64_t key = it->intKey();
766 std::cout << "w1:" << W1(key) << ", w2:" << W2(key)
767 << ", dist:" << (int32_t)DIST(key) << " - count:" << value
768 << std::endl;
769 }
770 std::cout << "ready dumping\n";
771}
772
773bool sortByNpmi(const Collocator &lhs, const Collocator &rhs) {
774 return lhs.npmi > rhs.npmi;
775}
776
777bool sortByLfmd(const Collocator &lhs, const Collocator &rhs) {
778 return lhs.lfmd > rhs.lfmd;
779}
780
781bool sortByLlr(const Collocator &lhs, const Collocator &rhs) {
782 return lhs.llr > rhs.llr;
783}
784
785bool sortByLogDice(const Collocator &lhs, const Collocator &rhs) {
786 return lhs.logdice > rhs.logdice;
787}
788
789bool sortByLogDiceAF(const Collocator &lhs, const Collocator &rhs) {
790 return lhs.ldaf > rhs.ldaf;
791}
792
793void CollocatorDB::applyCAMeasures(
794 const uint32_t w1, const uint32_t w2, uint64_t *sumWindow,
795 const uint64_t sum, const int usedPositions, int true_window_size,
796 Collocator *result) const {
797 uint64_t f1 = _vocab[w1].freq, f2 = _vocab[w2].freq;
798 double o = sum, r1 = f1 * true_window_size, c1 = f2, e = r1 * c1 / total,
799 pmi = log2(o / e), md = log2(o * o / e), lfmd = log2(o * o * o / e),
Marc Kupietze889cec2024-11-23 12:08:42 +0100800 llr = ca_ll(f1, f2, sum, total, true_window_size),
801 md_nws = ca_md(f1, f2, sum, total, 2 * WINDOW_SIZE),
802 ld = ca_logdice(f1, f2, sum, total, true_window_size);
Marc Kupietz39887082024-11-22 18:06:20 +0100803
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200804 // LDaf is the auto focus score itself, i.e. the highest value that any
805 // selection of positions reaches. It is deliberately not logDice of the
806 // selected window: the width penalty is what makes the score sensitive to
807 // how concentrated a pair is, and that sensitivity is why LDaf orders
808 // collocates more usefully than the other measures.
Marc Kupietz39887082024-11-22 18:06:20 +0100809 int bestWindow = usedPositions;
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200810 double bestFocus = ca_focus_score(f1, f2, sum, total, true_window_size);
Marc Kupietz39887082024-11-22 18:06:20 +0100811 // if(f1<75000000)
812 // #pragma omp parallel for reduction(max:bestAF)
813 // #pragma omp target teams distribute parallel for reduction(max:bestAF)
814 // map(tofrom:bestAF,currentAF,bestWindow,usedPositions)
815 for (int bitmask = 1; bitmask < (1 << (2 * WINDOW_SIZE)); bitmask++) {
816 if ((bitmask & usedPositions) == 0 || (bitmask & ~usedPositions) > 0)
817 continue;
818 uint64_t currentWindowSum = 0;
819 // #pragma omp target teams distribute parallel for
820 // reduction(+:currentWindowSum) map(tofrom:bitmask,usedPositions)
821 for (int pos = 0; pos < 2 * WINDOW_SIZE; pos++) {
822 if (((1 << pos) & bitmask & usedPositions) != 0)
823 currentWindowSum += sumWindow[pos];
824 }
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200825 double currentAF = ca_focus_score(f1, f2, currentWindowSum, total,
826 __builtin_popcount(bitmask));
827 if (currentAF > bestFocus) {
828 bestFocus = currentAF;
Marc Kupietz39887082024-11-22 18:06:20 +0100829 bestWindow = bitmask;
830 }
831 }
832
833 *result = {w2,
834 f2,
835 sum,
836 pmi,
837 pmi / (-log2(o / total / true_window_size)),
838 llr,
839 lfmd,
840 md,
Marc Kupietze889cec2024-11-23 12:08:42 +0100841 md_nws,
Marc Kupietz39887082024-11-22 18:06:20 +0100842 sumWindow[WINDOW_SIZE],
843 sumWindow[WINDOW_SIZE - 1],
844 ca_pmi(f1, f2, sumWindow[WINDOW_SIZE], total, 1),
845 ca_pmi(f1, f2, sumWindow[WINDOW_SIZE - 1], total, 1),
846 ca_dice(f1, f2, sum, total, true_window_size),
847 ld,
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200848 bestFocus,
Marc Kupietz39887082024-11-22 18:06:20 +0100849 usedPositions,
850 bestWindow};
851}
852
853std::vector<Collocator>
854CollocatorDB::get_collocators(uint32_t w1, uint32_t min_w2,
855 uint32_t max_w2) {
856 std::vector<Collocator> collocators;
857 uint64_t w2, last_w2 = 0xffffffffffffffff;
858 uint64_t maxv = 0, sum = 0;
Marc Kupietzaa354d82026-07-31 09:17:57 +0900859 /* fixed size, so it lives on the stack and cannot be leaked */
860 uint64_t sumWindow[2 * WINDOW_SIZE];
861 memset(sumWindow, 0, sizeof(sumWindow));
Marc Kupietze5108542026-09-04 15:29:52 +0200862 /* Counted up from zero, since the first key of the iteration takes the same
863 branch as any further key of the same collocate, which increments it. It
864 used to start at one, so that the first collocate of every query was scored
865 with a window one position too wide, which inflated its expected frequency
866 and thus lowered its pmi, npmi, md, lfmd and llr. */
867 int true_window_size = 0;
Marc Kupietz39887082024-11-22 18:06:20 +0100868 int usedPositions = 0;
869
870 if (w1 > _vocab.size()) {
871 std::cout << w1 << "> vocabulary size " << _vocab.size() << "\n";
872 w1 -= _vocab.size();
873 }
874#ifdef DEBUG
875 std::cout << "Searching for collocates of " << _vocab[w1].word << "\n";
876#endif
877 // #pragma omp parallel num_threads(40)
878 // #pragma omp single
879 for (auto it =
880 std::unique_ptr<CollocatorIterator>(SeekIterator(w1, min_w2, 0));
881 it->isValid(); it->Next()) {
882 uint64_t value = it->intValue(), key = it->intKey();
883 if ((w2 = W2(key)) > max_w2)
884 continue;
885 if (last_w2 == 0xffffffffffffffff)
886 last_w2 = w2;
887 if (w2 != last_w2) {
888 if (sum >= FREQUENCY_THRESHOLD) {
889 collocators.push_back({});
890 Collocator *result = &(collocators[collocators.size() - 1]);
891 // #pragma omp task firstprivate(last_w2, sumWindow, sum, usedPositions,
892 // true_window_size) shared(w1, result) if(sum > 1000000)
893 {
894 // uint64_t *nsw = (uint64_t *)malloc(sizeof(uint64_t) * 2
895 // *WINDOW_SIZE); memcpy(nsw, sumWindow, sizeof(uint64_t) * 2
896 // *WINDOW_SIZE);
897 applyCAMeasures(w1, last_w2, sumWindow, sum, usedPositions,
898 true_window_size, result);
899 // free(nsw);
900 }
901 }
902 memset(sumWindow, 0, 2 * WINDOW_SIZE * sizeof(uint64_t));
903 usedPositions = 1 << (-DIST(key) + WINDOW_SIZE - (DIST(key) < 0 ? 1 : 0));
904 sumWindow[-DIST(key) + WINDOW_SIZE - (DIST(key) < 0 ? 1 : 0)] = value;
905 last_w2 = w2;
906 maxv = value;
907 sum = value;
908 true_window_size = 1;
909 if (min_w2 == max_w2 && w2 != min_w2)
910 break;
911 } else {
912 sum += value;
913 if (value > maxv)
914 maxv = value;
915 usedPositions |=
916 1 << (-DIST(key) + WINDOW_SIZE - (DIST(key) < 0 ? 1 : 0));
917 sumWindow[-DIST(key) + WINDOW_SIZE - (DIST(key) < 0 ? 1 : 0)] = value;
918 true_window_size++;
919 }
920 }
921
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200922 /* A collocate is only finished when the next one begins, so the last one is
923 still in sumWindow when the iterator is through. It used to be dropped,
924 which cost every word one of its collocates. */
925 if (last_w2 != 0xffffffffffffffff && sum >= FREQUENCY_THRESHOLD) {
926 collocators.push_back({});
927 applyCAMeasures(w1, last_w2, sumWindow, sum, usedPositions,
928 true_window_size, &(collocators[collocators.size() - 1]));
929 }
930
Marc Kupietz39887082024-11-22 18:06:20 +0100931 // #pragma omp taskwait
932 sort(collocators.begin(), collocators.end(), sortByLogDiceAF);
933
934#ifdef DEBUG
935 int i = 0;
936 for (Collocator c : collocators) {
937 if (i++ > 10)
938 break;
939 std::cout << "w1:" << _vocab[w1].word << ", w2: *" << _vocab[c.w2].word
940 << "*"
941 << "\t f(w1):" << _vocab[w1].freq
942 << "\t f(w2):" << _vocab[c.w2].freq << "\t f(w1, w2):" << c.raw
943 << "\t pmi:" << c.pmi << "\t npmi:" << c.npmi
944 << "\t llr:" << c.llr << "\t md:" << c.md << "\t lfmd:" << c.lfmd
945 << "\t total:" << total << std::endl;
946 }
947#endif
948
949 return collocators;
950}
951
952std::vector<Collocator>
953CollocatorDB::get_collocation_scores(uint32_t w1, uint32_t w2) {
954 return get_collocators(w1, w2, w2);
955}
956
957std::vector<Collocator> CollocatorDB::get_collocators(uint32_t w1) {
958 return get_collocators(w1, 0, UINT32_MAX);
959}
960
961void CollocatorDB::dumpSparseLlr(uint32_t w1, uint32_t min_cooccur) {
962 std::vector<Collocator> collocators;
963 std::stringstream stream;
964 uint64_t w2, last_w2 = 0xffffffffffffffff;
965 uint64_t maxv = 0, total_w1 = 0;
966 bool first = true;
967 for (auto it = std::unique_ptr<CollocatorIterator>(SeekIterator(w1, 0, 0));
968 it->isValid(); it->Next()) {
969 uint64_t value = it->intValue(), key = it->intKey();
970 w2 = W2(key);
971 total_w1 += value;
972 if (last_w2 == 0xffffffffffffffff)
973 last_w2 = w2;
974 if (w2 != last_w2) {
975 if (maxv >= min_cooccur) {
976 double llr =
977 ca_ll(_vocab[w1].freq, _vocab[last_w2].freq, maxv, total, 1);
978 if (first)
979 first = false;
980 else
981 stream << " ";
982 stream << w2 << " " << llr;
983 }
984 last_w2 = w2;
985 maxv = value;
986 } else {
987 if (value > maxv)
988 maxv = value;
989 }
990 }
991 if (first)
992 stream << "1 0.0";
993 stream << "\n";
994 std::cout << stream.str();
995}
996
997Slice CollocatorIterator::key() const {
998 return base_iterator_->key();
999}
1000
1001Slice CollocatorIterator::value() const {
1002 return base_iterator_->value();
1003}
1004
1005Status CollocatorIterator::status() const {
1006 return base_iterator_->status();
1007}
1008
1009}; // namespace rocksdb
1010
1011string CollocatorDB::getWord(uint32_t w1) { return _vocab[w1].word; }
1012
1013uint64_t CollocatorDB::getWordId(const char *word) const {
Marc Kupietz979580e2024-11-21 18:05:07 +01001014 for (uint64_t i = 0; i < _vocab.size(); i++) {
1015 if (strcmp(_vocab[i].word.c_str(), word) == 0)
1016 return i;
1017 }
1018 return 0;
1019}
1020
Marc Kupietzd26b1052024-12-10 16:56:39 +01001021uint64_t CollocatorDB::getCorpusSize() const {
1022 return total;
1023}
1024
Marc Kupietz21b964c2024-12-10 17:10:50 +01001025uint64_t CollocatorDB::getWordFrequency(uint64_t w1) {
1026 return _vocab[w1].freq;
1027}
1028
Marc Kupietz39887082024-11-22 18:06:20 +01001029string CollocatorDB::collocators2json(uint32_t w1,
1030 const vector<Collocator>& collocators) {
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001031 ostringstream s;
Marc Kupietz0dd86ef2018-01-11 22:23:17 +01001032 int i = 0;
Marc Kupietz39887082024-11-22 18:06:20 +01001033 s << " { \"f1\": " << _vocab[w1].freq << "," << R"("w1":")"
1034 << string(_vocab[w1].word) << "\", " << "\"N\": " << total << ", "
1035 << "\"collocates\": [";
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001036 bool first = true;
1037 for (Collocator c : collocators) {
Marc Kupietz39887082024-11-22 18:06:20 +01001038 if (strncmp(_vocab[c.w2].word.c_str(), "quot", 4) == 0)
1039 continue;
Marc Kupietz0dd86ef2018-01-11 22:23:17 +01001040 if (i++ > 200)
1041 break;
Marc Kupietz12af0192021-03-13 18:05:14 +01001042 if (!first)
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001043 s << ",\n";
1044 else
1045 first = false;
1046 s << "{"
Marc Kupietz39887082024-11-22 18:06:20 +01001047 "\"word\":\""
1048 << (string(_vocab[c.w2].word) == "<num>"
1049 ? string("###")
1050 : string(_vocab[c.w2].word))
1051 << "\"," << "\"f2\":" << c.f2 << "," << "\"f\":" << c.raw << ","
1052 << "\"npmi\":" << c.npmi << "," << "\"pmi\":" << c.pmi << ","
1053 << "\"llr\":" << c.llr << "," << "\"lfmd\":" << c.lfmd << ","
Marc Kupietze889cec2024-11-23 12:08:42 +01001054 << "\"md\":" << c.md << "," << "\"md_nws\":" << c.md_nws << "," << "\"dice\":" << c.dice << ","
Marc Kupietz39887082024-11-22 18:06:20 +01001055 << "\"ld\":" << c.logdice << "," << "\"ln_count\":" << c.left_raw << ","
1056 << "\"rn_count\":" << c.right_raw << "," << "\"ln_pmi\":" << c.left_pmi
1057 << "," << "\"rn_pmi\":" << c.right_pmi << "," << "\"ldaf\":" << c.ldaf
1058 << "," << "\"win\":" << c.window << "," << "\"afwin\":" << c.af_window
1059 << "}";
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001060 }
Marc Kupietze9627152019-02-04 12:32:12 +01001061 s << "]}\n";
Marc Kupietz0421d092021-03-13 18:05:14 +01001062 // std::cout << s.str();
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001063 return s.str();
1064}
1065
Marc Kupietz39887082024-11-22 18:06:20 +01001066typedef CollocatorDB COLLOCATORS;
Marc Kupietz06c9a9f2018-01-02 16:56:43 +01001067
1068extern "C" {
Marc Kupietz12af0192021-03-13 18:05:14 +01001069#ifdef __clang__
1070#pragma clang diagnostic push
1071#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection"
1072#endif
Marc Kupietz39887082024-11-22 18:06:20 +01001073DLL_EXPORT COLLOCATORS *open_collocatordb_for_write(char *dbname) {
1074 return new CollocatorDB(dbname, false);
1075}
Marc Kupietz12af0192021-03-13 18:05:14 +01001076
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001077DLL_EXPORT void close_collocatordb(COLLOCATORS *db) { delete db; }
1078
Marc Kupietz39887082024-11-22 18:06:20 +01001079DLL_EXPORT COLLOCATORS *open_collocatordb(char *dbname) {
1080 return new CollocatorDB(dbname, true);
1081}
Marc Kupietz06c9a9f2018-01-02 16:56:43 +01001082
Marc Kupietz39887082024-11-22 18:06:20 +01001083DLL_EXPORT void inc_collocator(COLLOCATORS *db, uint32_t w1, uint32_t w2,
1084 int8_t dist) {
1085 db->inc(w1, w2, dist);
1086}
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001087
Marc Kupietz39887082024-11-22 18:06:20 +01001088DLL_EXPORT void dump_collocators(COLLOCATORS *db, uint32_t w1, uint32_t w2,
1089 int8_t dist) {
1090 db->dump(w1, w2, dist);
1091}
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001092
Marc Kupietz39887082024-11-22 18:06:20 +01001093DLL_EXPORT COLLOCATORS *get_collocators(COLLOCATORS *db, uint32_t w1) {
1094 std::vector<Collocator> c = db->get_collocators(w1);
1095 if (c.empty())
1096 return nullptr;
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001097 /* one entry more than there are collocators, terminated with w2 == 0 and
1098 raw == 0, so that callers can tell where the array ends */
1099 uint64_t size = (c.size() + 1) * sizeof c[0];
Marc Kupietz39887082024-11-22 18:06:20 +01001100 auto *p = (COLLOCATORS *)malloc(size);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001101 memset(p, 0, size);
1102 memcpy(p, c.data(), c.size() * sizeof c[0]);
Marc Kupietz39887082024-11-22 18:06:20 +01001103 return p;
1104}
Marc Kupietz88d116b2021-03-13 18:05:14 +01001105
Marc Kupietz39887082024-11-22 18:06:20 +01001106DLL_EXPORT COLLOCATORS *get_collocation_scores(COLLOCATORS *db, uint32_t w1,
1107 uint32_t w2) {
1108 std::vector<Collocator> c = db->get_collocation_scores(w1, w2);
1109 if (c.empty())
1110 return nullptr;
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001111 /* one entry more than there are collocators, terminated with w2 == 0 and
1112 raw == 0, so that callers can tell where the array ends */
1113 uint64_t size = (c.size() + 1) * sizeof c[0];
Marc Kupietz39887082024-11-22 18:06:20 +01001114 auto *p = (COLLOCATORS *)malloc(size);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001115 memset(p, 0, size);
1116 memcpy(p, c.data(), c.size() * sizeof c[0]);
Marc Kupietz39887082024-11-22 18:06:20 +01001117 return p;
1118}
Marc Kupietzca3a52e2018-06-05 14:16:23 +02001119
Marc Kupietz39887082024-11-22 18:06:20 +01001120DLL_EXPORT char *get_word(COLLOCATORS *db, uint32_t w) {
1121 return strdup(db->getWord(w).c_str());
1122}
Marc Kupietz979580e2024-11-21 18:05:07 +01001123
Marc Kupietz39887082024-11-22 18:06:20 +01001124DLL_EXPORT uint64_t get_word_id(COLLOCATORS *db, char *word) {
1125 return db->getWordId(word);
1126}
Marc Kupietzb4a683c2021-03-14 09:19:44 +01001127
Marc Kupietz39887082024-11-22 18:06:20 +01001128DLL_EXPORT void read_vocab(COLLOCATORS *db, char *fname) {
1129 std::string fName(fname);
1130 db->readVocab(fName);
1131}
Marc Kupietz88d116b2021-03-13 18:05:14 +01001132
Marc Kupietz39887082024-11-22 18:06:20 +01001133DLL_EXPORT const char *get_collocators_as_json(COLLOCATORS *db, uint32_t w1) {
1134 return strdup(db->collocators2json(w1, db->get_collocators(w1)).c_str());
1135}
Marc Kupietzb4a683c2021-03-14 09:19:44 +01001136
Marc Kupietz39887082024-11-22 18:06:20 +01001137DLL_EXPORT const char *
1138get_collocation_scores_as_json(COLLOCATORS *db, uint32_t w1, uint32_t w2) {
1139 return strdup(
1140 db->collocators2json(w1, db->get_collocation_scores(w1, w2)).c_str());
1141}
1142
1143DLL_EXPORT const char *get_version() { return PROJECT_VERSION; }
Marc Kupietz6208fd72024-11-15 15:46:19 +01001144
Marc Kupietzd26b1052024-12-10 16:56:39 +01001145DLL_EXPORT uint64_t get_corpus_size(COLLOCATORS *db) { return db->getCorpusSize(); };
1146
Marc Kupietz21b964c2024-12-10 17:10:50 +01001147DLL_EXPORT uint64_t get_word_frequency(COLLOCATORS *db, uint64_t w1) {
1148 return db->getWordFrequency(w1);
1149}
1150
Marc Kupietz12af0192021-03-13 18:05:14 +01001151#ifdef __clang__
1152#pragma clang diagnostic push
1153#endif
Marc Kupietz06c9a9f2018-01-02 16:56:43 +01001154}