usearch 0.2.3 → 0.4.0

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.
@@ -15,6 +15,8 @@
15
15
  #define NAPI_CPP_EXCEPTIONS
16
16
  #endif
17
17
 
18
+ #include <new> // `std::bad_alloc`
19
+
18
20
  #include <napi.h>
19
21
  #include <node_api.h>
20
22
 
@@ -38,6 +40,10 @@ class Index : public Napi::ObjectWrap<Index> {
38
40
  Napi::Value GetCapacity(Napi::CallbackInfo const& ctx);
39
41
  Napi::Value GetConnectivity(Napi::CallbackInfo const& ctx);
40
42
 
43
+ void Save(Napi::CallbackInfo const& ctx);
44
+ void Load(Napi::CallbackInfo const& ctx);
45
+ void View(Napi::CallbackInfo const& ctx);
46
+
41
47
  void Add(Napi::CallbackInfo const& ctx);
42
48
  Napi::Value Search(Napi::CallbackInfo const& ctx);
43
49
 
@@ -54,6 +60,9 @@ Napi::Object Index::Init(Napi::Env env, Napi::Object exports) {
54
60
  InstanceMethod("connectivity", &Index::GetConnectivity),
55
61
  InstanceMethod("add", &Index::Add),
56
62
  InstanceMethod("search", &Index::Search),
63
+ InstanceMethod("save", &Index::Save),
64
+ InstanceMethod("load", &Index::Load),
65
+ InstanceMethod("view", &Index::View),
57
66
  });
58
67
 
59
68
  Napi::FunctionReference* constructor = new Napi::FunctionReference();
@@ -150,6 +159,57 @@ Napi::Value Index::GetCapacity(Napi::CallbackInfo const& ctx) {
150
159
  return Napi::Number::New(ctx.Env(), native_->capacity());
151
160
  }
152
161
 
