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.
- package/.github/workflows/release.yml +62 -7
- package/.github/workflows/update_version.sh +6 -1
- package/.vscode/launch.json +21 -22
- package/.vscode/settings.json +7 -1
- package/CITATION.cff +4 -4
- package/CMakeLists.txt +1 -1
- package/Cargo.toml +1 -1
- package/Package.swift +0 -1
- package/README.md +100 -27
- package/binding.gyp +0 -1
- package/build.gradle +1 -1
- package/build.rs +3 -3
- package/c/lib.cpp +32 -9
- package/c/usearch.h +67 -36
- package/conanfile.py +1 -1
- package/{src → cpp}/bench.cpp +52 -36
- package/{src → cpp}/test.cpp +17 -7
- package/docs/benchmarks.md +33 -10
- package/docs/c/index.rst +11 -0
- package/docs/c/reference.rst +7 -0
- package/docs/compilation.md +24 -8
- package/docs/conf.dox +6 -6
- package/docs/conf.py +3 -1
- package/docs/cpp/index.rst +11 -0
- package/docs/cpp/reference.rst +7 -0
- package/docs/index.rst +10 -0
- package/docs/java/index.rst +11 -0
- package/docs/java/reference.rst +7 -0
- package/docs/javascript/index.rst +11 -0
- package/docs/javascript/reference.rst +9 -0
- package/docs/python/index.rst +11 -0
- package/docs/python/reference.rst +35 -0
- package/golang/lib.go +1 -1
- package/include/usearch/{usearch.hpp → index.hpp} +152 -148
- package/include/usearch/index_punned_dense.hpp +633 -0
- package/include/usearch/index_punned_helpers.hpp +528 -0
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +8 -7
- package/javascript/lib.cpp +26 -50
- package/javascript/usearch.js +117 -2
- package/objc/USearchObjective.mm +145 -49
- package/objc/include/USearchObjective.h +106 -30
- package/package.json +2 -2
- package/python/lib.cpp +178 -320
- package/python/scripts/bench.ipynb +170 -0
- package/python/scripts/bench.py +102 -185
- package/python/scripts/index_faiss.py +100 -0
- package/python/scripts/test.py +57 -34
- package/python/usearch/eval.py +353 -0
- package/python/usearch/index.py +138 -117
- package/python/usearch/io.py +7 -2
- package/python/usearch/join.py +100 -0
- package/python/usearch/numba.py +101 -0
- package/rust/lib.cpp +27 -14
- package/rust/lib.hpp +4 -10
- package/rust/lib.rs +50 -17
- package/setup.py +12 -2
- package/simsimd/include/simsimd/simsimd.h +32 -3
- package/swift/Index+Sugar.swift +55 -29
- package/swift/Test.swift +1 -1
- package/wolfram/lib.cpp +4 -4
- package/python/scripts/bench_params.py +0 -161
- package/python/usearch/synthetic.py +0 -32
- package/src/punned.hpp +0 -887
- /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
|
|
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::
|
|
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>
|
|
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
|
|
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::
|
|
45
|
-
use crate::ffi::
|
|
46
|
-
use crate::ffi::
|
|
47
|
-
use crate::ffi::
|
|
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
|
|
53
|
-
|
|
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!(
|
|
79
|
-
|
|
80
|
-
assert!(
|
|
81
|
-
|
|
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',
|
|
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
|
|
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
|
|
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
|
|
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);
|
package/swift/Index+Sugar.swift
CHANGED
|
@@ -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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
vector
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
26
|
-
|
|
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)
|