Summary
I've been running a personal fork of semble for almost 2 months now against a large monorepo. Roughly 15k files and hundreds of thousands of chunks. Almost everything I changed was about performance. The Rust module exists for the same reason.
I follow CONTRIBUTING and wanted to open a discussion issue first before sending PRs.
Fork: https://github.com/mazulo/semble
Matched benchmarks: https://gist.github.com/mazulo/94c8243730bcee05f90e97e2c5059fe8
The fork is currently synced with MinishLab main and tagged as 0.5.2 on my side.
Problem
On large local checkouts, cold indexing is dominated by:
- Tree-sitter parser load across many languages
- Embedding hundreds of thousands of chunks
- BM25 tokenization over every chunk, including identifier splitting for camelCase and snake_case
- Cache validation that re-walks the tree on every warm start
For my agent workflows the important loops are:
- first
semble search or MCP call after a clone or cache miss
- later searches against a warm cache while the repo did not change
Warm search latency was already fine for me. The painful part was time to first useful index, and warm cache validation on big trees.
Why this belongs in semble
These are optimizations inside the existing index, build, and cache pipeline. Not a separate search product.
- Same CLI, MCP, and Python API
- Same hybrid BM25 + dense ranking
- Optional native extension only for the hot tokenization loop, with a pure Python fallback
- No change to how agents call
search / find_related
A wrapper cannot fix parser preload, embed batching, or cache mtime validation without forking the library anyway.
What changed in the fork
Four independent pieces. If there is interest I would rather send them as separate PRs.
-
Parallel tree-sitter parser preload before chunking
Internal only. get_parser releases the GIL, so threads help.
-
Faster cache validation
Store per-file and per-dir mtimes at save time. Validate with stat instead of a full walk. Legacy caches can fall back to the current walk.
-
Optional Rust BM25 tokenizer via semble-core
No API change. tokens.tokenize tries semble_core and falls back to pure Python. This would be an optional install. I know CONTRIBUTING is careful about new dependencies, so I want your read on this before I open a PR.
-
Parallel chunk embedding with ThreadPoolExecutor when the chunk count is large
Internal only. HF tokenizer + numpy pooling release the GIL. Skip below a small threshold.
There is also a small warmup on index load so the first search after a cache hit does not pay a one-shot mixed dtype GEMM cost. model2vec often returns float16 and the vector store runs in float32. Casting the query embedding to float32 and priming encode + a dummy semantic query during load_from_disk fixed that.
Fork reference: https://github.com/mazulo/semble
Relevant commits on the fork history:
aef7959: parser preload + fast cache validation + large repo profiler
c3b5091: semble-core Rust tokenizer
7e1adc5: parallel embedding
Minimal example
Behaviour stays the same:
semble init
semble search "authentication middleware" ./app/api/v1/auth
If the optional native tokenizer is accepted upstream:
No new CLI flags are required for the core perf path. Profiler used for measurement:
uv run python -m benchmarks.profile_large_repo /path/to/large/repo
Matched benchmarks vs current upstream
Same machine. Both runs forced to the same settings:
_DESIRED_CHUNK_LENGTH_CHARS = 750
SEMBLE_MODEL_NAME=minishlab/potion-code-16M-v2
- Same chunk count: 341,832 across 14,691 files
- Fork with
semble-core installed
| Metric |
Upstream |
Fork |
Delta |
| Cold build |
90.8s |
83.4s |
-8% |
| Warm cache hit |
6.09s |
3.80s |
-38% |
| embed_chunks |
29.3s |
26.7s |
-9% |
| tokenize BM25 prep |
7.0s |
2.6s |
-64% |
| Search first cold |
42.9ms |
20.7ms |
-52% |
| Search warm p50 |
6.8ms |
6.3ms |
-7% |
| Search warm p90 |
8.1ms |
6.9ms |
-15% |
| Search warm p99 |
8.3ms |
7.2ms |
-13% |
Cold build phase detail:
Upstream: chunk 34.5s | embed 29.3s | tokenize 7.0s | bm25 10.1s | total 90.8s
Fork: chunk 35.6s | embed 26.7s | tokenize 2.6s | bm25 8.9s | total 83.4s
Full raw output is in the gist linked above.
Earlier internal measurements while developing the patches, same settings inside the fork tree:
| Patch |
Effect |
| Parallel parser preload |
Parser load ~33s → ~1s. One VSCode cold build ~97s → ~49s |
| Rust BM25 tokenizer |
Tokenize ~13s → ~1.5s on that VSCode run |
| Parallel embedding |
Embed ~28.7s → ~19.3s on ~105k chunks |
Parallel parser preload does not move chunk_source much on my mostly Python tree. Multi-language repos may still benefit more.
Questions for maintainers
- Are large repo indexing and warm cache improvements in scope for semble itself?
- Would an optional
semble-core wheel be acceptable if pure Python stays the default path? Or do you prefer staying pure Python only?
- Prefer four tiny PRs for preload, cache mtimes, tokenize, and embed, plus a small warm-on-load PR? Or one larger perf PR after this discussion?
- Anything here you would rather not take?
What I am not asking for yet
- Merging the fork as-is
- Shipping my packaging or release workflow differences
- Expanding scope beyond indexing and cache performance
Most importantly: thank you all for semble! It has been a daily driver on large codebases, which is why I spent time on this.
Summary
I've been running a personal fork of semble for almost 2 months now against a large monorepo. Roughly 15k files and hundreds of thousands of chunks. Almost everything I changed was about performance. The Rust module exists for the same reason.
I follow CONTRIBUTING and wanted to open a discussion issue first before sending PRs.
Fork: https://github.com/mazulo/semble
Matched benchmarks: https://gist.github.com/mazulo/94c8243730bcee05f90e97e2c5059fe8
The fork is currently synced with MinishLab main and tagged as
0.5.2on my side.Problem
On large local checkouts, cold indexing is dominated by:
For my agent workflows the important loops are:
semble searchor MCP call after a clone or cache missWarm search latency was already fine for me. The painful part was time to first useful index, and warm cache validation on big trees.
Why this belongs in semble
These are optimizations inside the existing index, build, and cache pipeline. Not a separate search product.
search/find_relatedA wrapper cannot fix parser preload, embed batching, or cache mtime validation without forking the library anyway.
What changed in the fork
Four independent pieces. If there is interest I would rather send them as separate PRs.
Parallel tree-sitter parser preload before chunking
Internal only.
get_parserreleases the GIL, so threads help.Faster cache validation
Store per-file and per-dir mtimes at save time. Validate with
statinstead of a full walk. Legacy caches can fall back to the current walk.Optional Rust BM25 tokenizer via
semble-coreNo API change.
tokens.tokenizetriessemble_coreand falls back to pure Python. This would be an optional install. I know CONTRIBUTING is careful about new dependencies, so I want your read on this before I open a PR.Parallel chunk embedding with
ThreadPoolExecutorwhen the chunk count is largeInternal only. HF tokenizer + numpy pooling release the GIL. Skip below a small threshold.
There is also a small warmup on index load so the first search after a cache hit does not pay a one-shot mixed dtype GEMM cost. model2vec often returns float16 and the vector store runs in float32. Casting the query embedding to float32 and priming encode + a dummy semantic query during
load_from_diskfixed that.Fork reference: https://github.com/mazulo/semble
Relevant commits on the fork history:
aef7959: parser preload + fast cache validation + large repo profilerc3b5091:semble-coreRust tokenizer7e1adc5: parallel embeddingMinimal example
Behaviour stays the same:
semble init semble search "authentication middleware" ./app/api/v1/authIf the optional native tokenizer is accepted upstream:
No new CLI flags are required for the core perf path. Profiler used for measurement:
Matched benchmarks vs current upstream
Same machine. Both runs forced to the same settings:
_DESIRED_CHUNK_LENGTH_CHARS = 750SEMBLE_MODEL_NAME=minishlab/potion-code-16M-v2semble-coreinstalledCold build phase detail:
Full raw output is in the gist linked above.
Earlier internal measurements while developing the patches, same settings inside the fork tree:
Parallel parser preload does not move
chunk_sourcemuch on my mostly Python tree. Multi-language repos may still benefit more.Questions for maintainers
semble-corewheel be acceptable if pure Python stays the default path? Or do you prefer staying pure Python only?What I am not asking for yet
Most importantly: thank you all for semble! It has been a daily driver on large codebases, which is why I spent time on this.