epzero 0.3.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 (89) hide show
  1. epzero-0.5.0/CHANGELOG.md +129 -0
  2. {epzero-0.3.0 → epzero-0.5.0}/CMakeLists.txt +52 -23
  3. epzero-0.5.0/PKG-INFO +242 -0
  4. {epzero-0.3.0 → epzero-0.5.0}/bench/bench_recall.cpp +34 -3
  5. epzero-0.5.0/bench/regression.json +58 -0
  6. epzero-0.5.0/fuzz/fuzz_metadata.cpp +23 -0
  7. {epzero-0.3.0 → epzero-0.5.0}/fuzz/make_seeds.cpp +29 -2
  8. epzero-0.5.0/include/epzero/auto.h +109 -0
  9. {epzero-0.3.0 → epzero-0.5.0}/include/epzero/database.h +1 -0
  10. {epzero-0.3.0 → epzero-0.5.0}/include/epzero/distance.h +11 -0
  11. {epzero-0.3.0 → epzero-0.5.0}/include/epzero/hnsw.h +112 -1
  12. epzero-0.5.0/include/epzero/metadata.h +92 -0
  13. {epzero-0.3.0 → epzero-0.5.0}/include/epzero/text.h +13 -3
  14. {epzero-0.3.0 → epzero-0.5.0}/pyproject.toml +10 -4
  15. epzero-0.5.0/python/README.md +206 -0
  16. epzero-0.5.0/python/epzero/__init__.py +45 -0
  17. epzero-0.5.0/python/epzero/auto.py +149 -0
  18. epzero-0.5.0/python/epzero/haystack.py +283 -0
  19. epzero-0.5.0/python/epzero/ir.py +257 -0
  20. {epzero-0.3.0 → epzero-0.5.0}/python/epzero/langchain.py +69 -8
  21. epzero-0.5.0/python/epzero/llamaindex.py +243 -0
  22. epzero-0.5.0/python/epzero_py.cpp +1106 -0
  23. epzero-0.5.0/python/tests/test_auto.py +238 -0
  24. {epzero-0.3.0 → epzero-0.5.0}/python/tests/test_epzero.py +78 -2
  25. epzero-0.5.0/python/tests/test_haystack.py +74 -0
  26. epzero-0.5.0/python/tests/test_ir.py +128 -0
  27. {epzero-0.3.0 → epzero-0.5.0}/python/tests/test_langchain.py +17 -0
  28. epzero-0.5.0/python/tests/test_llamaindex.py +91 -0
  29. epzero-0.5.0/python/tests/test_metadata.py +67 -0
  30. epzero-0.5.0/src/auto.cpp +430 -0
  31. {epzero-0.3.0 → epzero-0.5.0}/src/brute_force.cpp +6 -1
  32. {epzero-0.3.0 → epzero-0.5.0}/src/database.cpp +103 -31
  33. {epzero-0.3.0 → epzero-0.5.0}/src/distance.cpp +64 -21
  34. {epzero-0.3.0 → epzero-0.5.0}/src/distance_avx2.cpp +24 -1
  35. {epzero-0.3.0 → epzero-0.5.0}/src/distance_avxvnni.cpp +24 -1
  36. epzero-0.5.0/src/distance_f16c.cpp +72 -0
  37. {epzero-0.3.0 → epzero-0.5.0}/src/distance_neon.cpp +36 -0
  38. {epzero-0.3.0 → epzero-0.5.0}/src/distance_scalar.cpp +16 -0
  39. epzero-0.5.0/src/half.h +60 -0
  40. {epzero-0.3.0 → epzero-0.5.0}/src/hnsw.cpp +568 -65
  41. {epzero-0.3.0 → epzero-0.5.0}/src/kernels.h +7 -0
  42. epzero-0.5.0/src/metadata.cpp +295 -0
  43. epzero-0.5.0/src/pca.h +206 -0
  44. epzero-0.5.0/src/platform.h +154 -0
  45. epzero-0.5.0/src/porter.h +233 -0
  46. {epzero-0.3.0 → epzero-0.5.0}/src/recall.cpp +8 -2
  47. {epzero-0.3.0 → epzero-0.5.0}/src/text.cpp +36 -8
  48. epzero-0.5.0/tests/auto_test.cpp +345 -0
  49. epzero-0.5.0/tests/data/v4-from-0.4.0.epzero +0 -0
  50. {epzero-0.3.0 → epzero-0.5.0}/tests/database_test.cpp +33 -2
  51. epzero-0.5.0/tests/half_test.cpp +157 -0
  52. {epzero-0.3.0 → epzero-0.5.0}/tests/hnsw_test.cpp +201 -2
  53. epzero-0.5.0/tests/metadata_test.cpp +116 -0
  54. {epzero-0.3.0 → epzero-0.5.0}/tests/text_test.cpp +32 -0
  55. {epzero-0.3.0 → epzero-0.5.0}/wasm/epzero_wasm.cpp +52 -2
  56. epzero-0.3.0/PKG-INFO +0 -114
  57. epzero-0.3.0/python/README.md +0 -85
  58. epzero-0.3.0/python/epzero/__init__.py +0 -36
  59. epzero-0.3.0/python/epzero_py.cpp +0 -581
  60. {epzero-0.3.0 → epzero-0.5.0}/bench/bench_adaptive.cpp +0 -0
  61. {epzero-0.3.0 → epzero-0.5.0}/bench/bench_churn.cpp +0 -0
  62. {epzero-0.3.0 → epzero-0.5.0}/bench/bench_distance.cpp +0 -0
  63. {epzero-0.3.0 → epzero-0.5.0}/bench/bench_filter.cpp +0 -0
  64. {epzero-0.3.0 → epzero-0.5.0}/bench/bench_guard.cpp +0 -0
  65. {epzero-0.3.0 → epzero-0.5.0}/fuzz/common.h +0 -0
  66. {epzero-0.3.0 → epzero-0.5.0}/fuzz/fuzz_graph.cpp +0 -0
  67. {epzero-0.3.0 → epzero-0.5.0}/fuzz/fuzz_snapshot.cpp +0 -0
  68. {epzero-0.3.0 → epzero-0.5.0}/fuzz/fuzz_text.cpp +0 -0
  69. {epzero-0.3.0 → epzero-0.5.0}/fuzz/fuzz_wal.cpp +0 -0
  70. {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/graph-empty-with-entry-point +0 -0
  71. {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/graph-entry-level-overflow +0 -0
  72. {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/graph-upper-link-to-base-node +0 -0
  73. {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/snapshot-empty-with-entry-point +0 -0
  74. {epzero-0.3.0 → epzero-0.5.0}/fuzz/regressions/snapshot-huge-capacity +0 -0
  75. {epzero-0.3.0 → epzero-0.5.0}/include/epzero/brute_force.h +0 -0
  76. {epzero-0.3.0 → epzero-0.5.0}/include/epzero/dataset.h +0 -0
  77. {epzero-0.3.0 → epzero-0.5.0}/include/epzero/parallel.h +0 -0
  78. {epzero-0.3.0 → epzero-0.5.0}/include/epzero/recall.h +0 -0
  79. {epzero-0.3.0 → epzero-0.5.0}/server/node_main.cpp +0 -0
  80. {epzero-0.3.0 → epzero-0.5.0}/src/dataset.cpp +0 -0
  81. {epzero-0.3.0 → epzero-0.5.0}/src/distance_avx512.cpp +0 -0
  82. {epzero-0.3.0 → epzero-0.5.0}/src/distance_avx512vnni.cpp +0 -0
  83. {epzero-0.3.0 → epzero-0.5.0}/src/distance_wasm.cpp +0 -0
  84. {epzero-0.3.0 → epzero-0.5.0}/src/rabitq.h +0 -0
  85. {epzero-0.3.0 → epzero-0.5.0}/src/visited_pool.h +0 -0
  86. {epzero-0.3.0 → epzero-0.5.0}/tests/dataset_test.cpp +0 -0
  87. {epzero-0.3.0 → epzero-0.5.0}/tests/distance_test.cpp +0 -0
  88. {epzero-0.3.0 → epzero-0.5.0}/tests/recall_test.cpp +0 -0
  89. {epzero-0.3.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`).
@@ -48,9 +48,11 @@ 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
55
+ src/metadata.cpp
54
56
  src/recall.cpp
55
57
  src/database.cpp
56
58
  src/text.cpp
@@ -59,20 +61,38 @@ add_library(epzero_engine
59
61
  )
60
62
  target_include_directories(epzero_engine PUBLIC include PRIVATE src)
61
63
  target_link_libraries(epzero_engine PUBLIC Threads::Threads)
62
- 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()
63
70
  set_target_properties(epzero_engine PROPERTIES POSITION_INDEPENDENT_CODE ON)
64
71
 
65
72
  # SIMD kernels get ISA flags per file; the rest of the library stays baseline so the binary
66
73
  # runs on any CPU of the architecture and picks the best kernel at runtime.
67
74
  if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
68
- target_sources(epzero_engine PRIVATE src/distance_avx2.cpp src/distance_avx512.cpp)
69
- set_source_files_properties(src/distance_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mpopcnt")
70
- 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()
71
84
  # VNNI integer kernels, where the toolchain can build them: the compiler must know the
72
85
  # instructions (GCC 11+, Clang 12+ for AVX-VNNI) and so must the assembler it hands them to
73
86
  # (binutils 2.36+ for AVX-VNNI; RHEL 9's 2.35 doesn't), so each check compiles and links a use.
74
87
  include(CheckCXXSourceCompiles)
75
- 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}")
76
96
  check_cxx_source_compiles([[
77
97
  #include <immintrin.h>
78
98
  int main(int argc, char**) {
@@ -80,7 +100,7 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
80
100
  a = _mm256_dpbusd_avx_epi32(a, a, a);
81
101
  return _mm256_extract_epi32(a, 0);
82
102
  }]] EPZERO_TOOLCHAIN_AVXVNNI)
83
- set(CMAKE_REQUIRED_FLAGS "-mavx512f -mavx512bw -mavx512vnni")
103
+ set(CMAKE_REQUIRED_FLAGS "${EPZERO_AVX512VNNI_FLAGS}")
84
104
  check_cxx_source_compiles([[
85
105
  #include <immintrin.h>
86
106
  int main(int argc, char**) {
@@ -91,12 +111,14 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
91
111
  unset(CMAKE_REQUIRED_FLAGS)
92
112
  if(EPZERO_TOOLCHAIN_AVXVNNI)
93
113
  target_sources(epzero_engine PRIVATE src/distance_avxvnni.cpp)
94
- 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}")
95
116
  set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVXVNNI)
96
117
  endif()
97
118
  if(EPZERO_TOOLCHAIN_AVX512VNNI)
98
119
  target_sources(epzero_engine PRIVATE src/distance_avx512vnni.cpp)
99
- 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}")
100
122
  set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVX512VNNI)
101
123
  endif()
102
124
  elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
@@ -118,8 +140,9 @@ if(EPZERO_BUILD_TESTS)
118
140
  FetchContent_MakeAvailable(googletest)
119
141
 
120
142
  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)
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)
122
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")
123
146
  if(EMSCRIPTEN)
124
147
  # Runs under Node (CMAKE_CROSSCOMPILING_EMULATOR); workers are pre-spawned for the
125
148
  # concurrency tests and the real filesystem backs the save/load tests.
@@ -145,7 +168,7 @@ if(EPZERO_BUILD_BENCH)
145
168
  endif()
146
169
 
147
170
  if(EPZERO_BUILD_FUZZ)
148
- foreach(target snapshot graph wal text)
171
+ foreach(target snapshot graph wal text metadata)
149
172
  add_executable(fuzz_${target} fuzz/fuzz_${target}.cpp)
150
173
  target_link_libraries(fuzz_${target} PRIVATE epzero_engine)
151
174
  target_link_options(fuzz_${target} PRIVATE -fsanitize=fuzzer)
@@ -192,20 +215,26 @@ if(EPZERO_BUILD_NODE AND NOT EMSCRIPTEN)
192
215
  endif()
193
216
 
194
217
  if(EMSCRIPTEN)
195
- # ES module for the browser demo: `import createEpzero from "./epzero.js"`.
196
- add_executable(epzero_web wasm/epzero_wasm.cpp)
197
- 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()
198
235
  set_target_properties(epzero_web PROPERTIES OUTPUT_NAME epzero SUFFIX .js)
199
- target_link_options(epzero_web PRIVATE
200
- -lembind -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createEpzero -sENVIRONMENT=web,worker -sSTACK_SIZE=1MB
201
- # No eval / new Function in the generated bindings, so the site's Content Security Policy can
202
- # forbid it (only WebAssembly compilation is allowed).
203
- -sDYNAMIC_EXECUTION=0)
204
- if(EPZERO_WASM_THREADS)
205
- # Pre-spawn the workers: the module's own thread blocks while a build runs, so it cannot
206
- # create new ones on demand.
207
- target_link_options(epzero_web PRIVATE "-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency,16)")
208
- 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)
209
238
  endif()
210
239
 
211
240
  if(EPZERO_BUILD_PYTHON)
epzero-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.2
2
+ Name: epzero
3
+ Version: 0.5.0
4
+ Summary: A fast HNSW vector database in C++20 that configures itself: SIMD search, filtered search, recall monitoring, crash-safe storage
5
+ Keywords: vector database,hnsw,ann,nearest neighbor,embeddings,rag,langchain
6
+ Author: Lajat
7
+ License: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: C++
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Topic :: Database
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Project-URL: Homepage, https://epzero.dev/
19
+ Project-URL: Source, https://github.com/Lazeeez/Epzero
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: numpy
22
+ Provides-Extra: langchain
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"
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest; extra == "test"
30
+ Requires-Dist: pytest-asyncio; extra == "test"
31
+ Requires-Dist: langchain-core>=0.3; extra == "test"
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"
35
+ Description-Content-Type: text/markdown
36
+
37
+ # epzero
38
+
39
+ An HNSW vector database written from scratch in C++20, with Python bindings.
40
+
41
+ - **Configures itself:** `epzero.build(vectors)` picks compression, the graph build and the search
42
+ effort for your data and recall target, times memory prefetching on your CPU, saves the choices
43
+ inside the index and tells you why it made each one. Any setting can be overridden.
44
+ - **Fast search:** hand-written SIMD kernels (AVX2, AVX-512, VNNI, NEON) chosen at runtime, 8-bit
45
+ codes, an optional walk on principal directions, and lock-free reads. On dedicated Intel Xeon Gold
46
+ 6226 and AMD EPYC 7513 servers, one thread, at equal recall: 1.4-4.2× the queries per second of
47
+ Glass, the fastest library we compared, on GIST1M, OpenAI DBpedia and DEEP10M, and 1.0-1.3× on
48
+ GloVe-100 (with each dataset's best settings; see the project's scoreboard for every loss).
49
+ - **Filtered search that picks its own strategy:** an exact scan, a filtered graph walk, or a
50
+ two-hop walk (the idea behind ACORN), chosen per query by a cost model.
51
+ - **Recall you can see:** `RecallGuard` measures recall on a sample of live queries with a
52
+ confidence interval and moves the search effort to hold a target; `explain()` says why any given
53
+ item was not returned. With `per_query=True` each query also stops as soon as its answer settles,
54
+ so easy queries cost less and hard ones search further.
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.
61
+ - **Durable:** `Database` adds a write-ahead log with crash recovery and checkpoints.
62
+ - **8-bit quantization** with exact re-ranking, stopped early per query once the rest can't reach
63
+ the top k.
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.
67
+
68
+ ```bash
69
+ pip install epzero # the engine
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
73
+ ```
74
+
75
+ ## Quick start: let it configure itself
76
+
77
+ ```python
78
+ import numpy as np
79
+ import epzero
80
+
81
+ vectors = np.random.rand(100_000, 768).astype(np.float32)
82
+ index = epzero.build(vectors, metric="cosine") # chooses every setting (below)
83
+ labels, distances = index.search(vectors[:5], k=10) # at the effort calibrated for recall 0.95
84
+ print(index.plan) # what it chose, and why
85
+ ```
86
+
87
+ `build` decides from the data: 8-bit compression, whether to walk on principal directions (high
88
+ dimensions with a concentrated spectrum), the build passes, the re-rank, and the beam width that
89
+ reaches your recall target, measured on rows held out of the graph. From the machine: the SIMD
90
+ kernel, the prefetch setting (timed on this CPU) and every core. Change anything:
91
+
92
+ ```python
93
+ index = epzero.build(vectors, metric="cosine", recall=0.99) # a different target
94
+ index = epzero.build(vectors, metric="cosine", queries=my_queries) # calibrate on real queries
95
+ index = epzero.build(vectors, metric="cosine", nav_dim=0, refine=False, M=32) # override choices
96
+ plan = epzero.plan(vectors, metric="cosine") # look before building, edit, then pass it in
97
+ index = epzero.build(vectors, plan=plan.replace(ef_construction=400))
98
+ labels, distances = index.search(queries, k=50, ef=200) # per-call overrides
99
+ index.save("my.epz") # the settings and reasons are saved inside it
100
+ index = epzero.Index.load("my.epz") # search() uses them again
101
+ ```
102
+
103
+ `build` returns an ordinary `epzero.Index`: filters, deletes, `explain()` and `RecallGuard` all use
104
+ its settings. A saved index re-applies its prefetch setting only on the same kind of CPU (call
105
+ `tune_prefetch()` on another). The decisions are made in the C++ engine (`epzero/auto.h`), so C++
106
+ users get the same `build_auto()`.
107
+
108
+ If your queries differ from your data (text queries against image vectors, say), pass a few
109
+ hundred of them as `queries=`: recall calibrated on data rows is optimistic for those.
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
+
135
+ ## Configured by hand
136
+
137
+ ```python
138
+ vectors = np.random.rand(100_000, 128).astype(np.float32)
139
+ index = epzero.Index(dim=128, metric="l2", max_elements=100_000, M=16, ef_construction=200)
140
+ index.add(vectors) # labels 0..n-1 (or pass labels=); grows as needed
141
+
142
+ labels, distances = index.search(vectors[:5], k=10, ef=64)
143
+
144
+ # Filtered search: compile the allowed labels once, reuse across queries.
145
+ allowed = index.filter(np.arange(0, 100_000, 50))
146
+ labels, distances = index.search(vectors[:5], k=10, filter=allowed)
147
+
148
+ # Hold recall at 0.95 as the workload drifts, checking 1% of queries; per_query=True lets each
149
+ # query stop once its answer settles (cheaper for the same recall).
150
+ guard = epzero.RecallGuard(index, target=0.95, sample_rate=0.01, reference_ef=1024, per_query=True)
151
+ labels, distances = guard.search(vectors[:5], k=10)
152
+ print(guard.report())
153
+
154
+ # Optional: measure the best memory-prefetch setting for this machine and data (not saved).
155
+ index.tune_prefetch(vectors[:500])
156
+
157
+ index.save("vectors.epzero")
158
+ index = epzero.Index.load("vectors.epzero")
159
+
160
+ # Hybrid: keyword search on the same labels, merged with the vector search.
161
+ text = epzero.TextIndex()
162
+ text.add(None, documents) # labels 0..n-1, matching the index
163
+ labels, scores = epzero.hybrid_search(index, text, query_vector, "error E-1042 at checkout", k=10)
164
+ ```
165
+
166
+ ## LangChain
167
+
168
+ ```python
169
+ from langchain_openai import OpenAIEmbeddings
170
+ from epzero.langchain import EpzeroVectorStore
171
+
172
+ store = EpzeroVectorStore.from_texts(texts, OpenAIEmbeddings(), metadatas=metadatas)
173
+ store.similarity_search("how do I reset my password?", k=4, filter={"source": "faq"})
174
+ store.max_marginal_relevance_search("pricing", k=4, fetch_k=20)
175
+ store.hybrid_search("error E-1042 at checkout", k=4) # keywords + vectors
176
+ retriever = store.as_retriever(search_kwargs={"k": 8})
177
+
178
+ store.save_local("./store")
179
+ store = EpzeroVectorStore.load_local("./store", OpenAIEmbeddings())
180
+ ```
181
+
182
+ Metadata filters are a dict of required values (a list means "any of"), or a callable on the
183
+ metadata. They are compiled into an Epzero filter, so the engine chooses how to search the allowed
184
+ documents instead of over-fetching and discarding. The store passes LangChain's standard
185
+ vector-store test suite.
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
+
237
+ ## Platforms
238
+
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).
241
+
242
+ Live demo, benchmarks and design notes: https://epzero.dev/ · MIT license.
@@ -40,6 +40,16 @@ 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 half = false;
47
+ bool fill_after = false;
48
+ std::size_t refine = 0;
49
+ bool refine_exact = false;
50
+ std::uint64_t seed = 42;
51
+ float rerank_z = 0.0f;
52
+ std::size_t refine_ef = 0;
43
53
  std::size_t rerank = 0;
44
54
  std::size_t prefetch_depth = 0, prefetch_lines = 0; // 0: the index's default
45
55
  std::vector<std::size_t> ef = {10, 20, 40, 80, 160, 320};
@@ -86,6 +96,16 @@ Options parse(int argc, char** argv) {
86
96
  }
87
97
  else if (arg == "--no-keep") o.keep_vectors = false;
88
98
  else if (arg == "--rerank") o.rerank = std::stoul(value());
99
+ else if (arg == "--nav-dim") o.nav_dim = std::stoul(value());
100
+ else if (arg == "--fill-links") o.fill_links = true;
101
+ else if (arg == "--fast-build") o.fast_build = true;
102
+ else if (arg == "--half") o.half = true;
103
+ else if (arg == "--fill-after") o.fill_after = true;
104
+ else if (arg == "--refine") o.refine = std::stoul(value());
105
+ else if (arg == "--refine-exact") o.refine_exact = true;
106
+ else if (arg == "--seed") o.seed = std::stoull(value());
107
+ else if (arg == "--rerank-z") o.rerank_z = std::stof(value());
108
+ else if (arg == "--refine-ef") o.refine_ef = std::stoul(value());
89
109
  else if (arg == "--prefetch") { // depth,lines
90
110
  const auto v = parse_list(value());
91
111
  if (v.size() != 2) throw std::invalid_argument("--prefetch takes depth,lines");
@@ -171,8 +191,9 @@ int main(int argc, char** argv) try {
171
191
  std::printf("load %s in %.2fs\n", opt.load.c_str(), seconds_since(t0));
172
192
  } else {
173
193
  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};
194
+ .ef_construction = opt.ef_construction, .seed = opt.seed, .quantization = opt.quantization,
195
+ .keep_vectors = opt.keep_vectors, .nav_dim = opt.nav_dim, .fill_links = opt.fill_links,
196
+ .fast_build = opt.fast_build, .half_vectors = opt.half};
176
197
  loaded = std::make_unique<HnswIndex>(params);
177
198
  t0 = Clock::now();
178
199
  loaded->train(base.data.data(), std::min<std::size_t>(base.n, 100'000)); // no-op unquantized
@@ -184,6 +205,16 @@ int main(int argc, char** argv) try {
184
205
  });
185
206
  }
186
207
  for (auto& b : builders) b.join();
208
+ for (std::size_t pass = 0; pass < opt.refine; ++pass) {
209
+ const double refine_at = seconds_since(t0);
210
+ loaded->refine_links(opt.threads, opt.refine_ef, opt.refine_exact);
211
+ std::printf("refined pass %zu in %.2fs\n", pass + 1, seconds_since(t0) - refine_at);
212
+ }
213
+ if (opt.fill_after) {
214
+ const double links_at = seconds_since(t0);
215
+ const std::size_t added = loaded->fill_free_links(opt.threads);
216
+ std::printf("filled %zu links in %.2fs\n", added, seconds_since(t0) - links_at);
217
+ }
187
218
  build_s = seconds_since(t0);
188
219
  std::printf("build %.2fs, %.0f inserts/s (%zu threads, M=%zu, efc=%zu)\n", build_s, base.n / build_s,
189
220
  opt.threads, opt.M, opt.ef_construction);
@@ -230,7 +261,7 @@ int main(int argc, char** argv) try {
230
261
  const std::size_t q = i % queries.n;
231
262
  SearchStats stats;
232
263
  const auto start = Clock::now();
233
- auto found = index.search(queries.row(q), opt.k, {.ef = ef, .rerank = opt.rerank, .stats = &stats});
264
+ auto found = index.search(queries.row(q), opt.k, {.ef = ef, .rerank = opt.rerank, .rerank_z = opt.rerank_z, .stats = &stats});
234
265
  latencies[i] = std::chrono::duration<double, std::micro>(Clock::now() - start).count();
235
266
  distances_per_query[i] = stats.distance_computations;
236
267
  full_per_query[i] = stats.full_distance_computations;
@@ -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
+ }