usearch 0.13.0 → 0.14.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.
@@ -28,7 +28,7 @@ jobs:
28
28
  - run: npm install --save-dev @semantic-release/exec @semantic-release/git conventional-changelog-eslint semantic-release && npx semantic-release
29
29
 
30
30
  rebase:
31
- name: Rebase Developmtn Branch
31
+ name: Rebase Development Branch
32
32
  needs: versioning
33
33
  runs-on: ubuntu-latest
34
34
  steps:
@@ -41,11 +41,14 @@ jobs:
41
41
  run: |
42
42
  git checkout main-dev
43
43
  git rebase main
44
+
44
45
  - name: Push changes
45
- uses: ad-m/github-push-action@master
46
+ uses: CasperWA/push-protected@v2
46
47
  with:
47
- github_token: ${{ secrets.SEMANTIC_REBASE_TOKEN }}
48
+ token: ${{ secrets.SEMANTIC_REBASE_TOKEN }}
48
49
  branch: main-dev
50
+ unprotect_reviews: True
51
+ force: True
49
52
 
50
53
  build_wheels:
51
54
  name: Build Wheels for ${{ matrix.os }}
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "0.13.0"
3
+ version = "0.14.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/conanfile.py CHANGED
@@ -4,7 +4,7 @@ from conans import ConanFile
4
4
  class USearchConan(ConanFile):
5
5
 
6
6
  name = 'USearch'
7
- version = '0.13.0'
7
+ version = '0.14.0'
8
8
  license = 'Apache 2.0'
9
9
  url = 'https://github.com/unum-cloud/usearch'
10
10
  description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
@@ -94,6 +94,7 @@ Checking the effect of different embedding dimensions on construction speed:
94
94
  ```sh
95
95
  python python/scripts/bench_params.py dimensions
96
96
  python python/scripts/bench_params.py connectivity
97
+ python python/scripts/bench_params.py robustness
97
98
  ```
98
99
 
99
100
  ## JavaScript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.13.0",
3
+ "version": "0.14.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
@@ -347,8 +347,8 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
347
347
  py::array_t<label_t> labels_py(static_cast<Py_ssize_t>(wanted));
348
348
  py::array_t<distance_t> distances_py(static_cast<Py_ssize_t>(wanted));
349
349
  std::size_t count{};
350
- auto labels_py1d = labels_py.mutable_unchecked<1>();
351
- auto distances_py1d = distances_py.mutable_unchecked<1>();
350
+ auto labels_py1d = labels_py.template mutable_unchecked<1>();
351
+ auto distances_py1d = distances_py.template mutable_unchecked<1>();
352
352
 
353
353
  search_config_t config;
354
354
  config.exact = exact;
@@ -416,9 +416,9 @@ static py::tuple search_many_in_index( //
416
416
  py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
417
417
  py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
418
418
  py::array_t<Py_ssize_t> counts_py(vectors_count);
419
- auto labels_py2d = labels_py.mutable_unchecked<2>();
420
- auto distances_py2d = distances_py.mutable_unchecked<2>();
421
- auto counts_py1d = counts_py.mutable_unchecked<1>();
419
+ auto labels_py2d = labels_py.template mutable_unchecked<2>();
420
+ auto distances_py2d = distances_py.template mutable_unchecked<2>();
421
+ auto counts_py1d = counts_py.template mutable_unchecked<1>();
422
422
 
423
423
  // https://docs.python.org/3/library/struct.html#format-characters
424
424
  if (vectors_info.format == "c" || vectors_info.format == "b")
@@ -508,9 +508,9 @@ static py::tuple search_many_in_bits_index( //
508
508
  py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
509
509
  py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
510
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>();
511
+ auto labels_py2d = labels_py.template mutable_unchecked<2>();
512
+ auto distances_py2d = distances_py.template mutable_unchecked<2>();
513
+ auto counts_py1d = counts_py.template mutable_unchecked<1>();
514
514
 
515
515
  // https://docs.python.org/3/library/struct.html#format-characters
516
516
  if (vectors_info.format != "B")
@@ -548,6 +548,36 @@ template <typename index_at> std::size_t get_expansion_add(index_at const &index
548
548
  template <typename index_at> std::size_t get_expansion_search(index_at const &index) { return index.config().expansion_search; }
549
549
  // clang-format on
550
550
 
551
+ template <typename scalar_at, typename index_at>
552
+ py::array_t<scalar_at> get_typed_member(index_at const& index, label_t label) {
553
+ py::array_t<scalar_at> result_py(static_cast<Py_ssize_t>(index.dimensions()));
554
+ auto result_py1d = result_py.template mutable_unchecked<1>();
555
+ index.reconstruct(label, &result_py1d(0));
556
+ return result_py;
557
+ }
558
+
559
+ template <typename index_at>
560
+ py::array get_member(index_at const& index, label_t label, common_scalar_kind_t scalar_kind) {
561
+ if (scalar_kind == common_scalar_kind_t::f32_k)
562
+ return get_typed_member<f32_t>(index, label);
563
+ else if (scalar_kind == common_scalar_kind_t::f64_k)
564
+ return get_typed_member<f64_t>(index, label);
565
+ else if (scalar_kind == common_scalar_kind_t::f16_k)
566
+ return get_typed_member<f16_bits_t>(index, label);
567
+ else if (scalar_kind == common_scalar_kind_t::f8_k)
568
+ return get_typed_member<f8_bits_t>(index, label);
569
+ else
570
+ throw std::invalid_argument("Incompatible scalars in the query matrix!");
571
+ }
572
+
573
+ template <typename index_at> py::array_t<label_t> get_labels(index_at const& index) {
574
+ std::size_t result_length = index.size();
575
+ py::array_t<label_t> result_py(static_cast<Py_ssize_t>(result_length));
576
+ auto result_py1d = result_py.template mutable_unchecked<1>();
577
+ index.export_labels(&result_py1d(0), result_length);
578
+ return result_py;
579
+ }
580
+
551
581
  template <typename element_at> bool has_duplicates(element_at const* begin, element_at const* end) {
552
582
  if (begin == end)
553
583
  return false;
@@ -611,7 +641,7 @@ py::array_t<bits_numpy_word_t> hash_typed_buffer(py::buffer_info const& info, st
611
641
  Py_ssize_t rows_stride = info.strides[1];
612
642
  auto hashes_shape = py_shape_t{rows_count, words_per_row};
613
643
  auto hashes_py = py::array_t<bits_numpy_word_t>(hashes_shape);
614
- auto hashes_proxy = hashes_py.mutable_unchecked<2>();
644
+ auto hashes_proxy = hashes_py.template mutable_unchecked<2>();
615
645
 
616
646
  for (Py_ssize_t i = 0; i != rows_count; ++i)
617
647
  hash_typed_row( //
@@ -622,7 +652,7 @@ py::array_t<bits_numpy_word_t> hash_typed_buffer(py::buffer_info const& info, st
622
652
  } else {
623
653
  auto hashes_shape = py_shape_t{words_per_row};
624
654
  auto hashes_py = py::array_t<bits_numpy_word_t>(hashes_shape);
625
- auto hashes_proxy = hashes_py.mutable_unchecked<1>();
655
+ auto hashes_proxy = hashes_py.template mutable_unchecked<1>();
626
656
 
627
657
  hash_typed_row( //
628
658
  data, hashes_info.strides[0], static_cast<std::size_t>(elements_per_row), //
@@ -658,6 +688,18 @@ PYBIND11_MODULE(compiled, m) {
658
688
  m.attr("DEFAULT_EXPANSION_ADD") = py::int_(default_expansion_add());
659
689
  m.attr("DEFAULT_EXPANSION_SEARCH") = py::int_(default_expansion_search());
660
690
 
691
+ #if defined(USEARCH_USE_OPENMP)
692
+ m.attr("USES_OPENMP") = py::int_(1);
693
+ #else
694
+ m.attr("USES_OPENMP") = py::int_(0);
695
+ #endif
696
+
697
+ #if defined(USEARCH_USE_SIMSIMD)
698
+ m.attr("USES_SIMSIMD") = py::int_(1);
699
+ #else
700
+ m.attr("USES_SIMSIMD") = py::int_(0);
701
+ #endif
702
+
661
703
  py::enum_<common_metric_kind_t>(m, "MetricKind")
662
704
  .value("Unknown", common_metric_kind_t::unknown_k)
663
705
  .value("IP", common_metric_kind_t::ip_k)
@@ -670,6 +712,13 @@ PYBIND11_MODULE(compiled, m) {
670
712
  .value("BitwiseTanimoto", common_metric_kind_t::bitwise_tanimoto_k)
671
713
  .value("BitwiseSorensen", common_metric_kind_t::bitwise_sorensen_k);
672
714
 
715
+ py::enum_<common_scalar_kind_t>(m, "ScalarKind")
716
+ .value("F64", common_scalar_kind_t::f64_k)
717
+ .value("F32", common_scalar_kind_t::f32_k)
718
+ .value("F16", common_scalar_kind_t::f16_k)
719
+ .value("F8", common_scalar_kind_t::f8_k)
720
+ .value("B1", common_scalar_kind_t::b1_k);
721
+
673
722
  auto i = py::class_<punned_index_py_t>(m, "Index");
674
723
 
675
724
  i.def(py::init(&make_index), //
@@ -717,13 +766,19 @@ PYBIND11_MODULE(compiled, m) {
717
766
  i.def_property_readonly("capacity", &punned_index_py_t::capacity);
718
767
  i.def_property_readonly( //
719
768
  "dtype", [](punned_index_py_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
720
- i.def_property_readonly("memory_usage",
721
- [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); });
769
+ i.def_property_readonly( //
770
+ "memory_usage", [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); });
722
771
 
723
772
  i.def_property("expansion_add", &get_expansion_add<punned_index_py_t>, &punned_index_py_t::change_expansion_add);
724
773
  i.def_property("expansion_search", &get_expansion_search<punned_index_py_t>,
725
774
  &punned_index_py_t::change_expansion_search);
726
775
 
776
+ i.def_property_readonly("labels", &get_labels<punned_index_py_t>);
777
+ i.def("__contains__", &punned_index_py_t::contains);
778
+ i.def( //
779
+ "__getitem__", &get_member<punned_index_py_t>, py::arg("label"),
780
+ py::arg("dtype") = common_scalar_kind_t::f32_k);
781
+
727
782
  i.def("save", &save_index<punned_index_py_t>, py::arg("path"));
728
783
  i.def("load", &load_index<punned_index_py_t>, py::arg("path"));
729
784
  i.def("view", &view_index<punned_index_py_t>, py::arg("path"));
@@ -805,7 +860,7 @@ PYBIND11_MODULE(compiled, m) {
805
860
  auto proxy = set.unchecked<1>();
806
861
  auto view = set_view_t{&proxy(0), static_cast<std::size_t>(proxy.shape(0))};
807
862
  auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
808
- auto labels_proxy = labels_py.mutable_unchecked<1>();
863
+ auto labels_proxy = labels_py.template mutable_unchecked<1>();
809
864
  auto result = index.search(view, count);
810
865
  result.error.raise();
811
866
  auto found = result.dump_to(&labels_proxy(0));
@@ -7,6 +7,11 @@ import fire
7
7
 
8
8
  from usearch.index import Index
9
9
  from usearch.io import load_matrix
10
+ from usearch.index import (
11
+ DEFAULT_CONNECTIVITY,
12
+ DEFAULT_EXPANSION_ADD,
13
+ DEFAULT_EXPANSION_SEARCH,
14
+ )
10
15
 
11
16
 
12
17
  def measure(f) -> Tuple[float, Any]:
@@ -62,9 +67,9 @@ def main(
62
67
  vectors: str,
63
68
  queries: str,
64
69
  neighbors: str,
65
- connectivity: int = 16,
66
- expansion_add: int = 128,
67
- expansion_search: int = 64,
70
+ connectivity: int = DEFAULT_CONNECTIVITY,
71
+ expansion_add: int = DEFAULT_EXPANSION_ADD,
72
+ expansion_search: int = DEFAULT_EXPANSION_SEARCH,
68
73
  k: Optional[int] = None,
69
74
  ):
70
75
 
@@ -6,8 +6,15 @@ import plotly.express as px
6
6
  import numpy as np
7
7
  import pandas as pd
8
8
 
9
-
10
9
  from usearch.index import Index
10
+ from usearch.index import (
11
+ DEFAULT_CONNECTIVITY,
12
+ DEFAULT_EXPANSION_ADD,
13
+ DEFAULT_EXPANSION_SEARCH,
14
+
15
+ USES_OPENMP,
16
+ USES_SIMSIMD,
17
+ )
11
18
 
12
19
 
13
20
  def measure(f) -> float:
@@ -30,14 +37,23 @@ def bench_usearch(index, vectors: np.ndarray) -> float:
30
37
  return insertions_per_second
31
38
 
32
39
 
40
+ def recall_at_one(index: Index, vectors: np.ndarray) -> float:
41
+
42
+ labels = np.arange(vectors.shape[0])
43
+ index.add(labels=labels, vectors=vectors)
44
+ matches = index.search(vectors, 1)
45
+
46
+ return np.sum(matches.labels.flatten() == labels) / len(labels)
47
+
48
+
33
49
  class Main:
34
50
 
35
51
  def dimensions(
36
52
  self,
37
53
  count: int = 1_000_000,
38
- connectivity: int = 16,
39
- expansion_add: int = 128,
40
- expansion_search: int = 64,
54
+ connectivity: int = DEFAULT_CONNECTIVITY,
55
+ expansion_add: int = DEFAULT_EXPANSION_ADD,
56
+ expansion_search: int = DEFAULT_EXPANSION_SEARCH,
41
57
  ):
42
58
  """Measures indexing speed for different dimensionality vectors.
43
59
 
@@ -79,8 +95,8 @@ class Main:
79
95
  self,
80
96
  count: int = 1_000_000,
81
97
  ndim: int = 256,
82
- expansion_add: int = 128,
83
- expansion_search: int = 64,
98
+ expansion_add: int = DEFAULT_EXPANSION_ADD,
99
+ expansion_search: int = DEFAULT_EXPANSION_SEARCH,
84
100
  ):
85
101
  """Measures indexing speed and accuracy for different level of
86
102
  connectivity in the levels of the hierarchical proximity graph.
@@ -121,14 +137,14 @@ class Main:
121
137
 
122
138
  def robustness(
123
139
  self,
124
- experiments: int = 10,
125
- count: int = 1_000_000,
140
+ experiments: int = 1,
141
+ count: int = 10000,
126
142
  ndim: int = 256,
127
- connectivity: int = 16,
128
- expansion_add: int = 128,
129
- expansion_search: int = 64,
143
+ connectivity: int = DEFAULT_CONNECTIVITY,
144
+ expansion_add: int = DEFAULT_EXPANSION_ADD,
145
+ expansion_search: int = DEFAULT_EXPANSION_SEARCH,
130
146
  ):
131
- """How much does accuracy fluctuate depending on the indexing order.
147
+ """How much does accuracy and speed fluctuate depending on the indexing order.
132
148
 
133
149
  :param experiments: How many times to repeat, defaults to 10
134
150
  :type experiments: int, optional
@@ -138,11 +154,19 @@ class Main:
138
154
  :type ndim: int, optional
139
155
  """
140
156
 
157
+ print(f'Connectivity: {connectivity}')
158
+ print(f'Number of vectors: {count}')
159
+ print(f'Vector dimensions: {ndim}')
160
+ print(f'Expansion on Add: {expansion_add}')
161
+ print(f'Expansion on Search: {expansion_search}')
162
+ print(f'Uses OpenMP: {USES_OPENMP}')
163
+ print(f'Uses SimSIMD: {USES_SIMSIMD}')
164
+
141
165
  recall_levels = []
142
166
  vectors_mat = np.random.rand(count, ndim).astype(np.float32)
143
167
 
144
168
  for _ in range(experiments):
145
-
169
+ np.random.shuffle(vectors_mat)
146
170
  index = Index(
147
171
  ndim=ndim,
148
172
  expansion_add=expansion_add,
@@ -150,12 +174,28 @@ class Main:
150
174
  connectivity=connectivity,
151
175
  )
152
176
  bench_usearch(index, vectors_mat)
177
+ recall = recall_at_one(index, vectors_mat) * 100
178
+ print(f'- Recall @ 1 {recall:.2f} %')
179
+ recall_levels.append(recall)
153
180
  del index
154
181
 
155
- min_ = min(recall_levels)
156
- max_ = max(recall_levels)
157
- print(f'Recall@1 varies between {min_:.3f} and {max_:.3f}')
182
+ min_ = min(recall_levels) * 100
183
+ max_ = max(recall_levels) * 100
184
+ print(f'Recall @ 1 varies between {min_:.3f} and {max_:.3f}')
185
+
186
+ # Now let's try clustering and inserting in clusters
187
+ try:
188
+ from sklearn.cluster import KMeans
189
+ except ImportError:
190
+ return
191
+
192
+ count_clusters = count / expansion_add
193
+ clustering = KMeans(
194
+ n_clusters=count_clusters,
195
+ random_state=0, n_init='auto').fit(vectors_mat)
196
+ NamedTuple()
158
197
 
159
198
 
160
199
  if __name__ == '__main__':
200
+ Main().robustness()
161
201
  fire.Fire(Main)
@@ -3,14 +3,21 @@ import pytest
3
3
  import numpy as np
4
4
 
5
5
  from usearch.io import load_matrix, save_matrix
6
- from usearch.index import Index, SetsIndex, MetricKind
6
+ from usearch.synthetic import recall_members
7
+
8
+ from usearch.index import Index, SetsIndex, MetricKind, Matches
9
+ from usearch.index import (
10
+ DEFAULT_CONNECTIVITY,
11
+ DEFAULT_EXPANSION_ADD,
12
+ DEFAULT_EXPANSION_SEARCH,
13
+ )
7
14
 
8
15
 
9
16
  dimensions = [13, 99, 100, 256]
10
17
  batch_sizes = [1, 2, 100]
11
18
  index_types = ['f32', 'f64', 'f16', 'f8']
12
19
  numpy_types = [np.float32, np.float64, np.float16, np.byte]
13
- connectivity_options = [3, 13, 50]
20
+ connectivity_options = [3, 13, 50, DEFAULT_CONNECTIVITY]
14
21
  jit_options = [False]
15
22
  continuous_metrics = [
16
23
  MetricKind.Cos,
@@ -26,6 +33,7 @@ hash_metrics = [
26
33
  @pytest.mark.parametrize('rows', batch_sizes)
27
34
  @pytest.mark.parametrize('cols', dimensions)
28
35
  def test_serializing_fbin_matrix(rows: int, cols: int):
36
+
29
37
  original = np.random.rand(rows, cols).astype(np.float32)
30
38
  save_matrix(original, 'tmp.fbin')
31
39
  reconstructed = load_matrix('tmp.fbin')
@@ -36,6 +44,7 @@ def test_serializing_fbin_matrix(rows: int, cols: int):
36
44
  @pytest.mark.parametrize('rows', batch_sizes)
37
45
  @pytest.mark.parametrize('cols', dimensions)
38
46
  def test_serializing_ibin_matrix(rows: int, cols: int):
47
+
39
48
  original = np.random.randint(0, rows+1, size=(rows, cols)).astype(np.int32)
40
49
  save_matrix(original, 'tmp.ibin')
41
50
  reconstructed = load_matrix('tmp.ibin')
@@ -59,6 +68,8 @@ def test_index(
59
68
  ndim=ndim,
60
69
  dtype=index_type,
61
70
  connectivity=connectivity,
71
+ expansion_add=DEFAULT_EXPANSION_ADD,
72
+ expansion_search=DEFAULT_EXPANSION_SEARCH,
62
73
  jit=jit,
63
74
  )
64
75
  assert index.ndim == ndim
@@ -76,6 +87,11 @@ def test_index(
76
87
  assert matches[0] == 42
77
88
  assert distances[0] == pytest.approx(0, abs=1e-3)
78
89
 
90
+ assert 42 in index
91
+ assert 42 in index.labels
92
+ if numpy_type != np.byte:
93
+ assert np.allclose(index[42], vector, atol=0.1)
94
+
79
95
  index.save('tmp.usearch')
80
96
  index.clear()
81
97
  assert len(index) == 0
@@ -101,10 +117,13 @@ def test_index_batch(
101
117
  labels = np.array(range(batch_size), dtype=np.longlong)
102
118
 
103
119
  index.add(labels, vectors)
104
- matches, distances, count = index.search(vectors, 10)
120
+ matches: Matches = index.search(vectors, 10)
105
121
 
106
- assert matches.shape[0] == distances.shape[0]
107
- assert count.shape[0] == batch_size
122
+ assert matches.labels.shape[0] == matches.distances.shape[0]
123
+ assert matches.counts.shape[0] == batch_size
124
+ assert np.all(np.sort(index.labels) == np.sort(labels))
125
+
126
+ assert recall_members(index, exact=True) == 1
108
127
 
109
128
 
110
129
  @pytest.mark.parametrize('ndim', dimensions)
@@ -17,6 +17,9 @@ from usearch.compiled import (
17
17
  DEFAULT_CONNECTIVITY,
18
18
  DEFAULT_EXPANSION_ADD,
19
19
  DEFAULT_EXPANSION_SEARCH,
20
+
21
+ USES_OPENMP,
22
+ USES_SIMSIMD,
20
23
  )
21
24
 
22
25
  BitwiseMetricKind = (
@@ -65,6 +68,10 @@ class Matches(NamedTuple):
65
68
  for label, distance in zip(labels, distances)
66
69
  ]
67
70
 
71
+ def recall_first(self, expected: np.ndarray) -> float:
72
+ best_matches = self.labels if self.is_batch else self.labels[:, 0]
73
+ return np.sum(best_matches == expected) / len(expected)
74
+
68
75
  def __repr__(self) -> str:
69
76
  return f'usearch.Matches({self.total_matches})' if self.is_batch else \
70
77
  f'usearch.Matches({self.total_matches} across {self.batch_size} queries)'
@@ -344,7 +351,7 @@ class Index:
344
351
  return Matches(*tuple_)
345
352
 
346
353
  def __len__(self) -> int:
347
- return len(self._compiled)
354
+ return self._compiled.__len__()
348
355
 
349
356
  @property
350
357
  def jit(self) -> bool:
@@ -387,11 +394,11 @@ class Index:
387
394
  return self._compiled.expansion_search
388
395
 
389
396
  @expansion_add.setter
390
- def change_expansion_add(self, v: int):
397
+ def expansion_add(self, v: int):
391
398
  self._compiled.expansion_add = v
392
399
 
393
400
  @expansion_search.setter
394
- def change_expansion_search(self, v: int):
401
+ def expansion_search(self, v: int):
395
402
  self._compiled.expansion_search = v
396
403
 
397
404
  def save(self, path: os.PathLike):
@@ -406,5 +413,15 @@ class Index:
406
413
  def clear(self):
407
414
  self._compiled.clear()
408
415
 
409
- def remove(self, label: int):
410
- pass
416
+ @property
417
+ def labels(self) -> np.ndarray:
418
+ return self._compiled.labels
419
+
420
+ def __delitem__(self, label: int):
421
+ raise NotImplementedError()
422
+
423
+ def __contains__(self, label: int) -> bool:
424
+ return self._compiled.__contains__(label)
425
+
426
+ def __getitem__(self, label: int) -> np.ndarray:
427
+ return self._compiled.__getitem__(label)
@@ -22,11 +22,10 @@ def vectors(index: Index, count: int) -> np.ndarray:
22
22
  return x
23
23
 
24
24
 
25
- def recall_at_one(index: Index, vectors: np.ndarray) -> float:
26
- empty_replica: Index = index.fork()
27
-
28
- labels = np.arange(vectors.shape[0])
29
- empty_replica.add(labels=labels, vectors=vectors)
30
- matches: Matches = empty_replica.search(empty_replica, 1)
31
-
32
- return np.sum(matches.labels.flatten() == labels) / len(labels)
25
+ def recall_members(index: Index, *args, **kwargs) -> float:
26
+ vectors: np.ndarray = np.vstack([
27
+ index[i] for i in range(len(index))
28
+ ])
29
+ labels: np.ndarray = np.arange(vectors.shape[0])
30
+ matches: Matches = index.search(vectors, 1, *args, **kwargs)
31
+ return matches.recall_first(labels)
package/src/punned.hpp CHANGED
@@ -33,6 +33,14 @@
33
33
  namespace unum {
34
34
  namespace usearch {
35
35
 
36
+ enum class common_scalar_kind_t {
37
+ f64_k,
38
+ f32_k,
39
+ f16_k,
40
+ f8_k,
41
+ b1_k,
42
+ };
43
+
36
44
  using byte_t = char;
37
45
  using punned_distance_t = float;
38
46
  using punned_metric_t = punned_distance_t (*)(byte_t const*, byte_t const*, std::size_t, std::size_t);
@@ -527,6 +535,17 @@ class punned_gt {
527
535
  return typed_->memory_usage(allocator_entry_bytes);
528
536
  }
529
537
 
538
+ bool contains(label_t label) const {
539
+ shared_lock_t lock(lookup_table_mutex_);
540
+ return lookup_table_.contains(label);
541
+ }
542
+
543
+ void export_labels(label_t* labels, std::size_t limit) const {
544
+ shared_lock_t lock(lookup_table_mutex_);
545
+ for (auto it = lookup_table_.begin(); it != lookup_table_.end() && limit; ++it, ++labels, --limit)
546
+ *labels = it->first;
547
+ }
548
+
530
549
  // clang-format off
531
550
  add_result_t add(label_t label, f8_bits_t const* vector) { return add_(label, vector, casts_.from_f8); }
532
551
  add_result_t add(label_t label, f16_bits_t const* vector) { return add_(label, vector, casts_.from_f16); }