zeusdb-vector-database 0.5.0__tar.gz → 0.7.0__tar.gz
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.
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/NOTICE +9 -13
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/PKG-INFO +53 -21
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/README.md +52 -20
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/pyproject.toml +11 -5
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/src/zeusdb_vector_database/__init__.py +1 -1
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/src/zeusdb_vector_database/vector_database.py +87 -6
- zeusdb_vector_database-0.7.0/vdb-core/Cargo.lock +1073 -0
- zeusdb_vector_database-0.7.0/vdb-core/Cargo.toml +59 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/conversion.rs +128 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/distance.rs +1871 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/filter.rs +226 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/graph/dump.rs +1642 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/graph/levels.rs +129 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/graph/mod.rs +758 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/graph/mutable/insert.rs +538 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/graph/mutable/mod.rs +1590 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/graph/structure.rs +802 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/graph/test_graph.rs +116 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/graph/traverse.rs +320 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/hnsw_index/construct.rs +459 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/hnsw_index/graph_guard_tests.rs +478 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/hnsw_index/input.rs +649 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/hnsw_index/insert.rs +833 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/hnsw_index/mod.rs +1186 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/hnsw_index/persist.rs +448 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/hnsw_index/search.rs +481 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/hnsw_index/stats.rs +742 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/hnsw_index/training.rs +536 -0
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/vdb-core/src/lib.rs +10 -0
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/vdb-core/src/logging.rs +14 -18
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/vdb-core/src/persistence.rs +141 -128
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/vdb-core/src/pq.rs +117 -9
- zeusdb_vector_database-0.7.0/vdb-core/src/rerank.rs +1697 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/rng.rs +171 -0
- zeusdb_vector_database-0.7.0/vdb-core/src/test_vectors.rs +43 -0
- zeusdb_vector_database-0.5.0/LICENSES/hnsw_rs-Apache-2.0.txt +0 -176
- zeusdb_vector_database-0.5.0/LICENSES/hnsw_rs-MIT.txt +0 -25
- zeusdb_vector_database-0.5.0/vdb-core/Cargo.lock +0 -1681
- zeusdb_vector_database-0.5.0/vdb-core/Cargo.toml +0 -48
- zeusdb_vector_database-0.5.0/vdb-core/src/distance.rs +0 -1026
- zeusdb_vector_database-0.5.0/vdb-core/src/hnsw_index.rs +0 -8115
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/.cargo-ok +0 -1
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/.cargo_vcs_info.json +0 -6
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/.gitignore +0 -9
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/Cargo.lock +0 -1296
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/Cargo.toml +0 -145
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/Cargo.toml.orig +0 -111
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/Changes.md +0 -59
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/LICENSE-APACHE +0 -13
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/LICENSE-MIT +0 -25
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/README.md +0 -168
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/ZEUSDB-PATCH.md +0 -469
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/examples/ann-glove25-angular.rs +0 -220
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/examples/ann-mnist-784-euclidean.rs +0 -162
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/examples/ann-sift1m-128-euclidean.rs +0 -196
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/examples/levensthein.rs +0 -63
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/examples/random.rs +0 -80
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/examples/utils/annhdf5.rs +0 -233
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/examples/utils/mod.rs +0 -3
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/src/api.rs +0 -94
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/src/datamap.rs +0 -456
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/src/filter.rs +0 -24
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/src/flatten.rs +0 -200
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/src/hnsw.rs +0 -2023
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/src/hnswio.rs +0 -1721
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/src/lib.rs +0 -30
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/src/libext.rs +0 -1240
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/src/prelude.rs +0 -11
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/tests/deallocation_test.rs +0 -34
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/tests/filtertest.rs +0 -266
- zeusdb_vector_database-0.5.0/vendor/hnsw_rs/tests/serpar.rs +0 -328
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/LICENSE +0 -0
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/src/zeusdb_vector_database/logging_config.py +0 -0
- {zeusdb_vector_database-0.5.0 → zeusdb_vector_database-0.7.0}/src/zeusdb_vector_database/py.typed +0 -0
|
@@ -6,19 +6,15 @@ This product includes software developed by:
|
|
|
6
6
|
- The Python Software Foundation (https://www.python.org/psf/)
|
|
7
7
|
- The Rust Project Developers (https://www.rust-lang.org/)
|
|
8
8
|
- The PyO3 Project (https://pyo3.rs/)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
vendor/hnsw_rs/ZEUSDB-PATCH.md for the patch record. hnsw_rs is dual licensed
|
|
19
|
-
under MIT or Apache-2.0.
|
|
20
|
-
|
|
21
|
-
Third-party components may be licensed under their respective terms. See LICENSES/ for full license texts.
|
|
9
|
+
|
|
10
|
+
Releases up to and including 0.6.0 were built on a vendored copy of a Rust HNSW
|
|
11
|
+
crate by Jean-Pierre Both, dual licensed under MIT or Apache-2.0. From 0.7.0 the
|
|
12
|
+
graph structure, its traversal, its insert, its level stream and its on-disk
|
|
13
|
+
format are ZeusDB's own, and neither that crate nor its source is part of this
|
|
14
|
+
package. ZeusDB's graph reproduces the behaviour of that implementation, and the
|
|
15
|
+
acknowledgement is kept here for that reason.
|
|
16
|
+
|
|
17
|
+
Third-party components may be licensed under their respective terms.
|
|
22
18
|
|
|
23
19
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
24
20
|
you may not use this file except in compliance with the License.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zeusdb-vector-database
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Requires-Dist: numpy>=2.2.6,<3.0.0
|
|
@@ -35,7 +35,7 @@ Project-URL: Repository, https://github.com/zeusdb/zeusdb-vector-database
|
|
|
35
35
|
<td><strong>Meta</strong></td>
|
|
36
36
|
<td>
|
|
37
37
|
<a href="https://pypi.org/project/zeusdb-vector-database/"><img src="https://img.shields.io/pypi/v/zeusdb-vector-database?label=PyPI&color=blue"></a>
|
|
38
|
-
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%7C3.11%7C3.12%7C3.13-blue?logo=python&logoColor=ffdd54"></a>
|
|
38
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%7C3.11%7C3.12%7C3.13%7C3.14-blue?logo=python&logoColor=ffdd54"></a>
|
|
39
39
|
<a href="https://github.com/zeusdb/zeusdb-vector-database/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg"></a>
|
|
40
40
|
<a href="https://www.rust-lang.org"><img src="https://img.shields.io/badge/Powered%20by-Rust-black?logo=rust&logoColor=white" alt="Powered by Rust"></a>
|
|
41
41
|
<a href="https://github.com/ZeusDB"><img src="https://github.com/user-attachments/assets/e140d900-1160-4eaa-85c0-2b3507a5f0f5" alt="ZeusDB"></a>
|
|
@@ -212,7 +212,7 @@ HNSWIndex(dim=8, space=cosine, m=16, ef_construction=200, expected_size=5, vecto
|
|
|
212
212
|
| `dim` | `int` | `1536` | Dimensionality of the vectors to be indexed. Each vector must have this length. Must be positive. The default of 1536 matches the output dimensionality of OpenAI's `text-embedding-3-small` and `text-embedding-ada-002` models. |
|
|
213
213
|
| `space` | `str` | `"cosine"`| Distance metric used for similarity search. One of `"cosine"`, `"l1"`, `"l2"`. Case-insensitive. |
|
|
214
214
|
| `m` | `int` | `16` or `32`, see below | Number of bi-directional connections created for each new node, from 2 to 256. Higher `m` improves recall but increases index size and build time. |
|
|
215
|
-
| `ef_construction`| `int` | `200` |
|
|
215
|
+
| `ef_construction`| `int` | `200` | Width of the candidate search each insertion runs. Must be positive. It costs build time and buys graph quality, and it changes neither search latency nor the size of the finished index. See below. |
|
|
216
216
|
| `expected_size` | `int` | `10000` | Estimated number of records to be inserted, from 1 to 100,000,000. Used for preallocating internal data structures and for choosing the default `m`. Not a hard limit, see below. |
|
|
217
217
|
| `quantization_config` | `dict` | `None` | Product Quantization configuration for memory-efficient vector compression. See [Product Quantization](#️-product-quantization). |
|
|
218
218
|
|
|
@@ -229,6 +229,24 @@ The upper bound of 100,000,000 exists because the graph reserves one slot per de
|
|
|
229
229
|
|
|
230
230
|
**`m` starts at 2, not 1.** Layer assignment samples from a scale of `1 / ln(m)`, which is infinity at `m` of 1, so every point is redispatched uniformly across all 16 layers rather than following the exponential distribution the graph depends on. On 3,000 records of 32 dimensions, recall at 10 measured 0.0220 at `m` of 1 against 0.6880 at 2 and 1.0000 at 16.
|
|
231
231
|
|
|
232
|
+
**`ef_construction` governs insertion and nothing else.** It is the width of the candidate search each insertion runs, at the new point's own layer and at every layer below it. The descent above that layer runs at a width of 1 and ignores it. The candidates that search returns are the pool the neighbour selection draws from, so `ef_construction` sets the supply and `m` sets how much of it is kept.
|
|
233
|
+
|
|
234
|
+
**It does not change search latency or index size.** A node holds at most `2 × m` neighbours at layer zero and `m` above it whatever width found them, so the finished graph is the same size at every `ef_construction`. Measured over 50,000 records, mean query latency moved between 0.89 ms and 2.85 ms across a sixteenfold range of `ef_construction` with no trend in it, and resident memory ran backwards on one of the three datasets. What it does change is build time, which is linear in it above 100. On OpenAI embeddings of 1,536 dimensions, 50,000 records built in 62.7 s, 131.2 s, 262.8 s and 464.9 s at `ef_construction` 100, 200, 400 and 800. On GloVe of 100 dimensions the same four are 37.7 s, 75.0 s, 140.4 s and 239.1 s. Below 100 the curve flattens, because the fixed per-insert work stops being dominated by the candidate search.
|
|
235
|
+
|
|
236
|
+
**Recall stops improving at or near the default.** Recall at 10 over 50,000 records at the default `m` of 32, 500 queries, at the default search width:
|
|
237
|
+
|
|
238
|
+
| `ef_construction` | GloVe 100d | SIFT 128d | OpenAI 1536d |
|
|
239
|
+
|-------------------|------------|-----------|--------------|
|
|
240
|
+
| 50 | 0.9176 | 0.9982 | 0.9852 |
|
|
241
|
+
| 100 | 0.9400 | 0.9992 | 0.9912 |
|
|
242
|
+
| 200 | 0.9606 | 0.9996 | 0.9962 |
|
|
243
|
+
| 400 | 0.9684 | 0.9998 | 0.9972 |
|
|
244
|
+
| 800 | 0.9714 | 0.9996 | 0.9978 |
|
|
245
|
+
|
|
246
|
+
SIFT plateaus at 100 and OpenAI at 200. GloVe is the one dataset that keeps climbing, and there the cheaper move is the search width rather than the build. At `ef_construction` 200 GloVe returns 0.9606 at the default width and 0.9984 at `ef_search=500`, so widening the search buys 0.0378 on the graph already built where doubling `ef_construction` buys 0.0078 for 1.9 times the build time. Raise `ef_search` before raising this.
|
|
247
|
+
|
|
248
|
+
**Keep `ef_construction` above `2 × m`.** The neighbour selection heuristic runs only when the candidate list is longer than the neighbour budget, which is `2 × m` at layer zero, and the candidate list is exactly `ef_construction` long on any index holding more records than that. At or below the budget the graph keeps every candidate the search returned, in distance order, and prunes none of them. `create()` warns when the pair reaches that point. `m` is capped at 256, so `m=100` with the default `ef_construction` is enough to switch the heuristic off. The defaults are clear of it, 200 against a budget of 32 at `m` 16 and 64 at `m` 32.
|
|
249
|
+
|
|
232
250
|
<br/>
|
|
233
251
|
|
|
234
252
|
### 2️⃣ Add Data to the Index
|
|
@@ -625,17 +643,21 @@ storage_mode_description: raw_only
|
|
|
625
643
|
|
|
626
644
|
`get_stats()` returns a `str` to `str` map. It also carries `dimension`, `space`, `m`, `ef_construction`, `expected_size`, `index_type`, `raw_vectors_stored`, `quantized_codes_stored` and `storage_mode`, plus training and compression fields once quantization is configured.
|
|
627
645
|
|
|
628
|
-
It is also where the memory figures live, on every index rather than only on a quantized one. `graph_memory_mb` is the HNSW graph, `raw_vectors_memory_mb` is the raw vector store and `total_memory_mb` is the sum of everything below. On a quantized index `quantized_codes_memory_mb` scales with the record count, while `codebook_memory_mb` and `sdc_table_memory_mb` are fixed by `dim`, `subvectors` and `bits` and do not move as records arrive. `raw_vectors_retained` states the mode's policy: `none_once_trained` for `quantized_only`, whose raw vectors are released when training completes, and `all_records` for `quantized_with_raw`.
|
|
646
|
+
It is also where the memory figures live, on every index rather than only on a quantized one. `graph_memory_mb` is the HNSW graph, `raw_vectors_memory_mb` is the raw vector store, `index_bookkeeping_memory_mb` is the hash tables that find a record, and `total_memory_mb` is the sum of everything below. On a quantized index `quantized_codes_memory_mb` scales with the record count, while `codebook_memory_mb` and `sdc_table_memory_mb` are fixed by `dim`, `subvectors` and `bits` and do not move as records arrive. `raw_vectors_retained` states the mode's policy: `none_once_trained` for `quantized_only`, whose raw vectors are released when training completes, and `all_records` for `quantized_with_raw`.
|
|
629
647
|
|
|
630
648
|
**The graph is usually the largest of those figures, and on a memory optimized index it is nearly all of it.** It owns a second copy of every point on top of its neighbour lists, its sixteen per point layer headers and the counters around them. At 50,000 records of `dim=1536` a trained `quantized_only` index reports 98.1 MB of graph against 2.3 MB of codes and 7.5 MB of fixed tables.
|
|
631
649
|
|
|
632
|
-
|
|
650
|
+
`index_bookkeeping_memory_mb` is what the index spends on finding a record rather than on holding one. Five hash tables carry a record, being `id_map`, `rev_map`, the metadata map, the raw vector store and the code store, and two of them carry a second copy of its id. It is proportional to the record count and independent of the dimension, at 265 bytes per record with no quantization, 334 under `quantized_with_raw` and 265 under `quantized_only` at 50,000 records.
|
|
651
|
+
|
|
652
|
+
**`total_memory_mb` is not the resident set and it does not claim to be.** It prices the six structures above, meaning the bytes the index asked the allocator for. What it leaves out is the allocator itself, being its own block headers, its rounding and its fragmentation, and that is a property of the platform rather than something the index holds. Measured on three loaded indexes of 50,000 real 1,536-dimensional embeddings, one process each, with the interpreter's own resident set subtracted:
|
|
633
653
|
|
|
634
|
-
| storage mode | `total_memory_mb` | resident |
|
|
635
|
-
|
|
636
|
-
| no quantization |
|
|
637
|
-
| `quantized_with_raw` |
|
|
638
|
-
| `quantized_only` |
|
|
654
|
+
| storage mode | `total_memory_mb` | resident | share reported | unpriced |
|
|
655
|
+
|---|---:|---:|---:|---:|
|
|
656
|
+
| no quantization | 705.1 MB | 805.4 MiB | 0.88 | 2,103 B per record |
|
|
657
|
+
| `quantized_with_raw` | 415.9 MB | 473.3 MiB | 0.88 | 1,202 B per record |
|
|
658
|
+
| `quantized_only` | 119.7 MB | 181.0 MiB | 0.66 | 1,285 B per record |
|
|
659
|
+
|
|
660
|
+
The unpriced share is almost all allocator overhead on the graph, which asks for six small blocks per point. It runs 1.25 times the graph figure with no quantization, where the per point data block is 6,144 bytes and rounding is a small share of it, and 1.63 and 1.67 times under the two quantized modes, where that block is 48 bytes. That is why `quantized_only` reports the smallest share of its resident set.
|
|
639
661
|
|
|
640
662
|
Size infrastructure from the resident column rather than the reported one. The reported figure is what the index holds; the process holds that plus what the allocator takes to hold it.
|
|
641
663
|
|
|
@@ -1012,13 +1034,22 @@ The relation is curved as well as sublinear. On dbpedia-openai the calibration m
|
|
|
1012
1034
|
|
|
1013
1035
|
What the calibration asks for at 100,000 records, against what the data needs and against the fixed 2%. Every figure is measured on a built index through the ordinary search path, 1,000 queries at `top_k=10` against exact ground truth. The requirement is the smallest fetch on that same index reaching recall at 10 of 0.99, read off a sweep of the explicit `rerank` argument:
|
|
1014
1036
|
|
|
1015
|
-
| dataset | calibrated fetch | measured requirement |
|
|
1016
|
-
|
|
1017
|
-
| dbpedia-openai |
|
|
1018
|
-
| glove-100 |
|
|
1019
|
-
| sift-128 |
|
|
1037
|
+
| dataset | records | calibrated fetch | measured requirement | ratio | recall at 10 | mean query |
|
|
1038
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
1039
|
+
| dbpedia-openai | 50,000 | 554 | 860 | 0.64 | 0.9883 | 3.33 ms |
|
|
1040
|
+
| glove-100 | 50,000 | 6,534 | 3,060 | 2.14 | 0.9982 | 35.79 ms |
|
|
1041
|
+
| sift-128 | 50,000 | 465 | 370 | 1.26 | 0.9953 | 1.88 ms |
|
|
1042
|
+
| dbpedia-openai | 100,000 | 747 | 620 | 1.20 | 0.9907 | 5.02 ms |
|
|
1043
|
+
| glove-100 | 100,000 | 11,439 | 4,620 | 2.48 | 0.9985 | 140.08 ms |
|
|
1044
|
+
| sift-128 | 100,000 | 656 | 450 | 1.46 | 0.9968 | 3.19 ms |
|
|
1045
|
+
|
|
1046
|
+
**The margin is not the same on the three, and glove-100 carries most of it.** The rule is one measurement per index scaled by two constants that are the same everywhere, being a safety factor of 1.75 and a bias of 0.15 on the fitted exponent. Taking the fitted exponent alone with neither constant, the extrapolation to 100,000 records asks for 4,626 candidates on glove-100 against a requirement of 4,620, which is 1.001 times what that index needs. On the other two it asks for 0.49 and 0.59 times what they need. The constants exist for those two, and on glove-100 they are margin over a fetch that was already right.
|
|
1020
1047
|
|
|
1021
|
-
The
|
|
1048
|
+
Neither constant can come down without costing recall elsewhere. The constant that would bring glove-100 to the 1.20 ratio dbpedia-openai has at 100,000 records takes the dbpedia-openai fetch from 747 to 359, and a sweep of that same index reads recall 0.9787 at 360 candidates. dbpedia-openai also sits under 0.99 at 10,000, 25,000 and 50,000 records, at 0.9878, 0.9800 and 0.9883, so it has nothing to give back. What separates the datasets is the ratio between the requirement on a built index and the depth the calibration measures in the code ordering over its training sample, and the training sample cannot measure that ratio because the graph does not exist yet when it runs.
|
|
1049
|
+
|
|
1050
|
+
**If query time on glove-like data matters more to you than the last thousandth of recall, name `rerank` yourself.** On glove-100 at 50,000 records a fetch of 3,060 reads recall 0.9900 at 18.14 ms against 0.9982 at 35.79 ms, so half the query time buys back 0.008 of recall.
|
|
1051
|
+
|
|
1052
|
+
The two tables below carry fetch figures measured before the codebook seeding this release ships, which moved the calibrated fetch upward on glove-100. Read their ratios as the shape rather than as current figures.
|
|
1022
1053
|
|
|
1023
1054
|
**On data with no resolvable structure no fetch works.** Once the group the codes cannot separate is smaller than `top_k`, the true top ten span groups, the distances between groups differ in the fourth decimal, and nothing reaches them. At 5,000 clusters over 25,000 records, being five records to a cluster, recall at 10 reaches 0.917 at a fetch of half the corpus, and uniform points on the sphere reach 0.859. Measure recall on your own data before you rely on quantization.
|
|
1024
1055
|
|
|
@@ -1136,7 +1167,7 @@ print("saved:", sorted(os.listdir("my_index.zdb")))
|
|
|
1136
1167
|
|
|
1137
1168
|
*Output, with the progress lines omitted*
|
|
1138
1169
|
```
|
|
1139
|
-
saved: ['config.json', 'hnsw_index.
|
|
1170
|
+
saved: ['config.json', 'hnsw_index.zdbgraph', 'manifest.json', 'mappings.bin', 'metadata.json', 'vectors.bin']
|
|
1140
1171
|
```
|
|
1141
1172
|
|
|
1142
1173
|
<br />
|
|
@@ -1206,7 +1237,7 @@ print("saved:", sorted(os.listdir("quantized_index.zdb")))
|
|
|
1206
1237
|
quantization active: True
|
|
1207
1238
|
quantization active after load: True
|
|
1208
1239
|
storage mode after load: quantized_active
|
|
1209
|
-
saved: ['config.json', 'hnsw_index.
|
|
1240
|
+
saved: ['config.json', 'hnsw_index.zdbgraph', 'manifest.json', 'mappings.bin', 'metadata.json', 'pq_centroids.bin', 'pq_codes.bin', 'quantization.json', 'vectors.bin']
|
|
1210
1241
|
```
|
|
1211
1242
|
|
|
1212
1243
|
<br/>
|
|
@@ -1224,11 +1255,12 @@ my_index.zdb/
|
|
|
1224
1255
|
├── quantization.json # PQ configuration (if enabled)
|
|
1225
1256
|
├── pq_centroids.bin # Trained centroids (if PQ trained)
|
|
1226
1257
|
├── pq_codes.bin # Quantized codes (if PQ active)
|
|
1227
|
-
|
|
1228
|
-
└── hnsw_index.hnsw.data # HNSW graph payload
|
|
1258
|
+
└── hnsw_index.zdbgraph # HNSW graph structure and payload
|
|
1229
1259
|
```
|
|
1230
1260
|
|
|
1231
|
-
`manifest.json` lists
|
|
1261
|
+
`manifest.json` lists the graph file under `files_included`. The load path restores the saved graph rather than rebuilding it, so the file is required to reopen a directory holding records.
|
|
1262
|
+
|
|
1263
|
+
A directory saved by 0.6.0 or earlier holds `hnsw_index.hnsw.graph` and `hnsw_index.hnsw.data` instead. Opening it still works: the graph is rebuilt once from the stored records, and the next `.save()` writes the single file.
|
|
1232
1264
|
|
|
1233
1265
|
<br/>
|
|
1234
1266
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<td><strong>Meta</strong></td>
|
|
15
15
|
<td>
|
|
16
16
|
<a href="https://pypi.org/project/zeusdb-vector-database/"><img src="https://img.shields.io/pypi/v/zeusdb-vector-database?label=PyPI&color=blue"></a>
|
|
17
|
-
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%7C3.11%7C3.12%7C3.13-blue?logo=python&logoColor=ffdd54"></a>
|
|
17
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%7C3.11%7C3.12%7C3.13%7C3.14-blue?logo=python&logoColor=ffdd54"></a>
|
|
18
18
|
<a href="https://github.com/zeusdb/zeusdb-vector-database/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg"></a>
|
|
19
19
|
<a href="https://www.rust-lang.org"><img src="https://img.shields.io/badge/Powered%20by-Rust-black?logo=rust&logoColor=white" alt="Powered by Rust"></a>
|
|
20
20
|
<a href="https://github.com/ZeusDB"><img src="https://github.com/user-attachments/assets/e140d900-1160-4eaa-85c0-2b3507a5f0f5" alt="ZeusDB"></a>
|
|
@@ -191,7 +191,7 @@ HNSWIndex(dim=8, space=cosine, m=16, ef_construction=200, expected_size=5, vecto
|
|
|
191
191
|
| `dim` | `int` | `1536` | Dimensionality of the vectors to be indexed. Each vector must have this length. Must be positive. The default of 1536 matches the output dimensionality of OpenAI's `text-embedding-3-small` and `text-embedding-ada-002` models. |
|
|
192
192
|
| `space` | `str` | `"cosine"`| Distance metric used for similarity search. One of `"cosine"`, `"l1"`, `"l2"`. Case-insensitive. |
|
|
193
193
|
| `m` | `int` | `16` or `32`, see below | Number of bi-directional connections created for each new node, from 2 to 256. Higher `m` improves recall but increases index size and build time. |
|
|
194
|
-
| `ef_construction`| `int` | `200` |
|
|
194
|
+
| `ef_construction`| `int` | `200` | Width of the candidate search each insertion runs. Must be positive. It costs build time and buys graph quality, and it changes neither search latency nor the size of the finished index. See below. |
|
|
195
195
|
| `expected_size` | `int` | `10000` | Estimated number of records to be inserted, from 1 to 100,000,000. Used for preallocating internal data structures and for choosing the default `m`. Not a hard limit, see below. |
|
|
196
196
|
| `quantization_config` | `dict` | `None` | Product Quantization configuration for memory-efficient vector compression. See [Product Quantization](#️-product-quantization). |
|
|
197
197
|
|
|
@@ -208,6 +208,24 @@ The upper bound of 100,000,000 exists because the graph reserves one slot per de
|
|
|
208
208
|
|
|
209
209
|
**`m` starts at 2, not 1.** Layer assignment samples from a scale of `1 / ln(m)`, which is infinity at `m` of 1, so every point is redispatched uniformly across all 16 layers rather than following the exponential distribution the graph depends on. On 3,000 records of 32 dimensions, recall at 10 measured 0.0220 at `m` of 1 against 0.6880 at 2 and 1.0000 at 16.
|
|
210
210
|
|
|
211
|
+
**`ef_construction` governs insertion and nothing else.** It is the width of the candidate search each insertion runs, at the new point's own layer and at every layer below it. The descent above that layer runs at a width of 1 and ignores it. The candidates that search returns are the pool the neighbour selection draws from, so `ef_construction` sets the supply and `m` sets how much of it is kept.
|
|
212
|
+
|
|
213
|
+
**It does not change search latency or index size.** A node holds at most `2 × m` neighbours at layer zero and `m` above it whatever width found them, so the finished graph is the same size at every `ef_construction`. Measured over 50,000 records, mean query latency moved between 0.89 ms and 2.85 ms across a sixteenfold range of `ef_construction` with no trend in it, and resident memory ran backwards on one of the three datasets. What it does change is build time, which is linear in it above 100. On OpenAI embeddings of 1,536 dimensions, 50,000 records built in 62.7 s, 131.2 s, 262.8 s and 464.9 s at `ef_construction` 100, 200, 400 and 800. On GloVe of 100 dimensions the same four are 37.7 s, 75.0 s, 140.4 s and 239.1 s. Below 100 the curve flattens, because the fixed per-insert work stops being dominated by the candidate search.
|
|
214
|
+
|
|
215
|
+
**Recall stops improving at or near the default.** Recall at 10 over 50,000 records at the default `m` of 32, 500 queries, at the default search width:
|
|
216
|
+
|
|
217
|
+
| `ef_construction` | GloVe 100d | SIFT 128d | OpenAI 1536d |
|
|
218
|
+
|-------------------|------------|-----------|--------------|
|
|
219
|
+
| 50 | 0.9176 | 0.9982 | 0.9852 |
|
|
220
|
+
| 100 | 0.9400 | 0.9992 | 0.9912 |
|
|
221
|
+
| 200 | 0.9606 | 0.9996 | 0.9962 |
|
|
222
|
+
| 400 | 0.9684 | 0.9998 | 0.9972 |
|
|
223
|
+
| 800 | 0.9714 | 0.9996 | 0.9978 |
|
|
224
|
+
|
|
225
|
+
SIFT plateaus at 100 and OpenAI at 200. GloVe is the one dataset that keeps climbing, and there the cheaper move is the search width rather than the build. At `ef_construction` 200 GloVe returns 0.9606 at the default width and 0.9984 at `ef_search=500`, so widening the search buys 0.0378 on the graph already built where doubling `ef_construction` buys 0.0078 for 1.9 times the build time. Raise `ef_search` before raising this.
|
|
226
|
+
|
|
227
|
+
**Keep `ef_construction` above `2 × m`.** The neighbour selection heuristic runs only when the candidate list is longer than the neighbour budget, which is `2 × m` at layer zero, and the candidate list is exactly `ef_construction` long on any index holding more records than that. At or below the budget the graph keeps every candidate the search returned, in distance order, and prunes none of them. `create()` warns when the pair reaches that point. `m` is capped at 256, so `m=100` with the default `ef_construction` is enough to switch the heuristic off. The defaults are clear of it, 200 against a budget of 32 at `m` 16 and 64 at `m` 32.
|
|
228
|
+
|
|
211
229
|
<br/>
|
|
212
230
|
|
|
213
231
|
### 2️⃣ Add Data to the Index
|
|
@@ -604,17 +622,21 @@ storage_mode_description: raw_only
|
|
|
604
622
|
|
|
605
623
|
`get_stats()` returns a `str` to `str` map. It also carries `dimension`, `space`, `m`, `ef_construction`, `expected_size`, `index_type`, `raw_vectors_stored`, `quantized_codes_stored` and `storage_mode`, plus training and compression fields once quantization is configured.
|
|
606
624
|
|
|
607
|
-
It is also where the memory figures live, on every index rather than only on a quantized one. `graph_memory_mb` is the HNSW graph, `raw_vectors_memory_mb` is the raw vector store and `total_memory_mb` is the sum of everything below. On a quantized index `quantized_codes_memory_mb` scales with the record count, while `codebook_memory_mb` and `sdc_table_memory_mb` are fixed by `dim`, `subvectors` and `bits` and do not move as records arrive. `raw_vectors_retained` states the mode's policy: `none_once_trained` for `quantized_only`, whose raw vectors are released when training completes, and `all_records` for `quantized_with_raw`.
|
|
625
|
+
It is also where the memory figures live, on every index rather than only on a quantized one. `graph_memory_mb` is the HNSW graph, `raw_vectors_memory_mb` is the raw vector store, `index_bookkeeping_memory_mb` is the hash tables that find a record, and `total_memory_mb` is the sum of everything below. On a quantized index `quantized_codes_memory_mb` scales with the record count, while `codebook_memory_mb` and `sdc_table_memory_mb` are fixed by `dim`, `subvectors` and `bits` and do not move as records arrive. `raw_vectors_retained` states the mode's policy: `none_once_trained` for `quantized_only`, whose raw vectors are released when training completes, and `all_records` for `quantized_with_raw`.
|
|
608
626
|
|
|
609
627
|
**The graph is usually the largest of those figures, and on a memory optimized index it is nearly all of it.** It owns a second copy of every point on top of its neighbour lists, its sixteen per point layer headers and the counters around them. At 50,000 records of `dim=1536` a trained `quantized_only` index reports 98.1 MB of graph against 2.3 MB of codes and 7.5 MB of fixed tables.
|
|
610
628
|
|
|
611
|
-
|
|
629
|
+
`index_bookkeeping_memory_mb` is what the index spends on finding a record rather than on holding one. Five hash tables carry a record, being `id_map`, `rev_map`, the metadata map, the raw vector store and the code store, and two of them carry a second copy of its id. It is proportional to the record count and independent of the dimension, at 265 bytes per record with no quantization, 334 under `quantized_with_raw` and 265 under `quantized_only` at 50,000 records.
|
|
630
|
+
|
|
631
|
+
**`total_memory_mb` is not the resident set and it does not claim to be.** It prices the six structures above, meaning the bytes the index asked the allocator for. What it leaves out is the allocator itself, being its own block headers, its rounding and its fragmentation, and that is a property of the platform rather than something the index holds. Measured on three loaded indexes of 50,000 real 1,536-dimensional embeddings, one process each, with the interpreter's own resident set subtracted:
|
|
612
632
|
|
|
613
|
-
| storage mode | `total_memory_mb` | resident |
|
|
614
|
-
|
|
615
|
-
| no quantization |
|
|
616
|
-
| `quantized_with_raw` |
|
|
617
|
-
| `quantized_only` |
|
|
633
|
+
| storage mode | `total_memory_mb` | resident | share reported | unpriced |
|
|
634
|
+
|---|---:|---:|---:|---:|
|
|
635
|
+
| no quantization | 705.1 MB | 805.4 MiB | 0.88 | 2,103 B per record |
|
|
636
|
+
| `quantized_with_raw` | 415.9 MB | 473.3 MiB | 0.88 | 1,202 B per record |
|
|
637
|
+
| `quantized_only` | 119.7 MB | 181.0 MiB | 0.66 | 1,285 B per record |
|
|
638
|
+
|
|
639
|
+
The unpriced share is almost all allocator overhead on the graph, which asks for six small blocks per point. It runs 1.25 times the graph figure with no quantization, where the per point data block is 6,144 bytes and rounding is a small share of it, and 1.63 and 1.67 times under the two quantized modes, where that block is 48 bytes. That is why `quantized_only` reports the smallest share of its resident set.
|
|
618
640
|
|
|
619
641
|
Size infrastructure from the resident column rather than the reported one. The reported figure is what the index holds; the process holds that plus what the allocator takes to hold it.
|
|
620
642
|
|
|
@@ -991,13 +1013,22 @@ The relation is curved as well as sublinear. On dbpedia-openai the calibration m
|
|
|
991
1013
|
|
|
992
1014
|
What the calibration asks for at 100,000 records, against what the data needs and against the fixed 2%. Every figure is measured on a built index through the ordinary search path, 1,000 queries at `top_k=10` against exact ground truth. The requirement is the smallest fetch on that same index reaching recall at 10 of 0.99, read off a sweep of the explicit `rerank` argument:
|
|
993
1015
|
|
|
994
|
-
| dataset | calibrated fetch | measured requirement |
|
|
995
|
-
|
|
996
|
-
| dbpedia-openai |
|
|
997
|
-
| glove-100 |
|
|
998
|
-
| sift-128 |
|
|
1016
|
+
| dataset | records | calibrated fetch | measured requirement | ratio | recall at 10 | mean query |
|
|
1017
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
1018
|
+
| dbpedia-openai | 50,000 | 554 | 860 | 0.64 | 0.9883 | 3.33 ms |
|
|
1019
|
+
| glove-100 | 50,000 | 6,534 | 3,060 | 2.14 | 0.9982 | 35.79 ms |
|
|
1020
|
+
| sift-128 | 50,000 | 465 | 370 | 1.26 | 0.9953 | 1.88 ms |
|
|
1021
|
+
| dbpedia-openai | 100,000 | 747 | 620 | 1.20 | 0.9907 | 5.02 ms |
|
|
1022
|
+
| glove-100 | 100,000 | 11,439 | 4,620 | 2.48 | 0.9985 | 140.08 ms |
|
|
1023
|
+
| sift-128 | 100,000 | 656 | 450 | 1.46 | 0.9968 | 3.19 ms |
|
|
1024
|
+
|
|
1025
|
+
**The margin is not the same on the three, and glove-100 carries most of it.** The rule is one measurement per index scaled by two constants that are the same everywhere, being a safety factor of 1.75 and a bias of 0.15 on the fitted exponent. Taking the fitted exponent alone with neither constant, the extrapolation to 100,000 records asks for 4,626 candidates on glove-100 against a requirement of 4,620, which is 1.001 times what that index needs. On the other two it asks for 0.49 and 0.59 times what they need. The constants exist for those two, and on glove-100 they are margin over a fetch that was already right.
|
|
999
1026
|
|
|
1000
|
-
The
|
|
1027
|
+
Neither constant can come down without costing recall elsewhere. The constant that would bring glove-100 to the 1.20 ratio dbpedia-openai has at 100,000 records takes the dbpedia-openai fetch from 747 to 359, and a sweep of that same index reads recall 0.9787 at 360 candidates. dbpedia-openai also sits under 0.99 at 10,000, 25,000 and 50,000 records, at 0.9878, 0.9800 and 0.9883, so it has nothing to give back. What separates the datasets is the ratio between the requirement on a built index and the depth the calibration measures in the code ordering over its training sample, and the training sample cannot measure that ratio because the graph does not exist yet when it runs.
|
|
1028
|
+
|
|
1029
|
+
**If query time on glove-like data matters more to you than the last thousandth of recall, name `rerank` yourself.** On glove-100 at 50,000 records a fetch of 3,060 reads recall 0.9900 at 18.14 ms against 0.9982 at 35.79 ms, so half the query time buys back 0.008 of recall.
|
|
1030
|
+
|
|
1031
|
+
The two tables below carry fetch figures measured before the codebook seeding this release ships, which moved the calibrated fetch upward on glove-100. Read their ratios as the shape rather than as current figures.
|
|
1001
1032
|
|
|
1002
1033
|
**On data with no resolvable structure no fetch works.** Once the group the codes cannot separate is smaller than `top_k`, the true top ten span groups, the distances between groups differ in the fourth decimal, and nothing reaches them. At 5,000 clusters over 25,000 records, being five records to a cluster, recall at 10 reaches 0.917 at a fetch of half the corpus, and uniform points on the sphere reach 0.859. Measure recall on your own data before you rely on quantization.
|
|
1003
1034
|
|
|
@@ -1115,7 +1146,7 @@ print("saved:", sorted(os.listdir("my_index.zdb")))
|
|
|
1115
1146
|
|
|
1116
1147
|
*Output, with the progress lines omitted*
|
|
1117
1148
|
```
|
|
1118
|
-
saved: ['config.json', 'hnsw_index.
|
|
1149
|
+
saved: ['config.json', 'hnsw_index.zdbgraph', 'manifest.json', 'mappings.bin', 'metadata.json', 'vectors.bin']
|
|
1119
1150
|
```
|
|
1120
1151
|
|
|
1121
1152
|
<br />
|
|
@@ -1185,7 +1216,7 @@ print("saved:", sorted(os.listdir("quantized_index.zdb")))
|
|
|
1185
1216
|
quantization active: True
|
|
1186
1217
|
quantization active after load: True
|
|
1187
1218
|
storage mode after load: quantized_active
|
|
1188
|
-
saved: ['config.json', 'hnsw_index.
|
|
1219
|
+
saved: ['config.json', 'hnsw_index.zdbgraph', 'manifest.json', 'mappings.bin', 'metadata.json', 'pq_centroids.bin', 'pq_codes.bin', 'quantization.json', 'vectors.bin']
|
|
1189
1220
|
```
|
|
1190
1221
|
|
|
1191
1222
|
<br/>
|
|
@@ -1203,11 +1234,12 @@ my_index.zdb/
|
|
|
1203
1234
|
├── quantization.json # PQ configuration (if enabled)
|
|
1204
1235
|
├── pq_centroids.bin # Trained centroids (if PQ trained)
|
|
1205
1236
|
├── pq_codes.bin # Quantized codes (if PQ active)
|
|
1206
|
-
|
|
1207
|
-
└── hnsw_index.hnsw.data # HNSW graph payload
|
|
1237
|
+
└── hnsw_index.zdbgraph # HNSW graph structure and payload
|
|
1208
1238
|
```
|
|
1209
1239
|
|
|
1210
|
-
`manifest.json` lists
|
|
1240
|
+
`manifest.json` lists the graph file under `files_included`. The load path restores the saved graph rather than rebuilding it, so the file is required to reopen a directory holding records.
|
|
1241
|
+
|
|
1242
|
+
A directory saved by 0.6.0 or earlier holds `hnsw_index.hnsw.graph` and `hnsw_index.hnsw.data` instead. Opening it still works: the graph is rebuilt once from the stored records, and the next `.save()` writes the single file.
|
|
1211
1243
|
|
|
1212
1244
|
<br/>
|
|
1213
1245
|
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "zeusdb-vector-database"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Blazing-fast vector DB with real-time similarity search and metadata filtering."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "ZeusDB", email = "contact@zeusdb.com" }
|
|
8
8
|
]
|
|
9
|
+
# The floor and the abi3 target move together. vdb-core/Cargo.toml enables
|
|
10
|
+
# pyo3/abi3-py310, so the wheel is tagged cp310-abi3 and pip on 3.10 selects
|
|
11
|
+
# it. Raising one without the other leaves a version with no wheel that pip
|
|
12
|
+
# can match, which falls back to building the sdist from source.
|
|
9
13
|
requires-python = ">=3.10"
|
|
10
14
|
license = "Apache-2.0"
|
|
15
|
+
# CPython only, and now literally so. A stable ABI wheel cannot be loaded by
|
|
16
|
+
# PyPy or by a free-threaded build, so no wheel is produced for either.
|
|
11
17
|
classifiers = [
|
|
12
18
|
"Programming Language :: Rust",
|
|
13
19
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
@@ -21,20 +27,20 @@ requires = ["maturin>=1.9.1,<2.0"]
|
|
|
21
27
|
build-backend = "maturin"
|
|
22
28
|
|
|
23
29
|
[tool.maturin]
|
|
30
|
+
# abi3-py310 is deliberately not listed here. maturin has no abi3 key and
|
|
31
|
+
# reads the target from the pyo3 cargo feature, which is declared in
|
|
32
|
+
# vdb-core/Cargo.toml instead, so that cargo build, cargo clippy and cargo
|
|
33
|
+
# test compile the configuration the wheel ships rather than a different one.
|
|
24
34
|
features = ["pyo3/extension-module"]
|
|
25
35
|
python-source = "src"
|
|
26
36
|
manifest-path = "vdb-core/Cargo.toml"
|
|
27
37
|
module-name = "zeusdb_vector_database" # <-- matches Rust crate name
|
|
28
38
|
bindings = "pyo3" # <-- explicitly state to maturin that it's a PyO3 project
|
|
29
|
-
# vendor/** must ship in the sdist, or an sdist build cannot resolve the
|
|
30
|
-
# patched hnsw_rs path dependency declared in vdb-core/Cargo.toml.
|
|
31
39
|
include = [
|
|
32
40
|
"LICENSE",
|
|
33
41
|
"NOTICE",
|
|
34
42
|
"README.md",
|
|
35
43
|
"src/**",
|
|
36
|
-
{ path = "vendor/**/*", format = "sdist" },
|
|
37
|
-
{ path = "LICENSES/*", format = "sdist" },
|
|
38
44
|
]
|
|
39
45
|
|
|
40
46
|
[project.optional-dependencies]
|
|
@@ -69,6 +69,71 @@ class VectorDatabase:
|
|
|
69
69
|
return 16
|
|
70
70
|
return 32 if expected_size > 25_000 else 16
|
|
71
71
|
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _warn_if_selection_disabled(m: Any, ef_construction: Any) -> None:
|
|
74
|
+
"""Warn where ef_construction switches the neighbour selection heuristic off.
|
|
75
|
+
|
|
76
|
+
`select_neighbours` in the vendored crate opens with
|
|
77
|
+
`if candidates.len() <= nb_neighbours_asked`, and with
|
|
78
|
+
`extend_candidates` false it copies every candidate into the neighbour
|
|
79
|
+
list and returns from inside that branch. The budget is `2 * m` at
|
|
80
|
+
layer zero and `m` above it, both set at `hnsw.rs:1270` to `:1278`.
|
|
81
|
+
|
|
82
|
+
The candidate list is exactly `ef_construction` long once the index
|
|
83
|
+
holds more records than that, which is a property of `search_layer`
|
|
84
|
+
rather than an assumption. Insertion passes it no filter, so every
|
|
85
|
+
point it pushes into the candidate heap it also pushes into the result
|
|
86
|
+
heap, and the result heap is trimmed only above `ef`. While the result
|
|
87
|
+
heap is short it therefore holds every visited point, which makes the
|
|
88
|
+
nearest unexplored candidate a member of it and leaves the early
|
|
89
|
+
return unable to fire. The traversal
|
|
90
|
+
therefore stops either at `ef` results or with the whole reachable
|
|
91
|
+
component visited. Measured at m 8 over 2,000 records, mean layer zero
|
|
92
|
+
out-degree is 16.000 at ef_construction 16 with every node at the cap,
|
|
93
|
+
and 7.170 at ef_construction 17 with 6.5 percent of nodes at it, so the
|
|
94
|
+
flip lands exactly on `2 * m` and needs no margin. That measurement is
|
|
95
|
+
`neighbour_selection_threshold_is_twice_m` in the Rust guard tests.
|
|
96
|
+
|
|
97
|
+
The threshold is therefore `ef_construction <= 2 * m` and the warning
|
|
98
|
+
carries no slack. Slack would claim something the source does not say
|
|
99
|
+
and would fire on m 64 with the default ef_construction, which measured
|
|
100
|
+
higher recall on real embeddings than the default m of 32 does.
|
|
101
|
+
|
|
102
|
+
A pair the Rust layer is going to reject returns without warning, so
|
|
103
|
+
that an invalid m or ef_construction produces its real validation error
|
|
104
|
+
and nothing else. That covers a non-integer of either, an
|
|
105
|
+
ef_construction below 1, and an m outside the 2 to 256 the graph
|
|
106
|
+
accepts.
|
|
107
|
+
"""
|
|
108
|
+
integral = (
|
|
109
|
+
isinstance(m, int) and not isinstance(m, bool)
|
|
110
|
+
and isinstance(ef_construction, int)
|
|
111
|
+
and not isinstance(ef_construction, bool)
|
|
112
|
+
)
|
|
113
|
+
if not integral or ef_construction < 1 or not 2 <= m <= 256:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
budget = 2 * m
|
|
117
|
+
if ef_construction > budget:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# The largest m that leaves the heuristic running at this
|
|
121
|
+
# ef_construction. Below the floor of 2 there is no such m, so the
|
|
122
|
+
# message offers the one remedy that exists.
|
|
123
|
+
largest_m = (ef_construction - 1) // 2
|
|
124
|
+
remedy = f"Raise ef_construction above {budget}"
|
|
125
|
+
if largest_m >= 2:
|
|
126
|
+
remedy += f", or lower m to {largest_m} or below"
|
|
127
|
+
|
|
128
|
+
import warnings
|
|
129
|
+
warnings.warn(
|
|
130
|
+
f"ef_construction={ef_construction} is not greater than 2*m={budget}, "
|
|
131
|
+
f"so the neighbour selection heuristic does not run. Layer zero "
|
|
132
|
+
f"insertion keeps every candidate the construction search returns, in "
|
|
133
|
+
f"distance order, and prunes none of them. {remedy}, to run it.",
|
|
134
|
+
UserWarning,
|
|
135
|
+
stacklevel=3
|
|
136
|
+
)
|
|
72
137
|
|
|
73
138
|
def __init__(self):
|
|
74
139
|
"""Initialize the vector database factory."""
|
|
@@ -93,7 +158,17 @@ class VectorDatabase:
|
|
|
93
158
|
expected_size honestly or set m directly. The floor is 2
|
|
94
159
|
because at 1 the layer scale is infinite and the graph is
|
|
95
160
|
degenerate.
|
|
96
|
-
- ef_construction (int):
|
|
161
|
+
- ef_construction (int): Width of the candidate search each
|
|
162
|
+
insertion runs (default: 200). It sets the supply the
|
|
163
|
+
neighbour selection draws from, and m sets how much of it is
|
|
164
|
+
kept, so it changes neither search latency nor the size of
|
|
165
|
+
the finished index. Build time is linear in it above 100: at
|
|
166
|
+
50,000 records of 1,536 dimensions, 62.7s, 131.2s, 262.8s and
|
|
167
|
+
464.9s at 100, 200, 400 and 800. Recall at 10 plateaus at 100
|
|
168
|
+
on SIFT and at 200 on OpenAI embeddings, and on GloVe raising
|
|
169
|
+
ef_search on the graph already built buys more than raising
|
|
170
|
+
this. The neighbour selection heuristic runs only while this
|
|
171
|
+
is above 2 * m, and create() warns where it is not.
|
|
97
172
|
- expected_size (int): Expected number of vectors (default: 10000,
|
|
98
173
|
max: 100,000,000). Also selects the default m, see above. It is
|
|
99
174
|
a capacity hint rather than a limit, and an index that grows
|
|
@@ -121,10 +196,11 @@ class VectorDatabase:
|
|
|
121
196
|
Under 'quantized_only' the records collected for training are held at
|
|
122
197
|
full width only until training completes, then released. get_stats()
|
|
123
198
|
reports the codes, the raw vectors, the codebook, the centroid
|
|
124
|
-
distance table
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
199
|
+
distance table, the graph and the hash tables that find a record,
|
|
200
|
+
and sums them under total_memory_mb. That sum is what the index
|
|
201
|
+
asked the allocator for rather than what the process holds, since
|
|
202
|
+
the allocator's own headers and its rounding sit outside it at
|
|
203
|
+
between 1,200 and 2,100 bytes a record. Recall falls sharply,
|
|
128
204
|
not slightly. Only 'quantized_with_raw' can recover it, by reranking
|
|
129
205
|
against the raw vectors it keeps. Training triggers automatically on
|
|
130
206
|
the first .add() call that reaches the training_size threshold, and
|
|
@@ -195,6 +271,10 @@ class VectorDatabase:
|
|
|
195
271
|
kwargs.setdefault("expected_size", 10000)
|
|
196
272
|
# After expected_size, since the graph degree is derived from it.
|
|
197
273
|
kwargs.setdefault("m", self._default_m(kwargs["expected_size"]))
|
|
274
|
+
# After both, since the pair is what decides it. No default
|
|
275
|
+
# combination reaches it: the default m is 16 or 32, so the budget
|
|
276
|
+
# is 32 or 64, and the default ef_construction of 200 clears both.
|
|
277
|
+
self._warn_if_selection_disabled(kwargs["m"], kwargs["ef_construction"])
|
|
198
278
|
|
|
199
279
|
# Validate and process quantization config
|
|
200
280
|
if quantization_config is not None:
|
|
@@ -359,7 +439,8 @@ class VectorDatabase:
|
|
|
359
439
|
"holds 0.69 times an unquantized index at 10,000 records and 0.59 "
|
|
360
440
|
"times at 100,000. "
|
|
361
441
|
"get_stats() reports the memory the codes, the raw vectors, the codebook, "
|
|
362
|
-
"the centroid distance table
|
|
442
|
+
"the centroid distance table, the graph and the index bookkeeping hold "
|
|
443
|
+
"once records are loaded, "
|
|
363
444
|
"and sums them under total_memory_mb. This mode is required for rerank "
|
|
364
445
|
"and for exact vector reconstruction.",
|
|
365
446
|
UserWarning,
|