usearch 2.1.1 → 2.1.2

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.
@@ -30,7 +30,7 @@ jobs:
30
30
 
31
31
 
32
32
  rebase:
33
- name: Rebase Development Branch
33
+ name: Rebase Dev. Branch
34
34
  needs: versioning
35
35
  runs-on: ubuntu-latest
36
36
  steps:
@@ -41,13 +41,14 @@ jobs:
41
41
 
42
42
  - name: Perform rebase
43
43
  run: |
44
+ git fetch origin main
44
45
  git checkout main-dev
45
- git rebase main
46
+ git rebase origin/main
46
47
 
47
48
  - name: Push changes
48
49
  uses: CasperWA/push-protected@v2
49
50
  with:
50
- token: ${{ secrets.SEMANTIC_REBASE_TOKEN }}
51
+ token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
51
52
  branch: main-dev
52
53
  unprotect_reviews: True
53
54
  force: True
@@ -239,12 +240,14 @@ jobs:
239
240
  - name: Calculate source hashes
240
241
  id: hashes
241
242
  run: |
242
- source_zip_path=$(find $GITHUB_WORKSPACE/downloads -name "*.zip")
243
- zip_hash=$(sha256sum "$source_zip_path" | awk '{print $1}')
244
- source_tar_path=$(find $GITHUB_WORKSPACE/downloads -name "*.tar.gz" ! -name "docs.tar.gz")
245
- tar_hash=$(sha256sum "$source_tar_path" | awk '{print $1}')
246
- echo "zip_hash=$zip_hash" >> $GITHUB_OUTPUT
247
- echo "tar_hash=$tar_hash" >> $GITHUB_OUTPUT
243
+ i=1
244
+ cd $GITHUB_WORKSPACE/downloads
245
+ for asset_name in $(ls); do
246
+ asset_hash=$(sha256sum $asset_name | awk '{print $1}')
247
+ echo "asset_name_$i=$asset_name" >> $GITHUB_OUTPUT
248
+ echo "asset_hash_$i=$asset_hash" >> $GITHUB_OUTPUT
249
+ i=$((i+1))
250
+ done
248
251
 
249
252
  - name: Upload hashes to release
250
253
  uses: irongut/EditRelease@v1.2.0
@@ -255,8 +258,14 @@ jobs:
255
258
  replacebody: false
256
259
  body: |
257
260
  ## Hashes
258
- * Source code (zip) : `${{ steps.hashes.outputs.zip_hash }}`
259
- * Source code (tar.gz) : `${{ steps.hashes.outputs.tar_hash }}`
261
+ * ${{ steps.hashes.outputs.asset_name_1 }} : `${{ steps.hashes.outputs.asset_hash_1 }}`
262
+ * ${{ steps.hashes.outputs.asset_name_2 }} : `${{ steps.hashes.outputs.asset_hash_2 }}`
263
+ * ${{ steps.hashes.outputs.asset_name_3 }} : `${{ steps.hashes.outputs.asset_hash_3 }}`
264
+ * ${{ steps.hashes.outputs.asset_name_4 }} : `${{ steps.hashes.outputs.asset_hash_4 }}`
265
+ * ${{ steps.hashes.outputs.asset_name_5 }} : `${{ steps.hashes.outputs.asset_hash_5 }}`
266
+ * ${{ steps.hashes.outputs.asset_name_6 }} : `${{ steps.hashes.outputs.asset_hash_6 }}`
267
+ * ${{ steps.hashes.outputs.asset_name_7 }} : `${{ steps.hashes.outputs.asset_hash_7 }}`
268
+ * ${{ steps.hashes.outputs.asset_name_8 }} : `${{ steps.hashes.outputs.asset_hash_8 }}`
260
269
 
261
270
 
