Skip to content

Commit 53c984d

Browse files
devwhodevsclaude
andcommitted
feat: upgrade embedding model to bge-small-en-v1.5
Replaces all-MiniLM-L6-v2 (256 token context, MTEB ~60th) with bge-small-en-v1.5 (512 token context, MTEB ~30th). Same 384 dimensions — no migration or re-index schema change needed. Key improvement: chunks up to 512 tokens are now fully embedded instead of silently truncated at 256. This fixes a quality issue where the second half of larger chunks was invisible to semantic search. Trade-off: model download grows from 23MB to 127MB (one-time). Users must delete ~/.engraph/models/ and run engraph index --rebuild after upgrading. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 99ec5d0 commit 53c984d

6 files changed

Lines changed: 22 additions & 23 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Single binary with 20 modules behind a lib crate:
99
- `config.rs` — loads `~/.engraph/config.toml` and `vault.toml`, merges CLI args, provides `data_dir()`
1010
- `chunker.rs` — smart chunking with break-point scoring algorithm. Finds optimal split points considering headings, code fences, blank lines, and thematic breaks. `split_oversized_chunks()` handles token-aware secondary splitting with overlap
1111
- `docid.rs` — deterministic 6-char hex IDs for files (SHA-256 of path, truncated). Shown in search results for quick reference
12-
- `embedder.rs` — downloads and runs `all-MiniLM-L6-v2` ONNX model (384-dim). SHA256-verified on download. Uses `ort` for inference, `tokenizers` for tokenization. Implements `ModelBackend` trait. **Not `Send`** — all embedding is serial
12+
- `embedder.rs` — downloads and runs `bge-small-en-v1.5` ONNX model (384-dim, 512 token context). SHA256-verified on download. Uses `ort` for inference, `tokenizers` for tokenization. Implements `ModelBackend` trait. **Not `Send`** — all embedding is serial
1313
- `model.rs` — pluggable `ModelBackend` trait, model registry, and `parse_model_spec()`. Enables future model swapping without changing consumer code
1414
- `fts.rs` — FTS5 full-text search support. Re-exports `FtsResult` from store. BM25-ranked keyword search
1515
- `fusion.rs` — Reciprocal Rank Fusion (RRF) engine. Merges semantic + FTS5 + graph results. Supports lane weighting, `--explain` output with per-lane detail

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Plain vector search treats your notes as isolated documents. But knowledge isn't
2020
- **MCP server for AI agents**`engraph serve` exposes 13 tools (search, read, context bundles, note creation) that Claude, Cursor, or any MCP client can call directly.
2121
- **Real-time sync** — file watcher keeps the index fresh as you edit in Obsidian. No manual re-indexing needed.
2222
- **Smart write pipeline** — AI agents can create notes with automatic tag resolution, wikilink discovery, and folder placement based on semantic similarity.
23-
- **Fully local** — ONNX embeddings (`all-MiniLM-L6-v2`, 23MB), SQLite storage, no network required after initial model download.
23+
- **Fully local** — ONNX embeddings (`bge-small-en-v1.5`, 127MB), SQLite storage, no network required after initial model download.
2424

2525
## What problem it solves
2626

@@ -80,7 +80,7 @@ cargo install --git https://github.com/devwhodevs/engraph
8080

8181
```bash
8282
engraph index ~/path/to/vault
83-
# Downloads embedding model on first run (~23MB)
83+
# Downloads embedding model on first run (~127MB)
8484
# Incremental — only re-embeds changed files on subsequent runs
8585
```
8686

src/embedder.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ use tokenizers::Tokenizer;
1111
use tracing::info;
1212

1313
const MODEL_URL: &str =
14-
"https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx";
14+
"https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/onnx/model.onnx";
1515
const TOKENIZER_URL: &str =
16-
"https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json";
17-
/// SHA-256 of the ONNX model file. Set to empty string to skip verification
18-
/// until we can compute the real hash from a download.
19-
const MODEL_SHA256: &str = "6fd5d72fe4589f189f8ebc006442dbb529bb7ce38f8082112682524616046452";
16+
"https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/tokenizer.json";
17+
/// SHA-256 of the ONNX model file.
18+
const MODEL_SHA256: &str = "828e1496d7fabb79cfa4dcd84fa38625c0d3d21da474a00f08db0f559940cf35";
2019
pub const EMBEDDING_DIM: usize = 384;
2120

2221
pub struct Embedder {
@@ -175,7 +174,7 @@ impl crate::model::ModelBackend for Embedder {
175174
}
176175

177176
fn name(&self) -> &str {
178-
"onnx:all-MiniLM-L6-v2"
177+
"onnx:bge-small-en-v1.5"
179178
}
180179
}
181180

