usearch 0.9.4 → 0.9.5

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.
@@ -0,0 +1,105 @@
1
+ name: Pre-Release
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ branches: ["main-dev"]
7
+
8
+ env:
9
+ BUILD_TYPE: Release
10
+ GH_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
11
+ PYTHONUTF8: 1
12
+
13
+ # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+
19
+ test_python:
20
+ name: Test Python
21
+ runs-on: ${{ matrix.os }}
22
+ strategy:
23
+ matrix:
24
+ os: [ubuntu-20.04, macOS-11, windows-2022]
25
+ python-version: ["3.11"]
26
+
27
+ steps:
28
+ - uses: actions/checkout@v3
29
+ - run: git submodule update --init --recursive
30
+ - name: Set up Python ${{ matrix.python-version }}
31
+ uses: actions/setup-python@v4
32
+ with:
33
+ python-version: ${{ matrix.python-version }}
34
+ - name: Install dependencies
35
+ run: |
36
+ python -m pip install --upgrade pip
37
+ pip install pytest numpy
38
+ - name: Build locally
39
+ run: python -m pip install .
40
+ - name: Test with PyTest on NIX
41
+ if: matrix.os != 'windows-2022'
42
+ run: pytest python/scripts/test.py
43
+
44
+ test_javascript:
45
+ name: Test JavaScript
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: actions/checkout@v3
49
+ - run: git submodule update --init --recursive
50
+ - uses: actions/setup-node@v3
51
+ with:
52
+ node-version: 18
53
+ - run: npm install
54
+ - run: npm ci
55
+ - run: npm test
56
+
57
+ test_rust:
58
+ name: Test Rust
59
+ runs-on: ubuntu-latest
60
+ steps:
61
+ - uses: actions/checkout@v3
62
+ - run: git submodule update --init --recursive
63
+ - uses: actions-rs/toolchain@v1
64
+ with:
65
+ toolchain: stable
66
+ override: true
67
+
68
+ test_java:
69
+ name: Test Java
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - uses: actions/checkout@v3
73
+ - run: git submodule update --init --recursive
74
+ - uses: actions/setup-java@v3
75
+ with:
76
+ java-version: '11'
77
+ distribution: 'adopt'
78
+ - name: Setup Gradle
79
+ uses: gradle/gradle-build-action@v2.4.2
80
+ - name: Execute Gradle build
81
+ run: gradle clean build
82
+
83
+ test_swift:
84
+ name: Test ObjC & Swift
85
+ runs-on: macos-latest
86
+ steps:
87
+ - uses: actions/checkout@v3
88
+ - run: git submodule update --init --recursive
89
+ - name: Build
90
+ run: swift build
91
+ - name: Run tests
92
+ run: swift test
93
+
94
+ build_docker:
95
+ name: Test Docker Image
96
+ runs-on: ubuntu-latest
97
+ steps:
98
+ - uses: actions/checkout@v3
99
+ - run: git submodule update --init --recursive
100
+ - name: Build
101
+ uses: docker/build-push-action@v4
102
+ with:
103
+ context: .
104
+ load: true
105
+ push: false
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "0.9.4"
3
+ version = "0.9.5"
4
4
  authors = ["Ashot Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.4
1
+ 0.9.5
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 = '0.9.4'
7
+ version = '0.9.5'
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'
@@ -35,7 +35,13 @@ cmake -B ./build_release \
35
35
  -DUSEARCH_BUILD_BENCHMARK=1 \
36
36
  -DUSEARCH_BUILD_TEST=1 \
37
37
  && \
38
- make -C ./build_release -j
38
+ make -C ./build_release -j
39
+ ```
40
+
41
+ Linting:
42
+
43
+ ```sh
44
+ cppcheck --enable=all --suppress=cstyleCast --suppress=unusedFunction include/usearch/usearch.hpp src/punned.hpp
39
45
  ```
40
46
 
41
47
  Benchmarking:
@@ -61,6 +67,13 @@ pip install -e .
61
67
  pytest python/scripts/test.py
62
68
  ```
63
69
 
70
+ Linting:
71
+
72
+ ```sh
73
+ pip install ruff
74
+ ruff --format=github --select=E9,F63,F7,F82 --target-version=py37 python
75
+ ```
76
+
64
77
  Benchmarking:
65
78
 
66
79
  ```sh
@@ -103,7 +103,7 @@ using f64_t = double;
103
103
  using f32_t = float;
104
104
  using byte_t = char;
105
105
 
106
- template <typename at> at angle_to_radians(at angle) noexcept { return angle * M_PI / 180.f; }
106
+ template <typename at> at angle_to_radians(at angle) noexcept { return angle * at(3.14159265358979323846) / at(180); }
107
107
 
108
108
  template <typename at> at square(at value) noexcept { return value * value; }
109
109
 
@@ -409,9 +409,9 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
409
409
  static_assert(sizeof(byte_t) == 1, "Allocator must allocate separate addressable bytes");
410
410
 
411
411
  #if defined(USEARCH_IS_WINDOWS)
412
- using slot_t = LONG volatile;
412
+ using slot_t = LONGLONG;
413
413
  #else
414
- using slot_t = std::uint64_t;
414
+ using slot_t = unsigned long;
415
415
  #endif
416
416
 
417
417
  static constexpr std::size_t bits_per_slot() { return sizeof(slot_t) * CHAR_BIT; }
@@ -422,7 +422,7 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
422
422
 
423
423
  public:
424
424
  visits_bitset_gt() noexcept {}
425
- visits_bitset_gt(std::size_t capacity) noexcept {
425
+ explicit visits_bitset_gt(std::size_t capacity) noexcept {
426
426
  slots_count_ = divide_round_up<bits_per_slot()>(capacity);
427
427
  slots_ = (slot_t*)allocator_t{}.allocate(slots_count_ * sizeof(slot_t));
428
428
  clear();
@@ -449,28 +449,18 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
449
449
 
450
450
  #if defined(USEARCH_IS_WINDOWS)
451
451
 
452
- inline bool atomic_test(std::size_t i) noexcept {
453
- slot_t mask{1ul << (i & bits_mask())};
454
- return InterlockedOr(&slots_[i / bits_per_slot()], 0) & mask;
455
- }
456
-
457
452
  inline bool atomic_set(std::size_t i) noexcept {
458
453
  slot_t mask{1ul << (i & bits_mask())};
459
- return InterLockedOr(&slots_[i / bits_per_slot()], mask) & mask;
454
+ return InterlockedOr64Acquire((slot_t volatile*)&slots_[i / bits_per_slot()], mask) & mask;
460
455
  }
461
456
 
462
457
  inline void atomic_reset(std::size_t i) noexcept {
463
458
  slot_t mask{1ul << (i & bits_mask())};
464
- InterLockedAnd((LONG volatile*)&slots_[i / bits_per_slot()], ~mask);
459
+ InterlockedAnd64Release((slot_t volatile*)&slots_[i / bits_per_slot()], ~mask);
465
460
  }
466
461
 
467
462
  #else
468
463
 
469
- inline bool atomic_test(std::size_t i) noexcept {
470
- slot_t mask{1ul << (i & bits_mask())};
471
- return __atomic_load_n(&slots_[i / bits_per_slot()], __ATOMIC_RELAXED) & mask;
472
- }
473
-
474
464
  inline bool atomic_set(std::size_t i) noexcept {
475
465
  slot_t mask{1ul << (i & bits_mask())};
476
466
  return __atomic_fetch_or(&slots_[i / bits_per_slot()], mask, __ATOMIC_ACQUIRE) & mask;
@@ -781,7 +771,7 @@ template <typename scalar_at> class span_gt {
781
771
  std::size_t size_;
782
772
 
783
773
  public:
784
- span_gt(scalar_at* begin, scalar_at* end) noexcept : data_(begin), size_(end - begin) {}
774
+ span_gt(scalar_at* begin, scalar_at const* end) noexcept : data_(begin), size_(end - begin) {}
785
775
  span_gt(scalar_at* begin, std::size_t size) noexcept : data_(begin), size_(size) {}
786
776
  scalar_at* data() const noexcept { return data_; }
787
777
  std::size_t size() const noexcept { return size_; }
@@ -928,11 +918,12 @@ class index_gt {
928
918
 
929
919
  template <typename ref_at> class member_iterator_gt {
930
920
  using ref_t = ref_at;
931
- index_gt& index_;
932
- std::size_t offset_;
921
+ index_gt* index_{};
922
+ std::size_t offset_{};
933
923
 
924
+ friend class index_gt;
934
925
  member_iterator_gt() noexcept {}
935
- member_iterator_gt(index_gt& index, std::size_t offset) noexcept : index_(index), offset_(offset) {}
926
+ member_iterator_gt(index_gt* index, std::size_t offset) noexcept : index_(index), offset_(offset) {}
936
927
 
937
928
  public:
938
929
  using iterator_category = std::random_access_iterator_tag;
@@ -941,7 +932,7 @@ class index_gt {
941
932
  using pointer = void;
942
933
  using reference = ref_t;
943
934
 
944
- reference operator*() const noexcept { return index_.node_with_id_(offset_); }
935
+ reference operator*() const noexcept { return index_->node_with_id_(offset_).ref(); }
945
936
  member_iterator_gt operator++(int) noexcept { return member_iterator_gt(index_, offset_ + 1); }
946
937
  member_iterator_gt operator--(int) noexcept { return member_iterator_gt(index_, offset_ - 1); }
947
938
  member_iterator_gt operator+(difference_type d) noexcept { return member_iterator_gt(index_, offset_ + d); }
@@ -963,8 +954,11 @@ class index_gt {
963
954
  offset_ -= d;
964
955
  return *this;
965
956
  }
966
- bool operator!=(member_iterator_gt const& other) noexcept {
967
- return &index_ != &other.index_ || offset_ != other.offset_;
957
+ bool operator==(member_iterator_gt const& other) const noexcept {
958
+ return index_ == other.index_ && offset_ == other.offset_;
959
+ }
960
+ bool operator!=(member_iterator_gt const& other) const noexcept {
961
+ return index_ != other.index_ || offset_ != other.offset_;
968
962
  }
969
963
  };
970
964
 
@@ -1056,7 +1050,7 @@ class index_gt {
1056
1050
 
1057
1051
  operator vector_view_t() const noexcept { return {vector(), dim()}; }
1058
1052
  vector_view_t vector_view() const noexcept { return {vector(), dim()}; }
1059
- member_ref_t ref() noexcept { return {label(), vector_view()}; }
1053
+ member_ref_t ref() noexcept { return {{tape_}, vector_view()}; }
1060
1054
  member_cref_t cref() const noexcept { return {label(), vector_view()}; }
1061
1055
  };
1062
1056
 
@@ -1137,19 +1131,10 @@ class index_gt {
1137
1131
  config_t const& config() const { return config_; }
1138
1132
  bool is_immutable() const noexcept { return viewed_file_descriptor_ != 0; }
1139
1133
 
1140
- index_gt(config_t config = {}, metric_t metric = {}, allocator_t allocator = {}) noexcept(false)
1141
- : config_(normalize(config)), metric_(metric), allocator_(allocator) {
1142
-
1143
- // Externally defined hyper-parameters:
1144
- pre_ = precompute_(config);
1134
+ explicit index_gt(config_t config = {}, metric_t metric = {}, allocator_t allocator = {}) noexcept(false)
1135
+ : config_(normalize(config)), metric_(metric), allocator_(allocator), pre_(precompute_(config)),
1136
+ viewed_file_descriptor_(0), size_(0u), max_level_(-1), entry_id_(0u) {
1145
1137
 
1146
- // Configure initial empty state:
1147
- size_ = 0u;
1148
- max_level_ = -1;
1149
- entry_id_ = 0u;
1150
- viewed_file_descriptor_ = 0;
1151
-
1152
- // Dynamic memory:
1153
1138
  thread_contexts_.resize(config.max_threads());
1154
1139
  for (thread_context_t& context : thread_contexts_)
1155
1140
  context.metric = metric;
@@ -1159,7 +1144,7 @@ class index_gt {
1159
1144
  /**
1160
1145
  * @brief Clones the structure with the same hyper-parameters, but without contents.
1161
1146
  */
1162
- index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
1147
+ index_gt fork() noexcept(false) { return index_gt{config_, metric_, allocator_}; }
1163
1148
 
1164
1149
  ~index_gt() noexcept { clear(); }
1165
1150
 
@@ -1170,6 +1155,13 @@ class index_gt {
1170
1155
  return *this;
1171
1156
  }
1172
1157
 
1158
+ member_citerator_t cbegin() const noexcept { return {this, 0}; }
1159
+ member_citerator_t cend() const noexcept { return {this, size()}; }
1160
+ member_citerator_t begin() const noexcept { return {this, 0}; }
1161
+ member_citerator_t end() const noexcept { return {this, size()}; }
1162
+ member_iterator_t begin() noexcept { return {this, 0}; }
1163
+ member_iterator_t end() noexcept { return {this, size()}; }
1164
+
1173
1165
  #pragma region Adjusting Configuration
1174
1166
 
1175
1167
  /**
@@ -1200,12 +1192,12 @@ class index_gt {
1200
1192
  std::swap(thread_contexts_, other.thread_contexts_);
1201
1193
 
1202
1194
  // Non-atomic parts.
1203
- std::size_t capacity = capacity_;
1204
- std::size_t size = size_;
1195
+ std::size_t capacity_copy = capacity_;
1196
+ std::size_t size_copy = size_;
1205
1197
  capacity_ = other.capacity_.load();
1206
1198
  size_ = other.size_.load();
1207
- other.capacity_ = capacity;
1208
- other.size_ = size;
1199
+ other.capacity_ = capacity_copy;
1200
+ other.size_ = size_copy;
1209
1201
  }
1210
1202
 
1211
1203
  /**
@@ -1230,7 +1222,7 @@ class index_gt {
1230
1222
  precomputed_constants_t pre = precompute_(config);
1231
1223
  std::size_t bytes_per_node_base = node_head_bytes_() + pre.neighbors_base_bytes;
1232
1224
  std::size_t rounded_size = divide_round_up<64>(bytes_per_node_base) * 64;
1233
- std::size_t added_connections = (rounded_size - rounded_size) / sizeof(id_t);
1225
+ std::size_t added_connections = (rounded_size - bytes_per_node_base) / sizeof(id_t);
1234
1226
  config.connectivity = config.connectivity + added_connections / base_level_multiple_();
1235
1227
  return config;
1236
1228
  }
@@ -1309,11 +1301,11 @@ class index_gt {
1309
1301
 
1310
1302
  // Determining how much memory to allocate depends on the target level
1311
1303
  std::unique_lock<std::mutex> new_level_lock(global_mutex_);
1312
- level_t max_level = max_level_; // Copy under lock
1313
- id_t entry_id = entry_id_; // Copy under lock
1304
+ level_t max_level_copy = max_level_; // Copy under lock
1305
+ id_t entry_id_copy = entry_id_; // Copy under lock
1314
1306
  thread_context_t& context = thread_contexts_[config.thread];
1315
1307
  level_t target_level = choose_random_level_(context.level_generator);
1316
- if (target_level <= max_level)
1308
+ if (target_level <= max_level_copy)
1317
1309
  new_level_lock.unlock();
1318
1310
 
1319
1311
  // Allocate the neighbors
@@ -1333,10 +1325,10 @@ class index_gt {
1333
1325
  result.cycles = context.iteration_cycles;
1334
1326
 
1335
1327
  // Go down the level, tracking only the closest match
1336
- id_t closest_id = search_for_one_(entry_id, vector, max_level, target_level, context);
1328
+ id_t closest_id = search_for_one_(entry_id_copy, vector, max_level_copy, target_level, context);
1337
1329
 
1338
1330
  // From `target_level` down perform proper extensive search
1339
- for (level_t level = (std::min)(target_level, max_level); level >= 0; level--) {
1331
+ for (level_t level = (std::min)(target_level, max_level_copy); level >= 0; --level) {
1340
1332
  search_to_insert_(closest_id, vector, level, context);
1341
1333
  closest_id = connect_new_node_(new_id, level, context);
1342
1334
  }
@@ -1346,7 +1338,7 @@ class index_gt {
1346
1338
  result.cycles = context.iteration_cycles - result.cycles;
1347
1339
 
1348
1340
  // Updating the entry point if needed
1349
- if (target_level > max_level) {
1341
+ if (target_level > max_level_copy) {
1350
1342
  entry_id_ = new_id;
1351
1343
  max_level_ = target_level;
1352
1344
  }
@@ -1423,12 +1415,12 @@ class index_gt {
1423
1415
  struct stats_t {
1424
1416
  std::size_t nodes;
1425
1417
  std::size_t edges;
1426
- std::size_t max_edged;
1418
+ std::size_t max_edges;
1427
1419
  };
1428
1420
 
1429
1421
  stats_t stats() const noexcept {
1430
1422
  stats_t result{};
1431
- result.nodes = nodes_.size();
1423
+ result.nodes = size();
1432
1424
  for (std::size_t i = 0; i != result.nodes; ++i) {
1433
1425
  node_t node = node_with_id_(i);
1434
1426
  std::size_t max_edges = node.level() * config_.connectivity + base_level_multiple_() * config_.connectivity;
@@ -1444,7 +1436,7 @@ class index_gt {
1444
1436
 
1445
1437
  stats_t stats(std::size_t level) const noexcept {
1446
1438
  stats_t result{};
1447
- result.nodes = nodes_.size();
1439
+ result.nodes = size();
1448
1440
  for (std::size_t i = 0; i != result.nodes; ++i) {
1449
1441
  node_t node = node_with_id_(i);
1450
1442
  if (node.level() >= level)
@@ -1796,7 +1788,7 @@ class index_gt {
1796
1788
  thread_context_t& context) const noexcept {
1797
1789
 
1798
1790
  distance_t closest_dist = context.measure(query, node_with_id_(closest_id));
1799
- for (level_t level = begin_level; level > end_level; level--) {
1791
+ for (level_t level = begin_level; level > end_level; --level) {
1800
1792
  bool changed;
1801
1793
  do {
1802
1794
  changed = false;
@@ -1923,7 +1915,7 @@ class index_gt {
1923
1915
  top_candidates_t& top = context.top_candidates;
1924
1916
  top.clear();
1925
1917
  top.reserve(count);
1926
- for (std::size_t i = 0; i != nodes_.size(); ++i)
1918
+ for (std::size_t i = 0; i != size(); ++i)
1927
1919
  top.emplace_reserved(context.measure(query, node_with_id_(i)), static_cast<id_t>(i));
1928
1920
  }
1929
1921
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ashot Vardanian",
6
6
  "license": "Apache 2.0",
@@ -1,6 +1,5 @@
1
1
  import os
2
2
  import time
3
- import numpy as np
4
3
 
5
4
  import fire
6
5
  import plotly.express as px
@@ -18,9 +18,16 @@ def _vector_to_ascii(vector: np.ndarray) -> Optional[str]:
18
18
  return ascii
19
19
 
20
20
 
21
+ Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
22
+
23
+
21
24
  class IndexClient:
22
25
 
23
- def __init__(self, uri: str = '127.0.0.1', port: int = 8545, use_http: bool = True) -> None:
26
+ def __init__(
27
+ self,
28
+ uri: str = '127.0.0.1',
29
+ port: int = 8545,
30
+ use_http: bool = True) -> None:
24
31
  self.client = Client(uri=uri, port=port, use_http=use_http)
25
32
 
26
33
  def add_one(self, label: int, vector: np.ndarray):
@@ -46,7 +53,7 @@ class IndexClient:
46
53
  else:
47
54
  return self.add_many(labels, vectors)
48
55
 
49
- def search_one(self, vector: np.ndarray, count: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
56
+ def search_one(self, vector: np.ndarray, count: int) -> Triplet:
50
57
  matches: list[dict] = []
51
58
  vector = vector.flatten()
52
59
  ascii = _vector_to_ascii(vector)
@@ -68,7 +75,7 @@ class IndexClient:
68
75
 
69
76
  return labels, distances, counts
70
77
 
71
- def search_many(self, vectors: np.ndarray, count: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
78
+ def search_many(self, vectors: np.ndarray, count: int) -> Triplet:
72
79
  batch_size: int = vectors.shape[0]
73
80
  list_of_matches: list[list[dict]] = self.client.search_many(
74
81
  vectors=vectors, count=count)
@@ -84,7 +91,7 @@ class IndexClient:
84
91
 
85
92
  return labels, distances, counts
86
93
 
87
- def search(self, vectors: np.ndarray, count: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
94
+ def search(self, vectors: np.ndarray, count: int) -> Triplet:
88
95
  if vectors.ndim == 1 or (vectors.ndim == 2 and vectors.shape[0] == 1):
89
96
  return self.search_one(vectors, count)
90
97
  else:
@@ -8,12 +8,16 @@ import numpy as np
8
8
  from ucall.rich_posix import Server
9
9
  from usearch.index import Index
10
10
 
11
+ Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
12
+
13
+
14
+ def _results_to_json(results: Triplet, row: int) -> list[dict]:
11
15
 
12
- def _results_to_json(results: tuple[np.ndarray, np.ndarray, np.ndarray], row: int) -> list[dict]:
13
16
  count = results[2][row]
14
17
  labels = results[0][row, :count]
15
18
  distances = results[1][row, :count]
16
- return [{'label': int(l), 'distance': float(d)} for l, d in zip(labels, distances)]
19
+ return [{'label': int(label), 'distance': float(distance)}
20
+ for label, distance in zip(labels, distances)]
17
21
 
18
22
 
19
23
  def _ascii_to_vector(string: str) -> np.ndarray:
package/src/punned.hpp CHANGED
@@ -399,12 +399,15 @@ class punned_gt {
399
399
  isa_t acceleration;
400
400
  };
401
401
 
402
+ using member_iterator_t = typename index_t::member_iterator_t;
403
+ using member_citerator_t = typename index_t::member_citerator_t;
404
+
402
405
  std::size_t dimensions_ = 0;
403
406
  std::size_t casted_vector_bytes_ = 0;
404
407
  accuracy_t accuracy_ = accuracy_t::f32_k;
405
408
  isa_t acceleration_ = isa_t::auto_k;
406
409
 
407
- index_t* index_{};
410
+ index_t* typed_{};
408
411
  mutable std::vector<byte_t> cast_buffer_{};
409
412
  struct casts_t {
410
413
  cast_t from_f8{};
@@ -421,6 +424,7 @@ class punned_gt {
421
424
  public:
422
425
  using search_results_t = typename index_t::search_results_t;
423
426
  using add_result_t = typename index_t::add_result_t;
427
+ using stats_t = typename index_t::stats_t;
424
428
 
425
429
  punned_gt() = default;
426
430
  punned_gt(punned_gt&& other) { swap(other); }
@@ -429,14 +433,14 @@ class punned_gt {
429
433
  return *this;
430
434
  }
431
435
 
432
- ~punned_gt() { aligned_index_free_(index_); }
436
+ ~punned_gt() { aligned_index_free_(typed_); }
433
437
 
434
438
  void swap(punned_gt& other) {
435
439
  std::swap(dimensions_, other.dimensions_);
436
440
  std::swap(casted_vector_bytes_, other.casted_vector_bytes_);
437
441
  std::swap(accuracy_, other.accuracy_);
438
442
  std::swap(acceleration_, other.acceleration_);
439
- std::swap(index_, other.index_);
443
+ std::swap(typed_, other.typed_);
440
444
  std::swap(cast_buffer_, other.cast_buffer_);
441
445
  std::swap(casts_, other.casts_);
442
446
  std::swap(root_metric_, other.root_metric_);
@@ -446,19 +450,29 @@ class punned_gt {
446
450
  static config_t optimize(config_t config) noexcept { return index_t::optimize(config); }
447
451
 
448
452
  std::size_t dimensions() const { return dimensions_; }
449
- std::size_t connectivity() const { return index_->connectivity(); }
450
- std::size_t size() const { return index_->size(); }
451
- std::size_t capacity() const { return index_->capacity(); }
452
- config_t const& config() const { return index_->config(); }
453
- void clear() { return index_->clear(); }
453
+ std::size_t connectivity() const { return typed_->connectivity(); }
454
+ std::size_t size() const { return typed_->size(); }
455
+ std::size_t capacity() const { return typed_->capacity(); }
456
+ config_t const& config() const { return typed_->config(); }
457
+ void clear() { return typed_->clear(); }
458
+
459
+ member_citerator_t cbegin() const noexcept { return typed_->cbegin(); }
460
+ member_citerator_t cend() const noexcept { return typed_->cend(); }
461
+ member_citerator_t begin() const noexcept { return typed_->begin(); }
462
+ member_citerator_t end() const noexcept { return typed_->end(); }
463
+ member_iterator_t begin() noexcept { return typed_->begin(); }
464
+ member_iterator_t end() noexcept { return typed_->end(); }
465
+
466
+ stats_t stats() const noexcept { return typed_->stats(); }
467
+ stats_t stats(std::size_t level) const noexcept { return typed_->stats(level); }
454
468
 
455
469
  accuracy_t accuracy() const { return accuracy_; }
456
470
  isa_t acceleration() const { return acceleration_; }
457
471
 
458
- void save(char const* path) const { index_->save(path); }
459
- void load(char const* path) { index_->load(path); }
460
- void view(char const* path) { index_->view(path); }
461
- void reserve(std::size_t capacity) { index_->reserve(capacity); }
472
+ void save(char const* path) const { typed_->save(path); }
473
+ void load(char const* path) { typed_->load(path); }
474
+ void view(char const* path) { typed_->view(path); }
475
+ void reserve(std::size_t capacity) { typed_->reserve(capacity); }
462
476
 
463
477
  // clang-format off
464
478
  add_result_t add(label_t label, f8_bits_t const* vector) { return add_(label, vector, casts_.from_f8); }
@@ -516,7 +530,7 @@ class punned_gt {
516
530
  result.root_metric_ = root_metric_;
517
531
  index_t* raw = aligned_index_alloc_();
518
532
  new (raw) index_t(config(), root_metric_);
519
- result.index_ = raw;
533
+ result.typed_ = raw;
520
534
 
521
535
  return result;
522
536
  }
@@ -563,7 +577,7 @@ class punned_gt {
563
577
  if (casted)
564
578
  vector_data = casted_data, vector_bytes = casted_vector_bytes_, config.store_vector = true;
565
579
 
566
- return index_->add(label, {vector_data, vector_bytes}, config);
580
+ return typed_->add(label, {vector_data, vector_bytes}, config);
567
581
  }
568
582
 
569
583
  template <typename scalar_at>
@@ -579,7 +593,7 @@ class punned_gt {
579
593
  if (casted)
580
594
  vector_data = casted_data, vector_bytes = casted_vector_bytes_;
581
595
 
582
- return index_->search({vector_data, vector_bytes}, wanted, config);
596
+ return typed_->search({vector_data, vector_bytes}, wanted, config);
583
597
  }
584
598
 
585
599
  template <typename scalar_at>
@@ -595,14 +609,14 @@ class punned_gt {
595
609
  if (casted)
596
610
  vector_data = casted_data, vector_bytes = casted_vector_bytes_;
597
611
 
598
- return index_->search_around(static_cast<id_t>(hint), {vector_data, vector_bytes}, wanted, config);
612
+ return typed_->search_around(static_cast<id_t>(hint), {vector_data, vector_bytes}, wanted, config);
599
613
  }
600
614
 
601
615
  template <typename scalar_at> add_result_t add_(label_t label, scalar_at const* vector, cast_t const& cast) {
602
616
  thread_lock_t lock = thread_lock_();
603
- add_config_t config;
604
- config.thread = lock.thread_id;
605
- return add_(label, vector, config, cast);
617
+ add_config_t add_config;
618
+ add_config.thread = lock.thread_id;
619
+ return add_(label, vector, add_config, cast);
606
620
  }
607
621
 
608
622
  template <typename scalar_at>
@@ -610,9 +624,9 @@ class punned_gt {
610
624
  scalar_at const* vector, std::size_t wanted, //
611
625
  cast_t const& cast) const {
612
626
  thread_lock_t lock = thread_lock_();
613
- search_config_t config;
614
- config.thread = lock.thread_id;
615
- return search_(vector, wanted, config, cast);
627
+ search_config_t search_config;
628
+ search_config.thread = lock.thread_id;
629
+ return search_(vector, wanted, search_config, cast);
616
630
  }
617
631
 
618
632
  template <typename scalar_at>
@@ -620,9 +634,9 @@ class punned_gt {
620
634
  label_t hint, scalar_at const* vector, std::size_t wanted, //
621
635
  cast_t const& cast) const {
622
636
  thread_lock_t lock = thread_lock_();
623
- search_config_t config;
624
- config.thread = lock.thread_id;
625
- return search_around_(hint, vector, wanted, config, cast);
637
+ search_config_t search_config;
638
+ search_config.thread = lock.thread_id;
639
+ return search_around_(hint, vector, wanted, search_config, cast);
626
640
  }
627
641
 
628
642
  static punned_gt make_( //
@@ -650,7 +664,7 @@ class punned_gt {
650
664
  // Available since C11, but only C++17, so we use the C version.
651
665
  index_t* raw = aligned_index_alloc_();
652
666
  new (raw) index_t(config, metric_and_meta.metric);
653
- result.index_ = raw;
667
+ result.typed_ = raw;
654
668
  return result;
655
669
  }
656
670
 
package/src/test.cpp CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @brief A trivial test.
3
3
  */
4
+ #include <algorithm>
4
5
  #include <cassert>
5
6
 
6
7
  #include "punned.hpp"
@@ -15,10 +16,13 @@ template <typename scalar_at, typename index_at> void test3d(index_at&& index) {
15
16
  using distance_t = typename index_at::distance_t;
16
17
 
17
18
  scalar_at vec[3] = {10, 20, 15};
19
+ scalar_at vec_a[3] = {15, 16, 17};
20
+ scalar_at vec_b[3] = {16, 17, 18};
18
21
 
19
22
  index.reserve(10);
20
23
  index.add(42, span_t{&vec[0], 3ul});
21
24
 
25
+ // Default approximate search
22
26
  point_id_t matched_labels[10] = {0};
23
27
  distance_t matched_distances[10] = {0};
24
28
  std::size_t matched_count = index.search(span_t{&vec[0], 3ul}, 5).dump_to(matched_labels, matched_distances);
@@ -27,19 +31,38 @@ template <typename scalar_at, typename index_at> void test3d(index_at&& index) {
27
31
  assert(matched_labels[0] == 42);
28
32
  assert(std::abs(matched_distances[0]) < 0.01);
29
33
 
30
- index.save("tmp.usearch");
34
+ // Add more entries
35
+ search_config_t search_config;
36
+ search_config.exact = true;
37
+ index.add(43, span_t{&vec_a[0], 3ul});
38
+ index.add(44, span_t{&vec_b[0], 3ul});
39
+ assert(index.size() == 3);
40
+
41
+ // Perform exact search
42
+ matched_count = index.search(span_t{&vec[0], 3ul}, 5, search_config).dump_to(matched_labels, matched_distances);
43
+
44
+ // Validate scans
45
+ std::size_t count = 0;
46
+ for (auto member : index) {
47
+ point_id_t id = member.label;
48
+ assert(id >= 42 && id <= 44);
49
+ count++;
50
+ }
51
+ assert((count == 3));
52
+ assert((index.stats(0).nodes == 3));
31
53
 
32
54
  // Search again over reconstructed index
55
+ index.save("tmp.usearch");
33
56
  index.load("tmp.usearch");
34
57
  matched_count = index.search(span_t{&vec[0], 3ul}, 5).dump_to(matched_labels, matched_distances);
35
- assert(matched_count == 1);
58
+ assert(matched_count == 3);
36
59
  assert(matched_labels[0] == 42);
37
60
  assert(std::abs(matched_distances[0]) < 0.01);
38
61
 
39
62
  // Search again over mapped index
40
63
  index.view("tmp.usearch");
41
64
  matched_count = index.search(span_t{&vec[0], 3ul}, 5).dump_to(matched_labels, matched_distances);
42
- assert(matched_count == 1);
65
+ assert(matched_count == 3);
43
66
  assert(matched_labels[0] == 42);
44
67
  assert(std::abs(matched_distances[0]) < 0.01);
45
68
  }