usearch 0.18.6 → 0.19.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.
package/python/lib.cpp CHANGED
@@ -13,8 +13,9 @@
13
13
  * @copyright Copyright (c) 2023
14
14
  */
15
15
  #define PY_SSIZE_T_CLEAN
16
- #define NOMINMAX // Some of our dependencies call `std::max(x, y)`, which crashes Windows builds
17
- #include <thread>
16
+ #define NOMINMAX // Some of our dependencies call `std::max(x, y)`, which crashes Windows builds
17
+ #include <limits> // `std::numeric_limits`
18
+ #include <thread> // `std::thread`
18
19
 
19
20
  #include <pybind11/numpy.h>
20
21
  #include <pybind11/pybind11.h>
@@ -24,12 +25,23 @@
24
25
  using namespace unum::usearch;
25
26
  using namespace unum;
26
27
 
28
+ /**
29
+ * @brief The signature of the user-defined function.
30
+ * Can be just two array pointers, precompiled for a specific array length,
31
+ * or include one or two array sizes as 64-bit unsigned integers.
32
+ */
33
+ enum class metric_signature_t {
34
+ array_array_k = 0,
35
+ array_array_size_k,
36
+ array_size_array_size_k,
37
+ };
38
+
27
39
  namespace py = pybind11;
28
40
  using py_shape_t = py::array::ShapeContainer;
29
41
 
30
42
  using label_t = std::uint32_t;
31
43
  using distance_t = punned_distance_t;
32
- using metric_t = punned_metric_t;
44
+ using metric_t = index_punned_dense_metric_t;
33
45
  using id_t = std::uint32_t;
34
46
  using big_id_t = std::uint64_t;
35
47
 
