usearch 2.4.0 → 2.5.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 +1 -1
- package/.github/workflows/release.yml +93 -1
- 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/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 +1 -1
- package/package.json +2 -2
- package/python/lib.cpp +3 -3
- package/python/usearch/index.py +1 -1
- package/rust/lib.cpp +38 -31
- package/rust/lib.hpp +11 -7
- package/rust/lib.rs +303 -170
- 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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usearch",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Smaller & Faster Single-File Vector Search Engine from Unum",
|
|
5
5
|
"author": "Ash Vardanian",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@semantic-release/exec": "^6.0.3",
|
|
26
26
|
"@semantic-release/git": "^10.0.1",
|
|
27
27
|
"conventional-changelog-eslint": "^3.0.9",
|
|
28
|
-
"semantic-release": "^21.1.
|
|
28
|
+
"semantic-release": "^21.1.2",
|
|
29
29
|
"typescript": "^5.1.6"
|
|
30
30
|
}
|
|
31
31
|
}
|
package/python/lib.cpp
CHANGED
|
@@ -608,9 +608,9 @@ static py::tuple search_many_brute_force( //
|
|
|
608
608
|
if (dataset_kind != queries_kind)
|
|
609
609
|
throw std::invalid_argument("The types of vectors don't match!");
|
|
610
610
|
|
|
611
|
-
py::array_t<dense_key_t> keys_py({
|
|
612
|
-
py::array_t<distance_t> distances_py({
|
|
613
|
-
py::array_t<Py_ssize_t> counts_py(
|
|
611
|
+
py::array_t<dense_key_t> keys_py({queries_count, static_cast<Py_ssize_t>(wanted)});
|
|
612
|
+
py::array_t<distance_t> distances_py({queries_count, static_cast<Py_ssize_t>(wanted)});
|
|
613
|
+
py::array_t<Py_ssize_t> counts_py(queries_count);
|
|
614
614
|
|
|
615
615
|
std::size_t dimensions = static_cast<std::size_t>(queries_dimensions);
|
|
616
616
|
metric_t metric = //
|
package/python/usearch/index.py
CHANGED
|
@@ -1200,7 +1200,7 @@ class Index:
|
|
|
1200
1200
|
def __repr__(self) -> str:
|
|
1201
1201
|
if not hasattr(self, "_compiled"):
|
|
1202
1202
|
return "usearch.Index(failed)"
|
|
1203
|
-
f = "usearch.Index({} x {}, {}, connectivity: {}, expansion: {} & {}, {:,} vectors in {} levels, {} hardware
|
|
1203
|
+
f = "usearch.Index({} x {}, {}, connectivity: {}, expansion: {} & {}, {:,} vectors in {} levels, {} hardware acceleration)"
|
|
1204
1204
|
return f.format(
|
|
1205
1205
|
self.dtype,
|
|
1206
1206
|
self.ndim,
|
package/rust/lib.cpp
CHANGED
|
@@ -23,65 +23,71 @@ template <typename scalar_at> Matches search_(index_dense_t& index, scalar_at co
|
|
|
23
23
|
return matches;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
NativeIndex::NativeIndex(std::unique_ptr<index_t> index) : index_(std::move(index)) {}
|
|
27
27
|
|
|
28
28
|
// clang-format off
|
|
29
|
-
void
|
|
30
|
-
void
|
|
31
|
-
void
|
|
32
|
-
void
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Matches
|
|
36
|
-
Matches
|
|
37
|
-
Matches
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
void NativeIndex::add_i8(key_t key, rust::Slice<int8_t const> vec) const { index_->add(key, vec.data()).error.raise(); }
|
|
30
|
+
void NativeIndex::add_f16(key_t key, rust::Slice<uint16_t const> vec) const { index_->add(key, (f16_t const*)vec.data()).error.raise(); }
|
|
31
|
+
void NativeIndex::add_f32(key_t key, rust::Slice<float const> vec) const { index_->add(key, vec.data()).error.raise(); }
|
|
32
|
+
void NativeIndex::add_f64(key_t key, rust::Slice<double const> vec) const { index_->add(key, vec.data()).error.raise(); }
|
|
33
|
+
|
|
34
|
+
Matches NativeIndex::search_i8(rust::Slice<int8_t const> vec, size_t count) const { return search_(*index_, vec.data(), count); }
|
|
35
|
+
Matches NativeIndex::search_f16(rust::Slice<uint16_t const> vec, size_t count) const { return search_(*index_, (f16_t const*)vec.data(), count); }
|
|
36
|
+
Matches NativeIndex::search_f32(rust::Slice<float const> vec, size_t count) const { return search_(*index_, vec.data(), count); }
|
|
37
|
+
Matches NativeIndex::search_f64(rust::Slice<double const> vec, size_t count) const { return search_(*index_, vec.data(), count); }
|
|
38
|
+
|
|
39
|
+
size_t NativeIndex::get_i8(key_t key, rust::Slice<int8_t> vec) const { return index_->get(key, vec.data(), vec.size() / dimensions()); }
|
|
40
|
+
size_t NativeIndex::get_f16(key_t key, rust::Slice<uint16_t> vec) const { return index_->get(key, (f16_t*)vec.data(), vec.size() / dimensions()); }
|
|
41
|
+
size_t NativeIndex::get_f32(key_t key, rust::Slice<float> vec) const { return index_->get(key, vec.data(), vec.size() / dimensions()); }
|
|
42
|
+
size_t NativeIndex::get_f64(key_t key, rust::Slice<double> vec) const { return index_->get(key, vec.data(), vec.size() / dimensions()); }
|
|
40
43
|
// clang-format on
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
size_t NativeIndex::remove(key_t key) const {
|
|
43
46
|
labeling_result_t result = index_->remove(key);
|
|
44
47
|
result.error.raise();
|
|
45
48
|
return result.completed;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
size_t NativeIndex::rename(key_t from, key_t to) const {
|
|
49
52
|
labeling_result_t result = index_->rename(from, to);
|
|
50
53
|
result.error.raise();
|
|
51
54
|
return result.completed;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
size_t NativeIndex::count(key_t key) const { return index_->count(key); }
|
|
58
|
+
bool NativeIndex::contains(key_t key) const { return index_->contains(key); }
|
|
55
59
|
|
|
56
|
-
void
|
|
60
|
+
void NativeIndex::reserve(size_t capacity) const { index_->reserve(capacity); }
|
|
57
61
|
|
|
58
|
-
size_t
|
|
59
|
-
size_t
|
|
60
|
-
size_t
|
|
61
|
-
size_t
|
|
62
|
-
size_t
|
|
62
|
+
size_t NativeIndex::dimensions() const { return index_->dimensions(); }
|
|
63
|
+
size_t NativeIndex::connectivity() const { return index_->connectivity(); }
|
|
64
|
+
size_t NativeIndex::size() const { return index_->size(); }
|
|
65
|
+
size_t NativeIndex::capacity() const { return index_->capacity(); }
|
|
66
|
+
size_t NativeIndex::serialized_length() const { return index_->serialized_length(); }
|
|
63
67
|
|
|
64
|
-
void
|
|
65
|
-
void
|
|
66
|
-
void
|
|
68
|
+
void NativeIndex::save(rust::Str path) const { index_->save(output_file_t(std::string(path).c_str())).error.raise(); }
|
|
69
|
+
void NativeIndex::load(rust::Str path) const { index_->load(input_file_t(std::string(path).c_str())).error.raise(); }
|
|
70
|
+
void NativeIndex::view(rust::Str path) const {
|
|
71
|
+
index_->view(memory_mapped_file_t(std::string(path).c_str())).error.raise();
|
|
72
|
+
}
|
|
67
73
|
|
|
68
|
-
void
|
|
74
|
+
void NativeIndex::save_to_buffer(rust::Slice<uint8_t> buffer) const {
|
|
69
75
|
index_->save(memory_mapped_file_t((byte_t*)buffer.data(), buffer.size())).error.raise();
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
void
|
|
78
|
+
void NativeIndex::load_from_buffer(rust::Slice<uint8_t const> buffer) const {
|
|
73
79
|
index_->load(memory_mapped_file_t((byte_t*)buffer.data(), buffer.size())).error.raise();
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
void
|
|
82
|
+
void NativeIndex::view_from_buffer(rust::Slice<uint8_t const> buffer) const {
|
|
77
83
|
index_->view(memory_mapped_file_t((byte_t*)buffer.data(), buffer.size())).error.raise();
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
std::unique_ptr<
|
|
86
|
+
std::unique_ptr<NativeIndex> wrap(index_t&& index) {
|
|
81
87
|
std::unique_ptr<index_t> punned_ptr;
|
|
82
88
|
punned_ptr.reset(new index_t(std::move(index)));
|
|
83
|
-
std::unique_ptr<
|
|
84
|
-
result.reset(new
|
|
89
|
+
std::unique_ptr<NativeIndex> result;
|
|
90
|
+
result.reset(new NativeIndex(std::move(punned_ptr)));
|
|
85
91
|
return result;
|
|
86
92
|
}
|
|
87
93
|
|
|
@@ -110,10 +116,11 @@ scalar_kind_t rust_to_cpp_scalar(ScalarKind value) {
|
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
std::unique_ptr<
|
|
119
|
+
std::unique_ptr<NativeIndex> new_native_index(IndexOptions const& options) {
|
|
114
120
|
metric_kind_t metric_kind = rust_to_cpp_metric(options.metric);
|
|
115
121
|
scalar_kind_t scalar_kind = rust_to_cpp_scalar(options.quantization);
|
|
116
122
|
metric_punned_t metric(options.dimensions, metric_kind, scalar_kind);
|
|
117
123
|
index_dense_config_t config(options.connectivity, options.expansion_add, options.expansion_search);
|
|
124
|
+
config.multi = options.multi;
|
|
118
125
|
return wrap(index_t::make(metric, config));
|
|
119
126
|
}
|
package/rust/lib.hpp
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
struct Matches;
|
|
9
9
|
struct IndexOptions;
|
|
10
10
|
|
|
11
|
-
class
|
|
11
|
+
class NativeIndex {
|
|
12
12
|
public:
|
|
13
13
|
using metric_t = unum::usearch::metric_punned_t;
|
|
14
14
|
using distance_t = unum::usearch::distance_punned_t;
|
|
@@ -17,25 +17,29 @@ class Index {
|
|
|
17
17
|
using search_result_t = typename index_dense_t::search_result_t;
|
|
18
18
|
using key_t = typename index_dense_t::key_t;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
NativeIndex(std::unique_ptr<index_dense_t> index);
|
|
21
21
|
|
|
22
22
|
void reserve(size_t) const;
|
|
23
23
|
|
|
24
|
-
void add(key_t key, rust::Slice<float const> vector) const;
|
|
25
24
|
void add_i8(key_t key, rust::Slice<int8_t const> vector) const;
|
|
26
25
|
void add_f16(key_t key, rust::Slice<uint16_t const> vector) const;
|
|
27
26
|
void add_f32(key_t key, rust::Slice<float const> vector) const;
|
|
28
27
|
void add_f64(key_t key, rust::Slice<double const> vector) const;
|
|
29
28
|
|
|
30
|
-
Matches search(rust::Slice<float const> vector, size_t count) const;
|
|
31
29
|
Matches search_i8(rust::Slice<int8_t const> vector, size_t count) const;
|
|
32
30
|
Matches search_f16(rust::Slice<uint16_t const> vector, size_t count) const;
|
|
33
31
|
Matches search_f32(rust::Slice<float const> vector, size_t count) const;
|
|
34
32
|
Matches search_f64(rust::Slice<double const> vector, size_t count) const;
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
size_t get_i8(key_t key, rust::Slice<int8_t> vector) const;
|
|
35
|
+
size_t get_f16(key_t key, rust::Slice<uint16_t> vector) const;
|
|
36
|
+
size_t get_f32(key_t key, rust::Slice<float> vector) const;
|
|
37
|
+
size_t get_f64(key_t key, rust::Slice<double> vector) const;
|
|
38
|
+
|
|
39
|
+
size_t count(key_t key) const;
|
|
40
|
+
size_t remove(key_t key) const;
|
|
41
|
+
size_t rename(key_t from, key_t to) const;
|
|
37
42
|
bool contains(key_t key) const;
|
|
38
|
-
bool rename(key_t from, key_t to) const;
|
|
39
43
|
|
|
40
44
|
size_t dimensions() const;
|
|
41
45
|
size_t connectivity() const;
|
|
@@ -55,4 +59,4 @@ class Index {
|
|
|
55
59
|
std::unique_ptr<index_dense_t> index_;
|
|
56
60
|
};
|
|
57
61
|
|
|
58
|
-
std::unique_ptr<
|
|
62
|
+
std::unique_ptr<NativeIndex> new_native_index(IndexOptions const& options);
|