Three numbers in the local embedding path disagree with each other:
| setting |
value |
source |
max_chunk_bytes |
24576 |
config.rs, commented "~6K-7K tokens, safe for any embedding model" |
EMBEDDING_MAX_LENGTH |
512 |
local_models/mod.rs |
| model max sequence length |
8192 |
jina-embeddings-v5-text-nano-retrieval model card |
load_tokenizer applies the middle one as a hard truncation:
tokenizer.with_truncation(Some(tokenizers::TruncationParams {
max_length, // 512
strategy: tokenizers::TruncationStrategy::LongestFirst,
..Default::default()
}))
So the chunker is allowed to emit chunks of ~6-7K tokens, the model can encode 8192, and everything
past token 512 is discarded before embedding. The comment on max_chunk_bytes says the size is
chosen to be safe for the embedding model, but the embedding model never sees most of it.
Measured impact
Indexed two repositories from the eval corpus at their pinned commits and measured chunk sizes
against a ~2KB proxy for 512 tokens (code runs roughly 3-4 characters per token):
| repo |
chunks |
over ~512 tokens |
largest chunk |
content past the window |
ripgrep |
5495 |
505 (9.2%) |
24558 B |
1.5 MB |
fastify |
8986 |
185 (2.1%) |
24554 B |
529 KB |
A minority of chunks by count, but they are the largest ones — whole functions and tier-0 line
chunks — and 1.5 MB of ripgrep's indexed content contributes nothing to its own embedding. The
tail of a long function is unreachable by vector search, though BM25 still matches it, which is
probably why this has not been obvious.
Notes
- The 2KB proxy is rough. The precise figure needs tokenizing the corpus, but the direction does
not depend on the constant: the largest chunk is 12x the window whatever the exact ratio.
- Raising
EMBEDDING_MAX_LENGTH is not free. Attention cost grows with sequence length and the
local backend is already the slow path, so the sensible fix might instead be lowering
max_chunk_bytes to match the window, or splitting large chunks for embedding and keeping them
whole for display.
- Whichever way it goes, the two constants should be derived from one another rather than set
independently, so they cannot drift apart again.
Raising this as an issue rather than a PR because the resolution is a quality-versus-latency call
that wants benchmark evidence, and this touches the indexing path for every local-backend user.
Three numbers in the local embedding path disagree with each other:
max_chunk_bytesconfig.rs, commented "~6K-7K tokens, safe for any embedding model"EMBEDDING_MAX_LENGTHlocal_models/mod.rsjina-embeddings-v5-text-nano-retrievalmodel cardload_tokenizerapplies the middle one as a hard truncation:So the chunker is allowed to emit chunks of ~6-7K tokens, the model can encode 8192, and everything
past token 512 is discarded before embedding. The comment on
max_chunk_bytessays the size ischosen to be safe for the embedding model, but the embedding model never sees most of it.
Measured impact
Indexed two repositories from the eval corpus at their pinned commits and measured chunk sizes
against a ~2KB proxy for 512 tokens (code runs roughly 3-4 characters per token):
ripgrepfastifyA minority of chunks by count, but they are the largest ones — whole functions and tier-0 line
chunks — and 1.5 MB of ripgrep's indexed content contributes nothing to its own embedding. The
tail of a long function is unreachable by vector search, though BM25 still matches it, which is
probably why this has not been obvious.
Notes
not depend on the constant: the largest chunk is 12x the window whatever the exact ratio.
EMBEDDING_MAX_LENGTHis not free. Attention cost grows with sequence length and thelocal backend is already the slow path, so the sensible fix might instead be lowering
max_chunk_bytesto match the window, or splitting large chunks for embedding and keeping themwhole for display.
independently, so they cannot drift apart again.
Raising this as an issue rather than a PR because the resolution is a quality-versus-latency call
that wants benchmark evidence, and this touches the indexing path for every local-backend user.