epzero 0.3.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 (60) hide show
  1. epzero-0.3.0/CMakeLists.txt +219 -0
  2. epzero-0.3.0/PKG-INFO +114 -0
  3. epzero-0.3.0/bench/bench_adaptive.cpp +171 -0
  4. epzero-0.3.0/bench/bench_churn.cpp +297 -0
  5. epzero-0.3.0/bench/bench_distance.cpp +58 -0
  6. epzero-0.3.0/bench/bench_filter.cpp +236 -0
  7. epzero-0.3.0/bench/bench_guard.cpp +305 -0
  8. epzero-0.3.0/bench/bench_recall.cpp +285 -0
  9. epzero-0.3.0/fuzz/common.h +41 -0
  10. epzero-0.3.0/fuzz/fuzz_graph.cpp +22 -0
  11. epzero-0.3.0/fuzz/fuzz_snapshot.cpp +19 -0
  12. epzero-0.3.0/fuzz/fuzz_text.cpp +21 -0
  13. epzero-0.3.0/fuzz/fuzz_wal.cpp +28 -0
  14. epzero-0.3.0/fuzz/make_seeds.cpp +57 -0
  15. epzero-0.3.0/fuzz/regressions/graph-empty-with-entry-point +0 -0
  16. epzero-0.3.0/fuzz/regressions/graph-entry-level-overflow +0 -0
  17. epzero-0.3.0/fuzz/regressions/graph-upper-link-to-base-node +0 -0
  18. epzero-0.3.0/fuzz/regressions/snapshot-empty-with-entry-point +0 -0
  19. epzero-0.3.0/fuzz/regressions/snapshot-huge-capacity +0 -0
  20. epzero-0.3.0/include/epzero/brute_force.h +28 -0
  21. epzero-0.3.0/include/epzero/database.h +134 -0
  22. epzero-0.3.0/include/epzero/dataset.h +34 -0
  23. epzero-0.3.0/include/epzero/distance.h +65 -0
  24. epzero-0.3.0/include/epzero/hnsw.h +376 -0
  25. epzero-0.3.0/include/epzero/parallel.h +47 -0
  26. epzero-0.3.0/include/epzero/recall.h +162 -0
  27. epzero-0.3.0/include/epzero/text.h +105 -0
  28. epzero-0.3.0/pyproject.toml +57 -0
  29. epzero-0.3.0/python/README.md +85 -0
  30. epzero-0.3.0/python/epzero/__init__.py +36 -0
  31. epzero-0.3.0/python/epzero/langchain.py +346 -0
  32. epzero-0.3.0/python/epzero_py.cpp +581 -0
  33. epzero-0.3.0/python/tests/test_epzero.py +251 -0
  34. epzero-0.3.0/python/tests/test_langchain.py +114 -0
  35. epzero-0.3.0/server/node_main.cpp +435 -0
  36. epzero-0.3.0/src/brute_force.cpp +156 -0
  37. epzero-0.3.0/src/database.cpp +451 -0
  38. epzero-0.3.0/src/dataset.cpp +88 -0
  39. epzero-0.3.0/src/distance.cpp +208 -0
  40. epzero-0.3.0/src/distance_avx2.cpp +124 -0
  41. epzero-0.3.0/src/distance_avx512.cpp +83 -0
  42. epzero-0.3.0/src/distance_avx512vnni.cpp +34 -0
  43. epzero-0.3.0/src/distance_avxvnni.cpp +37 -0
  44. epzero-0.3.0/src/distance_neon.cpp +82 -0
  45. epzero-0.3.0/src/distance_scalar.cpp +53 -0
  46. epzero-0.3.0/src/distance_wasm.cpp +97 -0
  47. epzero-0.3.0/src/hnsw.cpp +1763 -0
  48. epzero-0.3.0/src/kernels.h +46 -0
  49. epzero-0.3.0/src/rabitq.h +162 -0
  50. epzero-0.3.0/src/recall.cpp +192 -0
  51. epzero-0.3.0/src/text.cpp +303 -0
  52. epzero-0.3.0/src/visited_pool.h +87 -0
  53. epzero-0.3.0/tests/database_test.cpp +303 -0
  54. epzero-0.3.0/tests/dataset_test.cpp +74 -0
  55. epzero-0.3.0/tests/distance_test.cpp +112 -0
  56. epzero-0.3.0/tests/hnsw_test.cpp +733 -0
  57. epzero-0.3.0/tests/recall_test.cpp +163 -0
  58. epzero-0.3.0/tests/text_test.cpp +143 -0
  59. epzero-0.3.0/tools/export_graph.cpp +74 -0
  60. epzero-0.3.0/wasm/epzero_wasm.cpp +690 -0