162
+ void Index::Save(Napi::CallbackInfo const& ctx) {
163
+ Napi::Env env = ctx.Env();
164
+
165
+ int length = ctx.Length();
166
+ if (length == 0 || !ctx[0].IsString()) {
167
+ Napi::TypeError::New(env, "Function expects a string path argument").ThrowAsJavaScriptException();
168
+ return;
169
+ }
170
+
171
+ try {
172
+ std::string path = ctx[0].As<Napi::String>();
173
+ native_->save(path.c_str());
174
+ } catch (...) {
175
+ Napi::TypeError::New(env, "Serialization failed").ThrowAsJavaScriptException();
176
+ }
177
+ }
178
+
179
+ void Index::Load(Napi::CallbackInfo const& ctx) {
180
+ Napi::Env env = ctx.Env();
181
+
182
+ int length = ctx.Length();
183
+ if (length == 0 || !ctx[0].IsString()) {
184
+ Napi::TypeError::New(env, "Function expects a string path argument").ThrowAsJavaScriptException();
185
+ return;
186
+ }
187
+
188
+ try {
189
+ std::string path = ctx[0].As<Napi::String>();
190
+ native_->load(path.c_str());
191
+ } catch (...) {
192
+ Napi::TypeError::New(env, "Loading failed").ThrowAsJavaScriptException();
193
+ }
194
+ }
195
+
196
+ void Index::View(Napi::CallbackInfo const& ctx) {
197
+ Napi::Env env = ctx.Env();
198
+
199
+ int length = ctx.Length();
200
+ if (length == 0 || !ctx[0].IsString()) {
201
+ Napi::TypeError::New(env, "Function expects a string path argument").ThrowAsJavaScriptException();
202
+ return;
203
+ }
204
+
205
+ try {
206
+ std::string path = ctx[0].As<Napi::String>();
207
+ native_->view(path.c_str());
208
+ } catch (...) {
209
+ Napi::TypeError::New(env, "Memory-mapping failed").ThrowAsJavaScriptException();
210
+ }
211
+ }
212
+
153
213
  void Index::Add(Napi::CallbackInfo const& ctx) {
154
214
  Napi::Env env = ctx.Env();
155
215
  if (ctx.Length() < 2 || !ctx[0].IsNumber() || !ctx[1].IsTypedArray())
@@ -166,7 +226,14 @@ void Index::Add(Napi::CallbackInfo const& ctx) {
166
226
 
167
227
  if (native_->size() + 1 >= native_->capacity())
168
228
  native_->reserve(ceil2(native_->size() + 1));
169
- native_->add(label, vector);
229
+
230
+ try {
231
+ native_->add(label, vector);
232
+ } catch (std::bad_alloc const&) {
233
+ Napi::TypeError::New(env, "Out of memory").ThrowAsJavaScriptException();
234
+ } catch (...) {
235
+ Napi::TypeError::New(env, "Insertion failed").ThrowAsJavaScriptException();
236
+ }
170
237
  }
171
238
 
172
239
  Napi::Value Index::Search(Napi::CallbackInfo const& ctx) {
@@ -190,13 +257,20 @@ Napi::Value Index::Search(Napi::CallbackInfo const& ctx) {
190
257
 
191
258
  Napi::Uint32Array matches_js = Napi::Uint32Array::New(env, wanted);
192
259
  Napi::Float32Array distances_js = Napi::Float32Array::New(env, wanted);
193
- std::size_t count = native_->search(vector, wanted, matches_js.Data(), distances_js.Data());
194
-
195
- Napi::Object result_js = Napi::Object::New(env);
196
- result_js.Set("labels", matches_js);
197
- result_js.Set("distances", distances_js);
198
- result_js.Set("count", Napi::Number::New(env, count));
199
- return result_js;
260
+ try {
261
+ std::size_t count = native_->search(vector, wanted, matches_js.Data(), distances_js.Data());
262
+ Napi::Object result_js = Napi::Object::New(env);
263
+ result_js.Set("labels", matches_js);
264
+ result_js.Set("distances", distances_js);
265
+ result_js.Set("count", Napi::Number::New(env, count));
266
+ return result_js;
267
+ } catch (std::bad_alloc const&) {
268
+ Napi::TypeError::New(env, "Out of memory").ThrowAsJavaScriptException();
269
+ return {};
270
+ } catch (...) {
271
+ Napi::TypeError::New(env, "Search failed").ThrowAsJavaScriptException();
272
+ return {};
273
+ }
200
274
  }
201
275
 
202
276
  Napi::Object InitAll(Napi::Env env, Napi::Object exports) { return Index::Init(env, exports); }
@@ -107,14 +107,14 @@ using shared_index_t = std::shared_ptr<native_index_t>;
107
107
 
108
108
  - (void)addImprecise:(UInt32)label
109
109
  vector:(void const * _Nonnull)vector {
110
- _native->add(label, (f16_converted_t const *)vector);
110
+ _native->add(label, (f16_bits_t const *)vector);
111
111
  }
112
112
 
113
113
  - (UInt32)searchImprecise:(void const* _Nonnull)vector
114
114
  count:(UInt32)wanted
115
115
  labels:(UInt32* _Nullable)labels
116
116
  distances:(Float32* _Nullable)distances {
117
- std::size_t found = _native->search((f16_converted_t const *)vector, static_cast<std::size_t>(wanted), labels, distances);
117
+ std::size_t found = _native->search((f16_bits_t const *)vector, static_cast<std::size_t>(wanted), labels, distances);
118
118
  return static_cast<UInt32>(found);
119
119
  }
120
120
 
@@ -10,6 +10,7 @@ typedef NS_ENUM(NSUInteger, Quantization) {
10
10
  kDoublePrecision
11
11
  };
12
12
 
13
+ API_AVAILABLE(ios(13.0), macos(10.15), tvos(13.0), watchos(6.0))
13
14
  @interface Index: NSObject
14
15
 
15
16
  @property (readonly) UInt32 dimensions;
@@ -33,9 +34,9 @@ NS_UNAVAILABLE;
33
34
  NS_SWIFT_NAME(addSingle(label:vector:));
34
35
 
35
36
  - (UInt32)searchSingle:(Float32 const* _Nonnull)vector
36
- count:(UInt32)count
37
- labels:(UInt32* _Nullable)labels
38
- distances:(Float32* _Nullable)distances
37
+ count:(UInt32)count
38
+ labels:(UInt32* _Nullable)labels
39
+ distances:(Float32* _Nullable)distances
39
40
  NS_SWIFT_NAME(searchSingle(vector:count:labels:distances:));
40
41
 
41
42
  - (void)addPrecise:(UInt32)label
@@ -43,9 +44,9 @@ NS_SWIFT_NAME(searchSingle(vector:count:labels:distances:));
43
44
  NS_SWIFT_NAME(addPrecise(label:vector:));
44
45
 
45
46
  - (UInt32)searchPrecise:(Float64 const* _Nonnull)vector
46
- count:(UInt32)count
47
- labels:(UInt32* _Nullable)labels
48
- distances:(Float32* _Nullable)distances
47
+ count:(UInt32)count
48
+ labels:(UInt32* _Nullable)labels
49
+ distances:(Float32* _Nullable)distances
49
50
  NS_SWIFT_NAME(searchPrecise(vector:count:labels:distances:));
50
51
 
51
52
  - (void)addImprecise:(UInt32)label
@@ -53,9 +54,9 @@ NS_SWIFT_NAME(searchPrecise(vector:count:labels:distances:));
53
54
  NS_SWIFT_NAME(addImprecise(label:vector:));
54
55
 
55
56
  - (UInt32)searchImprecise:(void const* _Nonnull)vector
56
- count:(UInt32)count
57
- labels:(UInt32* _Nullable)labels
58
- distances:(Float32* _Nullable)distances
57
+ count:(UInt32)count
58
+ labels:(UInt32* _Nullable)labels
59
+ distances:(Float32* _Nullable)distances
59
60
  NS_SWIFT_NAME(searchImprecise(vector:count:labels:distances:));
60
61
 
61
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ashot Vardanian",
6
6
  "license": "Apache 2.0",
@@ -18,5 +18,11 @@
18
18
  },
19
19
  "scripts": {
20
20
  "test": "node javascript/test.js"
21
+ },
22
+ "devDependencies": {
23
+ "@semantic-release/exec": "^6.0.3",
24
+ "@semantic-release/git": "^10.0.1",
25
+ "conventional-changelog-eslint": "^3.0.9",
26
+ "semantic-release": "^21.0.2"
21
27
  }
22
28
  }