262
271
  build_wheels:
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "2.1.1"
3
+ version = "2.1.2"
4
4
  authors = ["Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/README.md CHANGED
@@ -83,7 +83,7 @@ USearch is compact and broadly compatible without sacrificing performance, with
83
83
  Base functionality is identical to FAISS, and the interface must be familiar if you have ever investigated Approximate Nearest Neighbors search:
84
84
 
85
85
  ```py
86
- $ pip install usearch numpy
86
+ $ pip install usearch
87
87
 
88
88
  import numpy as np
89
89
  from usearch.index import Index
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 = '2.1.1'
7
+ version = '2.1.2'
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'
package/docs/format.md ADDED
@@ -0,0 +1,63 @@
1
+ # USearch Format Specification
2
+
3
+ > ⚠️ **Note**: The format is expected to change in the 3.0 release to support sparse indexes and offer even faster startup times when `view`-ing large indexes from disk.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Dense Indexes](#dense-indexes)
8
+ - [Core](#core)
9
+ - [Fragmented Storage](#fragmented-storage)
10
+
11
+ ## Dense Indexes
12
+
13
+ Dense index files consist of two main parts: [a binary matrix of all vectors](#matrix-blob) and the [index](#index-blob) itself.
14
+
15
+ ### Matrix BLOB
16
+
17
+ 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
+
19
+ - The number of `rows` corresponds to the number of vectors in the index.
20
+ - The number of `columns` corresponds to the number of bytes in a single vector.
21
+
22
+ ### Index BLOB
23
+
24
+ The index BLOB is composed of three main sections:
25
+
26
+ 1. **Metadata**: Occupies 64 bytes and contains the following fields:
27
+ 1. 7-byte magic string `usearch` to infer the MIME type of the file.
28
+ 2. 3-byte version number, containing the `major`, `minor`, and `patch` release numbers.
29
+ 3. 1-byte enums for `metric`, `scalar`, `key`, and `compressed_slot` types.
30
+ 4. 8-byte integers for the number of present vectors, number of deleted vectors, and number of dimensions.
31
+ 5. 1-byte flags for multi-vector support.
32
+
33
+ 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
+
35
+ 3. **Core**: Detailed in the [Core section](#core). This format is consistent across all types of indexes.
36
+
37
+ ## Fragmented Storage
38
+
39
+ The matrix BLOB can be stored separately and is not required to successfully open the file.
40
+
41
+ ## Core
42
+
43
+ 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
+
45
+ ### Header
46
+
47
+ The core section begins with a header containing the following fields:
48
+
49
+ - `uint64_t size`: Number of nodes in the index.
50
+ - `uint64_t connectivity`: Maximum number of edges for a node.
51
+ - `uint64_t connectivity_base`: Base connectivity value.
52
+ - `uint64_t max_level`: Maximum level in the multi-level graph.
53
+ - `uint64_t entry_slot`: Entry slot for the graph.
54
+
55
+ These fields are serialized in a contiguous block of bytes, in the order listed.
56
+
57
+ ### Levels
58
+
59
+ 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
+
61
+ ### Nodes
62
+
63
+ 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.
@@ -0,0 +1,5 @@
1
+ ========================
2
+ Format
3
+ ========================
4
+
5
+ .. mdinclude:: format.md
package/docs/index.rst CHANGED
@@ -24,6 +24,7 @@ Overview
24
24
 
25
25
  compilation
26
26
  benchmarks
27
+ format
27
28
 
28
29
  .. toctree::
29
30
  :hidden:
@@ -1272,7 +1272,7 @@ struct dummy_executor_t {
1272
1272
  * @brief An example of what a USearch-compatible key-to-key mapping should look like.
1273
1273
  *
1274
1274
  * This is particularly helpful for "Semantic Joins", where we map entries of one collection
1275
- * to entries of another. In assymetric setups, where A -> B is needed, but B -> A is not,
1275
+ * to entries of another. In asymmetric setups, where A -> B is needed, but B -> A is not,
1276
1276
  * this can be passed to minimize memory usage.
1277
1277
  */
1278
1278
  struct dummy_key_to_key_mapping_t {
@@ -1284,16 +1284,16 @@ struct dummy_key_to_key_mapping_t {
1284
1284
 
1285
1285
  /**
1286
1286
  * @brief Checks if the provided object has a dummy type, emulating an interface,
1287
- * but performaing no real computation.
1287
+ * but performing no real computation.
1288
1288
  */
1289
1289
  template <typename object_at> static constexpr bool is_dummy() {
1290
1290
  using object_t = typename std::remove_all_extents<object_at>::type;
1291
- return std::is_same<object_t, dummy_predicate_t>::value || //
1292
- std::is_same<object_t, dummy_callback_t>::value || //
1293
- std::is_same<object_t, dummy_progress_t>::value || //
1294
- std::is_same<object_t, dummy_prefetch_t>::value || //
1295
- std::is_same<object_t, dummy_executor_t>::value || //
1296
- std::is_same<object_t, dummy_key_to_key_mapping_t>::value;
1291
+ return std::is_same<typename std::decay<object_t>::type, dummy_predicate_t>::value || //
1292
+ std::is_same<typename std::decay<object_t>::type, dummy_callback_t>::value || //
1293
+ std::is_same<typename std::decay<object_t>::type, dummy_progress_t>::value || //
1294
+ std::is_same<typename std::decay<object_t>::type, dummy_prefetch_t>::value || //
1295
+ std::is_same<typename std::decay<object_t>::type, dummy_executor_t>::value || //
1296
+ std::is_same<typename std::decay<object_t>::type, dummy_key_to_key_mapping_t>::value;
1297
1297
  }
1298
1298
 
1299
1299
  template <typename, typename at> struct has_reset_gt {
@@ -1513,6 +1513,8 @@ class memory_mapped_file_t {
1513
1513
  #else
1514
1514
  int descriptor = open(path_, O_RDONLY);
1515
1515
  #endif
1516
+ if (descriptor < 0)
1517
+ return result.failed(std::strerror(errno));
1516
1518
 
1517
1519
  // Estimate the file size
1518
1520
  struct stat file_stat;
@@ -2502,7 +2504,7 @@ class index_gt {
2502
2504
  }
2503
2505
 
2504
2506
  /**
2505
- * @brief Identifies the closest cluster to the gived ::query. Thread-safe.
2507
+ * @brief Identifies the closest cluster to the given ::query. Thread-safe.
2506
2508
  *
2507
2509
  * @param[in] query Content that will be compared against other entries in the index.
2508
2510
  * @param[in] level The index level to target. Higher means lower resolution.
@@ -3325,7 +3327,7 @@ class index_gt {
3325
3327
  visits.clear();
3326
3328
 
3327
3329
  // Optional prefetching
3328
- if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
3330
+ if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value)
3329
3331
  prefetch(citerator_at(closest_slot), citerator_at(closest_slot + 1));
3330
3332
 
3331
3333
  distance_t closest_dist = context.measure(query, citerator_at(closest_slot), metric);
@@ -3337,7 +3339,7 @@ class index_gt {
3337
3339
  neighbors_ref_t closest_neighbors = neighbors_non_base_(node_at_(closest_slot), level);
3338
3340
 
3339
3341
  // Optional prefetching
3340
- if (!std::is_same<prefetch_at, dummy_prefetch_t>::value) {
3342
+ if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value) {
3341
3343
  candidates_range_t missing_candidates{*this, closest_neighbors, visits};
3342
3344
  prefetch(missing_candidates.begin(), missing_candidates.end());
3343
3345
  }
@@ -3379,7 +3381,7 @@ class index_gt {
3379
3381
  return false;
3380
3382
 
3381
3383
  // Optional prefetching
3382
- if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
3384
+ if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value)
3383
3385
  prefetch(citerator_at(start_slot), citerator_at(start_slot + 1));
3384
3386
 
3385
3387
  distance_t radius = context.measure(query, citerator_at(start_slot), metric);
@@ -3404,7 +3406,7 @@ class index_gt {
3404
3406
  neighbors_ref_t candidate_neighbors = neighbors_(candidate_ref, level);
3405
3407
 
3406
3408
  // Optional prefetching
3407
- if (!std::is_same<prefetch_at, dummy_prefetch_t>::value) {
3409
+ if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value) {
3408
3410
  candidates_range_t missing_candidates{*this, candidate_neighbors, visits};
3409
3411
  prefetch(missing_candidates.begin(), missing_candidates.end());
3410
3412
  }
@@ -3453,7 +3455,7 @@ class index_gt {
3453
3455
  return false;
3454
3456
 
3455
3457
  // Optional prefetching
3456
- if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
3458
+ if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value)
3457
3459
  prefetch(citerator_at(start_slot), citerator_at(start_slot + 1));
3458
3460
 
3459
3461
  distance_t radius = context.measure(query, citerator_at(start_slot), metric);
@@ -3473,7 +3475,7 @@ class index_gt {
3473
3475
  neighbors_ref_t candidate_neighbors = neighbors_base_(node_at_(candidate.slot));
3474
3476
 
3475
3477
  // Optional prefetching
3476
- if (!std::is_same<prefetch_at, dummy_prefetch_t>::value) {
3478
+ if (!std::is_same<typename std::decay<prefetch_at>::type, dummy_prefetch_t>::value) {
3477
3479
  candidates_range_t missing_candidates{*this, candidate_neighbors, visits};
3478
3480
  prefetch(missing_candidates.begin(), missing_candidates.end());
3479
3481
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian",
6
6
  "license": "Apache 2.0",
@@ -562,12 +562,11 @@ class Index:
562
562
  )
563
563
 
564
564
  self.path = path
565
- if path and os.path.exists(path):
566
- path = os.fspath(path)
565
+ if path is not None:
567
566
  if view:
568
- self._compiled.view(path)
567
+ self.view(path)
569
568
  else:
570
- self._compiled.load(path)
569
+ self.load(path)
571
570
 
572
571
  @staticmethod
573
572
  def metadata(path_or_buffer: PathOrBuffer) -> Optional[dict]:
@@ -919,7 +918,9 @@ class Index:
919
918
  def expansion_search(self, v: int):
920
919
  self._compiled.expansion_search = v
921
920
 
922
- def save(self, path_or_buffer: Union[str, os.PathLike, NoneType] = None) -> Optional[bytes]:
921
+ def save(
922
+ self, path_or_buffer: Union[str, os.PathLike, NoneType] = None
923
+ ) -> Optional[bytes]:
923
924
  path_or_buffer = path_or_buffer if path_or_buffer else self.path
924
925
  if path_or_buffer is None:
925
926
  return self._compiled.save_index_to_buffer()
@@ -937,7 +938,9 @@ class Index:
937
938
  else:
938
939
  self._compiled.load_index_from_path(os.fspath(path_or_buffer))
939
940
 
940
- def view(self, path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None):
941
+ def view(
942
+ self, path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None
943
+ ):
941
944
  path_or_buffer = path_or_buffer if path_or_buffer else self.path
942
945
  if path_or_buffer is None:
943
946
  raise Exception("Define the source")
package/setup.py CHANGED
@@ -66,6 +66,7 @@ setup(
66
66
  package_dir={"usearch": "python/usearch"},
67
67
  description="Smaller & Faster Single-File Vector Search Engine from Unum",
68
68
  author="Ash Vardanian",
69
+ author_email="info@unum.cloud",
69
70
  long_description=long_description,
70
71
  long_description_content_type="text/markdown",
71
72
  license="Apache-2.0",
package/wasmer.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name="unum/usearch"
3
- version="2.1.1"
3
+ version="2.1.2"
4
4
  description="Smaller & Faster Single-File Vector Search Engine from Unum"
5
5
  license="Apache-2.0"
6
6
  readme="README.md"