src/model.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ impl Default for ModelRegistry {
4242
fn default() -> Self {
4343
Self {
4444
entries: vec![ModelRegistryEntry {
45-
name: "onnx:all-MiniLM-L6-v2".to_string(),
45+
name: "onnx:bge-small-en-v1.5".to_string(),
4646
format: ModelFormat::Onnx,
47-
url: "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx".to_string(),
48-
sha256: "6fd5d72fe4589f189f8ebc006442dbb529bb7ce38f8082112682524616046452".to_string(),
47+
url: "https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/onnx/model.onnx".to_string(),
48+
sha256: "828e1496d7fabb79cfa4dcd84fa38625c0d3d21da474a00f08db0f559940cf35".to_string(),
4949
dim: 384,
50-
description: "Lightweight general-purpose sentence embeddings".to_string(),
50+
description: "High-quality English embeddings, 512 token context".to_string(),
5151
}],
5252
}
5353
}
@@ -96,16 +96,16 @@ mod tests {
9696
let registry = ModelRegistry::default();
9797
assert_eq!(registry.entries.len(), 1);
9898
let entry = &registry.entries[0];
99-
assert_eq!(entry.name, "onnx:all-MiniLM-L6-v2");
99+
assert_eq!(entry.name, "onnx:bge-small-en-v1.5");
100100
assert_eq!(entry.dim, 384);
101101
assert_eq!(entry.format, ModelFormat::Onnx);
102102
}
103103

104104
#[test]
105105
fn test_parse_model_spec_onnx() {
106-
let spec = parse_model_spec("onnx:all-MiniLM-L6-v2");
106+
let spec = parse_model_spec("onnx:bge-small-en-v1.5");
107107
assert_eq!(spec.format, ModelFormat::Onnx);
108-
assert_eq!(spec.name, "all-MiniLM-L6-v2");
108+
assert_eq!(spec.name, "bge-small-en-v1.5");
109109
assert!(spec.path.is_empty());
110110
}
111111

@@ -128,7 +128,7 @@ mod tests {
128128
#[test]
129129
fn test_registry_get_existing() {
130130
let registry = ModelRegistry::default();
131-
let entry = registry.get("onnx:all-MiniLM-L6-v2");
131+
let entry = registry.get("onnx:bge-small-en-v1.5");
132132
assert!(entry.is_some());
133133
assert_eq!(entry.unwrap().dim, 384);
134134
}

src/search.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ pub fn run_status(json: bool, data_dir: &Path) -> Result<()> {
233233
// Compute index size on disk (sqlite db file).
234234
let index_size = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
235235

236-
let model_name = "all-MiniLM-L6-v2";
236+
let model_name = "bge-small-en-v1.5";
237237

238238
let output = format_status(&stats, index_size, model_name, json);
239239
print!("{output}");
@@ -451,15 +451,15 @@ mod tests {
451451
wikilink_count: None,
452452
mention_count: None,
453453
};
454-
let output = format_status(&stats, 2_516_582, "all-MiniLM-L6-v2", false);
454+
let output = format_status(&stats, 2_516_582, "bge-small-en-v1.5", false);
455455

456456
assert!(output.contains("/path/to/vault"), "missing vault path");
457457
assert!(output.contains("42"), "missing file count");
458458
assert!(output.contains("187"), "missing chunk count");
459459
assert!(output.contains("3"), "missing tombstone count");
460460
assert!(output.contains("2026-03-19 14:30:00"), "missing last index");
461461
assert!(output.contains("2.4 MB"), "missing index size");
462-
assert!(output.contains("all-MiniLM-L6-v2"), "missing model");
462+
assert!(output.contains("bge-small-en-v1.5"), "missing model");
463463
}
464464

465465
#[test]
@@ -474,7 +474,7 @@ mod tests {
474474
wikilink_count: None,
475475
mention_count: None,
476476
};
477-
let output = format_status(&stats, 2_516_582, "all-MiniLM-L6-v2", true);
477+
let output = format_status(&stats, 2_516_582, "bge-small-en-v1.5", true);
478478
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
479479

480480
assert_eq!(parsed["vault"], "/path/to/vault");
@@ -483,7 +483,7 @@ mod tests {
483483
assert_eq!(parsed["tombstones"], 3);
484484
assert_eq!(parsed["last_indexed"], "2026-03-19 14:30:00");
485485
assert_eq!(parsed["index_size"], 2_516_582);
486-
assert_eq!(parsed["model"], "all-MiniLM-L6-v2");
486+
assert_eq!(parsed["model"], "bge-small-en-v1.5");
487487
}
488488

489489
#[test]

tests/integration.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Integration tests for engraph.
22
//!
3-
//! All tests are `#[ignore]` because they require the ONNX model download (~23MB).
3+
//! All tests are `#[ignore]` because they require the ONNX model download (~127MB).
44
//! Run with: `cargo test --test integration -- --ignored`
55
66
use std::path::{Path, PathBuf};

0 commit comments

Comments
 (0)