package/python/bench.py CHANGED
@@ -12,30 +12,50 @@ from usearch.io import load_matrix
12
12
 
13
13
  def measure(f) -> float:
14
14
  a = time.time_ns()
15
- f()
15
+ result = f()
16
16
  b = time.time_ns()
17
17
  c = b - a
18
18
  secs = c / (10 ** 9)
19
19
  print(f'- Took: {secs:.2f} seconds')
20
- return secs
20
+ return secs, result
21
21
 
22
22
 
23
23
  def bench_faiss(index, vectors: np.array, queries: np.array, neighbors: np.array):
24
- dt = measure(lambda: index.add(vectors))
25
- print(f'- Performance: {vectors.shape[0]/dt:.2f} vectors/s')
24
+ dt, _ = measure(lambda: index.add(vectors))
25
+ print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
26
+
27
+ dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
28
+ print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
29
+
30
+ recall_at_one = 0.0
31
+ matches = results[1]
32
+ for i in range(queries.shape[0]):
33
+ recall_at_one += neighbors[i, 0] in matches[i, :]
34
+ recall_at_one /= queries.shape[0]
35
+ print(f'- Recall@1: {recall_at_one * 100:.2f} %')
26
36
 
27
37
 
28
38
  def bench_usearch(index, vectors: np.array, queries: np.array, neighbors: np.array):
