usearch 1.1.0 → 1.2.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/prerelease.yml +3 -3
- package/.github/workflows/release.yml +3 -3
- package/.github/workflows/update_version.sh +1 -1
- package/.gitmodules +1 -4
- package/Cargo.toml +1 -1
- package/Package.swift +2 -1
- package/README.md +129 -46
- package/binding.gyp +16 -12
- package/build.gradle +1 -1
- package/build.rs +0 -1
- package/c/CMakeLists.txt +11 -7
- package/c/lib.cpp +36 -24
- package/c/usearch.h +42 -12
- package/cluster.ipynb +167 -0
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +8 -3
- package/cpp/bench.cpp +2 -2
- package/cpp/test.cpp +42 -25
- package/docs/benchmarks.md +8 -0
- package/docs/compilation.md +7 -7
- package/docs/conf.py +27 -24
- package/docs/javascript/reference.rst +3 -0
- package/docs/wolfram/index.rst +6 -0
- package/golang/README.md +31 -7
- package/golang/lib.go +1 -1
- package/include/usearch/index.hpp +348 -69
- package/include/usearch/index_dense.hpp +587 -179
- package/include/usearch/index_plugins.hpp +194 -16
- package/java/cloud/unum/usearch/Index.java +13 -0
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +40 -9
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.h +16 -0
- package/javascript/docs.js +111 -0
- package/javascript/lib.cpp +29 -9
- package/javascript/usearch.d.ts +9 -1
- package/objc/USearchObjective.mm +11 -11
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- package/python/lib.cpp +492 -159
- package/python/scripts/index_faiss.py +3 -3
- package/python/scripts/join.py +6 -6
- package/python/scripts/test_index.py +219 -0
- package/python/scripts/test_sparse.py +21 -21
- package/python/scripts/test_tooling.py +111 -0
- package/python/usearch/eval.py +9 -5
- package/python/usearch/index.py +491 -216
- package/rust/lib.cpp +6 -24
- package/rust/lib.hpp +1 -3
- package/rust/lib.rs +89 -0
- package/setup.py +5 -6
- package/wasm/CMakeLists.txt +0 -1
- package/wasmer.toml +1 -1
- package/python/scripts/test.py +0 -364
- package/robin-map/.codecov.yml +0 -5
- package/robin-map/.github/workflows/ci.yml +0 -136
- package/robin-map/CMakeLists.txt +0 -86
- package/robin-map/LICENSE +0 -21
- package/robin-map/README.md +0 -483
- package/robin-map/cmake/tsl-robin-mapConfig.cmake.in +0 -9
- package/robin-map/doxygen.conf +0 -2483
- package/robin-map/include/tsl/robin_growth_policy.h +0 -406
- package/robin-map/include/tsl/robin_hash.h +0 -1639
- package/robin-map/include/tsl/robin_map.h +0 -807
- package/robin-map/include/tsl/robin_set.h +0 -660
- package/robin-map/tests/CMakeLists.txt +0 -26
- package/robin-map/tests/custom_allocator_tests.cpp +0 -138
- package/robin-map/tests/main.cpp +0 -26
- package/robin-map/tests/policy_tests.cpp +0 -97
- package/robin-map/tests/robin_map_tests.cpp +0 -1451
- package/robin-map/tests/robin_set_tests.cpp +0 -164
- package/robin-map/tests/utils.h +0 -443
- package/robin-map/tsl-robin-map.natvis +0 -78
package/rust/lib.cpp
CHANGED
|
@@ -11,30 +11,6 @@ using labeling_result_t = typename index_t::labeling_result_t;
|
|
|
11
11
|
|
|
12
12
|
Index::Index(std::unique_ptr<index_t> index) : index_(std::move(index)) {}
|
|
13
13
|
|
|
14
|
-
void Index::add_in_thread(key_t key, rust::Slice<float const> vector, size_t thread) const {
|
|
15
|
-
index_update_config_t config;
|
|
16
|
-
config.thread = thread;
|
|
17
|
-
config.expansion = index_->expansion_add();
|
|
18
|
-
index_->add(key, vector.data(), config).error.raise();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
Matches Index::search_in_thread(rust::Slice<float const> vector, size_t count, size_t thread) const {
|
|
22
|
-
Matches matches;
|
|
23
|
-
matches.keys.reserve(count);
|
|
24
|
-
matches.distances.reserve(count);
|
|
25
|
-
for (size_t i = 0; i != count; ++i)
|
|
26
|
-
matches.keys.push_back(0), matches.distances.push_back(0);
|
|
27
|
-
index_search_config_t config;
|
|
28
|
-
config.thread = thread;
|
|
29
|
-
config.expansion = index_->expansion_search();
|
|
30
|
-
search_result_t result = index_->search(vector.data(), count, config);
|
|
31
|
-
result.error.raise();
|
|
32
|
-
count = result.dump_to(matches.keys.data(), matches.distances.data());
|
|
33
|
-
matches.keys.truncate(count);
|
|
34
|
-
matches.distances.truncate(count);
|
|
35
|
-
return matches;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
14
|
void Index::add(key_t key, rust::Slice<float const> vector) const { index_->add(key, vector.data()).error.raise(); }
|
|
39
15
|
|
|
40
16
|
bool Index::remove(key_t key) const {
|
|
@@ -43,6 +19,12 @@ bool Index::remove(key_t key) const {
|
|
|
43
19
|
return result.completed;
|
|
44
20
|
}
|
|
45
21
|
|
|
22
|
+
bool Index::rename(key_t from, key_t to) const {
|
|
23
|
+
labeling_result_t result = index_->rename(from, to);
|
|
24
|
+
result.error.raise();
|
|
25
|
+
return result.completed;
|
|
26
|
+
}
|
|
27
|
+
|
|
46
28
|
bool Index::contains(key_t key) const { return index_->contains(key); }
|
|
47
29
|
|
|
48
30
|
Matches Index::search(rust::Slice<float const> vector, size_t count) const {
|
package/rust/lib.hpp
CHANGED
|
@@ -22,13 +22,11 @@ class Index {
|
|
|
22
22
|
void reserve(size_t) const;
|
|
23
23
|
|
|
24
24
|
void add(key_t key, rust::Slice<float const> vector) const;
|
|
25
|
-
void add_in_thread(key_t key, rust::Slice<float const> vector, size_t thread) const;
|
|
26
|
-
|
|
27
25
|
Matches search(rust::Slice<float const> vector, size_t count) const;
|
|
28
|
-
Matches search_in_thread(rust::Slice<float const> vector, size_t count, size_t thread) const;
|
|
29
26
|
|
|
30
27
|
bool remove(key_t key) const;
|
|
31
28
|
bool contains(key_t key) const;
|
|
29
|
+
bool rename(key_t from, key_t to) const;
|
|
32
30
|
|
|
33
31
|
size_t dimensions() const;
|
|
34
32
|
size_t connectivity() const;
|
package/rust/lib.rs
CHANGED
|
@@ -39,25 +39,114 @@ pub mod ffi {
|
|
|
39
39
|
unsafe extern "C++" {
|
|
40
40
|
include!("lib.hpp");
|
|
41
41
|
|
|
42
|
+
/// Represents the USearch index.
|
|
42
43
|
type Index;
|
|
43
44
|
|
|
45
|
+
/// Initializes a new instance of the index with the provided options.
|
|
46
|
+
///
|
|
47
|
+
/// # Arguments
|
|
48
|
+
///
|
|
49
|
+
/// * `options` - A reference to the `IndexOptions` structure containing initialization options.
|
|
50
|
+
///
|
|
51
|
+
/// # Returns
|
|
52
|
+
///
|
|
53
|
+
/// A `Result` which is `Ok` if the index is successfully initialized, or `Err` if an error occurs.
|
|
44
54
|
pub fn new_index(options: &IndexOptions) -> Result<UniquePtr<Index>>;
|
|
45
55
|
|
|
56
|
+
/// Reserves memory for a specified number of incoming vectors.
|
|
57
|
+
///
|
|
58
|
+
/// # Arguments
|
|
59
|
+
///
|
|
60
|
+
/// * `capacity` - The desired total capacity including the current size.
|
|
46
61
|
pub fn reserve(self: &Index, capacity: usize) -> Result<()>;
|
|
47
62
|
|
|
63
|
+
/// Retrieves the number of dimensions in the vectors indexed.
|
|
48
64
|
pub fn dimensions(self: &Index) -> usize;
|
|
65
|
+
|
|
66
|
+
/// Retrieves the connectivity parameter that limits connections-per-node in the graph.
|
|
49
67
|
pub fn connectivity(self: &Index) -> usize;
|
|
68
|
+
|
|
69
|
+
/// Retrieves the current number of vectors in the index.
|
|
50
70
|
pub fn size(self: &Index) -> usize;
|
|
71
|
+
|
|
72
|
+
/// Retrieves the total capacity of the index, including reserved space.
|
|
51
73
|
pub fn capacity(self: &Index) -> usize;
|
|
52
74
|
|
|
75
|
+
/// Adds a vector with a specified key to the index.
|
|
76
|
+
///
|
|
77
|
+
/// # Arguments
|
|
78
|
+
///
|
|
79
|
+
/// * `key` - The key associated with the vector.
|
|
80
|
+
/// * `vector` - A slice containing the vector data.
|
|
53
81
|
pub fn add(self: &Index, key: u64, vector: &[f32]) -> Result<()>;
|
|
82
|
+
|
|
83
|
+
/// Performs k-Approximate Nearest Neighbors (kANN) Search for closest vectors to the provided query.
|
|
84
|
+
///
|
|
85
|
+
/// # Arguments
|
|
86
|
+
///
|
|
87
|
+
/// * `query` - A slice containing the query vector data.
|
|
88
|
+
/// * `count` - The maximum number of neighbors to search for.
|
|
89
|
+
///
|
|
90
|
+
/// # Returns
|
|
91
|
+
///
|
|
92
|
+
/// A `Result` containing the matches found.
|
|
54
93
|
pub fn search(self: &Index, query: &[f32], count: usize) -> Result<Matches>;
|
|
94
|
+
|
|
95
|
+
/// Removes the vector associated with the given key from the index.
|
|
96
|
+
///
|
|
97
|
+
/// # Arguments
|
|
98
|
+
///
|
|
99
|
+
/// * `key` - The key of the vector to be removed.
|
|
100
|
+
///
|
|
101
|
+
/// # Returns
|
|
102
|
+
///
|
|
103
|
+
/// `true` if the vector is successfully removed, `false` otherwise.
|
|
55
104
|
pub fn remove(self: &Index, key: u64) -> Result<bool>;
|
|
105
|
+
|
|
106
|
+
/// Renames the vector under a certain key.
|
|
107
|
+
///
|
|
108
|
+
/// # Arguments
|
|
109
|
+
///
|
|
110
|
+
/// * `from` - The key of the vector to be renamed.
|
|
111
|
+
/// * `to` - The new name.
|
|
112
|
+
///
|
|
113
|
+
/// # Returns
|
|
114
|
+
///
|
|
115
|
+
/// `true` if the vector is successfully renamed, `false` otherwise.
|
|
116
|
+
pub fn rename(self: &Index, from: u64, to: u64) -> Result<bool>;
|
|
117
|
+
|
|
118
|
+
/// Checks if the index contains a vector with a specified key.
|
|
119
|
+
///
|
|
120
|
+
/// # Arguments
|
|
121
|
+
///
|
|
122
|
+
/// * `key` - The key to be checked.
|
|
123
|
+
///
|
|
124
|
+
/// # Returns
|
|
125
|
+
///
|
|
126
|
+
/// `true` if the index contains the vector with the given key, `false` otherwise.
|
|
56
127
|
pub fn contains(self: &Index, key: u64) -> bool;
|
|
57
128
|
|
|
129
|
+
/// Saves the index to a specified file.
|
|
130
|
+
///
|
|
131
|
+
/// # Arguments
|
|
132
|
+
///
|
|
133
|
+
/// * `path` - The file path where the index will be saved.
|
|
58
134
|
pub fn save(self: &Index, path: &str) -> Result<()>;
|
|
135
|
+
|
|
136
|
+
/// Loads the index from a specified file.
|
|
137
|
+
///
|
|
138
|
+
/// # Arguments
|
|
139
|
+
///
|
|
140
|
+
/// * `path` - The file path from where the index will be loaded.
|
|
59
141
|
pub fn load(self: &Index, path: &str) -> Result<()>;
|
|
142
|
+
|
|
143
|
+
/// Creates a view of the index from a file without loading it into memory.
|
|
144
|
+
///
|
|
145
|
+
/// # Arguments
|
|
146
|
+
///
|
|
147
|
+
/// * `path` - The file path from where the view will be created.
|
|
60
148
|
pub fn view(self: &Index, path: &str) -> Result<()>;
|
|
149
|
+
|
|
61
150
|
}
|
|
62
151
|
}
|
|
63
152
|
|
package/setup.py
CHANGED
|
@@ -13,7 +13,7 @@ macros_args = [
|
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
if sys.platform == "linux":
|
|
16
|
-
compile_args.append("-std=c++
|
|
16
|
+
compile_args.append("-std=c++17")
|
|
17
17
|
compile_args.append("-O3") # Maximize performance
|
|
18
18
|
compile_args.append("-g") # Simplify debugging
|
|
19
19
|
compile_args.append("-Wno-unknown-pragmas")
|
|
@@ -26,18 +26,18 @@ if sys.platform == "darwin":
|
|
|
26
26
|
# MacOS 10.15 or higher is needed for `aligned_alloc` support.
|
|
27
27
|
# https://github.com/unum-cloud/usearch/actions/runs/4975434891/jobs/8902603392
|
|
28
28
|
compile_args.append("-mmacosx-version-min=10.15")
|
|
29
|
-
compile_args.append("-std=c++
|
|
29
|
+
compile_args.append("-std=c++17")
|
|
30
30
|
compile_args.append("-O3") # Maximize performance
|
|
31
31
|
compile_args.append("-g") # Simplify debugging
|
|
32
32
|
compile_args.append("-Wno-unknown-pragmas")
|
|
33
33
|
|
|
34
|
-
# Linking OpenMP requires additional
|
|
34
|
+
# Linking OpenMP requires additional preparation in CIBuildWheel
|
|
35
35
|
# macros_args.append(("USEARCH_USE_OPENMP", "1"))
|
|
36
36
|
# compile_args.append("-Xpreprocessor -fopenmp")
|
|
37
37
|
# link_args.append("-Xpreprocessor -lomp")
|
|
38
38
|
|
|
39
39
|
if sys.platform == "win32":
|
|
40
|
-
compile_args.append("/std:c++
|
|
40
|
+
compile_args.append("/std:c++17")
|
|
41
41
|
compile_args.append("/O2")
|
|
42
42
|
|
|
43
43
|
ext_modules = [
|
|
@@ -90,11 +90,10 @@ setup(
|
|
|
90
90
|
"Topic :: Database :: Database Engines/Servers",
|
|
91
91
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
92
92
|
],
|
|
93
|
-
include_dirs=["include", "fp16/include", "
|
|
93
|
+
include_dirs=["include", "fp16/include", "simsimd/include"],
|
|
94
94
|
ext_modules=ext_modules,
|
|
95
95
|
install_requires=[
|
|
96
96
|
"numpy",
|
|
97
|
-
"pandas",
|
|
98
97
|
"tqdm",
|
|
99
98
|
'ucall; python_version >= "3.9"',
|
|
100
99
|
],
|
package/wasm/CMakeLists.txt
CHANGED
package/wasmer.toml
CHANGED
package/python/scripts/test.py
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
import numpy as np
|
|
5
|
-
|
|
6
|
-
from usearch.io import load_matrix, save_matrix
|
|
7
|
-
from usearch.eval import random_vectors
|
|
8
|
-
from usearch.index import search
|
|
9
|
-
|
|
10
|
-
from usearch.index import (
|
|
11
|
-
Index,
|
|
12
|
-
Indexes,
|
|
13
|
-
MetricKind,
|
|
14
|
-
ScalarKind,
|
|
15
|
-
Match,
|
|
16
|
-
Matches,
|
|
17
|
-
BatchMatches,
|
|
18
|
-
)
|
|
19
|
-
from usearch.index import (
|
|
20
|
-
DEFAULT_CONNECTIVITY,
|
|
21
|
-
DEFAULT_EXPANSION_ADD,
|
|
22
|
-
DEFAULT_EXPANSION_SEARCH,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
# When running the tests concurrently in different VMs, one can get access violation errors.
|
|
26
|
-
# To avoid that, we embed the OS name into the temporary files name.
|
|
27
|
-
# https://github.com/unum-cloud/usearch/actions/runs/5705359698/job/15459945738
|
|
28
|
-
temporary_filename = f"tmp-{os.name}"
|
|
29
|
-
temporary_usearch_filename = temporary_filename + ".usearch"
|
|
30
|
-
|
|
31
|
-
dimensions = [3, 97, 256]
|
|
32
|
-
batch_sizes = [1, 77]
|
|
33
|
-
index_types = [
|
|
34
|
-
ScalarKind.F32,
|
|
35
|
-
ScalarKind.F64,
|
|
36
|
-
ScalarKind.F16,
|
|
37
|
-
ScalarKind.I8,
|
|
38
|
-
]
|
|
39
|
-
numpy_types = [np.float32, np.float64, np.float16]
|
|
40
|
-
|
|
41
|
-
connectivity_options = [3, 13, 50, DEFAULT_CONNECTIVITY]
|
|
42
|
-
continuous_metrics = [
|
|
43
|
-
MetricKind.Cos,
|
|
44
|
-
MetricKind.L2sq,
|
|
45
|
-
]
|
|
46
|
-
hash_metrics = [
|
|
47
|
-
MetricKind.Hamming,
|
|
48
|
-
MetricKind.Tanimoto,
|
|
49
|
-
MetricKind.Sorensen,
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@pytest.mark.parametrize("rows", batch_sizes)
|
|
54
|
-
@pytest.mark.parametrize("cols", dimensions)
|
|
55
|
-
def test_serializing_fbin_matrix(rows: int, cols: int):
|
|
56
|
-
"""
|
|
57
|
-
Test the serialization of floating point binary matrix.
|
|
58
|
-
|
|
59
|
-
:param int rows: The number of rows in the matrix.
|
|
60
|
-
:param int cols: The number of columns in the matrix.
|
|
61
|
-
"""
|
|
62
|
-
original = np.random.rand(rows, cols).astype(np.float32)
|
|
63
|
-
save_matrix(original, temporary_filename + ".fbin")
|
|
64
|
-
reconstructed = load_matrix(temporary_filename + ".fbin")
|
|
65
|
-
assert np.allclose(original, reconstructed)
|
|
66
|
-
os.remove(temporary_filename + ".fbin")
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@pytest.mark.parametrize("rows", batch_sizes)
|
|
70
|
-
@pytest.mark.parametrize("cols", dimensions)
|
|
71
|
-
def test_serializing_ibin_matrix(rows: int, cols: int):
|
|
72
|
-
"""
|
|
73
|
-
Test the serialization of integer binary matrix.
|
|
74
|
-
|
|
75
|
-
:param int rows: The number of rows in the matrix.
|
|
76
|
-
:param int cols: The number of columns in the matrix.
|
|
77
|
-
"""
|
|
78
|
-
original = np.random.randint(0, rows + 1, size=(rows, cols)).astype(np.int32)
|
|
79
|
-
save_matrix(original, temporary_filename + ".ibin")
|
|
80
|
-
reconstructed = load_matrix(temporary_filename + ".ibin")
|
|
81
|
-
assert np.allclose(original, reconstructed)
|
|
82
|
-
os.remove(temporary_filename + ".ibin")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@pytest.mark.parametrize("rows", batch_sizes)
|
|
86
|
-
@pytest.mark.parametrize("cols", dimensions)
|
|
87
|
-
def test_exact_search(rows: int, cols: int):
|
|
88
|
-
"""
|
|
89
|
-
Test exact search.
|
|
90
|
-
|
|
91
|
-
:param int rows: The number of rows in the matrix.
|
|
92
|
-
:param int cols: The number of columns in the matrix.
|
|
93
|
-
"""
|
|
94
|
-
original = np.random.rand(rows, cols)
|
|
95
|
-
matches: BatchMatches = search(original, original, 10, exact=True)
|
|
96
|
-
top_matches = (
|
|
97
|
-
[int(m.keys[0]) for m in matches] if rows > 1 else int(matches.keys[0])
|
|
98
|
-
)
|
|
99
|
-
assert np.all(top_matches == np.arange(rows))
|
|
100
|
-
|
|
101
|
-
matches: Matches = search(original, original[0], 10, exact=True)
|
|
102
|
-
top_match = int(matches.keys[0])
|
|
103
|
-
assert top_match == 0
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@pytest.mark.parametrize("ndim", dimensions)
|
|
107
|
-
@pytest.mark.parametrize("metric", continuous_metrics)
|
|
108
|
-
@pytest.mark.parametrize("index_type", index_types)
|
|
109
|
-
@pytest.mark.parametrize("numpy_type", numpy_types)
|
|
110
|
-
@pytest.mark.parametrize("connectivity", connectivity_options)
|
|
111
|
-
def test_index(
|
|
112
|
-
ndim: int,
|
|
113
|
-
metric: MetricKind,
|
|
114
|
-
index_type: ScalarKind,
|
|
115
|
-
numpy_type: str,
|
|
116
|
-
connectivity: int,
|
|
117
|
-
):
|
|
118
|
-
index = Index(
|
|
119
|
-
metric=metric,
|
|
120
|
-
ndim=ndim,
|
|
121
|
-
dtype=index_type,
|
|
122
|
-
connectivity=connectivity,
|
|
123
|
-
expansion_add=DEFAULT_EXPANSION_ADD,
|
|
124
|
-
expansion_search=DEFAULT_EXPANSION_SEARCH,
|
|
125
|
-
)
|
|
126
|
-
assert index.ndim == ndim
|
|
127
|
-
assert index.connectivity == connectivity
|
|
128
|
-
|
|
129
|
-
vector = random_vectors(count=1, ndim=ndim, dtype=numpy_type).flatten()
|
|
130
|
-
index.add(42, vector)
|
|
131
|
-
|
|
132
|
-
assert len(index) == 1, "Size after addition"
|
|
133
|
-
assert 42 in index, "Presence in the index"
|
|
134
|
-
assert 42 in index.keys, "Presence among keys"
|
|
135
|
-
assert 43 not in index, "Presence in the index, false positive"
|
|
136
|
-
assert index[42] is not None, "Vector recovery"
|
|
137
|
-
assert index[43] is None, "Vector recovery, false positive"
|
|
138
|
-
assert len(index[42]) == ndim
|
|
139
|
-
if numpy_type != np.byte:
|
|
140
|
-
assert np.allclose(index[42], vector, atol=0.1)
|
|
141
|
-
|
|
142
|
-
matches: Matches = index.search(vector, 10)
|
|
143
|
-
assert len(matches.keys) == 1, "Number of matches"
|
|
144
|
-
assert len(matches.keys) == len(matches.distances), "Symmetric match sub-arrays"
|
|
145
|
-
assert len({match.key for match in matches}) == 1, "Iteration over matches"
|
|
146
|
-
assert matches[0].key == 42
|
|
147
|
-
assert matches[0].distance == pytest.approx(0, abs=1e-3)
|
|
148
|
-
assert matches.computed_distances != 0
|
|
149
|
-
assert matches.visited_members != 0
|
|
150
|
-
|
|
151
|
-
# Validating the index structure and metadata:
|
|
152
|
-
assert index.max_level >= 0
|
|
153
|
-
assert index.levels_stats.nodes >= 1
|
|
154
|
-
assert index.level_stats(0).nodes == 1
|
|
155
|
-
assert str(index).startswith("usearch.")
|
|
156
|
-
|
|
157
|
-
# Try removals
|
|
158
|
-
other_vector = random_vectors(count=1, ndim=ndim, dtype=numpy_type).flatten()
|
|
159
|
-
index.add(43, other_vector)
|
|
160
|
-
assert len(index) == 2
|
|
161
|
-
index.remove(43)
|
|
162
|
-
assert len(index) == 1
|
|
163
|
-
|
|
164
|
-
# Try inserting back
|
|
165
|
-
index.add(43, other_vector)
|
|
166
|
-
assert len(index) == 2
|
|
167
|
-
index.remove(43)
|
|
168
|
-
assert len(index) == 1
|
|
169
|
-
|
|
170
|
-
index.save(temporary_usearch_filename)
|
|
171
|
-
|
|
172
|
-
# Re-populate cleared index
|
|
173
|
-
index.clear()
|
|
174
|
-
assert len(index) == 0
|
|
175
|
-
index.add(42, vector)
|
|
176
|
-
assert len(index) == 1
|
|
177
|
-
matches: Matches = index.search(vector, 10)
|
|
178
|
-
assert len(matches) == 1
|
|
179
|
-
|
|
180
|
-
index_copy = index.copy()
|
|
181
|
-
assert len(index_copy) == 1
|
|
182
|
-
assert len(index_copy[42]) == ndim
|
|
183
|
-
matches_copy: Matches = index_copy.search(vector, 10)
|
|
184
|
-
assert np.all(matches_copy.keys == matches.keys)
|
|
185
|
-
|
|
186
|
-
index.load(temporary_usearch_filename)
|
|
187
|
-
assert len(index) == 1
|
|
188
|
-
assert len(index[42]) == ndim
|
|
189
|
-
|
|
190
|
-
matches_loaded: Matches = index.search(vector, 10)
|
|
191
|
-
assert np.all(matches_loaded.keys == matches.keys)
|
|
192
|
-
|
|
193
|
-
index = Index.restore(temporary_usearch_filename, view=True)
|
|
194
|
-
assert len(index) == 1
|
|
195
|
-
assert len(index[42]) == ndim
|
|
196
|
-
|
|
197
|
-
matches_viewed: Matches = index.search(vector, 10)
|
|
198
|
-
assert np.all(matches_viewed.keys == matches.keys)
|
|
199
|
-
|
|
200
|
-
# Cleanup
|
|
201
|
-
index.reset()
|
|
202
|
-
os.remove(temporary_usearch_filename)
|
|
203
|
-
|
|
204
|
-
# Try opening a missing file
|
|
205
|
-
meta = Index.metadata(temporary_usearch_filename)
|
|
206
|
-
assert meta is None
|
|
207
|
-
index = Index.restore(temporary_usearch_filename)
|
|
208
|
-
assert index is None
|
|
209
|
-
|
|
210
|
-
# Try opening a corrupt file
|
|
211
|
-
with open(temporary_usearch_filename, "w") as file:
|
|
212
|
-
file.write("Some random string")
|
|
213
|
-
meta = Index.metadata(temporary_usearch_filename)
|
|
214
|
-
assert meta is None
|
|
215
|
-
index = Index.restore(temporary_usearch_filename)
|
|
216
|
-
|
|
217
|
-
assert index is None
|
|
218
|
-
os.remove(temporary_usearch_filename)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
@pytest.mark.parametrize("ndim", dimensions)
|
|
222
|
-
@pytest.mark.parametrize("metric", continuous_metrics)
|
|
223
|
-
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
224
|
-
@pytest.mark.parametrize("index_type", index_types)
|
|
225
|
-
@pytest.mark.parametrize("numpy_type", numpy_types)
|
|
226
|
-
def test_index_batch(
|
|
227
|
-
ndim: int,
|
|
228
|
-
metric: MetricKind,
|
|
229
|
-
batch_size: int,
|
|
230
|
-
index_type: ScalarKind,
|
|
231
|
-
numpy_type: str,
|
|
232
|
-
):
|
|
233
|
-
index = Index(ndim=ndim, metric=metric, dtype=index_type)
|
|
234
|
-
|
|
235
|
-
keys = np.arange(batch_size)
|
|
236
|
-
vectors = random_vectors(count=batch_size, ndim=ndim, dtype=numpy_type)
|
|
237
|
-
|
|
238
|
-
index.add(keys, vectors, threads=2)
|
|
239
|
-
assert len(index) == batch_size
|
|
240
|
-
assert np.allclose(index.get_vectors(keys).astype(numpy_type), vectors, atol=0.1)
|
|
241
|
-
|
|
242
|
-
matches: BatchMatches = index.search(vectors, 10, threads=2)
|
|
243
|
-
assert matches.keys.shape[0] == matches.distances.shape[0]
|
|
244
|
-
assert len(matches) == batch_size
|
|
245
|
-
assert np.all(np.sort(index.keys) == np.sort(keys))
|
|
246
|
-
|
|
247
|
-
if batch_size > 1:
|
|
248
|
-
assert index.max_level >= 1
|
|
249
|
-
else:
|
|
250
|
-
assert index.max_level >= 0
|
|
251
|
-
assert index.levels_stats.nodes >= batch_size
|
|
252
|
-
assert index.level_stats(0).nodes == batch_size
|
|
253
|
-
|
|
254
|
-
index.save(temporary_usearch_filename)
|
|
255
|
-
index.clear()
|
|
256
|
-
assert len(index) == 0
|
|
257
|
-
|
|
258
|
-
index.load(temporary_usearch_filename)
|
|
259
|
-
assert len(index) == batch_size
|
|
260
|
-
assert len(index[0]) == ndim
|
|
261
|
-
|
|
262
|
-
if batch_size > 1:
|
|
263
|
-
matches_loaded: BatchMatches = index.search(vectors, 10, threads=2)
|
|
264
|
-
for idx in range(len(matches_loaded)):
|
|
265
|
-
assert np.all(matches_loaded[idx].keys == matches[idx].keys)
|
|
266
|
-
|
|
267
|
-
index = Index.restore(temporary_usearch_filename, view=True)
|
|
268
|
-
assert len(index) == batch_size
|
|
269
|
-
assert len(index[0]) == ndim
|
|
270
|
-
|
|
271
|
-
if batch_size > 1:
|
|
272
|
-
matches_viewed: BatchMatches = index.search(vectors, 10, threads=2)
|
|
273
|
-
for idx in range(len(matches_viewed)):
|
|
274
|
-
assert np.all(matches_viewed[idx].keys == matches[idx].keys)
|
|
275
|
-
|
|
276
|
-
# Cleanup
|
|
277
|
-
index.reset()
|
|
278
|
-
os.remove(temporary_usearch_filename)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
@pytest.mark.parametrize("metric", [MetricKind.L2sq])
|
|
282
|
-
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
283
|
-
@pytest.mark.parametrize("index_type", index_types)
|
|
284
|
-
@pytest.mark.parametrize("numpy_type", numpy_types)
|
|
285
|
-
def test_exact_recall(
|
|
286
|
-
metric: MetricKind,
|
|
287
|
-
batch_size: int,
|
|
288
|
-
index_type: ScalarKind,
|
|
289
|
-
numpy_type: str,
|
|
290
|
-
):
|
|
291
|
-
ndim: int = batch_size
|
|
292
|
-
index = Index(ndim=ndim, metric=metric, dtype=index_type)
|
|
293
|
-
assert index.ndim == ndim
|
|
294
|
-
|
|
295
|
-
vectors = np.zeros((batch_size, ndim), dtype=numpy_type)
|
|
296
|
-
for i in range(batch_size):
|
|
297
|
-
vectors[i, i] = 1
|
|
298
|
-
keys = np.arange(batch_size)
|
|
299
|
-
index.add(keys, vectors)
|
|
300
|
-
assert len(index) == batch_size
|
|
301
|
-
|
|
302
|
-
# Search one at a time
|
|
303
|
-
for i in range(batch_size):
|
|
304
|
-
matches: Matches = index.search(vectors[i], 10, exact=True)
|
|
305
|
-
found_labels = matches.keys
|
|
306
|
-
assert found_labels[0] == i
|
|
307
|
-
assert matches.computed_distances == len(index)
|
|
308
|
-
assert matches.visited_members == 0, "Exact search won't traverse the graph"
|
|
309
|
-
|
|
310
|
-
# Search the whole batch
|
|
311
|
-
if batch_size > 1:
|
|
312
|
-
matches: BatchMatches = index.search(vectors, 10, exact=True)
|
|
313
|
-
assert matches.computed_distances == len(index) * len(vectors)
|
|
314
|
-
assert matches.visited_members == 0, "Exact search won't traverse the graph"
|
|
315
|
-
|
|
316
|
-
found_labels = matches.keys
|
|
317
|
-
for i in range(batch_size):
|
|
318
|
-
assert found_labels[i, 0] == i
|
|
319
|
-
|
|
320
|
-
# Match entries against themselves
|
|
321
|
-
index_copy: Index = index.copy()
|
|
322
|
-
mapping: dict = index.join(index_copy, exact=True)
|
|
323
|
-
for man, woman in mapping.items():
|
|
324
|
-
assert man == woman, "Stable marriage failed"
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def test_indexes():
|
|
328
|
-
ndim = 10
|
|
329
|
-
index_a = Index(ndim=ndim)
|
|
330
|
-
index_b = Index(ndim=ndim)
|
|
331
|
-
|
|
332
|
-
vectors = random_vectors(count=3, ndim=ndim)
|
|
333
|
-
index_a.add(42, vectors[0])
|
|
334
|
-
index_b.add(43, vectors[1])
|
|
335
|
-
|
|
336
|
-
indexes = Indexes([index_a, index_b])
|
|
337
|
-
matches = indexes.search(vectors[2], 10)
|
|
338
|
-
assert len(matches) == 2
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
@pytest.mark.parametrize("bits", dimensions)
|
|
342
|
-
@pytest.mark.parametrize("metric", hash_metrics)
|
|
343
|
-
@pytest.mark.parametrize("connectivity", connectivity_options)
|
|
344
|
-
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
345
|
-
def test_bitwise_index(
|
|
346
|
-
bits: int,
|
|
347
|
-
metric: MetricKind,
|
|
348
|
-
connectivity: int,
|
|
349
|
-
batch_size: int,
|
|
350
|
-
):
|
|
351
|
-
index = Index(ndim=bits, metric=metric, connectivity=connectivity)
|
|
352
|
-
|
|
353
|
-
keys = np.arange(batch_size)
|
|
354
|
-
byte_vectors = np.random.randint(2, size=(batch_size, bits))
|
|
355
|
-
bit_vectors = np.packbits(byte_vectors, axis=1)
|
|
356
|
-
|
|
357
|
-
index.add(keys, bit_vectors)
|
|
358
|
-
assert np.all(index.get_vectors(keys, ScalarKind.B1) == bit_vectors)
|
|
359
|
-
|
|
360
|
-
index.search(bit_vectors, 10)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if __name__ == "__main__":
|
|
364
|
-
pytest.main(args=["python/scripts/test.py", "-s", "-x", "-v"])
|