blob: 63029c40da6184dab06e173dc24cd5c3dedc0042 [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
Marc Kupietzca7510f2026-09-04 17:53:57 +0200182// The table classifies co-occurrence tokens, not corpus tokens: with a window of
183// window_size positions, every occurrence of either word takes part in that many
184// pairs, so the sample size and both marginals scale with it. Scaling only the
185// row, as this did before, leaves cells that do not add up to one sample and lets
186// n - window_size * w1 turn negative for a frequent node in a wide window. The
187// expected co-occurrence frequency is window_size * w1 * w2 / n either way, which
188// is why the other measures do not depend on this.
Marc Kupietz39887082024-11-22 18:06:20 +0100189static double ca_ll(uint64_t w1, uint64_t w2, uint64_t w12, uint64_t n,
190 uint64_t window_size) {
Marc Kupietzca7510f2026-09-04 17:53:57 +0200191 double total = (double)n * window_size,
192 r1 = (double)w1 * window_size, r2 = total - r1,
193 c1 = (double)w2 * window_size, c2 = total - c1,
194 o11 = w12, o12 = r1 - o11, o21 = c1 - o11, o22 = r2 - o21,
195 e11 = r1 * c1 / total, e12 = r1 * c2 / total, e21 = r2 * c1 / total,
196 e22 = r2 * c2 / total;
Marc Kupietz39887082024-11-22 18:06:20 +0100197 return (2 * ((o11 > 0 ? o11 * log(o11 / e11) : 0) +
198 (o12 > 0 ? o12 * log(o12 / e12) : 0) +
199 (o21 > 0 ? o21 * log(o21 / e21) : 0) +
200 (o22 > 0 ? o22 * log(o22 / e22) : 0)));
201}
202
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200203// The Dice coefficient relates the co-occurrence frequency to how often the two
204// words occur at all, so its denominator sums marginal word frequencies. It
205// therefore takes no window size factor, unlike the scores above, which need one
206// in their expected frequency: w1 * window_size would count window positions
207// rather than word tokens, and adding that to w2 would also make the coefficient
208// asymmetric.
Marc Kupietz39887082024-11-22 18:06:20 +0100209static double ca_dice(uint64_t w1, uint64_t w2, uint64_t w12, uint64_t n,
210 uint64_t window_size) {
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200211 double r1 = (double)w1, c1 = w2;
Marc Kupietz39887082024-11-22 18:06:20 +0100212 return 2 * w12 / (c1 + r1);
213}
214
215// Rychlý, Pavel (2008): <a
216// href="http://www.fi.muni.cz/usr/sojka/download/raslan2008/13.pdf">A
217// lexicographer-friendly association score.</a> In Proceedings of Recent
218// Advances in Slavonic Natural Language Processing, RASLAN, 6–9.
219static double ca_logdice(uint64_t w1, uint64_t w2, uint64_t w12,
220 uint64_t n, uint64_t window_size) {
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200221 double r1 = (double)w1, c1 = w2;
222 return 14 + log2(2 * w12 / (c1 + r1));
223}
224
225// The auto focus score, reported as LDaf: its maximum over all selections of
226// positions both picks the auto focus window and is the value shown for it. It
227// used to be computed by ca_logdice(), which is why that one carried a window
228// size factor.
229//
230// It is Dice-like, but it is not logDice and the two are not on a common scale:
231// multiplying f1 by the number of positions penalizes wide windows. That
232// penalty is the point. It makes the score sensitive to how concentrated a pair
233// is, which is what tells actual collocations from words that merely share
234// contexts, and is why LDaf orders collocates more usefully than the other
235// measures. logDice itself cannot serve here: its denominator does not depend
236// on the window while the co-occurrence count only grows as positions are
237// added, so the widest window would always win.
238static double ca_focus_score(uint64_t w1, uint64_t w2, uint64_t w12, uint64_t n,
239 uint64_t window_size) {
Marc Kupietz39887082024-11-22 18:06:20 +0100240 double r1 = (double)w1 * window_size, c1 = w2;
241 return 14 + log2(2 * w12 / (c1 + r1));
242}
243
244class CountMergeOperator : public AssociativeMergeOperator {
245public:
246 CountMergeOperator() {
247 mergeOperator_ = MergeOperators::CreateUInt64AddOperator();
248 }
249
250 bool Merge(const Slice &key, const Slice *existing_value,
251 const Slice &value, std::string *new_value,
252 Logger *logger) const override {
253 assert(new_value->empty());
254 ++num_merge_operator_calls;
255 if (existing_value == nullptr) {
256 new_value->assign(value.data(), value.size());
257 return true;
258 }
259
260 return mergeOperator_->PartialMerge(key, *existing_value, value, new_value,
261 logger);
262 }
263
264 const char *Name() const override { return "UInt64AddOperator"; }
265
266private:
267 std::shared_ptr<MergeOperator> mergeOperator_;
268};
269
270class CollocatorIterator : public Iterator {
271 char prefixc[sizeof(uint64_t)]{};
272 Iterator *base_iterator_;
273
274public:
275 explicit CollocatorIterator(Iterator *base_iterator) : base_iterator_(base_iterator) {}
276
Marc Kupietzaa354d82026-07-31 09:17:57 +0900277 /* Takes ownership of the iterator handed in by SeekIterator(). Without this
278 every seek leaked a rocksdb iterator and the resources it pins. */
279 ~CollocatorIterator() override { delete base_iterator_; }
280
Marc Kupietz39887082024-11-22 18:06:20 +0100281 void setPrefix(char *prefix) { memcpy(prefixc, prefix, sizeof(uint64_t)); }
282
283 void SeekToFirst() override { base_iterator_->SeekToFirst(); }
284
285 void SeekToLast() override { base_iterator_->SeekToLast(); }
286
287 void Seek(const rocksdb::Slice &s) override { base_iterator_->Seek(s); }
288
289 void SeekForPrev(const rocksdb::Slice &s) override {
290 base_iterator_->SeekForPrev(s);
291 }
292
293 void Prev() override { base_iterator_->Prev(); }
294
295 void Next() override { base_iterator_->Next(); }
296
297 Slice key() const override;
298
299 Slice value() const override;
300
301 Status status() const override;
302
303 bool Valid() const override;
304
305 bool isValid();
306
307 uint64_t intValue();
308
309 uint64_t intKey();
310};
311
312// rocksdb::CollocatorIterator::CollocatorIterator(Iterator* base_iterator) {}
313
314bool CollocatorIterator::Valid() const {
315 return base_iterator_->Valid() && key().starts_with(std::string(prefixc, 3));
316}
317
318bool CollocatorIterator::isValid() {
319 return base_iterator_->Valid() && key().starts_with(std::string(prefixc, 3));
320 // return key().starts_with(std::string(prefixc,3));
321}
322
323uint64_t CollocatorIterator::intKey() {
324 return DecodeFixed64(base_iterator_->key().data());
325}
326
327uint64_t CollocatorIterator::intValue() {
328 return DecodeFixed64(base_iterator_->value().data());
329}
330
331class VocabEntry {
332public:
333 string word;
334 uint64_t freq;
335};
336
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200337static uint64_t env_size(const char *name, uint64_t fallback);
338
Marc Kupietz39887082024-11-22 18:06:20 +0100339class CollocatorDB {
340 WriteOptions merge_option_; // for merge
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200341 // to repeat a write that rocksdb cancelled, rather than lose the count
342 WriteOptions blocking_merge_option_;
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200343 mutable std::atomic<uint64_t> stalled_writes_{0};
344 mutable std::atomic<uint64_t> failed_writes_{0};
Marc Kupietz39887082024-11-22 18:06:20 +0100345 char _one[sizeof(uint64_t)]{};
346 Slice _one_slice;
347 vector<VocabEntry> _vocab;
348 uint64_t total = 0;
349 uint64_t sentences = 0;
350 float avg_window_size = 8.0;
351
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200352 /* Single-key merges are collected in a batch, so that a long indexing run
353 does one rocksdb write per batch instead of one per collocation pair.
354 rocksdb inserts the merge operands of a key in order anyway, which is why
355 the collection point has a mutex: it is the same serialization rocksdb
356 would impose on the writes.
357
358 They are mutable so that the const read methods can write out what is
359 still buffered: a read has to see every increment that came before it. */
360 mutable std::mutex batch_mutex_;
361 mutable WriteBatch batch_;
362 size_t batch_target_ = 65536;
363
364 /* Writes out a batch of merge operands, retrying a cancelled write the same
365 way merge_one() used to, before the merges were batched.
366
367 Takes a batch that merge_one() or flush() took out of the shared batch,
368 and is called without batch_mutex_: the write is the slow part, and the
369 other threads must be able to keep accumulating while it is in flight.
370 Holding the mutex across the write made the indexer run slower with many
371 threads than with one, because the threads queued up on the mutex and
372 rocksdb never saw concurrent writers to batch into one. */
373 void write_batch(WriteBatch &to_write) const {
374 if (to_write.Count() == 0)
375 return;
376 Status s = db_->Write(merge_option_, &to_write);
377 if (s.ok())
378 return;
379 if (s.IsIncomplete()) {
380 ++stalled_writes_;
381 s = db_->Write(blocking_merge_option_, &to_write);
382 if (s.ok())
383 return;
384 }
385 if (failed_writes_++ == 0)
386 std::cerr << "collocatordb: cannot write, counts are lost: "
387 << s.ToString() << std::endl;
388 }
389
390 /* A read has to see the increments that are still in the batch, so the read
391 methods call this first. */
392 void flush() const {
393 WriteBatch to_write;
394 {
395 std::lock_guard<std::mutex> lock(batch_mutex_);
396 std::swap(batch_, to_write);
397 }
398 write_batch(to_write);
399 }
400
Marc Kupietz39887082024-11-22 18:06:20 +0100401protected:
402 std::shared_ptr<DB> db_;
403
404 WriteOptions put_option_;
405 ReadOptions get_option_;
406 WriteOptions delete_option_;
407
408 uint64_t default_{};
409
410 std::shared_ptr<DB> OpenDb(const char *dbname);
411
412 std::shared_ptr<DB> OpenDbForRead(const char *dbname);
413
414public:
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200415 virtual ~CollocatorDB(); // flushes what is still in memory, see close()
Marc Kupietz39887082024-11-22 18:06:20 +0100416 void readVocab(const string& fname);
417 string getWord(uint32_t w1);
418
419 uint64_t getWordId(const char *word) const;
420
Marc Kupietzd26b1052024-12-10 16:56:39 +0100421 uint64_t getCorpusSize() const;
422
Marc Kupietz21b964c2024-12-10 17:10:50 +0100423 uint64_t getWordFrequency(uint64_t w1);
424
Marc Kupietz39887082024-11-22 18:06:20 +0100425 CollocatorDB(const char *db_name, bool read_only);
426
427 // public interface of CollocatorDB.
428 // All four functions return false
429 // if the underlying level db operation failed.
430
431 // mapped to a levedb Put
432 bool set(const std::string &key, uint64_t value) {
433 // just treat the internal rep of int64 as the string
434 char buf[sizeof(value)];
435 EncodeFixed64(buf, value);
436 Slice slice(buf, sizeof(value));
437 auto s = db_->Put(put_option_, key, slice);
438
439 if (s.ok()) {
440 return true;
441 } else {
442 std::cerr << s.ToString() << std::endl;
443 return false;
444 }
445 }
446
447 DB *getDb() { return db_.get(); }
448
449 // mapped to a rocksdb Delete
450 bool remove(const std::string &key) {
451 auto s = db_->Delete(delete_option_, key);
452
453 if (s.ok()) {
454 return true;
455 } else {
456 std::cerr << s.ToString() << std::endl;
457 return false;
458 }
459 }
460
461 // mapped to a rocksdb Get
462 bool get(const std::string &key, uint64_t *value) {
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200463 flush();
Marc Kupietz39887082024-11-22 18:06:20 +0100464 std::string str;
465 auto s = db_->Get(get_option_, key, &str);
466
467 if (s.IsNotFound()) {
468 // return default value if not found;
469 *value = default_;
470 return true;
471 } else if (s.ok()) {
472 // deserialization
473 if (str.size() != sizeof(uint64_t)) {
474 std::cerr << "value corruption\n";
475 return false;
476 }
477 *value = DecodeFixed64(&str[0]);
478 return true;
479 } else {
480 std::cerr << s.ToString() << std::endl;
481 return false;
482 }
483 }
484
485 uint64_t get(const uint32_t w1, const uint32_t w2, const int8_t dist) {
486 char encoded_key[sizeof(uint64_t)];
487 EncodeFixed64(encoded_key, encodeCollocation(w1, w2, dist));
488 uint64_t value = default_;
489 get(std::string(encoded_key, 8), &value);
490 return value;
491 }
492
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200493 /* A merge that does not lose the count. rocksdb cancels a write with
494 Status::Incomplete instead of waiting when it is asked not to slow down,
495 and then the increment is simply gone. Such a write was not applied, so
496 repeating it blocking cannot count twice. */
497 void merge_one(const Slice &key) {
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200498 WriteBatch to_write;
499 {
500 std::lock_guard<std::mutex> lock(batch_mutex_);
501 batch_.Merge(key, _one_slice);
502 if (batch_.Count() >= (int)batch_target_)
503 std::swap(batch_, to_write);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200504 }
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200505 write_batch(to_write);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200506 }
507
Marc Kupietz39887082024-11-22 18:06:20 +0100508 virtual void inc(const std::string &key) {
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200509 merge_one(Slice(key));
Marc Kupietz39887082024-11-22 18:06:20 +0100510 }
511
512 void inc(const uint64_t key) {
513 char encoded_key[sizeof(uint64_t)];
514 EncodeFixed64(encoded_key, key);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200515 merge_one(Slice(encoded_key, sizeof(uint64_t)));
Marc Kupietz39887082024-11-22 18:06:20 +0100516 }
517
518 virtual void inc(uint32_t w1, uint32_t w2, uint8_t dist);
519
520 void dump(uint32_t w1, uint32_t w2, int8_t dist) const;
521
522 vector<Collocator> get_collocators(uint32_t w1);
523
524 vector<Collocator> get_collocators(uint32_t w1, uint32_t max_w2);
525
526 vector<Collocator> get_collocation_scores(uint32_t w1, uint32_t w2);
527
528 vector<Collocator> get_collocators(uint32_t w1, uint32_t min_w2,
529 uint32_t max_w2);
530
531 void applyCAMeasures(uint32_t w1, uint32_t w2,
532 uint64_t *sumWindow, uint64_t sum,
533 int usedPositions, int true_window_size,
534 Collocator *result) const;
535
536 void dumpSparseLlr(uint32_t w1, uint32_t min_cooccur);
537
538 string collocators2json(uint32_t w1, const vector<Collocator>& collocators);
539
540 // mapped to a rocksdb Merge operation
541 virtual bool add(const std::string &key, uint64_t value) {
542 char encoded[sizeof(uint64_t)];
543 EncodeFixed64(encoded, value);
544 Slice slice(encoded, sizeof(uint64_t));
545 auto s = db_->Merge(merge_option_, key, slice);
546
547 if (s.ok()) {
548 return true;
549 } else {
550 std::cerr << s.ToString() << std::endl;
551 return false;
552 }
553 }
554
555 CollocatorIterator *SeekIterator(uint64_t w1, uint64_t w2, int8_t dist) const;
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200556
557 /* Writes what is still in memory and closes the database. Without this
558 everything that has not been flushed yet is lost when the process ends,
559 because the write ahead log is switched off for speed. */
560 void close() {
561 if (!db_)
562 return;
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200563 flush();
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200564 if (stalled_writes_ > 0)
565 std::cerr << "collocatordb: repeated " << stalled_writes_
566 << " writes that rocksdb had cancelled" << std::endl;
567 if (failed_writes_ > 0)
568 std::cerr << "collocatordb: " << failed_writes_
569 << " writes failed, the database is missing counts" << std::endl;
570 FlushOptions flush_options;
571 flush_options.wait = true;
572 Status s = db_->Flush(flush_options);
573 if (!s.ok() && !s.IsNotSupported())
574 std::cerr << "collocatordb: cannot write what is still in memory: "
575 << s.ToString() << std::endl;
576 db_.reset();
577 }
578
Marc Kupietz39887082024-11-22 18:06:20 +0100579};
580
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200581CollocatorDB::~CollocatorDB() { close(); }
582
Marc Kupietz39887082024-11-22 18:06:20 +0100583CollocatorDB::CollocatorDB(const char *db_name,
584 bool read_only = false) {
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200585 batch_target_ = (size_t)env_size("COLLOCATORDB_BATCH_SIZE", 65536);
Marc Kupietz39887082024-11-22 18:06:20 +0100586 // merge_option_.sync = true;
587 if (read_only)
588 db_ = OpenDbForRead(strdup(db_name));
589 else
590 db_ = OpenDb(db_name);
591 assert(db_);
592 uint64_t one = 1;
593 EncodeFixed64(_one, one);
594 _one_slice = Slice(_one, sizeof(uint64_t));
595}
596
597void CollocatorDB::inc(const uint32_t w1, const uint32_t w2,
598 const uint8_t dist) {
599 inc(encodeCollocation(w1, w2, dist));
600}
601
602void CollocatorDB::readVocab(const string& fname) {
603 char strbuf[2048];
604 uint64_t freq;
605 FILE *fin = fopen(fname.c_str(), "rb");
606 if (fin == nullptr) {
607 cout << "Vocabulary file " << fname << " not found\n";
608 exit(1);
609 }
610 uint64_t i = 0;
611 while (fscanf(fin, "%s %lu", strbuf, &freq) == 2) {
612 _vocab.push_back({strbuf, freq});
613 total += freq;
614 i++;
615 }
616 fclose(fin);
617
618 char size_fname[256];
619 strcpy(size_fname, fname.c_str());
620 char *pos = strstr(size_fname, ".vocab");
621 if (pos) {
622 *pos = 0;
623 strcat(size_fname, ".size");
624 FILE *fp = fopen(size_fname, "r");
625 if (fp != nullptr) {
626 fscanf(fp, "%lu", &sentences);
627 fscanf(fp, "%lu", &total);
628 float sl = (float)total / (float)sentences;
629 float w = WINDOW_SIZE;
630 avg_window_size =
631 ((sl > 2 * w ? (sl - 2 * w) * 2 * w : 0) + (double)w * (3 * w - 1)) /
632 sl;
633 fprintf(stdout,
634 "Size corrections found: corpus size: %lu tokens in %lu "
635 "sentences, avg. sentence size: %f, avg. window size: %f\n",
636 total, sentences, sl, avg_window_size);
637 fclose(fp);
638 } else {
639 // std::cout << "size file " << size_fname << " not found\n";
640 }
641 } else {
642 std::cout << "cannot determine size file " << size_fname << "\n";
643 }
644}
645
646std::shared_ptr<DB> CollocatorDB::OpenDbForRead(const char *name) {
Marc Kupietz65d44792026-07-31 09:46:34 +0900647 ROCKSDB_DB_HANDLE db;
Marc Kupietz39887082024-11-22 18:06:20 +0100648 Options options;
649 options.env->SetBackgroundThreads(4);
650 options.create_if_missing = true;
651 options.merge_operator = std::make_shared<CountMergeOperator>();
652 options.max_successive_merges = 0;
653 // options.prefix_extractor.reset(NewFixedPrefixTransform(8));
654 options.IncreaseParallelism();
655 options.OptimizeLevelStyleCompaction();
656 options.prefix_extractor.reset(NewFixedPrefixTransform(3));
657 ostringstream dbname, vocabname;
658 dbname << name << ".rocksdb";
659 auto s = DB::OpenForReadOnly(options, dbname.str(), &db);
660 if (!s.ok()) {
661 std::cerr << s.ToString() << std::endl;
662 assert(false);
663 }
664 vocabname << name << ".vocab";
665 readVocab(vocabname.str());
Marc Kupietz65d44792026-07-31 09:46:34 +0900666 return std::shared_ptr<DB>(ROCKSDB_DB_RELEASE(db));
Marc Kupietz39887082024-11-22 18:06:20 +0100667}
668
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200669 /* Reads a size from the environment, so that a long indexing run can be
670 tuned without recompiling. Returns the default when unset or unusable. */
671 static uint64_t env_size(const char *name, uint64_t fallback) {
672 const char *value = getenv(name);
673 if (value == nullptr)
674 return fallback;
675 char *end = nullptr;
676 unsigned long long parsed = strtoull(value, &end, 10);
677 if (end == value || parsed == 0) {
678 std::cerr << "collocatordb: ignoring " << name << "=" << value << std::endl;
679 return fallback;
680 }
681 return (uint64_t)parsed;
682 }
683
Marc Kupietzc630c152025-01-23 11:17:47 +0100684 std::shared_ptr<DB> CollocatorDB::OpenDb(const char *dbname) {
Marc Kupietz65d44792026-07-31 09:46:34 +0900685 ROCKSDB_DB_HANDLE db;
Marc Kupietzc630c152025-01-23 11:17:47 +0100686 Options options;
Marc Kupietz39887082024-11-22 18:06:20 +0100687
Marc Kupietzc630c152025-01-23 11:17:47 +0100688 int max_cores = static_cast<int>(std::thread::hardware_concurrency());
689
Marc Kupietzc630c152025-01-23 11:17:47 +0100690 options.create_if_missing = true;
691 options.merge_operator = std::make_shared<CountMergeOperator>();
Marc Kupietzc630c152025-01-23 11:17:47 +0100692
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200693 /* Indexing a corpus is one long stream of merge operands for the same
694 keys. rocksdb only collapses them when memtables are merged and when
695 files are compacted, so the settings aim at doing that early and often -
696 every collapsed operand is one less to write, to read and to merge
697 again later.
Marc Kupietzc630c152025-01-23 11:17:47 +0100698
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200699 All of it can be overridden from the environment, to be able to tune a
700 run that takes days without recompiling. */
701 options.write_buffer_size = env_size("COLLOCATORDB_WRITE_BUFFER_MB", 256) << 20;
702 options.max_write_buffer_number = (int)env_size("COLLOCATORDB_WRITE_BUFFERS", 8);
703 /* collapses the operands of several memtables before they are written */
704 options.min_write_buffer_number_to_merge =
705 (int)env_size("COLLOCATORDB_WRITE_BUFFERS_TO_MERGE", 4);
706
707 /* Compaction has to keep up with the writer, otherwise the level 0 files
708 pile up and every read has to merge through all of them. */
709 options.max_background_jobs =
710 (int)env_size("COLLOCATORDB_BACKGROUND_JOBS",
711 max_cores > 4 ? (max_cores < 32 ? max_cores : 32) : 4);
712 options.max_subcompactions = (int)env_size("COLLOCATORDB_SUBCOMPACTIONS", 4);
713 options.level0_file_num_compaction_trigger = 4;
714 options.level0_slowdown_writes_trigger = 20;
715 options.level0_stop_writes_trigger = 36;
716
717 /* Merge operands are inserted one writer at a time, rocksdb does not
718 support concurrent memtable writes for them, which is why more threads
719 in the indexer do not help beyond a certain point. */
720 options.allow_concurrent_memtable_write = false;
Marc Kupietzc630c152025-01-23 11:17:47 +0100721 options.enable_write_thread_adaptive_yield = true;
Marc Kupietzc630c152025-01-23 11:17:47 +0100722 options.allow_mmap_reads = true;
723
Marc Kupietzc630c152025-01-23 11:17:47 +0100724 BlockBasedTableOptions table_options;
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200725 table_options.block_cache =
726 NewLRUCache(env_size("COLLOCATORDB_BLOCK_CACHE_MB", 512) << 20);
Marc Kupietzc630c152025-01-23 11:17:47 +0100727 options.table_factory.reset(NewBlockBasedTableFactory(table_options));
728
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200729 /* No write ahead log: an interrupted indexing run is repeated, and the log
730 would double the amount written. Everything that has not been flushed is
731 lost when the process dies, which is what close_collocatordb() is for. */
732 merge_option_.disableWAL = true;
733 merge_option_.sync = false;
734 /* Let rocksdb slow the writer down when compaction falls behind, instead
735 of cancelling the write. It used to be told to do neither, which threw
736 counts away. merge_one() repeats a cancelled write, whatever the
737 settings are. */
738 merge_option_.low_pri = false;
739 merge_option_.no_slowdown = false;
740 blocking_merge_option_ = merge_option_;
741 blocking_merge_option_.low_pri = false;
742 blocking_merge_option_.no_slowdown = false;
Marc Kupietzc630c152025-01-23 11:17:47 +0100743
744 Status s = DB::Open(options, dbname, &db);
745 if (!s.ok()) {
746 std::cerr << s.ToString() << std::endl;
747 assert(false);
748 }
749 total = 1000;
Marc Kupietz65d44792026-07-31 09:46:34 +0900750 return std::shared_ptr<DB>(ROCKSDB_DB_RELEASE(db));
Marc Kupietz39887082024-11-22 18:06:20 +0100751 }
Marc Kupietz39887082024-11-22 18:06:20 +0100752
753CollocatorIterator *
754CollocatorDB::SeekIterator(uint64_t w1, uint64_t w2, int8_t dist) const {
Marc Kupietz122ba3c2026-08-09 17:13:24 +0200755 flush();
Marc Kupietz39887082024-11-22 18:06:20 +0100756 ReadOptions options;
757 options.prefix_same_as_start = true;
758 char prefixc[sizeof(uint64_t)];
759 EncodeFixed64(prefixc, encodeCollocation(w1, w2, dist));
760 Iterator *it = db_->NewIterator(options);
761 auto *cit = new CollocatorIterator(it);
762 if (w2 > 0)
763 cit->Seek(std::string(prefixc, 6));
764 else
765 cit->Seek(std::string(prefixc, 3));
766 cit->setPrefix(prefixc);
767 return cit;
768}
769
770void CollocatorDB::dump(uint32_t w1, uint32_t w2, int8_t dist) const {
771 auto it = std::unique_ptr<CollocatorIterator>(SeekIterator(w1, w2, dist));
772 for (; it->isValid(); it->Next()) {
773 uint64_t value = it->intValue();
774 uint64_t key = it->intKey();
775 std::cout << "w1:" << W1(key) << ", w2:" << W2(key)
776 << ", dist:" << (int32_t)DIST(key) << " - count:" << value
777 << std::endl;
778 }
779 std::cout << "ready dumping\n";
780}
781
782bool sortByNpmi(const Collocator &lhs, const Collocator &rhs) {
783 return lhs.npmi > rhs.npmi;
784}
785
786bool sortByLfmd(const Collocator &lhs, const Collocator &rhs) {
787 return lhs.lfmd > rhs.lfmd;
788}
789
790bool sortByLlr(const Collocator &lhs, const Collocator &rhs) {
791 return lhs.llr > rhs.llr;
792}
793
794bool sortByLogDice(const Collocator &lhs, const Collocator &rhs) {
795 return lhs.logdice > rhs.logdice;
796}
797
798bool sortByLogDiceAF(const Collocator &lhs, const Collocator &rhs) {
799 return lhs.ldaf > rhs.ldaf;
800}
801
802void CollocatorDB::applyCAMeasures(
803 const uint32_t w1, const uint32_t w2, uint64_t *sumWindow,
804 const uint64_t sum, const int usedPositions, int true_window_size,
805 Collocator *result) const {
806 uint64_t f1 = _vocab[w1].freq, f2 = _vocab[w2].freq;
807 double o = sum, r1 = f1 * true_window_size, c1 = f2, e = r1 * c1 / total,
808 pmi = log2(o / e), md = log2(o * o / e), lfmd = log2(o * o * o / e),
Marc Kupietze889cec2024-11-23 12:08:42 +0100809 llr = ca_ll(f1, f2, sum, total, true_window_size),
810 md_nws = ca_md(f1, f2, sum, total, 2 * WINDOW_SIZE),
811 ld = ca_logdice(f1, f2, sum, total, true_window_size);
Marc Kupietz39887082024-11-22 18:06:20 +0100812
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200813 // LDaf is the auto focus score itself, i.e. the highest value that any
814 // selection of positions reaches. It is deliberately not logDice of the
815 // selected window: the width penalty is what makes the score sensitive to
816 // how concentrated a pair is, and that sensitivity is why LDaf orders
817 // collocates more usefully than the other measures.
Marc Kupietz39887082024-11-22 18:06:20 +0100818 int bestWindow = usedPositions;
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200819 double bestFocus = ca_focus_score(f1, f2, sum, total, true_window_size);
Marc Kupietz39887082024-11-22 18:06:20 +0100820 // if(f1<75000000)
821 // #pragma omp parallel for reduction(max:bestAF)
822 // #pragma omp target teams distribute parallel for reduction(max:bestAF)
823 // map(tofrom:bestAF,currentAF,bestWindow,usedPositions)
824 for (int bitmask = 1; bitmask < (1 << (2 * WINDOW_SIZE)); bitmask++) {
825 if ((bitmask & usedPositions) == 0 || (bitmask & ~usedPositions) > 0)
826 continue;
827 uint64_t currentWindowSum = 0;
828 // #pragma omp target teams distribute parallel for
829 // reduction(+:currentWindowSum) map(tofrom:bitmask,usedPositions)
830 for (int pos = 0; pos < 2 * WINDOW_SIZE; pos++) {
831 if (((1 << pos) & bitmask & usedPositions) != 0)
832 currentWindowSum += sumWindow[pos];
833 }
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200834 double currentAF = ca_focus_score(f1, f2, currentWindowSum, total,
835 __builtin_popcount(bitmask));
836 if (currentAF > bestFocus) {
837 bestFocus = currentAF;
Marc Kupietz39887082024-11-22 18:06:20 +0100838 bestWindow = bitmask;
839 }
840 }
841
842 *result = {w2,
843 f2,
844 sum,
845 pmi,
846 pmi / (-log2(o / total / true_window_size)),
847 llr,
848 lfmd,
849 md,
Marc Kupietze889cec2024-11-23 12:08:42 +0100850 md_nws,
Marc Kupietz39887082024-11-22 18:06:20 +0100851 sumWindow[WINDOW_SIZE],
852 sumWindow[WINDOW_SIZE - 1],
853 ca_pmi(f1, f2, sumWindow[WINDOW_SIZE], total, 1),
854 ca_pmi(f1, f2, sumWindow[WINDOW_SIZE - 1], total, 1),
855 ca_dice(f1, f2, sum, total, true_window_size),
856 ld,
Marc Kupietz0fec0be2026-09-04 19:42:38 +0200857 bestFocus,
Marc Kupietz39887082024-11-22 18:06:20 +0100858 usedPositions,
859 bestWindow};
860}
861
862std::vector<Collocator>
863CollocatorDB::get_collocators(uint32_t w1, uint32_t min_w2,
864 uint32_t max_w2) {
865 std::vector<Collocator> collocators;
866 uint64_t w2, last_w2 = 0xffffffffffffffff;
867 uint64_t maxv = 0, sum = 0;
Marc Kupietzaa354d82026-07-31 09:17:57 +0900868 /* fixed size, so it lives on the stack and cannot be leaked */
869 uint64_t sumWindow[2 * WINDOW_SIZE];
870 memset(sumWindow, 0, sizeof(sumWindow));
Marc Kupietze5108542026-09-04 15:29:52 +0200871 /* Counted up from zero, since the first key of the iteration takes the same
872 branch as any further key of the same collocate, which increments it. It
873 used to start at one, so that the first collocate of every query was scored
874 with a window one position too wide, which inflated its expected frequency
875 and thus lowered its pmi, npmi, md, lfmd and llr. */
876 int true_window_size = 0;
Marc Kupietz39887082024-11-22 18:06:20 +0100877 int usedPositions = 0;
878
879 if (w1 > _vocab.size()) {
880 std::cout << w1 << "> vocabulary size " << _vocab.size() << "\n";
881 w1 -= _vocab.size();
882 }
883#ifdef DEBUG
884 std::cout << "Searching for collocates of " << _vocab[w1].word << "\n";
885#endif
886 // #pragma omp parallel num_threads(40)
887 // #pragma omp single
888 for (auto it =
889 std::unique_ptr<CollocatorIterator>(SeekIterator(w1, min_w2, 0));
890 it->isValid(); it->Next()) {
891 uint64_t value = it->intValue(), key = it->intKey();
892 if ((w2 = W2(key)) > max_w2)
893 continue;
894 if (last_w2 == 0xffffffffffffffff)
895 last_w2 = w2;
896 if (w2 != last_w2) {
897 if (sum >= FREQUENCY_THRESHOLD) {
898 collocators.push_back({});
899 Collocator *result = &(collocators[collocators.size() - 1]);
900 // #pragma omp task firstprivate(last_w2, sumWindow, sum, usedPositions,
901 // true_window_size) shared(w1, result) if(sum > 1000000)
902 {
903 // uint64_t *nsw = (uint64_t *)malloc(sizeof(uint64_t) * 2
904 // *WINDOW_SIZE); memcpy(nsw, sumWindow, sizeof(uint64_t) * 2
905 // *WINDOW_SIZE);
906 applyCAMeasures(w1, last_w2, sumWindow, sum, usedPositions,
907 true_window_size, result);
908 // free(nsw);
909 }
910 }
911 memset(sumWindow, 0, 2 * WINDOW_SIZE * sizeof(uint64_t));
912 usedPositions = 1 << (-DIST(key) + WINDOW_SIZE - (DIST(key) < 0 ? 1 : 0));
913 sumWindow[-DIST(key) + WINDOW_SIZE - (DIST(key) < 0 ? 1 : 0)] = value;
914 last_w2 = w2;
915 maxv = value;
916 sum = value;
917 true_window_size = 1;
918 if (min_w2 == max_w2 && w2 != min_w2)
919 break;
920 } else {
921 sum += value;
922 if (value > maxv)
923 maxv = value;
924 usedPositions |=
925 1 << (-DIST(key) + WINDOW_SIZE - (DIST(key) < 0 ? 1 : 0));
926 sumWindow[-DIST(key) + WINDOW_SIZE - (DIST(key) < 0 ? 1 : 0)] = value;
927 true_window_size++;
928 }
929 }
930
Marc Kupietz2f65bcf2026-08-02 14:04:37 +0200931 /* A collocate is only finished when the next one begins, so the last one is
932 still in sumWindow when the iterator is through. It used to be dropped,
933 which cost every word one of its collocates. */
934 if (last_w2 != 0xffffffffffffffff && sum >= FREQUENCY_THRESHOLD) {
935 collocators.push_back({});
936 applyCAMeasures(w1, last_w2, sumWindow, sum, usedPositions,
937 true_window_size, &(collocators[collocators.size() - 1]));
938 }
939
Marc Kupietz39887082024-11-22 18:06:20 +0100940 // #pragma omp taskwait
941 sort(collocators.begin(), collocators.end(), sortByLogDiceAF);
942
943#ifdef DEBUG
944 int i = 0;
945 for (Collocator c : collocators) {
946 if (i++ > 10)
947 break;
948 std::cout << "w1:" << _vocab[w1].word << ", w2: *" << _vocab[c.w2].word
949 << "*"
950 << "\t f(w1):" << _vocab[w1].freq
951 << "\t f(w2):" << _vocab[c.w2].freq << "\t f(w1, w2):" << c.raw
952 << "\t pmi:" << c.pmi << "\t npmi:" << c.npmi
953 << "\t llr:" << c.llr << "\t md:" << c.md << "\t lfmd:" << c.lfmd
954 << "\t total:" << total << std::endl;
955 }
956#endif
957
958 return collocators;
959}
960
961std::vector<Collocator>
962CollocatorDB::get_collocation_scores(uint32_t w1, uint32_t w2) {
963 return get_collocators(w1, w2, w2);
964}
965
966std::vector<Collocator> CollocatorDB::get_collocators(uint32_t w1) {
967 return get_collocators(w1, 0, UINT32_MAX);
968}
969
970void CollocatorDB::dumpSparseLlr(uint32_t w1, uint32_t min_cooccur) {
971 std::vector<Collocator> collocators;
972 std::stringstream stream;
973 uint64_t w2, last_w2 = 0xffffffffffffffff;
974 uint64_t maxv = 0, total_w1 = 0;
975 bool first = true;
976 for (auto it = std::unique_ptr<CollocatorIterator>(SeekIterator(w1, 0, 0));
977 it->isValid(); it->Next()) {
978 uint64_t value = it->intValue(), key = it->intKey();
979 w2 = W2(key);
980 total_w1 += value;
981 if (last_w2 == 0xffffffffffffffff)
982 last_w2 = w2;
983 if (w2 != last_w2) {
984 if (maxv >= min_cooccur) {
985 double llr =
986 ca_ll(_vocab[w1].freq, _vocab[last_w2].freq, maxv, total, 1);
987 if (first)
988 first = false;
989 else
990 stream << " ";
991 stream << w2 << " " << llr;
992 }
993 last_w2 = w2;
994 maxv = value;
995 } else {
996 if (value > maxv)
997 maxv = value;
998 }
999 }
1000 if (first)
1001 stream << "1 0.0";
1002 stream << "\n";
1003 std::cout << stream.str();
1004}
1005
1006Slice CollocatorIterator::key() const {
1007 return base_iterator_->key();
1008}
1009
1010Slice CollocatorIterator::value() const {
1011 return base_iterator_->value();
1012}
1013
1014Status CollocatorIterator::status() const {
1015 return base_iterator_->status();
1016}
1017
1018}; // namespace rocksdb
1019
1020string CollocatorDB::getWord(uint32_t w1) { return _vocab[w1].word; }
1021
1022uint64_t CollocatorDB::getWordId(const char *word) const {
Marc Kupietz979580e2024-11-21 18:05:07 +01001023 for (uint64_t i = 0; i < _vocab.size(); i++) {
1024 if (strcmp(_vocab[i].word.c_str(), word) == 0)
1025 return i;
1026 }
1027 return 0;
1028}
1029
Marc Kupietzd26b1052024-12-10 16:56:39 +01001030uint64_t CollocatorDB::getCorpusSize() const {
1031 return total;
1032}
1033
Marc Kupietz21b964c2024-12-10 17:10:50 +01001034uint64_t CollocatorDB::getWordFrequency(uint64_t w1) {
1035 return _vocab[w1].freq;
1036}
1037
Marc Kupietz39887082024-11-22 18:06:20 +01001038string CollocatorDB::collocators2json(uint32_t w1,
1039 const vector<Collocator>& collocators) {
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001040 ostringstream s;
Marc Kupietz0dd86ef2018-01-11 22:23:17 +01001041 int i = 0;
Marc Kupietz39887082024-11-22 18:06:20 +01001042 s << " { \"f1\": " << _vocab[w1].freq << "," << R"("w1":")"
1043 << string(_vocab[w1].word) << "\", " << "\"N\": " << total << ", "
1044 << "\"collocates\": [";
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001045 bool first = true;
1046 for (Collocator c : collocators) {
Marc Kupietz39887082024-11-22 18:06:20 +01001047 if (strncmp(_vocab[c.w2].word.c_str(), "quot", 4) == 0)
1048 continue;
Marc Kupietz0dd86ef2018-01-11 22:23:17 +01001049 if (i++ > 200)
1050 break;
Marc Kupietz12af0192021-03-13 18:05:14 +01001051 if (!first)
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001052 s << ",\n";
1053 else
1054 first = false;
1055 s << "{"
Marc Kupietz39887082024-11-22 18:06:20 +01001056 "\"word\":\""
1057 << (string(_vocab[c.w2].word) == "<num>"
1058 ? string("###")
1059 : string(_vocab[c.w2].word))
1060 << "\"," << "\"f2\":" << c.f2 << "," << "\"f\":" << c.raw << ","
1061 << "\"npmi\":" << c.npmi << "," << "\"pmi\":" << c.pmi << ","
1062 << "\"llr\":" << c.llr << "," << "\"lfmd\":" << c.lfmd << ","
Marc Kupietze889cec2024-11-23 12:08:42 +01001063 << "\"md\":" << c.md << "," << "\"md_nws\":" << c.md_nws << "," << "\"dice\":" << c.dice << ","
Marc Kupietz39887082024-11-22 18:06:20 +01001064 << "\"ld\":" << c.logdice << "," << "\"ln_count\":" << c.left_raw << ","
1065 << "\"rn_count\":" << c.right_raw << "," << "\"ln_pmi\":" << c.left_pmi
1066 << "," << "\"rn_pmi\":" << c.right_pmi << "," << "\"ldaf\":" << c.ldaf
1067 << "," << "\"win\":" << c.window << "," << "\"afwin\":" << c.af_window
1068 << "}";
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001069 }
Marc Kupietze9627152019-02-04 12:32:12 +01001070 s << "]}\n";
Marc Kupietz0421d092021-03-13 18:05:14 +01001071 // std::cout << s.str();
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001072 return s.str();
1073}
1074
Marc Kupietz39887082024-11-22 18:06:20 +01001075typedef CollocatorDB COLLOCATORS;
Marc Kupietz06c9a9f2018-01-02 16:56:43 +01001076
1077extern "C" {
Marc Kupietz12af0192021-03-13 18:05:14 +01001078#ifdef __clang__
1079#pragma clang diagnostic push
1080#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection"
1081#endif
Marc Kupietz39887082024-11-22 18:06:20 +01001082DLL_EXPORT COLLOCATORS *open_collocatordb_for_write(char *dbname) {
1083 return new CollocatorDB(dbname, false);
1084}
Marc Kupietz12af0192021-03-13 18:05:14 +01001085
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001086DLL_EXPORT void close_collocatordb(COLLOCATORS *db) { delete db; }
1087
Marc Kupietz39887082024-11-22 18:06:20 +01001088DLL_EXPORT COLLOCATORS *open_collocatordb(char *dbname) {
1089 return new CollocatorDB(dbname, true);
1090}
Marc Kupietz06c9a9f2018-01-02 16:56:43 +01001091
Marc Kupietz39887082024-11-22 18:06:20 +01001092DLL_EXPORT void inc_collocator(COLLOCATORS *db, uint32_t w1, uint32_t w2,
1093 int8_t dist) {
1094 db->inc(w1, w2, dist);
1095}
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001096
Marc Kupietz39887082024-11-22 18:06:20 +01001097DLL_EXPORT void dump_collocators(COLLOCATORS *db, uint32_t w1, uint32_t w2,
1098 int8_t dist) {
1099 db->dump(w1, w2, dist);
1100}
Marc Kupietzc8ddf452018-01-07 21:33:12 +01001101
Marc Kupietz39887082024-11-22 18:06:20 +01001102DLL_EXPORT COLLOCATORS *get_collocators(COLLOCATORS *db, uint32_t w1) {
1103 std::vector<Collocator> c = db->get_collocators(w1);
1104 if (c.empty())
1105 return nullptr;
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001106 /* one entry more than there are collocators, terminated with w2 == 0 and
1107 raw == 0, so that callers can tell where the array ends */
1108 uint64_t size = (c.size() + 1) * sizeof c[0];
Marc Kupietz39887082024-11-22 18:06:20 +01001109 auto *p = (COLLOCATORS *)malloc(size);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001110 memset(p, 0, size);
1111 memcpy(p, c.data(), c.size() * sizeof c[0]);
Marc Kupietz39887082024-11-22 18:06:20 +01001112 return p;
1113}
Marc Kupietz88d116b2021-03-13 18:05:14 +01001114
Marc Kupietz39887082024-11-22 18:06:20 +01001115DLL_EXPORT COLLOCATORS *get_collocation_scores(COLLOCATORS *db, uint32_t w1,
1116 uint32_t w2) {
1117 std::vector<Collocator> c = db->get_collocation_scores(w1, w2);
1118 if (c.empty())
1119 return nullptr;
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001120 /* one entry more than there are collocators, terminated with w2 == 0 and
1121 raw == 0, so that callers can tell where the array ends */
1122 uint64_t size = (c.size() + 1) * sizeof c[0];
Marc Kupietz39887082024-11-22 18:06:20 +01001123 auto *p = (COLLOCATORS *)malloc(size);
Marc Kupietz2f65bcf2026-08-02 14:04:37 +02001124 memset(p, 0, size);
1125 memcpy(p, c.data(), c.size() * sizeof c[0]);
Marc Kupietz39887082024-11-22 18:06:20 +01001126 return p;
1127}
Marc Kupietzca3a52e2018-06-05 14:16:23 +02001128
Marc Kupietz39887082024-11-22 18:06:20 +01001129DLL_EXPORT char *get_word(COLLOCATORS *db, uint32_t w) {
1130 return strdup(db->getWord(w).c_str());
1131}
Marc Kupietz979580e2024-11-21 18:05:07 +01001132
Marc Kupietz39887082024-11-22 18:06:20 +01001133DLL_EXPORT uint64_t get_word_id(COLLOCATORS *db, char *word) {
1134 return db->getWordId(word);
1135}
Marc Kupietzb4a683c2021-03-14 09:19:44 +01001136
Marc Kupietz39887082024-11-22 18:06:20 +01001137DLL_EXPORT void read_vocab(COLLOCATORS *db, char *fname) {
1138 std::string fName(fname);
1139 db->readVocab(fName);
1140}
Marc Kupietz88d116b2021-03-13 18:05:14 +01001141
Marc Kupietz39887082024-11-22 18:06:20 +01001142DLL_EXPORT const char *get_collocators_as_json(COLLOCATORS *db, uint32_t w1) {
1143 return strdup(db->collocators2json(w1, db->get_collocators(w1)).c_str());
1144}
Marc Kupietzb4a683c2021-03-14 09:19:44 +01001145
Marc Kupietz39887082024-11-22 18:06:20 +01001146DLL_EXPORT const char *
1147get_collocation_scores_as_json(COLLOCATORS *db, uint32_t w1, uint32_t w2) {
1148 return strdup(
1149 db->collocators2json(w1, db->get_collocation_scores(w1, w2)).c_str());
1150}
1151
1152DLL_EXPORT const char *get_version() { return PROJECT_VERSION; }
Marc Kupietz6208fd72024-11-15 15:46:19 +01001153
Marc Kupietzd26b1052024-12-10 16:56:39 +01001154DLL_EXPORT uint64_t get_corpus_size(COLLOCATORS *db) { return db->getCorpusSize(); };
1155
Marc Kupietz21b964c2024-12-10 17:10:50 +01001156DLL_EXPORT uint64_t get_word_frequency(COLLOCATORS *db, uint64_t w1) {
1157 return db->getWordFrequency(w1);
1158}
1159
Marc Kupietz12af0192021-03-13 18:05:14 +01001160#ifdef __clang__
1161#pragma clang diagnostic push
1162#endif
Marc Kupietz06c9a9f2018-01-02 16:56:43 +01001163}