usearch 0.1.10 → 0.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/.vscode/settings.json +5 -0
- package/Cargo.toml +1 -1
- package/README.md +12 -7
- package/VERSION +1 -1
- package/include/usearch/usearch.hpp +2 -1
- package/package.json +1 -1
- package/python/bench.py +14 -2
- package/python/lib.cpp +349 -39
- package/python/test.py +7 -0
- package/src/advanced.hpp +17 -7
- package/src/bench.cpp +15 -0
package/.vscode/settings.json
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -227,6 +227,15 @@ index = usearch.Index(
|
|
|
227
227
|
expansion_search=64, # Control the quality of search, optional
|
|
228
228
|
)
|
|
229
229
|
|
|
230
|
+
vector = np.random.uniform(0, 0.3, (index.ndim)).astype(np.float32)
|
|
231
|
+
index.add(42, vector)
|
|
232
|
+
matches, distances, count = index.search(vector, 10)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Same can be done with batches, rather than single entries.
|
|
236
|
+
Under the hood, worker threads will be spawned, to parallelize the procedure.
|
|
237
|
+
|
|
238
|
+
```py
|
|
230
239
|
n = 100
|
|
231
240
|
labels = np.array(range(n), dtype=np.longlong)
|
|
232
241
|
vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
|
|
@@ -234,9 +243,6 @@ vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
|
|
|
234
243
|
# You can avoid copying the data
|
|
235
244
|
# Handy when build 1B+ indexes of memory-mapped files
|
|
236
245
|
index.add(labels, vectors, copy=True)
|
|
237
|
-
assert len(index) == n
|
|
238
|
-
|
|
239
|
-
# You can search a batch at once
|
|
240
246
|
matches, distances, counts = index.search(vectors, 10)
|
|
241
247
|
```
|
|
242
248
|
|
|
@@ -407,15 +413,14 @@ index = usearch.Index(ndim=256)
|
|
|
407
413
|
def add(label: int, photo: Image.Image):
|
|
408
414
|
image = model.preprocess_image(photo)
|
|
409
415
|
vector = model.encode_image(image).detach().numpy()
|
|
410
|
-
|
|
411
|
-
index.add(labels, vector, copy=True)
|
|
416
|
+
index.add(label, vector.flatten(), copy=True)
|
|
412
417
|
|
|
413
418
|
@server
|
|
414
419
|
def search(query: str) -> np.ndarray:
|
|
415
420
|
tokens = model.preprocess_text(query)
|
|
416
421
|
vector = model.encode_text(tokens).detach().numpy()
|
|
417
|
-
neighbors = index.search(vector, 3)
|
|
418
|
-
return neighbors
|
|
422
|
+
neighbors, _, _ = index.search(vector.flatten(), 3)
|
|
423
|
+
return neighbors
|
|
419
424
|
|
|
420
425
|
server.run()
|
|
421
426
|
```
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.2.0
|
|
@@ -339,8 +339,9 @@ class max_heap_gt {
|
|
|
339
339
|
bool empty() const noexcept { return !size_; }
|
|
340
340
|
std::size_t size() const noexcept { return size_; }
|
|
341
341
|
std::size_t capacity() const noexcept { return capacity_; }
|
|
342
|
+
/// @brief Selects the largest element in the heap.
|
|
343
|
+
/// @return Reference to the stored element.
|
|
342
344
|
element_t const& top() const noexcept { return elements_[0]; }
|
|
343
|
-
element_t const& max() const noexcept { return elements_[0]; }
|
|
344
345
|
|
|
345
346
|
void clear() noexcept {
|
|
346
347
|
while (size_) {
|
package/package.json
CHANGED
package/python/bench.py
CHANGED
|
@@ -2,11 +2,11 @@ import datetime
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
import fire
|
|
4
4
|
|
|
5
|
-
from usearchh.io import load_matrix
|
|
6
|
-
|
|
7
5
|
import faiss
|
|
8
6
|
import usearch
|
|
9
7
|
|
|
8
|
+
import struct
|
|
9
|
+
import numpy as np
|
|
10
10
|
|
|
11
11
|
def measure(f) -> float:
|
|
12
12
|
a = datetime.datetime.now()
|
|
@@ -44,11 +44,23 @@ def main(
|
|
|
44
44
|
neighbors_mat = load_matrix(neighbors)
|
|
45
45
|
dim = vectors_mat.shape[1]
|
|
46
46
|
|
|
47
|
+
# Test FAISS
|
|
47
48
|
index = faiss.IndexHNSWFlat(dim, connectivity)
|
|
48
49
|
index.hnsw.efSearch = expansion_search
|
|
49
50
|
index.hnsw.efConstruction = expansion_add
|
|
50
51
|
bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
|
|
51
52
|
|
|
53
|
+
# Test FAISS with default Product Quantization
|
|
54
|
+
nlist = 10000 # Number of inverted lists (number of partitions or cells).
|
|
55
|
+
nsegment = 16 # Number of segments for PQ (number of subquantizers).
|
|
56
|
+
nbit = 8 # Number of bits to encode each segment.
|
|
57
|
+
coarse_quantizer = faiss.IndexHNSWFlat(dim, connectivity)
|
|
58
|
+
index = faiss.IndexIVFPQ(coarse_quantizer, dim, nlist, nsegment, nbit)
|
|
59
|
+
index.train(vectors_mat)
|
|
60
|
+
index.nprobe = 10
|
|
61
|
+
bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
|
|
62
|
+
|
|
63
|
+
# Test USearch `Index`
|
|
52
64
|
index = usearch.Index(
|
|
53
65
|
ndim=dim,
|
|
54
66
|
expansion_add=expansion_add,
|
package/python/lib.cpp
CHANGED
|
@@ -24,11 +24,41 @@ using namespace unum::usearch;
|
|
|
24
24
|
using namespace unum;
|
|
25
25
|
|
|
26
26
|
namespace py = pybind11;
|
|
27
|
+
using py_shape_t = py::array::ShapeContainer;
|
|
27
28
|
|
|
28
29
|
using label_t = Py_ssize_t;
|
|
29
30
|
using distance_t = punned_distance_t;
|
|
31
|
+
using id_t = std::uint32_t;
|
|
30
32
|
using native_index_t = auto_index_gt<label_t>;
|
|
31
33
|
|
|
34
|
+
using set_member_t = std::uint32_t;
|
|
35
|
+
using set_view_t = span_gt<set_member_t const>;
|
|
36
|
+
using sets_index_t = index_gt<jaccard_gt<set_member_t>, label_t, id_t, set_member_t>;
|
|
37
|
+
|
|
38
|
+
using hash_word_t = std::uint64_t;
|
|
39
|
+
using hash_index_t = index_gt<bit_hamming_gt<hash_word_t>, label_t, id_t, hash_word_t>;
|
|
40
|
+
static constexpr std::size_t bits_per_hash_word_k = sizeof(hash_word_t) * CHAR_BIT;
|
|
41
|
+
|
|
42
|
+
struct hash_index_w_meta_t : public hash_index_t {
|
|
43
|
+
using hash_index_t::add;
|
|
44
|
+
using hash_index_t::capacity;
|
|
45
|
+
using hash_index_t::reserve;
|
|
46
|
+
using hash_index_t::search;
|
|
47
|
+
using hash_index_t::size;
|
|
48
|
+
|
|
49
|
+
std::vector<hash_word_t> buffer_;
|
|
50
|
+
std::size_t words_;
|
|
51
|
+
std::size_t bits_;
|
|
52
|
+
|
|
53
|
+
hash_index_w_meta_t(config_t config, std::size_t bits) : hash_index_t(config) {
|
|
54
|
+
words_ = divide_round_up<bits_per_hash_word_k>(bits);
|
|
55
|
+
bits_ = words_ * bits_per_hash_word_k;
|
|
56
|
+
buffer_.resize(words_);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
span_gt<hash_word_t const> buffer() const noexcept { return {buffer_.data(), buffer_.size()}; }
|
|
60
|
+
};
|
|
61
|
+
|
|
32
62
|
static native_index_t make_index( //
|
|
33
63
|
std::size_t dimensions, //
|
|
34
64
|
std::size_t capacity, //
|
|
@@ -56,33 +86,91 @@ static native_index_t make_index( //
|
|
|
56
86
|
return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
static
|
|
89
|
+
static std::unique_ptr<sets_index_t> make_sets_index( //
|
|
90
|
+
std::size_t capacity, //
|
|
91
|
+
std::size_t connectivity, //
|
|
92
|
+
std::size_t expansion_add, //
|
|
93
|
+
std::size_t expansion_search //
|
|
94
|
+
) {
|
|
95
|
+
config_t config;
|
|
96
|
+
config.expansion_add = expansion_add;
|
|
97
|
+
config.expansion_search = expansion_search;
|
|
98
|
+
config.connectivity = connectivity;
|
|
99
|
+
config.max_elements = capacity;
|
|
100
|
+
config.max_threads_add = 1;
|
|
101
|
+
config.max_threads_search = 1;
|
|
102
|
+
|
|
103
|
+
return std::unique_ptr<sets_index_t>(new sets_index_t(config));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static std::unique_ptr<hash_index_w_meta_t> make_hash_index( //
|
|
107
|
+
std::size_t bits, //
|
|
108
|
+
std::size_t capacity, //
|
|
109
|
+
std::size_t connectivity, //
|
|
110
|
+
std::size_t expansion_add, //
|
|
111
|
+
std::size_t expansion_search //
|
|
112
|
+
) {
|
|
113
|
+
config_t config;
|
|
114
|
+
config.expansion_add = expansion_add;
|
|
115
|
+
config.expansion_search = expansion_search;
|
|
116
|
+
config.connectivity = connectivity;
|
|
117
|
+
config.max_elements = capacity;
|
|
118
|
+
config.max_threads_add = 1;
|
|
119
|
+
config.max_threads_search = 1;
|
|
120
|
+
|
|
121
|
+
return std::unique_ptr<hash_index_w_meta_t>(new hash_index_w_meta_t(config, bits));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static void add_one_to_index(native_index_t& index, label_t label, py::buffer vector, bool copy) {
|
|
125
|
+
|
|
126
|
+
py::buffer_info vector_info = vector.request();
|
|
127
|
+
if (vector_info.ndim != 1)
|
|
128
|
+
throw std::invalid_argument("Expects a vector, not a higher-rank tensor!");
|
|
129
|
+
|
|
130
|
+
ssize_t vector_dimensions = vector_info.shape[0];
|
|
131
|
+
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
132
|
+
if (vector_dimensions != static_cast<ssize_t>(index.dimensions()))
|
|
133
|
+
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
134
|
+
|
|
135
|
+
if (index.size() + 1 >= index.capacity())
|
|
136
|
+
index.reserve(ceil2(index.size() + 1));
|
|
137
|
+
|
|
138
|
+
// https://docs.python.org/3/library/struct.html#format-characters
|
|
139
|
+
if (vector_info.format == "e")
|
|
140
|
+
index.add(label, reinterpret_cast<f16_converted_t const*>(vector_data), 0, copy);
|
|
141
|
+
else if (vector_info.format == "f")
|
|
142
|
+
index.add(label, reinterpret_cast<float const*>(vector_data), 0, copy);
|
|
143
|
+
else if (vector_info.format == "d")
|
|
144
|
+
index.add(label, reinterpret_cast<double const*>(vector_data), 0, copy);
|
|
145
|
+
else
|
|
146
|
+
throw std::invalid_argument("Incompatible scalars in the vector!");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static void add_many_to_index(native_index_t& index, py::buffer labels, py::buffer vectors, bool copy) {
|
|
60
150
|
|
|
61
151
|
py::buffer_info labels_info = labels.request();
|
|
62
152
|
py::buffer_info vectors_info = vectors.request();
|
|
63
153
|
|
|
64
154
|
if (labels_info.format != py::format_descriptor<label_t>::format())
|
|
65
|
-
throw std::
|
|
155
|
+
throw std::invalid_argument("Incompatible label type!");
|
|
66
156
|
|
|
67
157
|
if (labels_info.ndim != 1)
|
|
68
|
-
throw std::
|
|
158
|
+
throw std::invalid_argument("Labels must be placed in a single-dimensional array!");
|
|
69
159
|
|
|
70
160
|
if (vectors_info.ndim != 2)
|
|
71
|
-
throw std::
|
|
161
|
+
throw std::invalid_argument("Expects a matrix of vectors to add!");
|
|
72
162
|
|
|
73
163
|
ssize_t labels_count = labels_info.shape[0];
|
|
74
164
|
ssize_t vectors_count = vectors_info.shape[0];
|
|
75
165
|
ssize_t vectors_dimensions = vectors_info.shape[1];
|
|
76
|
-
if (vectors_dimensions != index.dimensions())
|
|
77
|
-
throw std::
|
|
166
|
+
if (vectors_dimensions != static_cast<ssize_t>(index.dimensions()))
|
|
167
|
+
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
78
168
|
|
|
79
169
|
if (labels_count != vectors_count)
|
|
80
|
-
throw std::
|
|
170
|
+
throw std::invalid_argument("Number of labels and vectors must match!");
|
|
81
171
|
|
|
82
|
-
if (index.size() + vectors_count >= index.capacity())
|
|
83
|
-
|
|
84
|
-
index.reserve(next_capacity);
|
|
85
|
-
}
|
|
172
|
+
if (index.size() + vectors_count >= index.capacity())
|
|
173
|
+
index.reserve(ceil2(index.size() + vectors_count));
|
|
86
174
|
|
|
87
175
|
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
88
176
|
char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
|
|
@@ -108,11 +196,48 @@ static void add_to_index(native_index_t& index, py::buffer labels, py::buffer ve
|
|
|
108
196
|
index.add(label, vector, thread_idx, copy);
|
|
109
197
|
});
|
|
110
198
|
else
|
|
111
|
-
throw std::
|
|
199
|
+
throw std::invalid_argument("Incompatible scalars in the vectors matrix!");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, std::size_t wanted) {
|
|
203
|
+
|
|
204
|
+
py::buffer_info vector_info = vector.request();
|
|
205
|
+
ssize_t vector_dimensions = vector_info.shape[0];
|
|
206
|
+
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
207
|
+
if (vector_dimensions != static_cast<ssize_t>(index.dimensions()))
|
|
208
|
+
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
209
|
+
|
|
210
|
+
py::array_t<label_t> labels_py(static_cast<ssize_t>(wanted));
|
|
211
|
+
py::array_t<distance_t> distances_py(static_cast<ssize_t>(wanted));
|
|
212
|
+
std::size_t count{};
|
|
213
|
+
auto labels_py1d = labels_py.mutable_unchecked<1>();
|
|
214
|
+
auto distances_py1d = distances_py.mutable_unchecked<1>();
|
|
215
|
+
|
|
216
|
+
// https://docs.python.org/3/library/struct.html#format-characters
|
|
217
|
+
if (vector_info.format == "e")
|
|
218
|
+
count = index.search( //
|
|
219
|
+
reinterpret_cast<f16_converted_t const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), 0);
|
|
220
|
+
else if (vector_info.format == "f")
|
|
221
|
+
count = index.search( //
|
|
222
|
+
reinterpret_cast<float const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), 0);
|
|
223
|
+
else if (vector_info.format == "d")
|
|
224
|
+
count = index.search( //
|
|
225
|
+
reinterpret_cast<double const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), 0);
|
|
226
|
+
else
|
|
227
|
+
throw std::invalid_argument("Incompatible scalars in the query vector!");
|
|
228
|
+
|
|
229
|
+
labels_py.resize(py_shape_t{static_cast<ssize_t>(count)});
|
|
230
|
+
distances_py.resize(py_shape_t{static_cast<ssize_t>(count)});
|
|
231
|
+
|
|
232
|
+
py::tuple results(3);
|
|
233
|
+
results[0] = labels_py;
|
|
234
|
+
results[1] = distances_py;
|
|
235
|
+
results[2] = static_cast<Py_ssize_t>(count);
|
|
236
|
+
return results;
|
|
112
237
|
}
|
|
113
238
|
|
|
114
239
|
/**
|
|
115
|
-
* @param vectors Matrix of vectors to search for
|
|
240
|
+
* @param vectors Matrix of vectors to search for.
|
|
116
241
|
* @param wanted Number of matches per request.
|
|
117
242
|
*
|
|
118
243
|
* @return Tuple with:
|
|
@@ -120,26 +245,30 @@ static void add_to_index(native_index_t& index, py::buffer labels, py::buffer ve
|
|
|
120
245
|
* 2. matrix of distances,
|
|
121
246
|
* 3. array with match counts.
|
|
122
247
|
*/
|
|
123
|
-
static py::tuple
|
|
248
|
+
static py::tuple search_many_in_index(native_index_t& index, py::buffer vectors, std::size_t wanted) {
|
|
249
|
+
|
|
250
|
+
if (wanted == 0)
|
|
251
|
+
return py::tuple(3);
|
|
124
252
|
|
|
125
253
|
py::buffer_info vectors_info = vectors.request();
|
|
254
|
+
if (vectors_info.ndim == 1)
|
|
255
|
+
return search_one_in_index(index, vectors, wanted);
|
|
126
256
|
if (vectors_info.ndim != 2)
|
|
127
|
-
throw std::
|
|
257
|
+
throw std::invalid_argument("Expects a matrix of vectors to add!");
|
|
128
258
|
|
|
129
259
|
ssize_t vectors_count = vectors_info.shape[0];
|
|
130
260
|
ssize_t vectors_dimensions = vectors_info.shape[1];
|
|
131
|
-
|
|
132
|
-
|
|
261
|
+
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
262
|
+
if (vectors_dimensions != static_cast<ssize_t>(index.dimensions()))
|
|
263
|
+
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
133
264
|
|
|
134
|
-
py::array_t<label_t> labels_py({vectors_count, wanted});
|
|
135
|
-
py::array_t<distance_t> distances_py({vectors_count, wanted});
|
|
265
|
+
py::array_t<label_t> labels_py({vectors_count, static_cast<ssize_t>(wanted)});
|
|
266
|
+
py::array_t<distance_t> distances_py({vectors_count, static_cast<ssize_t>(wanted)});
|
|
136
267
|
py::array_t<Py_ssize_t> counts_py(vectors_count);
|
|
137
268
|
auto labels_py2d = labels_py.mutable_unchecked<2>();
|
|
138
269
|
auto distances_py2d = distances_py.mutable_unchecked<2>();
|
|
139
270
|
auto counts_py1d = counts_py.mutable_unchecked<1>();
|
|
140
271
|
|
|
141
|
-
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
142
|
-
|
|
143
272
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
144
273
|
if (vectors_info.format == "e")
|
|
145
274
|
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
@@ -160,7 +289,7 @@ static py::tuple search_in_index(native_index_t& index, py::buffer vectors, ssiz
|
|
|
160
289
|
index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), thread_idx));
|
|
161
290
|
});
|
|
162
291
|
else
|
|
163
|
-
throw std::
|
|
292
|
+
throw std::invalid_argument("Incompatible scalars in the query matrix!");
|
|
164
293
|
|
|
165
294
|
py::tuple results(3);
|
|
166
295
|
results[0] = labels_py;
|
|
@@ -169,9 +298,78 @@ static py::tuple search_in_index(native_index_t& index, py::buffer vectors, ssiz
|
|
|
169
298
|
return results;
|
|
170
299
|
}
|
|
171
300
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
301
|
+
// clang-format off
|
|
302
|
+
template <typename index_at> void save_index(index_at const& index, std::string const& path) { index.save(path.c_str()); }
|
|
303
|
+
template <typename index_at> void load_index(index_at& index, std::string const& path) { index.load(path.c_str()); }
|
|
304
|
+
template <typename index_at> void view_index(index_at& index, std::string const& path) { index.view(path.c_str()); }
|
|
305
|
+
template <typename index_at> void clear_index(index_at& index) { index.clear(); }
|
|
306
|
+
// clang-format on
|
|
307
|
+
|
|
308
|
+
template <typename element_at> bool has_duplicates(element_at const* begin, element_at const* end) {
|
|
309
|
+
if (begin == end)
|
|
310
|
+
return false;
|
|
311
|
+
element_at const* last = begin;
|
|
312
|
+
begin++;
|
|
313
|
+
for (; begin != end; ++begin, ++last) {
|
|
314
|
+
if (*begin == *last)
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
void validate_set(py::array_t<set_member_t> const& set) {
|
|
321
|
+
if (set.ndim() != 1)
|
|
322
|
+
throw std::invalid_argument("Set can't be multi-dimensional!");
|
|
323
|
+
if (set.strides(0) != sizeof(set_member_t))
|
|
324
|
+
throw std::invalid_argument("Set can't be strided!");
|
|
325
|
+
auto proxy = set.unchecked<1>();
|
|
326
|
+
set_member_t const* begin = &proxy(0);
|
|
327
|
+
set_member_t const* end = begin + proxy.size();
|
|
328
|
+
if (!std::is_sorted(begin, end))
|
|
329
|
+
throw std::invalid_argument("Set must be sorted!");
|
|
330
|
+
if (has_duplicates(begin, end))
|
|
331
|
+
throw std::invalid_argument("Set must be deduplicated!");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
inline std::uint64_t hash_ror64(std::uint64_t v, int r) noexcept { return (v >> r) | (v << (64 - r)); }
|
|
335
|
+
|
|
336
|
+
inline std::uint64_t hash(std::uint64_t v) noexcept {
|
|
337
|
+
v ^= hash_ror64(v, 25) ^ hash_ror64(v, 50);
|
|
338
|
+
v *= 0xA24BAED4963EE407UL;
|
|
339
|
+
v ^= hash_ror64(v, 24) ^ hash_ror64(v, 49);
|
|
340
|
+
v *= 0x9FB21C651E98DF25UL;
|
|
341
|
+
return v ^ v >> 28;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
template <typename scalar_at>
|
|
345
|
+
inline void hash_typed_buffer(hash_index_w_meta_t& index, py::buffer_info const& vector_info) noexcept {
|
|
346
|
+
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
347
|
+
ssize_t vector_dimensions = vector_info.shape[0];
|
|
348
|
+
ssize_t vector_stride = vector_info.strides[0];
|
|
349
|
+
std::memset(index.buffer_.data(), 0, index.words_ * sizeof(hash_word_t));
|
|
350
|
+
|
|
351
|
+
for (std::size_t i = 0; i != static_cast<std::size_t>(vector_dimensions); ++i) {
|
|
352
|
+
scalar_at scalar = *reinterpret_cast<scalar_at const*>(vector_data + i * vector_stride);
|
|
353
|
+
std::uint64_t scalar_hash = hash(scalar);
|
|
354
|
+
index.buffer_[scalar_hash % index.words_] |= hash_word_t(1) << (scalar_hash % bits_per_hash_word_k);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
void hash_buffer(hash_index_w_meta_t& index, py::buffer vector) {
|
|
359
|
+
py::buffer_info info = vector.request();
|
|
360
|
+
if (info.ndim != 1)
|
|
361
|
+
throw std::invalid_argument("Array can't be multi-dimensional!");
|
|
362
|
+
|
|
363
|
+
// https://docs.python.org/3/library/struct.html#format-characters
|
|
364
|
+
if (info.format == "h" || info.format == "H")
|
|
365
|
+
return hash_typed_buffer<std::uint16_t>(index, info);
|
|
366
|
+
else if (info.format == "i" || info.format == "I" || info.format == "l" || info.format == "L")
|
|
367
|
+
return hash_typed_buffer<std::uint32_t>(index, info);
|
|
368
|
+
else if (info.format == "q" || info.format == "Q" || info.format == "n" || info.format == "N")
|
|
369
|
+
return hash_typed_buffer<std::uint64_t>(index, info);
|
|
370
|
+
else
|
|
371
|
+
throw std::invalid_argument("Array elements must be 16, 32, or 64 bit hashable integers!");
|
|
372
|
+
}
|
|
175
373
|
|
|
176
374
|
PYBIND11_MODULE(usearch, m) {
|
|
177
375
|
m.doc() = "Unum USearch Python bindings";
|
|
@@ -180,7 +378,7 @@ PYBIND11_MODULE(usearch, m) {
|
|
|
180
378
|
|
|
181
379
|
i.def(py::init(&make_index), //
|
|
182
380
|
py::kw_only(), //
|
|
183
|
-
py::arg("ndim"),
|
|
381
|
+
py::arg("ndim") = 0, //
|
|
184
382
|
py::arg("capacity") = 0, //
|
|
185
383
|
py::arg("dtype") = std::string("f32"), //
|
|
186
384
|
py::arg("metric") = std::string("ip"), //
|
|
@@ -190,18 +388,26 @@ PYBIND11_MODULE(usearch, m) {
|
|
|
190
388
|
py::arg("metric_pointer") = 0 //
|
|
191
389
|
);
|
|
192
390
|
|
|
193
|
-
i.def(
|
|
194
|
-
"add", &
|
|
195
|
-
py::arg("labels"),
|
|
196
|
-
py::arg("vectors"),
|
|
197
|
-
py::kw_only(),
|
|
198
|
-
py::arg("copy") = true
|
|
391
|
+
i.def( //
|
|
392
|
+
"add", &add_many_to_index, //
|
|
393
|
+
py::arg("labels"), //
|
|
394
|
+
py::arg("vectors"), //
|
|
395
|
+
py::kw_only(), //
|
|
396
|
+
py::arg("copy") = true //
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
i.def( //
|
|
400
|
+
"add", &add_one_to_index, //
|
|
401
|
+
py::arg("label"), //
|
|
402
|
+
py::arg("vector"), //
|
|
403
|
+
py::kw_only(), //
|
|
404
|
+
py::arg("copy") = true //
|
|
199
405
|
);
|
|
200
406
|
|
|
201
|
-
i.def(
|
|
202
|
-
"search", &
|
|
203
|
-
py::arg("
|
|
204
|
-
py::arg("count") = 10
|
|
407
|
+
i.def( //
|
|
408
|
+
"search", &search_many_in_index, //
|
|
409
|
+
py::arg("query"), //
|
|
410
|
+
py::arg("count") = 10 //
|
|
205
411
|
);
|
|
206
412
|
|
|
207
413
|
i.def("__len__", &native_index_t::size);
|
|
@@ -210,7 +416,111 @@ PYBIND11_MODULE(usearch, m) {
|
|
|
210
416
|
i.def_property_readonly("connectivity", &native_index_t::connectivity);
|
|
211
417
|
i.def_property_readonly("capacity", &native_index_t::capacity);
|
|
212
418
|
|
|
213
|
-
i.def("save", &save_index
|
|
214
|
-
i.def("load", &load_index
|
|
215
|
-
i.def("view", &view_index
|
|
419
|
+
i.def("save", &save_index<native_index_t>, py::arg("path"));
|
|
420
|
+
i.def("load", &load_index<native_index_t>, py::arg("path"));
|
|
421
|
+
i.def("view", &view_index<native_index_t>, py::arg("path"));
|
|
422
|
+
i.def("clear", &clear_index<native_index_t>);
|
|
423
|
+
|
|
424
|
+
auto si = py::class_<sets_index_t>(m, "SetsIndex");
|
|
425
|
+
|
|
426
|
+
si.def( //
|
|
427
|
+
py::init(&make_sets_index), //
|
|
428
|
+
py::kw_only(), //
|
|
429
|
+
py::arg("capacity") = 0, //
|
|
430
|
+
py::arg("connectivity") = config_t::connectivity_default_k, //
|
|
431
|
+
py::arg("expansion_add") = config_t::expansion_add_default_k, //
|
|
432
|
+
py::arg("expansion_search") = config_t::expansion_search_default_k //
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
si.def( //
|
|
436
|
+
"add",
|
|
437
|
+
[](sets_index_t& index, label_t label, py::array_t<set_member_t> set, bool copy) {
|
|
438
|
+
validate_set(set);
|
|
439
|
+
if (index.size() + 1 >= index.capacity())
|
|
440
|
+
index.reserve(ceil2(index.size() + 1));
|
|
441
|
+
auto proxy = set.unchecked<1>();
|
|
442
|
+
auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
|
|
443
|
+
index.add(label, view, 0, copy);
|
|
444
|
+
}, //
|
|
445
|
+
py::arg("label"), //
|
|
446
|
+
py::arg("set"), //
|
|
447
|
+
py::kw_only(), //
|
|
448
|
+
py::arg("copy") = true //
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
si.def( //
|
|
452
|
+
"search",
|
|
453
|
+
[](sets_index_t& index, py::array_t<set_member_t> set, std::size_t count) -> py::array_t<label_t> {
|
|
454
|
+
validate_set(set);
|
|
455
|
+
auto proxy = set.unchecked<1>();
|
|
456
|
+
auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
|
|
457
|
+
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<ssize_t>(count)});
|
|
458
|
+
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
459
|
+
auto found = index.search(view, count, &labels_proxy(0), nullptr, 0);
|
|
460
|
+
labels_py.resize(py_shape_t{static_cast<ssize_t>(found)});
|
|
461
|
+
return labels_py;
|
|
462
|
+
},
|
|
463
|
+
py::arg("set"), //
|
|
464
|
+
py::arg("count") = 10 //
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
si.def("__len__", &sets_index_t::size);
|
|
468
|
+
si.def_property_readonly("size", &sets_index_t::size);
|
|
469
|
+
si.def_property_readonly("connectivity", &sets_index_t::connectivity);
|
|
470
|
+
si.def_property_readonly("capacity", &sets_index_t::capacity);
|
|
471
|
+
|
|
472
|
+
si.def("save", &save_index<sets_index_t>, py::arg("path"));
|
|
473
|
+
si.def("load", &load_index<sets_index_t>, py::arg("path"));
|
|
474
|
+
si.def("view", &view_index<sets_index_t>, py::arg("path"));
|
|
475
|
+
si.def("clear", &clear_index<sets_index_t>);
|
|
476
|
+
|
|
477
|
+
auto hi = py::class_<hash_index_w_meta_t>(m, "HashIndex");
|
|
478
|
+
|
|
479
|
+
hi.def( //
|
|
480
|
+
py::init(&make_hash_index), //
|
|
481
|
+
py::kw_only(), //
|
|
482
|
+
py::arg("bits"), //
|
|
483
|
+
py::arg("capacity") = 0, //
|
|
484
|
+
py::arg("connectivity") = config_t::connectivity_default_k, //
|
|
485
|
+
py::arg("expansion_add") = config_t::expansion_add_default_k, //
|
|
486
|
+
py::arg("expansion_search") = config_t::expansion_search_default_k //
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
hi.def( //
|
|
490
|
+
"add",
|
|
491
|
+
[](hash_index_w_meta_t& index, label_t label, py::buffer array) {
|
|
492
|
+
if (index.size() + 1 >= index.capacity())
|
|
493
|
+
index.reserve(ceil2(index.size() + 1));
|
|
494
|
+
hash_buffer(index, array);
|
|
495
|
+
index.add(label, index.buffer(), 0);
|
|
496
|
+
}, //
|
|
497
|
+
py::arg("label"), //
|
|
498
|
+
py::arg("array") //
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
hi.def( //
|
|
502
|
+
"search",
|
|
503
|
+
[](hash_index_w_meta_t& index, py::buffer array, std::size_t count) -> py::array_t<label_t> {
|
|
504
|
+
if (index.size() + 1 >= index.capacity())
|
|
505
|
+
index.reserve(ceil2(index.size() + 1));
|
|
506
|
+
hash_buffer(index, array);
|
|
507
|
+
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<ssize_t>(count)});
|
|
508
|
+
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
509
|
+
auto found = index.search(index.buffer(), count, &labels_proxy(0), nullptr, 0);
|
|
510
|
+
labels_py.resize(py_shape_t{static_cast<ssize_t>(found)});
|
|
511
|
+
return labels_py;
|
|
512
|
+
},
|
|
513
|
+
py::arg("array"), //
|
|
514
|
+
py::arg("count") = 10 //
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
hi.def("__len__", &hash_index_w_meta_t::size);
|
|
518
|
+
hi.def_property_readonly("size", &hash_index_w_meta_t::size);
|
|
519
|
+
hi.def_property_readonly("connectivity", &hash_index_w_meta_t::connectivity);
|
|
520
|
+
hi.def_property_readonly("capacity", &hash_index_w_meta_t::capacity);
|
|
521
|
+
|
|
522
|
+
hi.def("save", &save_index<hash_index_w_meta_t>, py::arg("path"));
|
|
523
|
+
hi.def("load", &load_index<hash_index_w_meta_t>, py::arg("path"));
|
|
524
|
+
hi.def("view", &view_index<hash_index_w_meta_t>, py::arg("path"));
|
|
525
|
+
hi.def("clear", &clear_index<hash_index_w_meta_t>);
|
|
216
526
|
}
|
package/python/test.py
CHANGED
|
@@ -43,3 +43,10 @@ assert len(index) == count_vectors
|
|
|
43
43
|
|
|
44
44
|
results = index.search(vectors, 10)
|
|
45
45
|
print('found', results[0].shape, results[1].shape, results[2].shape)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
index_sets = usearch.SetsIndex()
|
|
49
|
+
index_sets.add(10, np.array([10, 12, 15], dtype=np.uint32))
|
|
50
|
+
index_sets.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))
|
|
51
|
+
results = index_sets.search(np.array([12, 15], dtype=np.uint32), 20)
|
|
52
|
+
print(results)
|
package/src/advanced.hpp
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
#include <functional>
|
|
3
|
+
#include <stdexcept> // `std::invalid_argument`
|
|
3
4
|
#include <thread>
|
|
4
5
|
#include <vector>
|
|
5
6
|
|
|
@@ -229,7 +230,7 @@ inline accuracy_t accuracy_from_name(char const* name, std::size_t len) {
|
|
|
229
230
|
else if (str_equals(name, len, "i8q100"))
|
|
230
231
|
accuracy = accuracy_t::i8q100_k;
|
|
231
232
|
else
|
|
232
|
-
throw std::
|
|
233
|
+
throw std::invalid_argument("Unknown type, choose: f32, f16, f64, i8q100");
|
|
233
234
|
return accuracy;
|
|
234
235
|
}
|
|
235
236
|
|
|
@@ -237,16 +238,24 @@ template <typename index_at>
|
|
|
237
238
|
inline index_at index_from_name( //
|
|
238
239
|
char const* name, std::size_t len, std::size_t dimensions, accuracy_t accuracy, config_t const& config) {
|
|
239
240
|
|
|
240
|
-
if (str_equals(name, len, "l2_sq") || str_equals(name, len, "euclidean_sq"))
|
|
241
|
+
if (str_equals(name, len, "l2_sq") || str_equals(name, len, "euclidean_sq")) {
|
|
242
|
+
if (dimensions == 0)
|
|
243
|
+
throw std::invalid_argument("The number of dimensions must be positive");
|
|
241
244
|
return index_at::l2(dimensions, accuracy, config);
|
|
242
|
-
else if (str_equals(name, len, "ip") || str_equals(name, len, "inner") || str_equals(name, len, "dot"))
|
|
245
|
+
} else if (str_equals(name, len, "ip") || str_equals(name, len, "inner") || str_equals(name, len, "dot")) {
|
|
246
|
+
if (dimensions == 0)
|
|
247
|
+
throw std::invalid_argument("The number of dimensions must be positive");
|
|
243
248
|
return index_at::ip(dimensions, accuracy, config);
|
|
244
|
-
else if (str_equals(name, len, "cos") || str_equals(name, len, "angular"))
|
|
249
|
+
} else if (str_equals(name, len, "cos") || str_equals(name, len, "angular")) {
|
|
250
|
+
if (dimensions == 0)
|
|
251
|
+
throw std::invalid_argument("The number of dimensions must be positive");
|
|
245
252
|
return index_at::cos(dimensions, accuracy, config);
|
|
246
|
-
else if (str_equals(name, len, "haversine"))
|
|
253
|
+
} else if (str_equals(name, len, "haversine")) {
|
|
254
|
+
if (dimensions != 2 && dimensions != 0)
|
|
255
|
+
throw std::invalid_argument("The number of dimensions must be equal to two");
|
|
247
256
|
return index_at::haversine(accuracy, config);
|
|
248
|
-
else
|
|
249
|
-
throw std::
|
|
257
|
+
} else
|
|
258
|
+
throw std::invalid_argument("Unknown distance, choose: l2_sq, ip, cos, hamming, jaccard");
|
|
250
259
|
return {};
|
|
251
260
|
}
|
|
252
261
|
|
|
@@ -383,6 +392,7 @@ class auto_index_gt {
|
|
|
383
392
|
std::size_t connectivity() const noexcept { return index_->connectivity(); }
|
|
384
393
|
std::size_t size() const noexcept { return index_->size(); }
|
|
385
394
|
std::size_t capacity() const noexcept { return index_->capacity(); }
|
|
395
|
+
void clear() noexcept { return index_->clear(); }
|
|
386
396
|
|
|
387
397
|
isa_t acceleration() const noexcept { return acceleration_; }
|
|
388
398
|
std::size_t concurrency() const noexcept {
|
package/src/bench.cpp
CHANGED
|
@@ -346,6 +346,18 @@ void run_big_or_small(dataset_at& dataset, args_t const& args, config_t config)
|
|
|
346
346
|
run_type_punned<auto_index_gt<std::size_t, neighbor_id_at>>(dataset, args, config);
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
void report_alternative_setups() {
|
|
350
|
+
using set_member_t = std::uint32_t;
|
|
351
|
+
using sets_index_t = index_gt<jaccard_gt<set_member_t>, std::size_t, std::uint32_t, set_member_t>;
|
|
352
|
+
set_member_t set_a[] = {10, 12, 15};
|
|
353
|
+
set_member_t set_b[] = {11, 12, 15, 16};
|
|
354
|
+
sets_index_t index;
|
|
355
|
+
index.reserve(100);
|
|
356
|
+
index.add(10, span_gt<set_member_t const>{&set_a[0], 3});
|
|
357
|
+
index.add(11, span_gt<set_member_t const>{&set_b[0], 4});
|
|
358
|
+
exit(0);
|
|
359
|
+
}
|
|
360
|
+
|
|
349
361
|
void report_expected_losses(persisted_dataset_gt<float, vector_id_t> const& dataset) {
|
|
350
362
|
|
|
351
363
|
auto vec1 = dataset.vector(0);
|
|
@@ -433,6 +445,9 @@ int main(int argc, char** argv) {
|
|
|
433
445
|
std::printf("-- Queries count: %zu\n", dataset.queries_count());
|
|
434
446
|
std::printf("-- Neighbors per query: %zu\n", dataset.neighborhood_size());
|
|
435
447
|
|
|
448
|
+
report_alternative_setups();
|
|
449
|
+
report_expected_losses(dataset);
|
|
450
|
+
|
|
436
451
|
config_t config;
|
|
437
452
|
config.connectivity = args.connectivity;
|
|
438
453
|
config.expansion_add = args.expansion_add;
|