epzero 0.4.0__tar.gz → 0.5.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.
Files changed (86) hide show
  1. epzero-0.5.0/CHANGELOG.md +129 -0
  2. {epzero-0.4.0 → epzero-0.5.0}/CMakeLists.txt +51 -23
  3. {epzero-0.4.0 → epzero-0.5.0}/PKG-INFO +96 -7
  4. {epzero-0.4.0 → epzero-0.5.0}/bench/bench_recall.cpp +3 -1
  5. epzero-0.5.0/bench/regression.json +58 -0
  6. epzero-0.5.0/fuzz/fuzz_metadata.cpp +23 -0
  7. {epzero-0.4.0 → epzero-0.5.0}/fuzz/make_seeds.cpp +12 -2
  8. {epzero-0.4.0 → epzero-0.5.0}/include/epzero/auto.h +39 -0
  9. {epzero-0.4.0 → epzero-0.5.0}/include/epzero/distance.h +6 -0
  10. {epzero-0.4.0 → epzero-0.5.0}/include/epzero/hnsw.h +46 -1
  11. epzero-0.5.0/include/epzero/metadata.h +92 -0
  12. {epzero-0.4.0 → epzero-0.5.0}/include/epzero/text.h +13 -3
  13. {epzero-0.4.0 → epzero-0.5.0}/pyproject.toml +6 -3
  14. {epzero-0.4.0 → epzero-0.5.0}/python/README.md +88 -6
  15. {epzero-0.4.0 → epzero-0.5.0}/python/epzero/__init__.py +2 -1
  16. {epzero-0.4.0 → epzero-0.5.0}/python/epzero/auto.py +5 -2
  17. epzero-0.5.0/python/epzero/haystack.py +283 -0
  18. epzero-0.5.0/python/epzero/ir.py +257 -0
  19. {epzero-0.4.0 → epzero-0.5.0}/python/epzero/langchain.py +50 -7
  20. epzero-0.5.0/python/epzero/llamaindex.py +243 -0
  21. {epzero-0.4.0 → epzero-0.5.0}/python/epzero_py.cpp +371 -22
  22. {epzero-0.4.0 → epzero-0.5.0}/python/tests/test_auto.py +76 -0
  23. {epzero-0.4.0 → epzero-0.5.0}/python/tests/test_epzero.py +78 -2
  24. epzero-0.5.0/python/tests/test_haystack.py +74 -0
  25. epzero-0.5.0/python/tests/test_ir.py +128 -0
  26. {epzero-0.4.0 → epzero-0.5.0}/python/tests/test_langchain.py +17 -0
  27. epzero-0.5.0/python/tests/test_llamaindex.py +91 -0
  28. epzero-0.5.0/python/tests/test_metadata.py +67 -0
  29. {epzero-0.4.0 → epzero-0.5.0}/src/auto.cpp +162 -1
  30. {epzero-0.4.0 → epzero-0.5.0}/src/brute_force.cpp +6 -1
  31. {epzero-0.4.0 → epzero-0.5.0}/src/database.cpp +70 -23
  32. {epzero-0.4.0 → epzero-0.5.0}/src/distance.cpp +22 -21
  33. epzero-0.5.0/src/distance_f16c.cpp +72 -0
  34. {epzero-0.4.0 → epzero-0.5.0}/src/distance_neon.cpp +36 -0
  35. {epzero-0.4.0 → epzero-0.5.0}/src/distance_scalar.cpp +16 -0
  36. epzero-0.5.0/src/half.h +60 -0
  37. {epzero-0.4.0 → epzero-0.5.0}/src/hnsw.cpp +233 -37
  38. {epzero-0.4.0 → epzero-0.5.0}/src/kernels.h +7 -0
  39. epzero-0.5.0/src/metadata.cpp +295 -0
  40. epzero-0.5.0/src/platform.h +154 -0
  41. epzero-0.5.0/src/porter.h +233 -0
  42. {epzero-0.4.0 → epzero-0.5.0}/src/text.cpp +36 -8
  43. {epzero-0.4.0 → epzero-0.5.0}/tests/auto_test.cpp +129 -0
  44. epzero-0.5.0/tests/data/v4-from-0.4.0.epzero +0 -0
  45. {epzero-0.4.0 → epzero-0.5.0}/tests/database_test.cpp +2 -2
  46. epzero-0.5.0/tests/half_test.cpp +157 -0
  47. {epzero-0.4.0 → epzero-0.5.0}/tests/hnsw_test.cpp +66 -0
  48. epzero-0.5.0/tests/metadata_test.cpp +116 -0
  49. {epzero-0.4.0 → epzero-0.5.0}/tests/text_test.cpp +32 -0
  50. {epzero-0.4.0 → epzero-0.5.0}/wasm/epzero_wasm.cpp +52 -2
  51. epzero-0.4.0/CHANGELOG.md +0 -54
  52. {epzero-0.4.0 → epzero-0.5.0}/bench/bench_adaptive.cpp +0 -0
  53. {epzero-0.4.0 → epzero-0.5.0}/bench/bench_churn.cpp +0 -0
  54. {epzero-0.4.0 → epzero-0.5.0}/bench/bench_distance.cpp +0 -0
  55. {epzero-0.4.0 → epzero-0.5.0}/bench/bench_filter.cpp +0 -0
  56. {epzero-0.4.0 → epzero-0.5.0}/bench/bench_guard.cpp +0 -0
  57. {epzero-0.4.0 → epzero-0.5.0}/fuzz/common.h +0 -0
  58. {epzero-0.4.0 → epzero-0.5.0}/fuzz/fuzz_graph.cpp +0 -0
  59. {epzero-0.4.0 → epzero-0.5.0}/fuzz/fuzz_snapshot.cpp +0 -0
  60. {epzero-0.4.0 → epzero-0.5.0}/fuzz/fuzz_text.cpp +0 -0
  61. {epzero-0.4.0 → epzero-0.5.0}/fuzz/fuzz_wal.cpp +0 -0
  62. {epzero-0.4.0 → epzero-0.5.0}/fuzz/regressions/graph-empty-with-entry-point +0 -0
  63. {epzero-0.4.0 → epzero-0.5.0}/fuzz/regressions/graph-entry-level-overflow +0 -0
  64. {epzero-0.4.0 → epzero-0.5.0}/fuzz/regressions/graph-upper-link-to-base-node +0 -0
  65. {epzero-0.4.0 → epzero-0.5.0}/fuzz/regressions/snapshot-empty-with-entry-point +0 -0
  66. {epzero-0.4.0 → epzero-0.5.0}/fuzz/regressions/snapshot-huge-capacity +0 -0
  67. {epzero-0.4.0 → epzero-0.5.0}/include/epzero/brute_force.h +0 -0
  68. {epzero-0.4.0 → epzero-0.5.0}/include/epzero/database.h +0 -0
  69. {epzero-0.4.0 → epzero-0.5.0}/include/epzero/dataset.h +0 -0
  70. {epzero-0.4.0 → epzero-0.5.0}/include/epzero/parallel.h +0 -0
  71. {epzero-0.4.0 → epzero-0.5.0}/include/epzero/recall.h +0 -0
  72. {epzero-0.4.0 → epzero-0.5.0}/server/node_main.cpp +0 -0
  73. {epzero-0.4.0 → epzero-0.5.0}/src/dataset.cpp +0 -0
  74. {epzero-0.4.0 → epzero-0.5.0}/src/distance_avx2.cpp +0 -0
  75. {epzero-0.4.0 → epzero-0.5.0}/src/distance_avx512.cpp +0 -0
  76. {epzero-0.4.0 → epzero-0.5.0}/src/distance_avx512vnni.cpp +0 -0
  77. {epzero-0.4.0 → epzero-0.5.0}/src/distance_avxvnni.cpp +0 -0
  78. {epzero-0.4.0 → epzero-0.5.0}/src/distance_wasm.cpp +0 -0
  79. {epzero-0.4.0 → epzero-0.5.0}/src/pca.h +0 -0
  80. {epzero-0.4.0 → epzero-0.5.0}/src/rabitq.h +0 -0
  81. {epzero-0.4.0 → epzero-0.5.0}/src/recall.cpp +0 -0
  82. {epzero-0.4.0 → epzero-0.5.0}/src/visited_pool.h +0 -0
  83. {epzero-0.4.0 → epzero-0.5.0}/tests/dataset_test.cpp +0 -0
  84. {epzero-0.4.0 → epzero-0.5.0}/tests/distance_test.cpp +0 -0
  85. {epzero-0.4.0 → epzero-0.5.0}/tests/recall_test.cpp +0 -0
  86. {epzero-0.4.0 → epzero-0.5.0}/tools/export_graph.cpp +0 -0
