usearch 2.3.2 → 2.4.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/prerelease.yml +3 -3
- package/.github/workflows/release.yml +96 -5
- package/CITATION.cff +1 -1
- package/CMakeLists.txt +2 -2
- package/Cargo.toml +1 -1
- package/README.md +2 -2
- package/c/CMakeLists.txt +1 -1
- package/c/README.md +1 -1
- package/conanfile.py +14 -7
- package/cpp/test.cpp +7 -0
- package/csharp/nuget/nuget-package.props +1 -1
- package/csharp/src/Cloud.Unum.USearch.Tests/USearchIndexTests.cs +1013 -0
- package/docs/format.md +40 -17
- package/include/usearch/index.hpp +2 -2
- package/include/usearch/index_dense.hpp +47 -27
- package/include/usearch/index_plugins.hpp +516 -7
- package/package.json +2 -2
- package/python/lib.cpp +8 -5
- package/python/scripts/cluster.ipynb +0 -7
- package/python/usearch/index.py +25 -13
- package/rust/lib.rs +24 -2
- package/wasmer.toml +1 -1
- package/csharp/src/Cloud.Unum.USearch.Tests/InitTests.cs +0 -306
package/docs/format.md
CHANGED
|
@@ -1,48 +1,71 @@
|
|
|
1
|
-
# USearch Format Specification
|
|
1
|
+
# USearch File Format Specification
|
|
2
2
|
|
|
3
|
-
> ⚠️
|
|
3
|
+
> ⚠️ __Important Note__: The file format is slated for updates in the upcoming 3.0 release. These changes aim to support sparse indexes and enhance startup performance when loading large indexes from disk using the `view` command.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Upcoming Version: v3
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- [Core](#core)
|
|
9
|
-
- [Fragmented Storage](#fragmented-storage)
|
|
7
|
+
The v3 file format is designed for compatibility with Apache Arrow arrays and consists of an array of variable-length binary strings. The structure for a file with `N` entries is as follows:
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
### File Structure
|
|
10
|
+
|
|
11
|
+
1. __File Header__: Metadata and other essential information.
|
|
12
|
+
2. __Offset Array__: An array of `N+1` eight-byte unsigned integers (`uint64_t`).
|
|
13
|
+
- The entry at index `0 <= i < N` indicates the byte-level offset of the `i-th` vector in the file.
|
|
14
|
+
- The entry at index `N` specifies the total length of the binary file.
|
|
15
|
+
3. __Data Chunks__: `N` binary data chunks, each starting with the vector itself, followed by index system data.
|
|
16
|
+
|
|
17
|
+
### Data Retrieval
|
|
18
|
+
|
|
19
|
+
To retrieve the Binary Large Object (BLOB) corresponding to a specific vector, subtract the consecutive offsets indicated in the Offset Array.
|
|
20
|
+
|
|
21
|
+
### Advantages of the v3 Format
|
|
22
|
+
|
|
23
|
+
The v3 format offers several benefits, making it more future-proof:
|
|
24
|
+
|
|
25
|
+
- __Flexibility__: Easily accommodates variable-length strings.
|
|
26
|
+
- __Performance__: Co-locates vectors with proximity graph entries for optimized data access.
|
|
27
|
+
- __Compatibility__: Simplifies memory-mapping and casting to an Apache Arrow array of vectors or strings.
|
|
28
|
+
- __Extensibility__: Enables external storage systems to reuse the underlying `index_gt` more extensively, particularly in database systems.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Current: v2
|
|
33
|
+
|
|
34
|
+
### Dense Indexes
|
|
12
35
|
|
|
13
36
|
Dense index files consist of two main parts: [a binary matrix of all vectors](#matrix-blob) and the [index](#index-blob) itself.
|
|
14
37
|
|
|
15
|
-
|
|
38
|
+
#### Matrix BLOB
|
|
16
39
|
|
|
17
40
|
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
41
|
|
|
19
42
|
- The number of `rows` corresponds to the number of vectors in the index.
|
|
20
43
|
- The number of `columns` corresponds to the number of bytes in a single vector.
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
#### Index BLOB
|
|
23
46
|
|
|
24
47
|
The index BLOB is composed of three main sections:
|
|
25
48
|
|
|
26
|
-
1.
|
|
49
|
+
1. __Metadata__: Occupies 64 bytes and contains the following fields:
|
|
27
50
|
1. 7-byte magic string `usearch` to infer the MIME type of the file.
|
|
28
51
|
2. 3-byte version number, containing the `major`, `minor`, and `patch` release numbers.
|
|
29
52
|
3. 1-byte enums for `metric`, `scalar`, `key`, and `compressed_slot` types.
|
|
30
53
|
4. 8-byte integers for the number of present vectors, number of deleted vectors, and number of dimensions.
|
|
31
54
|
5. 1-byte flags for multi-vector support.
|
|
32
55
|
|
|
33
|
-
2.
|
|
56
|
+
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
57
|
|
|
35
|
-
3.
|
|
58
|
+
3. __Core__: Detailed in the [Core section](#core). This format is consistent across all types of indexes.
|
|
36
59
|
|
|
37
|
-
|
|
60
|
+
### Fragmented Storage
|
|
38
61
|
|
|
39
62
|
The matrix BLOB can be stored separately and is not required to successfully open the file.
|
|
40
63
|
|
|
41
|
-
|
|
64
|
+
### Core
|
|
42
65
|
|
|
43
66
|
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
67
|
|
|
45
|
-
|
|
68
|
+
#### Header
|
|
46
69
|
|
|
47
70
|
The core section begins with a header containing the following fields:
|
|
48
71
|
|
|
@@ -54,10 +77,10 @@ The core section begins with a header containing the following fields:
|
|
|
54
77
|
|
|
55
78
|
These fields are serialized in a contiguous block of bytes, in the order listed.
|
|
56
79
|
|
|
57
|
-
|
|
80
|
+
#### Levels
|
|
58
81
|
|
|
59
82
|
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
83
|
|
|
61
|
-
|
|
84
|
+
#### Nodes
|
|
62
85
|
|
|
63
86
|
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.
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
#define UNUM_USEARCH_HPP
|
|
11
11
|
|
|
12
12
|
#define USEARCH_VERSION_MAJOR 2
|
|
13
|
-
#define USEARCH_VERSION_MINOR
|
|
14
|
-
#define USEARCH_VERSION_PATCH
|
|
13
|
+
#define USEARCH_VERSION_MINOR 4
|
|
14
|
+
#define USEARCH_VERSION_PATCH 1
|
|
15
15
|
|
|
16
16
|
// Inferring C++ version
|
|
17
17
|
// https://stackoverflow.com/a/61552074
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
#include <stdlib.h> // `aligned_alloc`
|
|
3
3
|
|
|
4
|
-
#include <functional>
|
|
5
|
-
#include <numeric>
|
|
6
|
-
#include <shared_mutex>
|
|
7
|
-
#include <thread>
|
|
8
|
-
#include <
|
|
9
|
-
#include <vector> // `std::vector`
|
|
4
|
+
#include <functional> // `std::function`
|
|
5
|
+
#include <numeric> // `std::iota`
|
|
6
|
+
#include <shared_mutex> // `std::shared_mutex`
|
|
7
|
+
#include <thread> // `std::thread`
|
|
8
|
+
#include <vector> // `std::vector`
|
|
10
9
|
|
|
11
10
|
#include <usearch/index.hpp>
|
|
12
11
|
#include <usearch/index_plugins.hpp>
|
|
@@ -93,6 +92,17 @@ struct index_dense_config_t : public index_config_t {
|
|
|
93
92
|
bool exclude_vectors = false;
|
|
94
93
|
bool multi = false;
|
|
95
94
|
|
|
95
|
+
/**
|
|
96
|
+
* @brief Allows you to reduce RAM consumption by avoiding
|
|
97
|
+
* reverse-indexing keys-to-vectors, and only keeping
|
|
98
|
+
* the vectors-to-keys mappings.
|
|
99
|
+
*
|
|
100
|
+
* ! This configuration parameter doesn't affect the serialized file,
|
|
101
|
+
* ! and is not preserved between runs. Makes sense for small vector
|
|
102
|
+
* ! representations that fit ina single cache line.
|
|
103
|
+
*/
|
|
104
|
+
bool enable_key_lookups = true;
|
|
105
|
+
|
|
96
106
|
index_dense_config_t(index_config_t base) noexcept : index_config_t(base) {}
|
|
97
107
|
|
|
98
108
|
index_dense_config_t( //
|
|
@@ -399,7 +409,7 @@ class index_dense_gt {
|
|
|
399
409
|
};
|
|
400
410
|
|
|
401
411
|
/// @brief Multi-Map from keys to IDs, and allocated vectors.
|
|
402
|
-
|
|
412
|
+
flat_hash_multi_set_gt<key_and_slot_t, lookup_key_hash_t, lookup_key_same_t> slot_lookup_;
|
|
403
413
|
|
|
404
414
|
/// @brief Mutex, controlling concurrent access to `slot_lookup_`.
|
|
405
415
|
mutable shared_mutex_t slot_lookup_mutex_;
|
|
@@ -1149,7 +1159,7 @@ class index_dense_gt {
|
|
|
1149
1159
|
*/
|
|
1150
1160
|
bool contains(key_t key) const {
|
|
1151
1161
|
shared_lock_t lock(slot_lookup_mutex_);
|
|
1152
|
-
return slot_lookup_.
|
|
1162
|
+
return slot_lookup_.contains(key_and_slot_t::any_slot(key));
|
|
1153
1163
|
}
|
|
1154
1164
|
|
|
1155
1165
|
/**
|
|
@@ -1203,7 +1213,7 @@ class index_dense_gt {
|
|
|
1203
1213
|
free_keys_.push(slot);
|
|
1204
1214
|
typed_->at(slot).key = free_key_;
|
|
1205
1215
|
}
|
|
1206
|
-
slot_lookup_.erase(
|
|
1216
|
+
slot_lookup_.erase(key);
|
|
1207
1217
|
result.completed = matching_count;
|
|
1208
1218
|
|
|
1209
1219
|
return result;
|
|
@@ -1239,14 +1249,15 @@ class index_dense_gt {
|
|
|
1239
1249
|
// - present in `free_keys_`
|
|
1240
1250
|
// - missing in the `slot_lookup_`
|
|
1241
1251
|
// - marked in the `typed_` index with a `free_key_`
|
|
1252
|
+
matching_count = 0;
|
|
1242
1253
|
for (auto slots_it = matching_slots.first; slots_it != matching_slots.second; ++slots_it) {
|
|
1243
1254
|
compressed_slot_t slot = (*slots_it).slot;
|
|
1244
1255
|
free_keys_.push(slot);
|
|
1245
1256
|
typed_->at(slot).key = free_key_;
|
|
1257
|
+
++matching_count;
|
|
1246
1258
|
}
|
|
1247
1259
|
|
|
1248
|
-
|
|
1249
|
-
slot_lookup_.erase(matching_slots.first, matching_slots.second);
|
|
1260
|
+
slot_lookup_.erase(key);
|
|
1250
1261
|
result.completed += matching_count;
|
|
1251
1262
|
}
|
|
1252
1263
|
|
|
@@ -1265,20 +1276,18 @@ class index_dense_gt {
|
|
|
1265
1276
|
labeling_result_t result;
|
|
1266
1277
|
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
1267
1278
|
|
|
1268
|
-
if (!multi() && slot_lookup_.
|
|
1279
|
+
if (!multi() && slot_lookup_.contains(key_and_slot_t::any_slot(to)))
|
|
1269
1280
|
return result.failed("Renaming impossible, the key is already in use");
|
|
1270
1281
|
|
|
1271
1282
|
// The `from` may map to multiple entries
|
|
1272
1283
|
while (true) {
|
|
1273
|
-
|
|
1274
|
-
if (
|
|
1284
|
+
key_and_slot_t key_and_slot_removed;
|
|
1285
|
+
if (!slot_lookup_.pop_first(key_and_slot_t::any_slot(from), key_and_slot_removed))
|
|
1275
1286
|
break;
|
|
1276
1287
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
slot_lookup_.insert(key_and_slot);
|
|
1281
|
-
typed_->at(slot).key = to;
|
|
1288
|
+
key_and_slot_t key_and_slot_replacing{to, key_and_slot_removed.slot};
|
|
1289
|
+
slot_lookup_.try_emplace(key_and_slot_replacing); // This can't fail
|
|
1290
|
+
typed_->at(key_and_slot_removed.slot).key = to;
|
|
1282
1291
|
++result.completed;
|
|
1283
1292
|
}
|
|
1284
1293
|
|
|
@@ -1293,11 +1302,18 @@ class index_dense_gt {
|
|
|
1293
1302
|
*/
|
|
1294
1303
|
void export_keys(key_t* keys, std::size_t offset, std::size_t limit) const {
|
|
1295
1304
|
shared_lock_t lock(slot_lookup_mutex_);
|
|
1296
|
-
auto it = slot_lookup_.begin();
|
|
1297
1305
|
offset = (std::min)(offset, slot_lookup_.size());
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1306
|
+
slot_lookup_.for_each([&](key_and_slot_t const& key_and_slot) {
|
|
1307
|
+
if (offset) {
|
|
1308
|
+
--offset;
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (limit) {
|
|
1312
|
+
*keys = key_and_slot.key;
|
|
1313
|
+
++keys;
|
|
1314
|
+
--limit;
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1301
1317
|
}
|
|
1302
1318
|
|
|
1303
1319
|
struct copy_result_t {
|
|
@@ -1720,7 +1736,7 @@ class index_dense_gt {
|
|
|
1720
1736
|
bool reuse_node = free_slot != default_free_value<compressed_slot_t>();
|
|
1721
1737
|
auto on_success = [&](member_ref_t member) {
|
|
1722
1738
|
unique_lock_t slot_lock(slot_lookup_mutex_);
|
|
1723
|
-
slot_lookup_.
|
|
1739
|
+
slot_lookup_.try_emplace(key_and_slot_t{key, static_cast<compressed_slot_t>(member.slot)});
|
|
1724
1740
|
if (copy_vector) {
|
|
1725
1741
|
if (!reuse_node)
|
|
1726
1742
|
vectors_lookup_[member.slot] = vectors_tape_allocator_.allocate(metric_.bytes_per_vector());
|
|
@@ -1842,19 +1858,23 @@ class index_dense_gt {
|
|
|
1842
1858
|
count_removed += member.key == free_key_;
|
|
1843
1859
|
}
|
|
1844
1860
|
|
|
1861
|
+
if (!count_removed && !config_.enable_key_lookups)
|
|
1862
|
+
return;
|
|
1863
|
+
|
|
1845
1864
|
// Pull entries from the underlying `typed_` into either
|
|
1846
1865
|
// into `slot_lookup_`, or `free_keys_` if they are unused.
|
|
1847
1866
|
unique_lock_t lock(slot_lookup_mutex_);
|
|
1848
1867
|
slot_lookup_.clear();
|
|
1849
|
-
|
|
1868
|
+
if (config_.enable_key_lookups)
|
|
1869
|
+
slot_lookup_.reserve(count_total - count_removed);
|
|
1850
1870
|
free_keys_.clear();
|
|
1851
1871
|
free_keys_.reserve(count_removed);
|
|
1852
1872
|
for (std::size_t i = 0; i != typed_->size(); ++i) {
|
|
1853
1873
|
member_cref_t member = typed_->at(i);
|
|
1854
1874
|
if (member.key == free_key_)
|
|
1855
1875
|
free_keys_.push(static_cast<compressed_slot_t>(i));
|
|
1856
|
-
else
|
|
1857
|
-
slot_lookup_.
|
|
1876
|
+
else if (config_.enable_key_lookups)
|
|
1877
|
+
slot_lookup_.try_emplace(key_and_slot_t{key_t(member.key), static_cast<compressed_slot_t>(i)});
|
|
1858
1878
|
}
|
|
1859
1879
|
}
|
|
1860
1880
|
|