usearch 1.2.2 → 1.3.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 +26 -0
- package/.github/workflows/release.yml +1 -1
- package/CMakeLists.txt +1 -0
- package/Cargo.toml +1 -1
- package/README.md +16 -10
- package/c/lib.cpp +35 -1
- package/c/test.c +117 -114
- package/c/usearch.h +28 -1
- package/conanfile.py +1 -1
- package/cpp/test.cpp +2 -2
- package/docs/benchmarks.md +6 -6
- package/docs/compilation.md +1 -1
- package/include/usearch/index.hpp +172 -52
- package/include/usearch/index_dense.hpp +194 -57
- package/include/usearch/index_plugins.hpp +34 -20
- package/package.json +2 -2
- package/python/lib.cpp +93 -31
- package/python/scripts/test_index.py +38 -10
- package/python/usearch/index.py +87 -37
- package/rust/README.md +2 -2
- package/rust/lib.cpp +15 -4
- package/rust/lib.hpp +5 -0
- package/rust/lib.rs +75 -43
- package/wasmer.toml +1 -1
|
@@ -17,6 +17,32 @@ permissions:
|
|
|
17
17
|
|
|
18
18
|
jobs:
|
|
19
19
|
|
|
20
|
+
test_c:
|
|
21
|
+
name: Test C
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v3
|
|
25
|
+
with:
|
|
26
|
+
ref: main-dev
|
|
27
|
+
- run: git submodule update --init --recursive
|
|
28
|
+
- name: Prepare Environment
|
|
29
|
+
run: |
|
|
30
|
+
sudo apt update &&
|
|
31
|
+
sudo apt install -y cmake g++-12 build-essential libjemalloc-dev
|
|
32
|
+
- name: Build
|
|
33
|
+
run: |
|
|
34
|
+
cmake -B ./build_release \
|
|
35
|
+
-DCMAKE_CXX_COMPILER="g++-12" \
|
|
36
|
+
-DCMAKE_BUILD_TYPE=Release \
|
|
37
|
+
-DUSEARCH_BUILD_TEST=1 \
|
|
38
|
+
-DUSEARCH_USE_OPENMP=1 \
|
|
39
|
+
-DUSEARCH_USE_SIMSIMD=1 \
|
|
40
|
+
-DUSEARCH_USE_JEMALLOC=1 \
|
|
41
|
+
-DUSEARCH_BUILD_BENCHMARK=0 &&
|
|
42
|
+
make -C ./build_release -j
|
|
43
|
+
- name: Run tests
|
|
44
|
+
run: ./build_release/test_c
|
|
45
|
+
|
|
20
46
|
test_cpp:
|
|
21
47
|
name: Test C++
|
|
22
48
|
runs-on: ubuntu-latest
|
|
@@ -287,7 +287,7 @@ jobs:
|
|
|
287
287
|
run: |
|
|
288
288
|
sudo apt update &&
|
|
289
289
|
sudo apt install -y doxygen graphviz dia git &&
|
|
290
|
-
pip install sphinx==
|
|
290
|
+
pip install sphinx==5.3.0 sphinx-js==3.2.1 breathe==4.35.0 furo==2023.3.27 m2r2==0.3.3.post2 sphinxcontrib-googleanalytics==0.2.dev20220708 sphinxcontrib-jquery==4.1 &&
|
|
291
291
|
npm install -g jsdoc
|
|
292
292
|
- name: Install USearch from PyPi
|
|
293
293
|
run: pip install usearch
|
package/CMakeLists.txt
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -109,10 +109,10 @@ Comparing the performance of FAISS against USearch on 1 Million 96-dimensional v
|
|
|
109
109
|
|
|
110
110
|
| | FAISS, `f32` | USearch, `f32` | USearch, `f16` | USearch, `i8` |
|
|
111
111
|
| :----------- | -----------: | -------------: | -------------: | ----------------: |
|
|
112
|
-
| Batch Insert | 16 K/s | 73 K/s | 100 K/s | 104 K/s
|
|
113
|
-
| Batch Search | 82 K/s | 103 K/s | 113 K/s | 134 K/s
|
|
114
|
-
| Bulk Insert | 76 K/s | 105 K/s | 115 K/s | 202 K/s
|
|
115
|
-
| Bulk Search | 118 K/s | 174 K/s | 173 K/s | 304 K/s
|
|
112
|
+
| Batch Insert | 16 K/s | 73 K/s | 100 K/s | 104 K/s __+550%__ |
|
|
113
|
+
| Batch Search | 82 K/s | 103 K/s | 113 K/s | 134 K/s __+63%__ |
|
|
114
|
+
| Bulk Insert | 76 K/s | 105 K/s | 115 K/s | 202 K/s __+165%__ |
|
|
115
|
+
| Bulk Search | 118 K/s | 174 K/s | 173 K/s | 304 K/s __+157%__ |
|
|
116
116
|
| Recall @ 10 | 99% | 99.2% | 99.1% | 99.2% |
|
|
117
117
|
|
|
118
118
|
> HNSW was configured with identical hyper-parameters:
|
|
@@ -152,10 +152,16 @@ Instead, we have focused on high-precision arithmetic over low-precision downcas
|
|
|
152
152
|
The same index, and `add` and `search` operations will automatically down-cast or up-cast between `f32_t`, `f16_t`, `f64_t`, and `i8_t` representations, even if the hardware doesn't natively support it.
|
|
153
153
|
Continuing the topic of memory efficiency, we provide a `uint40_t` to allow collection with over 4B+ vectors without allocating 8 bytes for every neighbor reference in the proximity graph.
|
|
154
154
|
|
|
155
|
-
## Serving `Index` from Disk
|
|
155
|
+
## Serialization & Serving `Index` from Disk
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
USearch supports multiple forms of serialization:
|
|
158
|
+
|
|
159
|
+
- Into a __file__ defined with a path.
|
|
160
|
+
- Into a __stream__ defined with a callback, serializing or reconstructing incrementally.
|
|
161
|
+
- Into a __buffer__ of fixed length, or a memory-mapped file, that supports random access.
|
|
162
|
+
|
|
163
|
+
The latter allows you to serve indexes from external memory, enabling you to optimize your server choices for indexing speed and serving costs.
|
|
164
|
+
This can result in __20x cost reduction__ on AWS and other public clouds.
|
|
159
165
|
|
|
160
166
|
```py
|
|
161
167
|
index.save("index.usearch")
|
|
@@ -186,10 +192,10 @@ many_in_many: BatchMatches = search(vectors, vectors, 50, MetricKind.L2sq, exact
|
|
|
186
192
|
```
|
|
187
193
|
|
|
188
194
|
By passing the `exact=True` argument, the system bypasses indexing altogether and performs a brute-force search through the entire dataset using SIMD-optimized similarity metrics from [SimSIMD](https://github.com/ashvardanian/simsimd).
|
|
189
|
-
When compared to FAISS's `IndexFlatL2` in Google Colab,
|
|
195
|
+
When compared to FAISS's `IndexFlatL2` in Google Colab, __[USearch may offer up to a 20x performance improvement](https://github.com/unum-cloud/usearch/issues/176#issuecomment-1666650778)__:
|
|
190
196
|
|
|
191
|
-
- `faiss.IndexFlatL2`:
|
|
192
|
-
- `usearch.index.search`:
|
|
197
|
+
- `faiss.IndexFlatL2`: __55.3 ms__.
|
|
198
|
+
- `usearch.index.search`: __2.54 ms__.
|
|
193
199
|
|
|
194
200
|
## `Indexes` for Multi-Index Lookups
|
|
195
201
|
|
package/c/lib.cpp
CHANGED
|
@@ -54,7 +54,7 @@ add_result_t add_(index_dense_t* index, usearch_key_t key, void const* vector, s
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
std::size_t get_(index_dense_t* index, usearch_key_t key, size_t count, void* vector, scalar_kind_t kind) {
|
|
58
58
|
switch (kind) {
|
|
59
59
|
case scalar_kind_t::f32_k: return index->get(key, (f32_t*)vector, count);
|
|
60
60
|
case scalar_kind_t::f64_k: return index->get(key, (f64_t*)vector, count);
|
|
@@ -103,6 +103,11 @@ USEARCH_EXPORT void usearch_free(usearch_index_t index, usearch_error_t*) {
|
|
|
103
103
|
delete reinterpret_cast<index_dense_t*>(index);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
USEARCH_EXPORT size_t usearch_serialized_length(usearch_index_t index, usearch_error_t*) {
|
|
107
|
+
assert(index);
|
|
108
|
+
return reinterpret_cast<index_dense_t*>(index)->serialized_length();
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
USEARCH_EXPORT void usearch_save(usearch_index_t index, char const* path, usearch_error_t* error) {
|
|
107
112
|
|
|
108
113
|
assert(index && path && error);
|
|
@@ -127,6 +132,35 @@ USEARCH_EXPORT void usearch_view(usearch_index_t index, char const* path, usearc
|
|
|
127
132
|
*error = result.error.release();
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
USEARCH_EXPORT void usearch_save_buffer(usearch_index_t index, void* buffer, size_t length, usearch_error_t* error) {
|
|
136
|
+
|
|
137
|
+
assert(index && buffer && length && error);
|
|
138
|
+
memory_mapped_file_t memory_map((byte_t*)buffer, length);
|
|
139
|
+
serialization_result_t result = reinterpret_cast<index_dense_t*>(index)->save(std::move(memory_map));
|
|
140
|
+
if (!result)
|
|
141
|
+
*error = result.error.release();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
USEARCH_EXPORT void usearch_load_buffer(usearch_index_t index, void const* buffer, size_t length,
|
|
145
|
+
usearch_error_t* error) {
|
|
146
|
+
|
|
147
|
+
assert(index && buffer && length && error);
|
|
148
|
+
memory_mapped_file_t memory_map((byte_t*)buffer, length);
|
|
149
|
+
serialization_result_t result = reinterpret_cast<index_dense_t*>(index)->load(std::move(memory_map));
|
|
150
|
+
if (!result)
|
|
151
|
+
*error = result.error.release();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
USEARCH_EXPORT void usearch_view_buffer(usearch_index_t index, void const* buffer, size_t length,
|
|
155
|
+
usearch_error_t* error) {
|
|
156
|
+
|
|
157
|
+
assert(index && buffer && length && error);
|
|
158
|
+
memory_mapped_file_t memory_map((byte_t*)buffer, length);
|
|
159
|
+
serialization_result_t result = reinterpret_cast<index_dense_t*>(index)->view(std::move(memory_map));
|
|
160
|
+
if (!result)
|
|
161
|
+
*error = result.error.release();
|
|
162
|
+
}
|
|
163
|
+
|
|
130
164
|
USEARCH_EXPORT size_t usearch_size(usearch_index_t index, usearch_error_t*) { //
|
|
131
165
|
return reinterpret_cast<index_dense_t*>(index)->size();
|
|
132
166
|
}
|
package/c/test.c
CHANGED
|
@@ -13,46 +13,18 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ASSERT(file, "Failed to open the file");
|
|
23
|
-
|
|
24
|
-
// Read vectors dimension and calculate count
|
|
25
|
-
int dim = 0;
|
|
26
|
-
size_t read = fread(&dim, sizeof(int), 1, file);
|
|
27
|
-
ASSERT(read == 1, "Failed to read from the file");
|
|
28
|
-
ASSERT(st.st_size % ((dim + 1) * sizeof(float)) == 0, "File does not contain a whole number of vectors");
|
|
29
|
-
*dimension = (size_t)dim;
|
|
30
|
-
*count = st.st_size / ((dim + 1) * sizeof(float));
|
|
31
|
-
|
|
32
|
-
// Allocate memory for the vectors' data
|
|
33
|
-
*data = (float*)malloc(*count * *dimension * sizeof(float));
|
|
34
|
-
ASSERT(*data, "Failed to allocate memory");
|
|
35
|
-
|
|
36
|
-
// Read the data
|
|
37
|
-
for (size_t i = 0; i < *count; ++i) {
|
|
38
|
-
read = fread(*data + i * *dimension, sizeof(float), *dimension, file);
|
|
39
|
-
ASSERT(read == *dimension, "Failed to read from the file");
|
|
40
|
-
if (i == *count - 1)
|
|
41
|
-
break;
|
|
42
|
-
|
|
43
|
-
// Skip
|
|
44
|
-
float skip;
|
|
45
|
-
read = fread(&skip, sizeof(float), 1, file);
|
|
46
|
-
ASSERT(read == 1, "Failed to read from the file");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
fclose(file);
|
|
16
|
+
float* create_vectors(size_t count, size_t dimensions) {
|
|
17
|
+
float* data = (float*)malloc(count * dimensions * sizeof(float));
|
|
18
|
+
ASSERT(data, "Failed to allocate memory");
|
|
19
|
+
for (size_t idx = 0; idx < count * dimensions; ++idx)
|
|
20
|
+
data[idx] = (float)rand() / (float)RAND_MAX;
|
|
21
|
+
return data;
|
|
50
22
|
}
|
|
51
23
|
|
|
52
|
-
usearch_init_options_t create_options(size_t
|
|
24
|
+
usearch_init_options_t create_options(size_t dimensions) {
|
|
53
25
|
usearch_init_options_t opts;
|
|
54
26
|
opts.connectivity = 2; // 32 in faiss
|
|
55
|
-
opts.dimensions =
|
|
27
|
+
opts.dimensions = dimensions;
|
|
56
28
|
opts.expansion_add = 40; // 40 in faiss
|
|
57
29
|
opts.expansion_search = 16; // 10 in faiss
|
|
58
30
|
opts.metric_kind = usearch_metric_ip_k;
|
|
@@ -61,14 +33,13 @@ usearch_init_options_t create_options(size_t vector_dimension) {
|
|
|
61
33
|
return opts;
|
|
62
34
|
}
|
|
63
35
|
|
|
64
|
-
void test_init(size_t
|
|
36
|
+
void test_init(size_t collection_size, size_t dimensions) {
|
|
65
37
|
printf("Test: Index Initialization...\n");
|
|
66
38
|
|
|
67
39
|
// Init index
|
|
68
|
-
usearch_index_t idx = NULL;
|
|
69
40
|
usearch_error_t error = NULL;
|
|
70
|
-
usearch_init_options_t opts = create_options(
|
|
71
|
-
idx = usearch_init(&opts, &error);
|
|
41
|
+
usearch_init_options_t opts = create_options(dimensions);
|
|
42
|
+
usearch_index_t idx = usearch_init(&opts, &error);
|
|
72
43
|
ASSERT(!error, error);
|
|
73
44
|
usearch_free(idx, &error);
|
|
74
45
|
ASSERT(!error, error);
|
|
@@ -79,15 +50,15 @@ void test_init(size_t vectors_count, size_t vector_dimension) {
|
|
|
79
50
|
|
|
80
51
|
ASSERT(usearch_size(idx, &error) == 0, error);
|
|
81
52
|
ASSERT(usearch_capacity(idx, &error) == 0, error);
|
|
82
|
-
ASSERT(usearch_dimensions(idx, &error) ==
|
|
53
|
+
ASSERT(usearch_dimensions(idx, &error) == dimensions, error);
|
|
83
54
|
ASSERT(usearch_connectivity(idx, &error) == opts.connectivity, error);
|
|
84
55
|
|
|
85
56
|
// Reserve
|
|
86
|
-
usearch_reserve(idx,
|
|
57
|
+
usearch_reserve(idx, collection_size, &error);
|
|
87
58
|
ASSERT(!error, error);
|
|
88
59
|
ASSERT(usearch_size(idx, &error) == 0, error);
|
|
89
|
-
ASSERT(usearch_capacity(idx, &error) ==
|
|
90
|
-
ASSERT(usearch_dimensions(idx, &error) ==
|
|
60
|
+
ASSERT(usearch_capacity(idx, &error) == collection_size, error);
|
|
61
|
+
ASSERT(usearch_dimensions(idx, &error) == dimensions, error);
|
|
91
62
|
ASSERT(usearch_connectivity(idx, &error) == opts.connectivity, error);
|
|
92
63
|
|
|
93
64
|
usearch_free(idx, &error);
|
|
@@ -96,110 +67,148 @@ void test_init(size_t vectors_count, size_t vector_dimension) {
|
|
|
96
67
|
printf("Test: Index Initialization - PASSED\n");
|
|
97
68
|
}
|
|
98
69
|
|
|
99
|
-
void test_add_vector(size_t
|
|
70
|
+
void test_add_vector(size_t collection_size, size_t dimensions) {
|
|
100
71
|
printf("Test: Add Vector...\n");
|
|
101
72
|
|
|
102
|
-
usearch_index_t idx = NULL;
|
|
103
73
|
usearch_error_t error = NULL;
|
|
104
|
-
usearch_init_options_t opts = create_options(
|
|
105
|
-
idx = usearch_init(&opts, &error);
|
|
106
|
-
usearch_reserve(idx,
|
|
74
|
+
usearch_init_options_t opts = create_options(dimensions);
|
|
75
|
+
usearch_index_t idx = usearch_init(&opts, &error);
|
|
76
|
+
usearch_reserve(idx, collection_size, &error);
|
|
107
77
|
|
|
108
78
|
// Add vectors
|
|
109
|
-
|
|
79
|
+
float* data = create_vectors(collection_size, dimensions);
|
|
80
|
+
for (size_t i = 0; i < collection_size; ++i) {
|
|
110
81
|
usearch_key_t key = i;
|
|
111
|
-
usearch_add(idx, key, data + i *
|
|
82
|
+
usearch_add(idx, key, data + i * dimensions, usearch_scalar_f32_k, &error);
|
|
112
83
|
ASSERT(!error, error);
|
|
113
84
|
}
|
|
114
85
|
|
|
115
|
-
ASSERT(usearch_size(idx, &error) ==
|
|
116
|
-
ASSERT(usearch_capacity(idx, &error) ==
|
|
86
|
+
ASSERT(usearch_size(idx, &error) == collection_size, error);
|
|
87
|
+
ASSERT(usearch_capacity(idx, &error) == collection_size, error);
|
|
117
88
|
|
|
118
89
|
// Check vectors in the index
|
|
119
|
-
for (size_t i = 0; i <
|
|
90
|
+
for (size_t i = 0; i < collection_size; ++i) {
|
|
120
91
|
usearch_key_t key = i;
|
|
121
92
|
ASSERT(usearch_contains(idx, key, &error), error);
|
|
122
93
|
}
|
|
123
94
|
ASSERT(!usearch_contains(idx, -1, &error), error); // Non existing key
|
|
124
95
|
|
|
96
|
+
free(data);
|
|
125
97
|
usearch_free(idx, &error);
|
|
126
98
|
printf("Test: Add Vector - PASSED\n");
|
|
127
99
|
}
|
|
128
100
|
|
|
129
|
-
void test_find_vector(size_t
|
|
101
|
+
void test_find_vector(size_t collection_size, size_t dimensions) {
|
|
130
102
|
printf("Test: Find Vector...\n");
|
|
131
103
|
|
|
132
|
-
usearch_index_t idx = NULL;
|
|
133
104
|
usearch_error_t error = NULL;
|
|
134
|
-
usearch_init_options_t opts = create_options(
|
|
135
|
-
idx = usearch_init(&opts, &error);
|
|
136
|
-
usearch_reserve(idx,
|
|
105
|
+
usearch_init_options_t opts = create_options(dimensions);
|
|
106
|
+
usearch_index_t idx = usearch_init(&opts, &error);
|
|
107
|
+
usearch_reserve(idx, collection_size, &error);
|
|
137
108
|
|
|
138
109
|
// Create result buffers
|
|
139
|
-
int results_count =
|
|
110
|
+
int results_count = collection_size;
|
|
140
111
|
usearch_key_t* keys = (usearch_key_t*)malloc(results_count * sizeof(usearch_key_t));
|
|
141
112
|
float* distances = (float*)malloc(results_count * sizeof(float));
|
|
142
113
|
ASSERT(keys && distances, "Failed to allocate memory");
|
|
143
114
|
|
|
144
115
|
// Add vectors
|
|
145
|
-
|
|
116
|
+
float* data = create_vectors(collection_size, dimensions);
|
|
117
|
+
for (size_t i = 0; i < collection_size; ++i) {
|
|
146
118
|
usearch_key_t key = i;
|
|
147
|
-
usearch_add(idx, key, data + i *
|
|
119
|
+
usearch_add(idx, key, data + i * dimensions, usearch_scalar_f32_k, &error);
|
|
148
120
|
ASSERT(!error, error);
|
|
149
121
|
}
|
|
150
122
|
|
|
151
123
|
// Find the vectors
|
|
152
|
-
for (size_t i = 0; i <
|
|
153
|
-
const void *query_vector = data + i *
|
|
124
|
+
for (size_t i = 0; i < collection_size; i++) {
|
|
125
|
+
const void *query_vector = data + i * dimensions;
|
|
154
126
|
size_t found_count = usearch_search(idx, query_vector, usearch_scalar_f32_k, results_count, keys, distances, &error);
|
|
155
127
|
ASSERT(!error, error);
|
|
156
128
|
ASSERT(found_count = results_count, "Vector is missing");
|
|
157
129
|
}
|
|
158
130
|
|
|
131
|
+
free(data);
|
|
132
|
+
free(keys);
|
|
133
|
+
free(distances);
|
|
159
134
|
usearch_free(idx, &error);
|
|
160
135
|
printf("Test: Find Vector - PASSED\n");
|
|
161
136
|
}
|
|
162
137
|
|
|
163
|
-
void
|
|
138
|
+
void test_get_vector(size_t collection_size, size_t dimensions) {
|
|
139
|
+
printf("Test: Get Vector...\n");
|
|
140
|
+
|
|
141
|
+
usearch_error_t error = NULL;
|
|
142
|
+
usearch_init_options_t opts = create_options(dimensions);
|
|
143
|
+
opts.multi = true;
|
|
144
|
+
usearch_index_t idx = usearch_init(&opts, &error);
|
|
145
|
+
usearch_reserve(idx, collection_size, &error);
|
|
146
|
+
|
|
147
|
+
// Create result buffers
|
|
148
|
+
int results_count = collection_size;
|
|
149
|
+
float* vectors = (float*)malloc(results_count * dimensions * sizeof(float));
|
|
150
|
+
ASSERT(vectors, "Failed to allocate memory");
|
|
151
|
+
|
|
152
|
+
// Add multiple vectors with SAME key
|
|
153
|
+
usearch_key_t const key = 1;
|
|
154
|
+
float* data = create_vectors(collection_size, dimensions);
|
|
155
|
+
for (size_t i = 0; i < collection_size; i++) {
|
|
156
|
+
usearch_add(idx, key, data + i * dimensions, usearch_scalar_f32_k, &error);
|
|
157
|
+
ASSERT(!error, error);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Retrieve vectors from index
|
|
161
|
+
size_t count = usearch_get(idx, key, collection_size, vectors, usearch_scalar_f32_k, &error);
|
|
162
|
+
ASSERT(count == results_count, "Vector is missing");
|
|
163
|
+
|
|
164
|
+
free(vectors);
|
|
165
|
+
free(data);
|
|
166
|
+
usearch_free(idx, &error);
|
|
167
|
+
|
|
168
|
+
printf("Test: Get Vector - PASSED\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
void test_remove_vector(size_t collection_size, size_t dimensions) {
|
|
164
172
|
printf("Test: Remove Vector...\n");
|
|
165
173
|
|
|
166
|
-
usearch_index_t idx = NULL;
|
|
167
174
|
usearch_error_t error = NULL;
|
|
168
|
-
usearch_init_options_t opts = create_options(
|
|
169
|
-
idx = usearch_init(&opts, &error);
|
|
170
|
-
usearch_reserve(idx,
|
|
175
|
+
usearch_init_options_t opts = create_options(dimensions);
|
|
176
|
+
usearch_index_t idx = usearch_init(&opts, &error);
|
|
177
|
+
usearch_reserve(idx, collection_size, &error);
|
|
171
178
|
|
|
172
179
|
// Add vectors
|
|
173
|
-
|
|
180
|
+
float* data = create_vectors(collection_size, dimensions);
|
|
181
|
+
for (size_t i = 0; i < collection_size; ++i) {
|
|
174
182
|
usearch_key_t key = i;
|
|
175
|
-
usearch_add(idx, key, data + i *
|
|
183
|
+
usearch_add(idx, key, data + i * dimensions, usearch_scalar_f32_k, &error);
|
|
176
184
|
ASSERT(!error, error);
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
// Remove the vectors
|
|
180
|
-
for (size_t i = 0; i <
|
|
188
|
+
for (size_t i = 0; i < collection_size; i++) {
|
|
181
189
|
usearch_key_t key = i;
|
|
182
190
|
usearch_remove(idx, key, &error);
|
|
183
|
-
ASSERT(error, "Currently, Remove is not supported");
|
|
191
|
+
ASSERT(!error, "Currently, Remove is not supported");
|
|
184
192
|
}
|
|
185
193
|
|
|
194
|
+
free(data);
|
|
186
195
|
usearch_free(idx, &error);
|
|
187
196
|
printf("Test: Remove Vector - PASSED\n");
|
|
188
197
|
}
|
|
189
198
|
|
|
190
|
-
void test_save_load(size_t
|
|
199
|
+
void test_save_load(size_t collection_size, size_t dimensions) {
|
|
191
200
|
printf("Test: Save/Load...\n");
|
|
192
201
|
|
|
193
|
-
usearch_index_t idx = NULL;
|
|
194
202
|
usearch_error_t error = NULL;
|
|
195
|
-
usearch_init_options_t opts = create_options(
|
|
196
|
-
idx = usearch_init(&opts, &error);
|
|
197
|
-
usearch_reserve(idx,
|
|
203
|
+
usearch_init_options_t opts = create_options(dimensions);
|
|
204
|
+
usearch_index_t idx = usearch_init(&opts, &error);
|
|
205
|
+
usearch_reserve(idx, collection_size, &error);
|
|
198
206
|
|
|
199
207
|
// Add vectors
|
|
200
|
-
|
|
208
|
+
float* data = create_vectors(collection_size, dimensions);
|
|
209
|
+
for (size_t i = 0; i < collection_size; ++i) {
|
|
201
210
|
usearch_key_t key = i;
|
|
202
|
-
usearch_add(idx, key, data + i *
|
|
211
|
+
usearch_add(idx, key, data + i * dimensions, usearch_scalar_f32_k, &error);
|
|
203
212
|
ASSERT(!error, error);
|
|
204
213
|
}
|
|
205
214
|
|
|
@@ -217,34 +226,35 @@ void test_save_load(size_t vectors_count, size_t vector_dimension, float const*
|
|
|
217
226
|
// Load
|
|
218
227
|
usearch_load(idx, "usearch_index.bin", &error);
|
|
219
228
|
ASSERT(!error, error);
|
|
220
|
-
ASSERT(usearch_size(idx, &error) ==
|
|
221
|
-
ASSERT(usearch_capacity(idx, &error) ==
|
|
222
|
-
ASSERT(usearch_dimensions(idx, &error) ==
|
|
229
|
+
ASSERT(usearch_size(idx, &error) == collection_size, error);
|
|
230
|
+
ASSERT(usearch_capacity(idx, &error) == collection_size, error);
|
|
231
|
+
ASSERT(usearch_dimensions(idx, &error) == dimensions, error);
|
|
223
232
|
ASSERT(usearch_connectivity(idx, &error) == opts.connectivity, error);
|
|
224
233
|
|
|
225
234
|
// Check vectors in the index
|
|
226
|
-
for (size_t i = 0; i <
|
|
235
|
+
for (size_t i = 0; i < collection_size; ++i) {
|
|
227
236
|
usearch_key_t key = i;
|
|
228
237
|
ASSERT(usearch_contains(idx, key, &error), error);
|
|
229
238
|
}
|
|
230
239
|
|
|
240
|
+
free(data);
|
|
231
241
|
usearch_free(idx, &error);
|
|
232
242
|
printf("Test: Save/Load - PASSED\n");
|
|
233
243
|
}
|
|
234
244
|
|
|
235
|
-
void test_view(size_t
|
|
245
|
+
void test_view(size_t collection_size, size_t dimensions) {
|
|
236
246
|
printf("Test: View...\n");
|
|
237
247
|
|
|
238
|
-
usearch_index_t idx = NULL;
|
|
239
248
|
usearch_error_t error = NULL;
|
|
240
|
-
usearch_init_options_t opts = create_options(
|
|
241
|
-
idx = usearch_init(&opts, &error);
|
|
242
|
-
usearch_reserve(idx,
|
|
249
|
+
usearch_init_options_t opts = create_options(dimensions);
|
|
250
|
+
usearch_index_t idx = usearch_init(&opts, &error);
|
|
251
|
+
usearch_reserve(idx, collection_size, &error);
|
|
243
252
|
|
|
244
253
|
// Add vectors
|
|
245
|
-
|
|
254
|
+
float* data = create_vectors(collection_size, dimensions);
|
|
255
|
+
for (size_t i = 0; i < collection_size; ++i) {
|
|
246
256
|
usearch_key_t key = i;
|
|
247
|
-
usearch_add(idx, key, data + i *
|
|
257
|
+
usearch_add(idx, key, data + i * dimensions, usearch_scalar_f32_k, &error);
|
|
248
258
|
ASSERT(!error, error);
|
|
249
259
|
}
|
|
250
260
|
|
|
@@ -262,33 +272,26 @@ void test_view(size_t vectors_count, size_t vector_dimension, float const* data)
|
|
|
262
272
|
usearch_view(idx, "usearch_index.bin", &error);
|
|
263
273
|
ASSERT(!error, error);
|
|
264
274
|
|
|
275
|
+
free(data);
|
|
265
276
|
usearch_free(idx, &error);
|
|
266
277
|
printf("Test: View - PASSED\n");
|
|
267
278
|
}
|
|
268
279
|
|
|
269
|
-
int main(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
test_add_vector(vectors_count, vector_dimension, data);
|
|
285
|
-
test_find_vector(vectors_count, vector_dimension, data);
|
|
286
|
-
test_remove_vector(vectors_count, vector_dimension, data);
|
|
287
|
-
test_save_load(vectors_count, vector_dimension, data);
|
|
288
|
-
test_view(vectors_count, vector_dimension, data);
|
|
289
|
-
|
|
290
|
-
// Free vectors' buffer
|
|
291
|
-
free(data);
|
|
280
|
+
int main() {
|
|
281
|
+
|
|
282
|
+
size_t collection_sizes[] = {11, 512};
|
|
283
|
+
size_t dimensions[] = {83, 1};
|
|
284
|
+
for (size_t idx = 0; idx < sizeof(collection_sizes) / sizeof(collection_sizes[0]); ++idx) {
|
|
285
|
+
for (size_t jdx = 0; jdx < sizeof(dimensions) / sizeof(dimensions[0]); ++jdx) {
|
|
286
|
+
test_init(collection_sizes[idx], dimensions[jdx]);
|
|
287
|
+
test_add_vector(collection_sizes[idx], dimensions[jdx]);
|
|
288
|
+
test_find_vector(collection_sizes[idx], dimensions[jdx]);
|
|
289
|
+
test_get_vector(collection_sizes[idx], dimensions[jdx]);
|
|
290
|
+
test_remove_vector(collection_sizes[idx], dimensions[jdx]);
|
|
291
|
+
test_save_load(collection_sizes[idx], dimensions[jdx]);
|
|
292
|
+
test_view(collection_sizes[idx], dimensions[jdx]);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
292
295
|
|
|
293
296
|
return 0;
|
|
294
297
|
}
|
package/c/usearch.h
CHANGED
|
@@ -109,6 +109,12 @@ USEARCH_EXPORT usearch_index_t usearch_init(usearch_init_options_t* options, use
|
|
|
109
109
|
*/
|
|
110
110
|
USEARCH_EXPORT void usearch_free(usearch_index_t, usearch_error_t* error);
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* @brief Reports expected file size after serialization.
|
|
114
|
+
* @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
|
|
115
|
+
*/
|
|
116
|
+
USEARCH_EXPORT size_t usearch_serialized_length(usearch_index_t, usearch_error_t* error);
|
|
117
|
+
|
|
112
118
|
/**
|
|
113
119
|
* @brief Saves the index to a file.
|
|
114
120
|
* @param[in] path The file path where the index will be saved.
|
|
@@ -124,12 +130,33 @@ USEARCH_EXPORT void usearch_save(usearch_index_t, char const* path, usearch_erro
|
|
|
124
130
|
USEARCH_EXPORT void usearch_load(usearch_index_t, char const* path, usearch_error_t* error);
|
|
125
131
|
|
|
126
132
|
/**
|
|
127
|
-
* @brief Creates a view of the index from a file without
|
|
133
|
+
* @brief Creates a view of the index from a file without copying it into memory.
|
|
128
134
|
* @param[in] path The file path from where the view will be created.
|
|
129
135
|
* @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
|
|
130
136
|
*/
|
|
131
137
|
USEARCH_EXPORT void usearch_view(usearch_index_t, char const* path, usearch_error_t* error);
|
|
132
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @brief Saves the index to an in-memory buffer.
|
|
141
|
+
* @param[in] buffer The in-memory continuous buffer where the index will be saved.
|
|
142
|
+
* @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
|
|
143
|
+
*/
|
|
144
|
+
USEARCH_EXPORT void usearch_save_buffer(usearch_index_t, void* buffer, size_t length, usearch_error_t* error);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @brief Loads the index from an in-memory buffer.
|
|
148
|
+
* @param[in] buffer The in-memory continuous buffer from where the index will be loaded.
|
|
149
|
+
* @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
|
|
150
|
+
*/
|
|
151
|
+
USEARCH_EXPORT void usearch_load_buffer(usearch_index_t, void const* buffer, size_t length, usearch_error_t* error);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @brief Creates a view of the index from an in-memory buffer without copying it into memory.
|
|
155
|
+
* @param[in] buffer The in-memory continuous buffer from where the view will be created.
|
|
156
|
+
* @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
|
|
157
|
+
*/
|
|
158
|
+
USEARCH_EXPORT void usearch_view_buffer(usearch_index_t, void const* buffer, size_t length, usearch_error_t* error);
|
|
159
|
+
|
|
133
160
|
USEARCH_EXPORT size_t usearch_size(usearch_index_t, usearch_error_t* error);
|
|
134
161
|
USEARCH_EXPORT size_t usearch_capacity(usearch_index_t, usearch_error_t* error);
|
|
135
162
|
USEARCH_EXPORT size_t usearch_dimensions(usearch_index_t, usearch_error_t* error);
|
package/conanfile.py
CHANGED
|
@@ -4,7 +4,7 @@ from conans import ConanFile
|
|
|
4
4
|
class USearchConan(ConanFile):
|
|
5
5
|
|
|
6
6
|
name = 'USearch'
|
|
7
|
-
version = '1.
|
|
7
|
+
version = '1.3.0'
|
|
8
8
|
license = 'Apache 2.0'
|
|
9
9
|
url = 'https://github.com/unum-cloud/usearch'
|
|
10
10
|
description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
|
package/cpp/test.cpp
CHANGED
|
@@ -118,7 +118,7 @@ void test_cosine(index_at& index, std::vector<std::vector<scalar_at>> const& vec
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// Search again over mapped index
|
|
121
|
-
// file_head_result_t head =
|
|
121
|
+
// file_head_result_t head = index_dense_metadata_from_path("tmp.usearch");
|
|
122
122
|
// expect(head.size == 3);
|
|
123
123
|
index.view("tmp.usearch");
|
|
124
124
|
matched_count = index.search(vector_first, 5, args...).dump_to(matched_keys, matched_distances);
|
|
@@ -140,7 +140,7 @@ void test_cosine(index_at& index, std::vector<std::vector<scalar_at>> const& vec
|
|
|
140
140
|
|
|
141
141
|
// Check metadata
|
|
142
142
|
if constexpr (punned_ak) {
|
|
143
|
-
index_dense_metadata_result_t meta =
|
|
143
|
+
index_dense_metadata_result_t meta = index_dense_metadata_from_path("tmp.usearch");
|
|
144
144
|
expect(bool(meta));
|
|
145
145
|
}
|
|
146
146
|
}
|
package/docs/benchmarks.md
CHANGED
|
@@ -19,13 +19,13 @@ The default values vary drastically.
|
|
|
19
19
|
Below are the performance numbers for a benchmark running on the 64 cores of AWS `c7g.metal` "Graviton 3"-based instances.
|
|
20
20
|
The main columns are:
|
|
21
21
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
22
|
+
- __Add__: Number of insertion Queries Per Second.
|
|
23
|
+
- __Search__: Number search Queries Per Second.
|
|
24
|
+
- __Recall @1__: How often does approximate search yield the exact best match?
|
|
25
25
|
|
|
26
26
|
### Different "connectivity"
|
|
27
27
|
|
|
28
|
-
| Vectors | Connectivity | EF @ A | EF @ S |
|
|
28
|
+
| Vectors | Connectivity | EF @ A | EF @ S | __Add__, QPS | __Search__, QPS | __Recall @1__ |
|
|
29
29
|
| :--------- | :----------: | :----: | :----: | :----------: | :-------------: | ------------: |
|
|
30
30
|
| `f32` x256 | 16 | 128 | 64 | 75'640 | 131'654 | 99.3% |
|
|
31
31
|
| `f32` x256 | 12 | 128 | 64 | 81'747 | 149'728 | 99.0% |
|
|
@@ -33,7 +33,7 @@ The main columns are:
|
|
|
33
33
|
|
|
34
34
|
### Different "expansion factors"
|
|
35
35
|
|
|
36
|
-
| Vectors | Connectivity | EF @ A | EF @ S |
|
|
36
|
+
| Vectors | Connectivity | EF @ A | EF @ S | __Add__, QPS | __Search__, QPS | __Recall @1__ |
|
|
37
37
|
| :--------- | :----------: | :----: | :----: | :----------: | :-------------: | ------------: |
|
|
38
38
|
| `f32` x256 | 16 | 128 | 64 | 75'640 | 131'654 | 99.3% |
|
|
39
39
|
| `f32` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
|
|
@@ -41,7 +41,7 @@ The main columns are:
|
|
|
41
41
|
|
|
42
42
|
### Different vectors "quantization"
|
|
43
43
|
|
|
44
|
-
| Vectors | Connectivity | EF @ A | EF @ S |
|
|
44
|
+
| Vectors | Connectivity | EF @ A | EF @ S | __Add__, QPS | __Search__, QPS | __Recall @1__ |
|
|
45
45
|
| :----------- | :----------: | :----: | :----: | :----------: | :-------------: | ------------: |
|
|
46
46
|
| `f32` x256 | 16 | 128 | 64 | 87'995 | 171'856 | 99.1% |
|
|
47
47
|
| `f16` x256 | 16 | 128 | 64 | 87'270 | 153'788 | 98.4% |
|