@@ -50,7 +62,7 @@ struct punned_index_py_t : public punned_index_t {
50
62
 
51
63
  using set_member_t = std::uint32_t;
52
64
  using set_view_t = span_gt<set_member_t const>;
53
- using sparse_index_t = index_gt<jaccard_gt<set_member_t>, label_t, id_t, set_member_t>;
65
+ using sparse_index_t = index_gt<jaccard_gt<set_member_t>, label_t, id_t>;
54
66
 
55
67
  struct sparse_index_py_t : public sparse_index_t {
56
68
  using native_t = sparse_index_t;
@@ -63,32 +75,58 @@ struct sparse_index_py_t : public sparse_index_t {
63
75
  sparse_index_py_t(native_t&& base) : native_t(std::move(base)) {}
64
76
  };
65
77
 
66
- template <typename scalar_at> metric_t udf(std::size_t metric_uintptr) {
67
- return [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
68
- using metric_raw_t = punned_distance_t (*)(scalar_at const*, scalar_at const*);
69
- metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
70
- return metric_ptr((scalar_at const*)a.data(), (scalar_at const*)b.data());
71
- };
78
+ template <typename scalar_at>
79
+ metric_t udf(metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr) {
80
+ //
81
+ metric_t result;
82
+ result.kind_ = kind;
83
+ switch (signature) {
84
+ case metric_signature_t::array_array_k:
85
+ result.func_ = [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
86
+ using metric_raw_t = punned_distance_t (*)(scalar_at const*, scalar_at const*);
87
+ metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
88
+ return metric_ptr((scalar_at const*)a.data(), (scalar_at const*)b.data());
89
+ };
90
+ break;
91
+ case metric_signature_t::array_array_size_k:
92
+ result.func_ = [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
93
+ using metric_raw_t = punned_distance_t (*)(scalar_at const*, scalar_at const*, size_t);
94
+ metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
95
+ return metric_ptr((scalar_at const*)a.data(), (scalar_at const*)b.data(), a.size() / sizeof(scalar_at));
96
+ };
97
+ break;
98
+ case metric_signature_t::array_size_array_size_k:
99
+ result.func_ = [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
100
+ using metric_raw_t = punned_distance_t (*)(scalar_at const*, size_t, scalar_at const*, size_t);
101
+ metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
102
+ return metric_ptr( //
103
+ (scalar_at const*)a.data(), a.size() / sizeof(scalar_at), //
104
+ (scalar_at const*)b.data(), b.size() / sizeof(scalar_at));
105
+ };
106
+ break;
107
+ }
108
+ return result;
72
109
  }
73
110
 
74
- metric_t udf(std::size_t metric_uintptr, scalar_kind_t accuracy) {
111
+ metric_t udf(metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr, scalar_kind_t accuracy) {
75
112
  switch (accuracy) {
76
- case scalar_kind_t::f8_k: return udf<f8_bits_t>(metric_uintptr);
77
- case scalar_kind_t::f16_k: return udf<f16_t>(metric_uintptr);
78
- case scalar_kind_t::f32_k: return udf<f32_t>(metric_uintptr);
79
- case scalar_kind_t::f64_k: return udf<f64_t>(metric_uintptr);
113
+ case scalar_kind_t::f8_k: return udf<f8_bits_t>(kind, signature, metric_uintptr);
114
+ case scalar_kind_t::f16_k: return udf<f16_t>(kind, signature, metric_uintptr);
115
+ case scalar_kind_t::f32_k: return udf<f32_t>(kind, signature, metric_uintptr);
116
+ case scalar_kind_t::f64_k: return udf<f64_t>(kind, signature, metric_uintptr);
80
117
  default: return {};
81
118
  }
82
119
  }
83
120
 
84
- static punned_index_py_t make_index( //
85
- std::size_t dimensions, //
86
- scalar_kind_t scalar_kind, //
87
- metric_kind_t metric_kind, //
88
- std::size_t connectivity, //
89
- std::size_t expansion_add, //
90
- std::size_t expansion_search, //
91
- std::size_t metric_uintptr, //
121
+ static punned_index_py_t make_index( //
122
+ std::size_t dimensions, //
123
+ scalar_kind_t scalar_kind, //
124
+ metric_kind_t metric_kind, //
125
+ std::size_t connectivity, //
126
+ std::size_t expansion_add, //
127
+ std::size_t expansion_search, //
128
+ metric_signature_t metric_signature, //
129
+ std::uintptr_t metric_uintptr, //
92
130
  bool tune) {
93
131
 
94
132
  index_config_t config;
@@ -99,7 +137,8 @@ static punned_index_py_t make_index( //
99
137
 
100
138
  if (metric_uintptr)
101
139
  return punned_index_t::make( //
102
- dimensions, udf(metric_uintptr, scalar_kind), config, scalar_kind, expansion_add, expansion_search);
140
+ dimensions, udf(metric_kind, metric_signature, metric_uintptr, scalar_kind), config, scalar_kind,
141
+ expansion_add, expansion_search);
103
142
  else
104
143
  return punned_index_t::make(dimensions, metric_kind, config, scalar_kind, expansion_add, expansion_search);
105
144
  }
@@ -417,14 +456,18 @@ template <typename index_at> py::object get_member(index_at const& index, label_
417
456
  throw std::invalid_argument("Incompatible scalars in the query matrix!");
418
457
  }
419
458
 
420
- template <typename index_at> py::array_t<label_t> get_labels(index_at const& index) {
421
- std::size_t result_length = index.size();
422
- py::array_t<label_t> result_py(static_cast<Py_ssize_t>(result_length));
459
+ template <typename index_at> py::array_t<label_t> get_labels(index_at const& index, std::size_t limit) {
460
+ limit = std::min(index.size(), limit);
461
+ py::array_t<label_t> result_py(static_cast<Py_ssize_t>(limit));
423
462
  auto result_py1d = result_py.template mutable_unchecked<1>();
424
- index.export_labels(&result_py1d(0), result_length);
463
+ index.export_labels(&result_py1d(0), limit);
425
464
  return result_py;
426
465
  }
427
466
 
467
+ template <typename index_at> py::array_t<label_t> get_all_labels(index_at const& index) {
468
+ return get_labels(index, index.size());
469
+ }
470
+
428
471
  template <typename element_at> bool has_duplicates(element_at const* begin, element_at const* end) {
429
472
  if (begin == end)
430
473
  return false;
@@ -539,6 +582,11 @@ PYBIND11_MODULE(compiled, m) {
539
582
  m.attr("USES_SIMSIMD") = py::int_(USEARCH_USE_SIMSIMD);
540
583
  m.attr("USES_NATIVE_F16") = py::int_(USEARCH_USE_NATIVE_F16);
541
584
 
585
+ py::enum_<metric_signature_t>(m, "MetricSignature")
586
+ .value("ArrayArray", metric_signature_t::array_array_k)
587
+ .value("ArrayArraySize", metric_signature_t::array_array_size_k)
588
+ .value("ArraySizeArraySize", metric_signature_t::array_size_array_size_k);
589
+
542
590
  py::enum_<metric_kind_t>(m, "MetricKind")
543
591
  .value("Unknown", metric_kind_t::unknown_k)
544
592
  .value("IP", metric_kind_t::ip_k)
@@ -558,18 +606,44 @@ PYBIND11_MODULE(compiled, m) {
558
606
  .value("F8", scalar_kind_t::f8_k)
559
607
  .value("B1", scalar_kind_t::b1x8_k);
560
608
 
609
+ auto h = py::class_<file_head_result_t>(m, "IndexMetadata");
610
+ h.def(py::init([](std::string const& path) {
611
+ file_head_result_t h = index_metadata(path.c_str());
612
+ h.error.raise();
613
+ return h;
614
+ }));
615
+ h.def_property_readonly("version", [](file_head_result_t const& h) {
616
+ return //
617
+ std::to_string(h.version_major) + "." + //
618
+ std::to_string(h.version_minor) + "." + //
619
+ std::to_string(h.version_patch);
620
+ });
621
+ h.def_readonly("metric", &file_head_result_t::metric);
622
+ h.def_readonly("connectivity", &file_head_result_t::connectivity);
623
+ h.def_readonly("max_level", &file_head_result_t::max_level);
624
+ h.def_readonly("vector_alignment", &file_head_result_t::vector_alignment);
625
+ h.def_readonly("bytes_per_label", &file_head_result_t::bytes_per_label);
626
+ h.def_readonly("bytes_per_id", &file_head_result_t::bytes_per_id);
627
+ h.def_readonly("scalar_kind", &file_head_result_t::scalar_kind);
628
+ h.def_readonly("size", &file_head_result_t::size);
629
+ h.def_readonly("entry_idx", &file_head_result_t::entry_idx);
630
+ h.def_readonly("bytes_for_graphs", &file_head_result_t::bytes_for_graphs);
631
+ h.def_readonly("bytes_for_vectors", &file_head_result_t::bytes_for_vectors);
632
+ h.def_readonly("bytes_checksum", &file_head_result_t::bytes_checksum);
633
+
561
634
  auto i = py::class_<punned_index_py_t>(m, "Index");
562
635
 
563
- i.def(py::init(&make_index), //
564
- py::kw_only(), //
565
- py::arg("ndim") = 0, //
566
- py::arg("dtype") = scalar_kind_t::f32_k, //
567
- py::arg("metric") = metric_kind_t::ip_k, //
568
- py::arg("connectivity") = default_connectivity(), //
569
- py::arg("expansion_add") = default_expansion_add(), //
570
- py::arg("expansion_search") = default_expansion_search(), //
571
- py::arg("metric_pointer") = 0, //
572
- py::arg("tune") = false //
636
+ i.def(py::init(&make_index), //
637
+ py::kw_only(), //
638
+ py::arg("ndim") = 0, //
639
+ py::arg("dtype") = scalar_kind_t::f32_k, //
640
+ py::arg("metric") = metric_kind_t::ip_k, //
641
+ py::arg("connectivity") = default_connectivity(), //
642
+ py::arg("expansion_add") = default_expansion_add(), //
643
+ py::arg("expansion_search") = default_expansion_search(), //
644
+ py::arg("metric_signature") = metric_signature_t::array_array_k, //
645
+ py::arg("metric_pointer") = 0, //
646
+ py::arg("tune") = false //
573
647
  );
574
648
 
575
649
  i.def( //
@@ -604,7 +678,8 @@ PYBIND11_MODULE(compiled, m) {
604
678
  i.def_property_readonly("connectivity", &punned_index_py_t::connectivity);
605
679
  i.def_property_readonly("capacity", &punned_index_py_t::capacity);
606
680
  i.def_property_readonly( //
607
- "dtype", [](punned_index_py_t const& index) -> std::string { return scalar_kind_name(index.scalar_kind()); });
681
+ "dtype",
682
+ [](punned_index_py_t const& index) -> std::string { return scalar_kind_name(index.metric().scalar_kind_); });
608
683
  i.def_property_readonly( //
609
684
  "memory_usage", [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); });
610
685
 
@@ -612,7 +687,8 @@ PYBIND11_MODULE(compiled, m) {
612
687
  i.def_property("expansion_search", &punned_index_py_t::expansion_search,
613
688
  &punned_index_py_t::change_expansion_search);
614
689
 
615
- i.def_property_readonly("labels", &get_labels<punned_index_py_t>);
690
+ i.def_property_readonly("labels", &get_all_labels<punned_index_py_t>);
691
+ i.def("get_labels", &get_labels<punned_index_py_t>, py::arg("limit") = std::numeric_limits<std::size_t>::max());
616
692
  i.def("__contains__", &punned_index_py_t::contains);
617
693
  i.def( //
618
694
  "__getitem__", &get_member<punned_index_py_t>, py::arg("label"), py::arg("dtype") = scalar_kind_t::f32_k);
@@ -6,7 +6,7 @@
6
6
  "metadata": {},
7
7
  "outputs": [],
8
8
  "source": [
9
- "!pip install pandas nbformat jinja2 kaleido tqdm sklearn scikit-learn scipy matplotlib"
9
+ "!pip install faiss-cpu numba pandas plotly nbformat jinja2 kaleido tqdm sklearn scikit-learn scipy matplotlib"
10
10
  ]
11
11
  },
12
12
  {
@@ -75,7 +75,7 @@
75
75
  "outputs": [],
76
76
  "source": [
77
77
  "eval_default = Evaluation.for_dataset(data)\n",
78
- "results_default = bench_speed(eval_default)\n",
78
+ "results_default = bench_speed(eval_default, jit=True)\n",
79
79
  "results_default"
80
80
  ]
81
81
  },
@@ -95,7 +95,7 @@
95
95
  "outputs": [],
96
96
  "source": [
97
97
  "eval_sliced = Evaluation.for_dataset(data, batch_size=256)\n",
98
- "results_sliced = bench_speed(eval_sliced)\n",
98
+ "results_sliced = bench_speed(eval_sliced, jit=True)\n",
99
99
  "results_sliced"
100
100
  ]
101
101
  },
@@ -161,7 +161,7 @@
161
161
  "name": "python",
162
162
  "nbconvert_exporter": "python",
163
163
  "pygments_lexer": "ipython3",
164
- "version": "3.11.3"
164
+ "version": "3.10.10"
165
165
  },
166
166
  "orig_nbformat": 4
167
167
  },
@@ -21,15 +21,16 @@ def bench_speed(
21
21
  connectivity: int = DEFAULT_CONNECTIVITY,
22
22
  expansion_add: int = DEFAULT_EXPANSION_ADD,
23
23
  expansion_search: int = DEFAULT_EXPANSION_SEARCH,
24
+ jit: bool = False,
24
25
  train: bool = False,
25
26
  ) -> pd.DataFrame:
26
-
27
27
  # Build various indexes:
28
28
  indexes = []
29
29
  jit_options = [False]
30
- dtype_options = ['f32', 'f16', 'f8']
30
+ if jit:
31
+ jit_options.append(jit)
32
+ dtype_options = ["f32", "f16", "f8"]
31
33
  for jit, dtype in itertools.product(jit_options, dtype_options):
32
-
33
34
  index = Index(
34
35
  ndim=eval.ndim,
35
36
  jit=jit,
@@ -37,48 +38,71 @@ def bench_speed(
37
38
  expansion_add=expansion_add,
38
39
  expansion_search=expansion_search,
39
40
  connectivity=connectivity,
40
- path='USearch' + ['', '+JIT'][jit] + ':' + dtype,
41
+ path="USearch" + ["", "+JIT"][jit] + ":" + dtype,
41
42
  )
43
+
44
+ # Skip the cases, where JIT-ing is impossible
45
+ if jit and not index.jit:
46
+ continue
42
47
  indexes.append(index)
43
48
 
44
49
  # Add FAISS indexes to the mix:
45
50
  try:
46
51
  from index_faiss import IndexFAISS, IndexQuantizedFAISS
47
- indexes.append(IndexFAISS(
48
- ndim=eval.ndim,
49
- expansion_add=expansion_add,
50
- expansion_search=expansion_search,
51
- connectivity=connectivity,
52
- path='FAISS:f32',
53
- ))
54
- if train:
55
- indexes.append(IndexQuantizedFAISS(
56
- train=eval.tasks[0].vectors,
52
+
53
+ indexes.append(
54
+ IndexFAISS(
55
+ ndim=eval.ndim,
57
56
  expansion_add=expansion_add,
58
57
  expansion_search=expansion_search,
59
58
  connectivity=connectivity,
60
- path='FAISS+IVFPQ:f32',
61
- ))
59
+ path="FAISS:f32",
60
+ )
61
+ )
62
+ if train:
63
+ indexes.append(
64
+ IndexQuantizedFAISS(
65
+ train=eval.tasks[0].vectors,
66
+ expansion_add=expansion_add,
67
+ expansion_search=expansion_search,
68
+ connectivity=connectivity,
69
+ path="FAISS+IVFPQ:f32",
70
+ )
71
+ )
62
72
  except (ImportError, ModuleNotFoundError):
63
73
  pass
64
74
 
65
75
  # Time to evaluate:
66
76
  results = [eval(index) for index in indexes]
67
- return pd.DataFrame({
68
- 'names': [i.path for i in indexes],
69
- 'add_per_second': [x['add_per_second'] for x in results],
70
- 'search_per_second': [x['search_per_second'] for x in results],
71
- 'recall_at_one': [x['recall_at_one'] for x in results],
72
- })
77
+ return pd.DataFrame(
78
+ {
79
+ "names": [i.path for i in indexes],
80
+ "add_per_second": [x["add_per_second"] for x in results],
81
+ "search_per_second": [x["search_per_second"] for x in results],
82
+ "recall_at_one": [x["recall_at_one"] for x in results],
83
+ }
84
+ )
73
85
 
74
86
 
75
87
  def bench_params(
76
88
  count: int = 1_000_000,
77
89
  connectivities: int = range(10, 20),
78
90
  dimensions: List[int] = [
79
- 2, 3, 4, 8, 16, 32, 96, 100,
80
- 256, 384, 512, 768, 1024, 1536],
81
-
91
+ 2,
92
+ 3,
93
+ 4,
94
+ 8,
95
+ 16,
96
+ 32,
97
+ 96,
98
+ 100,
99
+ 256,
100
+ 384,
101
+ 512,
102
+ 768,
103
+ 1024,
104
+ 1536,
105
+ ],
82
106
  expansion_add: int = DEFAULT_EXPANSION_ADD,
83
107
  expansion_search: int = DEFAULT_EXPANSION_SEARCH,
84
108
  ) -> pd.DataFrame:
@@ -90,7 +114,6 @@ def bench_params(
90
114
 
91
115
  results = []
92
116
  for connectivity, ndim in itertools.product(connectivities, dimensions):
93
-
94
117
  task = AddTask(
95
118
  labels=np.arange(count, dtype=Label),
96
119
  vectors=np.random.rand(count, ndim).astype(np.float32),
@@ -102,8 +125,8 @@ def bench_params(
102
125
  expansion_search=expansion_search,
103
126
  )
104
127
  result = asdict(task(index))
105
- result['ndim'] = dimensions
106
- result['connectivity'] = connectivity
128
+ result["ndim"] = dimensions
129
+ result["connectivity"] = connectivity
107
130
  results.append(result)
108
131
 
109
132
  # return self._execute_tasks(
@@ -13,7 +13,6 @@ from usearch.index import (
13
13
 
14
14
 
15
15
  class IndexFAISS:
16
-
17
16
  def __init__(
18
17
  self,
19
18
  index: IndexHNSWFlat = None,
@@ -22,9 +21,9 @@ class IndexFAISS:
22
21
  expansion_add: int = DEFAULT_EXPANSION_ADD,
23
22
  expansion_search: int = DEFAULT_EXPANSION_SEARCH,
24
23
  path: Optional[os.PathLike] = None,
25
- *args, **kwargs,
24
+ *args,
25
+ **kwargs,
26
26
  ):
27
-
28
27
  if index is None:
29
28
  index = IndexHNSWFlat(ndim, connectivity)
30
29
  index.hnsw.efConstruction = expansion_add
@@ -32,11 +31,11 @@ class IndexFAISS:
32
31
 
33
32
  self._faiss = index
34
33
  self._specs = {
35
- 'Class': 'usearch.IndexFAISS',
36
- 'Dimensions': ndim,
37
- 'Connectivity': connectivity,
38
- 'Expansion@Add': expansion_add,
39
- 'Expansion@Search': expansion_search,
34
+ "Class": "usearch.IndexFAISS",
35
+ "Dimensions": ndim,
36
+ "Connectivity": connectivity,
37
+ "Expansion@Add": expansion_add,
38
+ "Expansion@Search": expansion_search,
40
39
  }
41
40
 
42
41
  self.path = path
@@ -58,9 +57,11 @@ class IndexFAISS:
58
57
 
59
58
  @property
60
59
  def specs(self) -> dict:
61
- self._specs.update({
62
- 'Size': len(self),
63
- })
60
+ self._specs.update(
61
+ {
62
+ "Size": len(self),
63
+ }
64
+ )
64
65
  return self._specs
65
66
 
66
67
  def load(self, path: os.PathLike):
@@ -68,33 +69,32 @@ class IndexFAISS:
68
69
 
69
70
 
70
71
  class IndexQuantizedFAISS(IndexFAISS):
71
-
72
72
  def __init__(
73
73
  self,
74
74
  train: np.ndarray,
75
75
  connectivity: int = DEFAULT_CONNECTIVITY,
76
76
  expansion_add: int = DEFAULT_EXPANSION_ADD,
77
77
  expansion_search: int = DEFAULT_EXPANSION_SEARCH,
78
- *args, **kwargs,
78
+ *args,
79
+ **kwargs,
79
80
  ):
80
-
81
81
  ndim = train.shape[1]
82
82
  super().__init__(
83
83
  ndim=ndim,
84
84
  connectivity=connectivity,
85
85
  expansion_add=expansion_add,
86
86
  expansion_search=expansion_search,
87
- *args, **kwargs,
87
+ *args,
88
+ **kwargs,
88
89
  )
89
90
 
90
91
  nlist = 10000 # Number of inverted lists (number of partitions).
91
92
  nsegment = 16 # Number of segments for PQ (number of subquantizers).
92
- nbit = 8 # Number of bits to encode each segment.
93
+ nbit = 8 # Number of bits to encode each segment.
93
94
 
94
95
  self._original_faiss = self._faiss
95
- self._faiss = IndexIVFPQ(
96
- self._faiss, ndim, nlist, nsegment, nbit)
96
+ self._faiss = IndexIVFPQ(self._faiss, ndim, nlist, nsegment, nbit)
97
97
 
98
98
  self._faiss.train(train)
99
99
  self._faiss.nprobe = 10
100
- self._specs['Class'] = 'usearch.IndexQuantizedFAISS'
100
+ self._specs["Class"] = "usearch.IndexQuantizedFAISS"