usearch 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/python/lib.cpp CHANGED
@@ -29,19 +29,20 @@ using py_shape_t = py::array::ShapeContainer;
29
29
  using label_t = Py_ssize_t;
30
30
  using distance_t = punned_distance_t;
31
31
  using id_t = std::uint32_t;
32
- using punned_t = punned_gt<label_t, id_t>;
33
32
 
34
- struct punned_py_t : public punned_t {
35
- using native_t = punned_t;
33
+ using punned_index_t = punned_gt<label_t, id_t>;
34
+ using punned_add_result_t = typename punned_index_t::add_result_t;
35
+ using punned_search_result_t = typename punned_index_t::search_result_t;
36
+
37
+ struct punned_index_py_t : public punned_index_t {
38
+ using native_t = punned_index_t;
36
39
  using native_t::add;
37
40
  using native_t::capacity;
38
41
  using native_t::reserve;
39
42
  using native_t::search;
40
43
  using native_t::size;
41
44
 
42
- std::vector<char> ascii_normalization_buffer_;
43
-
44
- punned_py_t(native_t&& base) : native_t(std::move(base)) {}
45
+ punned_index_py_t(native_t&& base) : native_t(std::move(base)) {}
45
46
  };
46
47
 
47
48
  using set_member_t = std::uint32_t;
@@ -59,33 +60,53 @@ struct sets_index_py_t : public sets_index_t {
59
60
  sets_index_py_t(native_t&& base) : native_t(std::move(base)) {}
60
61
  };
61
62
 
62
- using hash_word_t = std::uint64_t;
63
- using hash_index_t = index_gt<bit_hamming_gt<hash_word_t>, label_t, id_t, hash_word_t>;
64
- static constexpr std::size_t bits_per_hash_word_k = sizeof(hash_word_t) * CHAR_BIT;
63
+ using bits_numpy_word_t = std::uint8_t;
64
+ using bits_native_word_t = std::uint64_t;
65
+ using bits_distance_t = float;
66
+ using bits_metric_t =
67
+ std::function<bits_distance_t(bits_native_word_t const*, bits_native_word_t const*, std::size_t, std::size_t)>;
68
+ using bits_index_t = index_gt<bits_metric_t, label_t, id_t, bits_native_word_t>;
69
+ using bits_add_result_t = typename bits_index_t::add_result_t;
70
+ using bits_search_result_t = typename bits_index_t::search_result_t;
71
+
72
+ static constexpr std::size_t bits_per_hash_numpy_word_k = sizeof(bits_numpy_word_t) * CHAR_BIT;
73
+ static constexpr std::size_t bits_per_hash_native_word_k = sizeof(bits_native_word_t) * CHAR_BIT;
65
74
 
66
- struct hash_index_py_t : public hash_index_t {
67
- using native_t = hash_index_t;
75
+ struct bits_index_py_t : public bits_index_t {
76
+ using native_t = bits_index_t;
68
77
  using native_t::add;
69
78
  using native_t::capacity;
79
+ using native_t::limits;
70
80
  using native_t::reserve;
71
81
  using native_t::search;
72
82
  using native_t::size;
73
83
 
74
- std::vector<hash_word_t> buffer_;
75
- std::size_t words_;
76
84
  std::size_t bits_;
85
+ std::size_t numpy_words_;
86
+ std::size_t native_words_;
87
+ std::vector<bits_native_word_t> word_aligned_buffer_;
77
88
 
78
- hash_index_py_t(native_t&& base) : native_t(std::move(base)) {}
79
-
80
- hash_index_py_t(config_t config, std::size_t bits) : hash_index_t(config) {
81
- words_ = divide_round_up<bits_per_hash_word_k>(bits);
82
- bits_ = words_ * bits_per_hash_word_k;
83
- buffer_.resize(words_);
84
- }
89
+ bits_index_py_t(native_t&& base) : native_t(std::move(base)) {}
85
90
 
86
- span_gt<hash_word_t const> buffer() const noexcept { return {buffer_.data(), buffer_.size()}; }
91
+ bits_index_py_t(index_config_t config, std::size_t bits, bits_metric_t metric)
92
+ : bits_index_t(config, metric), bits_(bits), //
93
+ numpy_words_(divide_round_up<bits_per_hash_numpy_word_k>(bits)), //
94
+ native_words_(divide_round_up<bits_per_hash_native_word_k>(bits)), //
95
+ word_aligned_buffer_(native_words_* index_limits_t{}.threads()) {}
87
96
  };
88
97
 
98
+ bits_metric_t bits_metric(common_metric_kind_t kind) {
99
+ switch (kind) {
100
+ case common_metric_kind_t::bitwise_hamming_k:
101
+ return bits_metric_t(bitwise_hamming_gt<bits_native_word_t, bits_distance_t>{});
102
+ case common_metric_kind_t::bitwise_tanimoto_k:
103
+ return bits_metric_t(bitwise_tanimoto_gt<bits_native_word_t, bits_distance_t>{});
104
+ case common_metric_kind_t::bitwise_sorensen_k:
105
+ return bits_metric_t(bitwise_sorensen_gt<bits_native_word_t, bits_distance_t>{});
106
+ default: throw std::invalid_argument("Not a bitwise metric!"); return {};
107
+ }
108
+ }
109
+
89
110
  template <typename scalar_at> punned_stateful_metric_t udf(std::size_t metric_uintptr) {
90
111
  return [metric_uintptr](byte_t const* a_bytes, byte_t const* b_bytes, std::size_t n, std::size_t m) -> distance_t {
91
112
  using metric_t = punned_distance_t (*)(scalar_at const*, scalar_at const*, std::size_t, std::size_t);
@@ -105,71 +126,59 @@ punned_stateful_metric_t udf(std::size_t metric_uintptr, accuracy_t accuracy) {
105
126
  }
106
127
  }
107
128
 
108
- static punned_py_t make_index( //
109
- std::size_t dimensions, //
110
- std::size_t capacity, //
111
- std::string const& scalar_type, //
112
- std::string const& metric, //
113
- std::size_t connectivity, //
114
- std::size_t expansion_add, //
115
- std::size_t expansion_search, //
116
- std::size_t metric_uintptr, //
129
+ static punned_index_py_t make_index( //
130
+ std::size_t dimensions, //
131
+ std::string const& scalar_type, //
132
+ common_metric_kind_t metric, //
133
+ std::size_t connectivity, //
134
+ std::size_t expansion_add, //
135
+ std::size_t expansion_search, //
136
+ std::size_t metric_uintptr, //
117
137
  bool tune) {
118
138
 
119
- config_t config;
139
+ index_config_t config;
120
140
  config.expansion_add = expansion_add;
121
141
  config.expansion_search = expansion_search;
122
142
  config.connectivity = connectivity;
123
- config.max_elements = capacity;
124
- config.max_threads_add = std::thread::hardware_concurrency();
125
- config.max_threads_search = std::thread::hardware_concurrency();
126
143
 
127
144
  if (tune)
128
- config = punned_t::optimize(config);
145
+ config = punned_index_t::optimize(config);
129
146
 
130
147
  accuracy_t accuracy = accuracy_from_name(scalar_type.c_str(), scalar_type.size());
131
148
  if (metric_uintptr)
132
- return punned_t::udf(dimensions, udf(metric_uintptr, accuracy), accuracy, config);
149
+ return punned_index_t::udf(dimensions, udf(metric_uintptr, accuracy), accuracy, config);
133
150
  else
134
- return index_from_name<punned_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
151
+ return punned_index_t(make_punned<punned_index_t>(metric, dimensions, accuracy, config));
135
152
  }
136
153
 
137
154
  static std::unique_ptr<sets_index_py_t> make_sets_index( //
138
- std::size_t capacity, //
139
155
  std::size_t connectivity, //
140
156
  std::size_t expansion_add, //
141
157
  std::size_t expansion_search //
142
158
  ) {
143
- config_t config;
159
+ index_config_t config;
144
160
  config.expansion_add = expansion_add;
145
161
  config.expansion_search = expansion_search;
146
162
  config.connectivity = connectivity;
147
- config.max_elements = capacity;
148
- config.max_threads_add = 1;
149
- config.max_threads_search = 1;
150
163
 
151
164
  return std::unique_ptr<sets_index_py_t>(new sets_index_py_t(sets_index_t(config)));
152
165
  }
153
166
 
154
- static std::unique_ptr<hash_index_py_t> make_hash_index( //
167
+ static std::unique_ptr<bits_index_py_t> make_hash_index( //
155
168
  std::size_t bits, //
156
- std::size_t capacity, //
169
+ common_metric_kind_t metric, //
157
170
  std::size_t connectivity, //
158
171
  std::size_t expansion_add, //
159
172
  std::size_t expansion_search //
160
173
  ) {
161
- config_t config;
174
+ index_config_t config;
162
175
  config.expansion_add = expansion_add;
163
176
  config.expansion_search = expansion_search;
164
177
  config.connectivity = connectivity;
165
- config.max_elements = capacity;
166
- config.max_threads_add = 1;
167
- config.max_threads_search = 1;
168
-
169
- return std::unique_ptr<hash_index_py_t>(new hash_index_py_t(config, bits));
178
+ return std::unique_ptr<bits_index_py_t>(new bits_index_py_t(config, bits, bits_metric(metric)));
170
179
  }
171
180
 
172
- static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vector, bool copy, std::size_t) {
181
+ static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer vector, bool copy, std::size_t) {
173
182
 
174
183
  py::buffer_info vector_info = vector.request();
175
184
  if (vector_info.ndim != 1)
@@ -188,22 +197,22 @@ static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vecto
188
197
 
189
198
  // https://docs.python.org/3/library/struct.html#format-characters
190
199
  if (vector_info.format == "c" || vector_info.format == "b")
191
- index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config);
200
+ index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config).error.raise();
192
201
  else if (vector_info.format == "e")
193
- index.add(label, reinterpret_cast<f16_bits_t const*>(vector_data), config);
202
+ index.add(label, reinterpret_cast<f16_bits_t const*>(vector_data), config).error.raise();
194
203
  else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
195
- index.add(label, reinterpret_cast<float const*>(vector_data), config);
204
+ index.add(label, reinterpret_cast<float const*>(vector_data), config).error.raise();
196
205
  else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
197
- index.add(label, reinterpret_cast<double const*>(vector_data), config);
206
+ index.add(label, reinterpret_cast<double const*>(vector_data), config).error.raise();
198
207
  else
199
208
  throw std::invalid_argument("Incompatible scalars in the vector!");
200
209
  }
201
210
 
202
- static void add_many_to_index( //
203
- punned_py_t& index, py::buffer labels, py::buffer vectors, //
211
+ static void add_many_to_index( //
212
+ punned_index_py_t& index, py::buffer labels, py::buffer vectors, //
204
213
  bool copy, std::size_t threads) {
205
214
 
206
- if (index.config().max_threads_add < threads)
215
+ if (index.limits().threads_add < threads)
207
216
  throw std::invalid_argument("Can't use that many threads!");
208
217
 
209
218
  py::buffer_info labels_info = labels.request();
@@ -242,7 +251,7 @@ static void add_many_to_index( //
242
251
  label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
243
252
  f8_bits_t const* vector =
244
253
  reinterpret_cast<f8_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
245
- index.add(label, vector, config);
254
+ index.add(label, vector, config).error.raise();
246
255
  });
247
256
  else if (vectors_info.format == "e")
248
257
  multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
@@ -252,7 +261,7 @@ static void add_many_to_index( //
252
261
  label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
253
262
  f16_bits_t const* vector =
254
263
  reinterpret_cast<f16_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
255
- index.add(label, vector, config);
264
+ index.add(label, vector, config).error.raise();
256
265
  });
257
266
  else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
258
267
  multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
@@ -261,7 +270,7 @@ static void add_many_to_index( //
261
270
  config.thread = thread_idx;
262
271
  label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
263
272
  float const* vector = reinterpret_cast<float const*>(vectors_data + task_idx * vectors_info.strides[0]);
264
- index.add(label, vector, config);
273
+ index.add(label, vector, config).error.raise();
265
274
  });
266
275
  else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
267
276
  multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
@@ -270,13 +279,64 @@ static void add_many_to_index( //
270
279
  config.thread = thread_idx;
271
280
  label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
272
281
  double const* vector = reinterpret_cast<double const*>(vectors_data + task_idx * vectors_info.strides[0]);
273
- index.add(label, vector, config);
282
+ index.add(label, vector, config).error.raise();
274
283
  });
