usearch 0.20.0 → 0.21.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.
@@ -170,6 +170,7 @@ class index_punned_dense_gt {
170
170
  using search_result_t = typename index_t::search_result_t;
171
171
  using add_result_t = typename index_t::add_result_t;
172
172
  using serialization_result_t = typename index_t::serialization_result_t;
173
+ using join_result_t = typename index_t::join_result_t;
173
174
  using stats_t = typename index_t::stats_t;
174
175
 
175
176
  index_punned_dense_gt() = default;
@@ -184,12 +185,15 @@ class index_punned_dense_gt {
184
185
  void swap(index_punned_dense_gt& other) {
185
186
  std::swap(dimensions_, other.dimensions_);
186
187
  std::swap(scalar_words_, other.scalar_words_);
188
+ std::swap(expansion_add_, other.expansion_add_);
189
+ std::swap(expansion_search_, other.expansion_search_);
187
190
  std::swap(casted_vector_bytes_, other.casted_vector_bytes_);
188
191
  std::swap(typed_, other.typed_);
189
192
  std::swap(cast_buffer_, other.cast_buffer_);
190
193
  std::swap(casts_, other.casts_);
191
194
  std::swap(root_metric_, other.root_metric_);
192
195
  std::swap(available_threads_, other.available_threads_);
196
+ std::swap(lookup_table_, other.lookup_table_);
193
197
  }
194
198
 
195
199
  static index_config_t optimize(index_config_t config) { return index_t::optimize(config); }
@@ -199,6 +203,7 @@ class index_punned_dense_gt {
199
203
  std::size_t connectivity() const { return typed_->connectivity(); }
200
204
  std::size_t size() const { return typed_->size(); }
201
205
  std::size_t capacity() const { return typed_->capacity(); }
206
+ std::size_t max_level() const noexcept { return typed_->max_level(); }
202
207
  index_config_t const& config() const { return typed_->config(); }
203
208
  index_limits_t const& limits() const { return typed_->limits(); }
204
209
  void clear() { return typed_->clear(); }
@@ -323,23 +328,76 @@ class index_punned_dense_gt {
323
328
  metric_t(metric), make_casts_(accuracy));
324
329
  }
325
330
 
326
- index_punned_dense_gt fork() const {
327
- index_punned_dense_gt result;
331
+ struct copy_result_t {
332
+ index_punned_dense_gt index;
333
+ error_t error;
334
+
335
+ explicit operator bool() const noexcept { return !error; }
336
+ copy_result_t failed(error_t message) noexcept {
337
+ error = std::move(message);
338
+ return std::move(*this);
339
+ }
340
+ };
341
+
342
+ copy_result_t copy(copy_config_t config = {}) const {
343
+ copy_result_t result = fork();
344
+ if (!result)
345
+ return result;
346
+ auto typed_result = typed_->copy(config);
347
+ if (!typed_result)
348
+ return result.failed(std::move(typed_result.error));
349
+ *result.index.typed_ = std::move(typed_result.index);
350
+ result.index.lookup_table_ = lookup_table_;
351
+ return result;
352
+ }
328
353
 
329
- result.dimensions_ = dimensions_;
330
- result.scalar_words_ = scalar_words_;
331
- result.casted_vector_bytes_ = casted_vector_bytes_;
332
- result.cast_buffer_ = cast_buffer_;
333
- result.casts_ = casts_;
354
+ copy_result_t fork() const {
355
+ copy_result_t result;
356
+ index_punned_dense_gt& other = result.index;
357
+
358
+ other.dimensions_ = dimensions_;
359
+ other.scalar_words_ = scalar_words_;
360
+ other.expansion_add_ = expansion_add_;
361
+ other.expansion_search_ = expansion_search_;
362
+ other.casted_vector_bytes_ = casted_vector_bytes_;
363
+ other.cast_buffer_ = cast_buffer_;
364
+ other.casts_ = casts_;
365
+
366
+ other.root_metric_ = root_metric_;
367
+ other.available_threads_ = available_threads_;
334
368
 
335
- result.root_metric_ = root_metric_;
336
369
  index_t* raw = index_allocator_t{}.allocate(1);
337
- new (raw) index_t(config(), root_metric_);
338
- result.typed_ = raw;
370
+ if (!raw)
371
+ return result.failed("Can't allocate the index");
339
372
 
373
+ new (raw) index_t(config(), root_metric_);
374
+ other.typed_ = raw;
340
375
  return result;
341
376
  }
342
377
 
378
+ template < //
379
+ typename first_to_second_at = dummy_label_to_label_mapping_t, //
380
+ typename second_to_first_at = dummy_label_to_label_mapping_t, //
381
+ typename executor_at = dummy_executor_t, //
382
+ typename progress_at = dummy_progress_t //
383
+ >
384
+ static join_result_t join( //
385
+ index_punned_dense_gt const& first, //
386
+ index_punned_dense_gt const& second, //
387
+ join_config_t config = {}, //
388
+ first_to_second_at&& first_to_second = first_to_second_at{}, //
389
+ second_to_first_at&& second_to_first = second_to_first_at{}, //
390
+ executor_at&& executor = executor_at{}, //
391
+ progress_at&& progress = progress_at{}) noexcept {
392
+
393
+ return index_t::join( //
394
+ *first.typed_, *second.typed_, config, //
395
+ std::forward<first_to_second_at>(first_to_second), //
396
+ std::forward<second_to_first_at>(second_to_first), //
397
+ std::forward<executor_at>(executor), //
398
+ std::forward<progress_at>(progress));
399
+ }
400
+
343
401
  private:
344
402
  struct thread_lock_t {
345
403
  index_punned_dense_gt const& parent;
@@ -313,9 +313,11 @@ class executor_stl_t {
313
313
  std::size_t threads_count_{};
314
314
 
315
315
  public:
316
- executor_stl_t(std::size_t threads_count) noexcept
316
+ executor_stl_t(std::size_t threads_count = 0) noexcept
317
317
  : threads_count_(threads_count ? threads_count : std::thread::hardware_concurrency()) {}
318
318
 
319
+ std::size_t size() const noexcept { return threads_count_; }
320
+
319
321
  template <typename thread_aware_function_at>
320
322
  void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept(false) {
321
323
  std::vector<std::thread> threads_pool;
@@ -330,22 +332,39 @@ class executor_stl_t {
330
332
  for (std::size_t thread_idx = 0; thread_idx != threads_count_; ++thread_idx)
331
333
  threads_pool[thread_idx].join();
332
334
  }
335
+
336
+ template <typename thread_aware_function_at>
337
+ void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept(false) {
338
+ std::vector<std::thread> threads_pool;
339
+ for (std::size_t thread_idx = 0; thread_idx != threads_count_; ++thread_idx)
340
+ threads_pool.emplace_back([=]() { thread_aware_function(thread_idx); });
341
+ for (std::size_t thread_idx = 0; thread_idx != threads_count_; ++thread_idx)
342
+ threads_pool[thread_idx].join();
343
+ }
333
344
  };
334
345
 
335
346
  #if USEARCH_USE_OPENMP
336
347
 
337
348
  class executor_openmp_t {
338
349
  public:
339
- executor_openmp_t(std::size_t threads_count) noexcept {
350
+ executor_openmp_t(std::size_t threads_count = 0) noexcept {
340
351
  omp_set_num_threads(threads_count ? threads_count : std::thread::hardware_concurrency());
341
352
  }
342
353
 
354
+ std::size_t size() const noexcept { return omp_get_num_threads(); }
355
+
343
356
  template <typename thread_aware_function_at>
344
357
  void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept(false) {
345
358
  #pragma omp parallel for schedule(dynamic)
346
- for (std::size_t i = 0; i < tasks; ++i)
359
+ for (std::size_t i = 0; i != tasks; ++i)
347
360
  thread_aware_function(omp_get_thread_num(), i);
348
361
  }
362
+
363
+ template <typename thread_aware_function_at>
364
+ void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept(false) {
365
+ #pragma omp parallel
366
+ { thread_aware_function(omp_get_thread_num()); }
367
+ }
349
368
  };
350
369
 
351
370
  using executor_default_t = executor_openmp_t;
@@ -451,6 +470,12 @@ template <std::size_t alignment_ak = 1> class memory_mapping_allocator_gt {
451
470
  return *this;
452
471
  }
453
472
 
473
+ memory_mapping_allocator_gt(memory_mapping_allocator_gt const&) noexcept {}
474
+ memory_mapping_allocator_gt& operator=(memory_mapping_allocator_gt const&) noexcept {
475
+ reset();
476
+ return *this;
477
+ }
478
+
454
479
  ~memory_mapping_allocator_gt() noexcept { reset(); }
455
480
 
456
481
  inline byte_t* allocate(std::size_t count_bytes) noexcept {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.20.0",
3
+ "version": "0.21.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/lib.cpp CHANGED
@@ -18,6 +18,7 @@
18
18
  #define PY_SSIZE_T_CLEAN
19
19
  #include <pybind11/numpy.h>
20
20
  #include <pybind11/pybind11.h>
21
+ #include <pybind11/stl.h>
21
22
 
22
23
  #include <usearch/index_punned_dense.hpp>
23
24
 
@@ -356,7 +357,6 @@ static py::tuple search_many_in_index( //
356
357
 
357
358
  Py_ssize_t vectors_count = vectors_info.shape[0];
358
359
  Py_ssize_t vectors_dimensions = vectors_info.shape[1];
359
- char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
360
360
  if (vectors_dimensions != static_cast<Py_ssize_t>(index.scalar_words()))
361
361
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
362
362
 
@@ -381,10 +381,48 @@ static py::tuple search_many_in_index( //
381
381
  return results;
382
382
  }
383
383
 
384
+ static std::unordered_map<label_t, label_t> join_index( //
385
+ dense_index_py_t const& a, dense_index_py_t const& b, //
386
+ std::size_t max_proposals, bool exact) {
387
+
388
+ std::unordered_map<label_t, label_t> a_to_b;
389
+ a_to_b.reserve((std::min)(a.size(), b.size()));
390
+
391
+ using join_result_t = typename dense_index_py_t::join_result_t;
392
+ join_config_t config;
393
+
394
+ config.max_proposals = max_proposals;
395
+ config.exact = exact;
396
+ config.expansion = (std::max)(a.expansion_search(), b.expansion_search());
397
+ std::size_t threads = (std::min)(a.limits().threads(), b.limits().threads());
398
+ executor_default_t executor{threads};
399
+ join_result_t result = dense_index_py_t::join( //
400
+ a, b, config, //
401
+ a_to_b, //
402
+ dummy_label_to_label_mapping_t{}, //
403
+ executor);
404
+ result.error.raise();
405
+ return a_to_b;
406
+ }
407
+
408
+ static dense_index_py_t copy_index(dense_index_py_t const& index) {
409
+
410
+ using copy_result_t = typename dense_index_py_t::copy_result_t;
411
+ copy_config_t config;
412
+
413
+ copy_result_t result = index.copy();
414
+ result.error.raise();
415
+ return std::move(result.index);
416
+ }
417
+
384
418
  // clang-format off
385
419
  template <typename index_at> void save_index(index_at const& index, std::string const& path) { index.save(path.c_str()).error.raise(); }
386
420
  template <typename index_at> void load_index(index_at& index, std::string const& path) { index.load(path.c_str()).error.raise(); }
387
421
  template <typename index_at> void view_index(index_at& index, std::string const& path) { index.view(path.c_str()).error.raise(); }
422
+ template <typename index_at> void clear_index(index_at& index) { index.clear(); }
423
+ template <typename index_at> std::size_t max_level(index_at const &index) { return index.max_level(); }
424
+ template <typename index_at> typename index_at::stats_t compute_stats(index_at const &index) { return index.stats(); }
425
+ template <typename index_at> typename index_at::stats_t compute_level_stats(index_at const &index, std::size_t level) { return index.stats(level); }
388
426
  // clang-format on
389
427
 
390
428
  template <typename internal_at, typename external_at = internal_at, typename index_at = void>
@@ -650,7 +688,21 @@ PYBIND11_MODULE(compiled, m) {
650
688
  i.def("save", &save_index<dense_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
651
689
  i.def("load", &load_index<dense_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
652
690
  i.def("view", &view_index<dense_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
653
- i.def("clear", &dense_index_py_t::clear, py::call_guard<py::gil_scoped_release>());
691
+ i.def("clear", &clear_index<dense_index_py_t>, py::call_guard<py::gil_scoped_release>());
692
+ i.def("copy", &copy_index, py::call_guard<py::gil_scoped_release>());
693
+ i.def("join", &join_index, py::arg("other"), py::arg("max_proposals") = 0, py::arg("exact") = false,
694
+ py::call_guard<py::gil_scoped_release>());
695
+
696
+ using punned_index_stats_t = typename dense_index_py_t::stats_t;
697
+ auto i_stats = py::class_<punned_index_stats_t>(m, "IndexStats");
698
+ i_stats.def_readonly("nodes", &punned_index_stats_t::nodes);
699
+ i_stats.def_readonly("edges", &punned_index_stats_t::edges);
700
+ i_stats.def_readonly("max_edges", &punned_index_stats_t::max_edges);
701
+ i_stats.def_readonly("allocated_bytes", &punned_index_stats_t::allocated_bytes);
702
+
703
+ i.def_property_readonly("max_level", &max_level<dense_index_py_t>);
704
+ i.def_property_readonly("levels_stats", &compute_stats<dense_index_py_t>);
705
+ i.def("level_stats", &compute_level_stats<dense_index_py_t>, py::arg("level"));
654
706
 
655
707
  auto si = py::class_<sparse_index_py_t>(m, "SparseIndex");
656
708
 
@@ -704,8 +756,8 @@ PYBIND11_MODULE(compiled, m) {
704
756
  si.def_property_readonly("connectivity", &sparse_index_py_t::connectivity);
705
757
  si.def_property_readonly("capacity", &sparse_index_py_t::capacity);
706
758
 
707
- si.def("save", &sparse_index_py_t::save, py::arg("path"));
708
- si.def("load", &sparse_index_py_t::load, py::arg("path"));
709
- si.def("view", &sparse_index_py_t::view, py::arg("path"));
759
+ si.def("save", &save_index<sparse_index_py_t>, py::arg("path"));
760
+ si.def("load", &load_index<sparse_index_py_t>, py::arg("path"));
761
+ si.def("view", &view_index<sparse_index_py_t>, py::arg("path"));
710
762
  si.def("clear", &sparse_index_py_t::clear);
711
763
  }
@@ -0,0 +1,151 @@
1
+ """
2
+ wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/clip_images.fbin -P datasets/cc_3M/
3
+ wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/clip_texts.fbin -P datasets/cc_3M/
4
+ wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/images.fbin -P datasets/cc_3M/
5
+ wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/texts.fbin -P datasets/cc_3M/
6
+ wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/uform_english_images.fbin -P datasets/cc_3M/
7
+ wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/uform_english_texts.fbin -P datasets/cc_3M/
8
+ rm -rf datasets/cc_3M/*.usearch
9
+ python python/scripts/join.py
10
+ """
11
+ from numpy import dot
12
+ from numpy.linalg import norm
13
+ from tqdm import tqdm
14
+ from simsimd import cos_f32x4_neon, to_int
15
+
16
+ from usearch.index import Index, MetricKind, CompiledMetric, MetricSignature
17
+ from usearch.io import load_matrix
18
+ from usearch.eval import measure_seconds
19
+
20
+ k = 10
21
+ exact = False
22
+ batch_size = 1024 * 4
23
+ max_elements = 1000000
24
+
25
+ a_name = "cc_3M/texts"
26
+ b_name = "cc_3M/images"
27
+
28
+ a_mat = load_matrix(f"datasets/{a_name}.fbin", view=True)
29
+ b_mat = load_matrix(f"datasets/{b_name}.fbin", view=True)
30
+
31
+ a_mat = a_mat[:max_elements]
32
+ b_mat = b_mat[:max_elements]
33
+
34
+ print(f"Loaded two datasets of shape: {a_mat.shape}, {b_mat.shape}")
35
+ print("--------------------------------------")
36
+ print("---------------Indexing---------------")
37
+ print("--------------------------------------")
38
+
39
+ metric = CompiledMetric(
40
+ pointer=to_int(cos_f32x4_neon),
41
+ kind=MetricKind.Cos,
42
+ signature=MetricSignature.ArrayArraySize,
43
+ )
44
+
45
+ a = Index(
46
+ a_mat.shape[1],
47
+ metric=metric,
48
+ path=f"datasets/{a_name}.f32.usearch",
49
+ dtype="f32",
50
+ )
51
+ b = Index(
52
+ b_mat.shape[1],
53
+ metric=metric,
54
+ path=f"datasets/{b_name}.f32.usearch",
55
+ dtype="f32",
56
+ )
57
+
58
+ if len(a) != a_mat.shape[0]:
59
+ a.clear()
60
+ a.add(None, a_mat, log=True, batch_size=batch_size)
61
+ a.save()
62
+
63
+ if len(b) != b_mat.shape[0]:
64
+ b.clear()
65
+ b.add(None, b_mat, log=True, batch_size=batch_size)
66
+ b.save()
67
+
68
+
69
+ print(
70
+ f"Loaded two indexes of size: {len(a):,} for {a_name} and {len(b):,} for {b_name}"
71
+ )
72
+ min_elements = min(len(a), len(b))
73
+
74
+ run_diagnostics = input("Would you like to run diagnostics? [Y/n]: ")
75
+ if len(run_diagnostics) == 0 or run_diagnostics.lower() == "y":
76
+ print("--------------------------------------")
77
+ print("-------------Diagnostics--------------")
78
+ print("--------------------------------------")
79
+
80
+ mean_similarity = 0.0
81
+ mean_recovered_similarity = 0.0
82
+
83
+ for i in tqdm(range(min_elements), desc="Pairwise Similarity"):
84
+ a_vec = a_mat[i]
85
+ b_vec = b_mat[i]
86
+ cos_similarity = dot(a_vec, b_vec) / (norm(a_vec) * norm(b_vec))
87
+ mean_similarity += cos_similarity
88
+
89
+ a_vec = a[i]
90
+ b_vec = b[i]
91
+ cos_similarity = dot(a_vec, b_vec) / (norm(a_vec) * norm(b_vec))
92
+ mean_recovered_similarity += cos_similarity
93
+
94
+ mean_similarity /= min_elements
95
+ mean_recovered_similarity /= min_elements
96
+ print(
97
+ f"Average vector similarity is {mean_similarity:.4f} in original dataset, "
98
+ f"and {mean_recovered_similarity:.4f} in recovered state in index"
99
+ )
100
+
101
+ dt = measure_seconds
102
+ args = dict(
103
+ k=k,
104
+ batch_size=batch_size,
105
+ log=True,
106
+ exact=exact,
107
+ )
108
+
109
+ secs, a_self_recall = dt(lambda: a.search(a.vectors, **args).recall(a.labels))
110
+ print(
111
+ "Self-recall @{} of {} index: {:.2f}%, took {:.2f}s".format(
112
+ k, a_name, a_self_recall * 100, secs
113
+ )
114
+ )
115
+
116
+ secs, b_self_recall = dt(lambda: b.search(b.vectors, **args).recall(b.labels))
117
+ print(
118
+ "Self-recall @{} of {} index: {:.2f}%, took {:.2f}s".format(
119
+ k, b_name, b_self_recall * 100, secs
120
+ )
121
+ )
122
+
123
+ secs, ab_recall = dt(lambda: b.search(a.vectors, **args).recall(b.labels))
124
+ print(
125
+ "Cross-recall @{} of {} in {}: {:.2f}%, took {:.2f}s".format(
126
+ k, a_name, b_name, ab_recall * 100, secs
127
+ )
128
+ )
129
+
130
+ secs, ba_recall = dt(lambda: a.search(b.vectors, **args).recall(a.labels))
131
+ print(
132
+ "Cross-recall @{} of {} in {}: {:.2f}%, took {:.2f}s".format(
133
+ k, b_name, a_name, ba_recall * 100, secs
134
+ )
135
+ )
136
+
137
+
138
+ print("--------------------------------------")
139
+ print("-----------------Join-----------------")
140
+ print("--------------------------------------")
141
+
142
+ secs, bimapping = measure_seconds(lambda: a.join(b, max_proposals=100))
143
+ mapping_size = len(bimapping)
144
+ recall = 0
145
+ for i, j in bimapping.items():
146
+ recall += i == j
147
+
148
+ recall *= 100.0 / min_elements
149
+ print(
150
+ f"Took {secs:.2f}s to find {mapping_size:,} pairings with {recall:.2f}% being exact"
151
+ )
@@ -115,6 +115,11 @@ def test_index(
115
115
  assert matches[0] == 42
116
116
  assert distances[0] == pytest.approx(0, abs=1e-3)
117
117
 
118
+ assert index.max_level >= 0
119
+ assert index.levels_stats.nodes >= 1
120
+ assert index.level_stats(0).nodes == 1
121
+ assert str(index).startswith("usearch.index.Index")
122
+
118
123
  index.save("tmp.usearch")
119
124
  index.clear()
120
125
  assert len(index) == 0
@@ -127,6 +132,10 @@ def test_index(
127
132
  assert len(index) == 1
128
133
  assert len(index[42]) == ndim
129
134
 
135
+ index_copy = index.copy()
136
+ assert len(index_copy) == 1
137
+ assert len(index_copy[42]) == ndim
138
+
130
139
  # Cleanup
131
140
  os.remove("tmp.usearch")
132
141
 
@@ -157,6 +166,10 @@ def test_index_batch(
157
166
  assert matches.counts.shape[0] == batch_size
158
167
  assert np.all(np.sort(index.labels) == np.sort(labels))
159
168
 
169
+ assert index.max_level >= 0 # TODO: This should be 1
170
+ assert index.levels_stats.nodes >= batch_size
171
+ assert index.level_stats(0).nodes == batch_size
172
+
160
173
  index.save("tmp.usearch")
161
174
  index.clear()
162
175
  assert len(index) == 0
@@ -205,6 +218,12 @@ def test_exact_recall(
205
218
  for i in range(batch_size):
206
219
  assert found_labels[i, 0] == i
207
220
 
221
+ # Match entries aginst themselves
222
+ index_copy: Index = index.copy()
223
+ mapping: dict = index.join(index_copy, exact=True)
224
+ for man, woman in mapping.items():
225
+ assert man == woman, "Stable marriage failed"
226
+
208
227
 
209
228
  @pytest.mark.parametrize("ndim", dimensions)
210
229
  @pytest.mark.parametrize("batch_size", batch_sizes)
@@ -3,6 +3,7 @@ from time import time_ns
3
3
  from typing import Tuple, Any, Callable, Union, Optional, List
4
4
  from dataclasses import dataclass, asdict
5
5
  from collections import defaultdict
6
+ from math import ceil
6
7
 
7
8
  import numpy as np
8
9
 
@@ -47,7 +48,7 @@ def random_vectors(
47
48
  return x
48
49
 
49
50
 
50
- def recall_members(index: Index, *args, **kwargs) -> float:
51
+ def recall_members(index: Index, sample: float = 1, **kwargs) -> float:
51
52
  """Simplest benchmark for a quality of search, which queries every
52
53
  existing member of the index, to make sure approximate search finds
53
54
  the point itself.
@@ -59,9 +60,16 @@ def recall_members(index: Index, *args, **kwargs) -> float:
59
60
  """
60
61
  if len(index) == 0:
61
62
  return 0
63
+ if "k" not in kwargs:
64
+ kwargs["k"] = 1
62
65
 
63
- matches: Matches = index.search(index.vectors, 1, *args, **kwargs)
64
- return matches.recall_first(index.labels)
66
+ labels = index.labels
67
+ if sample != 1:
68
+ labels = np.random.choice(labels, int(ceil(len(labels) * sample)))
69
+
70
+ queries = index.get_vectors(labels, index.dtype)
71
+ matches: Matches = index.search(queries, **kwargs)
72
+ return matches.recall_first(labels)
65
73
 
66
74
 
67
75
  def measure_seconds(f: Callable) -> Tuple[float, Any]: