# Local semantic search Searchi uses **Google EmbeddingGemma 2**, with the pinned Q8 GGUF text encoder and llama.cpp revision used by DigUp. Production has no MiniLM, simulated-vector, or remote embedding fallback. The rejected ONNX experiment is retained only under the opt-in minilm-proof feature and tools/semantic-proof/HISTORICAL.md. ## Model and runtime - Model: google/embeddinggemma-2, **768-dimensional** output. - GGUF publisher: ggml-org/embeddinggemma-2-GGUF. - Revision: bfcd298762cc34d0357ece5ebdd31791a3a374d8. - llama.cpp: b11461, commit 4d756bc72bf00a4aacf410ae15a2d315f3db400d, with DigUp's no-logits memory patch. - Query prompt: task: search result | query: TEXT. - Document prompt: title: TITLE_OR_none | text: TEXT. - Noncausal encoder, mean pooling, finite L2-normalized vectors. - Query/document token budgets: 512/2048. The persistent native helper uses bounded NDJSON over stdin/stdout. It has no HTTP server or download code. Rust verifies the text model's length and SHA256 before starting it. Setup provisions assets; search never downloads them. Production device mode is auto, with available compiled acceleration and CPU fallback. Model/context initialization and returned inference/nonfinite-output failures retry on CPU once. A lost GPU worker can be restarted as CPU for one retry of that request. WorkerInfo and responses report the actual device. **Windows VM tests remain CPU only**, via SEARCHI_FORCE_CPU=1 and Device::Cpu. GPU execution/failure recovery has not been exercised here. The Windows worker links a static MSVC CRT and static llama libraries, without an external OpenMP runtime. Production CMake supports cpu, auto, metal, vulkan, and cuda; CPU builds force GPU flags off. Runtime/provider parity and packaging on other operating systems remain independent release checks. | Artifact | Bytes | SHA256 | | --- | ---: | --- | | embeddinggemma-2-Q8_0.gguf | 309,855,456 | 2188ac1deca4b77dffefd603c2776a9d76d9d74ec01841392982ebb840b09135 | | mmproj-embeddinggemma-2-Q8_0.gguf | 554,821,024 | c4a8a52691ecef40618438928bdf9e68379b854e24166f292592353db0aab64f | Setup downloads both pinned files. **The current worker implements text**; image/audio projection loading and image/audio/video embeddings remain required work. Downloaded media weights do not establish functioning multimodal search. Weights use Apache-2.0; llama.cpp and DigUp use MIT; USearch and NumKong use Apache-2.0. Retain notices with distributions. Five DigUp text reference vectors are copied under MIT in crates/searchi-semantic/tests/fixtures with the upstream license and source commit alongside them. Primary sources: - [Google developer guide](https://developers.googleblog.com/embeddinggemma-2-the-developer-guide/) - [Model documentation](https://huggingface.co/docs/transformers/model_doc/embedding_gemma2) - [Pinned GGUF publisher](https://huggingface.co/ggml-org/embeddinggemma-2-GGUF/tree/bfcd298762cc34d0357ece5ebdd31791a3a374d8) - [Pinned DigUp reference](https://github.com/ARahim3/DigUp/tree/6faa29e4c315a3a591de1b41bd78751bdf39588d) - [USearch HNSW and filtered retrieval](https://github.com/unum-cloud/USearch) ## Shared API and bounds GemmaEmbedder owns actual tokenization, document/query prompts, and inference. HybridSearch is the shared CLI/desktop service: ~~~rust HybridSearch::open(db, model_dir, worker_executable, threads) HybridSearch::embedding_status(db) // No model/runtime load or network service.warmup() // Real query inference + ANN cache service.index_embeddings() service.search(query, limit, &SearchOptions) service.search_semantic(query, limit, &SearchOptions) // Diagnostics service.worker_info() // Actual model/runtime/device/threads ~~~ Threads are bounded to 1–8; VM tests use one. Document batches contain at most eight windows. Request/response budgets are 2/4 MiB, startup has a 120-second deadline, and each request has a 180-second deadline. Malformed or interrupted IPC and invalid embeddings fail explicitly. The service makes 64 KiB UTF-8 slices with 512-byte overlap before genuine model token windows. It preserves global byte ranges and page/paragraph/line sources, handles 2 MiB passages, caps each passage at 2048 windows, and bounds titles to 512 UTF-8 bytes. Generation fingerprints include this chunk policy as well as model/runtime/prompts/token budgets/normalization. Keep a prewarmed query service and a separate background builder. Readiness requires actual inference, rather than merely finding verified model files. ## Persistence, ANN, and ranking Semantic storage has an independent schema version within the shared SQLite database. Chunks store generation, passage/path identity, original-source hash, location, byte range, snippet, and vector. HNSW uses USearch 2.26.4, cosine/F32, 16 connections, construction expansion 128, and search expansion 64. NumKong dispatches supported CPU SIMD kernels with portable fallback. The Cargo config contains the Windows NK_DYNAMIC override required for static NumKong linkage. Staging vectors and a real HNSW graph are persisted with an ANN blob SHA256. Activation is atomic. Failed rebuilds preserve the active generation; the next rebuild reclaims abandoned staging under a cross-process lock. Search uses a consistent read snapshot. Metadata filters and source hashes establish eligible keys before ANN top-k. The cache is keyed by generation, corpus revision, and normalized filters. Persistent dirty-path triggers invalidate affected files; unchanged warm queries hash zero passages. Large change batches or expired history rebuild eligibility. Default-filter prewarm includes that initial work. Fusion uses reciprocal-rank fusion with k = 60. Exact filenames and literal identifiers retain priority. Ordinary partial filenames receive lexical weight 1.15 and can be outranked by keyword/semantic consensus. File results retain source snippets and deterministic path tie breaks. Current caps are 100,000 chunks and a 512 MiB serialized ANN blob per generation. Full rebuilds still regenerate embeddings and the graph. Incremental reuse/jobs and efficient ANN updates remain production work. One million filename records does not imply one million semantic chunks under these defaults. ## Executed Windows CPU evidence Actual Google CPU vectors match five pinned DigUp text references at cosine **0.99979–0.99994**, exceeding the 0.999 gate. These cover English, Bengali, Arabic, a short document, and a document beyond the sliding attention window. Actual tests also cover English/German retrieval, persisted vectors, changed/ deleted-source suppression, verified long windows, and a tail **after 64 KiB**. The immutable 20-document/24-query seed corpus produced: | Lane | Top 1 | Recall at 3 | | --- | ---: | ---: | | Lexical | 5/24 | 5/24 | | Google semantic | 24/24 | 24/24 | | Hybrid HNSW | 24/24 | 24/24 | With one CPU thread, fresh model/context startup took approximately **7 seconds in the background**, actual query/ANN prewarm **128 ms**, and first foreground natural-language search **99 ms** with the correct supplier document first. Warm seed-corpus hybrid p50/p95 was **92/107 ms**. Model downloads are excluded. A separate release-profile benchmark used 100,000 synthetic normalized 768-dimensional vectors and 20 queries against exact nearest neighbors: | Metric | Measurement | | --- | ---: | | Recall at 10 vs exact | 98.5% | | HNSW query p95 | 1.37 ms | | Warm service retrieval p95 | 6.40 ms | | Warm passages rehashed | 0 | | Initial eligibility work | 1.61 seconds | | Graph construction/serialization | 57.95 seconds | | Reported graph memory | 554 MB | | Dispatched CPU kernel | skylake | This is **index-only performance**, not neural quality across 100,000 documents. The seed corpus and synthetic vectors do not certify production relevance. Representative corpora, cold/selective-filter latency, concurrent indexing, resource profiles, GPU parity/fallback, multimodal retrieval, and platform releases remain validation gates. ## Reproduce Run from the repository root. Provisioning uses the network explicitly: ~~~powershell python tools/embeddinggemma2/download.py python tools/embeddinggemma2/build_runtime.py --backend cpu $env:SEARCHI_FORCE_CPU = '1' $env:SEARCHI_GEMMA_MODEL_DIR = (Resolve-Path .searchi/models/embeddinggemma2).Path $env:SEARCHI_GEMMA_WORKER = (Resolve-Path .searchi/runtime/cpu/Release/searchi-gemma-worker.exe).Path cargo test -p searchi-semantic --test gemma_cpu -- --ignored --nocapture cargo test -p searchi-service --test google_cpu -- --ignored --nocapture cargo test -p searchi-service --lib google_cpu_preserves -- --ignored --nocapture cargo run -p searchi-service --example evaluate -- .searchi/models/embeddinggemma2 .searchi/runtime/cpu/Release/searchi-gemma-worker.exe .searchi/google-relevance-ann.json $env:SEARCHI_ANN_REPORT = (Join-Path (Get-Location) '.searchi/ann-100k.json') cargo test --release -p searchi-service ann_100k -- --ignored --nocapture ~~~ The ignored ANN test deliberately requires release mode. Default tests require no model/network and cover protocol bounds, invalid vectors, UTF-8 coverage, fusion, lifecycle, checksums, pre-top-k filtering, and dirty-path caching. Linux/macOS and GPU checks are not inferred from this Windows CPU run.