275
284
  else
276
285
  throw std::invalid_argument("Incompatible scalars in the vectors matrix!");
277
286
  }
278
287
 
279
- static py::tuple search_one_in_index(punned_py_t& index, py::buffer vector, std::size_t wanted, bool exact) {
288
+ static void add_many_to_bits_index( //
289
+ bits_index_py_t& index, py::buffer labels, py::buffer vectors, //
290
+ bool copy, std::size_t threads) {
291
+
292
+ if (index.limits().threads_add < threads)
293
+ throw std::invalid_argument("Can't use that many threads!");
294
+
295
+ py::buffer_info labels_info = labels.request();
296
+ py::buffer_info vectors_info = vectors.request();
297
+
298
+ if (labels_info.format != py::format_descriptor<label_t>::format())
299
+ throw std::invalid_argument("Incompatible label type!");
300
+
301
+ if (labels_info.ndim != 1)
302
+ throw std::invalid_argument("Labels must be placed in a single-dimensional array!");
303
+
304
+ if (vectors_info.ndim != 2)
305
+ throw std::invalid_argument("Expects a matrix of vectors to add!");
306
+
307
+ Py_ssize_t labels_count = labels_info.shape[0];
308
+ Py_ssize_t vectors_count = vectors_info.shape[0];
309
+ Py_ssize_t vectors_dimensions = vectors_info.shape[1];
310
+ if (vectors_dimensions != static_cast<Py_ssize_t>(index.numpy_words_))
311
+ throw std::invalid_argument("The number of vector dimensions doesn't match!");
312
+
313
+ if (labels_count != vectors_count)
314
+ throw std::invalid_argument("Number of labels and vectors must match!");
315
+
316
+ if (index.size() + vectors_count >= index.capacity())
317
+ index.reserve(ceil2(index.size() + vectors_count));
318
+
319
+ char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
320
+ char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
321
+
322
+ // https://docs.python.org/3/library/struct.html#format-characters
323
+ if (vectors_info.format != "B")
324
+ throw std::invalid_argument("Incompatible scalars in the vectors matrix!");
325
+
326
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
327
+ add_config_t config;
328
+ config.store_vector = copy;
329
+ config.thread = thread_idx;
330
+ label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
331
+ bits_numpy_word_t const* vector =
332
+ reinterpret_cast<bits_numpy_word_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
333
+ bits_native_word_t* vector_words = index.word_aligned_buffer_.data() + index.native_words_ * thread_idx;
334
+ std::memcpy(vector_words, vector, vectors_dimensions);
335
+ index.add(label, {vector_words, index.native_words_}, config).error.raise();
336
+ });
337
+ }
338
+
339
+ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector, std::size_t wanted, bool exact) {
280
340
 
281
341
  py::buffer_info vector_info = vector.request();
282
342
  Py_ssize_t vector_dimensions = vector_info.shape[0];
@@ -294,23 +354,23 @@ static py::tuple search_one_in_index(punned_py_t& index, py::buffer vector, std:
294
354
  config.exact = exact;
295
355
 
296
356
  // https://docs.python.org/3/library/struct.html#format-characters
297
- if (vector_info.format == "c" || vector_info.format == "b")
298
- count = index //
299
- .search(reinterpret_cast<f8_bits_t const*>(vector_data), wanted, config)
300
- .dump_to(&labels_py1d(0), &distances_py1d(0));
301
- else if (vector_info.format == "e")
302
- count = index //
303
- .search(reinterpret_cast<f16_bits_t const*>(vector_data), wanted, config)
304
- .dump_to(&labels_py1d(0), &distances_py1d(0));
305
- else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
306
- count = index //
307
- .search(reinterpret_cast<float const*>(vector_data), wanted, config)
308
- .dump_to(&labels_py1d(0), &distances_py1d(0));
309
- else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
310
- count = index //
311
- .search(reinterpret_cast<double const*>(vector_data), wanted, config)
312
- .dump_to(&labels_py1d(0), &distances_py1d(0));
313
- else
357
+ if (vector_info.format == "c" || vector_info.format == "b") {
358
+ punned_search_result_t result = index.search(reinterpret_cast<f8_bits_t const*>(vector_data), wanted, config);
359
+ result.error.raise();
360
+ count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
361
+ } else if (vector_info.format == "e") {
362
+ punned_search_result_t result = index.search(reinterpret_cast<f16_bits_t const*>(vector_data), wanted, config);
363
+ result.error.raise();
364
+ count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
365
+ } else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4") {
366
+ punned_search_result_t result = index.search(reinterpret_cast<float const*>(vector_data), wanted, config);
367
+ result.error.raise();
368
+ count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
369
+ } else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8") {
370
+ punned_search_result_t result = index.search(reinterpret_cast<double const*>(vector_data), wanted, config);
371
+ result.error.raise();
372
+ count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
373
+ } else
314
374
  throw std::invalid_argument("Incompatible scalars in the query vector!");