29
39
  labels = np.arange(vectors.shape[0], dtype=np.longlong)
30
40
  assert len(index) == 0
31
- dt = measure(lambda: index.add(labels, vectors))
41
+ dt, _ = measure(lambda: index.add(labels, vectors))
32
42
  assert len(index) == vectors.shape[0]
33
- print(f'- Performance: {vectors.shape[0]/dt:.2f} vectors/s')
43
+ print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
44
+
45
+ dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
46
+ print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
34
47
 
48
+ recall_at_one = 0.0
49
+ matches = results[0]
50
+ for i in range(queries.shape[0]):
51
+ recall_at_one += neighbors[i, 0] in matches[i, :]
52
+ recall_at_one /= queries.shape[0]
53
+ print(f'- Recall@1: {recall_at_one * 100:.2f} %')
35
54
 
36
55
  # Showcases how to use Numba to JIT-compile similarity measures for USearch.
37
56
  # https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
38
57
 
58
+
39
59
  signature_f32 = types.float32(
40
60
  types.CPointer(types.float32),
41
61
  types.CPointer(types.float32),
@@ -85,7 +105,7 @@ def main(
85
105
  )
86
106
  bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
87
107
 
88
- print('USearch: HNSW + Numba JIT + Half Precision')
108
+ print('USearch: HNSW + Half Precision')
89
109
  index = Index(
90
110
  ndim=dim,
91
111
  dtype='f16',
@@ -95,6 +115,16 @@ def main(
95
115
  )
96
116
  bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
97
117
 
118
+ print('USearch: HNSW + Quarter Precision')
119
+ index = Index(
120
+ ndim=dim,
121
+ dtype='f8',
122
+ expansion_add=expansion_add,
123
+ expansion_search=expansion_search,
124
+ connectivity=connectivity,
125
+ )
126
+ bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
127
+
98
128
  print('FAISS: HNSW')
99
129
  index = IndexHNSWFlat(dim, connectivity)
100
130
  index.hnsw.efSearch = expansion_search
package/python/lib.cpp CHANGED
@@ -145,21 +145,24 @@ static void add_one_to_index(native_index_t& index, label_t label, py::buffer ve
145
145
  if (vector_info.ndim != 1)
146
146
  throw std::invalid_argument("Expects a vector, not a higher-rank tensor!");
147
147
 
148
- ssize_t vector_dimensions = vector_info.shape[0];
148
+ Py_ssize_t vector_dimensions = vector_info.shape[0];
149
149
  char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
150
- if (vector_dimensions != static_cast<ssize_t>(index.dimensions()))
150
+ if (vector_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
151
151
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
152
152
 
153
153
  if (index.size() + 1 >= index.capacity())
154
154
  index.reserve(ceil2(index.size() + 1));
155
155
 
156
+ add_config_t config;
157
+ config.store_vector = copy;
158
+
156
159
  // https://docs.python.org/3/library/struct.html#format-characters
157
160
  if (vector_info.format == "e")
158
- index.add(label, reinterpret_cast<f16_bits_t const*>(vector_data), 0, copy);
161
+ index.add(label, reinterpret_cast<f16_bits_t const*>(vector_data), config);
159
162
  else if (vector_info.format == "f")
160
- index.add(label, reinterpret_cast<float const*>(vector_data), 0, copy);
163
+ index.add(label, reinterpret_cast<float const*>(vector_data), config);
161
164
  else if (vector_info.format == "d")
162
- index.add(label, reinterpret_cast<double const*>(vector_data), 0, copy);
165
+ index.add(label, reinterpret_cast<double const*>(vector_data), config);
163
166
  else
164
167
  throw std::invalid_argument("Incompatible scalars in the vector!");
165
168
  }
@@ -180,10 +183,10 @@ static void add_many_to_index( //
180
183
  if (vectors_info.ndim != 2)
181
184
  throw std::invalid_argument("Expects a matrix of vectors to add!");
182
185
 
183
- ssize_t labels_count = labels_info.shape[0];
184
- ssize_t vectors_count = vectors_info.shape[0];
185
- ssize_t vectors_dimensions = vectors_info.shape[1];
186
- if (vectors_dimensions != static_cast<ssize_t>(index.dimensions()))
186
+ Py_ssize_t labels_count = labels_info.shape[0];
187
+ Py_ssize_t vectors_count = vectors_info.shape[0];
188
+ Py_ssize_t vectors_dimensions = vectors_info.shape[1];
189
+ if (vectors_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
187
190
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
188
191
 
189
192
  if (labels_count != vectors_count)
@@ -198,56 +201,68 @@ static void add_many_to_index( //
198
201
  // https://docs.python.org/3/library/struct.html#format-characters
199
202
  if (vectors_info.format == "e")
200
203
  multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
204
+ add_config_t config;
205
+ config.store_vector = copy;
206
+ config.thread = thread_idx;
201
207
  label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
202
208
  f16_bits_t const* vector =
203
209
  reinterpret_cast<f16_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
204
- index.add(label, vector, thread_idx, copy);
210
+ index.add(label, vector, config);
205
211
  });
206
212
  else if (vectors_info.format == "f")
207
213
  multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
214
+ add_config_t config;
215
+ config.store_vector = copy;
216
+ config.thread = thread_idx;
208
217
  label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
209
218
  float const* vector = reinterpret_cast<float const*>(vectors_data + task_idx * vectors_info.strides[0]);
210
- index.add(label, vector, thread_idx, copy);
219
+ index.add(label, vector, config);
211
220
  });
212
221
  else if (vectors_info.format == "d")
213
222
  multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
223
+ add_config_t config;
224
+ config.store_vector = copy;
225
+ config.thread = thread_idx;
214
226
  label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
215
227
  double const* vector = reinterpret_cast<double const*>(vectors_data + task_idx * vectors_info.strides[0]);
216
- index.add(label, vector, thread_idx, copy);
228
+ index.add(label, vector, config);
217
229
  });
