usearch 2.1.1 → 2.1.2
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 +20 -11
- package/Cargo.toml +1 -1
- package/README.md +1 -1
- package/conanfile.py +1 -1
- package/docs/format.md +63 -0
- package/docs/format.rst +5 -0
- package/docs/index.rst +1 -0
- package/include/usearch/index.hpp +17 -15
- package/package.json +1 -1
- package/python/usearch/index.py +9 -6
- package/setup.py +1 -0
- package/wasmer.toml +1 -1
|
@@ -30,7 +30,7 @@ jobs:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
rebase:
|
|
33
|
-
name: Rebase
|
|
33
|
+
name: Rebase Dev. Branch
|
|
34
34
|
needs: versioning
|
|
35
35
|
runs-on: ubuntu-latest
|
|
36
36
|
steps:
|
|
@@ -41,13 +41,14 @@ jobs:
|
|
|
41
41
|
|
|
42
42
|
- name: Perform rebase
|
|
43
43
|
run: |
|
|
44
|
+
git fetch origin main
|
|
44
45
|
git checkout main-dev
|
|
45
|
-
git rebase main
|
|
46
|
+
git rebase origin/main
|
|
46
47
|
|
|
47
48
|
- name: Push changes
|
|
48
49
|
uses: CasperWA/push-protected@v2
|
|
49
50
|
with:
|
|
50
|
-
token: ${{ secrets.
|
|
51
|
+
token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
|
|
51
52
|
branch: main-dev
|
|
52
53
|
unprotect_reviews: True
|
|
53
54
|
force: True
|
|
@@ -239,12 +240,14 @@ jobs:
|
|
|
239
240
|
- name: Calculate source hashes
|
|
240
241
|
id: hashes
|
|
241
242
|
run: |
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
i=1
|
|
244
|
+
cd $GITHUB_WORKSPACE/downloads
|
|
245
|
+
for asset_name in $(ls); do
|
|
246
|
+
asset_hash=$(sha256sum $asset_name | awk '{print $1}')
|
|
247
|
+
echo "asset_name_$i=$asset_name" >> $GITHUB_OUTPUT
|
|
248
|
+
echo "asset_hash_$i=$asset_hash" >> $GITHUB_OUTPUT
|
|
249
|
+
i=$((i+1))
|
|
250
|
+
done
|
|
248
251
|
|
|
249
252
|
- name: Upload hashes to release
|
|
250
253
|
uses: irongut/EditRelease@v1.2.0
|
|
@@ -255,8 +258,14 @@ jobs:
|
|
|
255
258
|
replacebody: false
|
|
256
259
|
body: |
|
|
257
260
|
## Hashes
|
|
258
|
-
*
|
|
259
|
-
*
|
|
261
|
+
* ${{ steps.hashes.outputs.asset_name_1 }} : `${{ steps.hashes.outputs.asset_hash_1 }}`
|
|
262
|
+
* ${{ steps.hashes.outputs.asset_name_2 }} : `${{ steps.hashes.outputs.asset_hash_2 }}`
|
|
263
|
+
* ${{ steps.hashes.outputs.asset_name_3 }} : `${{ steps.hashes.outputs.asset_hash_3 }}`
|
|
264
|
+
* ${{ steps.hashes.outputs.asset_name_4 }} : `${{ steps.hashes.outputs.asset_hash_4 }}`
|
|
265
|
+
* ${{ steps.hashes.outputs.asset_name_5 }} : `${{ steps.hashes.outputs.asset_hash_5 }}`
|
|
266
|
+
* ${{ steps.hashes.outputs.asset_name_6 }} : `${{ steps.hashes.outputs.asset_hash_6 }}`
|
|
267
|
+
* ${{ steps.hashes.outputs.asset_name_7 }} : `${{ steps.hashes.outputs.asset_hash_7 }}`
|
|
268
|
+
* ${{ steps.hashes.outputs.asset_name_8 }} : `${{ steps.hashes.outputs.asset_hash_8 }}`
|
|
260
269
|
|
|
261
270
|
|
|
262
271
|
build_wheels:
|
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ USearch is compact and broadly compatible without sacrificing performance, with
|
|
|
83
83
|
Base functionality is identical to FAISS, and the interface must be familiar if you have ever investigated Approximate Nearest Neighbors search:
|
|
84
84
|
|
|
85
85
|
```py
|
|
86
|
-
$ pip install usearch
|
|
86
|
+
$ pip install usearch
|
|
87
87
|
|
|
88
88
|
import numpy as np
|
|
89
89
|
from usearch.index import Index
|
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 = '2.1.
|
|
7
|
+
version = '2.1.2'
|
|
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/docs/format.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# USearch Format Specification
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Note**: The format is expected to change in the 3.0 release to support sparse indexes and offer even faster startup times when `view`-ing large indexes from disk.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Dense Indexes](#dense-indexes)
|
|
8
|
+
- [Core](#core)
|
|
9
|
+
- [Fragmented Storage](#fragmented-storage)
|
|
10
|
+
|
|
11
|
+
## Dense Indexes
|
|
12
|
+
|
|
13
|
+
Dense index files consist of two main parts: [a binary matrix of all vectors](#matrix-blob) and the [index](#index-blob) itself.
|
|
14
|
+
|
|
15
|
+
### Matrix BLOB
|
|
16
|
+
|
|
17
|
+
The matrix BLOB is optionally prepended to the index file. It starts with the number of `rows` and `columns`, which can be either 32-bit or 64-bit unsigned integers.
|
|
18
|
+
|
|
19
|
+
- The number of `rows` corresponds to the number of vectors in the index.
|
|
20
|
+
- The number of `columns` corresponds to the number of bytes in a single vector.
|
|
21
|
+
|
|
22
|
+
### Index BLOB
|
|
23
|
+
|
|
24
|
+
The index BLOB is composed of three main sections:
|
|
25
|
+
|
|
26
|
+
1. **Metadata**: Occupies 64 bytes and contains the following fields:
|
|
27
|
+
1. 7-byte magic string `usearch` to infer the MIME type of the file.
|
|
28
|
+
2. 3-byte version number, containing the `major`, `minor`, and `patch` release numbers.
|
|
29
|
+
3. 1-byte enums for `metric`, `scalar`, `key`, and `compressed_slot` types.
|
|
30
|
+
4. 8-byte integers for the number of present vectors, number of deleted vectors, and number of dimensions.
|
|
31
|
+
5. 1-byte flags for multi-vector support.
|
|
32
|
+
|
|
33
|
+
2. **Levels**: Contains 1-byte integers representing the level each node occupies. This information is used to estimate memory requirements and to enable memory-mapping of the entire file.
|
|
34
|
+
|
|
35
|
+
3. **Core**: Detailed in the [Core section](#core). This format is consistent across all types of indexes.
|
|
36
|
+
|
|
37
|
+
## Fragmented Storage
|
|
38
|
+
|
|
39
|
+
The matrix BLOB can be stored separately and is not required to successfully open the file.
|
|
40
|
+
|
|
41
|
+
## Core
|
|
42
|
+
|
|
43
|
+
The core section of the index is serialized as a sequence of nodes, each representing a point in the index. This section immediately follows the `levels` block in the serialized format.
|
|
44
|
+
|
|
45
|
+
### Header
|
|
46
|
+
|
|
47
|
+
The core section begins with a header containing the following fields:
|
|
48
|
+
|
|
49
|
+
- `uint64_t size`: Number of nodes in the index.
|
|
50
|
+
- `uint64_t connectivity`: Maximum number of edges for a node.
|
|
51
|
+
- `uint64_t connectivity_base`: Base connectivity value.
|
|
52
|
+
- `uint64_t max_level`: Maximum level in the multi-level graph.
|
|
53
|
+
- `uint64_t entry_slot`: Entry slot for the graph.
|
|
54
|
+
|
|
55
|
+
These fields are serialized in a contiguous block of bytes, in the order listed.
|
|
56
|
+
|
|
57
|
+
### Levels
|
|
58
|
+
|
|
59
|
+
Following the header, the `levels` block contains a sequence of 1-byte integers. Each integer represents the level of a node in the index. The number of levels is equal to the `size` field in the header.
|
|
60
|
+
|
|
61
|
+
### Nodes
|
|
62
|
+
|
|
63
|
+
After the `levels` block, the nodes are serialized. Each node is represented as a contiguous block of bytes. The size and format of these blocks are implementation-specific. Nodes are serialized in the same order as their corresponding levels in the `levels` block.
|
package/docs/format.rst
ADDED
package/docs/index.rst
CHANGED
|
@@ -1272,7 +1272,7 @@ struct dummy_executor_t {
|
|
|
1272
1272
|
* @brief An example of what a USearch-compatible key-to-key mapping should look like.
|
|
1273
1273
|
*
|
|
1274
1274
|
* This is particularly helpful for "Semantic Joins", where we map entries of one collection
|
|
1275
|
-
* to entries of another. In
|
|
1275
|
+
* to entries of another. In asymmetric setups, where A -> B is needed, but B -> A is not,
|
|
1276
1276
|
* this can be passed to minimize memory usage.
|
|
1277
1277
|
*/
|
|
1278
1278
|
struct dummy_key_to_key_mapping_t {
|
|
@@ -1284,16 +1284,16 @@ struct dummy_key_to_key_mapping_t {
|
|
|
1284
1284
|
|
|
1285
1285
|
/**
|
|
1286
1286
|
* @brief Checks if the provided object has a dummy type, emulating an interface,
|
|
1287
|
-
* but
|
|
1287
|
+
* but performing no real computation.
|
|
1288
1288
|
*/
|
|
1289
1289
|
template <typename object_at> static constexpr bool is_dummy() {
|
|
1290
1290
|
using object_t = typename std::remove_all_extents<object_at>::type;
|
|
1291
|
-
return std::is_same<object_t, dummy_predicate_t>::value || //
|
|
1292
|
-
std::is_same<object_t, dummy_callback_t>::value || //
|
|
1293
|
-
std::is_same<object_t, dummy_progress_t>::value || //
|
|
1294
|
-
std::is_same<object_t, dummy_prefetch_t>::value || //
|
|
1295
|
-
std::is_same<object_t, dummy_executor_t>::value || //
|
|
1296
|
-
std::is_same<object_t, dummy_key_to_key_mapping_t>::value;
|
|
1291
|
+
return std::is_same<typename std::decay<object_t>::type, dummy_predicate_t>::value || //
|
|
1292
|
+
std::is_same<typename std::decay<object_t>::type, dummy_callback_t>::value || //
|
|
1293
|
+
std::is_same<typename std::decay<object_t>::type, dummy_progress_t>::value || //
|
|
1294
|
+
std::is_same<typename std::decay<object_t>::type, dummy_prefetch_t>::value || //
|
|
1295
|
+
std::is_same<typename std::decay<object_t>::type, dummy_executor_t>::value || //
|
|
1296
|
+
std::is_same<typename std::decay<object_t>::type, dummy_key_to_key_mapping_t>::value;
|
|
1297
1297
|
}
|
|
1298
1298
|
|
|
1299
1299
|
template <typename, typename at> struct has_reset_gt {
|
|
@@ -1513,6 +1513,8 @@ class memory_mapped_file_t {
|
|
|
1513
1513
|
#else
|
|
1514
1514
|
int descriptor = open(path_, O_RDONLY);
|
|
1515
1515
|
#endif
|
|
1516
|
+
if (descriptor < 0)
|
|
1517
|
+
return result.failed(std::strerror(errno));
|
|
1516
1518
|
|
|
1517
1519
|
// Estimate the file size
|
|
1518
1520
|
struct stat file_stat;
|
|
@@ -2502,7 +2504,7 @@ class index_gt {
|
|
|
2502
2504
|
}
|
|
2503
2505
|
|
|
2504
2506
|
/**
|
|
2505
|
-
* @brief Identifies the closest cluster to the
|
|
2507
|
+
* @brief Identifies the closest cluster to the given ::query. Thread-safe.
|
|
2506
2508
|
*
|
|
2507
2509
|
* @param[in] query Content that will be compared against other entries in the index.
|
|
2508
2510
|
* @param[in] level The index level to target. Higher means lower resolution.
|
|
@@ -3325,7 +3327,7 @@ class index_gt {
|
|
|
3325
3327
|
visits.clear();
|
|
3326
3328
|
|
|
3327
3329
|
// Optional prefetching
|
|
3328
|
-
if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
|
|
3330
|
+
if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value)
|
|
3329
3331
|
prefetch(citerator_at(closest_slot), citerator_at(closest_slot + 1));
|
|
3330
3332
|
|
|
3331
3333
|
distance_t closest_dist = context.measure(query, citerator_at(closest_slot), metric);
|
|
@@ -3337,7 +3339,7 @@ class index_gt {
|
|
|
3337
3339
|
neighbors_ref_t closest_neighbors = neighbors_non_base_(node_at_(closest_slot), level);
|
|
3338
3340
|
|
|
3339
3341
|
// Optional prefetching
|
|
3340
|
-
if (!std::is_same<prefetch_at, dummy_prefetch_t>::value) {
|
|
3342
|
+
if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value) {
|
|
3341
3343
|
candidates_range_t missing_candidates{*this, closest_neighbors, visits};
|
|
3342
3344
|
prefetch(missing_candidates.begin(), missing_candidates.end());
|
|
3343
3345
|
}
|
|
@@ -3379,7 +3381,7 @@ class index_gt {
|
|
|
3379
3381
|
return false;
|
|
3380
3382
|
|
|
3381
3383
|
// Optional prefetching
|
|
3382
|
-
if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
|
|
3384
|
+
if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value)
|
|
3383
3385
|
prefetch(citerator_at(start_slot), citerator_at(start_slot + 1));
|
|
3384
3386
|
|
|
3385
3387
|
distance_t radius = context.measure(query, citerator_at(start_slot), metric);
|
|
@@ -3404,7 +3406,7 @@ class index_gt {
|
|
|
3404
3406
|
neighbors_ref_t candidate_neighbors = neighbors_(candidate_ref, level);
|
|
3405
3407
|
|
|
3406
3408
|
// Optional prefetching
|
|
3407
|
-
if (!std::is_same<prefetch_at, dummy_prefetch_t>::value) {
|
|
3409
|
+
if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value) {
|
|
3408
3410
|
candidates_range_t missing_candidates{*this, candidate_neighbors, visits};
|
|
3409
3411
|
prefetch(missing_candidates.begin(), missing_candidates.end());
|
|
3410
3412
|
}
|
|
@@ -3453,7 +3455,7 @@ class index_gt {
|
|
|
3453
3455
|
return false;
|
|
3454
3456
|
|
|
3455
3457
|
// Optional prefetching
|
|
3456
|
-
if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
|
|
3458
|
+
if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value)
|
|
3457
3459
|
prefetch(citerator_at(start_slot), citerator_at(start_slot + 1));
|
|
3458
3460
|
|
|
3459
3461
|
distance_t radius = context.measure(query, citerator_at(start_slot), metric);
|
|
@@ -3473,7 +3475,7 @@ class index_gt {
|
|
|
3473
3475
|
neighbors_ref_t candidate_neighbors = neighbors_base_(node_at_(candidate.slot));
|
|
3474
3476
|
|
|
3475
3477
|
// Optional prefetching
|
|
3476
|
-
if (!std::is_same<prefetch_at, dummy_prefetch_t>::value) {
|
|
3478
|
+
if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value) {
|
|
3477
3479
|
candidates_range_t missing_candidates{*this, candidate_neighbors, visits};
|
|
3478
3480
|
prefetch(missing_candidates.begin(), missing_candidates.end());
|
|
3479
3481
|
}
|
package/package.json
CHANGED
package/python/usearch/index.py
CHANGED
|
@@ -562,12 +562,11 @@ class Index:
|
|
|
562
562
|
)
|
|
563
563
|
|
|
564
564
|
self.path = path
|
|
565
|
-
if path
|
|
566
|
-
path = os.fspath(path)
|
|
565
|
+
if path is not None:
|
|
567
566
|
if view:
|
|
568
|
-
self.
|
|
567
|
+
self.view(path)
|
|
569
568
|
else:
|
|
570
|
-
self.
|
|
569
|
+
self.load(path)
|
|
571
570
|
|
|
572
571
|
@staticmethod
|
|
573
572
|
def metadata(path_or_buffer: PathOrBuffer) -> Optional[dict]:
|
|
@@ -919,7 +918,9 @@ class Index:
|
|
|
919
918
|
def expansion_search(self, v: int):
|
|
920
919
|
self._compiled.expansion_search = v
|
|
921
920
|
|
|
922
|
-
def save(
|
|
921
|
+
def save(
|
|
922
|
+
self, path_or_buffer: Union[str, os.PathLike, NoneType] = None
|
|
923
|
+
) -> Optional[bytes]:
|
|
923
924
|
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
924
925
|
if path_or_buffer is None:
|
|
925
926
|
return self._compiled.save_index_to_buffer()
|
|
@@ -937,7 +938,9 @@ class Index:
|
|
|
937
938
|
else:
|
|
938
939
|
self._compiled.load_index_from_path(os.fspath(path_or_buffer))
|
|
939
940
|
|
|
940
|
-
def view(
|
|
941
|
+
def view(
|
|
942
|
+
self, path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None
|
|
943
|
+
):
|
|
941
944
|
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
942
945
|
if path_or_buffer is None:
|
|
943
946
|
raise Exception("Define the source")
|
package/setup.py
CHANGED
|
@@ -66,6 +66,7 @@ setup(
|
|
|
66
66
|
package_dir={"usearch": "python/usearch"},
|
|
67
67
|
description="Smaller & Faster Single-File Vector Search Engine from Unum",
|
|
68
68
|
author="Ash Vardanian",
|
|
69
|
+
author_email="info@unum.cloud",
|
|
69
70
|
long_description=long_description,
|
|
70
71
|
long_description_content_type="text/markdown",
|
|
71
72
|
license="Apache-2.0",
|