Skip to content

Commit 55ff236

Browse files
authored
feat: MCP server v2, smart embedding defaults, incremental build fixes (#279)
feat: MCP server v2 — build, status tools, smart embedding defaults, and incremental build fixes
2 parents 773a47f + e3593fe commit 55ff236

8 files changed

Lines changed: 440 additions & 129 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,6 @@ test-code/
121121
localtestmcp/
122122
*.csv
123123
*.pickle
124+
125+
# Personal dev notes (not tracked)
126+
docs/dev/

.vscode/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,10 @@
1818
"**/*.egg-info/**": true,
1919
"**/build/**": true,
2020
"**/dist/**": true
21-
}
21+
},
22+
"accessibility.signals.terminalBell": {
23+
"sound": "on",
24+
"announcement": "auto"
25+
},
26+
"cmake.sourceDirectory": "/Users/yichuan/Desktop/code/LEANN/leann/packages/leann-backend-hnsw"
2227
}

docs/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,28 @@
33
Append-only log of major changes to LEANN (new features, breaking changes, important
44
fixes). Newest entries at the bottom.
55

6+
## 2026-03-05: IVF backend incremental update support
7+
8+
- Added `leann-backend-ivf` with FAISS IndexIVFFlat + DirectMap.Hashtable.
9+
- IVF supports in-place `add_vectors` and `remove_ids` without full rebuild.
10+
- `leann build` is now idempotent: re-running on an existing index does incremental update (add new, remove deleted, re-index modified files).
11+
- Fixed incremental build chunking inconsistency and shared metadata dict bug.
12+
- Fixed IVF incremental update duplicate chunks from stale `passages.jsonl`.
13+
14+
## 2026-03-05: MCP server v2 — build, status, and structured search
15+
16+
- Added `leann_build` MCP tool: build or incrementally update indexes directly from Claude Code.
17+
- Added `leann_status` MCP tool: inspect index details (backend, embedding model, chunk/file count, size).
18+
- `leann_search` now uses `--json` output with file paths always included, formatted as markdown code blocks.
19+
- Fixed `float32` JSON serialization bug in `leann search --json`.
20+
- Cleaned up MCP tool descriptions (concise, no emoji).
21+
22+
## 2026-03-05: Documentation — roadmap, vision, and dev guidelines
23+
24+
- Rewrote `docs/roadmap.md` with current P0/P1 priorities from GitHub issue #237.
25+
- Added `docs/ultimate_goal.md` — long-term vision (personal data platform, best code retrieval MCP, multimodal, local-first).
26+
- Added self-contained documentation principle and dev doc maintenance rules to `CLAUDE.md`.
27+
628
## 2026-06-02: GPU FlashLib IVF backend (`flashlib_ivf`)
729

830
- Add `leann-backend-flashlib-ivf`, a GPU IVF-Flat (inverted file) approximate-NN
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Smart default embedding model based on platform and corpus size
2+
3+
## Summary
4+
5+
Propose platform- and corpus-aware default embedding model selection for `leann build` when `--embedding-model` is not explicitly specified. This would improve out-of-the-box experience for different deployment scenarios (macOS CPU, NVIDIA GPU, etc.) without changing behavior when users pass an explicit model.
6+
7+
## Motivation
8+
9+
- **Current default**: `facebook/contriever` (~420MB, 768 dim) — heavy for CPU-only builds on large corpora
10+
- **macOS users** often hit slow builds on 20K+ chunks; lighter models like `all-MiniLM-L6-v2` (~90MB) are much faster
11+
- **NVIDIA GPU users** can leverage stronger models; smaller corpora benefit from quality (e.g. Qwen3-Embedding-0.6B), larger ones from balanced models (e.g. bge-base-en-v1.5)
12+
13+
## Proposed logic
14+
15+
| Platform | Chunk count | Default model |
16+
|----------|-------------|---------------|
17+
| **macOS** | ≥ 20,000 | `sentence-transformers/all-MiniLM-L6-v2` |
18+
| **macOS** | < 20,000 | `intfloat/e5-small-v2` |
19+
| **NVIDIA GPU** | < 5,000 | `Qwen/Qwen3-Embedding-0.6B` |
20+
| **NVIDIA GPU** | ≥ 5,000 | `BAAI/bge-base-en-v1.5` |
21+
| **Other** | any | `facebook/contriever` (unchanged) |
22+
23+
## Implementation notes
24+
25+
1. **Platform detection**: `torch.cuda.is_available()` for NVIDIA; `sys.platform == "darwin"` for macOS
26+
2. **Chunk count**: Known only after loading/chunking; may need to either:
27+
- Do a lightweight pre-scan (e.g. file count × rough chunks per file), or
28+
- Defer default choice until after first chunking pass (and cache for incremental)
29+
3. **Explicit override**: If user passes `--embedding-model`, always use it; this logic applies only when the flag is omitted
30+
31+
## Model references
32+
33+
- `sentence-transformers/all-MiniLM-L6-v2`: ~90MB, 384 dim, fast on CPU
34+
- `intfloat/e5-small-v2`: ~90MB, 384 dim
35+
- `Qwen/Qwen3-Embedding-0.6B`: 0.6B params, 1024 dim, strong retrieval
36+
- `BAAI/bge-base-en-v1.5`: ~110M params, 768 dim, good MTEB scores
37+
38+
## Open questions
39+
40+
- Should we add a `--embedding-model auto` to explicitly opt into this logic?
41+
- Pre-scan vs post-chunk decision: trade-off between accuracy and implementation complexity

packages/leann-core/src/leann/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ class LeannBuilder:
379379
def __init__(
380380
self,
381381
backend_name: str,
382-
embedding_model: str = "facebook/contriever",
382+
embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2",
383383
dimensions: Optional[int] = None,
384384
embedding_mode: str = "sentence-transformers",
385385
embedding_options: Optional[dict[str, Any]] = None,

packages/leann-core/src/leann/cli.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@
3131
from .sync import DEFAULT_INDEX_EXTENSIONS, FileSynchronizer, parse_include_extensions
3232

3333

34+
def _default_embedding_model() -> str:
35+
"""Pick a sensible default embedding model based on platform.
36+
37+
| Platform | Default model |
38+
|------------|------------------------------------------------|
39+
| NVIDIA GPU | BAAI/bge-base-en-v1.5 |
40+
| macOS | sentence-transformers/all-MiniLM-L6-v2 |
41+
| Other/CPU | sentence-transformers/all-MiniLM-L6-v2 |
42+
"""
43+
44+
try:
45+
import torch
46+
47+
if torch.cuda.is_available():
48+
return "BAAI/bge-base-en-v1.5"
49+
except ImportError:
50+
pass
51+
52+
# macOS (MPS or CPU) and all other platforms: lightweight model
53+
return "sentence-transformers/all-MiniLM-L6-v2"
54+
55+
3456
def _normalize_path(path: str) -> str:
3557
"""Return absolute path string for consistent keys."""
3658
if not path:
@@ -260,11 +282,12 @@ def create_parser(self) -> argparse.ArgumentParser:
260282
choices=["hnsw", "diskann", "ivf"],
261283
help="Backend to use (default: hnsw)",
262284
)
285+
_default_model = _default_embedding_model()
263286
build_parser.add_argument(
264287
"--embedding-model",
265288
type=str,
266-
default="facebook/contriever",
267-
help="Embedding model (default: facebook/contriever)",
289+
default=_default_model,
290+
help=f"Embedding model (default: {_default_model})",
268291
)
269292
build_parser.add_argument(
270293
"--embedding-mode",
@@ -2432,8 +2455,15 @@ async def build_index(self, args):
24322455
is_compact = meta.get(
24332456
"is_compact", meta.get("backend_kwargs", {}).get("is_compact", True)
24342457
)
2458+
2459+
# Normalize model names for comparison — e.g. "all-MiniLM-L6-v2"
2460+
# should match "sentence-transformers/all-MiniLM-L6-v2"
2461+
def _normalize_model_name(name: str) -> str:
2462+
return name.rsplit("/", 1)[-1] if name else name
2463+
24352464
same_embedding = (
2436-
meta.get("embedding_model") == args.embedding_model
2465+
_normalize_model_name(meta.get("embedding_model", ""))
2466+
== _normalize_model_name(args.embedding_model)
24372467
and meta.get("embedding_mode") == args.embedding_mode
24382468
)
24392469

@@ -2519,6 +2549,11 @@ async def build_index(self, args):
25192549

25202550
else:
25212551
self._log_rebuild_reason(meta, args, new_paths, removed_paths, modified_paths)
2552+
# Force full reload — the partial all_texts from incremental
2553+
# path only contains changed files, not the full corpus.
2554+
all_texts = self.load_documents(
2555+
docs_paths, args.file_types, include_hidden=args.include_hidden, args=args
2556+
)
25222557

25232558
# Full rebuild: load documents if not already loaded (first build or force)
25242559
try:
@@ -3135,7 +3170,7 @@ async def search_documents(self, args):
31353170
json_results = [
31363171
{
31373172
"id": r.id,
3138-
"score": r.score,
3173+
"score": float(r.score),
31393174
"text": r.text,
31403175
"metadata": r.metadata,
31413176
}

0 commit comments

Comments
 (0)