218
230
  else
219
231
  throw std::invalid_argument("Incompatible scalars in the vectors matrix!");
220
232
  }
221
233
 
222
- static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, std::size_t wanted) {
234
+ static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, std::size_t wanted, bool exact) {
223
235
 
224
236
  py::buffer_info vector_info = vector.request();
225
- ssize_t vector_dimensions = vector_info.shape[0];
237
+ Py_ssize_t vector_dimensions = vector_info.shape[0];
226
238
  char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
227
- if (vector_dimensions != static_cast<ssize_t>(index.dimensions()))
239
+ if (vector_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
228
240
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
229
241
 
230
- py::array_t<label_t> labels_py(static_cast<ssize_t>(wanted));
231
- py::array_t<distance_t> distances_py(static_cast<ssize_t>(wanted));
242
+ py::array_t<label_t> labels_py(static_cast<Py_ssize_t>(wanted));
243
+ py::array_t<distance_t> distances_py(static_cast<Py_ssize_t>(wanted));
232
244
  std::size_t count{};
233
245
  auto labels_py1d = labels_py.mutable_unchecked<1>();
234
246
  auto distances_py1d = distances_py.mutable_unchecked<1>();
235
247
 
248
+ search_config_t config;
249
+ config.exact = exact;
250
+
236
251
  // https://docs.python.org/3/library/struct.html#format-characters
237
252
  if (vector_info.format == "e")
238
253
  count = index.search( //
239
- reinterpret_cast<f16_bits_t const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), 0);
254
+ reinterpret_cast<f16_bits_t const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), config);
240
255
  else if (vector_info.format == "f")
