Thanks for basic-memory — it backs a small bilingual (Turkish/English) engineering vault for our team, and the local-first design is why we can keep it on a shared repo of plain markdown at all.
Following CONTRIBUTING's "create an issue first to discuss the approach": while tuning multilingual retrieval I measured something in the chunk-building path that looks unintended, and the fix appears well contained.
Environment
- basic-memory v0.22.1, sqlite backend, FastEmbed provider
- Embedding model
intfloat/multilingual-e5-large
- Corpus: 8 shared team notes, Turkish queries against English notes
- Code below re-checked against current
main, where the logic moved to src/basic_memory/repository/semantic_chunking.py but is otherwise identical
What happens
compose_row_source_text() prepends the note's title and permalink to the row's source text. split_text_into_chunks() then splits that text on markdown headings and on every bullet. Title and permalink are ordinary non-bullet lines, so they merge into the first chunk and only the first: every later chunk — which for an observations-style note means every single - [fact] … bullet — is embedded with no indication of which note it belongs to.
For a vault whose notes are mostly bullet lists of discrete facts, that means most vectors carry no note identity at all.
Why
Two pointers in semantic_chunking.py (search_repository_base.py up to v0.22.x):
compose_row_source_text() joins title, permalink, content_snippet into one blob. The title is contributed once, positionally.
split_text_into_chunks() starts a new section whenever _BULLET_PATTERN matches, so the first bullet closes the title's section. Everything after inherits nothing.
Measurements
Read back from search_vector_chunks on a real vault, not estimated:
|
|
| chunks |
134 (8 notes) |
| median chunk |
178 chars |
| p25 / p75 |
89 / 441 chars |
| longest |
888 chars |
| under 60 chars |
23 chunks (17%) |
The short tail is structural rather than sloppy writing:
## Relations is embedded as a standalone 12-character chunk, once per note that has one
- each
- relates_to [[Some Note]] line becomes its own chunk
- [name] abcws-monorepo (23 chars) is its own chunk
Direct probe of the cost, multilingual-e5-large, Turkish query against English bullets: naming the subject inside the bullet raised gold-chunk cosine from 0.769 to 0.807 (and top score 0.854 → 0.898) versus the bare bullet the chunker actually stores.
Being honest about that probe's limits: 6 candidate chunks, and it did not change ranking — both variants retrieved at rank 1. It measures the margin the missing context costs, not a recall win. On a larger vault where many notes share vocabulary, that margin is what has to separate the right note from a plausible wrong one.
Suggested shapes
Both are localized to semantic_chunking.py:
- Carry note identity into every chunk — prefix each chunk with the title (and optionally the permalink) rather than relying on positional inheritance in chunk 0.
- Don't emit content-free chunks — a chunk that is only a heading, or only a
relates_to wikilink, spends an embedding and can never match a real query. Merging a bare heading into the following section would also make heading text useful to the chunks under it.
Either changes chunk text, so build_entity_fingerprint() invalidates and existing vaults re-embed on next sync — worth calling out in release notes.
Related but distinct
Offer
Happy to test a branch against this Turkish/English vault and report before/after chunk stats and retrieval scores — just point me at it. Not opening a PR directly since CONTRIBUTING asks for an issue first, and the CLA is a call for the repo owner on our side rather than mine.
Thanks for basic-memory — it backs a small bilingual (Turkish/English) engineering vault for our team, and the local-first design is why we can keep it on a shared repo of plain markdown at all.
Following CONTRIBUTING's "create an issue first to discuss the approach": while tuning multilingual retrieval I measured something in the chunk-building path that looks unintended, and the fix appears well contained.
Environment
intfloat/multilingual-e5-largemain, where the logic moved tosrc/basic_memory/repository/semantic_chunking.pybut is otherwise identicalWhat happens
compose_row_source_text()prepends the note's title and permalink to the row's source text.split_text_into_chunks()then splits that text on markdown headings and on every bullet. Title and permalink are ordinary non-bullet lines, so they merge into the first chunk and only the first: every later chunk — which for an observations-style note means every single- [fact] …bullet — is embedded with no indication of which note it belongs to.For a vault whose notes are mostly bullet lists of discrete facts, that means most vectors carry no note identity at all.
Why
Two pointers in
semantic_chunking.py(search_repository_base.pyup to v0.22.x):compose_row_source_text()joinstitle,permalink,content_snippetinto one blob. The title is contributed once, positionally.split_text_into_chunks()starts a new section whenever_BULLET_PATTERNmatches, so the first bullet closes the title's section. Everything after inherits nothing.Measurements
Read back from
search_vector_chunkson a real vault, not estimated:The short tail is structural rather than sloppy writing:
## Relationsis embedded as a standalone 12-character chunk, once per note that has one- relates_to [[Some Note]]line becomes its own chunk- [name] abcws-monorepo(23 chars) is its own chunkDirect probe of the cost,
multilingual-e5-large, Turkish query against English bullets: naming the subject inside the bullet raised gold-chunk cosine from 0.769 to 0.807 (and top score 0.854 → 0.898) versus the bare bullet the chunker actually stores.Being honest about that probe's limits: 6 candidate chunks, and it did not change ranking — both variants retrieved at rank 1. It measures the margin the missing context costs, not a recall win. On a larger vault where many notes share vocabulary, that margin is what has to separate the right note from a plausible wrong one.
Suggested shapes
Both are localized to
semantic_chunking.py:relates_towikilink, spends an embedding and can never match a real query. Merging a bare heading into the following section would also make heading text useful to the chunks under it.Either changes chunk text, so
build_entity_fingerprint()invalidates and existing vaults re-embed on next sync — worth calling out in release notes.Related but distinct
embed_query()on currentmainstill delegates straight toembed_documents(), so if that issue was closed as documentation-only, this one is independent of it either way. Measured on our vault, applying the e5 prefixes by hand made no difference to ranking, so we are not blocked on it.Offer
Happy to test a branch against this Turkish/English vault and report before/after chunk stats and retrieval scores — just point me at it. Not opening a PR directly since CONTRIBUTING asks for an issue first, and the CLA is a call for the repo owner on our side rather than mine.