@@ -0,0 +1,129 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0
4
+
5
+ An index that stays accurate as its data changes, fixed in place; metadata and keyword search in the
6
+ engine; LlamaIndex and Haystack beside LangChain; retrieval evaluation; and Windows. Files written by
7
+ 0.4 load unchanged.
8
+
9
+ **Staying accurate after the build.**
10
+
11
+ - `index.recalibrate(queries)` (C++: `epzero::recalibrate`): re-measures the beam width for the
12
+ recall target on real queries and saves it in the profile, without rebuilding.
13
+ - `index.health()` (C++: `check_health`): live and deleted vectors, growth since calibration, and how
14
+ much of the data the principal directions still capture, with advice in plain words: compact,
15
+ recalibrate, or rebuild.
16
+ - `RecallGuard(index)` takes its target and starting beam width from the profile.
17
+ - `index.check_queries(queries)` (C++: `check_queries`) says whether queries look like the data: text
18
+ queries against image vectors sit 2.2-2.3× farther from their nearest neighbours than rows of the data,
19
+ against 0.99-1.09× for queries like the data, and recall calibrated on the data then falls to 0.74-0.80
20
+ for a 0.95 target. Its advice: `recalibrate(queries)`, which restored 0.945-0.958 on VIBE's
21
+ text-to-image sets.
22
+ - `index.requantize()` (C++: `HnswIndex::requantize`, then `refine_links`): for vectors added since the
23
+ build that lie outside the ranges the 8-bit codes were fitted on. It refits the ranges on the live
24
+ vectors, re-encodes them and reconnects the graph, in about half a rebuild's time; after a 4 sd drift
25
+ it brought the beam width for recall 0.95 from 200 back to a rebuild's 12. `health()` reports
26
+ `code_error` and advises it above 0.01.
27
+ - `index.get_vector(label)` returns a stored vector.
28
+
29
+ **Information retrieval.** `epzero.ir`: a `Collection` of documents with string ids, retrieved by vector,
30
+ by keyword (BM25) or both (reciprocal rank fusion), growing and shrinking after the build; TREC run files
31
+ in and out; relevance judgements in TREC or BEIR form; nDCG@k, MAP, recall, precision and MRR, matching
32
+ trec_eval (tested against it); and a BEIR dataset loader.
33
+
34
+ **Stemming.** `TextIndex` reduces English words to their Porter stems, so "connections" matches
35
+ "connected" (`stemmer="none"` keeps words as written). It is the reference implementation's algorithm,
36
+ the one Lucene and Anserini use, and agrees with NLTK's on all 39,011 words of SciFact's and NFCorpus's
37
+ vocabularies. nDCG@10 for BM25 went from 0.660 to 0.688 on SciFact and from 0.305 to 0.321 on NFCorpus
38
+ (BEIR's Anserini baselines: 0.665 and 0.325), and for hybrid search with all-MiniLM-L6-v2 from 0.682 to
39
+ 0.713 and from 0.344 to 0.354. Stemmed text indexes are saved as text format 2; files saved before load
40
+ unstemmed, as they were written.
41
+
42
+ **Fixes.** `hybrid_search` searched with a fixed beam width of 64 and the default re-rank, ignoring the
43
+ index's calibrated profile; it now uses the profile unless `ef` is given. Calibration could settle on a
44
+ beam width below k, which only k-sized searches had measured.
45
+
46
+ **File format 5.** Indexes with a profile or 16-bit vectors are written as version 5; 0.4's version 4
47
+ files are still read (a file written by 0.4.0 is part of the test suite). See `docs/file-format.md`.
48
+
49
+ **Indexes grow.** `add()` no longer fails when an index is full: it grows the capacity (at least
50
+ doubling), copying the nodes and keeping the graph; `index.reserve(n)` and `index.capacity` do it ahead.
51
+ The LangChain, LlamaIndex and Haystack stores, which rebuilt their whole graph each time they filled, now
52
+ grow in place: loading 40,000 documents in batches of 100 went from 6.0 s to 3.4 s. Searches on other
53
+ threads wait for a growth, which moves the index's memory; with a RecallGuard attached, growing must be
54
+ done beforehand with `reserve()`.
55
+
56
+ **Metadata in the engine.** `epzero.Metadata` (C++: `MetadataIndex`) keeps str, int, float and bool fields
57
+ per label, with an inverted index per field and ordered numeric values, and `select(where)` returns the
58
+ labels that pass ({field: value}, {field: [values]}, or `$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`,
59
+ `$lte`, `$exists`) for a filtered search. The LangChain store now runs dict filters there instead of
60
+ checking every document in Python. Saved in its own file format, fuzzed in CI.
61
+
62
+ **Memory.** `half_vectors=True` keeps the full vectors as 16-bit floats: about half an index's memory,
63
+ at some speed (up to 17% on GIST1M) and recall (0.003-0.004 on OpenAI embeddings), so never chosen
64
+ automatically. Full-precision search is 4-9% faster, as vector addresses are resolved once per query.
65
+
66
+ **LlamaIndex.** `epzero.llamaindex.EpzeroVectorStore` (`pip install "epzero[llamaindex]"`): nodes with
67
+ their text and metadata, filters in the engine, `delete_ref_doc`, and persistence.
68
+
69
+ **Haystack.** `epzero.haystack.EpzeroDocumentStore` and `EpzeroEmbeddingRetriever`
70
+ (`pip install "epzero[haystack]"`): Haystack filters in the engine where the answer is provably the same,
71
+ and Haystack's document-store test suite passes.
72
+
73
+ **Windows.** The engine builds with MSVC and its tests run on Windows in CI; wheels for Windows (x64).
74
+
75
+ **Easier to read.** `Index`, `Metadata`, `TextIndex` and `Database` print what they hold (size, metric,
76
+ compression, calibrated beam width), and shape and name errors say what was passed.
77
+
78
+ ## 0.4.0
79
+
80
+ **Automatic configuration.** `epzero.build(vectors, metric=...)` (C++: `epzero::build_auto` in
81
+ `epzero/auto.h`) configures an index from its data and the machine it is built on:
82
+
83
+ - From the data: 8-bit codes, whether to walk the graph on principal directions (at 256 dimensions or
84
+ more, when few directions hold most of the variance), the build passes, the re-rank, and the beam
85
+ width (`ef`) that reaches a recall target, calibrated on rows held out of the graph or on queries
86
+ you pass (`queries=`).
87
+ - From the machine: the prefetch setting, timed on this CPU, and every core.
88
+ - Measured on an Intel Xeon Gold 6226 and an AMD Threadripper PRO 9985WX: as fast as hand-tuned
89
+ settings (within 1-5%), recall 0.949-0.957 on real queries for a 0.95 target, and 1.3-4.1× Glass's
90
+ queries per second on GIST1M, OpenAI DBpedia and DEEP10M.
91
+ - Every setting can be fixed instead (`epzero.build(vectors, nav_dim=0, M=32)`, or `epzero.plan()`,
92
+ edit, build). `index.plan` shows each setting and why it was chosen.
93
+ - The choices are saved inside the index (`IndexProfile`, file format v4), and `search()` uses them
94
+ when called without settings. A loaded index re-applies its prefetch setting only on the same kind
95
+ of CPU (`epzero.machine()` names it).
96
+
97
+ **Faster search and builds.**
98
+
99
+ - `nav_dim`: walk the graph on 8-bit codes of principal directions, re-ranking with the full vectors.
100
+ With the other changes here, 2.8-4.2× Glass's queries per second on GIST1M and 1.6-2.7× on OpenAI
101
+ DBpedia embeddings at equal recall, on Intel and AMD servers.
102
+ - `refine_links()`: a second build pass in which every node searches the finished graph and
103
+ reconnects; `fill_free_links()` gives free link slots to two-hop neighbours.
104
+ - `fast_build`: build on the 8-bit codes.
105
+ - `rerank_z`: stop the re-rank early once the remaining candidates can't reach the top k.
106
+ - Queries are projected onto principal directions on the integer kernel; the AVX2 and AVX-VNNI integer
107
+ kernels finish with vector loads instead of a scalar tail.
108
+ - The prefetch tuner can fetch a record's every cache line; on a Xeon Gold 6226 that was worth up to
109
+ 1.7× on some datasets.
110
+ - PCA runs as blocked matrix products (choosing directions 5× faster on GIST1M), training runs on
111
+ every core, and index memory is zeroed on every core, which spreads it across NUMA nodes.
112
+
113
+ **Fixes.**
114
+
115
+ - `Database`: a write that could not be applied (the database full, or not yet trained) was written to
116
+ the log first, after which every open failed replaying it. Writes are now checked before they are
117
+ logged, and replacing a vector in a full database frees its slot before compacting.
118
+ - `RecallGuard` searched, and measured its reference, with the default re-rank depth; it now uses the
119
+ index's own settings and re-ranks the reference's whole beam. `explain()` and `trace()` likewise.
120
+ - The LangChain store copied every stored vector on each add (quadratic in the store's size).
121
+ - `pip install epzero` from source failed on machines with gRPC installed: it tried to build the
122
+ cluster node, whose protocol files aren't in the package.
123
+
124
+ **Compatibility.** Indexes without a profile are still written in the format older versions read
125
+ (v2, or v3 with principal directions); automatically configured ones are v4, which 0.3 cannot load.
126
+ `search()`'s `k`, `ef`, `rerank` and `rerank_z` now default to the index's profile, which for an index
127
+ built by hand is the old defaults (10, 64, 2 × k, off).
128
+
129
+ Earlier releases are described by their git tags (`v0.2.0`, `v0.3.0`).
@@ -52,6 +52,7 @@ add_library(epzero_engine
52
52
  src/distance.cpp
53
53
  src/distance_scalar.cpp
54
54
  src/hnsw.cpp
55
+ src/metadata.cpp
55
56
  src/recall.cpp
56
57
  src/database.cpp
57
58
  src/text.cpp
@@ -60,20 +61,38 @@ add_library(epzero_engine
60
61
  )
61
62
  target_include_directories(epzero_engine PUBLIC include PRIVATE src)
62
63
  target_link_libraries(epzero_engine PUBLIC Threads::Threads)
63
- target_compile_options(epzero_engine PRIVATE -Wall -Wextra -Wpedantic)
64
+ if(MSVC)
65
+ target_compile_options(epzero_engine PRIVATE /W3 /utf-8 /permissive- /Zc:__cplusplus)
66
+ target_compile_definitions(epzero_engine PRIVATE _CRT_SECURE_NO_WARNINGS NOMINMAX)
67
+ else()
68
+ target_compile_options(epzero_engine PRIVATE -Wall -Wextra -Wpedantic)
69
+ endif()
64
70
  set_target_properties(epzero_engine PROPERTIES POSITION_INDEPENDENT_CODE ON)
65
71
 
66
72
  # SIMD kernels get ISA flags per file; the rest of the library stays baseline so the binary
67
73
  # runs on any CPU of the architecture and picks the best kernel at runtime.
68
74
  if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
69
- target_sources(epzero_engine PRIVATE src/distance_avx2.cpp src/distance_avx512.cpp)
70
- set_source_files_properties(src/distance_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mpopcnt")
71
- set_source_files_properties(src/distance_avx512.cpp PROPERTIES COMPILE_OPTIONS "-mavx512f")
75
+ target_sources(epzero_engine PRIVATE src/distance_avx2.cpp src/distance_avx512.cpp src/distance_f16c.cpp)
76
+ if(MSVC) # MSVC allows any intrinsic; /arch only lets the compiler use the ISA on its own
77
+ set_source_files_properties(src/distance_f16c.cpp src/distance_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
78
+ set_source_files_properties(src/distance_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512")
79
+ else()
80
+ set_source_files_properties(src/distance_f16c.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
81
+ set_source_files_properties(src/distance_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mpopcnt")
82
+ set_source_files_properties(src/distance_avx512.cpp PROPERTIES COMPILE_OPTIONS "-mavx512f")
83
+ endif()
72
84
  # VNNI integer kernels, where the toolchain can build them: the compiler must know the
73
85
  # instructions (GCC 11+, Clang 12+ for AVX-VNNI) and so must the assembler it hands them to
74
86
  # (binutils 2.36+ for AVX-VNNI; RHEL 9's 2.35 doesn't), so each check compiles and links a use.
75
87
  include(CheckCXXSourceCompiles)
76
- set(CMAKE_REQUIRED_FLAGS "-mavx2 -mavxvnni")
88
+ if(MSVC)
89
+ set(EPZERO_AVXVNNI_FLAGS "/arch:AVX2")
90
+ set(EPZERO_AVX512VNNI_FLAGS "/arch:AVX512")
91
+ else()
92
+ set(EPZERO_AVXVNNI_FLAGS "-mavx2 -mavxvnni")
93
+ set(EPZERO_AVX512VNNI_FLAGS "-mavx512f -mavx512bw -mavx512vnni")
94
+ endif()
95
+ set(CMAKE_REQUIRED_FLAGS "${EPZERO_AVXVNNI_FLAGS}")
77
96
  check_cxx_source_compiles([[
78
97
  #include <immintrin.h>
79
98
  int main(int argc, char**) {
@@ -81,7 +100,7 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
81
100
  a = _mm256_dpbusd_avx_epi32(a, a, a);
82
101
  return _mm256_extract_epi32(a, 0);
83
102
  }]] EPZERO_TOOLCHAIN_AVXVNNI)
84
- set(CMAKE_REQUIRED_FLAGS "-mavx512f -mavx512bw -mavx512vnni")
103
+ set(CMAKE_REQUIRED_FLAGS "${EPZERO_AVX512VNNI_FLAGS}")
85
104
  check_cxx_source_compiles([[
86
105
  #include <immintrin.h>
87
106
  int main(int argc, char**) {
@@ -92,12 +111,14 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
92
111
  unset(CMAKE_REQUIRED_FLAGS)
93
112
  if(EPZERO_TOOLCHAIN_AVXVNNI)
94
113
  target_sources(epzero_engine PRIVATE src/distance_avxvnni.cpp)
95
- set_source_files_properties(src/distance_avxvnni.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mavxvnni")
114
+ string(REPLACE " " ";" flags "${EPZERO_AVXVNNI_FLAGS}")
115
+ set_source_files_properties(src/distance_avxvnni.cpp PROPERTIES COMPILE_OPTIONS "${flags}")
96
116
  set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVXVNNI)
97
117
  endif()
98
118
  if(EPZERO_TOOLCHAIN_AVX512VNNI)
99
119
  target_sources(epzero_engine PRIVATE src/distance_avx512vnni.cpp)
100
- set_source_files_properties(src/distance_avx512vnni.cpp PROPERTIES COMPILE_OPTIONS "-mavx512f;-mavx512bw;-mavx512vnni")
120
+ string(REPLACE " " ";" flags "${EPZERO_AVX512VNNI_FLAGS}")
121
+ set_source_files_properties(src/distance_avx512vnni.cpp PROPERTIES COMPILE_OPTIONS "${flags}")
101
122
  set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVX512VNNI)
102
123
  endif()
103
124
  elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
@@ -119,8 +140,9 @@ if(EPZERO_BUILD_TESTS)
119
140
  FetchContent_MakeAvailable(googletest)
120
141
 
121
142
  enable_testing()
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)
143
+ add_executable(epzero_tests tests/auto_test.cpp tests/distance_test.cpp tests/half_test.cpp tests/hnsw_test.cpp tests/metadata_test.cpp tests/recall_test.cpp tests/dataset_test.cpp tests/database_test.cpp tests/text_test.cpp)
123
144
  target_link_libraries(epzero_tests PRIVATE epzero_engine GTest::gtest_main)
145
+ target_compile_definitions(epzero_tests PRIVATE EPZERO_TEST_DATA="${CMAKE_CURRENT_SOURCE_DIR}/tests/data")
124
146
  if(EMSCRIPTEN)
125
147
  # Runs under Node (CMAKE_CROSSCOMPILING_EMULATOR); workers are pre-spawned for the
126
148
  # concurrency tests and the real filesystem backs the save/load tests.
@@ -146,7 +168,7 @@ if(EPZERO_BUILD_BENCH)
146
168
  endif()
147
169
 
148
170
  if(EPZERO_BUILD_FUZZ)
149
- foreach(target snapshot graph wal text)
171
+ foreach(target snapshot graph wal text metadata)
150
172
  add_executable(fuzz_${target} fuzz/fuzz_${target}.cpp)
151
173
  target_link_libraries(fuzz_${target} PRIVATE epzero_engine)
152
174
  target_link_options(fuzz_${target} PRIVATE -fsanitize=fuzzer)
@@ -193,20 +215,26 @@ if(EPZERO_BUILD_NODE AND NOT EMSCRIPTEN)
193
215
  endif()
194
216
 
195
217
  if(EMSCRIPTEN)
196
- # ES module for the browser demo: `import createEpzero from "./epzero.js"`.
197
- add_executable(epzero_web wasm/epzero_wasm.cpp)
198
- target_link_libraries(epzero_web PRIVATE epzero_engine)
218
+ # ES modules for the browser demo: `import createEpzero from "./epzero.js"`. epzero-lab.js adds the
219
+ # automatic configuration (build_auto) for the benchmark lab, which is 36 KB more compressed: kept out
220
+ # of the module every other page loads first.
221
+ foreach(web_target epzero_web epzero_lab)
222
+ add_executable(${web_target} wasm/epzero_wasm.cpp)
223
+ target_link_libraries(${web_target} PRIVATE epzero_engine)
224
+ target_link_options(${web_target} PRIVATE
225
+ -lembind -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createEpzero -sENVIRONMENT=web,worker -sSTACK_SIZE=1MB
226
+ # No eval / new Function in the generated bindings, so the site's Content Security Policy can
227
+ # forbid it (only WebAssembly compilation is allowed).
228
+ -sDYNAMIC_EXECUTION=0)
229
+ if(EPZERO_WASM_THREADS)
230
+ # Pre-spawn the workers: the module's own thread blocks while a build runs, so it cannot
231
+ # create new ones on demand.
232
+ target_link_options(${web_target} PRIVATE "-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency,16)")
233
+ endif()
234
+ endforeach()
199
235
  set_target_properties(epzero_web PROPERTIES OUTPUT_NAME epzero SUFFIX .js)
200
- target_link_options(epzero_web PRIVATE
201
- -lembind -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createEpzero -sENVIRONMENT=web,worker -sSTACK_SIZE=1MB
202
- # No eval / new Function in the generated bindings, so the site's Content Security Policy can
203
- # forbid it (only WebAssembly compilation is allowed).
204
- -sDYNAMIC_EXECUTION=0)
205
- if(EPZERO_WASM_THREADS)
206
- # Pre-spawn the workers: the module's own thread blocks while a build runs, so it cannot
207
- # create new ones on demand.
208
- target_link_options(epzero_web PRIVATE "-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency,16)")
209
- endif()
236
+ set_target_properties(epzero_lab PROPERTIES OUTPUT_NAME epzero-lab SUFFIX .js)
237
+ target_compile_definitions(epzero_lab PRIVATE EPZERO_WASM_LAB_AUTO=1)
210
238
  endif()
211
239
 
212
240
  if(EPZERO_BUILD_PYTHON)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: epzero
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
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
@@ -12,6 +12,7 @@ Classifier: Programming Language :: C++
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Operating System :: POSIX :: Linux
14
14
  Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: Microsoft :: Windows
15
16
  Classifier: Topic :: Database
16
17
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
18
  Project-URL: Homepage, https://epzero.dev/
@@ -20,11 +21,17 @@ Requires-Python: >=3.10
20
21
  Requires-Dist: numpy
21
22
  Provides-Extra: langchain
22
23
  Requires-Dist: langchain-core>=0.3; extra == "langchain"
24
+ Provides-Extra: llamaindex
25
+ Requires-Dist: llama-index-core>=0.12; extra == "llamaindex"
26
+ Provides-Extra: haystack
27
+ Requires-Dist: haystack-ai>=2.10; extra == "haystack"
23
28
  Provides-Extra: test
24
29
  Requires-Dist: pytest; extra == "test"
25
30
  Requires-Dist: pytest-asyncio; extra == "test"
26
31
  Requires-Dist: langchain-core>=0.3; extra == "test"
27
32
  Requires-Dist: langchain-tests>=0.3; extra == "test"
33
+ Requires-Dist: llama-index-core>=0.12; extra == "test"
34
+ Requires-Dist: haystack-ai>=2.10; extra == "test"
28
35
  Description-Content-Type: text/markdown
29
36
 
30
37
  # epzero
@@ -45,16 +52,24 @@ An HNSW vector database written from scratch in C++20, with Python bindings.
45
52
  confidence interval and moves the search effort to hold a target; `explain()` says why any given
46
53
  item was not returned. With `per_query=True` each query also stops as soon as its answer settles,
47
54
  so easy queries cost less and hard ones search further.
48
- - **Hybrid search:** `TextIndex` (BM25 keyword search) and `hybrid_search()`, which merges it
49
- with vector search by reciprocal rank fusion.
55
+ - **Hybrid search:** `TextIndex` (BM25 keyword search, with English stemming) and `hybrid_search()`,
56
+ which merges it with vector search by reciprocal rank fusion.
57
+ - **Retrieval evaluation:** `epzero.ir` keeps documents under string ids, runs dense, keyword and
58
+ hybrid retrieval, writes TREC run files and scores them against relevance judgements exactly as
59
+ trec_eval does. On BEIR with all-MiniLM-L6-v2, hybrid reaches nDCG@10 0.713 on SciFact and 0.354 on
60
+ NFCorpus, above either half alone.
50
61
  - **Durable:** `Database` adds a write-ahead log with crash recovery and checkpoints.
51
62
  - **8-bit quantization** with exact re-ranking, stopped early per query once the rest can't reach
52
63
  the top k.
53
- - **A LangChain vector store** in `epzero.langchain`.
64
+ - **Stays accurate as data changes:** `health()` says when an index needs recalibrating,
65
+ requantizing, compacting or rebuilding, and each fix runs in place.
66
+ - **LangChain, LlamaIndex and Haystack** stores, each passing its framework's own test suite.
54
67
 
55
68
  ```bash
56
69
  pip install epzero # the engine
57
70
  pip install "epzero[langchain]" # plus the LangChain vector store
71
+ pip install "epzero[llamaindex]" # plus the LlamaIndex vector store
72
+ pip install "epzero[haystack]" # plus the Haystack document store and retriever
58
73
  ```
59
74
 
60
75
  ## Quick start: let it configure itself
@@ -93,12 +108,36 @@ users get the same `build_auto()`.
93
108
  If your queries differ from your data (text queries against image vectors, say), pass a few
94
109
  hundred of them as `queries=`: recall calibrated on data rows is optimistic for those.
95
110
 
111
+ **Metadata.** Fields per label, filtered in the engine:
112
+
113
+ ```python
114
+ meta = epzero.Metadata()
115
+ meta.set_many(labels, [{"source": "faq", "year": 2024}, ...])
116
+ allowed = meta.select({"source": ["faq", "docs"], "year": {"$gte": 2022}})
117
+ labels, distances = index.search(queries, k=10, filter=index.filter(allowed))
118
+ ```
119
+
120
+ **After the build.** An index keeps changing; its calibration doesn't, by itself:
121
+
122
+ ```python
123
+ print(index.health()) # growth since calibration, deleted share, code error, fit of the directions, advice
124
+ print(index.check_queries(queries)) # do these queries look like the data? (text vs images: no)
125
+ index.recalibrate(queries) # re-measure the beam width on real queries; saved in the profile
126
+ index.requantize() # vectors drifted outside the 8-bit codes' ranges: refit and reconnect
127
+ guard = epzero.RecallGuard(index, sample_rate=0.01) # or measure live recall and adjust as you go
128
+ ```
129
+
130
+ When later vectors drift outside the ranges the 8-bit codes were fitted on, they are clipped and
131
+ the graph links chosen for them suffer: in a synthetic test with a 4 sd drift, the beam width needed
132
+ for recall 0.95 went from 12 to 200. `requantize()` refits the codes and reconnects the graph in about half a rebuild's
133
+ time, back to 12.
134
+
96
135
  ## Configured by hand
97
136
 
98
137
  ```python
99
138
  vectors = np.random.rand(100_000, 128).astype(np.float32)
100
139
  index = epzero.Index(dim=128, metric="l2", max_elements=100_000, M=16, ef_construction=200)
101
- index.add(vectors) # labels 0..n-1 (or pass labels=)
140
+ index.add(vectors) # labels 0..n-1 (or pass labels=); grows as needed
102
141
 
103
142
  labels, distances = index.search(vectors[:5], k=10, ef=64)
104
143
 
@@ -145,9 +184,59 @@ metadata. They are compiled into an Epzero filter, so the engine chooses how to
145
184
  documents instead of over-fetching and discarding. The store passes LangChain's standard
146
185
  vector-store test suite.
147
186
 
187
+ ## LlamaIndex
188
+
189
+ ```python
190
+ from llama_index.core import StorageContext, VectorStoreIndex
191
+ from epzero.llamaindex import EpzeroVectorStore
192
+
193
+ store = EpzeroVectorStore(metric="cosine")
194
+ index = VectorStoreIndex.from_documents(documents, storage_context=StorageContext.from_defaults(vector_store=store))
195
+ index.as_query_engine().query("How do I reset my password?")
196
+ store.persist("./epzero-store") # EpzeroVectorStore.from_persist_path("./epzero-store")
197
+ ```
198
+
199
+ Metadata filters (`==`, `!=`, `>`, `>=`, `<`, `<=`, `in`, `nin`, combined with AND or OR) run in the
200
+ engine, as do `doc_ids` and `node_ids`; `delete_ref_doc` removes a document's nodes.
201
+
202
+ ## Haystack
203
+
204
+ ```python
205
+ from haystack import Pipeline
206
+ from epzero.haystack import EpzeroDocumentStore, EpzeroEmbeddingRetriever
207
+
208
+ store = EpzeroDocumentStore(metric="cosine")
209
+ store.write_documents(documents) # Documents with embeddings
210
+ pipeline = Pipeline()
211
+ pipeline.add_component("retriever", EpzeroEmbeddingRetriever(document_store=store, top_k=5))
212
+ pipeline.run({"retriever": {"query_embedding": embedding,
213
+ "filters": {"field": "meta.lang", "operator": "==", "value": "en"}}})
214
+ ```
215
+
216
+ Filters use Haystack's syntax and run in the engine wherever that gives exactly Haystack's answer;
217
+ the rest (ranges on dates, `None`) go to Haystack's reference implementation. The store passes
218
+ Haystack's document-store test suite.
219
+
220
+ ## Retrieval evaluation
221
+
222
+ ```python
223
+ from epzero import ir
224
+
225
+ corpus, queries, qrels = ir.load_beir("scifact") # a BEIR dataset folder
226
+ collection = ir.Collection.build(list(corpus), doc_vectors, texts=list(corpus.values()), metric="cosine")
227
+ run = collection.retrieve(list(queries), vectors=query_vectors, texts=list(queries.values()),
228
+ k=100, mode="hybrid") # or "dense", "keyword"
229
+ ir.write_run(run, "run.txt", tag="my-system") # TREC format, for CLEF or TREC
230
+ print(ir.evaluate(run, qrels)) # nDCG@10, R@100, MAP, MRR@10
231
+ ```
232
+
233
+ `evaluate` matches trec_eval (tested against it, ties included) and also takes `per_query=True` and
234
+ measures like `P@5` or `nDCG@20`. Documents can be added, replaced and removed by id, and a collection
235
+ saves to a folder.
236
+
148
237
  ## Platforms
149
238
 
150
- Wheels for Linux (x86-64 and ARM64, glibc 2.28+) and macOS (Apple silicon and Intel), Python 3.10
151
- to 3.13. Elsewhere, `pip install epzero` builds from source (CMake 3.20+, a C++20 compiler).
239
+ Wheels for Linux (x86-64 and ARM64, glibc 2.28+), macOS (Apple silicon and Intel) and, from 0.5,
240
+ Windows (x64), Python 3.10 to 3.13. Elsewhere, `pip install epzero` builds from source (CMake 3.20+, a C++20 compiler).
152
241
 
153
242
  Live demo, benchmarks and design notes: https://epzero.dev/ · MIT license.
@@ -43,6 +43,7 @@ struct Options {
43
43
  std::size_t nav_dim = 0;
44
44
  bool fill_links = false;
45
45
  bool fast_build = false;
46
+ bool half = false;
46
47
  bool fill_after = false;
47
48
  std::size_t refine = 0;
48
49
  bool refine_exact = false;
@@ -98,6 +99,7 @@ Options parse(int argc, char** argv) {
98
99
  else if (arg == "--nav-dim") o.nav_dim = std::stoul(value());
99
100
  else if (arg == "--fill-links") o.fill_links = true;
100
101
  else if (arg == "--fast-build") o.fast_build = true;
102
+ else if (arg == "--half") o.half = true;
101
103
  else if (arg == "--fill-after") o.fill_after = true;
102
104
  else if (arg == "--refine") o.refine = std::stoul(value());
103
105
  else if (arg == "--refine-exact") o.refine_exact = true;
@@ -191,7 +193,7 @@ int main(int argc, char** argv) try {
191
193
  HnswParams params{.dim = dim, .metric = opt.metric, .max_elements = base.n, .M = opt.M,
192
194
  .ef_construction = opt.ef_construction, .seed = opt.seed, .quantization = opt.quantization,
193
195
  .keep_vectors = opt.keep_vectors, .nav_dim = opt.nav_dim, .fill_links = opt.fill_links,
194
- .fast_build = opt.fast_build};
196
+ .fast_build = opt.fast_build, .half_vectors = opt.half};
195
197
  loaded = std::make_unique<HnswIndex>(params);
196
198
  t0 = Clock::now();
197
199
  loaded->train(base.data.data(), std::min<std::size_t>(base.n, 100'000)); // no-op unquantized
@@ -0,0 +1,58 @@
1
+ {
2
+ "float-l2": {
3
+ "4": {
4
+ "recall": 0.9068,
5
+ "distances": 209.99
6
+ },
7
+ "8": {
8
+ "recall": 0.9068,
9
+ "distances": 209.99
10
+ },
11
+ "16": {
12
+ "recall": 0.9664,
13
+ "distances": 247.212
14
+ }
15
+ },
16
+ "float-cosine": {
17
+ "4": {
18
+ "recall": 0.9042,
19
+ "distances": 222.262
20
+ },
21
+ "8": {
22
+ "recall": 0.9042,
23
+ "distances": 222.262
24
+ },
25
+ "16": {
26
+ "recall": 0.967,
27
+ "distances": 263.986
28
+ }
29
+ },
30
+ "sq8-second-pass": {
31
+ "4": {
32
+ "recall": 0.9132,
33
+ "distances": 234.102
34
+ },
35
+ "8": {
36
+ "recall": 0.9132,
37
+ "distances": 234.102
38
+ },
39
+ "16": {
40
+ "recall": 0.981,
41
+ "distances": 275.388
42
+ }
43
+ },
44
+ "sq8-directions": {
45
+ "32": {
46
+ "recall": 0.4988,
47
+ "distances": 323.592
48
+ },
49
+ "64": {
50
+ "recall": 0.7316,
51
+ "distances": 390.994
52
+ },
53
+ "128": {
54
+ "recall": 0.9518,
55
+ "distances": 508.41
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,23 @@
1
+ // Fuzzes MetadataIndex::load: arbitrary bytes as a metadata file must either load into an index that is
2
+ // safe to query, change and save, or throw. Never crash, read out of bounds or hit undefined behavior.
3
+ #include <exception>
4
+ #include <string>
5
+
6
+ #include "common.h"
7
+ #include "epzero/metadata.h"
8
+
9
+ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) {
10
+ static const auto path = epzero::fuzz::scratch("metadata") / "input.meta";
11
+ epzero::fuzz::write_bytes(path, data, size);
12
+ try {
13
+ epzero::MetadataIndex index = epzero::MetadataIndex::load(path.string());
14
+ (void)index.select({{.field = "source", .any_of = {std::string("faq")}}, {.field = "year", .min = 2000.0}});
15
+ (void)index.select({{.field = "year", .max = 1e9, .negate = true}});
16
+ index.set(1, {{"source", std::string("new")}});
17
+ index.remove(1);
18
+ index.save(path.string());
19
+ } catch (const std::exception&) {
20
+ // Rejected cleanly: the expected outcome for corrupt input.
21
+ }
22
+ return 0;
23
+ }
@@ -9,6 +9,7 @@
9
9
 
10
10
  #include "common.h"
11
11
  #include "epzero/database.h"
12
+ #include "epzero/metadata.h"
12
13
  #include "epzero/text.h"
13
14
 
14
15
  namespace fs = std::filesystem;
@@ -20,7 +21,7 @@ int main(int argc, char** argv) {
20
21
  return 2;
21
22
  }
22
23
  const fs::path out = argv[1];
23
- for (const char* name : {"snapshot", "graph", "wal", "text"}) fs::create_directories(out / name);
24
+ for (const char* name : {"snapshot", "graph", "wal", "text", "metadata"}) fs::create_directories(out / name);
24
25
 
25
26
  std::vector<float> vectors(40 * kDim);
26
27
  for (std::size_t i = 0; i < vectors.size(); ++i) vectors[i] = static_cast<float>((i * 7919) % 101) / 101.0f;
@@ -68,7 +69,16 @@ int main(int argc, char** argv) {
68
69
  text.add(1, "red car by the river");
69
70
  text.add(2, "night city lights, city traffic");
70
71
  text.add(7, "café über 42");
71
- text.save((out / "text" / "valid").string());
72
+ text.save((out / "text" / "valid").string()); // version 2: stemmed
73
+ epzero::TextIndex plain({.stemmer = epzero::TextIndex::Stemmer::None});
74
+ plain.add(1, "red car by the river");
75
+ plain.add(7, "café über 42");
76
+ plain.save((out / "text" / "valid-v1").string());
77
+
78
+ epzero::MetadataIndex meta;
79
+ meta.set(1, {{"source", std::string("faq")}, {"year", std::int64_t{2021}}, {"public", true}});
80
+ meta.set(2, {{"source", std::string("blog")}, {"score", 0.5}});
81
+ meta.save((out / "metadata" / "valid").string());
72
82
  std::printf("seeds written to %s\n", out.string().c_str());
73
83
  return 0;
74
84
  }
@@ -18,7 +18,9 @@
18
18
  #include <cstdint>
19
19
  #include <memory>
20
20
  #include <optional>
21
+ #include <string>
21
22
  #include <utility>
23
+ #include <vector>
22
24
 
23
25
  #include "epzero/hnsw.h"
24
26
 
@@ -35,6 +37,7 @@ struct AutoFixed {
35
37
  std::optional<float> rerank_z;
36
38
  std::optional<std::size_t> ef; // skips calibration
37
39
  std::optional<std::pair<std::size_t, std::size_t>> prefetch; // (depth, lines); skips timing
40
+ std::optional<bool> half_vectors; // 16-bit full vectors: half their memory (never chosen automatically)
38
41
  };
39
42
 
40
43
  struct AutoOptions {
@@ -67,4 +70,40 @@ AutoPlan plan_auto(const float* data, std::size_t n, std::size_t dim, const Auto
67
70
  std::unique_ptr<HnswIndex> build_auto(const float* data, const std::uint64_t* labels, std::size_t n, std::size_t dim,
68
71
  const AutoOptions& options = {});
69
72
 
73
+ // Re-measures the beam width that reaches `recall` (0: the profile's target, else 0.95) on `queries`
74
+ // (count rows of dim floats) and stores it in the index's profile, which search() then uses. Pass real
75
+ // queries: rows already in the graph are linked to their own neighbours, so they make the search look
76
+ // easier than it is. Not thread-safe against concurrent searches. Returns (ef, recall reached).
77
+ std::pair<std::size_t, float> recalibrate(HnswIndex& index, const float* queries, std::size_t count, float recall = 0.0f,
78
+ std::size_t threads = 0);
79
+
80
+ // What an index needs, if anything, since it was built: a measurement plus advice in plain words.
81
+ struct IndexHealth {
82
+ std::size_t live = 0;
83
+ std::size_t deleted = 0; // tombstones still in the graph
84
+ double growth = 0.0; // change in live vectors since calibration, as a share (0.3: 30% more)
85
+ double nav_variance_built = 0.0; // share of the variance the principal directions held when built
86
+ double nav_variance_now = 0.0; // and on the vectors in the index now (0 without principal directions)
87
+ double code_error = 0.0; // share of the variance the 8-bit codes miss (HnswIndex::code_error)
88
+ std::vector<std::string> advice; // empty when nothing needs doing
89
+ };
90
+ IndexHealth check_health(const HnswIndex& index, std::size_t threads = 0);
91
+
92
+ // Whether `queries` (count rows) look like the data the index was calibrated on. A query from the data's
93
+ // distribution sits about as far from its nearest neighbour as rows of the data do from theirs; text
94
+ // queries against image vectors, say, sit far farther (VIBE's text-to-image sets: 2.3x, against 0.99-1.09x
95
+ // for queries like the data). Then recall calibrated on rows of the data overestimates theirs (0.74-0.80
96
+ // against a 0.95 target on those sets), and `advice` says to recalibrate on them.
97
+ struct QueryCheck {
98
+ double query_distance = 0.0; // median distance from a query to its nearest neighbour (sample)
99
+ double row_distance = 0.0; // the same for rows of the data
100
+ double ratio = 0.0; // query_distance / row_distance
101
+ bool shifted = false; // ratio above 1.5
102
+ std::vector<std::string> advice;
103
+ };
104
+ QueryCheck check_queries(const HnswIndex& index, const float* queries, std::size_t count, std::size_t threads = 0);
105
+
106
+ // Share of the variance of the index's vectors (a sample) that its principal directions hold.
107
+ double nav_variance(const HnswIndex& index, std::size_t threads = 0);
108
+
70
109
  } // namespace epzero
@@ -38,6 +38,12 @@ std::string machine_name();
38
38
  DistanceFn get_distance_fn(Metric metric, SimdLevel level) noexcept;
39
39
  DistanceFn get_distance_fn(Metric metric) noexcept;
40
40
 
41
+ // A float query against a vector stored as IEEE half floats (HnswParams::half_vectors): the metric's
42
+ // distance as get_distance_fn computes it. Always available (a scalar fallback); F16C or NEON where
43
+ // the CPU has them.
44
+ using HalfDistanceFn = float (*)(const float* query, const std::uint16_t* v, std::size_t dim) noexcept;
45
+ HalfDistanceFn get_half_distance_fn(Metric metric, SimdLevel level) noexcept;
46
+
41
47
  // Asymmetric-distance kernel for 8-bit scalar quantization: returns sum_i w[i] * codes[i].
42
48
  // The quantizer folds its per-dimension scales and offsets into `w` once per query, so scoring a
43
49
  // node reads only `dim` bytes. Returns nullptr if `level` is not supported.