241
256
  count = index.search( //
242
- reinterpret_cast<float const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), 0);
257
+ reinterpret_cast<float const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), config);
243
258
  else if (vector_info.format == "d")
244
259
  count = index.search( //
245
- reinterpret_cast<double const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), 0);
260
+ reinterpret_cast<double const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), config);
246
261
  else
247
262
  throw std::invalid_argument("Incompatible scalars in the query vector!");
248
263
 
249
- labels_py.resize(py_shape_t{static_cast<ssize_t>(count)});
250
- distances_py.resize(py_shape_t{static_cast<ssize_t>(count)});
264
+ labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
265
+ distances_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
251
266
 
252
267
  py::tuple results(3);
253
268
  results[0] = labels_py;
@@ -265,25 +280,25 @@ static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, s
265
280
  * 2. matrix of distances,
266
281
  * 3. array with match counts.
267
282
  */
268
- static py::tuple search_many_in_index(native_index_t& index, py::buffer vectors, std::size_t wanted) {
283
+ static py::tuple search_many_in_index(native_index_t& index, py::buffer vectors, std::size_t wanted, bool exact) {
269
284
 
270
285
  if (wanted == 0)
271
286
  return py::tuple(3);
272
287
 
273
288
  py::buffer_info vectors_info = vectors.request();
274
289
  if (vectors_info.ndim == 1)
275
- return search_one_in_index(index, vectors, wanted);
290
+ return search_one_in_index(index, vectors, wanted, exact);
276
291
  if (vectors_info.ndim != 2)
277
292
  throw std::invalid_argument("Expects a matrix of vectors to add!");
278
293
 
279
- ssize_t vectors_count = vectors_info.shape[0];
280
- ssize_t vectors_dimensions = vectors_info.shape[1];
294
+ Py_ssize_t vectors_count = vectors_info.shape[0];
295
+ Py_ssize_t vectors_dimensions = vectors_info.shape[1];
281
296
  char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
282
- if (vectors_dimensions != static_cast<ssize_t>(index.dimensions()))
297
+ if (vectors_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
283
298
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
284
299
 
285
- py::array_t<label_t> labels_py({vectors_count, static_cast<ssize_t>(wanted)});
286
- py::array_t<distance_t> distances_py({vectors_count, static_cast<ssize_t>(wanted)});
300
+ py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
301
+ py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
287
302
  py::array_t<Py_ssize_t> counts_py(vectors_count);
288
303
  auto labels_py2d = labels_py.mutable_unchecked<2>();
289
304
  auto distances_py2d = distances_py.mutable_unchecked<2>();
@@ -292,21 +307,30 @@ static py::tuple search_many_in_index(native_index_t& index, py::buffer vectors,
292
307
  // https://docs.python.org/3/library/struct.html#format-characters
293
308
  if (vectors_info.format == "e")
294
309
  multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
310
+ search_config_t config;
311
+ config.thread = thread_idx;
312
+ config.exact = exact;
295
313
  f16_bits_t const* vector = (f16_bits_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
296
314
  counts_py1d(task_idx) = static_cast<Py_ssize_t>(
297
- index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), thread_idx));
315
+ index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), config));
298
316
  });
299
317
  else if (vectors_info.format == "f")
300
318
  multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
319
+ search_config_t config;
320
+ config.thread = thread_idx;
321
+ config.exact = exact;
301
322
  float const* vector = (float const*)(vectors_data + task_idx * vectors_info.strides[0]);
302
323
  counts_py1d(task_idx) = static_cast<Py_ssize_t>(
303
- index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), thread_idx));
324
+ index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), config));
304
325
  });
305
326
  else if (vectors_info.format == "d")
306
327
  multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
328
+ search_config_t config;
329
+ config.thread = thread_idx;
330
+ config.exact = exact;
307
331
  double const* vector = (double const*)(vectors_data + task_idx * vectors_info.strides[0]);
