usearch 0.13.0 → 0.15.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.
Files changed (64) hide show
  1. package/.github/workflows/release.yml +62 -7
  2. package/.github/workflows/update_version.sh +6 -1
  3. package/.vscode/launch.json +21 -22
  4. package/.vscode/settings.json +7 -1
  5. package/CITATION.cff +4 -4
  6. package/CMakeLists.txt +1 -1
  7. package/Cargo.toml +1 -1
  8. package/Package.swift +0 -1
  9. package/README.md +100 -27
  10. package/binding.gyp +0 -1
  11. package/build.gradle +1 -1
  12. package/build.rs +3 -3
  13. package/c/lib.cpp +32 -9
  14. package/c/usearch.h +67 -36
  15. package/conanfile.py +1 -1
  16. package/{src → cpp}/bench.cpp +52 -36
  17. package/{src → cpp}/test.cpp +17 -7
  18. package/docs/benchmarks.md +33 -10
  19. package/docs/c/index.rst +11 -0
  20. package/docs/c/reference.rst +7 -0
  21. package/docs/compilation.md +24 -8
  22. package/docs/conf.dox +6 -6
  23. package/docs/conf.py +3 -1
  24. package/docs/cpp/index.rst +11 -0
  25. package/docs/cpp/reference.rst +7 -0
  26. package/docs/index.rst +10 -0
  27. package/docs/java/index.rst +11 -0
  28. package/docs/java/reference.rst +7 -0
  29. package/docs/javascript/index.rst +11 -0
  30. package/docs/javascript/reference.rst +9 -0
  31. package/docs/python/index.rst +11 -0
  32. package/docs/python/reference.rst +35 -0
  33. package/golang/lib.go +1 -1
  34. package/include/usearch/{usearch.hpp → index.hpp} +152 -148
  35. package/include/usearch/index_punned_dense.hpp +633 -0
  36. package/include/usearch/index_punned_helpers.hpp +528 -0
  37. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +8 -7
  38. package/javascript/lib.cpp +26 -50
  39. package/javascript/usearch.js +117 -2
  40. package/objc/USearchObjective.mm +145 -49
  41. package/objc/include/USearchObjective.h +106 -30
  42. package/package.json +2 -2
  43. package/python/lib.cpp +178 -320
  44. package/python/scripts/bench.ipynb +170 -0
  45. package/python/scripts/bench.py +102 -185
  46. package/python/scripts/index_faiss.py +100 -0
  47. package/python/scripts/test.py +57 -34
  48. package/python/usearch/eval.py +353 -0
  49. package/python/usearch/index.py +138 -117
  50. package/python/usearch/io.py +7 -2
  51. package/python/usearch/join.py +100 -0
  52. package/python/usearch/numba.py +101 -0
  53. package/rust/lib.cpp +27 -14
  54. package/rust/lib.hpp +4 -10
  55. package/rust/lib.rs +50 -17
  56. package/setup.py +12 -2
  57. package/simsimd/include/simsimd/simsimd.h +32 -3
  58. package/swift/Index+Sugar.swift +55 -29
  59. package/swift/Test.swift +1 -1
  60. package/wolfram/lib.cpp +4 -4
  61. package/python/scripts/bench_params.py +0 -161
  62. package/python/usearch/synthetic.py +0 -32
  63. package/src/punned.hpp +0 -887
  64. /package/{src → cpp}/CMakeLists.txt +0 -0
package/rust/lib.hpp CHANGED
@@ -3,19 +3,16 @@
3
3
 
4
4
  #include <memory> // `std::shared_ptr`
5
5
 
6
- #include "punned.hpp"
6
+ #include <usearch/index_punned_dense.hpp>
7
7
 
8
8
  struct Matches;
9
-
10
- enum class Metric;
11
-
12
- enum class Accuracy;
9
+ struct IndexOptions;
13
10
 
