epzero 0.3.0__tar.gz → 0.4.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.
- epzero-0.4.0/CHANGELOG.md +54 -0
- {epzero-0.3.0 → epzero-0.4.0}/CMakeLists.txt +2 -1
- {epzero-0.3.0 → epzero-0.4.0}/PKG-INFO +46 -7
- {epzero-0.3.0 → epzero-0.4.0}/bench/bench_recall.cpp +32 -3
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/make_seeds.cpp +17 -0
- epzero-0.4.0/include/epzero/auto.h +70 -0
- {epzero-0.3.0 → epzero-0.4.0}/include/epzero/database.h +1 -0
- {epzero-0.3.0 → epzero-0.4.0}/include/epzero/distance.h +5 -0
- {epzero-0.3.0 → epzero-0.4.0}/include/epzero/hnsw.h +66 -0
- {epzero-0.3.0 → epzero-0.4.0}/pyproject.toml +5 -2
- {epzero-0.3.0 → epzero-0.4.0}/python/README.md +44 -5
- {epzero-0.3.0 → epzero-0.4.0}/python/epzero/__init__.py +13 -5
- epzero-0.4.0/python/epzero/auto.py +146 -0
- {epzero-0.3.0 → epzero-0.4.0}/python/epzero/langchain.py +19 -1
- {epzero-0.3.0 → epzero-0.4.0}/python/epzero_py.cpp +198 -22
- epzero-0.4.0/python/tests/test_auto.py +162 -0
- epzero-0.4.0/src/auto.cpp +269 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/database.cpp +33 -8
- {epzero-0.3.0 → epzero-0.4.0}/src/distance.cpp +51 -9
- {epzero-0.3.0 → epzero-0.4.0}/src/distance_avx2.cpp +24 -1
- {epzero-0.3.0 → epzero-0.4.0}/src/distance_avxvnni.cpp +24 -1
- {epzero-0.3.0 → epzero-0.4.0}/src/hnsw.cpp +351 -44
- epzero-0.4.0/src/pca.h +206 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/recall.cpp +8 -2
- epzero-0.4.0/tests/auto_test.cpp +216 -0
- {epzero-0.3.0 → epzero-0.4.0}/tests/database_test.cpp +31 -0
- {epzero-0.3.0 → epzero-0.4.0}/tests/hnsw_test.cpp +135 -2
- {epzero-0.3.0 → epzero-0.4.0}/bench/bench_adaptive.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/bench/bench_churn.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/bench/bench_distance.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/bench/bench_filter.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/bench/bench_guard.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/common.h +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/fuzz_graph.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/fuzz_snapshot.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/fuzz_text.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/fuzz_wal.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/regressions/graph-empty-with-entry-point +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/regressions/graph-entry-level-overflow +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/regressions/graph-upper-link-to-base-node +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/regressions/snapshot-empty-with-entry-point +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/fuzz/regressions/snapshot-huge-capacity +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/include/epzero/brute_force.h +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/include/epzero/dataset.h +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/include/epzero/parallel.h +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/include/epzero/recall.h +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/include/epzero/text.h +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/python/tests/test_epzero.py +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/python/tests/test_langchain.py +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/server/node_main.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/brute_force.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/dataset.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/distance_avx512.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/distance_avx512vnni.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/distance_neon.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/distance_scalar.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/distance_wasm.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/kernels.h +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/rabitq.h +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/text.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/src/visited_pool.h +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/tests/dataset_test.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/tests/distance_test.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/tests/recall_test.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/tests/text_test.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/tools/export_graph.cpp +0 -0
- {epzero-0.3.0 → epzero-0.4.0}/wasm/epzero_wasm.cpp +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
**Automatic configuration.** `epzero.build(vectors, metric=...)` (C++: `epzero::build_auto` in
|
|
6
|
+
`epzero/auto.h`) configures an index from its data and the machine it is built on:
|
|
7
|
+
|
|
8
|
+
- From the data: 8-bit codes, whether to walk the graph on principal directions (at 256 dimensions or
|
|
9
|
+
more, when few directions hold most of the variance), the build passes, the re-rank, and the beam
|
|
10
|
+
width (`ef`) that reaches a recall target, calibrated on rows held out of the graph or on queries
|
|
11
|
+
you pass (`queries=`).
|
|
12
|
+
- From the machine: the prefetch setting, timed on this CPU, and every core.
|
|
13
|
+
- Measured on an Intel Xeon Gold 6226 and an AMD Threadripper PRO 9985WX: as fast as hand-tuned
|
|
14
|
+
settings (within 1-5%), recall 0.949-0.957 on real queries for a 0.95 target, and 1.3-4.1× Glass's
|
|
15
|
+
queries per second on GIST1M, OpenAI DBpedia and DEEP10M.
|
|
16
|
+
- Every setting can be fixed instead (`epzero.build(vectors, nav_dim=0, M=32)`, or `epzero.plan()`,
|
|
17
|
+
edit, build). `index.plan` shows each setting and why it was chosen.
|
|
18
|
+
- The choices are saved inside the index (`IndexProfile`, file format v4), and `search()` uses them
|
|
19
|
+
when called without settings. A loaded index re-applies its prefetch setting only on the same kind
|
|
20
|
+
of CPU (`epzero.machine()` names it).
|
|
21
|
+
|
|
22
|
+
**Faster search and builds.**
|
|
23
|
+
|
|
24
|
+
- `nav_dim`: walk the graph on 8-bit codes of principal directions, re-ranking with the full vectors.
|
|
25
|
+
With the other changes here, 2.8-4.2× Glass's queries per second on GIST1M and 1.6-2.7× on OpenAI
|
|
26
|
+
DBpedia embeddings at equal recall, on Intel and AMD servers.
|
|
27
|
+
- `refine_links()`: a second build pass in which every node searches the finished graph and
|
|
28
|
+
reconnects; `fill_free_links()` gives free link slots to two-hop neighbours.
|
|
29
|
+
- `fast_build`: build on the 8-bit codes.
|
|
30
|
+
- `rerank_z`: stop the re-rank early once the remaining candidates can't reach the top k.
|
|
31
|
+
- Queries are projected onto principal directions on the integer kernel; the AVX2 and AVX-VNNI integer
|
|
32
|
+
kernels finish with vector loads instead of a scalar tail.
|
|
33
|
+
- The prefetch tuner can fetch a record's every cache line; on a Xeon Gold 6226 that was worth up to
|
|
34
|
+
1.7× on some datasets.
|
|
35
|
+
- PCA runs as blocked matrix products (choosing directions 5× faster on GIST1M), training runs on
|
|
36
|
+
every core, and index memory is zeroed on every core, which spreads it across NUMA nodes.
|
|
37
|
+
|
|
38
|
+
**Fixes.**
|
|
39
|
+
|
|
40
|
+
- `Database`: a write that could not be applied (the database full, or not yet trained) was written to
|
|
41
|
+
the log first, after which every open failed replaying it. Writes are now checked before they are
|
|
42
|
+
logged, and replacing a vector in a full database frees its slot before compacting.
|
|
43
|
+
- `RecallGuard` searched, and measured its reference, with the default re-rank depth; it now uses the
|
|
44
|
+
index's own settings and re-ranks the reference's whole beam. `explain()` and `trace()` likewise.
|
|
45
|
+
- The LangChain store copied every stored vector on each add (quadratic in the store's size).
|
|
46
|
+
- `pip install epzero` from source failed on machines with gRPC installed: it tried to build the
|
|
47
|
+
cluster node, whose protocol files aren't in the package.
|
|
48
|
+
|
|
49
|
+
**Compatibility.** Indexes without a profile are still written in the format older versions read
|
|
50
|
+
(v2, or v3 with principal directions); automatically configured ones are v4, which 0.3 cannot load.
|
|
51
|
+
`search()`'s `k`, `ef`, `rerank` and `rerank_z` now default to the index's profile, which for an index
|
|
52
|
+
built by hand is the old defaults (10, 64, 2 × k, off).
|
|
53
|
+
|
|
54
|
+
Earlier releases are described by their git tags (`v0.2.0`, `v0.3.0`).
|
|
@@ -48,6 +48,7 @@ endif()
|
|
|
48
48
|
find_package(Threads REQUIRED)
|
|
49
49
|
|
|
50
50
|
add_library(epzero_engine
|
|
51
|
+
src/auto.cpp
|
|
51
52
|
src/distance.cpp
|
|
52
53
|
src/distance_scalar.cpp
|
|
53
54
|
src/hnsw.cpp
|
|
@@ -118,7 +119,7 @@ if(EPZERO_BUILD_TESTS)
|
|
|
118
119
|
FetchContent_MakeAvailable(googletest)
|
|
119
120
|
|
|
120
121
|
enable_testing()
|
|
121
|
-
add_executable(epzero_tests tests/distance_test.cpp tests/hnsw_test.cpp tests/recall_test.cpp tests/dataset_test.cpp tests/database_test.cpp tests/text_test.cpp)
|
|
122
|
+
add_executable(epzero_tests tests/auto_test.cpp tests/distance_test.cpp tests/hnsw_test.cpp tests/recall_test.cpp tests/dataset_test.cpp tests/database_test.cpp tests/text_test.cpp)
|
|
122
123
|
target_link_libraries(epzero_tests PRIVATE epzero_engine GTest::gtest_main)
|
|
123
124
|
if(EMSCRIPTEN)
|
|
124
125
|
# Runs under Node (CMAKE_CROSSCOMPILING_EMULATOR); workers are pre-spawned for the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: epzero
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A fast HNSW vector database in C++20 that configures itself: SIMD search, filtered search, recall monitoring, crash-safe storage
|
|
5
5
|
Keywords: vector database,hnsw,ann,nearest neighbor,embeddings,rag,langchain
|
|
6
6
|
Author: Lajat
|
|
7
7
|
License: MIT
|
|
@@ -31,9 +31,14 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
|
|
32
32
|
An HNSW vector database written from scratch in C++20, with Python bindings.
|
|
33
33
|
|
|
34
|
-
- **
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
- **Configures itself:** `epzero.build(vectors)` picks compression, the graph build and the search
|
|
35
|
+
effort for your data and recall target, times memory prefetching on your CPU, saves the choices
|
|
36
|
+
inside the index and tells you why it made each one. Any setting can be overridden.
|
|
37
|
+
- **Fast search:** hand-written SIMD kernels (AVX2, AVX-512, VNNI, NEON) chosen at runtime, 8-bit
|
|
38
|
+
codes, an optional walk on principal directions, and lock-free reads. On dedicated Intel Xeon Gold
|
|
39
|
+
6226 and AMD EPYC 7513 servers, one thread, at equal recall: 1.4-4.2× the queries per second of
|
|
40
|
+
Glass, the fastest library we compared, on GIST1M, OpenAI DBpedia and DEEP10M, and 1.0-1.3× on
|
|
41
|
+
GloVe-100 (with each dataset's best settings; see the project's scoreboard for every loss).
|
|
37
42
|
- **Filtered search that picks its own strategy:** an exact scan, a filtered graph walk, or a
|
|
38
43
|
two-hop walk (the idea behind ACORN), chosen per query by a cost model.
|
|
39
44
|
- **Recall you can see:** `RecallGuard` measures recall on a sample of live queries with a
|
|
@@ -43,7 +48,8 @@ An HNSW vector database written from scratch in C++20, with Python bindings.
|
|
|
43
48
|
- **Hybrid search:** `TextIndex` (BM25 keyword search) and `hybrid_search()`, which merges it
|
|
44
49
|
with vector search by reciprocal rank fusion.
|
|
45
50
|
- **Durable:** `Database` adds a write-ahead log with crash recovery and checkpoints.
|
|
46
|
-
- **8-bit quantization** with exact re-ranking
|
|
51
|
+
- **8-bit quantization** with exact re-ranking, stopped early per query once the rest can't reach
|
|
52
|
+
the top k.
|
|
47
53
|
- **A LangChain vector store** in `epzero.langchain`.
|
|
48
54
|
|
|
49
55
|
```bash
|
|
@@ -51,12 +57,45 @@ pip install epzero # the engine
|
|
|
51
57
|
pip install "epzero[langchain]" # plus the LangChain vector store
|
|
52
58
|
```
|
|
53
59
|
|
|
54
|
-
## Quick start
|
|
60
|
+
## Quick start: let it configure itself
|
|
55
61
|
|
|
56
62
|
```python
|
|
57
63
|
import numpy as np
|
|
58
64
|
import epzero
|
|
59
65
|
|
|
66
|
+
vectors = np.random.rand(100_000, 768).astype(np.float32)
|
|
67
|
+
index = epzero.build(vectors, metric="cosine") # chooses every setting (below)
|
|
68
|
+
labels, distances = index.search(vectors[:5], k=10) # at the effort calibrated for recall 0.95
|
|
69
|
+
print(index.plan) # what it chose, and why
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`build` decides from the data: 8-bit compression, whether to walk on principal directions (high
|
|
73
|
+
dimensions with a concentrated spectrum), the build passes, the re-rank, and the beam width that
|
|
74
|
+
reaches your recall target, measured on rows held out of the graph. From the machine: the SIMD
|
|
75
|
+
kernel, the prefetch setting (timed on this CPU) and every core. Change anything:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
index = epzero.build(vectors, metric="cosine", recall=0.99) # a different target
|
|
79
|
+
index = epzero.build(vectors, metric="cosine", queries=my_queries) # calibrate on real queries
|
|
80
|
+
index = epzero.build(vectors, metric="cosine", nav_dim=0, refine=False, M=32) # override choices
|
|
81
|
+
plan = epzero.plan(vectors, metric="cosine") # look before building, edit, then pass it in
|
|
82
|
+
index = epzero.build(vectors, plan=plan.replace(ef_construction=400))
|
|
83
|
+
labels, distances = index.search(queries, k=50, ef=200) # per-call overrides
|
|
84
|
+
index.save("my.epz") # the settings and reasons are saved inside it
|
|
85
|
+
index = epzero.Index.load("my.epz") # search() uses them again
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`build` returns an ordinary `epzero.Index`: filters, deletes, `explain()` and `RecallGuard` all use
|
|
89
|
+
its settings. A saved index re-applies its prefetch setting only on the same kind of CPU (call
|
|
90
|
+
`tune_prefetch()` on another). The decisions are made in the C++ engine (`epzero/auto.h`), so C++
|
|
91
|
+
users get the same `build_auto()`.
|
|
92
|
+
|
|
93
|
+
If your queries differ from your data (text queries against image vectors, say), pass a few
|
|
94
|
+
hundred of them as `queries=`: recall calibrated on data rows is optimistic for those.
|
|
95
|
+
|
|
96
|
+
## Configured by hand
|
|
97
|
+
|
|
98
|
+
```python
|
|
60
99
|
vectors = np.random.rand(100_000, 128).astype(np.float32)
|
|
61
100
|
index = epzero.Index(dim=128, metric="l2", max_elements=100_000, M=16, ef_construction=200)
|
|
62
101
|
index.add(vectors) # labels 0..n-1 (or pass labels=)
|
|
@@ -40,6 +40,15 @@ struct Options {
|
|
|
40
40
|
std::size_t repeat = 1; // passes over the query set, so many-thread runs last long enough to time
|
|
41
41
|
Quantization quantization = Quantization::None;
|
|
42
42
|
bool keep_vectors = true;
|
|
43
|
+
std::size_t nav_dim = 0;
|
|
44
|
+
bool fill_links = false;
|
|
45
|
+
bool fast_build = false;
|
|
46
|
+
bool fill_after = false;
|
|
47
|
+
std::size_t refine = 0;
|
|
48
|
+
bool refine_exact = false;
|
|
49
|
+
std::uint64_t seed = 42;
|
|
50
|
+
float rerank_z = 0.0f;
|
|
51
|
+
std::size_t refine_ef = 0;
|
|
43
52
|
std::size_t rerank = 0;
|
|
44
53
|
std::size_t prefetch_depth = 0, prefetch_lines = 0; // 0: the index's default
|
|
45
54
|
std::vector<std::size_t> ef = {10, 20, 40, 80, 160, 320};
|
|
@@ -86,6 +95,15 @@ Options parse(int argc, char** argv) {
|
|
|
86
95
|
}
|
|
87
96
|
else if (arg == "--no-keep") o.keep_vectors = false;
|
|
88
97
|
else if (arg == "--rerank") o.rerank = std::stoul(value());
|
|
98
|
+
else if (arg == "--nav-dim") o.nav_dim = std::stoul(value());
|
|
99
|
+
else if (arg == "--fill-links") o.fill_links = true;
|
|
100
|
+
else if (arg == "--fast-build") o.fast_build = true;
|
|
101
|
+
else if (arg == "--fill-after") o.fill_after = true;
|
|
102
|
+
else if (arg == "--refine") o.refine = std::stoul(value());
|
|
103
|
+
else if (arg == "--refine-exact") o.refine_exact = true;
|
|
104
|
+
else if (arg == "--seed") o.seed = std::stoull(value());
|
|
105
|
+
else if (arg == "--rerank-z") o.rerank_z = std::stof(value());
|
|
106
|
+
else if (arg == "--refine-ef") o.refine_ef = std::stoul(value());
|
|
89
107
|
else if (arg == "--prefetch") { // depth,lines
|
|
90
108
|
const auto v = parse_list(value());
|
|
91
109
|
if (v.size() != 2) throw std::invalid_argument("--prefetch takes depth,lines");
|
|
@@ -171,8 +189,9 @@ int main(int argc, char** argv) try {
|
|
|
171
189
|
std::printf("load %s in %.2fs\n", opt.load.c_str(), seconds_since(t0));
|
|
172
190
|
} else {
|
|
173
191
|
HnswParams params{.dim = dim, .metric = opt.metric, .max_elements = base.n, .M = opt.M,
|
|
174
|
-
.ef_construction = opt.ef_construction, .quantization = opt.quantization,
|
|
175
|
-
.keep_vectors = opt.keep_vectors
|
|
192
|
+
.ef_construction = opt.ef_construction, .seed = opt.seed, .quantization = opt.quantization,
|
|
193
|
+
.keep_vectors = opt.keep_vectors, .nav_dim = opt.nav_dim, .fill_links = opt.fill_links,
|
|
194
|
+
.fast_build = opt.fast_build};
|
|
176
195
|
loaded = std::make_unique<HnswIndex>(params);
|
|
177
196
|
t0 = Clock::now();
|
|
178
197
|
loaded->train(base.data.data(), std::min<std::size_t>(base.n, 100'000)); // no-op unquantized
|
|
@@ -184,6 +203,16 @@ int main(int argc, char** argv) try {
|
|
|
184
203
|
});
|
|
185
204
|
}
|
|
186
205
|
for (auto& b : builders) b.join();
|
|
206
|
+
for (std::size_t pass = 0; pass < opt.refine; ++pass) {
|
|
207
|
+
const double refine_at = seconds_since(t0);
|
|
208
|
+
loaded->refine_links(opt.threads, opt.refine_ef, opt.refine_exact);
|
|
209
|
+
std::printf("refined pass %zu in %.2fs\n", pass + 1, seconds_since(t0) - refine_at);
|
|
210
|
+
}
|
|
211
|
+
if (opt.fill_after) {
|
|
212
|
+
const double links_at = seconds_since(t0);
|
|
213
|
+
const std::size_t added = loaded->fill_free_links(opt.threads);
|
|
214
|
+
std::printf("filled %zu links in %.2fs\n", added, seconds_since(t0) - links_at);
|
|
215
|
+
}
|
|
187
216
|
build_s = seconds_since(t0);
|
|
188
217
|
std::printf("build %.2fs, %.0f inserts/s (%zu threads, M=%zu, efc=%zu)\n", build_s, base.n / build_s,
|
|
189
218
|
opt.threads, opt.M, opt.ef_construction);
|
|
@@ -230,7 +259,7 @@ int main(int argc, char** argv) try {
|
|
|
230
259
|
const std::size_t q = i % queries.n;
|
|
231
260
|
SearchStats stats;
|
|
232
261
|
const auto start = Clock::now();
|
|
233
|
-
auto found = index.search(queries.row(q), opt.k, {.ef = ef, .rerank = opt.rerank, .stats = &stats});
|
|
262
|
+
auto found = index.search(queries.row(q), opt.k, {.ef = ef, .rerank = opt.rerank, .rerank_z = opt.rerank_z, .stats = &stats});
|
|
234
263
|
latencies[i] = std::chrono::duration<double, std::micro>(Clock::now() - start).count();
|
|
235
264
|
distances_per_query[i] = stats.distance_computations;
|
|
236
265
|
full_per_query[i] = stats.full_distance_computations;
|
|
@@ -29,6 +29,23 @@ int main(int argc, char** argv) {
|
|
|
29
29
|
for (std::size_t i = 0; i < 40; ++i) index.add(i, vectors.data() + i * kDim);
|
|
30
30
|
index.save((out / "snapshot" / "valid").string());
|
|
31
31
|
|
|
32
|
+
// Format v4: 8-bit codes and a profile (search settings, reasons, a machine name).
|
|
33
|
+
epzero::HnswParams quantized = small_params();
|
|
34
|
+
quantized.quantization = epzero::Quantization::SQ8;
|
|
35
|
+
epzero::HnswIndex profiled(quantized);
|
|
36
|
+
profiled.train(vectors.data(), 40);
|
|
37
|
+
for (std::size_t i = 0; i < 40; ++i) profiled.add(i, vectors.data() + i * kDim);
|
|
38
|
+
epzero::IndexProfile profile;
|
|
39
|
+
profile.ef = 24;
|
|
40
|
+
profile.rerank = 30;
|
|
41
|
+
profile.rerank_z = 3.0f;
|
|
42
|
+
profile.recall_target = 0.95f;
|
|
43
|
+
profile.refined = true;
|
|
44
|
+
profile.machine = "fuzz/scalar";
|
|
45
|
+
profile.notes = {{"ef", "calibrated"}, {"nav_dim", "off"}};
|
|
46
|
+
profiled.set_profile(profile);
|
|
47
|
+
profiled.save((out / "snapshot" / "valid-profile").string());
|
|
48
|
+
|
|
32
49
|
const auto graph = index.export_graph();
|
|
33
50
|
std::ofstream(out / "graph" / "valid", std::ios::binary)
|
|
34
51
|
.write(reinterpret_cast<const char*>(graph.data()), static_cast<std::streamsize>(graph.size()));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Automatic configuration: an index set up for its data and for the machine it is built on, with
|
|
4
|
+
// every choice overridable. The rules come from measurements in docs/performance.md.
|
|
5
|
+
//
|
|
6
|
+
// epzero::AutoOptions options;
|
|
7
|
+
// options.metric = epzero::Metric::Cosine;
|
|
8
|
+
// options.fixed.nav_dim = 0; // override any choice
|
|
9
|
+
// auto index = epzero::build_auto(data, nullptr, n, dim, options);
|
|
10
|
+
// index->profile(); // what it chose, and why
|
|
11
|
+
//
|
|
12
|
+
// From the data: 8-bit codes, whether to walk on principal directions, the build passes, the
|
|
13
|
+
// re-rank, and the beam width (ef) that reaches the recall target, calibrated on rows held out of
|
|
14
|
+
// the graph (or on queries you pass). From the machine: the prefetch setting, timed here, and every
|
|
15
|
+
// core. All of it is saved with the index; see IndexProfile.
|
|
16
|
+
|
|
17
|
+
#include <cstddef>
|
|
18
|
+
#include <cstdint>
|
|
19
|
+
#include <memory>
|
|
20
|
+
#include <optional>
|
|
21
|
+
#include <utility>
|
|
22
|
+
|
|
23
|
+
#include "epzero/hnsw.h"
|
|
24
|
+
|
|
25
|
+
namespace epzero {
|
|
26
|
+
|
|
27
|
+
// Settings build_auto() would otherwise choose. Any that is set is used as given.
|
|
28
|
+
struct AutoFixed {
|
|
29
|
+
std::optional<Quantization> quantization;
|
|
30
|
+
std::optional<std::size_t> nav_dim;
|
|
31
|
+
std::optional<bool> fast_build;
|
|
32
|
+
std::optional<bool> refine; // refine_links() after the build
|
|
33
|
+
std::optional<bool> fill_after; // fill_free_links() after the build
|
|
34
|
+
std::optional<std::size_t> rerank;
|
|
35
|
+
std::optional<float> rerank_z;
|
|
36
|
+
std::optional<std::size_t> ef; // skips calibration
|
|
37
|
+
std::optional<std::pair<std::size_t, std::size_t>> prefetch; // (depth, lines); skips timing
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
struct AutoOptions {
|
|
41
|
+
Metric metric = Metric::L2;
|
|
42
|
+
std::size_t M = 16;
|
|
43
|
+
std::size_t ef_construction = 200;
|
|
44
|
+
std::size_t k = 10; // the k the beam width is calibrated for
|
|
45
|
+
float recall = 0.95f; // the recall target
|
|
46
|
+
std::size_t threads = 0; // 0: every core
|
|
47
|
+
std::size_t capacity = 0; // vectors the index can hold (0: the rows given)
|
|
48
|
+
std::uint64_t seed = 42;
|
|
49
|
+
// Optional typical queries (query_count rows of dim floats) to calibrate on. Rows of the data are
|
|
50
|
+
// accurate when queries look like the data, and optimistic when they don't.
|
|
51
|
+
const float* queries = nullptr;
|
|
52
|
+
std::size_t query_count = 0;
|
|
53
|
+
AutoFixed fixed;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
struct AutoPlan {
|
|
57
|
+
HnswParams params;
|
|
58
|
+
IndexProfile profile; // ef is 0 while it still has to be calibrated
|
|
59
|
+
bool calibrate = true; // build_auto will calibrate ef
|
|
60
|
+
bool tune = true; // build_auto will time prefetch settings
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// What build_auto() would do with `data` (n rows of dim floats), without building.
|
|
64
|
+
AutoPlan plan_auto(const float* data, std::size_t n, std::size_t dim, const AutoOptions& options = {});
|
|
65
|
+
|
|
66
|
+
// Builds an index of `data` with labels[i] for row i (labels null: 0..n-1), configured automatically.
|
|
67
|
+
std::unique_ptr<HnswIndex> build_auto(const float* data, const std::uint64_t* labels, std::size_t n, std::size_t dim,
|
|
68
|
+
const AutoOptions& options = {});
|
|
69
|
+
|
|
70
|
+
} // namespace epzero
|
|
@@ -107,6 +107,7 @@ class Database {
|
|
|
107
107
|
private:
|
|
108
108
|
Database(std::string dir, const HnswParams& params, const DatabaseOptions& options);
|
|
109
109
|
void recover();
|
|
110
|
+
void check_upserts(const std::uint64_t* labels, std::size_t n) const;
|
|
110
111
|
void append(const std::vector<std::uint8_t>& bytes, bool sync);
|
|
111
112
|
void apply_upsert(std::uint64_t label, const float* vector);
|
|
112
113
|
void apply_record(const LogRecord& record);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include <cstddef>
|
|
4
4
|
#include <cstdint>
|
|
5
|
+
#include <string>
|
|
5
6
|
#include <string_view>
|
|
6
7
|
#include <utility>
|
|
7
8
|
#include <vector>
|
|
@@ -29,6 +30,10 @@ bool simd_level_supported(SimdLevel level) noexcept;
|
|
|
29
30
|
// forces a specific (supported) level, which is how the benchmarks measure SIMD speedups.
|
|
30
31
|
SimdLevel active_simd_level() noexcept;
|
|
31
32
|
|
|
33
|
+
// The running CPU's model name ("" if unknown) and the active SIMD level, e.g. "AMD EPYC 7513 32-Core
|
|
34
|
+
// Processor/avx2": what machine-specific settings, such as prefetching, were measured on.
|
|
35
|
+
std::string machine_name();
|
|
36
|
+
|
|
32
37
|
// Returns nullptr if `level` is not supported on this machine.
|
|
33
38
|
DistanceFn get_distance_fn(Metric metric, SimdLevel level) noexcept;
|
|
34
39
|
DistanceFn get_distance_fn(Metric metric) noexcept;
|
|
@@ -40,6 +40,18 @@ struct HnswParams {
|
|
|
40
40
|
// SQ8: also keep full-precision vectors, used to build a better graph and to re-rank the final
|
|
41
41
|
// candidates exactly. Without them the index stores ~4x less vector data. RaBitQ requires them.
|
|
42
42
|
bool keep_vectors = true;
|
|
43
|
+
// SQ8: walk the graph on 8-bit codes of this many principal directions, learned by train(), instead
|
|
44
|
+
// of one code per dimension (0 = off). Each scored node then reads and computes nav_dim bytes, not
|
|
45
|
+
// dim, and the final re-rank with full vectors recovers exact distances. For embeddings whose
|
|
46
|
+
// variance sits in far fewer directions than they have dimensions. Needs keep_vectors, and
|
|
47
|
+
// usually a larger SearchOptions::rerank than the default.
|
|
48
|
+
std::size_t nav_dim = 0;
|
|
49
|
+
// Building: when the diversity heuristic leaves a node's link list short, fill the free slots with
|
|
50
|
+
// the closest candidates it pruned. The slots are allocated either way. Not saved with the index.
|
|
51
|
+
bool fill_links = false;
|
|
52
|
+
// Building, with SQ8: search for neighbours and prune them on the codes instead of the full
|
|
53
|
+
// vectors. Not saved with the index: later inserts use full vectors.
|
|
54
|
+
bool fast_build = false;
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
struct SearchResult {
|
|
@@ -114,6 +126,10 @@ struct SearchOptions {
|
|
|
114
126
|
// and as re-scoring all ef of them, to within 0.0003, and 3-13% more queries per second than 3 x k
|
|
115
127
|
// where full vectors are large). Ignored otherwise.
|
|
116
128
|
std::size_t rerank = 0;
|
|
129
|
+
// Stops the re-rank early (0 = off): candidates are scored in blocks, and scoring ends once the
|
|
130
|
+
// next one, placed by this query's gap between estimated and exact distances, is more than this
|
|
131
|
+
// many standard deviations past the k-th best. `rerank` is then only a cap. 3 is a safe value.
|
|
132
|
+
float rerank_z = 0.0f;
|
|
117
133
|
SearchStats* stats = nullptr; // optional
|
|
118
134
|
SearchTrace* trace = nullptr; // optional; costs nothing when null
|
|
119
135
|
// Optional: only labels the filter allows are returned. Must come from this index's make_filter().
|
|
@@ -141,6 +157,27 @@ std::string_view to_string(MissExplanation::Cause cause) noexcept;
|
|
|
141
157
|
|
|
142
158
|
// Hierarchical Navigable Small World index (Malkov & Yashunin, 2016).
|
|
143
159
|
//
|
|
160
|
+
// How an index was configured, and why, saved in its file. search() callers that pass no settings
|
|
161
|
+
// use k, ef, rerank and rerank_z from here (bindings fill them in; the C++ SearchOptions defaults are
|
|
162
|
+
// unchanged). build_auto() fills it in; set_profile() sets it by hand.
|
|
163
|
+
struct IndexProfile {
|
|
164
|
+
std::size_t k = 10;
|
|
165
|
+
std::size_t ef = 64;
|
|
166
|
+
std::size_t rerank = 0;
|
|
167
|
+
float rerank_z = 0.0f;
|
|
168
|
+
float recall_target = 0.0f; // the recall ef was calibrated for (0: not calibrated)
|
|
169
|
+
float recall_measured = 0.0f; // what calibration reached
|
|
170
|
+
bool built_on_codes = false; // the build searched and pruned on 8-bit codes (fast_build)
|
|
171
|
+
bool refined = false; // refine_links ran after the build
|
|
172
|
+
bool filled = false; // fill_free_links ran after the build
|
|
173
|
+
// The prefetch setting chosen on `machine` (see machine_name()); a loaded index applies it only
|
|
174
|
+
// on the same kind of machine.
|
|
175
|
+
std::size_t prefetch_depth = std::numeric_limits<std::size_t>::max();
|
|
176
|
+
std::size_t prefetch_lines = 1;
|
|
177
|
+
std::string machine;
|
|
178
|
+
std::vector<std::pair<std::string, std::string>> notes; // setting, why it was chosen
|
|
179
|
+
};
|
|
180
|
+
|
|
144
181
|
// Thread safety: add(), remove() and search() may be called concurrently from any number of
|
|
145
182
|
// threads. train(), save() and compacted() must not run concurrently with add() or remove().
|
|
146
183
|
//
|
|
@@ -199,6 +236,16 @@ class HnswIndex {
|
|
|
199
236
|
// where the decoded approximations are used instead).
|
|
200
237
|
std::unique_ptr<HnswIndex> compacted(std::size_t threads = 0) const;
|
|
201
238
|
|
|
239
|
+
// Once the index is built: gives each node's free layer-0 link slots to its closest two-hop
|
|
240
|
+
// neighbours. A cheaper way than fill_links to the same end, as links added here are never pruned
|
|
241
|
+
// again. Must not run concurrently with add() or remove(). Returns the number of links added.
|
|
242
|
+
std::size_t fill_free_links(std::size_t threads = 0);
|
|
243
|
+
|
|
244
|
+
// Once the index is built: every node searches the finished graph and reconnects, as its first
|
|
245
|
+
// neighbours were chosen from a sparser one. Must not run concurrently with add() or remove().
|
|
246
|
+
// `exact` searches and prunes on full vectors even on an index built with fast_build.
|
|
247
|
+
void refine_links(std::size_t threads = 0, std::size_t ef = 0, bool exact = false); // ef 0: ef_construction
|
|
248
|
+
|
|
202
249
|
void save(const std::string& path) const;
|
|
203
250
|
// Loads a snapshot; corrupt or truncated files throw. With max_bytes, a snapshot whose index
|
|
204
251
|
// would allocate more than that is refused before allocating (for untrusted files).
|
|
@@ -242,6 +289,10 @@ class HnswIndex {
|
|
|
242
289
|
std::pair<std::size_t, std::size_t> tune_prefetch(const float* queries, std::size_t n, std::size_t k = 10,
|
|
243
290
|
std::size_t ef = 64);
|
|
244
291
|
|
|
292
|
+
const IndexProfile& profile() const noexcept { return profile_; }
|
|
293
|
+
// Not thread-safe against concurrent searches that read it; set it before serving.
|
|
294
|
+
void set_profile(IndexProfile profile) { profile_ = std::move(profile); }
|
|
295
|
+
|
|
245
296
|
private:
|
|
246
297
|
using id_t = std::uint32_t;
|
|
247
298
|
struct Candidate {
|
|
@@ -270,6 +321,11 @@ class HnswIndex {
|
|
|
270
321
|
// Full-precision vector: inside the layer-0 record (None) or in full_ (quantized + keep_vectors).
|
|
271
322
|
const float* full_vector(id_t id) const noexcept;
|
|
272
323
|
bool has_full_vectors() const noexcept { return !quantized() || params_.keep_vectors; }
|
|
324
|
+
// Bytes of SQ8 code per node, and the projection onto the walk's directions (nav_dim only).
|
|
325
|
+
std::size_t code_dim() const noexcept { return params_.nav_dim != 0 ? params_.nav_dim : params_.dim; }
|
|
326
|
+
void project(const float* v, float* out) const noexcept;
|
|
327
|
+
void project_query(const float* query, float* out) const;
|
|
328
|
+
void prepare_query_projection();
|
|
273
329
|
const std::uint8_t* codes_of(id_t id) const noexcept {
|
|
274
330
|
return reinterpret_cast<const std::uint8_t*>(record(id) + sizeof(float));
|
|
275
331
|
}
|
|
@@ -342,6 +398,15 @@ class HnswIndex {
|
|
|
342
398
|
// SQ8: per-dimension x = min + code * scale, and the kept full-precision vectors.
|
|
343
399
|
std::vector<float> sq8_min_;
|
|
344
400
|
std::vector<float> sq8_scale_;
|
|
401
|
+
bool walk_full_ = false;
|
|
402
|
+
bool build_exact_ = false; // refine_links(exact): pair_distance uses full vectors for now
|
|
403
|
+
std::vector<float> nav_basis_; // nav_dim: the directions, nav_dim rows of dim floats
|
|
404
|
+
DistanceFn dot_ = nullptr; // nav_dim: 1 - dot, to project a vector
|
|
405
|
+
// nav_dim, for queries: nav_basis_ rounded to int8, one row at a time, with each row's scale and
|
|
406
|
+
// sum. Not saved: rebuilt from nav_basis_.
|
|
407
|
+
std::vector<std::int8_t> nav_ibasis_;
|
|
408
|
+
std::vector<float> nav_iscale_;
|
|
409
|
+
std::vector<std::int64_t> nav_isum_;
|
|
345
410
|
std::unique_ptr<float[], AlignedFree> full_;
|
|
346
411
|
|
|
347
412
|
// RaBitQ: the centroid, the rotation's random signs, and the padded (power of two) dimension.
|
|
@@ -371,6 +436,7 @@ class HnswIndex {
|
|
|
371
436
|
|
|
372
437
|
std::size_t prefetch_depth_ = std::numeric_limits<std::size_t>::max(); // everything up front
|
|
373
438
|
std::size_t prefetch_lines_ = 1;
|
|
439
|
+
IndexProfile profile_;
|
|
374
440
|
};
|
|
375
441
|
|
|
376
442
|
} // namespace epzero
|
|
@@ -4,8 +4,8 @@ build-backend = "scikit_build_core.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "epzero"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "A fast HNSW vector database in C++20 that configures itself: SIMD search, filtered search, recall monitoring, crash-safe storage"
|
|
9
9
|
readme = "python/README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|
|
@@ -41,6 +41,9 @@ wheel.packages = ["python/epzero"]
|
|
|
41
41
|
EPZERO_BUILD_TESTS = "OFF"
|
|
42
42
|
EPZERO_BUILD_BENCH = "OFF"
|
|
43
43
|
EPZERO_BUILD_PYTHON = "ON"
|
|
44
|
+
# The cluster node needs ../proto, outside the package: built from an sdist on a machine with gRPC
|
|
45
|
+
# installed, it failed the whole install.
|
|
46
|
+
EPZERO_BUILD_NODE = "OFF"
|
|
44
47
|
|
|
45
48
|
[tool.cibuildwheel]
|
|
46
49
|
build = "cp310-* cp311-* cp312-* cp313-*"
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
An HNSW vector database written from scratch in C++20, with Python bindings.
|
|
4
4
|
|
|
5
|
-
- **
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
- **Configures itself:** `epzero.build(vectors)` picks compression, the graph build and the search
|
|
6
|
+
effort for your data and recall target, times memory prefetching on your CPU, saves the choices
|
|
7
|
+
inside the index and tells you why it made each one. Any setting can be overridden.
|
|
8
|
+
- **Fast search:** hand-written SIMD kernels (AVX2, AVX-512, VNNI, NEON) chosen at runtime, 8-bit
|
|
9
|
+
codes, an optional walk on principal directions, and lock-free reads. On dedicated Intel Xeon Gold
|
|
10
|
+
6226 and AMD EPYC 7513 servers, one thread, at equal recall: 1.4-4.2× the queries per second of
|
|
11
|
+
Glass, the fastest library we compared, on GIST1M, OpenAI DBpedia and DEEP10M, and 1.0-1.3× on
|
|
12
|
+
GloVe-100 (with each dataset's best settings; see the project's scoreboard for every loss).
|
|
8
13
|
- **Filtered search that picks its own strategy:** an exact scan, a filtered graph walk, or a
|
|
9
14
|
two-hop walk (the idea behind ACORN), chosen per query by a cost model.
|
|
10
15
|
- **Recall you can see:** `RecallGuard` measures recall on a sample of live queries with a
|
|
@@ -14,7 +19,8 @@ An HNSW vector database written from scratch in C++20, with Python bindings.
|
|
|
14
19
|
- **Hybrid search:** `TextIndex` (BM25 keyword search) and `hybrid_search()`, which merges it
|
|
15
20
|
with vector search by reciprocal rank fusion.
|
|
16
21
|
- **Durable:** `Database` adds a write-ahead log with crash recovery and checkpoints.
|
|
17
|
-
- **8-bit quantization** with exact re-ranking
|
|
22
|
+
- **8-bit quantization** with exact re-ranking, stopped early per query once the rest can't reach
|
|
23
|
+
the top k.
|
|
18
24
|
- **A LangChain vector store** in `epzero.langchain`.
|
|
19
25
|
|
|
20
26
|
```bash
|
|
@@ -22,12 +28,45 @@ pip install epzero # the engine
|
|
|
22
28
|
pip install "epzero[langchain]" # plus the LangChain vector store
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
## Quick start
|
|
31
|
+
## Quick start: let it configure itself
|
|
26
32
|
|
|
27
33
|
```python
|
|
28
34
|
import numpy as np
|
|
29
35
|
import epzero
|
|
30
36
|
|
|
37
|
+
vectors = np.random.rand(100_000, 768).astype(np.float32)
|
|
38
|
+
index = epzero.build(vectors, metric="cosine") # chooses every setting (below)
|
|
39
|
+
labels, distances = index.search(vectors[:5], k=10) # at the effort calibrated for recall 0.95
|
|
40
|
+
print(index.plan) # what it chose, and why
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`build` decides from the data: 8-bit compression, whether to walk on principal directions (high
|
|
44
|
+
dimensions with a concentrated spectrum), the build passes, the re-rank, and the beam width that
|
|
45
|
+
reaches your recall target, measured on rows held out of the graph. From the machine: the SIMD
|
|
46
|
+
kernel, the prefetch setting (timed on this CPU) and every core. Change anything:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
index = epzero.build(vectors, metric="cosine", recall=0.99) # a different target
|
|
50
|
+
index = epzero.build(vectors, metric="cosine", queries=my_queries) # calibrate on real queries
|
|
51
|
+
index = epzero.build(vectors, metric="cosine", nav_dim=0, refine=False, M=32) # override choices
|
|
52
|
+
plan = epzero.plan(vectors, metric="cosine") # look before building, edit, then pass it in
|
|
53
|
+
index = epzero.build(vectors, plan=plan.replace(ef_construction=400))
|
|
54
|
+
labels, distances = index.search(queries, k=50, ef=200) # per-call overrides
|
|
55
|
+
index.save("my.epz") # the settings and reasons are saved inside it
|
|
56
|
+
index = epzero.Index.load("my.epz") # search() uses them again
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`build` returns an ordinary `epzero.Index`: filters, deletes, `explain()` and `RecallGuard` all use
|
|
60
|
+
its settings. A saved index re-applies its prefetch setting only on the same kind of CPU (call
|
|
61
|
+
`tune_prefetch()` on another). The decisions are made in the C++ engine (`epzero/auto.h`), so C++
|
|
62
|
+
users get the same `build_auto()`.
|
|
63
|
+
|
|
64
|
+
If your queries differ from your data (text queries against image vectors, say), pass a few
|
|
65
|
+
hundred of them as `queries=`: recall calibrated on data rows is optimistic for those.
|
|
66
|
+
|
|
67
|
+
## Configured by hand
|
|
68
|
+
|
|
69
|
+
```python
|
|
31
70
|
vectors = np.random.rand(100_000, 128).astype(np.float32)
|
|
32
71
|
index = epzero.Index(dim=128, metric="l2", max_elements=100_000, M=16, ef_construction=200)
|
|
33
72
|
index.add(vectors) # labels 0..n-1 (or pass labels=)
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import epzero
|
|
5
5
|
|
|
6
|
-
index = epzero.
|
|
7
|
-
index.
|
|
8
|
-
|
|
6
|
+
index = epzero.build(vectors, metric="cosine") # configured for the data and this machine
|
|
7
|
+
labels, distances = index.search(queries, k=10) # at the effort calibrated for recall 0.95
|
|
8
|
+
print(index.plan) # every setting, and why; pass any to override
|
|
9
9
|
|
|
10
|
-
`
|
|
10
|
+
`build` chooses compression, the build passes, the re-rank and the beam width from the data, and
|
|
11
|
+
times prefetching on this CPU (see epzero.auto); the settings are saved with the index. `Index` is
|
|
12
|
+
the same engine configured by hand:
|
|
13
|
+
the in-memory HNSW index (filtered search, 8-bit quantization, exact search and miss
|
|
11
14
|
explanations); `Database` adds a write-ahead log and crash recovery; `RecallGuard` measures recall
|
|
12
15
|
on a sample of live queries and moves the search effort to hold a target. `TextIndex` is keyword
|
|
13
16
|
search (BM25), and `hybrid_search` merges it with vector search. A LangChain vector store is in
|
|
@@ -16,7 +19,8 @@ search (BM25), and `hybrid_search` merges it with vector search. A LangChain vec
|
|
|
16
19
|
|
|
17
20
|
from importlib.metadata import PackageNotFoundError, version
|
|
18
21
|
|
|
19
|
-
from ._core import Database, Filter, Index, RecallGuard, TextIndex, fuse_rankings, hybrid_search, simd_level
|
|
22
|
+
from ._core import Database, Filter, Index, RecallGuard, TextIndex, fuse_rankings, hybrid_search, machine, simd_level
|
|
23
|
+
from .auto import Plan, build, plan
|
|
20
24
|
|
|
21
25
|
try:
|
|
22
26
|
__version__ = version("epzero")
|
|
@@ -27,10 +31,14 @@ __all__ = [
|
|
|
27
31
|
"Database",
|
|
28
32
|
"Filter",
|
|
29
33
|
"Index",
|
|
34
|
+
"Plan",
|
|
30
35
|
"RecallGuard",
|
|
31
36
|
"TextIndex",
|
|
37
|
+
"build",
|
|
32
38
|
"fuse_rankings",
|
|
33
39
|
"hybrid_search",
|
|
40
|
+
"machine",
|
|
41
|
+
"plan",
|
|
34
42
|
"simd_level",
|
|
35
43
|
"__version__",
|
|
36
44
|
]
|