usearch 2.5.1 → 2.6.1
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 +7 -0
- package/.vscode/settings.json +1 -0
- package/CITATION.cff +1 -1
- package/Cargo.toml +1 -1
- package/README.md +1 -1
- package/conanfile.py +1 -1
- package/cpp/README.md +26 -22
- package/csharp/nuget/nuget-package.props +1 -1
- package/include/usearch/index.hpp +1 -1
- package/include/usearch/index_plugins.hpp +1 -1
- package/package.json +1 -1
- package/python/lib.cpp +37 -17
- package/python/scripts/bench_exact.py +44 -0
- package/python/scripts/test_tooling.py +5 -4
- package/python/usearch/index.py +5 -5
- package/wasmer.toml +1 -1
|
@@ -418,6 +418,13 @@ jobs:
|
|
|
418
418
|
verbose: true
|
|
419
419
|
print-hash: true
|
|
420
420
|
|
|
421
|
+
- name: Publish to Anaconda
|
|
422
|
+
run: |
|
|
423
|
+
pip install anaconda-client
|
|
424
|
+
export ANACONDA_API_TOKEN=${{ secrets.ANACONDA_API_TOKEN }}
|
|
425
|
+
anaconda login --token $ANACONDA_API_TOKEN
|
|
426
|
+
anaconda upload -u unum-cloud ./dist/artifact/*.tar.bz2
|
|
427
|
+
|
|
421
428
|
publish_javascript:
|
|
422
429
|
name: Publish JavaScript
|
|
423
430
|
needs: versioning
|
package/.vscode/settings.json
CHANGED
package/CITATION.cff
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
package/conanfile.py
CHANGED
|
@@ -7,7 +7,7 @@ required_conan_version = ">=1.53.0"
|
|
|
7
7
|
class USearchConan(ConanFile):
|
|
8
8
|
|
|
9
9
|
name = "usearch"
|
|
10
|
-
version = '2.
|
|
10
|
+
version = '2.6.1'
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
description = "Smaller & Faster Single-File Vector Search Engine from Unum"
|
|
13
13
|
homepage = "https://github.com/unum-cloud/usearch"
|
package/cpp/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Installation
|
|
4
4
|
|
|
5
|
-
To use in a C++ project
|
|
6
|
-
Alternatively fetch it with CMake:
|
|
5
|
+
To use in a C++ project, copy the `include/usearch/*` headers into your project.
|
|
6
|
+
Alternatively, fetch it with CMake:
|
|
7
7
|
|
|
8
8
|
```cmake
|
|
9
9
|
FetchContent_Declare(usearch GIT_REPOSITORY https://github.com/unum-cloud/usearch.git)
|
|
@@ -12,13 +12,17 @@ FetchContent_MakeAvailable(usearch)
|
|
|
12
12
|
|
|
13
13
|
## Quickstart
|
|
14
14
|
|
|
15
|
-
Once included, the
|
|
16
|
-
This covers 90% of use
|
|
15
|
+
Once included, the high-level C++11 interface is as simple as it gets: `reserve()`, `add()`, `search()`, `size()`, `capacity()`, `save()`, `load()`, `view()`.
|
|
16
|
+
This covers 90% of use cases.
|
|
17
17
|
|
|
18
|
-
```
|
|
18
|
+
```cpp
|
|
19
19
|
using namespace unum::usearch;
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
metric_punned_t metric(256, metric_kind_t::l2sq_k, scalar_kind_t::f32_k);
|
|
22
|
+
|
|
23
|
+
// If you plan to store more than 4 Billion entries - use `index_dense_big_t`.
|
|
24
|
+
// Or directly instantiate the template variant you need - `index_dense_gt<key_t, internal_id_t>`.
|
|
25
|
+
index_dense_t index = index_dense_t::make(metric);
|
|
22
26
|
float vec[3] = {0.1, 0.3, 0.2};
|
|
23
27
|
|
|
24
28
|
index.reserve(10);
|
|
@@ -30,10 +34,19 @@ for (std::size_t i = 0; i != results.size(); ++i)
|
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
The `add` is thread-safe for concurrent index construction.
|
|
37
|
+
It also has an overload for different vector types, casting them under the hood.
|
|
38
|
+
The same applies to the `search`, `get`, `cluster`, and `distance_between` functions.
|
|
39
|
+
|
|
40
|
+
```cpp
|
|
41
|
+
double vec_double[3] = {0.1, 0.3, 0.2};
|
|
42
|
+
_Float16 vec_half[3] = {0.1, 0.3, 0.2};
|
|
43
|
+
index.add(43, {&vec_double[0], 3});
|
|
44
|
+
index.add(44, {&vec_half[0], 3});
|
|
45
|
+
```
|
|
33
46
|
|
|
34
47
|
## Serialization
|
|
35
48
|
|
|
36
|
-
```
|
|
49
|
+
```cpp
|
|
37
50
|
index.save("index.usearch");
|
|
38
51
|
index.load("index.usearch"); // Copying from disk
|
|
39
52
|
index.view("index.usearch"); // Memory-mapping from disk
|
|
@@ -44,23 +57,14 @@ index.view("index.usearch"); // Memory-mapping from disk
|
|
|
44
57
|
For advanced users, more compile-time abstractions are available.
|
|
45
58
|
|
|
46
59
|
```cpp
|
|
47
|
-
template <typename
|
|
48
|
-
typename key_at =
|
|
49
|
-
typename
|
|
50
|
-
typename
|
|
51
|
-
typename
|
|
60
|
+
template <typename distance_at = default_distance_t, // `float`
|
|
61
|
+
typename key_at = default_key_t, // `int64_t`, `uuid_t`
|
|
62
|
+
typename compressed_slot_at = default_slot_t, // `uint32_t`, `uint40_t`
|
|
63
|
+
typename dynamic_allocator_at = std::allocator<byte_t>, //
|
|
64
|
+
typename tape_allocator_at = dynamic_allocator_at> //
|
|
52
65
|
class index_gt;
|
|
53
66
|
```
|
|
54
67
|
|
|
55
|
-
You may want to use a custom memory allocator or a rare scalar type, but most often, you would start by defining a custom similarity measure.
|
|
56
|
-
The function object should have the following signature to support different-length vectors.
|
|
57
|
-
|
|
58
|
-
```cpp
|
|
59
|
-
struct custom_metric_t {
|
|
60
|
-
T operator()(T const* a, T const* b, std::size_t a_length, std::size_t b_length) const;
|
|
61
|
-
};
|
|
62
|
-
```
|
|
63
|
-
|
|
64
68
|
The following distances are pre-packaged:
|
|
65
69
|
|
|
66
70
|
- `metric_cos_gt<scalar_t>` for "Cosine" or "Angular" distance.
|
|
@@ -81,7 +85,7 @@ Instead of spawning additional threads within USearch, we focus on the thread sa
|
|
|
81
85
|
```cpp
|
|
82
86
|
#pragma omp parallel for
|
|
83
87
|
for (std::size_t i = 0; i < n; ++i)
|
|
84
|
-
native.add(key, span_t{vector, dims}
|
|
88
|
+
native.add(key, span_t{vector, dims});
|
|
85
89
|
```
|
|
86
90
|
|
|
87
91
|
During initialization, we allocate enough temporary memory for all the cores on the machine.
|
|
@@ -1602,7 +1602,7 @@ class flat_hash_multi_set_gt {
|
|
|
1602
1602
|
auto slot_pointer = bucket_pointer + sizeof(bucket_header_t) + sizeof(element_t) * in_bucket_index;
|
|
1603
1603
|
return {
|
|
1604
1604
|
*reinterpret_cast<bucket_header_t*>(bucket_pointer),
|
|
1605
|
-
std::uint64_t(1ull) << in_bucket_index,
|
|
1605
|
+
static_cast<std::uint64_t>(1ull) << in_bucket_index,
|
|
1606
1606
|
*reinterpret_cast<element_t*>(slot_pointer),
|
|
1607
1607
|
};
|
|
1608
1608
|
}
|
package/package.json
CHANGED
package/python/lib.cpp
CHANGED
|
@@ -532,49 +532,69 @@ static void search_typed_brute_force( //
|
|
|
532
532
|
threads = std::thread::hardware_concurrency();
|
|
533
533
|
|
|
534
534
|
std::size_t tasks_count = static_cast<std::size_t>(dataset_count * queries_count);
|
|
535
|
+
executor_default_t executor{threads};
|
|
535
536
|
|
|
536
537
|
// Progress status
|
|
537
538
|
progress_t progress_{progress};
|
|
538
539
|
std::atomic<std::size_t> processed{0};
|
|
539
540
|
|
|
540
|
-
// Allocate temporary memory to store the distance matrix
|
|
541
|
-
// Previous version didn't need temporary memory, but the performance was much lower
|
|
542
541
|
struct dense_key_and_distance_t {
|
|
543
542
|
u32_t offset;
|
|
544
543
|
f32_t distance;
|
|
545
544
|
};
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
std::size_t dataset_idx = task_idx / queries_count;
|
|
550
|
-
std::size_t query_idx = task_idx % queries_count;
|
|
545
|
+
auto less = [](dense_key_and_distance_t const& a, dense_key_and_distance_t const& b) {
|
|
546
|
+
return a.distance < b.distance;
|
|
547
|
+
};
|
|
551
548
|
|
|
549
|
+
// Allocate temporary memory to store the distance matrix
|
|
550
|
+
// Previous version didn't need temporary memory, but the performance was much lower.
|
|
551
|
+
// In the new design we keep two buffers - original and transposed, as in-place transpositions
|
|
552
|
+
// of non-rectangular matrixes is expensive.
|
|
553
|
+
std::vector<dense_key_and_distance_t> keys_and_distances(tasks_count * 2);
|
|
554
|
+
dense_key_and_distance_t* keys_and_distances_per_dataset = keys_and_distances.data();
|
|
555
|
+
dense_key_and_distance_t* keys_and_distances_per_query = keys_and_distances_per_dataset + tasks_count;
|
|
556
|
+
executor.dynamic(dataset_count, [&](std::size_t thread_idx, std::size_t dataset_idx) {
|
|
552
557
|
byte_t const* dataset = dataset_data + dataset_idx * dataset_info.strides[0];
|
|
558
|
+
for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx) {
|
|
553
559
|
byte_t const* query = queries_data + query_idx * queries_info.strides[0];
|
|
554
560
|
distance_t distance = metric(dataset, query);
|
|
555
|
-
|
|
556
|
-
|
|
561
|
+
std::size_t task_idx = dataset_idx * queries_count + query_idx;
|
|
562
|
+
keys_and_distances_per_dataset[task_idx].offset = static_cast<u32_t>(dataset_idx);
|
|
563
|
+
keys_and_distances_per_dataset[task_idx].distance = static_cast<f32_t>(distance);
|
|
564
|
+
}
|
|
557
565
|
|
|
558
566
|
// We don't want to check for signals from multiple threads
|
|
559
|
-
|
|
567
|
+
processed += queries_count;
|
|
560
568
|
if (thread_idx == 0)
|
|
561
569
|
if (PyErr_CheckSignals() != 0 || !progress_(processed.load(), tasks_count))
|
|
562
570
|
return false;
|
|
563
571
|
return true;
|
|
564
572
|
});
|
|
573
|
+
if (processed.load() != tasks_count)
|
|
574
|
+
return;
|
|
575
|
+
|
|
576
|
+
// Transpose in a single thread to avoid contention writing into the same memory buffers
|
|
577
|
+
for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx) {
|
|
578
|
+
for (std::size_t dataset_idx = 0; dataset_idx != dataset_count; ++dataset_idx) {
|
|
579
|
+
std::size_t from_idx = queries_count * dataset_idx + query_idx;
|
|
580
|
+
std::size_t to_idx = dataset_count * query_idx + dataset_idx;
|
|
581
|
+
keys_and_distances_per_query[to_idx] = keys_and_distances_per_dataset[from_idx];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
565
584
|
|
|
566
585
|
// Partial-sort every query result
|
|
567
|
-
|
|
568
|
-
auto start = keys_and_distances.data() + query_idx * dataset_count;
|
|
569
|
-
std::partial_sort(start, start + wanted, start + dataset_count,
|
|
570
|
-
[](dense_key_and_distance_t const& a, dense_key_and_distance_t const& b) {
|
|
571
|
-
return a.distance < b.distance;
|
|
572
|
-
});
|
|
573
|
-
|
|
586
|
+
executor.fixed(queries_count, [&](std::size_t, std::size_t query_idx) {
|
|
574
587
|
dense_key_t* keys = &keys_py2d(query_idx, 0);
|
|
575
588
|
distance_t* distances = &distances_py2d(query_idx, 0);
|
|
589
|
+
auto start = keys_and_distances_per_query + dataset_count * query_idx;
|
|
590
|
+
if (wanted > 1) {
|
|
591
|
+
std::partial_sort(start, start + wanted, start + dataset_count, less);
|
|
576
592
|
for (std::size_t i = 0; i != wanted; ++i)
|
|
577
593
|
keys[i] = static_cast<dense_key_t>(start[i].offset), distances[i] = start[i].distance;
|
|
594
|
+
} else {
|
|
595
|
+
auto max_it = std::max_element(start, start + dataset_count, less);
|
|
596
|
+
keys[0] = static_cast<dense_key_t>(max_it->offset), distances[0] = max_it->distance;
|
|
597
|
+
}
|
|
578
598
|
});
|
|
579
599
|
|
|
580
600
|
// At the end report the latest numbers, because the reporter thread may be finished earlier
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from time import time
|
|
2
|
+
|
|
3
|
+
from usearch.index import search, MetricKind, ScalarKind
|
|
4
|
+
from usearch.compiled import hardware_acceleration
|
|
5
|
+
from faiss import IndexFlatL2, knn
|
|
6
|
+
import numpy as np
|
|
7
|
+
import fire
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(
|
|
11
|
+
n: int = 10**5,
|
|
12
|
+
q: int = 10,
|
|
13
|
+
k: int = 100,
|
|
14
|
+
ndim: int = 256,
|
|
15
|
+
half: bool = False,
|
|
16
|
+
):
|
|
17
|
+
usearch_dtype = ScalarKind.F16 if half else ScalarKind.F32
|
|
18
|
+
acceleration = hardware_acceleration(
|
|
19
|
+
dtype=usearch_dtype,
|
|
20
|
+
ndim=ndim,
|
|
21
|
+
metric_kind=MetricKind.L2sq,
|
|
22
|
+
)
|
|
23
|
+
print("Hardware acceleration in USearch: ", acceleration)
|
|
24
|
+
|
|
25
|
+
x = np.random.random((n, ndim))
|
|
26
|
+
x = x.astype(np.float16) if half else x.astype(np.float32)
|
|
27
|
+
|
|
28
|
+
start = time()
|
|
29
|
+
_ = search(
|
|
30
|
+
x,
|
|
31
|
+
x[:q],
|
|
32
|
+
k,
|
|
33
|
+
MetricKind.L2sq,
|
|
34
|
+
exact=True,
|
|
35
|
+
).keys
|
|
36
|
+
print("USearch: ", time() - start)
|
|
37
|
+
|
|
38
|
+
start = time()
|
|
39
|
+
_ = knn(x, x[:q], k)[1]
|
|
40
|
+
print("FAISS: ", time() - start)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
fire.Fire(run)
|
|
@@ -11,7 +11,7 @@ from usearch.index import Match, Matches, BatchMatches, Index, Indexes
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
dimensions = [3, 97, 256]
|
|
14
|
-
batch_sizes = [1, 77]
|
|
14
|
+
batch_sizes = [1, 77, 100]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@pytest.mark.parametrize("rows", batch_sizes)
|
|
@@ -56,15 +56,16 @@ def test_exact_search(rows: int, cols: int):
|
|
|
56
56
|
:param int cols: The number of columns in the matrix.
|
|
57
57
|
"""
|
|
58
58
|
original = np.random.rand(rows, cols)
|
|
59
|
+
keys = np.arange(rows)
|
|
59
60
|
matches: BatchMatches = search(original, original, min(10, rows), exact=True)
|
|
60
61
|
top_matches = (
|
|
61
62
|
[int(m.keys[0]) for m in matches] if rows > 1 else int(matches.keys[0])
|
|
62
63
|
)
|
|
63
|
-
assert np.all(top_matches ==
|
|
64
|
+
assert np.all(top_matches == keys)
|
|
64
65
|
|
|
65
|
-
matches: Matches = search(original, original[
|
|
66
|
+
matches: Matches = search(original, original[-1], min(10, rows), exact=True)
|
|
66
67
|
top_match = int(matches.keys[0])
|
|
67
|
-
assert top_match ==
|
|
68
|
+
assert top_match == keys[-1]
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
def test_matches_creation_and_methods():
|
package/python/usearch/index.py
CHANGED
|
@@ -180,7 +180,7 @@ def _search_in_compiled(
|
|
|
180
180
|
vectors = vectors.reshape(1, len(vectors))
|
|
181
181
|
count_vectors = vectors.shape[0]
|
|
182
182
|
|
|
183
|
-
def
|
|
183
|
+
def distill_batch(
|
|
184
184
|
batch_matches: BatchMatches,
|
|
185
185
|
) -> Union[BatchMatches, Matches]:
|
|
186
186
|
return batch_matches[0] if count_vectors == 1 else batch_matches
|
|
@@ -188,22 +188,22 @@ def _search_in_compiled(
|
|
|
188
188
|
# Create progress bar if needed
|
|
189
189
|
if log:
|
|
190
190
|
name = log if isinstance(log, str) else "Search"
|
|
191
|
-
|
|
191
|
+
progress_bar = tqdm(
|
|
192
192
|
desc=name,
|
|
193
193
|
total=count_vectors,
|
|
194
194
|
unit="vector",
|
|
195
195
|
)
|
|
196
196
|
|
|
197
197
|
def update_progress_bar(processed: int, total: int) -> bool:
|
|
198
|
-
|
|
198
|
+
progress_bar.update(processed - progress_bar.n)
|
|
199
199
|
return progress if progress else True
|
|
200
200
|
|
|
201
201
|
tuple_ = compiled_callable(vectors, progress=update_progress_bar, **kwargs)
|
|
202
|
-
|
|
202
|
+
progress_bar.close()
|
|
203
203
|
else:
|
|
204
204
|
tuple_ = compiled_callable(vectors, **kwargs)
|
|
205
205
|
|
|
206
|
-
return
|
|
206
|
+
return distill_batch(BatchMatches(*tuple_))
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
def _add_to_compiled(
|