14
11
  class Index {
15
12
  public:
16
13
  using label_t = std::uint32_t;
17
14
  using distance_t = unum::usearch::punned_distance_t;
18
- using punned_t = unum::usearch::punned_gt<label_t>;
15
+ using punned_t = unum::usearch::index_punned_dense_gt<label_t>;
19
16
 
20
17
  Index(std::unique_ptr<punned_t> index);
21
18
 
@@ -40,7 +37,4 @@ class Index {
40
37
  std::unique_ptr<punned_t> index_;
41
38
  };
42
39
 
43
- std::unique_ptr<Index> new_ip(size_t dims, rust::Str quant, size_t connectivity, size_t exp_add, size_t exp_search);
44
- std::unique_ptr<Index> new_l2sq(size_t dims, rust::Str quant, size_t connectivity, size_t exp_add, size_t exp_search);
45
- std::unique_ptr<Index> new_cos(size_t dims, rust::Str quant, size_t connectivity, size_t exp_add, size_t exp_search);
46
- std::unique_ptr<Index> new_haversine(rust::Str quant, size_t connectivity, size_t exp_add, size_t exp_search);
40
+ std::unique_ptr<Index> new_index(IndexOptions const& options);
package/rust/lib.rs CHANGED
@@ -8,16 +8,41 @@ pub mod ffi {
8
8
  distances: Vec<f32>,
9
9
  }
10
10
 
11
+ enum MetricKind {
12
+ IP,
13
+ L2Sq,
14
+ Cos,
15
+ Pearson,
16
+ Haversine,
17
+ Hamming,
18
+ Tanimoto,
19
+ Sorensen,
20
+ }
21
+
22
+ enum ScalarKind {
23
+ F64,
24
+ F32,
25
+ F16,
26
+ F8,
27
+ B1,
28
+ }
29
+
30
+ struct IndexOptions {
31
+ dimensions: usize,
32
+ metric: MetricKind,
33
+ quantization: ScalarKind,
34
+ connectivity: usize,
35
+ expansion_add: usize,
36
+ expansion_search: usize,
37
+ }
38
+
11
39
  // C++ types and signatures exposed to Rust.
12
40
  unsafe extern "C++" {
13
41
  include!("lib.hpp");
14
42
 
15
43
  type Index;
16
44
 
17
- pub fn new_ip(dimensions: usize, quantization: &str, connectivity: usize, expansion_add: usize, expansion_search: usize) -> Result<UniquePtr<Index>>;
18
- pub fn new_l2sq(dimensions: usize, quantization: &str, connectivity: usize, expansion_add: usize, expansion_search: usize) -> Result<UniquePtr<Index>>;
19
- pub fn new_cos(dimensions: usize, quantization: &str, connectivity: usize, expansion_add: usize, expansion_search: usize) -> Result<UniquePtr<Index>>;
20
- pub fn new_haversine(quantization: &str, connectivity: usize, expansion_add: usize, expansion_search: usize) -> Result<UniquePtr<Index>>;
45
+ pub fn new_index(options: &IndexOptions) -> Result<UniquePtr<Index>>;
21
46
 
22
47
  pub fn reserve(self: &Index, capacity: usize) -> Result<()>;
23
48
 
@@ -29,9 +54,6 @@ pub mod ffi {
29
54
  pub fn add(self: &Index, label: u32, vector: &[f32]) -> Result<()>;
30
55
  pub fn search(self: &Index, query: &[f32], count: usize) -> Result<Matches>;
31
56
 
32
- pub fn add_in_thread(self: &Index, label: u32, vector: &[f32], thread: usize) -> Result<()>;
33
- pub fn search_in_thread(self: &Index, query: &[f32], count: usize, thread: usize) -> Result<Matches>;
34
-
35
57
  pub fn save(self: &Index, path: &str) -> Result<()>;
36
58
  pub fn load(self: &Index, path: &str) -> Result<()>;
37
59
  pub fn view(self: &Index, path: &str) -> Result<()>;
@@ -41,16 +63,24 @@ pub mod ffi {
41
63
 
42
64
  #[cfg(test)]
43
65
  mod tests {
44
- use crate::ffi::new_ip;
45
- use crate::ffi::new_l2sq;
46
- use crate::ffi::new_cos;
47
- use crate::ffi::new_haversine;
66
+ use crate::ffi::MetricKind;
67
+ use crate::ffi::ScalarKind;
68
+ use crate::ffi::IndexOptions;
69
+ use crate::ffi::new_index;
48
70
 
49
71
  #[test]
50
72
  fn integration() {
51
73
 
52
- let quant = "f16";
53
- let index = new_ip(5, &quant, 0, 0, 0).unwrap();
74
+ let mut options = IndexOptions {
75
+ dimensions: 5,
76
+ metric: MetricKind::IP,
77
+ quantization: ScalarKind::F16,
78
+ connectivity: 0,
79
+ expansion_add: 0,
80
+ expansion_search: 0
81
+ };
82
+
83
+ let index = new_index(&options).unwrap();
54
84
 
55
85
  assert!(index.reserve(10).is_ok());
56
86
  assert!(index.capacity() >= 10);
@@ -75,9 +105,12 @@ mod tests {
75
105
  assert!(index.view("index.rust.usearch").is_ok());
76
106
 
77
107
  // Make sure every function is called at least once
78
- assert!(new_ip(5, &quant, 0, 0, 0).is_ok());
79
- assert!(new_l2sq(5, &quant, 0, 0, 0).is_ok());
80
- assert!(new_cos(5, &quant, 0, 0, 0).is_ok());
81
- assert!(new_haversine(&quant, 0, 0, 0).is_ok());
108
+ assert!(new_index(&options).is_ok());
109
+ options.metric = MetricKind::L2Sq;
110
+ assert!(new_index(&options).is_ok());
111
+ options.metric = MetricKind::Cos;
112
+ assert!(new_index(&options).is_ok());
113
+ options.metric = MetricKind::Haversine;
114
+ assert!(new_index(&options).is_ok());
82
115
  }
83
116
  }
package/setup.py CHANGED
@@ -7,10 +7,12 @@ from pybind11.setup_helpers import Pybind11Extension
7
7
 
8
8
  compile_args = ['-std=c++11', '-O3']
9
9
  link_args = []
10
+ macros_args = []
10
11
 
11
12
  if sys.platform == 'linux':
12
13
  compile_args.append('-fopenmp')
13
14
  link_args.append('-lgomp')
15
+ macros_args.append(('USEARCH_USE_OPENMP', '1'))
14
16
  compile_args.append('-Wno-unknown-pragmas')
15
17
 
16
18
  if sys.platform == 'darwin':
@@ -22,12 +24,14 @@ if sys.platform == 'darwin':
22
24
  if sys.platform == 'win32':
23
25
  compile_args = ['/std:c++14', '/O2']
24
26
 
27
+
25
28
  ext_modules = [
26
29
  Pybind11Extension(
27
30
  'usearch.compiled',
28
31
  ['python/lib.cpp'],
29
32
  extra_compile_args=compile_args,
30
33
  extra_link_args=link_args,
34
+ define_macros=macros_args,
31
35
  ),
32
36
  ]
33
37
 
@@ -79,10 +83,16 @@ setup(
79
83
  ],
80
84
 
81
85
  include_dirs=[
82
- 'include', 'src',
86
+ 'include',
83
87
  'fp16/include', 'robin-map/include', 'simsimd/include'],
88
+
84
89
  ext_modules=ext_modules,
90
+ install_requires=[
91
+ 'numpy',
92
+ 'ucall',
93
+ 'pandas',
94
+ ],
85
95
  extras_require={
86
- 'jit': ['numba'],
96
+ 'jit': ['numba', 'cppyy'],
87
97
  },
88
98
  )
@@ -128,7 +128,7 @@ inline static simsimd_f16_t simsimd_l2sq_f16sve(simsimd_f16_t const* a_enum, sim
128
128
  #endif
129
129
  }
130
130
 
131
- inline static size_t simsimd_hamming_u1x8sve(uint8_t const* a, uint8_t const* b, size_t d) {
131
+ inline static size_t simsimd_hamming_b1x8sve(uint8_t const* a, uint8_t const* b, size_t d) {
132
132
  #if defined(__ARM_FEATURE_SVE)
133
133
  size_t i = 0;
134
134
  svuint8_t d_vec = svdupq_n_u8(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
@@ -183,6 +183,35 @@ inline static simsimd_f32_t simsimd_cos_f16x4neon(simsimd_f16_t const* a, simsim
183
183
  #endif
184
184
  }
185
185
 
186
+ inline static simsimd_f32_t simsimd_cos_i8x16neon(int8_t const* a, int8_t const* b, size_t d) {
187
+ #if defined(__ARM_NEON)
188
+ int8x16_t const* a8x16 = reinterpret_cast<int8x16_t const*>(a);
189
+ int8x16_t const* b8x16 = reinterpret_cast<int8x16_t const*>(b);
190
+
191
+ int32x4_t ab_vec = vdupq_n_s32(0);
192
+ int32x4_t a2_vec = vdupq_n_s32(0);
193
+ int32x4_t b2_vec = vdupq_n_s32(0);
194
+ for (size_t i = 0; i != d / 16; ++i) {
195
+ int8x16_t a_vec = a8x16[i];
196
+ int8x16_t b_vec = b8x16[i];
197
+ ab_vec = vdotq_s32(ab_vec, a_vec, b_vec);
198
+ a2_vec = vdotq_s32(a2_vec, a_vec, a_vec);
199
+ b2_vec = vdotq_s32(b2_vec, b_vec, b_vec);
200
+ }
201
+
202
+ int32x2_t ab_part = vadd_s32(vget_high_s32(ab_vec), vget_low_s32(ab_vec));
203
+ int32_t ab = vget_lane_s32(vpadd_s32(ab_part, ab_part), 0);
204
+ int32x2_t a2_part = vadd_s32(vget_high_s32(a2_vec), vget_low_s32(a2_vec));
205
+ int32_t a2 = vget_lane_s32(vpadd_s32(a2_part, a2_part), 0);
206
+ int32x2_t b2_part = vadd_s32(vget_high_s32(b2_vec), vget_low_s32(b2_vec));
207
+ int32_t b2 = vget_lane_s32(vpadd_s32(b2_part, b2_part), 0);
208
+ return ab / (sqrt(a2) * sqrt(b2));
209
+ #else
210
+ (void)a, (void)b, (void)d;
211
+ return 0;
212
+ #endif
213
+ }
214
+
186
215
  inline static simsimd_f32_t simsimd_cos_f32x4neon(simsimd_f32_t const* a, simsimd_f32_t const* b, size_t d) {
187
216
  #if defined(__ARM_NEON)
188
217
  float32x4_t ab_vec = vdupq_n_f32(0);
@@ -205,7 +234,7 @@ inline static simsimd_f32_t simsimd_cos_f32x4neon(simsimd_f32_t const* a, simsim
205
234
  #endif
206
235
  }
207
236
 
208
- inline static size_t simsimd_hamming_u1x128sve(uint8_t const* a, uint8_t const* b, size_t d) {
237
+ inline static size_t simsimd_hamming_b1x128sve(uint8_t const* a, uint8_t const* b, size_t d) {
209
238
  #if defined(__ARM_NEON)
210
239
  /// Contains 16x 8-bit integers with running population count sums.
211
240
  uint8x16_t d_vec = vdupq_n_u8(0);
@@ -309,7 +338,7 @@ inline static simsimd_f32_t simsimd_cos_f32x4avx2(simsimd_f32_t const* a, simsim
309
338
  #endif
310
339
  }
311
340
 
312
- inline static size_t simsimd_hamming_u1x128avx512(uint8_t const* a, uint8_t const* b, size_t d) {
341
+ inline static size_t simsimd_hamming_b1x128avx512(uint8_t const* a, uint8_t const* b, size_t d) {
313
342
  #if defined(__AVX512VPOPCNTDQ__)
314
343
  size_t words = d / 128;
315
344
  uint64_t const* a64 = (uint64_t const*)(a);
@@ -1,20 +1,28 @@
1
1
  //
2
2
  // Index+Sugar.swift
3
- //
3
+ //
4
4
  //
5
5
  // Created by Ashot Vardanian on 5/11/23.
6
6
  //
7
7
 
8
8
  @available(iOS 13, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
9
- extension Index {
10
-
11
- public func add(label: UInt32, vector: ArraySlice<Float32>) -> () {
9
+ extension USearchIndex {
10
+ /// Adds a labeled vector to the index.
11
+ /// - Parameter label: Unique identifer for that object.
12
+ /// - Parameter vector: Single-precision vector.
13
+ /// - Throws: If runs out of memory.
14
+ public func add(label: UInt32, vector: ArraySlice<Float32>) {
12
15
  vector.withContiguousStorageIfAvailable {
13
16
  addSingle(label: label, vector: $0.baseAddress!)
14
17
  }
15
18
  }
19
+
16
20
 
17
-
21
+ /// Approximate nearest neighbors search.
22
+ /// - Parameter vector: Single-precision query vector.
23
+ /// - Parameter count: Upper limit on the number of matches to retrieve.
24
+ /// - Returns: Labels and distances to closest approximate matches in decreasing similarity order.
25
+ /// - Throws: If runs out of memory.
18
26
  public func search(vector: ArraySlice<Float32>, count: Int) -> ([UInt32], [Float]) {
19
27
  var matches: [UInt32] = Array(repeating: 0, count: count)
20
28
  var distances: [Float] = Array(repeating: 0, count: count)
@@ -26,46 +34,64 @@ extension Index {
26
34
  return (matches, distances)
27
35
  }
28
36
 
29
- public func add(label: UInt32, vector: ArraySlice<Float64>) -> () {
37
+ /// Adds a labeled vector to the index.
38
+ /// - Parameter label: Unique identifer for that object.
39
+ /// - Parameter vector: Double-precision vector.
40
+ /// - Throws: If runs out of memory.
41
+ public func add(label: UInt32, vector: ArraySlice<Float64>) {
30
42
  vector.withContiguousStorageIfAvailable {
31
- addPrecise(label: label, vector: $0.baseAddress!)
43
+ addDouble(label: label, vector: $0.baseAddress!)
32
44
  }
33
45
  }
34
-
46
+
47
+ /// Approximate nearest neighbors search.
48
+ /// - Parameter vector: Double-precision query vector.
49
+ /// - Parameter count: Upper limit on the number of matches to retrieve.
50
+ /// - Returns: Labels and distances to closest approximate matches in decreasing similarity order.
51
+ /// - Throws: If runs out of memory.
35
52
  public func search(vector: ArraySlice<Float64>, count: Int) -> ([UInt32], [Float]) {
36
53
  var matches: [UInt32] = Array(repeating: 0, count: count)
37
54
  var distances: [Float] = Array(repeating: 0, count: count)
38
55
  let results = vector.withContiguousStorageIfAvailable {
39
- searchPrecise(vector: $0.baseAddress!, count: CUnsignedInt(count), labels: &matches, distances: &distances)
56
+ searchDouble(vector: $0.baseAddress!, count: CUnsignedInt(count), labels: &matches, distances: &distances)
40
57
  }
41
58
  matches.removeLast(count - Int(results!))
42
59
  distances.removeLast(count - Int(results!))
43
60
  return (matches, distances)
44
61
  }
45
62
 
46
- #if arch(arm64)
63
+ #if arch(arm64)
47
64
 
48
- @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
49
- public func add(label: UInt32, vector: ArraySlice<Float16>) -> () {
50
- vector.withContiguousStorageIfAvailable { buffer in
51
- addImprecise(label: label, vector: buffer.baseAddress!)
65
+ /// Adds a labeled vector to the index.
66
+ /// - Parameter label: Unique identifer for that object.
67
+ /// - Parameter vector: Half-precision vector.
68
+ /// - Throws: If runs out of memory.
69
+ @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
70
+ public func add(label: UInt32, vector: ArraySlice<Float16>) {
71
+ vector.withContiguousStorageIfAvailable { buffer in
72
+ addHalf(label: label, vector: buffer.baseAddress!)
73
+ }
52
74
  }
53
- }
54
75
 
55
- @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
56
- public func search(vector: ArraySlice<Float16>, count: Int) -> ([UInt32], [Float]) {
57
- var matches: [UInt32] = Array(repeating: 0, count: count)
58
- var distances: [Float] = Array(repeating: 0, count: count)
59
- let results = vector.withContiguousStorageIfAvailable {
60
- searchImprecise(vector: $0.baseAddress!, count: CUnsignedInt(count), labels: &matches, distances: &distances)
76
+ /// Approximate nearest neighbors search.
77
+ /// - Parameter vector: Half-precision query vector.
78
+ /// - Parameter count: Upper limit on the number of matches to retrieve.
79
+ /// - Returns: Labels and distances to closest approximate matches in decreasing similarity order.
80
+ /// - Throws: If runs out of memory.
81
+ @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
82
+ public func search(vector: ArraySlice<Float16>, count: Int) -> ([UInt32], [Float]) {
83
+ var matches: [UInt32] = Array(repeating: 0, count: count)
84
+ var distances: [Float] = Array(repeating: 0, count: count)
85
+ let results = vector.withContiguousStorageIfAvailable {
86
+ searchHalf(vector: $0.baseAddress!, count: CUnsignedInt(count), labels: &matches, distances: &distances)
87
+ }
88
+ matches.removeLast(count - Int(results!))
89
+ distances.removeLast(count - Int(results!))
90
+ return (matches, distances)
61
91
  }
62
- matches.removeLast(count - Int(results!))
63
- distances.removeLast(count - Int(results!))
64
- return (matches, distances)
65
- }
66
-
67
- #endif
68
92
 
69
- public var count: Int { get { return Int(length) } }
70
-
93
+ #endif
94
+
95
+ /// Number of vectors in the index.
96
+ public var count: Int { return Int(length) }
71
97
  }
package/swift/Test.swift CHANGED
@@ -10,7 +10,7 @@ import USearch
10
10
 
11
11
  @available(iOS 13, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
12
12
  func testUnit() throws {
13
- let index = Index.l2sq(dimensions: 4, connectivity: 8)
13
+ let index = USearchIndex.make(metric:USearchMetric.l2sq, dimensions: 4, connectivity: 8, quantization:USearchScalar.F32)
14
14
  let vectorA: [Float32] = [0.3, 0.5, 1.2, 1.4]
15
15
  let vectorB: [Float32] = [0.4, 0.2, 1.2, 1.1]
16
16
  index.add(label: 42, vector: vectorA[...])
package/wolfram/lib.cpp CHANGED
@@ -1,9 +1,9 @@
1
1
  #include "WolframLibrary.h"
2
- #include "punned.hpp"
2
+ #include <usearch/index_punned_dense.hpp>
3
3
 
4
4
  using namespace unum::usearch;
5
5
  using distance_t = punned_distance_t;
6
- using punned_t = punned_gt<int>;
6
+ using punned_t = index_punned_dense_gt<int>;
7
7
  using span_t = span_gt<float>;
8
8
 
9
9
  EXTERN_C DLLEXPORT int WolframLibrary_initialize(WolframLibraryData libData) { return LIBRARY_NO_ERROR; }
@@ -22,8 +22,8 @@ EXTERN_C DLLEXPORT int IndexCreate(WolframLibraryData libData, mint Argc, MArgum
22
22
  config.expansion_add = static_cast<std::size_t>(MArgument_getInteger(Args[5]));
23
23
  config.expansion_search = static_cast<std::size_t>(MArgument_getInteger(Args[6]));
24
24
 
25
- accuracy_t accuracy = accuracy_from_name(accuracy_cstr, std::strlen(accuracy_cstr));
26
- common_metric_kind_t metric_kind = common_metric_from_name(metric_cstr, std::strlen(metric_cstr));
25
+ scalar_kind_t accuracy = scalar_kind_from_name(accuracy_cstr, std::strlen(accuracy_cstr));
26
+ metric_kind_t metric_kind = metric_from_name(metric_cstr, std::strlen(metric_cstr));
27
27
  punned_t index = make_punned<punned_t>(metric_kind, dimensions, accuracy, config);
28
28
  index.reserve(capacity);
29
29
 
@@ -1,161 +0,0 @@
1
- import os
2
- import time
3
-
4
- import fire
5
- import plotly.express as px
6
- import numpy as np
7
- import pandas as pd
8
-
9
-
10
- from usearch.index import Index
11
-
12
-
13
- def measure(f) -> float:
14
- a = time.time_ns()
15
- result = f()
16
- b = time.time_ns()
17
- c = b - a
18
- secs = c / (10 ** 9)
19
- print(f'- Took: {secs:.2f} seconds')
20
- return secs, result
21
-
22
-
23
- def bench_usearch(index, vectors: np.ndarray) -> float:
24
- labels = np.arange(vectors.shape[0], dtype=np.longlong)
25
- assert len(index) == 0
26
- dt, _ = measure(lambda: index.add(labels, vectors))
27
- assert len(index) == vectors.shape[0]
28
- insertions_per_second = vectors.shape[0]/dt
29
- print(f'- Performance: {insertions_per_second:.2f} insertions/s')
30
- return insertions_per_second
31
-
32
-
33
- class Main:
34
-
35
- def dimensions(
36
- self,
37
- count: int = 1_000_000,
38
- connectivity: int = 16,
39
- expansion_add: int = 128,
40
- expansion_search: int = 64,
41
- ):
42
- """Measures indexing speed for different dimensionality vectors.
43
-
44
- :param count: Number of vectors, defaults to 1_000_000
45
- :type count: int, optional
46
- """
47
-
48
- dimensions = [
49
- 2, 3, 4, 8, 16, 32, 96, 100,
50
- 256, 384, 512, 768, 1024, 1536]
51
- results = []
52
-
53
- try:
54
- for ndim in dimensions:
55
- vectors_mat = np.random.rand(count, ndim).astype(np.float32)
56
-
57
- print(f'USearch for {ndim}-dimensional f32 vectors')
58
- index = Index(
59
- ndim=ndim,
60
- expansion_add=expansion_add,
61
- expansion_search=expansion_search,
62
- connectivity=connectivity,
63
- )
64
- speed = bench_usearch(index, vectors_mat)
65
- results.append({'ndim': ndim, 'speed': speed})
66
- del index
67
- except KeyboardInterrupt:
68
- pass
69
-
70
- df = pd.DataFrame(results)
71
- fig = px.line(
72
- df, x='ndim', y='speed', log_x=True,
73
- title='HNSW Indexing Speed vs Vector Dimensions')
74
- fig.write_image(os.path.join(os.path.dirname(
75
- __file__), 'bench_dimensions.png'))
76
- fig.show()
77
-
78
- def connectivity(
79
- self,
80
- count: int = 1_000_000,
81
- ndim: int = 256,
82
- expansion_add: int = 128,
83
- expansion_search: int = 64,
84
- ):
85
- """Measures indexing speed and accuracy for different level of
86
- connectivity in the levels of the hierarchical proximity graph.
87
-
88
- :param count: Number of vectors, defaults to 1_000_000
89
- :type count: int, optional
90
- :param ndim: Number of dimensions per vector, defaults to 256
91
- :type ndim: int, optional
92
- """
93
-
94
- connectivity_options = range(10, 20)
95
- results = []
96
-
97
- try:
98
- for connectivity in connectivity_options:
99
- vectors_mat = np.random.rand(count, ndim).astype(np.float32)
100
-
101
- print(f'USearch for {connectivity} connectivity')
102
- index = Index(
103
- ndim=ndim,
104
- expansion_add=expansion_add,
105
- expansion_search=expansion_search,
106
- connectivity=connectivity,
107
- )
108
- speed = bench_usearch(index, vectors_mat)
109
- results.append({'connectivity': connectivity, 'speed': speed})
110
- del index
111
- except KeyboardInterrupt:
112
- pass
113
-
114
- df = pd.DataFrame(results)
115
- fig = px.line(
116
- df, x='connectivity', y='speed', log_x=True,
117
- title='HNSW Indexing Speed vs Connectivity')
118
- fig.write_image(os.path.join(os.path.dirname(
119
- __file__), 'bench_connectivity.png'))
120
- fig.show()
121
-
122
- def robustness(
123
- self,
124
- experiments: int = 10,
125
- count: int = 1_000_000,
126
- ndim: int = 256,
127
- connectivity: int = 16,
128
- expansion_add: int = 128,
129
- expansion_search: int = 64,
130
- ):
131
- """How much does accuracy fluctuate depending on the indexing order.
132
-
133
- :param experiments: How many times to repeat, defaults to 10
134
- :type experiments: int, optional
135
- :param count: Number of vectors, defaults to 1_000_000
136
- :type count: int, optional
137
- :param ndim: Number of dimensions per vector, defaults to 256
138
- :type ndim: int, optional
139
- """
140
-
141
- recall_levels = []
142
- vectors_mat = np.random.rand(count, ndim).astype(np.float32)
143
-
144
- for _ in range(experiments):
145
-
146
- index = Index(
147
- ndim=ndim,
148
- expansion_add=expansion_add,
149
- expansion_search=expansion_search,
150
- connectivity=connectivity,
151
- )
152
- bench_usearch(index, vectors_mat)
153
- del index
154
-
155
- min_ = min(recall_levels)
156
- max_ = max(recall_levels)
157
- print(f'Recall@1 varies between {min_:.3f} and {max_:.3f}')
158
-
159
-
160
- if __name__ == '__main__':
161
- fire.Fire(Main)
@@ -1,32 +0,0 @@
1
- import numpy as np
2
-
3
- from usearch.index import Index, Matches, MetricKind, BitwiseMetricKind
4
-
5
-
6
- def vectors(index: Index, count: int) -> np.ndarray:
7
-
8
- if not isinstance(index, Index):
9
- raise ValueError('Unsupported `index` type')
10
-
11
- dimensions = index.ndim
12
-
13
- if index.metric in BitwiseMetricKind:
14
- bit_vectors = np.random.randint(2, size=(count, dimensions))
15
- bit_vectors = np.packbits(bit_vectors, axis=1)
16
- return bit_vectors
17
-
18
- else:
19
- x = np.random.rand(count, dimensions).astype(index.numpy_dtype)
20
- if index.metric == MetricKind.IP:
21
- return x / np.linalg.norm(x, axis=1, keepdims=True)
22
- return x
23
-
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)