308
332
  counts_py1d(task_idx) = static_cast<Py_ssize_t>(
309
- index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), thread_idx));
333
+ index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), config));
310
334
  });
311
335
  else
312
336
  throw std::invalid_argument("Incompatible scalars in the query matrix!");
@@ -364,8 +388,8 @@ inline std::uint64_t hash(std::uint64_t v) noexcept {
364
388
  template <typename scalar_at>
365
389
  inline void hash_typed_buffer(hash_index_w_meta_t& index, py::buffer_info const& vector_info) noexcept {
366
390
  char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
367
- ssize_t vector_dimensions = vector_info.shape[0];
368
- ssize_t vector_stride = vector_info.strides[0];
391
+ Py_ssize_t vector_dimensions = vector_info.shape[0];
392
+ Py_ssize_t vector_stride = vector_info.strides[0];
369
393
  std::memset(index.buffer_.data(), 0, index.words_ * sizeof(hash_word_t));
370
394
 
371
395
  for (std::size_t i = 0; i != static_cast<std::size_t>(vector_dimensions); ++i) {
@@ -428,7 +452,8 @@ PYBIND11_MODULE(index, m) {
428
452
  i.def( //
429
453
  "search", &search_many_in_index, //
430
454
  py::arg("query"), //
431
- py::arg("count") = 10 //
455
+ py::arg("count") = 10, //
456
+ py::arg("exact") = false //
432
457
  );
433
458
 
434
459
  i.def("__len__", &native_index_t::size);
@@ -463,7 +488,9 @@ PYBIND11_MODULE(index, m) {
463
488
  index.reserve(ceil2(index.size() + 1));
464
489
  auto proxy = set.unchecked<1>();
465
490
  auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
466
- index.add(label, view, 0, copy);
491
+ add_config_t config;
492
+ config.store_vector = copy;
493
+ index.add(label, view, config);
467
494
  }, //
468
495
  py::arg("label"), //
469
496
  py::arg("set"), //
@@ -477,10 +504,10 @@ PYBIND11_MODULE(index, m) {
477
504
  validate_set(set);
478
505
  auto proxy = set.unchecked<1>();
479
506
  auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
480
- auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<ssize_t>(count)});
507
+ auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
481
508
  auto labels_proxy = labels_py.mutable_unchecked<1>();
482
- auto found = index.search(view, count, &labels_proxy(0), nullptr, 0);
483
- labels_py.resize(py_shape_t{static_cast<ssize_t>(found)});
509
+ auto found = index.search(view, count, &labels_proxy(0), nullptr);
510
+ labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
484
511
  return labels_py;
485
512
  },
486
513
  py::arg("set"), //
@@ -515,7 +542,7 @@ PYBIND11_MODULE(index, m) {
515
542
  if (index.size() + 1 >= index.capacity())
516
543
  index.reserve(ceil2(index.size() + 1));
517
544
  hash_buffer(index, array);
518
- index.add(label, index.buffer(), 0);
545
+ index.add(label, index.buffer());
519
546
  }, //
520
547
  py::arg("label"), //
521
548
  py::arg("array") //
@@ -527,10 +554,10 @@ PYBIND11_MODULE(index, m) {
527
554
  if (index.size() + 1 >= index.capacity())
528
555
  index.reserve(ceil2(index.size() + 1));
529
556
  hash_buffer(index, array);
530
- auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<ssize_t>(count)});
557
+ auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
531
558
  auto labels_proxy = labels_py.mutable_unchecked<1>();
532
- auto found = index.search(index.buffer(), count, &labels_proxy(0), nullptr, 0);
533
- labels_py.resize(py_shape_t{static_cast<ssize_t>(found)});
559
+ auto found = index.search(index.buffer(), count, &labels_proxy(0), nullptr);
560
+ labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
534
561
  return labels_py;
535
562
  },
536
563
  py::arg("array"), //