feat: integrate Google semantic search and branded desktop launcher
Core CI / core (ubuntu-latest) (push) Failing after 3m27s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 5m45s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Failing after 3m27s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 5m45s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
This commit is contained in:
1 parent
a90df15503
commit
caf6a2f82e
80 files changed
+14971
-837
No files matched your search
@@ -0,0 +1,58 @@
|
||||
"""Build a pinned isolated native worker; development VM builds use --backend cpu."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TAG = "b11461"
|
||||
COMMIT = "4d756bc72bf00a4aacf410ae15a2d315f3db400d"
|
||||
|
||||
|
||||
def run(arguments, cwd=None):
|
||||
subprocess.run([str(value) for value in arguments], cwd=cwd, check=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Build Searchi's pinned Google embedding worker")
|
||||
parser.add_argument("--backend", choices=["cpu", "auto", "metal", "vulkan", "cuda"], default="cpu")
|
||||
parser.add_argument("--jobs", type=int, default=2)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.jobs <= 16:
|
||||
parser.error("jobs must be 1–16")
|
||||
source = ROOT / ".searchi/references/llama.cpp"
|
||||
build = ROOT / ".searchi/runtime" / args.backend
|
||||
if not source.exists():
|
||||
source.parent.mkdir(parents=True, exist_ok=True)
|
||||
run(["git", "clone", "--depth", "1", "--branch", TAG,
|
||||
"https://github.com/ggml-org/llama.cpp.git", source])
|
||||
actual = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip()
|
||||
if actual != COMMIT:
|
||||
raise RuntimeError("Runtime reference checkout differs from pinned commit; refusing to reset it")
|
||||
patch = Path(__file__).with_name("no-logits.patch")
|
||||
check = subprocess.run(["git", "apply", "--check", str(patch)], cwd=source, capture_output=True)
|
||||
if check.returncode == 0:
|
||||
run(["git", "apply", patch], cwd=source)
|
||||
else:
|
||||
reverse = subprocess.run(["git", "apply", "--reverse", "--check", str(patch)], cwd=source, capture_output=True)
|
||||
if reverse.returncode:
|
||||
raise RuntimeError("Runtime memory patch cannot be applied or verified")
|
||||
cmake_exe = shutil.which("cmake")
|
||||
if not cmake_exe:
|
||||
try:
|
||||
import cmake
|
||||
cmake_exe = str(Path(cmake.CMAKE_BIN_DIR) / ("cmake.exe" if os.name == "nt" else "cmake"))
|
||||
except ImportError as error:
|
||||
raise RuntimeError("Install CMake first (python -m pip install cmake)") from error
|
||||
worker_source = ROOT / "crates/searchi-semantic/native"
|
||||
run([cmake_exe, "-S", worker_source, "-B", build,
|
||||
"-DCMAKE_BUILD_TYPE=Release", f"-DLLAMA_CPP_DIR={source}",
|
||||
f"-DSEARCHI_ACCELERATION={args.backend}"])
|
||||
run([cmake_exe, "--build", build, "--config", "Release", "--parallel", args.jobs])
|
||||
print(f"Runtime build complete: {build}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Explicitly provision the pinned Google model; search never calls this script."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import urllib.request
|
||||
|
||||
|
||||
def verified(path, spec):
|
||||
if not path.is_file() or path.stat().st_size != spec["bytes"]:
|
||||
return False
|
||||
with path.open("rb") as source:
|
||||
return hashlib.file_digest(source, "sha256").hexdigest() == spec["sha256"]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Download the verified Google EmbeddingGemma 2 Q8 model (865 MB)")
|
||||
parser.add_argument("--model-dir", type=Path, default=Path(".searchi/models/embeddinggemma2"))
|
||||
parser.add_argument("--text-only", action="store_true", help="Download only the 310 MB text backbone")
|
||||
args = parser.parse_args()
|
||||
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
||||
for spec in manifest["files"][:1] if args.text_only else manifest["files"]:
|
||||
destination = args.model_dir / spec["path"]
|
||||
if verified(destination, spec):
|
||||
print(f"Verified {spec['path']}", flush=True)
|
||||
continue
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = destination.with_suffix(".part")
|
||||
url = f"https://huggingface.co/{manifest['export_repository']}/resolve/{manifest['revision']}/{spec['path']}"
|
||||
try:
|
||||
request = urllib.request.Request(url, headers={"User-Agent": "Searchi/0.1"})
|
||||
print(f"Downloading {spec['path']} ({spec['bytes']} bytes)", flush=True)
|
||||
with urllib.request.urlopen(request, timeout=60) as response, temporary.open("wb") as output:
|
||||
total = 0
|
||||
last_report = 0
|
||||
while chunk := response.read(1024 * 1024):
|
||||
total += len(chunk)
|
||||
if total > spec["bytes"]:
|
||||
raise ValueError("Model download exceeds pinned size")
|
||||
output.write(chunk)
|
||||
if total - last_report >= 50 * 1024 * 1024:
|
||||
print(f"Downloaded {total}/{spec['bytes']} bytes", flush=True)
|
||||
last_report = total
|
||||
if not verified(temporary, spec):
|
||||
raise ValueError("Model size or SHA256 mismatch")
|
||||
os.replace(temporary, destination)
|
||||
print(f"Verified SHA256: {spec['sha256']}", flush=True)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"model_id": "google/embeddinggemma-2",
|
||||
"export_repository": "ggml-org/embeddinggemma-2-GGUF",
|
||||
"revision": "bfcd298762cc34d0357ece5ebdd31791a3a374d8",
|
||||
"dimensions": 768,
|
||||
"quantization": "Q8_0",
|
||||
"license": "Apache-2.0",
|
||||
"runtime_tag": "b11461",
|
||||
"query_prefix": "task: search result | query: ",
|
||||
"document_template": "title: {title} | text: {text}",
|
||||
"files": [
|
||||
{"path":"embeddinggemma-2-Q8_0.gguf","bytes":309855456,"sha256":"2188ac1deca4b77dffefd603c2776a9d76d9d74ec01841392982ebb840b09135"},
|
||||
{"path":"mmproj-embeddinggemma-2-Q8_0.gguf","bytes":554821024,"sha256":"c4a8a52691ecef40618438928bdf9e68379b854e24166f292592353db0aab64f"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
diff --git a/src/llama-context.cpp b/src/llama-context.cpp
|
||||
--- a/src/llama-context.cpp
|
||||
+++ b/src/llama-context.cpp
|
||||
@@ -2133,7 +2133,7 @@ uint32_t llama_context::output_reserve(int32_t n_outputs) {
|
||||
const auto n_embd = hparams.n_embd;
|
||||
const auto n_embd_out = hparams.n_embd_out();
|
||||
|
||||
- bool has_logits = true;
|
||||
+ bool has_logits = !(cparams.embeddings && !cparams.causal_attn);
|
||||
bool has_embd = cparams.embeddings;
|
||||
bool has_embd_nextn = cparams.embeddings_nextn;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Stage the native worker for installers; optional offline model bundle."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Stage Searchi runtime resources")
|
||||
parser.add_argument("--backend", choices=["cpu", "auto", "metal", "vulkan", "cuda"], default="cpu")
|
||||
parser.add_argument("--bundle-model", action="store_true")
|
||||
args = parser.parse_args()
|
||||
name = "searchi-gemma-worker.exe" if os.name == "nt" else "searchi-gemma-worker"
|
||||
source = ROOT / ".searchi/runtime" / args.backend
|
||||
if os.name == "nt":
|
||||
source /= "Release"
|
||||
source /= name
|
||||
if not source.is_file():
|
||||
raise RuntimeError("Build the pinned native worker before staging")
|
||||
destination = ROOT / "apps/desktop/src-tauri/resources"
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, destination / name)
|
||||
# Include the upstream runtime license with the statically linked helper.
|
||||
shutil.copy2(ROOT / ".searchi/references/llama.cpp/LICENSE", destination / "LLAMA-LICENSE.txt")
|
||||
if args.bundle_model:
|
||||
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
||||
model_destination = destination / "embeddinggemma2"
|
||||
model_destination.mkdir(exist_ok=True)
|
||||
for spec in manifest["files"]:
|
||||
artifact = ROOT / ".searchi/models/embeddinggemma2" / spec["path"]
|
||||
if artifact.stat().st_size != spec["bytes"]:
|
||||
raise RuntimeError("Model size mismatch")
|
||||
with artifact.open("rb") as stream:
|
||||
if hashlib.file_digest(stream, "sha256").hexdigest() != spec["sha256"]:
|
||||
raise RuntimeError("Model checksum mismatch")
|
||||
shutil.copy2(artifact, model_destination / spec["path"])
|
||||
print(f"Staged native worker: {destination / name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,160 @@
|
||||
# CPU semantic inference proof
|
||||
|
||||
**Historical experiment, not the accepted Searchi backend.** The user requires
|
||||
Google EmbeddingGemma 2 as used by DigUp. MiniLM results below establish only
|
||||
runtime plumbing and must not be presented as completed product semantic search.
|
||||
The implementation is being corrected to use the required Google model.
|
||||
|
||||
This milestone runs genuine neural embeddings locally. It does not yet connect
|
||||
semantic vectors to the SQLite search index or desktop interface. Production
|
||||
retrieval quality, large-corpus performance, and Linux/macOS runtime packaging
|
||||
remain unverified.
|
||||
|
||||
## Model and runtime decision
|
||||
|
||||
The first working baseline is the quantized ONNX export of
|
||||
`sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2`, published by
|
||||
`Xenova`. The upstream model card describes 384-dimensional text embeddings,
|
||||
attention-mask mean pooling, a 128-token maximum, and Apache-2.0 licensing.
|
||||
It covers multiple languages. This is a text-only baseline; it does not claim
|
||||
EmbeddingGemma 2's multimodal features or compatibility.
|
||||
|
||||
Sources reviewed on 2026-10-12:
|
||||
|
||||
- [Original model card and pooling architecture](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2)
|
||||
- [ONNX export publisher](https://huggingface.co/Xenova/paraphrase-multilingual-MiniLM-L12-v2)
|
||||
- [ONNX Runtime Python session API](https://onnxruntime.ai/docs/api/python/api_summary.html)
|
||||
- [Rust ort dynamic-library initialization](https://docs.rs/ort/2.0.0-rc.13/ort/environment/fn.init_from.html)
|
||||
- [Rust ort session configuration](https://docs.rs/ort/2.0.0-rc.13/ort/session/builder/struct.SessionBuilder.html)
|
||||
|
||||
Inference explicitly selects `CPUExecutionProvider`. Python uses the CPU
|
||||
`onnxruntime` wheel, never `onnxruntime-gpu`. The native Rust crate disables ort
|
||||
default features: there are no binary downloads, GPU provider features, or model
|
||||
HTTP client features. It loads an explicitly supplied local runtime library and
|
||||
disables runtime telemetry. Tokenization is sequential; ONNX sessions use
|
||||
sequential graph execution with bounded intra-op threads and one inter-op thread.
|
||||
|
||||
## Reproduce on Windows
|
||||
|
||||
Run these commands from the repository root. Provisioning commands intentionally
|
||||
use the network; the inference command does not provision anything.
|
||||
|
||||
```powershell
|
||||
python -m venv tools/semantic-proof/.venv
|
||||
& tools/semantic-proof/.venv/Scripts/python.exe -m pip install --only-binary=:all: -r tools/semantic-proof/requirements.txt
|
||||
& tools/semantic-proof/.venv/Scripts/python.exe tools/semantic-proof/download.py
|
||||
& tools/semantic-proof/.venv/Scripts/python.exe tools/semantic-proof/proof.py --threads 1 --output tools/semantic-proof/windows-cpu-result.json
|
||||
```
|
||||
|
||||
On Linux/macOS, use `.venv/bin/python` instead of `.venv/Scripts/python.exe`.
|
||||
These instructions have only been executed on Windows. Wheel availability and
|
||||
native library distribution must be checked on other platforms.
|
||||
|
||||
`download.py` downloads exactly two revision-pinned artifacts, streams them to a
|
||||
temporary file, rejects oversized downloads, checks byte length and SHA256, then
|
||||
atomically replaces the local asset. Assets occupy approximately 136 MB in
|
||||
decimal units and are ignored by Git. Inference validates both files again and
|
||||
fails clearly when either is missing or damaged. It never contacts the model
|
||||
hub. The Python proof sets offline environment flags and rejects Python socket
|
||||
connect, name-resolution, and bind audit events throughout import and inference.
|
||||
This guard checks Python socket activity; it is not an operating-system firewall
|
||||
against arbitrary native libraries.
|
||||
|
||||
The proof intentionally exits nonzero if any fixture query misses top-1. Its
|
||||
JSON report is written before the quality gate, preserving failures for review.
|
||||
|
||||
## Pinned artifacts and licensing
|
||||
|
||||
Revision: `2c4055b12046f11709e9df2c122e59ffbdc2f900`.
|
||||
The manifest in `tools/semantic-proof/model-manifest.json` records the same
|
||||
model contract and hashes used by the native adapter.
|
||||
|
||||
| Artifact | Bytes | SHA256 |
|
||||
| --- | ---: | --- |
|
||||
| `onnx/model_quantized.onnx` | 118,308,126 | `66fc00f5f29afcaff34092e1bdd20008ca3918265a82fb9695a551e510cc4ebc` |
|
||||
| `tokenizer.json` | 17,082,913 | `b60b6b43406a48bf3638526314f3d232d97058bc93472ff2de930d43686fa441` |
|
||||
|
||||
The base model card specifies Apache-2.0. The export card identifies that base
|
||||
model but does not display its own license declaration. Before redistributing
|
||||
the ONNX export in installers, confirm export licensing and preserve the base
|
||||
license, attribution, and applicable notices. Current code explicitly downloads
|
||||
user-local assets and does not ship model weights or runtime DLLs in Git.
|
||||
ONNX Runtime uses MIT licensing; ort uses MIT or Apache-2.0 and tokenizers uses
|
||||
Apache-2.0. Release packaging must retain third-party notices and validate
|
||||
platform-specific runtime hashes independently; a model hash does not attest
|
||||
to the runtime library.
|
||||
|
||||
The tested Windows ONNX Runtime 1.31.0 DLL is 19,288,888 bytes with SHA256
|
||||
`90497e3d406c09f4d59d364c696a2f93dc02f6ef13741b3b89eec95bdb4a3806`.
|
||||
The adapter currently trusts the explicitly supplied runtime path; it validates
|
||||
model hashes, not runtime-library hashes. A release distribution must add a
|
||||
platform runtime manifest before shipping. `requirements-lock-windows.txt`
|
||||
captures the exact Python environment used for this proof.
|
||||
|
||||
## Measured Windows VM result
|
||||
|
||||
`tools/semantic-proof/windows-cpu-result.json` contains the full measurement and
|
||||
rankings. The run used Windows 11 AMD64, Python 3.14.8, ONNX Runtime 1.31.0,
|
||||
four visible logical CPUs, and **one inference thread with only the CPU provider**.
|
||||
|
||||
| Metric | Measurement |
|
||||
| --- | ---: |
|
||||
| Fixture documents / queries | 8 / 6 |
|
||||
| Timed warm query runs | 60 |
|
||||
| Artifact verification plus runtime/model load | approximately 3.4 seconds |
|
||||
| Corpus batch embedding | approximately 145 ms |
|
||||
| Warm query p50 / p95 | approximately 3.1 / 11.9 ms |
|
||||
| Top-1 accuracy | 5/6 (83.3%) |
|
||||
| Recall at 3 | 6/6 (100%) |
|
||||
|
||||
The corpus includes English and German documents, same-language semantic
|
||||
queries, and cross-language queries. A question about a postponed product
|
||||
release ranks the supplier contract above the expected meeting notes. That
|
||||
failure remains in the fixture and blocks the strict top-1 quality gate.
|
||||
This small demonstration supports the feasibility of CPU inference; it does
|
||||
not establish large-corpus latency, robust multilingual quality, or production
|
||||
readiness. Quantization and batch shape can affect similarity scores.
|
||||
|
||||
## Native integration contract
|
||||
|
||||
`searchi-semantic` exposes:
|
||||
|
||||
```rust
|
||||
CpuEmbedder::load(model_dir, runtime_library, threads) -> Result<CpuEmbedder>
|
||||
CpuEmbedder::encode(&mut self, texts: &[&str]) -> Result<Vec<Vec<f32>>>
|
||||
DIMENSIONS // 384
|
||||
MODEL_FINGERPRINT // revision + quantization + pooling + normalization + token cap
|
||||
```
|
||||
|
||||
Inputs are bounded to eight texts per batch, 64 KiB UTF-8 per text, and 128
|
||||
tokens. Outputs are finite, normalized `f32` vectors. The runtime library is
|
||||
process-global; adapter initialization rejects attempts to switch libraries or
|
||||
silently accept an environment initialized outside the adapter. Supply a known,
|
||||
trusted runtime before calling any other ort APIs in the process.
|
||||
|
||||
Default unit tests need no model or runtime. The real-artifact test is opt-in:
|
||||
|
||||
```powershell
|
||||
$env:SEARCHI_MODEL_DIR = (Resolve-Path tools/semantic-proof/model).Path
|
||||
$env:SEARCHI_ORT_LIBRARY = (Resolve-Path tools/semantic-proof/.venv/Lib/site-packages/onnxruntime/capi/onnxruntime.dll).Path
|
||||
cargo test -p searchi-semantic --test cpu_model -- --ignored --nocapture
|
||||
```
|
||||
|
||||
The smoke test checks actual local inference, vector normalization, English and
|
||||
German retrieval, recall at three, and the measured five-of-six top-1 baseline.
|
||||
It does not replace the strict production quality gate.
|
||||
|
||||
The native real-artifact smoke was executed successfully on the Windows VM,
|
||||
using that CPU-only DLL. It reproduced five-of-six top-1 and six-of-six recall
|
||||
at three. Three default unit tests passed; targeted Clippy passed with warnings
|
||||
denied. Linux and macOS native checks have not been executed.
|
||||
|
||||
The next integration should read bounded `SearchIndex::passage_batch` rows,
|
||||
split long passages into overlapping tokenizer windows, preserve source page or
|
||||
paragraph locations, and persist each vector with a content fingerprint and
|
||||
`MODEL_FINGERPRINT`. Passage IDs are not immutable content identities: changes
|
||||
must invalidate stale vectors. Queries should combine lexical and semantic
|
||||
retrieval while preserving exact filename priority. Model upgrades build a
|
||||
separate vector generation before an atomic swap. Cancellation, persistent
|
||||
embedding jobs, ANN retrieval, representative quality corpora, and signed
|
||||
cross-platform runtime packaging are still required.
|
||||
Reference in new issue
Block a user