usearch 0.1.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/.clang-format +10 -0
- package/.github/workflows/package.json +82 -0
- package/.github/workflows/release.yml +111 -0
- package/.gitmodules +6 -0
- package/.vscode/launch.json +116 -0
- package/.vscode/settings.json +109 -0
- package/.vscode/tasks.json +35 -0
- package/CMakeLists.txt +84 -0
- package/CMakeListsInstructuons.md +109 -0
- package/README.md +69 -0
- package/VERSION +1 -0
- package/assets/unum.png +0 -0
- package/binding.gyp +28 -0
- package/build.rs +14 -0
- package/cargo.toml +15 -0
- package/cmake/clipp.cmake +8 -0
- package/cmake/jemalloc.cmake +80 -0
- package/docs/Makefile +20 -0
- package/docs/_static/custom.css +26 -0
- package/docs/_static/custom.js +7 -0
- package/docs/conf.py +48 -0
- package/docs/index.rst +10 -0
- package/fp16/CMakeLists.txt +176 -0
- package/fp16/LICENSE +11 -0
- package/fp16/README.md +20 -0
- package/fp16/bench/alt-element.cc +89 -0
- package/fp16/bench/from-alt-array.cc +175 -0
- package/fp16/bench/from-ieee-array.cc +399 -0
- package/fp16/bench/ieee-element.cc +272 -0
- package/fp16/bench/to-alt-array.cc +91 -0
- package/fp16/bench/to-ieee-array.cc +303 -0
- package/fp16/cmake/DownloadGoogleBenchmark.cmake +15 -0
- package/fp16/cmake/DownloadGoogleTest.cmake +15 -0
- package/fp16/cmake/DownloadPSimd.cmake +15 -0
- package/fp16/configure.py +65 -0
- package/fp16/confu.yaml +8 -0
- package/fp16/include/fp16/__init__.py +0 -0
- package/fp16/include/fp16/avx.py +50 -0
- package/fp16/include/fp16/avx2.py +53 -0
- package/fp16/include/fp16/bitcasts.h +92 -0
- package/fp16/include/fp16/fp16.h +451 -0
- package/fp16/include/fp16/psimd.h +131 -0
- package/fp16/include/fp16.h +11 -0
- package/fp16/jni/Android.mk +6 -0
- package/fp16/jni/Application.mk +4 -0
- package/fp16/test/alt-from-fp32-value.cc +498 -0
- package/fp16/test/alt-to-fp32-bits.cc +263 -0
- package/fp16/test/alt-to-fp32-psimd.cc +145 -0
- package/fp16/test/alt-to-fp32-value.cc +348 -0
- package/fp16/test/alt-to-fp32x2-psimd.cc +245 -0
- package/fp16/test/bitcasts.cc +56 -0
- package/fp16/test/ieee-from-fp32-value.cc +523 -0
- package/fp16/test/ieee-to-fp32-bits.cc +333 -0
- package/fp16/test/ieee-to-fp32-psimd.cc +312 -0
- package/fp16/test/ieee-to-fp32-value.cc +420 -0
- package/fp16/test/ieee-to-fp32x2-psimd.cc +541 -0
- package/fp16/test/peachpy/alt-xmm-to-fp32-xmm-avx.cc +96 -0
- package/fp16/test/peachpy/alt-xmm-to-fp32-ymm-avx2.cc +96 -0
- package/fp16/test/peachpy/stubs.py +38 -0
- package/fp16/test/tables.cc +530 -0
- package/fp16/test/tables.h +13 -0
- package/fp16/third-party/THHalf.h +92 -0
- package/fp16/third-party/eigen-half.h +170 -0
- package/fp16/third-party/float16-compressor.h +80 -0
- package/fp16/third-party/half.hpp +2916 -0
- package/fp16/third-party/npy-halffloat.h +328 -0
- package/include/usearch/usearch.hpp +1345 -0
- package/package.json +19 -0
- package/pyproject.toml +33 -0
- package/setup.py +63 -0
- package/simsimd/.clang-format +10 -0
- package/simsimd/.vscode/launch.json +24 -0
- package/simsimd/.vscode/settings.json +73 -0
- package/simsimd/.vscode/tasks.json +17 -0
- package/simsimd/CMakeLists.txt +82 -0
- package/simsimd/LICENSE +201 -0
- package/simsimd/README.md +73 -0
- package/simsimd/bench.cxx +82 -0
- package/simsimd/include/simsimd/simsimd.h +322 -0
- package/simsimd/include/simsimd/simsimd.hpp +443 -0
- package/src/advanced.hpp +489 -0
- package/src/bench.cpp +415 -0
- package/src/bench.py +89 -0
- package/src/javascript.cpp +198 -0
- package/src/main.rs +42 -0
- package/src/python.cpp +226 -0
- package/src/rust.cpp +23 -0
- package/src/rust.h +22 -0
- package/src/test.js +17 -0
- package/src/test.py +20 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file javascript.cpp
|
|
3
|
+
* @author Ashot Vardanian
|
|
4
|
+
* @brief JavaScript bindings for Unum USearch.
|
|
5
|
+
* @date 2023-04-26
|
|
6
|
+
*
|
|
7
|
+
* @copyright Copyright (c) 2023
|
|
8
|
+
*
|
|
9
|
+
* @see NodeJS docs: https://nodejs.org/api/addons.html#hello-world
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
#include <napi.h>
|
|
13
|
+
#include <node_api.h>
|
|
14
|
+
|
|
15
|
+
#include "advanced.hpp"
|
|
16
|
+
|
|
17
|
+
using namespace unum::usearch;
|
|
18
|
+
using namespace unum;
|
|
19
|
+
|
|
20
|
+
using label_t = std::uint32_t;
|
|
21
|
+
using distance_t = punned_distance_t;
|
|
22
|
+
using native_index_t = auto_index_gt<label_t>;
|
|
23
|
+
|
|
24
|
+
class Index : public Napi::ObjectWrap<Index> {
|
|
25
|
+
public:
|
|
26
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
27
|
+
Index(Napi::CallbackInfo const& ctx);
|
|
28
|
+
|
|
29
|
+
private:
|
|
30
|
+
Napi::Value GetDimensions(Napi::CallbackInfo const& ctx);
|
|
31
|
+
Napi::Value GetSize(Napi::CallbackInfo const& ctx);
|
|
32
|
+
Napi::Value GetCapacity(Napi::CallbackInfo const& ctx);
|
|
33
|
+
Napi::Value GetConnectivity(Napi::CallbackInfo const& ctx);
|
|
34
|
+
|
|
35
|
+
void Add(Napi::CallbackInfo const& ctx);
|
|
36
|
+
Napi::Value Search(Napi::CallbackInfo const& ctx);
|
|
37
|
+
|
|
38
|
+
std::unique_ptr<native_index_t> native_;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
Napi::Object Index::Init(Napi::Env env, Napi::Object exports) {
|
|
42
|
+
Napi::Function func = DefineClass( //
|
|
43
|
+
env, "Index",
|
|
44
|
+
{
|
|
45
|
+
InstanceMethod("dimensions", &Index::GetDimensions),
|
|
46
|
+
InstanceMethod("size", &Index::GetSize),
|
|
47
|
+
InstanceMethod("capacity", &Index::GetCapacity),
|
|
48
|
+
InstanceMethod("connectivity", &Index::GetConnectivity),
|
|
49
|
+
InstanceMethod("add", &Index::Add),
|
|
50
|
+
InstanceMethod("search", &Index::Search),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
Napi::FunctionReference* constructor = new Napi::FunctionReference();
|
|
54
|
+
*constructor = Napi::Persistent(func);
|
|
55
|
+
env.SetInstanceData(constructor);
|
|
56
|
+
|
|
57
|
+
exports.Set("Index", func);
|
|
58
|
+
return exports;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Index::Index(Napi::CallbackInfo const& ctx) : Napi::ObjectWrap<Index>(ctx) {
|
|
62
|
+
Napi::Env env = ctx.Env();
|
|
63
|
+
|
|
64
|
+
int length = ctx.Length();
|
|
65
|
+
if (length == 0 || length >= 2 || !ctx[0].IsObject()) {
|
|
66
|
+
Napi::TypeError::New(
|
|
67
|
+
env, "Pass args as named objects: dimensions: uint, capacity: uint, metric: [ip, cos, l2_sq, haversine]")
|
|
68
|
+
.ThrowAsJavaScriptException();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Napi::Object params = ctx[0].As<Napi::Object>();
|
|
73
|
+
std::size_t dimensions = params.Has("dimensions") ? params.Get("dimensions").As<Napi::Number>().Uint32Value() : 0;
|
|
74
|
+
|
|
75
|
+
config_t config;
|
|
76
|
+
if (params.Has("capacity"))
|
|
77
|
+
config.max_elements = params.Get("capacity").As<Napi::Number>().Uint32Value();
|
|
78
|
+
if (params.Has("connectivity"))
|
|
79
|
+
config.connectivity = params.Get("connectivity").As<Napi::Number>().Uint32Value();
|
|
80
|
+
|
|
81
|
+
accuracy_t accuracy = accuracy_t::f32_k;
|
|
82
|
+
if (params.Has("accuracy")) {
|
|
83
|
+
std::string accuracy_str = params.Get("connectivity").As<Napi::String>().Utf8Value();
|
|
84
|
+
if (accuracy_str == "f32")
|
|
85
|
+
accuracy = accuracy_t::f32_k;
|
|
86
|
+
else if (accuracy_str == "f64")
|
|
87
|
+
accuracy = accuracy_t::f64_k;
|
|
88
|
+
else if (accuracy_str == "f16")
|
|
89
|
+
accuracy = accuracy_t::f16_k;
|
|
90
|
+
else if (accuracy_str == "i8q100")
|
|
91
|
+
accuracy = accuracy_t::i8q100_k;
|
|
92
|
+
else {
|
|
93
|
+
Napi::TypeError::New(env, "Supported metrics are: [ip, cos, l2_sq, haversine]")
|
|
94
|
+
.ThrowAsJavaScriptException();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// By default we use the Inner Product similarity
|
|
100
|
+
if (params.Has("metric")) {
|
|
101
|
+
std::string name = params.Get("metric").As<Napi::String>().Utf8Value();
|
|
102
|
+
if (name == "l2_sq" || name == "euclidean_sq") {
|
|
103
|
+
if (!dimensions) {
|
|
104
|
+
Napi::TypeError::New(env, "Please define the number of dimensions").ThrowAsJavaScriptException();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
native_.reset(new native_index_t(native_index_t::l2(dimensions, accuracy, config)));
|
|
108
|
+
} else if (name == "ip" || name == "inner" || name == "dot") {
|
|
109
|
+
if (!dimensions) {
|
|
110
|
+
Napi::TypeError::New(env, "Please define the number of dimensions").ThrowAsJavaScriptException();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
native_.reset(new native_index_t(native_index_t::ip(dimensions, accuracy, config)));
|
|
114
|
+
} else if (name == "cos" || name == "angular") {
|
|
115
|
+
if (!dimensions) {
|
|
116
|
+
Napi::TypeError::New(env, "Please define the number of dimensions").ThrowAsJavaScriptException();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
native_.reset(new native_index_t(native_index_t::cos(dimensions, accuracy, config)));
|
|
120
|
+
} else if (name == "haversine") {
|
|
121
|
+
native_.reset(new native_index_t(native_index_t::haversine(accuracy, config)));
|
|
122
|
+
} else {
|
|
123
|
+
Napi::TypeError::New(env, "Supported metrics are: [ip, cos, l2_sq, haversine]")
|
|
124
|
+
.ThrowAsJavaScriptException();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
if (!dimensions) {
|
|
129
|
+
Napi::TypeError::New(env, "Please define the number of dimensions").ThrowAsJavaScriptException();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
native_.reset(new native_index_t(native_index_t::ip(dimensions, accuracy, config)));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Napi::Value Index::GetDimensions(Napi::CallbackInfo const& ctx) {
|
|
137
|
+
return Napi::Number::New(ctx.Env(), native_->dimensions());
|
|
138
|
+
}
|
|
139
|
+
Napi::Value Index::GetSize(Napi::CallbackInfo const& ctx) { return Napi::Number::New(ctx.Env(), native_->size()); }
|
|
140
|
+
Napi::Value Index::GetConnectivity(Napi::CallbackInfo const& ctx) {
|
|
141
|
+
return Napi::Number::New(ctx.Env(), native_->connectivity());
|
|
142
|
+
}
|
|
143
|
+
Napi::Value Index::GetCapacity(Napi::CallbackInfo const& ctx) {
|
|
144
|
+
return Napi::Number::New(ctx.Env(), native_->capacity());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void Index::Add(Napi::CallbackInfo const& ctx) {
|
|
148
|
+
Napi::Env env = ctx.Env();
|
|
149
|
+
if (ctx.Length() < 2 || !ctx[0].IsNumber() || !ctx[1].IsTypedArray())
|
|
150
|
+
return Napi::TypeError::New(env, "Expects an integral label and a float vector").ThrowAsJavaScriptException();
|
|
151
|
+
|
|
152
|
+
Napi::Number label_js = ctx[0].As<Napi::Number>();
|
|
153
|
+
Napi::Float32Array vector_js = ctx[1].As<Napi::Float32Array>();
|
|
154
|
+
|
|
155
|
+
label_t label = label_js.Uint32Value();
|
|
156
|
+
float const* vector = vector_js.Data();
|
|
157
|
+
std::size_t dimensions = static_cast<std::size_t>(vector_js.ElementLength());
|
|
158
|
+
if (dimensions != native_->dimensions())
|
|
159
|
+
return Napi::TypeError::New(env, "Wrong number of dimensions").ThrowAsJavaScriptException();
|
|
160
|
+
|
|
161
|
+
if (native_->size() + 1 >= native_->capacity())
|
|
162
|
+
native_->reserve(ceil2(native_->size() + 1));
|
|
163
|
+
native_->add(label, vector);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Napi::Value Index::Search(Napi::CallbackInfo const& ctx) {
|
|
167
|
+
Napi::Env env = ctx.Env();
|
|
168
|
+
if (ctx.Length() < 2 || !ctx[0].IsTypedArray() || !ctx[1].IsNumber()) {
|
|
169
|
+
Napi::TypeError::New(env, "Expects a float vector and the number of wanted results")
|
|
170
|
+
.ThrowAsJavaScriptException();
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Napi::Float32Array vector_js = ctx[0].As<Napi::Float32Array>();
|
|
175
|
+
Napi::Number wanted_js = ctx[1].As<Napi::Number>();
|
|
176
|
+
|
|
177
|
+
float const* vector = vector_js.Data();
|
|
178
|
+
std::size_t dimensions = static_cast<std::size_t>(vector_js.ElementLength());
|
|
179
|
+
std::uint32_t wanted = wanted_js.Uint32Value();
|
|
180
|
+
if (dimensions != native_->dimensions()) {
|
|
181
|
+
Napi::TypeError::New(env, "Wrong number of dimensions").ThrowAsJavaScriptException();
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
Napi::Uint32Array matches_js = Napi::Uint32Array::New(env, wanted);
|
|
186
|
+
Napi::Float32Array distances_js = Napi::Float32Array::New(env, wanted);
|
|
187
|
+
std::size_t count = native_->search(vector, wanted, matches_js.Data(), distances_js.Data());
|
|
188
|
+
|
|
189
|
+
Napi::Object result_js = Napi::Object::New(env);
|
|
190
|
+
result_js.Set("labels", matches_js);
|
|
191
|
+
result_js.Set("distances", distances_js);
|
|
192
|
+
result_js.Set("count", Napi::Number::New(env, count));
|
|
193
|
+
return result_js;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Napi::Object InitAll(Napi::Env env, Napi::Object exports) { return Index::Init(env, exports); }
|
|
197
|
+
|
|
198
|
+
NODE_API_MODULE(usearch, InitAll)
|
package/src/main.rs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#[cxx::bridge]
|
|
2
|
+
mod ffi {
|
|
3
|
+
|
|
4
|
+
// Shared structs with fields visible to both languages.
|
|
5
|
+
struct Results {
|
|
6
|
+
size: usize,
|
|
7
|
+
labels: Vec<u32>,
|
|
8
|
+
distances: Vec<f32>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// C++ types and signatures exposed to Rust.
|
|
12
|
+
unsafe extern "C++" {
|
|
13
|
+
include!("rust.h");
|
|
14
|
+
|
|
15
|
+
type Index;
|
|
16
|
+
|
|
17
|
+
fn new_index() -> UniquePtr<Index>;
|
|
18
|
+
|
|
19
|
+
fn dim(&self) -> u64;
|
|
20
|
+
fn connectivity(&self) -> u64;
|
|
21
|
+
fn size(&self) -> u64;
|
|
22
|
+
fn capacity(&self) -> u64;
|
|
23
|
+
|
|
24
|
+
fn add(&self, label: u64, vector: &[f32]);
|
|
25
|
+
fn search(&self, query: &[f32], count: u64) -> Results;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
fn main() {
|
|
31
|
+
let client = ffi::new_index();
|
|
32
|
+
|
|
33
|
+
// Upload a blob.
|
|
34
|
+
let chunks = vec![b"fearless".to_vec(), b"concurrency".to_vec()];
|
|
35
|
+
let mut buf = MultiBuf { chunks, pos: 0 };
|
|
36
|
+
let blobid = client.add(&mut buf);
|
|
37
|
+
println!("blobid = {}", blobid);
|
|
38
|
+
|
|
39
|
+
// Read back the tags.
|
|
40
|
+
let search = client.search(blobid);
|
|
41
|
+
println!("tags = {:?}", search.tags);
|
|
42
|
+
}
|
package/src/python.cpp
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file python.cpp
|
|
3
|
+
* @author Ashot Vardanian
|
|
4
|
+
* @brief Python bindings for Unum USearch.
|
|
5
|
+
* @date 2023-04-26
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
* https://pythoncapi.readthedocs.io/type_object.html
|
|
9
|
+
* https://numpy.org/doc/stable/reference/c-api/types-and-structures.html
|
|
10
|
+
* https://pythonextensionpatterns.readthedocs.io/en/latest/refcount.html
|
|
11
|
+
* https://docs.python.org/3/extending/newtypes_tutorial.html#adding-data-and-methods-to-the-basic-example
|
|
12
|
+
*
|
|
13
|
+
* @copyright Copyright (c) 2023
|
|
14
|
+
*/
|
|
15
|
+
#define PY_SSIZE_T_CLEAN
|
|
16
|
+
#include <thread>
|
|
17
|
+
|
|
18
|
+
#include <pybind11/numpy.h>
|
|
19
|
+
#include <pybind11/pybind11.h>
|
|
20
|
+
|
|
21
|
+
#include "advanced.hpp"
|
|
22
|
+
|
|
23
|
+
using namespace unum::usearch;
|
|
24
|
+
using namespace unum;
|
|
25
|
+
|
|
26
|
+
namespace py = pybind11;
|
|
27
|
+
|
|
28
|
+
using label_t = Py_ssize_t;
|
|
29
|
+
using distance_t = punned_distance_t;
|
|
30
|
+
using native_index_t = auto_index_gt<label_t>;
|
|
31
|
+
|
|
32
|
+
static native_index_t make_index( //
|
|
33
|
+
std::size_t dimensions, //
|
|
34
|
+
std::size_t capacity, //
|
|
35
|
+
std::string const& scalar_type, //
|
|
36
|
+
std::string const& metric, //
|
|
37
|
+
std::size_t connectivity, //
|
|
38
|
+
std::size_t expansion_add, //
|
|
39
|
+
std::size_t expansion_search //
|
|
40
|
+
) {
|
|
41
|
+
|
|
42
|
+
config_t config;
|
|
43
|
+
config.expansion_add = expansion_add;
|
|
44
|
+
config.expansion_search = expansion_search;
|
|
45
|
+
config.connectivity = connectivity;
|
|
46
|
+
config.max_elements = capacity;
|
|
47
|
+
config.max_threads_add = std::thread::hardware_concurrency();
|
|
48
|
+
config.max_threads_search = std::thread::hardware_concurrency();
|
|
49
|
+
|
|
50
|
+
accuracy_t accuracy;
|
|
51
|
+
if (scalar_type == "f32")
|
|
52
|
+
accuracy = accuracy_t::f32_k;
|
|
53
|
+
else if (scalar_type == "f64")
|
|
54
|
+
accuracy = accuracy_t::f64_k;
|
|
55
|
+
else if (scalar_type == "f16")
|
|
56
|
+
accuracy = accuracy_t::f16_k;
|
|
57
|
+
else if (scalar_type == "i8q100")
|
|
58
|
+
accuracy = accuracy_t::i8q100_k;
|
|
59
|
+
else
|
|
60
|
+
throw std::runtime_error("Unknown type, choose: f32, f16, f64, i8q100");
|
|
61
|
+
|
|
62
|
+
if (metric == "l2_sq" || metric == "euclidean_sq")
|
|
63
|
+
return native_index_t::l2(dimensions, accuracy, config);
|
|
64
|
+
else if (metric == "ip" || metric == "inner" || metric == "dot")
|
|
65
|
+
return native_index_t::ip(dimensions, accuracy, config);
|
|
66
|
+
else if (metric == "cos" || metric == "angular")
|
|
67
|
+
return native_index_t::cos(dimensions, accuracy, config);
|
|
68
|
+
else if (metric == "haversine")
|
|
69
|
+
return native_index_t::haversine(accuracy, config);
|
|
70
|
+
else
|
|
71
|
+
throw std::runtime_error("Unknown distance, choose: l2_sq, ip, cos, hamming, jaccard");
|
|
72
|
+
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static void add_to_index(native_index_t& index, py::buffer labels, py::buffer vectors, bool copy) {
|
|
77
|
+
|
|
78
|
+
py::buffer_info labels_info = labels.request();
|
|
79
|
+
py::buffer_info vectors_info = vectors.request();
|
|
80
|
+
|
|
81
|
+
if (labels_info.format != py::format_descriptor<label_t>::format())
|
|
82
|
+
throw std::runtime_error("Incompatible label type!");
|
|
83
|
+
|
|
84
|
+
if (labels_info.ndim != 1)
|
|
85
|
+
throw std::runtime_error("Labels must be placed in a single-dimensional array!");
|
|
86
|
+
|
|
87
|
+
if (vectors_info.ndim != 2)
|
|
88
|
+
throw std::runtime_error("Expects a matrix of vectors to add!");
|
|
89
|
+
|
|
90
|
+
ssize_t labels_count = labels_info.shape[0];
|
|
91
|
+
ssize_t vectors_count = vectors_info.shape[0];
|
|
92
|
+
ssize_t vectors_dimensions = vectors_info.shape[1];
|
|
93
|
+
if (vectors_dimensions != index.dimensions())
|
|
94
|
+
throw std::runtime_error("The number of vector dimensions doesn't match!");
|
|
95
|
+
|
|
96
|
+
if (labels_count != vectors_count)
|
|
97
|
+
throw std::runtime_error("Number of labels and vectors must match!");
|
|
98
|
+
|
|
99
|
+
if (index.size() + vectors_count >= index.capacity()) {
|
|
100
|
+
std::size_t next_capacity = ceil2(index.size() + vectors_count);
|
|
101
|
+
index.reserve(next_capacity);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
105
|
+
char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
|
|
106
|
+
|
|
107
|
+
// https://docs.python.org/3/library/struct.html#format-characters
|
|
108
|
+
if (vectors_info.format == "e")
|
|
109
|
+
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
110
|
+
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
111
|
+
f16_converted_t const* vector =
|
|
112
|
+
reinterpret_cast<f16_converted_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
113
|
+
index.add(label, vector, thread_idx, copy);
|
|
114
|
+
});
|
|
115
|
+
else if (vectors_info.format == "f")
|
|
116
|
+
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
117
|
+
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
118
|
+
float const* vector = reinterpret_cast<float const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
119
|
+
index.add(label, vector, thread_idx, copy);
|
|
120
|
+
});
|
|
121
|
+
else if (vectors_info.format == "d")
|
|
122
|
+
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
123
|
+
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
124
|
+
double const* vector = reinterpret_cast<double const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
125
|
+
index.add(label, vector, thread_idx, copy);
|
|
126
|
+
});
|
|
127
|
+
else
|
|
128
|
+
throw std::runtime_error("Incompatible scalars in the vectors matrix!");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param vectors Matrix of vectors to search for..
|
|
133
|
+
* @param wanted Number of matches per request.
|
|
134
|
+
*
|
|
135
|
+
* @return Tuple with:
|
|
136
|
+
* 1. matrix of neighbors,
|
|
137
|
+
* 2. matrix of distances,
|
|
138
|
+
* 3. array with match counts.
|
|
139
|
+
*/
|
|
140
|
+
static py::tuple search_in_index(native_index_t& index, py::buffer vectors, ssize_t wanted) {
|
|
141
|
+
|
|
142
|
+
py::buffer_info vectors_info = vectors.request();
|
|
143
|
+
if (vectors_info.ndim != 2)
|
|
144
|
+
throw std::runtime_error("Expects a matrix of vectors to add!");
|
|
145
|
+
|
|
146
|
+
ssize_t vectors_count = vectors_info.shape[0];
|
|
147
|
+
ssize_t vectors_dimensions = vectors_info.shape[1];
|
|
148
|
+
if (vectors_dimensions != index.dimensions())
|
|
149
|
+
throw std::runtime_error("The number of vector dimensions doesn't match!");
|
|
150
|
+
|
|
151
|
+
py::array_t<label_t> labels_py({vectors_count, wanted});
|
|
152
|
+
py::array_t<distance_t> distances_py({vectors_count, wanted});
|
|
153
|
+
py::array_t<Py_ssize_t> counts_py(vectors_count);
|
|
154
|
+
auto labels_py2d = labels_py.mutable_unchecked<2>();
|
|
155
|
+
auto distances_py2d = distances_py.mutable_unchecked<2>();
|
|
156
|
+
auto counts_py1d = counts_py.mutable_unchecked<1>();
|
|
157
|
+
|
|
158
|
+
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
159
|
+
|
|
160
|
+
// https://docs.python.org/3/library/struct.html#format-characters
|
|
161
|
+
if (vectors_info.format == "e")
|
|
162
|
+
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
163
|
+
f16_converted_t const* vector = (f16_converted_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
164
|
+
counts_py1d(task_idx) = static_cast<Py_ssize_t>(
|
|
165
|
+
index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), thread_idx));
|
|
166
|
+
});
|
|
167
|
+
else if (vectors_info.format == "f")
|
|
168
|
+
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
169
|
+
float const* vector = (float const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
170
|
+
counts_py1d(task_idx) = static_cast<Py_ssize_t>(
|
|
171
|
+
index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), thread_idx));
|
|
172
|
+
});
|
|
173
|
+
else if (vectors_info.format == "d")
|
|
174
|
+
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
175
|
+
double const* vector = (double const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
176
|
+
counts_py1d(task_idx) = static_cast<Py_ssize_t>(
|
|
177
|
+
index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0), thread_idx));
|
|
178
|
+
});
|
|
179
|
+
else
|
|
180
|
+
throw std::runtime_error("Incompatible scalars in the query matrix!");
|
|
181
|
+
|
|
182
|
+
py::tuple results(3);
|
|
183
|
+
results[0] = labels_py;
|
|
184
|
+
results[1] = distances_py;
|
|
185
|
+
results[2] = counts_py;
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static void save_index(native_index_t const& index, std::string const& path) { index.save(path.c_str()); }
|
|
190
|
+
static void load_index(native_index_t& index, std::string const& path) { index.load(path.c_str()); }
|
|
191
|
+
static void view_index(native_index_t& index, std::string const& path) { index.view(path.c_str()); }
|
|
192
|
+
|
|
193
|
+
PYBIND11_MODULE(usearch, m) {
|
|
194
|
+
m.doc() = "Unum USearch Python bindings";
|
|
195
|
+
|
|
196
|
+
auto i = py::class_<native_index_t>(m, "Index");
|
|
197
|
+
|
|
198
|
+
i.def(py::init(&make_index), //
|
|
199
|
+
py::kw_only(), //
|
|
200
|
+
py::arg("dim"), //
|
|
201
|
+
py::arg("capacity") = 0, //
|
|
202
|
+
py::arg("dtype") = std::string("f32"), //
|
|
203
|
+
py::arg("metric") = std::string("ip"), //
|
|
204
|
+
py::arg("connectivity") = config_t::connectivity_default_k, //
|
|
205
|
+
py::arg("expansion_add") = config_t::expansion_construction_default_k, //
|
|
206
|
+
py::arg("expansion_search") = config_t::expansion_search_default_k //
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
i.def( //
|
|
210
|
+
"add", &add_to_index, //
|
|
211
|
+
py::arg("labels"), //
|
|
212
|
+
py::arg("vectors"), //
|
|
213
|
+
py::kw_only(), //
|
|
214
|
+
py::arg("copy") = true //
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
i.def( //
|
|
218
|
+
"search", &search_in_index, //
|
|
219
|
+
py::arg("vectors"), //
|
|
220
|
+
py::arg("count") = 10 //
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
i.def("save", &save_index, py::arg("path"));
|
|
224
|
+
i.def("load", &load_index, py::arg("path"));
|
|
225
|
+
i.def("view", &view_index, py::arg("path"));
|
|
226
|
+
}
|
package/src/rust.cpp
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#include "rust.h"
|
|
2
|
+
|
|
3
|
+
#include "advanced.hpp"
|
|
4
|
+
|
|
5
|
+
using namespace unum::usearch;
|
|
6
|
+
using namespace unum;
|
|
7
|
+
|
|
8
|
+
class Index::impl {
|
|
9
|
+
friend Index;
|
|
10
|
+
downcasting_index_t native_;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
Index::Index() : impl(new class Index::impl) {}
|
|
14
|
+
|
|
15
|
+
void Index::add(uint64_t label, rust::Slice<float const> vector) const {}
|
|
16
|
+
Results Index::search(rust::Slice<float const> vector, uint64_t count) const { return {}; }
|
|
17
|
+
|
|
18
|
+
uint64_t Index::dim() const { return impl->native_.dim(); }
|
|
19
|
+
uint64_t Index::connectivity() const { return impl->native_.connectivity(); }
|
|
20
|
+
uint64_t Index::size() const { return impl->native_.size(); }
|
|
21
|
+
uint64_t Index::capacity() const { return impl->native_.capacity(); }
|
|
22
|
+
|
|
23
|
+
std::unique_ptr<Index> new_index() { return std::make_unique<Index>(); }
|
package/src/rust.h
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include "rust/cxx.h"
|
|
3
|
+
|
|
4
|
+
struct Results;
|
|
5
|
+
|
|
6
|
+
class Index {
|
|
7
|
+
public:
|
|
8
|
+
Index();
|
|
9
|
+
void add(uint64_t label, rust::Slice<float const> vector) const;
|
|
10
|
+
Results search(rust::Slice<float const> vector, uint64_t count) const;
|
|
11
|
+
|
|
12
|
+
uint64_t dim() const;
|
|
13
|
+
uint64_t connectivity() const;
|
|
14
|
+
uint64_t size() const;
|
|
15
|
+
uint64_t capacity() const;
|
|
16
|
+
|
|
17
|
+
private:
|
|
18
|
+
class impl;
|
|
19
|
+
std::shared_ptr<impl> impl;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
std::unique_ptr<Index> new_index();
|
package/src/test.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
var assert = require('assert');
|
|
2
|
+
var usearch = require('bindings')('usearch');
|
|
3
|
+
|
|
4
|
+
var index = new usearch.Index({ metric: 'l2_sq', connectivity: 16, dimensions: 2 })
|
|
5
|
+
assert.equal(index.connectivity(), 16)
|
|
6
|
+
assert.equal(index.dimensions(), 2)
|
|
7
|
+
assert.equal(index.size(), 0)
|
|
8
|
+
|
|
9
|
+
index.add(15, new Float32Array([10, 20]))
|
|
10
|
+
index.add(16, new Float32Array([10, 25]))
|
|
11
|
+
assert.equal(index.size(), 2)
|
|
12
|
+
|
|
13
|
+
var results = index.search(new Float32Array([13, 14]), 2)
|
|
14
|
+
assert.deepEqual(results.labels, new Uint32Array([15, 16]))
|
|
15
|
+
assert.deepEqual(results.distances, new Float32Array([45, 130]))
|
|
16
|
+
|
|
17
|
+
console.log('JavaScript tests passed!')
|
package/src/test.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
import usearch
|
|
4
|
+
|
|
5
|
+
count_vectors = 100
|
|
6
|
+
count_dimensions = 96
|
|
7
|
+
|
|
8
|
+
index = usearch.Index(dim=96)
|
|
9
|
+
print(index)
|
|
10
|
+
|
|
11
|
+
vectors = np.random.uniform(
|
|
12
|
+
0, 0.3, (count_vectors, count_dimensions)).astype(np.float32)
|
|
13
|
+
labels = np.array(range(count_vectors), dtype=np.longlong)
|
|
14
|
+
print('will insert', labels.shape, vectors.shape)
|
|
15
|
+
|
|
16
|
+
index.add(labels, vectors, copy=True)
|
|
17
|
+
|
|
18
|
+
print('will search')
|
|
19
|
+
results = index.search(vectors, 10)
|
|
20
|
+
print('found', results[0].shape, results[1].shape, results[2].shape)
|