Skip to content

Vector chunks after the first lose their note's title and permalink #1335

Description

@gkhnelbstn

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):

  1. compose_row_source_text() joins title, permalink, content_snippet into one blob. The title is contributed once, positionally.
  2. 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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions