usearch 0.1.10 → 0.2.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.
@@ -100,10 +100,15 @@
100
100
  "version": "cpp"
101
101
  },
102
102
  "cSpell.words": [
103
+ "astype",
104
+ "dtype",
103
105
  "equidimensional",
106
+ "hashable",
104
107
  "jaccard",
108
+ "longlong",
105
109
  "memmap",
106
110
  "ndim",
111
+ "preprocess",
107
112
  "simsimd",
108
113
  "usearch"
109
114
  ],
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "0.1.10"
3
+ version = "0.2.0"
4
4
  authors = ["Ashot Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/README.md CHANGED
@@ -227,6 +227,15 @@ index = usearch.Index(
227
227
  expansion_search=64, # Control the quality of search, optional
228
228
  )
229
229
 
230
+ vector = np.random.uniform(0, 0.3, (index.ndim)).astype(np.float32)
231
+ index.add(42, vector)
232
+ matches, distances, count = index.search(vector, 10)
233
+ ```
234
+
235
+ Same can be done with batches, rather than single entries.
236
+ Under the hood, worker threads will be spawned, to parallelize the procedure.
237
+
238
+ ```py
230
239
  n = 100
231
240
  labels = np.array(range(n), dtype=np.longlong)
232
241
  vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
@@ -234,9 +243,6 @@ vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
234
243
  # You can avoid copying the data
235
244
  # Handy when build 1B+ indexes of memory-mapped files
236
245
  index.add(labels, vectors, copy=True)
237
- assert len(index) == n
238
-
239
- # You can search a batch at once
240
246
  matches, distances, counts = index.search(vectors, 10)
241
247
  ```
242
248
 
@@ -407,15 +413,14 @@ index = usearch.Index(ndim=256)
407
413
  def add(label: int, photo: Image.Image):
408
414
  image = model.preprocess_image(photo)
409
415
  vector = model.encode_image(image).detach().numpy()
410
- labels = np.array([label], dtype=np.longlong)
411
- index.add(labels, vector, copy=True)
416
+ index.add(label, vector.flatten(), copy=True)
412
417
 
413
418
  @server
414
419
  def search(query: str) -> np.ndarray:
415
420
  tokens = model.preprocess_text(query)
416
421
  vector = model.encode_text(tokens).detach().numpy()
417
- neighbors = index.search(vector, 3)
418
- return neighbors[0][:neighbors[2][0]]
422
+ neighbors, _, _ = index.search(vector.flatten(), 3)
423
+ return neighbors
419
424
 
420
425
  server.run()
421
426
  ```
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.10
1
+ 0.2.0
@@ -339,8 +339,9 @@ class max_heap_gt {
339
339
  bool empty() const noexcept { return !size_; }
340
340
  std::size_t size() const noexcept { return size_; }
341
341
  std::size_t capacity() const noexcept { return capacity_; }
342
+ /// @brief Selects the largest element in the heap.
343
+ /// @return Reference to the stored element.
342
344
  element_t const& top() const noexcept { return elements_[0]; }
343
- element_t const& max() const noexcept { return elements_[0]; }
344
345
 
345
346
  void clear() noexcept {
346
347
  while (size_) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.1.10",
3
+ "version": "0.2.0",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ashot Vardanian",
6
6
  "license": "Apache 2.0",
package/python/bench.py CHANGED
@@ -2,11 +2,11 @@ import datetime
2
2
  import numpy as np
3
3
  import fire
4
4
 
5
- from usearchh.io import load_matrix
6
-
7
5
  import faiss
8
6
  import usearch
9
7
 
8
+ import struct
9
+ import numpy as np
10
10
 
11
11
  def measure(f) -> float:
12
12
  a = datetime.datetime.now()
@@ -44,11 +44,23 @@ def main(
44
44
  neighbors_mat = load_matrix(neighbors)
45
45
  dim = vectors_mat.shape[1]
46
46
 
47
+ # Test FAISS
47
48
  index = faiss.IndexHNSWFlat(dim, connectivity)
48
49
  index.hnsw.efSearch = expansion_search
49
50
  index.hnsw.efConstruction = expansion_add
50
51
  bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
51
52
 
53
+ # Test FAISS with default Product Quantization
54
+ nlist = 10000 # Number of inverted lists (number of partitions or cells).
55
+ nsegment = 16 # Number of segments for PQ (number of subquantizers).
56
+ nbit = 8 # Number of bits to encode each segment.
57
+ coarse_quantizer = faiss.IndexHNSWFlat(dim, connectivity)
58
+ index = faiss.IndexIVFPQ(coarse_quantizer, dim, nlist, nsegment, nbit)
59
+ index.train(vectors_mat)
60
+ index.nprobe = 10
61
+ bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
62
+
63
+ # Test USearch `Index`
52
64
  index = usearch.Index(
53
65
  ndim=dim,
54
66
  expansion_add=expansion_add,
package/python/lib.cpp CHANGED
@@ -24,11 +24,41 @@ using namespace unum::usearch;
24
24
  using namespace unum;
25
25
 
26
26
  namespace py = pybind11;
27
+ using py_shape_t = py::array::ShapeContainer;
27
28
 
28
29
  using label_t = Py_ssize_t;
29
30
  using distance_t = punned_distance_t;
31
+ using id_t = std::uint32_t;
30
32
  using native_index_t = auto_index_gt<label_t>;
31
33
 
34
+ using set_member_t = std::uint32_t;
35
+ using set_view_t = span_gt<set_member_t const>;
36
+ using sets_index_t = index_gt<jaccard_gt<set_member_t>, label_t, id_t, set_member_t>;
37
+
38
+ using hash_word_t = std::uint64_t;
39
+ using hash_index_t = index_gt<bit_hamming_gt<hash_word_t>, label_t, id_t, hash_word_t>;
40
+ static constexpr std::size_t bits_per_hash_word_k = sizeof(hash_word_t) * CHAR_BIT;
41
+
42
+ struct hash_index_w_meta_t : public hash_index_t {
43
+ using hash_index_t::add;
44
+ using hash_index_t::capacity;
45
+ using hash_index_t::reserve;
46
+ using hash_index_t::search;
47
+ using hash_index_t::size;
48
+
49
+ std::vector<hash_word_t> buffer_;
50
+ std::size_t words_;
51
+ std::size_t bits_;
52
+
53
+ hash_index_w_meta_t(config_t config, std::size_t bits) : hash_index_t(config) {
54
+ words_ = divide_round_up<bits_per_hash_word_k>(bits);
55
+ bits_ = words_ * bits_per_hash_word_k;
56
+ buffer_.resize(words_);
57
+ }
58
+
59
+ span_gt<hash_word_t const> buffer() const noexcept { return {buffer_.data(), buffer_.size()}; }
60
+ };
61
+
32
62
  static native_index_t make_index( //
33
63
  std::size_t dimensions, //
34
64
  std::size_t capacity, //
@@ -56,33 +86,91 @@ static native_index_t make_index( //
56
86
  return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
57
87
  }
58
88
 
59
- static void add_to_index(native_index_t& index, py::buffer labels, py::buffer vectors, bool copy) {
89
+ static std::unique_ptr<sets_index_t> make_sets_index( //
90
+ std::size_t capacity, //
91
+ std::size_t connectivity, //
92
+ std::size_t expansion_add, //
93
+ std::size_t expansion_search //
94
+ ) {
95
+ config_t config;
96
+ config.expansion_add = expansion_add;
97
+ config.expansion_search = expansion_search;
98
+ config.connectivity = connectivity;
99
+ config.max_elements = capacity;
100
+ config.max_threads_add = 1;
101
+ config.max_threads_search = 1;
102
+
103
+ return std::unique_ptr<sets_index_t>(new sets_index_t(config));
104
+ }
105
+
106
+ static std::unique_ptr<hash_index_w_meta_t> make_hash_index( //
107
+ std::size_t bits, //
108
+ std::size_t capacity, //
109
+ std::size_t connectivity, //
110
+ std::size_t expansion_add, //
111
+ std::size_t expansion_search //
112
+ ) {
113
+ config_t config;
114
+ config.expansion_add = expansion_add;
115
+ config.expansion_search = expansion_search;
116
+ config.connectivity = connectivity;
117
+ config.max_elements = capacity;
118
+ config.max_threads_add = 1;
119
+ config.max_threads_search = 1;
120
+
121
+ return std::unique_ptr<hash_index_w_meta_t>(new hash_index_w_meta_t(config, bits));
122
+ }
123
+
124
+ static void add_one_to_index(native_index_t& index, label_t label, py::buffer vector, bool copy) {
125
+
126
+ py::buffer_info vector_info = vector.request();
127
+ if (vector_info.ndim != 1)
128
+ throw std::invalid_argument("Expects a vector, not a higher-rank tensor!");
129
+
130
+ ssize_t vector_dimensions = vector_info.shape[0];
131
+ char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
132
+ if (vector_dimensions != static_cast<ssize_t>(index.dimensions()))
133
+ throw std::invalid_argument("The number of vector dimensions doesn't match!");
134
+
135
+ if (index.size() + 1 >= index.capacity())
136
+ index.reserve(ceil2(index.size() + 1));
137
+
138
+ // https://docs.python.org/3/library/struct.html#format-characters
139
+ if (vector_info.format == "e")
140
+ index.add(label, reinterpret_cast<f16_converted_t const*>(vector_data), 0, copy);
141
+ else if (vector_info.format == "f")
142
+ index.add(label, reinterpret_cast<float const*>(vector_data), 0, copy);
143
+ else if (vector_info.format == "d")
144
+ index.add(label, reinterpret_cast<double const*>(vector_data), 0, copy);
145
+ else
146
+ throw std::invalid_argument("Incompatible scalars in the vector!");
147
+ }
148
+
149
+ static void add_many_to_index(native_index_t& index, py::buffer labels, py::buffer vectors, bool copy) {
60
150
 
61
151
  py::buffer_info labels_info = labels.request();
62
152
  py::buffer_info vectors_info = vectors.request();
63
153
 
64
154
  if (labels_info.format != py::format_descriptor<label_t>::format())
65
- throw std::runtime_error("Incompatible label type!");
155
+ throw std::invalid_argument("Incompatible label type!");
66
156
 
67
157
  if (labels_info.ndim != 1)
68
- throw std::runtime_error("Labels must be placed in a single-dimensional array!");
158
+ throw std::invalid_argument("Labels must be placed in a single-dimensional array!");
69
159
 
70
160
  if (vectors_info.ndim != 2)
71
- throw std::runtime_error("Expects a matrix of vectors to add!");
161
+ throw std::invalid_argument("Expects a matrix of vectors to add!");
72
162
 
73
163
  ssize_t labels_count = labels_info.shape[0];
74
164
  ssize_t vectors_count = vectors_info.shape[0];
75
165
  ssize_t vectors_dimensions = vectors_info.shape[1];
76
- if (vectors_dimensions != index.dimensions())
77
- throw std::runtime_error("The number of vector dimensions doesn't match!");
166
+ if (vectors_dimensions != static_cast<ssize_t>(index.dimensions()))
167
+ throw std::invalid_argument("The number of vector dimensions doesn't match!");
78
168
 
79
169
  if (labels_count != vectors_count)
80
- throw std::runtime_error("Number of labels and vectors must match!");
170
+ throw std::invalid_argument("Number of labels and vectors must match!");
81
171
 
82
- if (index.size() + vectors_count >= index.capacity()) {
83
- std::size_t next_capacity = ceil2(index.size() + vectors_count);
84
- index.reserve(next_capacity);
85
- }
172
+ if (index.size() + vectors_count >= index.capacity())
173
+ index.reserve(ceil2(index.size() + vectors_count));
86
174
 
87
175
  char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
88
176
  char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
@@ -108,11 +196,48 @@ static void add_to_index(native_index_t& index, py::buffer labels, py::buffer ve
108
196
  index.add(label, vector, thread_idx, copy);
109
197
  });
110
198
  else
111
- throw std::runtime_error("Incompatible scalars in the vectors matrix!");
199
+ throw std::invalid_argument("Incompatible scalars in the vectors matrix!");
200
+ }
201
+
202
+ static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, std::size_t wanted) {
203
+
204
+ py::buffer_info vector_info = vector.request();
205
+ ssize_t vector_dimensions = vector_info.shape[0];
206
+ char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
207
+ if (vector_dimensions != static_cast<ssize_t>(index.dimensions()))
208
+ throw std::invalid_argument("The number of vector dimensions doesn't match!");
209
+
210
+ py::array_t<label_t> labels_py(static_cast<ssize_t>(wanted));
211
+ py::array_t<distance_t> distances_py(static_cast<ssize_t>(wanted));
212
+ std::size_t count{};
213
+ auto labels_py1d = labels_py.mutable_unchecked<1>();
214
+ auto distances_py1d = distances_py.mutable_unchecked<1>();
215
+
216
+ // https://docs.python.org/3/library/struct.html#format-characters
217
+ if (vector_info.format == "e")
218
+ count = index.search( //
219
+ reinterpret_cast<f16_converted_t const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), 0);
220
+ else if (vector_info.format == "f")
221
+ count = index.search( //
222
+ reinterpret_cast<float const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), 0);
223
+ else if (vector_info.format == "d")
224
+ count = index.search( //
225
+ reinterpret_cast<double const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), 0);
226
+ else
227
+ throw std::invalid_argument("Incompatible scalars in the query vector!");
228
+
229
+ labels_py.resize(py_shape_t{static_cast<ssize_t>(count)});
230
+ distances_py.resize(py_shape_t{static_cast<ssize_t>(count)});
231
+
232
+ py::tuple results(3);
233
+ results[0] = labels_py;
234
+ results[1] = distances_py;
235
+ results[2] = static_cast<Py_ssize_t>(count);
236
+ return results;
112
237
  }
113
238
 
114
239
  /**
115
- * @param vectors Matrix of vectors to search for..
240
+ * @param vectors Matrix of vectors to search for.
116
241
  * @param wanted Number of matches per request.
117
242
  *
118
243
  * @return Tuple with:
@@ -120,26 +245,30 @@ static void add_to_index(native_index_t& index, py::buffer labels, py::buffer ve
120
245
  * 2. matrix of distances,
121
246
  * 3. array with match counts.
122
247
  */
123
- static py::tuple search_in_index(native_index_t& index, py::buffer vectors, ssize_t wanted) {
248
+ static py::tuple search_many_in_index(native_index_t& index, py::buffer vectors, std::size_t wanted) {
249
+
250
+ if (wanted == 0)
251
+ return py::tuple(3);
124
252
 
125
253
  py::buffer_info vectors_info = vectors.request();
254
+ if (vectors_info.ndim == 1)
255
+ return search_one_in_index(index, vectors, wanted);
126
256
  if (vectors_info.ndim != 2)
127
- throw std::runtime_error("Expects a matrix of vectors to add!");
257
+ throw std::invalid_argument("Expects a matrix of vectors to add!");
128
258
 
129
259
  ssize_t vectors_count = vectors_info.shape[0];
130
260
  ssize_t vectors_dimensions = vectors_info.shape[1];
131
- if (vectors_dimensions != index.dimensions())
132
- throw std::runtime_error("The number of vector dimensions doesn't match!");
261
+ char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
262
+ if (vectors_dimensions != static_cast<ssize_t>(index.dimensions()))
263
+ throw std::invalid_argument("The number of vector dimensions doesn't match!");
133
264
 
134
- py::array_t<label_t> labels_py({vectors_count, wanted});
135
- py::array_t<distance_t> distances_py({vectors_count, wanted});
265
+ py::array_t<label_t> labels_py({vectors_count, static_cast<ssize_t>(wanted)});
266
+ py::array_t<distance_t> distances_py({vectors_count, static_cast<ssize_t>(wanted)});
136
267
  py::array_t<Py_ssize_t> counts_py(vectors_count);
137
268
  auto labels_py2d = labels_py.mutable_unchecked<2>();
138
269
  auto distances_py2d = distances_py.mutable_unchecked<2>();
139
270
  auto counts_py1d = counts_py.mutable_unchecked<1>();
140
271
 
141
- char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
142
-
143
272
  // https://docs.python.org/3/library/struct.html#format-characters
144
273
  if (vectors_info.format == "e")
145
274
  multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
@@ -160,7 +289,7 @@ static py::tuple search_in_index(native_index_t& index, py::buffer vectors, ssiz
160
289
  index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), thread_idx));
161
290
  });
162
291
  else
163
- throw std::runtime_error("Incompatible scalars in the query matrix!");
292
+ throw std::invalid_argument("Incompatible scalars in the query matrix!");
164
293
 
165
294
  py::tuple results(3);
166
295
  results[0] = labels_py;
@@ -169,9 +298,78 @@ static py::tuple search_in_index(native_index_t& index, py::buffer vectors, ssiz
169
298
  return results;
170
299
  }
171
300
 
172
- static void save_index(native_index_t const& index, std::string const& path) { index.save(path.c_str()); }
173
- static void load_index(native_index_t& index, std::string const& path) { index.load(path.c_str()); }
174
- static void view_index(native_index_t& index, std::string const& path) { index.view(path.c_str()); }
301
+ // clang-format off
302
+ template <typename index_at> void save_index(index_at const& index, std::string const& path) { index.save(path.c_str()); }
303
+ template <typename index_at> void load_index(index_at& index, std::string const& path) { index.load(path.c_str()); }
304
+ template <typename index_at> void view_index(index_at& index, std::string const& path) { index.view(path.c_str()); }
305
+ template <typename index_at> void clear_index(index_at& index) { index.clear(); }
306
+ // clang-format on
307
+
308
+ template <typename element_at> bool has_duplicates(element_at const* begin, element_at const* end) {
309
+ if (begin == end)
310
+ return false;
311
+ element_at const* last = begin;
312
+ begin++;
313
+ for (; begin != end; ++begin, ++last) {
314
+ if (*begin == *last)
315
+ return true;
316
+ }
317
+ return false;
318
+ }
319
+
320
+ void validate_set(py::array_t<set_member_t> const& set) {
321
+ if (set.ndim() != 1)
322
+ throw std::invalid_argument("Set can't be multi-dimensional!");
323
+ if (set.strides(0) != sizeof(set_member_t))
324
+ throw std::invalid_argument("Set can't be strided!");
325
+ auto proxy = set.unchecked<1>();
326
+ set_member_t const* begin = &proxy(0);
327
+ set_member_t const* end = begin + proxy.size();
328
+ if (!std::is_sorted(begin, end))
329
+ throw std::invalid_argument("Set must be sorted!");
330
+ if (has_duplicates(begin, end))
331
+ throw std::invalid_argument("Set must be deduplicated!");
332
+ }
333
+
334
+ inline std::uint64_t hash_ror64(std::uint64_t v, int r) noexcept { return (v >> r) | (v << (64 - r)); }
335
+
336
+ inline std::uint64_t hash(std::uint64_t v) noexcept {
337
+ v ^= hash_ror64(v, 25) ^ hash_ror64(v, 50);
338
+ v *= 0xA24BAED4963EE407UL;
339
+ v ^= hash_ror64(v, 24) ^ hash_ror64(v, 49);
340
+ v *= 0x9FB21C651E98DF25UL;
341
+ return v ^ v >> 28;
342
+ }
343
+
344
+ template <typename scalar_at>
345
+ inline void hash_typed_buffer(hash_index_w_meta_t& index, py::buffer_info const& vector_info) noexcept {
346
+ char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
347
+ ssize_t vector_dimensions = vector_info.shape[0];
348
+ ssize_t vector_stride = vector_info.strides[0];
349
+ std::memset(index.buffer_.data(), 0, index.words_ * sizeof(hash_word_t));
350
+
351
+ for (std::size_t i = 0; i != static_cast<std::size_t>(vector_dimensions); ++i) {
352
+ scalar_at scalar = *reinterpret_cast<scalar_at const*>(vector_data + i * vector_stride);
353
+ std::uint64_t scalar_hash = hash(scalar);
354
+ index.buffer_[scalar_hash % index.words_] |= hash_word_t(1) << (scalar_hash % bits_per_hash_word_k);
355
+ }
356
+ }
357
+
358
+ void hash_buffer(hash_index_w_meta_t& index, py::buffer vector) {
359
+ py::buffer_info info = vector.request();
360
+ if (info.ndim != 1)
361
+ throw std::invalid_argument("Array can't be multi-dimensional!");
362
+
363
+ // https://docs.python.org/3/library/struct.html#format-characters
364
+ if (info.format == "h" || info.format == "H")
365
+ return hash_typed_buffer<std::uint16_t>(index, info);
366
+ else if (info.format == "i" || info.format == "I" || info.format == "l" || info.format == "L")
367
+ return hash_typed_buffer<std::uint32_t>(index, info);
368
+ else if (info.format == "q" || info.format == "Q" || info.format == "n" || info.format == "N")
369
+ return hash_typed_buffer<std::uint64_t>(index, info);
370
+ else
371
+ throw std::invalid_argument("Array elements must be 16, 32, or 64 bit hashable integers!");
372
+ }
175
373
 
176
374
  PYBIND11_MODULE(usearch, m) {
177
375
  m.doc() = "Unum USearch Python bindings";
@@ -180,7 +378,7 @@ PYBIND11_MODULE(usearch, m) {
180
378
 
181
379
  i.def(py::init(&make_index), //
182
380
  py::kw_only(), //
183
- py::arg("ndim"), //
381
+ py::arg("ndim") = 0, //
184
382
  py::arg("capacity") = 0, //
185
383
  py::arg("dtype") = std::string("f32"), //
186
384
  py::arg("metric") = std::string("ip"), //
@@ -190,18 +388,26 @@ PYBIND11_MODULE(usearch, m) {
190
388
  py::arg("metric_pointer") = 0 //
191
389
  );
192
390
 
193
- i.def( //
194
- "add", &add_to_index, //
195
- py::arg("labels"), //
196
- py::arg("vectors"), //
197
- py::kw_only(), //
198
- py::arg("copy") = true //
391
+ i.def( //
392
+ "add", &add_many_to_index, //
393
+ py::arg("labels"), //
394
+ py::arg("vectors"), //
395
+ py::kw_only(), //
396
+ py::arg("copy") = true //
397
+ );
398
+
399
+ i.def( //
400
+ "add", &add_one_to_index, //
401
+ py::arg("label"), //
402
+ py::arg("vector"), //
403
+ py::kw_only(), //
404
+ py::arg("copy") = true //
199
405
  );
200
406
 
201
- i.def( //
202
- "search", &search_in_index, //
203
- py::arg("vectors"), //
204
- py::arg("count") = 10 //
407
+ i.def( //
408
+ "search", &search_many_in_index, //
409
+ py::arg("query"), //
410
+ py::arg("count") = 10 //
205
411
  );
206
412
 
207
413
  i.def("__len__", &native_index_t::size);
@@ -210,7 +416,111 @@ PYBIND11_MODULE(usearch, m) {
210
416
  i.def_property_readonly("connectivity", &native_index_t::connectivity);
211
417
  i.def_property_readonly("capacity", &native_index_t::capacity);
212
418
 
213
- i.def("save", &save_index, py::arg("path"));
214
- i.def("load", &load_index, py::arg("path"));
215
- i.def("view", &view_index, py::arg("path"));
419
+ i.def("save", &save_index<native_index_t>, py::arg("path"));
420
+ i.def("load", &load_index<native_index_t>, py::arg("path"));
421
+ i.def("view", &view_index<native_index_t>, py::arg("path"));
422
+ i.def("clear", &clear_index<native_index_t>);
423
+
424
+ auto si = py::class_<sets_index_t>(m, "SetsIndex");
425
+
426
+ si.def( //
427
+ py::init(&make_sets_index), //
428
+ py::kw_only(), //
429
+ py::arg("capacity") = 0, //
430
+ py::arg("connectivity") = config_t::connectivity_default_k, //
431
+ py::arg("expansion_add") = config_t::expansion_add_default_k, //
432
+ py::arg("expansion_search") = config_t::expansion_search_default_k //
433
+ );
434
+
435
+ si.def( //
436
+ "add",
437
+ [](sets_index_t& index, label_t label, py::array_t<set_member_t> set, bool copy) {
438
+ validate_set(set);
439
+ if (index.size() + 1 >= index.capacity())
440
+ index.reserve(ceil2(index.size() + 1));
441
+ auto proxy = set.unchecked<1>();
442
+ auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
443
+ index.add(label, view, 0, copy);
444
+ }, //
445
+ py::arg("label"), //
446
+ py::arg("set"), //
447
+ py::kw_only(), //
448
+ py::arg("copy") = true //
449
+ );
450
+
451
+ si.def( //
452
+ "search",
453
+ [](sets_index_t& index, py::array_t<set_member_t> set, std::size_t count) -> py::array_t<label_t> {
454
+ validate_set(set);
455
+ auto proxy = set.unchecked<1>();
456
+ auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
457
+ auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<ssize_t>(count)});
458
+ auto labels_proxy = labels_py.mutable_unchecked<1>();
459
+ auto found = index.search(view, count, &labels_proxy(0), nullptr, 0);
460
+ labels_py.resize(py_shape_t{static_cast<ssize_t>(found)});
461
+ return labels_py;
462
+ },
463
+ py::arg("set"), //
464
+ py::arg("count") = 10 //
465
+ );
466
+
467
+ si.def("__len__", &sets_index_t::size);
468
+ si.def_property_readonly("size", &sets_index_t::size);
469
+ si.def_property_readonly("connectivity", &sets_index_t::connectivity);
470
+ si.def_property_readonly("capacity", &sets_index_t::capacity);
471
+
472
+ si.def("save", &save_index<sets_index_t>, py::arg("path"));
473
+ si.def("load", &load_index<sets_index_t>, py::arg("path"));
474
+ si.def("view", &view_index<sets_index_t>, py::arg("path"));
475
+ si.def("clear", &clear_index<sets_index_t>);
476
+
477
+ auto hi = py::class_<hash_index_w_meta_t>(m, "HashIndex");
478
+
479
+ hi.def( //
480
+ py::init(&make_hash_index), //
481
+ py::kw_only(), //
482
+ py::arg("bits"), //
483
+ py::arg("capacity") = 0, //
484
+ py::arg("connectivity") = config_t::connectivity_default_k, //
485
+ py::arg("expansion_add") = config_t::expansion_add_default_k, //
486
+ py::arg("expansion_search") = config_t::expansion_search_default_k //
487
+ );
488
+
489
+ hi.def( //
490
+ "add",
491
+ [](hash_index_w_meta_t& index, label_t label, py::buffer array) {
492
+ if (index.size() + 1 >= index.capacity())
493
+ index.reserve(ceil2(index.size() + 1));
494
+ hash_buffer(index, array);
495
+ index.add(label, index.buffer(), 0);
496
+ }, //
497
+ py::arg("label"), //
498
+ py::arg("array") //
499
+ );
500
+
501
+ hi.def( //
502
+ "search",
503
+ [](hash_index_w_meta_t& index, py::buffer array, std::size_t count) -> py::array_t<label_t> {
504
+ if (index.size() + 1 >= index.capacity())
505
+ index.reserve(ceil2(index.size() + 1));
506
+ hash_buffer(index, array);
507
+ auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<ssize_t>(count)});
508
+ auto labels_proxy = labels_py.mutable_unchecked<1>();
509
+ auto found = index.search(index.buffer(), count, &labels_proxy(0), nullptr, 0);
510
+ labels_py.resize(py_shape_t{static_cast<ssize_t>(found)});
511
+ return labels_py;
512
+ },
513
+ py::arg("array"), //
514
+ py::arg("count") = 10 //
515
+ );
516
+
517
+ hi.def("__len__", &hash_index_w_meta_t::size);
518
+ hi.def_property_readonly("size", &hash_index_w_meta_t::size);
519
+ hi.def_property_readonly("connectivity", &hash_index_w_meta_t::connectivity);
520
+ hi.def_property_readonly("capacity", &hash_index_w_meta_t::capacity);
521
+
522
+ hi.def("save", &save_index<hash_index_w_meta_t>, py::arg("path"));
523
+ hi.def("load", &load_index<hash_index_w_meta_t>, py::arg("path"));
524
+ hi.def("view", &view_index<hash_index_w_meta_t>, py::arg("path"));
525
+ hi.def("clear", &clear_index<hash_index_w_meta_t>);
216
526
  }
package/python/test.py CHANGED
@@ -43,3 +43,10 @@ assert len(index) == count_vectors
43
43
 
44
44
  results = index.search(vectors, 10)
45
45
  print('found', results[0].shape, results[1].shape, results[2].shape)
46
+
47
+
48
+ index_sets = usearch.SetsIndex()
49
+ index_sets.add(10, np.array([10, 12, 15], dtype=np.uint32))
50
+ index_sets.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))
51
+ results = index_sets.search(np.array([12, 15], dtype=np.uint32), 20)
52
+ print(results)
package/src/advanced.hpp CHANGED
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
  #include <functional>
3
+ #include <stdexcept> // `std::invalid_argument`
3
4
  #include <thread>
4
5
  #include <vector>
5
6
 
@@ -229,7 +230,7 @@ inline accuracy_t accuracy_from_name(char const* name, std::size_t len) {
229
230
  else if (str_equals(name, len, "i8q100"))
230
231
  accuracy = accuracy_t::i8q100_k;
231
232
  else
232
- throw std::runtime_error("Unknown type, choose: f32, f16, f64, i8q100");
233
+ throw std::invalid_argument("Unknown type, choose: f32, f16, f64, i8q100");
233
234
  return accuracy;
234
235
  }
235
236
 
@@ -237,16 +238,24 @@ template <typename index_at>
237
238
  inline index_at index_from_name( //
238
239
  char const* name, std::size_t len, std::size_t dimensions, accuracy_t accuracy, config_t const& config) {
239
240
 
240
- if (str_equals(name, len, "l2_sq") || str_equals(name, len, "euclidean_sq"))
241
+ if (str_equals(name, len, "l2_sq") || str_equals(name, len, "euclidean_sq")) {
242
+ if (dimensions == 0)
243
+ throw std::invalid_argument("The number of dimensions must be positive");
241
244
  return index_at::l2(dimensions, accuracy, config);
242
- else if (str_equals(name, len, "ip") || str_equals(name, len, "inner") || str_equals(name, len, "dot"))
245
+ } else if (str_equals(name, len, "ip") || str_equals(name, len, "inner") || str_equals(name, len, "dot")) {
246
+ if (dimensions == 0)
247
+ throw std::invalid_argument("The number of dimensions must be positive");
243
248
  return index_at::ip(dimensions, accuracy, config);
244
- else if (str_equals(name, len, "cos") || str_equals(name, len, "angular"))
249
+ } else if (str_equals(name, len, "cos") || str_equals(name, len, "angular")) {
250
+ if (dimensions == 0)
251
+ throw std::invalid_argument("The number of dimensions must be positive");
245
252
  return index_at::cos(dimensions, accuracy, config);
246
- else if (str_equals(name, len, "haversine"))
253
+ } else if (str_equals(name, len, "haversine")) {
254
+ if (dimensions != 2 && dimensions != 0)
255
+ throw std::invalid_argument("The number of dimensions must be equal to two");
247
256
  return index_at::haversine(accuracy, config);
248
- else
249
- throw std::runtime_error("Unknown distance, choose: l2_sq, ip, cos, hamming, jaccard");
257
+ } else
258
+ throw std::invalid_argument("Unknown distance, choose: l2_sq, ip, cos, hamming, jaccard");
250
259
  return {};
251
260
  }
252
261
 
@@ -383,6 +392,7 @@ class auto_index_gt {
383
392
  std::size_t connectivity() const noexcept { return index_->connectivity(); }
384
393
  std::size_t size() const noexcept { return index_->size(); }
385
394
  std::size_t capacity() const noexcept { return index_->capacity(); }
395
+ void clear() noexcept { return index_->clear(); }
386
396
 
387
397
  isa_t acceleration() const noexcept { return acceleration_; }
388
398
  std::size_t concurrency() const noexcept {
package/src/bench.cpp CHANGED
@@ -346,6 +346,18 @@ void run_big_or_small(dataset_at& dataset, args_t const& args, config_t config)
346
346
  run_type_punned<auto_index_gt<std::size_t, neighbor_id_at>>(dataset, args, config);
347
347
  }
348
348
 
349
+ void report_alternative_setups() {
350
+ using set_member_t = std::uint32_t;
351
+ using sets_index_t = index_gt<jaccard_gt<set_member_t>, std::size_t, std::uint32_t, set_member_t>;
352
+ set_member_t set_a[] = {10, 12, 15};
353
+ set_member_t set_b[] = {11, 12, 15, 16};
354
+ sets_index_t index;
355
+ index.reserve(100);
356
+ index.add(10, span_gt<set_member_t const>{&set_a[0], 3});
357
+ index.add(11, span_gt<set_member_t const>{&set_b[0], 4});
358
+ exit(0);
359
+ }
360
+
349
361
  void report_expected_losses(persisted_dataset_gt<float, vector_id_t> const& dataset) {
350
362
 
351
363
  auto vec1 = dataset.vector(0);
@@ -433,6 +445,9 @@ int main(int argc, char** argv) {
433
445
  std::printf("-- Queries count: %zu\n", dataset.queries_count());
434
446
  std::printf("-- Neighbors per query: %zu\n", dataset.neighborhood_size());
435
447
 
448
+ report_alternative_setups();
449
+ report_expected_losses(dataset);
450
+
436
451
  config_t config;
437
452
  config.connectivity = args.connectivity;
438
453
  config.expansion_add = args.expansion_add;