@@ -0,0 +1,219 @@
1
+ cmake_minimum_required(VERSION 3.20)
2
+ project(epzero VERSION 0.1.0 LANGUAGES CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 20)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ set(CMAKE_CXX_EXTENSIONS OFF)
7
+ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
8
+
9
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
10
+ set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
11
+ endif()
12
+
13
+ option(EPZERO_BUILD_TESTS "Build unit tests" ON)
14
+ option(EPZERO_BUILD_BENCH "Build benchmarks" ON)
15
+ option(EPZERO_BUILD_PYTHON "Build the Python module (normally via pip install ./engine)" OFF)
16
+ set(EPZERO_SANITIZE "" CACHE STRING "Sanitizer to enable: address, thread, or undefined")
17
+
18
+ if(EPZERO_SANITIZE)
19
+ add_compile_options(-fsanitize=${EPZERO_SANITIZE} -fno-omit-frame-pointer -g)
20
+ add_link_options(-fsanitize=${EPZERO_SANITIZE})
21
+ endif()
22
+
23
+ # Fuzzing (Clang + libFuzzer): every parser of untrusted bytes, under AddressSanitizer and UBSan.
24
+ # The engine is instrumented for coverage; only the fuzz targets link libFuzzer's main().
25
+ option(EPZERO_BUILD_FUZZ "Build the libFuzzer targets (Clang only)" OFF)
26
+ if(EPZERO_BUILD_FUZZ)
27
+ if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
28
+ message(FATAL_ERROR "EPZERO_BUILD_FUZZ needs Clang (libFuzzer)")
29
+ endif()
30
+ add_compile_options(-fsanitize=fuzzer-no-link,address,undefined -fno-sanitize-recover=undefined
31
+ -fno-omit-frame-pointer -g)
32
+ add_link_options(-fsanitize=address,undefined)
33
+ endif()
34
+
35
+ # WebAssembly: fixed-width SIMD is supported by every browser the demo targets, so the whole
36
+ # build uses it (there is no runtime feature detection in WASM). Threads map to Web Workers
37
+ # sharing one SharedArrayBuffer, which requires a cross-origin isolated page.
38
+ option(EPZERO_WASM_THREADS "WebAssembly build uses threads (needs a cross-origin isolated page)" ON)
39
+ if(EMSCRIPTEN)
40
+ add_compile_options(-msimd128 -fwasm-exceptions)
41
+ add_link_options(-fwasm-exceptions -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=4GB)
42
+ if(EPZERO_WASM_THREADS)
43
+ add_compile_options(-pthread)
44
+ add_link_options(-pthread -Wno-pthreads-mem-growth)
45
+ endif()
46
+ endif()
47
+
48
+ find_package(Threads REQUIRED)
49
+
50
+ add_library(epzero_engine
51
+ src/distance.cpp
52
+ src/distance_scalar.cpp
53
+ src/hnsw.cpp
54
+ src/recall.cpp
55
+ src/database.cpp
56
+ src/text.cpp
57
+ src/brute_force.cpp
58
+ src/dataset.cpp
59
+ )
60
+ target_include_directories(epzero_engine PUBLIC include PRIVATE src)
61
+ target_link_libraries(epzero_engine PUBLIC Threads::Threads)
62
+ target_compile_options(epzero_engine PRIVATE -Wall -Wextra -Wpedantic)
63
+ set_target_properties(epzero_engine PROPERTIES POSITION_INDEPENDENT_CODE ON)
64
+
65
+ # SIMD kernels get ISA flags per file; the rest of the library stays baseline so the binary
66
+ # runs on any CPU of the architecture and picks the best kernel at runtime.
67
+ 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")
71
+ # VNNI integer kernels, where the toolchain can build them: the compiler must know the
72
+ # instructions (GCC 11+, Clang 12+ for AVX-VNNI) and so must the assembler it hands them to
73
+ # (binutils 2.36+ for AVX-VNNI; RHEL 9's 2.35 doesn't), so each check compiles and links a use.
74
+ include(CheckCXXSourceCompiles)
75
+ set(CMAKE_REQUIRED_FLAGS "-mavx2 -mavxvnni")
76
+ check_cxx_source_compiles([[
77
+ #include <immintrin.h>
78
+ int main(int argc, char**) {
79
+ __m256i a = _mm256_set1_epi8(static_cast<char>(argc));
80
+ a = _mm256_dpbusd_avx_epi32(a, a, a);
81
+ return _mm256_extract_epi32(a, 0);
82
+ }]] EPZERO_TOOLCHAIN_AVXVNNI)
83
+ set(CMAKE_REQUIRED_FLAGS "-mavx512f -mavx512bw -mavx512vnni")
84
+ check_cxx_source_compiles([[
85
+ #include <immintrin.h>
86
+ int main(int argc, char**) {
87
+ __m512i a = _mm512_set1_epi8(static_cast<char>(argc));
88
+ a = _mm512_dpbusd_epi32(a, a, a);
89
+ return _mm512_reduce_add_epi32(a);
90
+ }]] EPZERO_TOOLCHAIN_AVX512VNNI)
91
+ unset(CMAKE_REQUIRED_FLAGS)
92
+ if(EPZERO_TOOLCHAIN_AVXVNNI)
93
+ target_sources(epzero_engine PRIVATE src/distance_avxvnni.cpp)
94
+ set_source_files_properties(src/distance_avxvnni.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mavxvnni")
95
+ set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVXVNNI)
96
+ endif()
97
+ if(EPZERO_TOOLCHAIN_AVX512VNNI)
98
+ target_sources(epzero_engine PRIVATE src/distance_avx512vnni.cpp)
99
+ set_source_files_properties(src/distance_avx512vnni.cpp PROPERTIES COMPILE_OPTIONS "-mavx512f;-mavx512bw;-mavx512vnni")
100
+ set_property(SOURCE src/distance.cpp APPEND PROPERTY COMPILE_DEFINITIONS EPZERO_HAVE_AVX512VNNI)
101
+ endif()
102
+ elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
103
+ target_sources(epzero_engine PRIVATE src/distance_neon.cpp)
104
+ elseif(EMSCRIPTEN)
105
+ target_sources(epzero_engine PRIVATE src/distance_wasm.cpp)
106
+ endif()
107
+
108
+ if(EMSCRIPTEN AND NOT EPZERO_WASM_THREADS)
109
+ set(EPZERO_BUILD_TESTS OFF) # the concurrency tests need threads
110
+ endif()
111
+
112
+ if(EPZERO_BUILD_TESTS)
113
+ include(FetchContent)
114
+ FetchContent_Declare(googletest
115
+ URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz
116
+ DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
117
+ set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
118
+ FetchContent_MakeAvailable(googletest)
119
+
120
+ 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
+ target_link_libraries(epzero_tests PRIVATE epzero_engine GTest::gtest_main)
123
+ if(EMSCRIPTEN)
124
+ # Runs under Node (CMAKE_CROSSCOMPILING_EMULATOR); workers are pre-spawned for the
125
+ # concurrency tests and the real filesystem backs the save/load tests.
126
+ target_link_options(epzero_tests PRIVATE -sPTHREAD_POOL_SIZE=16 -sNODERAWFS=1 -sENVIRONMENT=node)
127
+ endif()
128
+ include(GoogleTest)
129
+ gtest_discover_tests(epzero_tests DISCOVERY_TIMEOUT 60)
130
+ endif()
131
+
132
+ if(EPZERO_BUILD_BENCH)
133
+ add_executable(bench_distance bench/bench_distance.cpp)
134
+ target_link_libraries(bench_distance PRIVATE epzero_engine)
135
+ add_executable(bench_recall bench/bench_recall.cpp)
136
+ target_link_libraries(bench_recall PRIVATE epzero_engine)
137
+ add_executable(bench_filter bench/bench_filter.cpp)
138
+ target_link_libraries(bench_filter PRIVATE epzero_engine)
139
+ add_executable(bench_churn bench/bench_churn.cpp)
140
+ target_link_libraries(bench_churn PRIVATE epzero_engine)
141
+ add_executable(bench_guard bench/bench_guard.cpp)
142
+ target_link_libraries(bench_guard PRIVATE epzero_engine)
143
+ add_executable(bench_adaptive bench/bench_adaptive.cpp)
144
+ target_link_libraries(bench_adaptive PRIVATE epzero_engine)
145
+ endif()
146
+
147
+ if(EPZERO_BUILD_FUZZ)
148
+ foreach(target snapshot graph wal text)
149
+ add_executable(fuzz_${target} fuzz/fuzz_${target}.cpp)
150
+ target_link_libraries(fuzz_${target} PRIVATE epzero_engine)
151
+ target_link_options(fuzz_${target} PRIVATE -fsanitize=fuzzer)
152
+ endforeach()
153
+ add_executable(fuzz_seeds fuzz/make_seeds.cpp)
154
+ target_link_libraries(fuzz_seeds PRIVATE epzero_engine)
155
+ endif()
156
+
157
+ # Writes the web demo's prebuilt graph (see tools/export_graph.cpp and web/scripts/build-graph.sh).
158
+ if(NOT EMSCRIPTEN)
159
+ add_executable(epzero_export_graph tools/export_graph.cpp)
160
+ set_target_properties(epzero_export_graph PROPERTIES OUTPUT_NAME epzero-export-graph)
161
+ target_link_libraries(epzero_export_graph PRIVATE epzero_engine)
162
+ endif()
163
+
164
+ option(EPZERO_BUILD_NODE "Build the cluster data node (needs gRPC C++ and protobuf)" ON)
165
+ if(EPZERO_BUILD_NODE AND NOT EMSCRIPTEN)
166
+ find_package(PkgConfig)
167
+ if(PkgConfig_FOUND)
168
+ pkg_check_modules(GRPCPP IMPORTED_TARGET grpc++)
169
+ pkg_check_modules(PROTOBUF IMPORTED_TARGET protobuf)
170
+ endif()
171
+ find_program(PROTOC protoc)
172
+ find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin)
173
+ if(GRPCPP_FOUND AND PROTOBUF_FOUND AND PROTOC AND GRPC_CPP_PLUGIN)
174
+ set(PROTO_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../proto)
175
+ set(PROTO_OUT ${CMAKE_CURRENT_BINARY_DIR}/proto)
176
+ file(MAKE_DIRECTORY ${PROTO_OUT})
177
+ set(PROTO_SRCS ${PROTO_OUT}/epzero/v1/epzero.pb.cc ${PROTO_OUT}/epzero/v1/epzero.grpc.pb.cc)
178
+ add_custom_command(
179
+ OUTPUT ${PROTO_SRCS}
180
+ COMMAND ${PROTOC} -I ${PROTO_ROOT} --cpp_out=${PROTO_OUT} --grpc_out=${PROTO_OUT}
181
+ --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN} epzero/v1/epzero.proto
182
+ DEPENDS ${PROTO_ROOT}/epzero/v1/epzero.proto
183
+ COMMENT "Generating gRPC code for epzero.proto")
184
+ add_executable(epzero_node server/node_main.cpp ${PROTO_SRCS})
185
+ set_target_properties(epzero_node PROPERTIES OUTPUT_NAME epzero-node)
186
+ target_include_directories(epzero_node PRIVATE ${PROTO_OUT})
187
+ target_link_libraries(epzero_node PRIVATE epzero_engine PkgConfig::GRPCPP PkgConfig::PROTOBUF)
188
+ install(TARGETS epzero_node RUNTIME DESTINATION bin)
189
+ else()
190
+ message(STATUS "gRPC C++ not found: skipping epzero-node")
191
+ endif()
192
+ endif()
193
+
194
+ 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)
198
+ 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()
209
+ endif()
210
+
211
+ if(EPZERO_BUILD_PYTHON)
212
+ find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
213
+ find_package(pybind11 CONFIG REQUIRED)
214
+ pybind11_add_module(epzero_python python/epzero_py.cpp)
215
+ # The compiled core of the Python package epzero (engine/python/epzero), installed inside it.
216
+ set_target_properties(epzero_python PROPERTIES OUTPUT_NAME _core)
217
+ target_link_libraries(epzero_python PRIVATE epzero_engine)
218
+ install(TARGETS epzero_python DESTINATION epzero)
219
+ endif()
epzero-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.2
2
+ Name: epzero
3
+ Version: 0.3.0
4
+ Summary: An HNSW vector database built from scratch in C++20: 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: Topic :: Database
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Project-URL: Homepage, https://epzero.dev/
18
+ Project-URL: Source, https://github.com/Lazeeez/Epzero
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: numpy
21
+ Provides-Extra: langchain
22
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest; extra == "test"
25
+ Requires-Dist: pytest-asyncio; extra == "test"
26
+ Requires-Dist: langchain-core>=0.3; extra == "test"
27
+ Requires-Dist: langchain-tests>=0.3; extra == "test"
28
+ Description-Content-Type: text/markdown
29
+
30
+ # epzero
31
+
32
+ An HNSW vector database written from scratch in C++20, with Python bindings.
33
+
34
+ - **Fast search:** hand-written SIMD kernels (AVX2, AVX-512, NEON), chosen at runtime, and
35
+ lock-free reads. On SIFT1M it serves 1.13× hnswlib's and 1.17× FAISS's queries per second at
36
+ equal recall, on the same machine.
37
+ - **Filtered search that picks its own strategy:** an exact scan, a filtered graph walk, or a
38
+ two-hop walk (the idea behind ACORN), chosen per query by a cost model.
39
+ - **Recall you can see:** `RecallGuard` measures recall on a sample of live queries with a
40
+ confidence interval and moves the search effort to hold a target; `explain()` says why any given
41
+ item was not returned. With `per_query=True` each query also stops as soon as its answer settles,
42
+ so easy queries cost less and hard ones search further.
43
+ - **Hybrid search:** `TextIndex` (BM25 keyword search) and `hybrid_search()`, which merges it
44
+ with vector search by reciprocal rank fusion.
45
+ - **Durable:** `Database` adds a write-ahead log with crash recovery and checkpoints.
46
+ - **8-bit quantization** with exact re-ranking.
47
+ - **A LangChain vector store** in `epzero.langchain`.
48
+
49
+ ```bash
50
+ pip install epzero # the engine
51
+ pip install "epzero[langchain]" # plus the LangChain vector store
52
+ ```
53
+
54
+ ## Quick start
55
+
56
+ ```python
57
+ import numpy as np
58
+ import epzero
59
+
60
+ vectors = np.random.rand(100_000, 128).astype(np.float32)
61
+ index = epzero.Index(dim=128, metric="l2", max_elements=100_000, M=16, ef_construction=200)
62
+ index.add(vectors) # labels 0..n-1 (or pass labels=)
63
+
64
+ labels, distances = index.search(vectors[:5], k=10, ef=64)
65
+
66
+ # Filtered search: compile the allowed labels once, reuse across queries.
67
+ allowed = index.filter(np.arange(0, 100_000, 50))
68
+ labels, distances = index.search(vectors[:5], k=10, filter=allowed)
69
+
70
+ # Hold recall at 0.95 as the workload drifts, checking 1% of queries; per_query=True lets each
71
+ # query stop once its answer settles (cheaper for the same recall).
72
+ guard = epzero.RecallGuard(index, target=0.95, sample_rate=0.01, reference_ef=1024, per_query=True)
73
+ labels, distances = guard.search(vectors[:5], k=10)
74
+ print(guard.report())
75
+
76
+ # Optional: measure the best memory-prefetch setting for this machine and data (not saved).
77
+ index.tune_prefetch(vectors[:500])
78
+
79
+ index.save("vectors.epzero")
80
+ index = epzero.Index.load("vectors.epzero")
81
+
82
+ # Hybrid: keyword search on the same labels, merged with the vector search.
83
+ text = epzero.TextIndex()
84
+ text.add(None, documents) # labels 0..n-1, matching the index
85
+ labels, scores = epzero.hybrid_search(index, text, query_vector, "error E-1042 at checkout", k=10)
86
+ ```
87
+
88
+ ## LangChain
89
+
90
+ ```python
91
+ from langchain_openai import OpenAIEmbeddings
92
+ from epzero.langchain import EpzeroVectorStore
93
+
94
+ store = EpzeroVectorStore.from_texts(texts, OpenAIEmbeddings(), metadatas=metadatas)
95
+ store.similarity_search("how do I reset my password?", k=4, filter={"source": "faq"})
96
+ store.max_marginal_relevance_search("pricing", k=4, fetch_k=20)
97
+ store.hybrid_search("error E-1042 at checkout", k=4) # keywords + vectors
98
+ retriever = store.as_retriever(search_kwargs={"k": 8})
99
+
100
+ store.save_local("./store")
101
+ store = EpzeroVectorStore.load_local("./store", OpenAIEmbeddings())
102
+ ```
103
+
104
+ Metadata filters are a dict of required values (a list means "any of"), or a callable on the
105
+ metadata. They are compiled into an Epzero filter, so the engine chooses how to search the allowed
106
+ documents instead of over-fetching and discarding. The store passes LangChain's standard
107
+ vector-store test suite.
108
+
109
+ ## Platforms
110
+
111
+ Wheels for Linux (x86-64 and ARM64, glibc 2.28+) and macOS (Apple silicon and Intel), Python 3.10
112
+ to 3.13. Elsewhere, `pip install epzero` builds from source (CMake 3.20+, a C++20 compiler).
113
+
114
+ Live demo, benchmarks and design notes: https://epzero.dev/ · MIT license.
@@ -0,0 +1,171 @@
1
+ // Per-query effort: a fixed beam width for every query against patience, which stops each query
2
+ // once its best k have stopped changing (SearchOptions::patience, with ef as a cap).
3
+ //
4
+ // A fixed ef spends the same effort on every query: too much on easy ones, too little on the
5
+ // hardest. For each setting this reports mean recall@k and mean distance computations (the cost
6
+ // curve), and the tail: the share of queries answered badly (recall@k below 0.8 and 0.5), the
7
+ // 99th-percentile cost, and single-thread throughput.
8
+ //
9
+ // bench_adaptive --base sift_base.fvecs --query sift_query.fvecs --truth sift_groundtruth.ivecs \
10
+ // --index sift.idx --json adaptive.json
11
+ // (--metric cosine for angular datasets such as GloVe; ground truth as ivecs, top-k first)
12
+
13
+ #include <algorithm>
14
+ #include <chrono>
15
+ #include <cstdio>
16
+ #include <filesystem>
17
+ #include <fstream>
18
+ #include <stdexcept>
19
+ #include <string>
20
+ #include <thread>
21
+ #include <vector>
22
+
23
+ #include "epzero/dataset.h"
24
+ #include "epzero/hnsw.h"
25
+ #include "epzero/parallel.h"
26
+
27
+ using namespace epzero;
28
+
29
+ namespace {
30
+
31
+ struct Options {
32
+ std::string base, query, truth, index, json, name = "dataset";
33
+ Metric metric = Metric::L2;
34
+ std::size_t k = 10, M = 16, ef_construction = 200, threads = 0, queries = 0, rounds = 3;
35
+ std::vector<std::size_t> efs = {10, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024};
36
+ std::vector<std::size_t> patiences = {2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128};
37
+ std::vector<std::size_t> caps = {256, 1024};
38
+ };
39
+
40
+ std::vector<std::size_t> list(const std::string& s) {
41
+ std::vector<std::size_t> out;
42
+ std::size_t start = 0;
43
+ while (start < s.size()) {
44
+ const std::size_t comma = s.find(',', start);
45
+ out.push_back(std::stoul(s.substr(start, comma - start)));
46
+ if (comma == std::string::npos) break;
47
+ start = comma + 1;
48
+ }
49
+ return out;
50
+ }
51
+
52
+ Options parse(int argc, char** argv) {
53
+ Options o;
54
+ for (int i = 1; i < argc; ++i) {
55
+ const std::string arg = argv[i];
56
+ auto value = [&]() -> std::string {
57
+ if (i + 1 >= argc) throw std::invalid_argument(arg + " needs a value");
58
+ return argv[++i];
59
+ };
60
+ if (arg == "--base") o.base = value();
61
+ else if (arg == "--query") o.query = value();
62
+ else if (arg == "--truth") o.truth = value();
63
+ else if (arg == "--index") o.index = value();
64
+ else if (arg == "--json") o.json = value();
65
+ else if (arg == "--name") o.name = value();
66
+ else if (arg == "--metric") o.metric = value() == "cosine" ? Metric::Cosine : Metric::L2;
67
+ else if (arg == "--k") o.k = std::stoul(value());
68
+ else if (arg == "--M") o.M = std::stoul(value());
69
+ else if (arg == "--efc") o.ef_construction = std::stoul(value());
70
+ else if (arg == "--threads") o.threads = std::stoul(value());
71
+ else if (arg == "--queries") o.queries = std::stoul(value());
72
+ else if (arg == "--rounds") o.rounds = std::stoul(value());
73
+ else if (arg == "--efs") o.efs = list(value());
74
+ else if (arg == "--patience") o.patiences = list(value());
75
+ else if (arg == "--caps") o.caps = list(value());
76
+ else throw std::invalid_argument("unknown argument " + arg);
77
+ }
78
+ if (o.base.empty() || o.query.empty() || o.truth.empty()) throw std::invalid_argument("pass --base, --query and --truth");
79
+ if (o.threads == 0) o.threads = std::max(1u, std::thread::hardware_concurrency());
80
+ return o;
81
+ }
82
+
83
+ struct Row {
84
+ std::string policy; // "fixed" or "patience"
85
+ std::size_t ef, patience;
86
+ double recall, distances, p99_distances, below_08, below_05, qps;
87
+ };
88
+
89
+ } // namespace
90
+
91
+ int main(int argc, char** argv) try {
92
+ const Options opt = parse(argc, argv);
93
+ const Dataset queries = read_fvecs(opt.query, opt.queries);
94
+ const auto truth = read_ivecs(opt.truth, queries.n);
95
+ const std::size_t nq = queries.n, k = opt.k;
96
+
97
+ std::unique_ptr<HnswIndex> index;
98
+ if (!opt.index.empty() && std::filesystem::exists(opt.index)) {
99
+ index = HnswIndex::load(opt.index);
100
+ std::printf("loaded %s (%zu vectors)\n", opt.index.c_str(), index->size());
101
+ } else {
102
+ const Dataset base = read_fvecs(opt.base);
103
+ index = std::make_unique<HnswIndex>(HnswParams{.dim = base.dim, .metric = opt.metric, .max_elements = base.n,
104
+ .M = opt.M, .ef_construction = opt.ef_construction});
105
+ const auto start = std::chrono::steady_clock::now();
106
+ parallel_for(0, base.n, opt.threads, [&](std::size_t i) { index->add(i, base.row(i)); });
107
+ std::printf("built %zu x %zu in %.1f s\n", base.n, base.dim,
108
+ std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count());
109
+ if (!opt.index.empty()) index->save(opt.index);
110
+ }
111
+
112
+ // One setting: per-query recall and cost, then throughput as the best of `rounds` timed passes.
113
+ const auto run = [&](const std::string& policy, std::size_t ef, std::size_t patience) {
114
+ std::vector<double> recall(nq), cost(nq);
115
+ for (std::size_t q = 0; q < nq; ++q) {
116
+ SearchStats stats;
117
+ const auto got = index->search(queries.row(q), k, {.ef = ef, .patience = patience, .stats = &stats});
118
+ std::size_t hits = 0;
119
+ for (std::size_t t = 0; t < k && t < truth[q].size(); ++t) {
120
+ hits += std::any_of(got.begin(), got.end(), [&](const SearchResult& r) { return r.label == truth[q][t]; });
121
+ }
122
+ recall[q] = static_cast<double>(hits) / static_cast<double>(k);
123
+ cost[q] = static_cast<double>(stats.distance_computations);
124
+ }
125
+ double best = 0;
126
+ for (std::size_t r = 0; r < opt.rounds; ++r) {
127
+ const auto start = std::chrono::steady_clock::now();
128
+ for (std::size_t q = 0; q < nq; ++q) index->search(queries.row(q), k, {.ef = ef, .patience = patience});
129
+ best = std::max(best, static_cast<double>(nq) / std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count());
130
+ }
131
+ Row row{policy, ef, patience, 0, 0, 0, 0, 0, best};
132
+ for (std::size_t q = 0; q < nq; ++q) {
133
+ row.recall += recall[q] / static_cast<double>(nq);
134
+ row.distances += cost[q] / static_cast<double>(nq);
135
+ row.below_08 += (recall[q] < 0.8) / static_cast<double>(nq);
136
+ row.below_05 += (recall[q] < 0.5) / static_cast<double>(nq);
137
+ }
138
+ std::sort(cost.begin(), cost.end());
139
+ row.p99_distances = cost[std::min(nq - 1, nq * 99 / 100)];
140
+ std::printf("%-8s ef %5zu patience %4zu recall %.4f dist %7.0f p99 dist %7.0f <0.8 %5.2f%% <0.5 %5.2f%% qps %8.0f\n",
141
+ policy.c_str(), ef, patience, row.recall, row.distances, row.p99_distances, 100 * row.below_08,
142
+ 100 * row.below_05, row.qps);
143
+ return row;
144
+ };
145
+
146
+ std::vector<Row> rows;
147
+ for (const auto ef : opt.efs) rows.push_back(run("fixed", ef, 0));
148
+ for (const auto cap : opt.caps) {
149
+ for (const auto p : opt.patiences) rows.push_back(run("patience", cap, p));
150
+ }
151
+
152
+ if (!opt.json.empty()) {
153
+ std::ofstream out(opt.json);
154
+ out << "{\n \"dataset\": \"" << opt.name << "\", \"n\": " << index->size() << ", \"dim\": " << index->params().dim
155
+ << ", \"queries\": " << nq << ", \"k\": " << k << ", \"M\": " << index->params().M
156
+ << ", \"ef_construction\": " << index->params().ef_construction << ",\n \"rows\": [\n";
157
+ for (std::size_t i = 0; i < rows.size(); ++i) {
158
+ const Row& r = rows[i];
159
+ out << " {\"policy\": \"" << r.policy << "\", \"ef\": " << r.ef << ", \"patience\": " << r.patience
160
+ << ", \"recall\": " << r.recall << ", \"distances\": " << r.distances << ", \"p99_distances\": " << r.p99_distances
161
+ << ", \"below_0.8\": " << r.below_08 << ", \"below_0.5\": " << r.below_05 << ", \"qps\": " << r.qps << "}"
162
+ << (i + 1 < rows.size() ? "," : "") << "\n";
163
+ }
164
+ out << " ]\n}\n";
165
+ std::printf("wrote %s\n", opt.json.c_str());
166
+ }
167
+ return 0;
168
+ } catch (const std::exception& e) {
169
+ std::fprintf(stderr, "error: %s\n", e.what());
170
+ return 1;
171
+ }