315
375
 
316
376
  labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
@@ -333,12 +393,12 @@ static py::tuple search_one_in_index(punned_py_t& index, py::buffer vector, std:
333
393
  * 3. array with match counts.
334
394
  */
335
395
  static py::tuple search_many_in_index( //
336
- punned_py_t& index, py::buffer vectors, std::size_t wanted, bool exact, std::size_t threads) {
396
+ punned_index_py_t& index, py::buffer vectors, std::size_t wanted, bool exact, std::size_t threads) {
337
397
 
338
398
  if (wanted == 0)
339
399
  return py::tuple(3);
340
400
 
341
- if (index.config().max_threads_add < threads)
401
+ if (index.limits().threads_add < threads)
342
402
  throw std::invalid_argument("Can't use that many threads!");
343
403
 
344
404
  py::buffer_info vectors_info = vectors.request();
@@ -367,8 +427,10 @@ static py::tuple search_many_in_index( //
367
427
  config.thread = thread_idx;
368
428
  config.exact = exact;
369
429
  f8_bits_t const* vector = (f8_bits_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
370
- counts_py1d(task_idx) = static_cast<Py_ssize_t>(
371
- index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
430
+ punned_search_result_t result = index.search(vector, wanted, config);
431
+ result.error.raise();
432
+ counts_py1d(task_idx) =
433
+ static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
372
434
  });
373
435
  else if (vectors_info.format == "e")
374
436
  multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
@@ -376,8 +438,10 @@ static py::tuple search_many_in_index( //
376
438
  config.thread = thread_idx;
377
439
  config.exact = exact;
378
440
  f16_bits_t const* vector = (f16_bits_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
379
- counts_py1d(task_idx) = static_cast<Py_ssize_t>(
380
- index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
441
+ punned_search_result_t result = index.search(vector, wanted, config);
442
+ result.error.raise();
443
+ counts_py1d(task_idx) =
444
+ static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
381
445
  });
382
446
  else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
383
447
  multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
@@ -385,8 +449,10 @@ static py::tuple search_many_in_index( //
385
449
  config.thread = thread_idx;
386
450
  config.exact = exact;
387
451
  float const* vector = (float const*)(vectors_data + task_idx * vectors_info.strides[0]);
388
- counts_py1d(task_idx) = static_cast<Py_ssize_t>(
389
- index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
452
+ punned_search_result_t result = index.search(vector, wanted, config);
453
+ result.error.raise();
454
+ counts_py1d(task_idx) =
455
+ static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
390
456
  });
391
457
  else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
392
458
  multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
@@ -394,8 +460,10 @@ static py::tuple search_many_in_index( //
394
460
  config.thread = thread_idx;
395
461
  config.exact = exact;
396
462
  double const* vector = (double const*)(vectors_data + task_idx * vectors_info.strides[0]);
397
- counts_py1d(task_idx) = static_cast<Py_ssize_t>(
398
- index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
463
+ punned_search_result_t result = index.search(vector, wanted, config);
464
+ result.error.raise();
465
+ counts_py1d(task_idx) =
466
+ static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
399
467
  });
400
468
  else
401
469
  throw std::invalid_argument("Incompatible scalars in the query matrix!");
@@ -407,10 +475,74 @@ static py::tuple search_many_in_index( //
407
475
  return results;
408
476
  }
409
477
 
478
+ /**
479
+ * @param vectors Matrix of vectors to search for.
480
+ * @param wanted Number of matches per request.
481
+ *
482
+ * @return Tuple with:
483
+ * 1. matrix of neighbors,
484
+ * 2. matrix of distances,
485
+ * 3. array with match counts.
486
+ */
487
+ static py::tuple search_many_in_bits_index( //
488
+ bits_index_py_t& index, py::buffer vectors, std::size_t wanted, bool exact, std::size_t threads) {
489
+
490
+ if (wanted == 0)
491
+ return py::tuple(3);
492
+
493
+ if (index.limits().threads_add < threads)
494
+ throw std::invalid_argument("Can't use that many threads!");
495
+
496
+ py::buffer_info vectors_info = vectors.request();
497
+ // if (vectors_info.ndim == 1)
498
+ // return search_one_in_index(index, vectors, wanted, exact);
499
+ if (vectors_info.ndim != 2)
500
+ throw std::invalid_argument("Expects a matrix of vectors to add!");
501
+
502
+ Py_ssize_t vectors_count = vectors_info.shape[0];
503
+ Py_ssize_t vectors_dimensions = vectors_info.shape[1];
504
+ char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
505
+ if (vectors_dimensions != static_cast<Py_ssize_t>(index.numpy_words_))
506
+ throw std::invalid_argument("The number of vector dimensions doesn't match!");
507
+
508
+ py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
509
+ py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
510
+ py::array_t<Py_ssize_t> counts_py(vectors_count);
511
+ auto labels_py2d = labels_py.mutable_unchecked<2>();
512
+ auto distances_py2d = distances_py.mutable_unchecked<2>();
513
+ auto counts_py1d = counts_py.mutable_unchecked<1>();
514
+
515
+ // https://docs.python.org/3/library/struct.html#format-characters
516
+ if (vectors_info.format != "B")
517
+ throw std::invalid_argument("Incompatible scalars in the query matrix!");
518
+
519
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
520
+ search_config_t config;
521
+ config.thread = thread_idx;
522
+ config.exact = exact;
523
+
524
+ bits_numpy_word_t const* vector =
525
+ reinterpret_cast<bits_numpy_word_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
526
+ bits_native_word_t* vector_words = index.word_aligned_buffer_.data() + index.native_words_ * thread_idx;
527
+ std::memcpy(vector_words, vector, vectors_dimensions);
528
+
529
+ bits_search_result_t result = index.search({vector_words, index.native_words_}, wanted, config);
530
+ result.error.raise();
531
+ counts_py1d(task_idx) =
532
+ static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
533
+ });
534
+
535
+ py::tuple results(3);
536
+ results[0] = labels_py;
537
+ results[1] = distances_py;
538
+ results[2] = counts_py;
539
+ return results;
540
+ }
541
+
410
542
  // clang-format off
411
- template <typename index_at> void save_index(index_at const& index, std::string const& path) { index.save(path.c_str()); }
412
- template <typename index_at> void load_index(index_at& index, std::string const& path) { index.load(path.c_str()); }
413
- template <typename index_at> void view_index(index_at& index, std::string const& path) { index.view(path.c_str()); }
543
+ template <typename index_at> void save_index(index_at const& index, std::string const& path) { index.save(path.c_str()).error.raise(); }
544
+ template <typename index_at> void load_index(index_at& index, std::string const& path) { index.load(path.c_str()).error.raise(); }
545
+ template <typename index_at> void view_index(index_at& index, std::string const& path) { index.view(path.c_str()).error.raise(); }
414
546
  template <typename index_at> void clear_index(index_at& index) { index.clear(); }
415
547
  template <typename index_at> std::size_t get_expansion_add(index_at const &index) { return index.config().expansion_add; }
416
548
  template <typename index_at> std::size_t get_expansion_search(index_at const &index) { return index.config().expansion_search; }
@@ -442,9 +574,9 @@ void validate_set(py::array_t<set_member_t> const& set) {
442
574
  throw std::invalid_argument("Set must be deduplicated!");
443
575
  }
444
576
 
445
- inline std::uint64_t hash_ror64(std::uint64_t v, int r) noexcept { return (v >> r) | (v << (64 - r)); }
577
+ inline bits_native_word_t hash_ror64(bits_native_word_t v, int r) noexcept { return (v >> r) | (v << (64 - r)); }
446
578
 
447
- inline std::uint64_t hash(std::uint64_t v) noexcept {
579
+ inline bits_native_word_t hash(bits_native_word_t v) noexcept {
448
580
  v ^= hash_ror64(v, 25) ^ hash_ror64(v, 50);
449
581
  v *= 0xA24BAED4963EE407UL;
450
582
  v ^= hash_ror64(v, 24) ^ hash_ror64(v, 49);
@@ -453,34 +585,71 @@ inline std::uint64_t hash(std::uint64_t v) noexcept {
453
585
  }
454
586
 
455
587
  template <typename scalar_at>
456
- inline void hash_typed_buffer(hash_index_py_t& index, py::buffer_info const& vector_info) noexcept {
457
- char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
458
- Py_ssize_t vector_dimensions = vector_info.shape[0];
459
- Py_ssize_t vector_stride = vector_info.strides[0];
460
- std::memset(index.buffer_.data(), 0, index.words_ * sizeof(hash_word_t));
588
+ void hash_typed_row( //
589
+ byte_t const* scalars, std::size_t scalar_stride, std::size_t scalars_count, //
590
+ bits_numpy_word_t* hashes, std::size_t bits) noexcept {
591
+
592
+ std::size_t words = divide_round_up<8>(bits);
593
+ for (std::size_t i = 0; i != scalars_count; ++i) {
594
+ scalar_at scalar;
595
+ std::memcpy(&scalar, scalars + i * scalar_stride, sizeof(scalar_at));
596
+ bits_native_word_t scalar_hash = hash(scalar);
597
+ hashes[scalar_hash & (words - 1)] |= std::uint8_t(1) << (scalar_hash % 8);
598
+ }
599
+ }
461
600
 
462
- for (std::size_t i = 0; i != static_cast<std::size_t>(vector_dimensions); ++i) {
463
- scalar_at scalar = *reinterpret_cast<scalar_at const*>(vector_data + i * vector_stride);
464
- std::uint64_t scalar_hash = hash(scalar);
465
- index.buffer_[scalar_hash % index.words_] |= hash_word_t(1) << (scalar_hash % bits_per_hash_word_k);
601
+ #if 0
602
+ template <typename scalar_at>
603
+ py::array_t<bits_numpy_word_t> hash_typed_buffer(py::buffer_info const& info, std::size_t bits) {
604
+ byte_t const* data = reinterpret_cast<byte_t const*>(info.ptr);
605
+ Py_ssize_t elements_per_row = info.shape[0];
606
+ Py_ssize_t elements_stride = info.strides[0];
607
+ Py_ssize_t words_per_row = divide_round_up<std::size_t>(bits, 8);
608
+
609
+ if (info.ndim == 2) {
610
+ Py_ssize_t rows_count = info.shape[1];
611
+ Py_ssize_t rows_stride = info.strides[1];
612
+ auto hashes_shape = py_shape_t{rows_count, words_per_row};
613
+ auto hashes_py = py::array_t<bits_numpy_word_t>(hashes_shape);
614
+ auto hashes_proxy = hashes_py.mutable_unchecked<2>();
615
+
616
+ for (Py_ssize_t i = 0; i != rows_count; ++i)
617
+ hash_typed_row( //
618
+ data + i * rows_stride, hashes_proxy.stride(1), static_cast<std::size_t>(elements_per_row), //
619
+ &hashes_proxy(i, 0), bits);
620
+
621
+ return hashes_py;
622
+ } else {
623
+ auto hashes_shape = py_shape_t{words_per_row};
624
+ auto hashes_py = py::array_t<bits_numpy_word_t>(hashes_shape);
625
+ auto hashes_proxy = hashes_py.mutable_unchecked<1>();
626
+
627
+ hash_typed_row( //
628
+ data, hashes_info.strides[0], static_cast<std::size_t>(elements_per_row), //
629
+ &hashes_proxy(0), bits);
630
+
631
+ return hashes_py;
466
632
  }
467
633
  }
468
634
 
469
- void hash_buffer(hash_index_py_t& index, py::buffer vector) {
635
+ py::array_t<bits_numpy_word_t> hash_buffer(py::buffer vector, std::size_t bits) {
636
+ if (std::bitset<64>(bits).count() == 1)
637
+ throw std::invalid_argument("Number of bits must be a power of two!");
470
638
  py::buffer_info info = vector.request();
471
- if (info.ndim != 1)
472
- throw std::invalid_argument("Array can't be multi-dimensional!");
639
+ if (info.ndim > 2)
640
+ throw std::invalid_argument("Only one or two-dimensional arrays are supported!");
473
641
 
474
642
  // https://docs.python.org/3/library/struct.html#format-characters
475
643
  if (info.format == "h" || info.format == "H")
476
- return hash_typed_buffer<std::uint16_t>(index, info);
644
+ return hash_typed_buffer<std::uint16_t>(info, bits);
477
645
  else if (info.format == "i" || info.format == "I" || info.format == "l" || info.format == "L")
478
- return hash_typed_buffer<std::uint32_t>(index, info);
646
+ return hash_typed_buffer<std::uint32_t>(info, bits);
479
647
  else if (info.format == "q" || info.format == "Q" || info.format == "n" || info.format == "N")
480
- return hash_typed_buffer<std::uint64_t>(index, info);
648
+ return hash_typed_buffer<std::uint64_t>(info, bits);
481
649
  else
482
650
  throw std::invalid_argument("Array elements must be 16, 32, or 64 bit hashable integers!");
483
651
  }
652
+ #endif
484
653
 
485
654
  PYBIND11_MODULE(compiled, m) {
486
655
  m.doc() = "Smaller & Faster Single-File Vector Search Engine from Unum";
@@ -489,14 +658,25 @@ PYBIND11_MODULE(compiled, m) {
489
658
  m.attr("DEFAULT_EXPANSION_ADD") = py::int_(default_expansion_add());
490
659
  m.attr("DEFAULT_EXPANSION_SEARCH") = py::int_(default_expansion_search());
491
660
 
492
- auto i = py::class_<punned_py_t>(m, "Index");
661
+ py::enum_<common_metric_kind_t>(m, "MetricKind")
662
+ .value("Unknown", common_metric_kind_t::unknown_k)
663
+ .value("IP", common_metric_kind_t::ip_k)
664
+ .value("Cos", common_metric_kind_t::cos_k)
665
+ .value("L2sq", common_metric_kind_t::l2sq_k)
666
+ .value("Haversine", common_metric_kind_t::haversine_k)
667
+ .value("Pearson", common_metric_kind_t::pearson_k)
668
+ .value("Jaccard", common_metric_kind_t::jaccard_k)
669
+ .value("BitwiseHamming", common_metric_kind_t::bitwise_hamming_k)
670
+ .value("BitwiseTanimoto", common_metric_kind_t::bitwise_tanimoto_k)
671
+ .value("BitwiseSorensen", common_metric_kind_t::bitwise_sorensen_k);
672
+
673
+ auto i = py::class_<punned_index_py_t>(m, "Index");
493
674
 
494
675
  i.def(py::init(&make_index), //
495
676
  py::kw_only(), //
496
677
  py::arg("ndim") = 0, //
497
- py::arg("capacity") = 0, //
498
678
  py::arg("dtype") = std::string("f32"), //
499
- py::arg("metric") = std::string("ip"), //
679
+ py::arg("metric") = common_metric_kind_t::ip_k, //
500
680
  py::arg("connectivity") = default_connectivity(), //
501
681
  py::arg("expansion_add") = default_expansion_add(), //
502
682
  py::arg("expansion_search") = default_expansion_search(), //
@@ -530,29 +710,69 @@ PYBIND11_MODULE(compiled, m) {
530
710
  py::arg("threads") = 0 //
531
711
  );
532
712
 
533
- i.def("__len__", &punned_py_t::size);
534
- i.def_property_readonly("size", &punned_py_t::size);
535
- i.def_property_readonly("ndim", &punned_py_t::dimensions);
536
- i.def_property_readonly("connectivity", &punned_py_t::connectivity);
537
- i.def_property_readonly("capacity", &punned_py_t::capacity);
713
+ i.def("__len__", &punned_index_py_t::size);
714
+ i.def_property_readonly("size", &punned_index_py_t::size);
715
+ i.def_property_readonly("ndim", &punned_index_py_t::dimensions);
716
+ i.def_property_readonly("connectivity", &punned_index_py_t::connectivity);
717
+ i.def_property_readonly("capacity", &punned_index_py_t::capacity);
538
718
  i.def_property_readonly( //
539
- "dtype", [](punned_py_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
540
- i.def_property_readonly("memory_usage", &punned_py_t::memory_usage);
719
+ "dtype", [](punned_index_py_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
720
+ i.def_property_readonly("memory_usage", &punned_index_py_t::memory_usage);
721
+
722
+ i.def_property("expansion_add", &get_expansion_add<punned_index_py_t>, &punned_index_py_t::change_expansion_add);
723
+ i.def_property("expansion_search", &get_expansion_search<punned_index_py_t>,
724
+ &punned_index_py_t::change_expansion_search);
725
+
726
+ i.def("save", &save_index<punned_index_py_t>, py::arg("path"));
727
+ i.def("load", &load_index<punned_index_py_t>, py::arg("path"));
728
+ i.def("view", &view_index<punned_index_py_t>, py::arg("path"));
729
+ i.def("clear", &clear_index<punned_index_py_t>);
730
+
731
+ auto bi = py::class_<bits_index_py_t>(m, "BitsIndex");
732
+
733
+ bi.def( //
734
+ py::init(&make_hash_index), //
735
+ py::kw_only(), //
736
+ py::arg("bits"), //
737
+ py::arg("metric") = common_metric_kind_t::bitwise_hamming_k, //
738
+ py::arg("connectivity") = default_connectivity(), //
739
+ py::arg("expansion_add") = default_expansion_add(), //
740
+ py::arg("expansion_search") = default_expansion_search() //
741
+ );
742
+
743
+ bi.def( //
744
+ "add",
745
+ &add_many_to_bits_index, //
746
+ py::arg("label"), //
747
+ py::arg("array"), //
748
+ py::kw_only(), //
749
+ py::arg("copy") = true, //
750
+ py::arg("threads") = 0 //
751
+ );
541
752
 
542
- i.def_property("expansion_add", &get_expansion_add<punned_py_t>, &punned_py_t::change_expansion_add);
543
- i.def_property("expansion_search", &get_expansion_search<punned_py_t>, &punned_py_t::change_expansion_search);
753
+ bi.def( //
754
+ "search", &search_many_in_bits_index, //
755
+ py::arg("query"), //
756
+ py::arg("count") = 10, //
757
+ py::arg("exact") = false, //
758
+ py::arg("threads") = 0 //
759
+ );
760
+
761
+ bi.def("__len__", &bits_index_py_t::size);
762
+ bi.def_property_readonly("size", &bits_index_py_t::size);
763
+ bi.def_property_readonly("connectivity", &bits_index_py_t::connectivity);
764
+ bi.def_property_readonly("capacity", &bits_index_py_t::capacity);
544
765
 
545
- i.def("save", &save_index<punned_py_t>, py::arg("path"));
546
- i.def("load", &load_index<punned_py_t>, py::arg("path"));
547
- i.def("view", &view_index<punned_py_t>, py::arg("path"));
548
- i.def("clear", &clear_index<punned_py_t>);
766
+ bi.def("save", &save_index<bits_index_py_t>, py::arg("path"));
767
+ bi.def("load", &load_index<bits_index_py_t>, py::arg("path"));
768
+ bi.def("view", &view_index<bits_index_py_t>, py::arg("path"));
769
+ bi.def("clear", &clear_index<bits_index_py_t>);
549
770
 
550
771
  auto si = py::class_<sets_index_py_t>(m, "SetsIndex");
551
772
 
552
773
  si.def( //
553
774
  py::init(&make_sets_index), //
554
775
  py::kw_only(), //
555
- py::arg("capacity") = 0, //
556
776
  py::arg("connectivity") = default_connectivity(), //
557
777
  py::arg("expansion_add") = default_expansion_add(), //
558
778
  py::arg("expansion_search") = default_expansion_search() //
@@ -565,10 +785,11 @@ PYBIND11_MODULE(compiled, m) {
565
785
  if (index.size() + 1 >= index.capacity())
566
786
  index.reserve(ceil2(index.size() + 1));
567
787
  auto proxy = set.unchecked<1>();
568
- auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
788
+ auto view = set_view_t{&proxy(0), static_cast<std::size_t>(proxy.shape(0))};
789
+
569
790
  add_config_t config;
570
791
  config.store_vector = copy;
571
- index.add(label, view, config);
792
+ index.add(label, view, config).error.raise();
572
793
  }, //
573
794
  py::arg("label"), //
574
795
  py::arg("set"), //
@@ -581,10 +802,12 @@ PYBIND11_MODULE(compiled, m) {
581
802
  [](sets_index_py_t& index, py::array_t<set_member_t> set, std::size_t count) -> py::array_t<label_t> {
582
803
  validate_set(set);
583
804
  auto proxy = set.unchecked<1>();
584
- auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
805
+ auto view = set_view_t{&proxy(0), static_cast<std::size_t>(proxy.shape(0))};
585
806
  auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
586
807
  auto labels_proxy = labels_py.mutable_unchecked<1>();
587
- auto found = index.search(view, count).dump_to(&labels_proxy(0), nullptr);
808
+ auto result = index.search(view, count);
809
+ result.error.raise();
810
+ auto found = result.dump_to(&labels_proxy(0));
588
811
  labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
589
812
  return labels_py;
590
813
  },
@@ -601,54 +824,4 @@ PYBIND11_MODULE(compiled, m) {
601
824
  si.def("load", &load_index<sets_index_py_t>, py::arg("path"));
602
825
  si.def("view", &view_index<sets_index_py_t>, py::arg("path"));
603
826
  si.def("clear", &clear_index<sets_index_py_t>);
604
-
605
- auto hi = py::class_<hash_index_py_t>(m, "HashIndex");
606
-
607
- hi.def( //
608
- py::init(&make_hash_index), //
609
- py::kw_only(), //
610
- py::arg("bits"), //
611
- py::arg("capacity") = 0, //
612
- py::arg("connectivity") = default_connectivity(), //
613
- py::arg("expansion_add") = default_expansion_add(), //
614
- py::arg("expansion_search") = default_expansion_search() //
615
- );
616
-
617
- hi.def( //
618
- "add",
619
- [](hash_index_py_t& index, label_t label, py::buffer array) {
620
- if (index.size() + 1 >= index.capacity())
621
- index.reserve(ceil2(index.size() + 1));
622
- hash_buffer(index, array);
623
- index.add(label, index.buffer());
624
- }, //
625
- py::arg("label"), //
626
- py::arg("array") //
627
- );
628
-
629
- hi.def( //
630
- "search",
631
- [](hash_index_py_t& index, py::buffer array, std::size_t count) -> py::array_t<label_t> {
632
- if (index.size() + 1 >= index.capacity())
633
- index.reserve(ceil2(index.size() + 1));
634
- hash_buffer(index, array);
635
- auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
636
- auto labels_proxy = labels_py.mutable_unchecked<1>();
637
- auto found = index.search(index.buffer(), count).dump_to(&labels_proxy(0), nullptr);
638
- labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
639
- return labels_py;
640
- },
641
- py::arg("array"), //
642
- py::arg("count") = 10 //
643
- );
644
-
645
- hi.def("__len__", &hash_index_py_t::size);
646
- hi.def_property_readonly("size", &hash_index_py_t::size);
647
- hi.def_property_readonly("connectivity", &hash_index_py_t::connectivity);
648
- hi.def_property_readonly("capacity", &hash_index_py_t::capacity);
649
-
650
- hi.def("save", &save_index<hash_index_py_t>, py::arg("path"));
651
- hi.def("load", &load_index<hash_index_py_t>, py::arg("path"));
652
- hi.def("view", &view_index<hash_index_py_t>, py::arg("path"));
653
- hi.def("clear", &clear_index<hash_index_py_t>);
654
827
  }