usearch 1.2.2 → 1.3.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 +26 -0
- package/.github/workflows/release.yml +1 -1
- package/CMakeLists.txt +1 -0
- package/Cargo.toml +1 -1
- package/README.md +16 -10
- package/c/lib.cpp +35 -1
- package/c/test.c +117 -114
- package/c/usearch.h +28 -1
- package/conanfile.py +1 -1
- package/cpp/test.cpp +2 -2
- package/docs/benchmarks.md +6 -6
- package/docs/compilation.md +1 -1
- package/include/usearch/index.hpp +172 -52
- package/include/usearch/index_dense.hpp +194 -57
- package/include/usearch/index_plugins.hpp +34 -20
- package/package.json +2 -2
- package/python/lib.cpp +93 -31
- package/python/scripts/test_index.py +38 -10
- package/python/usearch/index.py +87 -37
- package/rust/README.md +2 -2
- package/rust/lib.cpp +15 -4
- package/rust/lib.hpp +5 -0
- package/rust/lib.rs +75 -43
- package/wasmer.toml +1 -1
package/python/lib.cpp
CHANGED
|
@@ -759,17 +759,86 @@ static void compact_index(dense_index_py_t& index, std::size_t threads) {
|
|
|
759
759
|
index.compact(executor_default_t{threads});
|
|
760
760
|
}
|
|
761
761
|
|
|
762
|
+
static py::dict index_metadata(index_dense_metadata_result_t const& meta) {
|
|
763
|
+
py::dict result;
|
|
764
|
+
result["matrix_included"] = !meta.config.exclude_vectors;
|
|
765
|
+
result["matrix_uses_64_bit_dimensions"] = meta.config.use_64_bit_dimensions;
|
|
766
|
+
|
|
767
|
+
index_dense_head_t const& head = meta.head;
|
|
768
|
+
result["version"] = std::to_string(head.version_major) + "." + //
|
|
769
|
+
std::to_string(head.version_minor) + "." + //
|
|
770
|
+
std::to_string(head.version_patch);
|
|
771
|
+
|
|
772
|
+
result["kind_metric"] = metric_kind_t(head.kind_metric);
|
|
773
|
+
result["kind_scalar"] = scalar_kind_t(head.kind_scalar);
|
|
774
|
+
result["kind_key"] = scalar_kind_t(head.kind_key);
|
|
775
|
+
result["kind_compressed_slot"] = scalar_kind_t(head.kind_compressed_slot);
|
|
776
|
+
|
|
777
|
+
result["count_present"] = std::uint64_t(head.count_present);
|
|
778
|
+
result["count_deleted"] = std::uint64_t(head.count_deleted);
|
|
779
|
+
result["dimensions"] = std::uint64_t(head.dimensions);
|
|
780
|
+
|
|
781
|
+
return result;
|
|
782
|
+
}
|
|
783
|
+
|
|
762
784
|
// clang-format off
|
|
763
|
-
template <typename index_at> void
|
|
764
|
-
template <typename index_at> void
|
|
765
|
-
template <typename index_at> void
|
|
785
|
+
template <typename index_at> void save_index_to_path(index_at const& index, std::string const& path) { index.save(path.c_str()).error.raise(); }
|
|
786
|
+
template <typename index_at> void load_index_from_path(index_at& index, std::string const& path) { index.load(path.c_str()).error.raise(); }
|
|
787
|
+
template <typename index_at> void view_index_from_path(index_at& index, std::string const& path) { index.view(path.c_str()).error.raise(); }
|
|
766
788
|
template <typename index_at> void reset_index(index_at& index) { index.reset(); }
|
|
767
789
|
template <typename index_at> void clear_index(index_at& index) { index.clear(); }
|
|
768
790
|
template <typename index_at> std::size_t max_level(index_at const &index) { return index.max_level(); }
|
|
791
|
+
template <typename index_at> std::size_t serialized_length(index_at const &index) { return index.serialized_length(); }
|
|
769
792
|
template <typename index_at> typename index_at::stats_t compute_stats(index_at const &index) { return index.stats(); }
|
|
770
793
|
template <typename index_at> typename index_at::stats_t compute_level_stats(index_at const &index, std::size_t level) { return index.stats(level); }
|
|
771
794
|
// clang-format on
|
|
772
795
|
|
|
796
|
+
template <typename py_bytes_at> memory_mapped_file_t memory_map_from_bytes(py_bytes_at&& bytes) {
|
|
797
|
+
py::buffer_info info(py::buffer(bytes).request());
|
|
798
|
+
return {(byte_t*)(info.ptr), static_cast<std::size_t>(info.size)};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
template <typename index_at> py::object save_index_to_buffer(index_at const& index) {
|
|
802
|
+
std::size_t serialized_length = index.serialized_length();
|
|
803
|
+
|
|
804
|
+
// Create an empty bytearray object using CPython API
|
|
805
|
+
PyObject* byte_array = PyByteArray_FromStringAndSize(nullptr, 0);
|
|
806
|
+
if (!byte_array)
|
|
807
|
+
throw std::runtime_error("Could not allocate bytearray object");
|
|
808
|
+
|
|
809
|
+
// Resize the bytearray object to the desired length
|
|
810
|
+
if (PyByteArray_Resize(byte_array, static_cast<Py_ssize_t>(serialized_length)) != 0) {
|
|
811
|
+
Py_XDECREF(byte_array);
|
|
812
|
+
throw std::runtime_error("Could not resize bytearray object");
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
char* buffer = PyByteArray_AS_STRING(byte_array);
|
|
816
|
+
memory_mapped_file_t memory_map((byte_t*)buffer, serialized_length);
|
|
817
|
+
serialization_result_t result = index.save(std::move(memory_map));
|
|
818
|
+
|
|
819
|
+
if (!result) {
|
|
820
|
+
Py_XDECREF(byte_array);
|
|
821
|
+
result.error.raise();
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return py::reinterpret_steal<py::object>(byte_array);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
template <typename index_at> void load_index_from_buffer(index_at& index, py::bytes const& buffer) {
|
|
828
|
+
index.load(memory_map_from_bytes(buffer)).error.raise();
|
|
829
|
+
}
|
|
830
|
+
template <typename index_at> void view_index_from_buffer(index_at& index, py::bytes const& buffer) {
|
|
831
|
+
index.view(memory_map_from_bytes(buffer)).error.raise();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
template <typename index_at> std::vector<typename index_at::stats_t> compute_levels_stats(index_at const& index) {
|
|
835
|
+
using stats_t = typename index_at::stats_t;
|
|
836
|
+
std::size_t max_level = index.max_level();
|
|
837
|
+
std::vector<stats_t> result(max_level + 1);
|
|
838
|
+
index.stats(result.data(), max_level);
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
|
|
773
842
|
template <typename internal_at, typename external_at = internal_at, typename index_at = void>
|
|
774
843
|
static py::tuple get_typed_vectors_for_keys(index_at const& index, py::buffer keys) {
|
|
775
844
|
|
|
@@ -865,30 +934,16 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
865
934
|
.value("I16", scalar_kind_t::i16_k)
|
|
866
935
|
.value("I8", scalar_kind_t::i8_k);
|
|
867
936
|
|
|
868
|
-
m.def("
|
|
869
|
-
index_dense_metadata_result_t meta =
|
|
937
|
+
m.def("index_dense_metadata_from_path", [](std::string const& path) -> py::dict {
|
|
938
|
+
index_dense_metadata_result_t meta = index_dense_metadata_from_path(path.c_str());
|
|
870
939
|
forward_error(meta);
|
|
940
|
+
return index_metadata(meta);
|
|
941
|
+
});
|
|
871
942
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
result["matrix_uses_64_bit_dimensions"] = meta.config.use_64_bit_dimensions;
|
|
877
|
-
|
|
878
|
-
result["version"] = std::to_string(head.version_major) + "." + //
|
|
879
|
-
std::to_string(head.version_minor) + "." + //
|
|
880
|
-
std::to_string(head.version_patch);
|
|
881
|
-
|
|
882
|
-
result["kind_metric"] = metric_kind_t(head.kind_metric);
|
|
883
|
-
result["kind_scalar"] = scalar_kind_t(head.kind_scalar);
|
|
884
|
-
result["kind_key"] = scalar_kind_t(head.kind_key);
|
|
885
|
-
result["kind_compressed_slot"] = scalar_kind_t(head.kind_compressed_slot);
|
|
886
|
-
|
|
887
|
-
result["count_present"] = std::uint64_t(head.count_present);
|
|
888
|
-
result["count_deleted"] = std::uint64_t(head.count_deleted);
|
|
889
|
-
result["dimensions"] = std::uint64_t(head.dimensions);
|
|
890
|
-
|
|
891
|
-
return result;
|
|
943
|
+
m.def("index_dense_metadata_from_buffer", [](py::bytes const& buffer) -> py::dict {
|
|
944
|
+
index_dense_metadata_result_t meta = index_dense_metadata_from_buffer(memory_map_from_bytes(buffer));
|
|
945
|
+
forward_error(meta);
|
|
946
|
+
return index_metadata(meta);
|
|
892
947
|
});
|
|
893
948
|
|
|
894
949
|
m.def("exact_search", &search_many_brute_force, //
|
|
@@ -1034,8 +1089,9 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
1034
1089
|
[](dense_index_py_t const& index) -> std::size_t { return index.metric().dimensions(); });
|
|
1035
1090
|
i.def_property_readonly( //
|
|
1036
1091
|
"dtype", [](dense_index_py_t const& index) -> scalar_kind_t { return index.scalar_kind(); });
|
|
1037
|
-
|
|
1038
|
-
|
|
1092
|
+
|
|
1093
|
+
i.def_property_readonly("serialized_length", &dense_index_py_t::serialized_length);
|
|
1094
|
+
i.def_property_readonly("memory_usage", &dense_index_py_t::memory_usage);
|
|
1039
1095
|
|
|
1040
1096
|
i.def_property("expansion_add", &dense_index_py_t::expansion_add, &dense_index_py_t::change_expansion_add);
|
|
1041
1097
|
i.def_property("expansion_search", &dense_index_py_t::expansion_search, &dense_index_py_t::change_expansion_search);
|
|
@@ -1138,9 +1194,14 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
1138
1194
|
},
|
|
1139
1195
|
py::arg("offset"));
|
|
1140
1196
|
|
|
1141
|
-
i.def("
|
|
1142
|
-
i.def("
|
|
1143
|
-
i.def("
|
|
1197
|
+
i.def("save_index_to_path", &save_index_to_path<dense_index_py_t>);
|
|
1198
|
+
i.def("load_index_from_path", &load_index_from_path<dense_index_py_t>);
|
|
1199
|
+
i.def("view_index_from_path", &view_index_from_path<dense_index_py_t>);
|
|
1200
|
+
|
|
1201
|
+
i.def("save_index_to_buffer", &save_index_to_buffer<dense_index_py_t>);
|
|
1202
|
+
i.def("load_index_from_buffer", &load_index_from_buffer<dense_index_py_t>);
|
|
1203
|
+
i.def("view_index_from_buffer", &view_index_from_buffer<dense_index_py_t>);
|
|
1204
|
+
|
|
1144
1205
|
i.def("reset", &reset_index<dense_index_py_t>);
|
|
1145
1206
|
i.def("clear", &clear_index<dense_index_py_t>);
|
|
1146
1207
|
i.def("copy", ©_index, py::kw_only(), py::arg("copy") = true);
|
|
@@ -1155,7 +1216,8 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
1155
1216
|
i_stats.def_readonly("allocated_bytes", &punned_index_stats_t::allocated_bytes);
|
|
1156
1217
|
|
|
1157
1218
|
i.def_property_readonly("max_level", &max_level<dense_index_py_t>);
|
|
1158
|
-
i.def_property_readonly("
|
|
1219
|
+
i.def_property_readonly("stats", &compute_stats<dense_index_py_t>);
|
|
1220
|
+
i.def_property_readonly("levels_stats", &compute_levels_stats<dense_index_py_t>);
|
|
1159
1221
|
i.def("level_stats", &compute_level_stats<dense_index_py_t>, py::arg("level"));
|
|
1160
1222
|
|
|
1161
1223
|
auto is = py::class_<dense_indexes_py_t>(m, "Indexes");
|
|
@@ -126,35 +126,63 @@ def test_index_stats(batch_size):
|
|
|
126
126
|
index.add(keys, vectors, threads=threads)
|
|
127
127
|
|
|
128
128
|
assert index.max_level >= 0
|
|
129
|
-
assert index.
|
|
129
|
+
assert index.stats.nodes >= batch_size
|
|
130
|
+
assert index.levels_stats[0].nodes == batch_size
|
|
130
131
|
assert index.level_stats(0).nodes == batch_size
|
|
131
132
|
|
|
133
|
+
assert index.levels_stats[index.max_level].nodes > 0
|
|
134
|
+
|
|
132
135
|
|
|
133
136
|
@pytest.mark.parametrize("ndim", [1, 3, 8, 32, 256, 4096])
|
|
134
|
-
@pytest.mark.parametrize("batch_size", [1, 7, 1024])
|
|
137
|
+
@pytest.mark.parametrize("batch_size", [0, 1, 7, 1024])
|
|
135
138
|
@pytest.mark.parametrize("quantization", [ScalarKind.F32, ScalarKind.I8])
|
|
136
139
|
def test_index_save_load_restore_copy(ndim, quantization, batch_size):
|
|
137
140
|
index = Index(ndim=ndim, dtype=quantization, multi=False)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
|
|
142
|
+
if batch_size > 0:
|
|
143
|
+
keys = np.arange(batch_size)
|
|
144
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
145
|
+
index.add(keys, vectors, threads=threads)
|
|
141
146
|
|
|
142
147
|
index.save("tmp.usearch")
|
|
143
148
|
index.clear()
|
|
144
149
|
assert len(index) == 0
|
|
150
|
+
assert os.path.exists("tmp.usearch")
|
|
145
151
|
|
|
146
152
|
index.load("tmp.usearch")
|
|
147
153
|
assert len(index) == batch_size
|
|
148
|
-
|
|
154
|
+
if batch_size > 0:
|
|
155
|
+
assert len(index[0].flatten()) == ndim
|
|
149
156
|
|
|
150
|
-
|
|
157
|
+
index_meta = Index.metadata("tmp.usearch")
|
|
158
|
+
assert index_meta is not None
|
|
159
|
+
|
|
160
|
+
index = Index.restore("tmp.usearch", view=False)
|
|
151
161
|
assert len(index) == batch_size
|
|
152
|
-
|
|
162
|
+
if batch_size > 0:
|
|
163
|
+
assert len(index[0].flatten()) == ndim
|
|
153
164
|
|
|
154
165
|
copied_index = index.copy()
|
|
155
166
|
assert len(copied_index) == len(index)
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
if batch_size > 0:
|
|
168
|
+
assert np.allclose(
|
|
169
|
+
np.vstack(copied_index.get(keys)), np.vstack(index.get(keys))
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Perform the same operations in RAM, without touching the filesystem
|
|
173
|
+
serialized_index = index.save()
|
|
174
|
+
deserialized_metadata = Index.metadata(serialized_index)
|
|
175
|
+
assert deserialized_metadata is not None
|
|
176
|
+
|
|
177
|
+
deserialized_index = Index.restore(serialized_index)
|
|
178
|
+
assert len(deserialized_index) == len(index)
|
|
179
|
+
assert set(np.array(deserialized_index.keys)) == set(np.array(index.keys))
|
|
180
|
+
if batch_size > 0:
|
|
181
|
+
assert np.allclose(
|
|
182
|
+
np.vstack(deserialized_index.get(keys)), np.vstack(index.get(keys))
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
deserialized_index.reset()
|
|
158
186
|
index.reset()
|
|
159
187
|
os.remove("tmp.usearch")
|
|
160
188
|
|
package/python/usearch/index.py
CHANGED
|
@@ -21,14 +21,21 @@ from typing import (
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
from tqdm import tqdm
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
from usearch.compiled import
|
|
26
|
-
|
|
24
|
+
# Precompiled symbols that won't be exposed directly:
|
|
25
|
+
from usearch.compiled import (
|
|
26
|
+
Index as _CompiledIndex,
|
|
27
|
+
Indexes as _CompiledIndexes,
|
|
28
|
+
IndexStats as _CompiledIndexStats,
|
|
29
|
+
index_dense_metadata_from_path as _index_dense_metadata_from_path,
|
|
30
|
+
index_dense_metadata_from_buffer as _index_dense_metadata_from_buffer,
|
|
31
|
+
exact_search as _exact_search,
|
|
32
|
+
)
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
from usearch.compiled import exact_search as _exact_search
|
|
30
|
-
from usearch.compiled import MetricKind, ScalarKind, MetricSignature
|
|
34
|
+
# Precompiled symbols that will be exposed
|
|
31
35
|
from usearch.compiled import (
|
|
36
|
+
MetricKind,
|
|
37
|
+
ScalarKind,
|
|
38
|
+
MetricSignature,
|
|
32
39
|
DEFAULT_CONNECTIVITY,
|
|
33
40
|
DEFAULT_EXPANSION_ADD,
|
|
34
41
|
DEFAULT_EXPANSION_SEARCH,
|
|
@@ -60,6 +67,10 @@ DTypeLike = Union[str, ScalarKind]
|
|
|
60
67
|
|
|
61
68
|
MetricLike = Union[str, MetricKind, CompiledMetric]
|
|
62
69
|
|
|
70
|
+
NoneType = type(None)
|
|
71
|
+
|
|
72
|
+
PathOrBuffer = Union[str, os.PathLike, bytes]
|
|
73
|
+
|
|
63
74
|
|
|
64
75
|
def _normalize_dtype(dtype, metric: MetricKind = MetricKind.Cos) -> ScalarKind:
|
|
65
76
|
if dtype is None or dtype == "":
|
|
@@ -559,29 +570,38 @@ class Index:
|
|
|
559
570
|
self._compiled.load(path)
|
|
560
571
|
|
|
561
572
|
@staticmethod
|
|
562
|
-
def metadata(
|
|
563
|
-
path = os.fspath(path)
|
|
564
|
-
if not os.path.exists(path):
|
|
565
|
-
return None
|
|
573
|
+
def metadata(path_or_buffer: PathOrBuffer) -> Optional[dict]:
|
|
566
574
|
try:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
575
|
+
if isinstance(path_or_buffer, bytearray):
|
|
576
|
+
path_or_buffer = bytes(path_or_buffer)
|
|
577
|
+
if isinstance(path_or_buffer, bytes):
|
|
578
|
+
return _index_dense_metadata_from_buffer(path_or_buffer)
|
|
579
|
+
else:
|
|
580
|
+
path_or_buffer = os.fspath(path_or_buffer)
|
|
581
|
+
if not os.path.exists(path_or_buffer):
|
|
582
|
+
return None
|
|
583
|
+
return _index_dense_metadata_from_path(path_or_buffer)
|
|
584
|
+
except Exception as e:
|
|
585
|
+
raise e
|
|
570
586
|
|
|
571
587
|
@staticmethod
|
|
572
|
-
def restore(
|
|
573
|
-
|
|
574
|
-
meta = Index.metadata(path)
|
|
588
|
+
def restore(path_or_buffer: PathOrBuffer, view: bool = False) -> Optional[Index]:
|
|
589
|
+
meta = Index.metadata(path_or_buffer)
|
|
575
590
|
if not meta:
|
|
576
591
|
return None
|
|
577
|
-
|
|
592
|
+
|
|
593
|
+
index = Index(
|
|
578
594
|
ndim=meta["dimensions"],
|
|
579
595
|
dtype=meta["kind_scalar"],
|
|
580
596
|
metric=meta["kind_metric"],
|
|
581
|
-
path=path,
|
|
582
|
-
view=view,
|
|
583
597
|
)
|
|
584
598
|
|
|
599
|
+
if view:
|
|
600
|
+
index.view(path_or_buffer)
|
|
601
|
+
else:
|
|
602
|
+
index.load(path_or_buffer)
|
|
603
|
+
return index
|
|
604
|
+
|
|
585
605
|
def __len__(self) -> int:
|
|
586
606
|
return self._compiled.__len__()
|
|
587
607
|
|
|
@@ -837,6 +857,10 @@ class Index:
|
|
|
837
857
|
def ndim(self) -> int:
|
|
838
858
|
return self._compiled.ndim
|
|
839
859
|
|
|
860
|
+
@property
|
|
861
|
+
def serialized_length(self) -> int:
|
|
862
|
+
return self._compiled.serialized_length
|
|
863
|
+
|
|
840
864
|
@property
|
|
841
865
|
def metric(self) -> Union[MetricKind, CompiledMetric]:
|
|
842
866
|
return self._metric_jit if self._metric_jit else self._metric_kind
|
|
@@ -895,23 +919,34 @@ class Index:
|
|
|
895
919
|
def expansion_search(self, v: int):
|
|
896
920
|
self._compiled.expansion_search = v
|
|
897
921
|
|
|
898
|
-
def save(self,
|
|
899
|
-
|
|
900
|
-
if
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
922
|
+
def save(self, path_or_buffer: Union[str, os.PathLike, NoneType] = None) -> Optional[bytes]:
|
|
923
|
+
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
924
|
+
if path_or_buffer is None:
|
|
925
|
+
return self._compiled.save_index_to_buffer()
|
|
926
|
+
else:
|
|
927
|
+
self._compiled.save_index_to_path(os.fspath(path_or_buffer))
|
|
928
|
+
|
|
929
|
+
def load(self, path_or_buffer: Union[str, os.PathLike, bytes, NoneType] = None):
|
|
930
|
+
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
931
|
+
if path_or_buffer is None:
|
|
932
|
+
raise Exception("Define the source")
|
|
933
|
+
if isinstance(path_or_buffer, bytearray):
|
|
934
|
+
path_or_buffer = bytes(path_or_buffer)
|
|
935
|
+
if isinstance(path_or_buffer, bytes):
|
|
936
|
+
self._compiled.load_index_from_buffer(path_or_buffer)
|
|
937
|
+
else:
|
|
938
|
+
self._compiled.load_index_from_path(os.fspath(path_or_buffer))
|
|
939
|
+
|
|
940
|
+
def view(self, path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None):
|
|
941
|
+
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
942
|
+
if path_or_buffer is None:
|
|
943
|
+
raise Exception("Define the source")
|
|
944
|
+
if isinstance(path_or_buffer, bytearray):
|
|
945
|
+
path_or_buffer = bytes(path_or_buffer)
|
|
946
|
+
if isinstance(path_or_buffer, bytes):
|
|
947
|
+
self._compiled.view_index_from_buffer(path_or_buffer)
|
|
948
|
+
else:
|
|
949
|
+
self._compiled.view_index_from_path(os.fspath(path_or_buffer))
|
|
915
950
|
|
|
916
951
|
def clear(self):
|
|
917
952
|
"""Erases all the vectors from the index, preserving the space for future insertions."""
|
|
@@ -1049,12 +1084,27 @@ class Index:
|
|
|
1049
1084
|
return self._compiled.max_level + 1
|
|
1050
1085
|
|
|
1051
1086
|
@property
|
|
1052
|
-
def
|
|
1087
|
+
def stats(self) -> _CompiledIndexStats:
|
|
1053
1088
|
"""Get the accumulated statistics for the entire multi-level graph.
|
|
1054
1089
|
|
|
1055
1090
|
:return: Statistics for the entire multi-level graph.
|
|
1056
1091
|
:rtype: _CompiledIndexStats
|
|
1057
1092
|
|
|
1093
|
+
Statistics:
|
|
1094
|
+
- ``nodes`` (int): The number of nodes in that level.
|
|
1095
|
+
- ``edges`` (int): The number of edges in that level.
|
|
1096
|
+
- ``max_edges`` (int): The maximum possible number of edges in that level.
|
|
1097
|
+
- ``allocated_bytes`` (int): The amount of allocated memory for that level.
|
|
1098
|
+
"""
|
|
1099
|
+
return self._compiled.stats
|
|
1100
|
+
|
|
1101
|
+
@property
|
|
1102
|
+
def levels_stats(self) -> List[_CompiledIndexStats]:
|
|
1103
|
+
"""Get the accumulated statistics for every level graph.
|
|
1104
|
+
|
|
1105
|
+
:return: Statistics for every level graph.
|
|
1106
|
+
:rtype: List[_CompiledIndexStats]
|
|
1107
|
+
|
|
1058
1108
|
Statistics:
|
|
1059
1109
|
- ``nodes`` (int): The number of nodes in that level.
|
|
1060
1110
|
- ``edges`` (int): The number of edges in that level.
|
package/rust/README.md
CHANGED
|
@@ -11,7 +11,7 @@ cargo add usearch
|
|
|
11
11
|
```rust
|
|
12
12
|
|
|
13
13
|
let options = IndexOptions {
|
|
14
|
-
dimensions:
|
|
14
|
+
dimensions: 3,
|
|
15
15
|
metric: MetricKind::IP,
|
|
16
16
|
quantization: ScalarKind::F16,
|
|
17
17
|
connectivity: 0,
|
|
@@ -36,7 +36,7 @@ assert_eq!(index.size(), 2);
|
|
|
36
36
|
|
|
37
37
|
// Read back the tags
|
|
38
38
|
let results = index.search(&first, 10).unwrap();
|
|
39
|
-
assert_eq!(results.
|
|
39
|
+
assert_eq!(results.keys.len(), 2);
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
## Multi-Threading
|
package/rust/lib.cpp
CHANGED
|
@@ -47,12 +47,23 @@ size_t Index::dimensions() const { return index_->dimensions(); }
|
|
|
47
47
|
size_t Index::connectivity() const { return index_->connectivity(); }
|
|
48
48
|
size_t Index::size() const { return index_->size(); }
|
|
49
49
|
size_t Index::capacity() const { return index_->capacity(); }
|
|
50
|
+
size_t Index::serialized_length() const { return index_->serialized_length(); }
|
|
50
51
|
|
|
51
|
-
void Index::save(rust::Str path) const { index_->save(std::string(path).c_str()); }
|
|
52
|
-
void Index::load(rust::Str path) const { index_->load(std::string(path).c_str()); }
|
|
53
|
-
void Index::view(rust::Str path) const { index_->view(std::string(path).c_str()); }
|
|
52
|
+
void Index::save(rust::Str path) const { index_->save(output_file_t(std::string(path).c_str())).error.raise(); }
|
|
53
|
+
void Index::load(rust::Str path) const { index_->load(input_file_t(std::string(path).c_str())).error.raise(); }
|
|
54
|
+
void Index::view(rust::Str path) const { index_->view(memory_mapped_file_t(std::string(path).c_str())).error.raise(); }
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
void Index::save_to_buffer(rust::Slice<uint8_t> buffer) const {
|
|
57
|
+
index_->save(memory_mapped_file_t((byte_t*)buffer.data(), buffer.size())).error.raise();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
void Index::load_from_buffer(rust::Slice<uint8_t const> buffer) const {
|
|
61
|
+
index_->load(memory_mapped_file_t((byte_t*)buffer.data(), buffer.size())).error.raise();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
void Index::view_from_buffer(rust::Slice<uint8_t const> buffer) const {
|
|
65
|
+
index_->view(memory_mapped_file_t((byte_t*)buffer.data(), buffer.size())).error.raise();
|
|
66
|
+
}
|
|
56
67
|
|
|
57
68
|
std::unique_ptr<Index> wrap(index_t&& index) {
|
|
58
69
|
std::unique_ptr<index_t> punned_ptr;
|
package/rust/lib.hpp
CHANGED
|
@@ -32,11 +32,16 @@ class Index {
|
|
|
32
32
|
size_t connectivity() const;
|
|
33
33
|
size_t size() const;
|
|
34
34
|
size_t capacity() const;
|
|
35
|
+
size_t serialized_length() const;
|
|
35
36
|
|
|
36
37
|
void save(rust::Str path) const;
|
|
37
38
|
void load(rust::Str path) const;
|
|
38
39
|
void view(rust::Str path) const;
|
|
39
40
|
|
|
41
|
+
void save_to_buffer(rust::Slice<uint8_t> buffer) const;
|
|
42
|
+
void load_from_buffer(rust::Slice<uint8_t const> buffer) const;
|
|
43
|
+
void view_from_buffer(rust::Slice<uint8_t const> buffer) const;
|
|
44
|
+
|
|
40
45
|
private:
|
|
41
46
|
std::unique_ptr<index_dense_t> index_;
|
|
42
47
|
};
|