You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
openkos has no chunking layer on the read side. One document = one embedding = one retrieval unit = one context block. That single decision produces the retrieval defects filed separately as #882 and #887, and one more that is worse than either: roughly half of every long Source is invisible to dense retrieval.
The asymmetry is stark, because the codebase already solved this on the other side: extraction chunks long sources through _fan_out_windows (src/openkos/extraction/concept.py:2287-2295). openkos chunks when it writes knowledge and does not when it reads it.
Proof: the tail of a long document contributes nothing to its own embedding
src/openkos/state/reindex.py:320-330 composes title + description + tags + full body and sends it in one call:
bge-m3 allows 8192 tokens (/api/show: bert.context_length = 8192). bundle/sources/transcription3.md is 56,037 chars ≈ 15,785 tokens. Embedding the whole document, its first half, and its second half, then comparing cosine similarity:
cos(full, FIRST half) = 1.0000
cos(full, SECOND half) = 0.6582
cos(FIRST, SECOND) = 0.6582
1.0000, exactly. The full document's embedding is its first half's embedding. The second half contributes nothing — this is absence, not degradation.
Confirmed one vector per document, not per chunk: SELECT count(*) FROM vector_meta = 32 for 32 embeddable docs.
Blast radius in a 3-source corpus
source
chars
~tokens
invisible to dense retrieval
transcription1.md
55,403
15,606
47%
transcription2.md
10,604
2,987
none
transcription3.md
57,116
16,089
49%
The silent asymmetry between the two retrieval halves
query prints retrieval: 10 FTS + 10 dense → 5 fused. FTS5 has no length limit and indexes the same four fields (fts.py:220-234), so the lexical half sees the whole document while the dense half sees a prefix. Demonstrated with terms that occur only in the invisible tail of transcription3:
sqlite3 .openkos/fts.db "SELECT count(*) FROM docs WHERE docs MATCH 'desprende';" -> 1
sqlite3 .openkos/fts.db "SELECT count(*) FROM docs WHERE docs MATCH 'máquina';" -> 1
Findable lexically, unrepresentable semantically. Nothing discloses that the two halves of the fusion have different coverage.
This issue: half of every long Source is unreachable by semantic search, silently.
Fixing the chunking layer collapses all three. Fixing #882 alone leaves the embedding blindness untouched.
Proposed fix
Chunk on index. Split each document into windows below the embedder's limit, embed each, and store (concept_id, chunk_index, char_span) rows instead of one row per document. The window logic can likely reuse what extraction already does.
Retrieve chunks, cite parents. A context block becomes the matching chunk rather than the whole file, and provenance records the parent concept plus the span — which is also the honest fix for query sends unbounded context and files provenance the model never read #882's citation problem, since the span names exactly what the model was shown.
Stop embedding the raw source text into bundle/sources/*.md and keep it only in raw/. That removes the huge documents at the root, but changes the bundle format and loses the property that a Source document is self-contained. Chunking is the less invasive root fix and helps every long document, not only Sources.
Note on the measurement
context_window: 12288 in openkos.yaml is a separate lever and does not touch this: the embedder's 8192 limit is bge-m3's own, and raising the chat window changes nothing about what got embedded. Re-running openkos reindex will not help either — it recomputes the same one-vector-per-document truncated embedding.
Repro
python3 - <<'PY'# embed a >8192-token document whole, then its two halves, and compare cosinesPY
Any bundle/sources/*.md above ~29,000 chars reproduces cos(full, first_half) == 1.0000.
openkos has no chunking layer on the read side. One document = one embedding = one retrieval unit = one context block. That single decision produces the retrieval defects filed separately as #882 and #887, and one more that is worse than either: roughly half of every long Source is invisible to dense retrieval.
The asymmetry is stark, because the codebase already solved this on the other side: extraction chunks long sources through
_fan_out_windows(src/openkos/extraction/concept.py:2287-2295). openkos chunks when it writes knowledge and does not when it reads it.Proof: the tail of a long document contributes nothing to its own embedding
src/openkos/state/reindex.py:320-330composes title + description + tags + full body and sends it in one call:bge-m3allows 8192 tokens (/api/show:bert.context_length = 8192).bundle/sources/transcription3.mdis 56,037 chars ≈ 15,785 tokens. Embedding the whole document, its first half, and its second half, then comparing cosine similarity:1.0000, exactly. The full document's embedding is its first half's embedding. The second half contributes nothing — this is absence, not degradation.
Confirmed one vector per document, not per chunk:
SELECT count(*) FROM vector_meta= 32 for 32 embeddable docs.Blast radius in a 3-source corpus
transcription1.mdtranscription2.mdtranscription3.mdThe silent asymmetry between the two retrieval halves
queryprintsretrieval: 10 FTS + 10 dense → 5 fused. FTS5 has no length limit and indexes the same four fields (fts.py:220-234), so the lexical half sees the whole document while the dense half sees a prefix. Demonstrated with terms that occur only in the invisible tail oftranscription3:Findable lexically, unrepresentable semantically. Nothing discloses that the two halves of the fusion have different coverage.
Why the symptoms follow
Fixing the chunking layer collapses all three. Fixing #882 alone leaves the embedding blindness untouched.
Proposed fix
(concept_id, chunk_index, char_span)rows instead of one row per document. The window logic can likely reuse what extraction already does.Alternative considered
Stop embedding the raw source text into
bundle/sources/*.mdand keep it only inraw/. That removes the huge documents at the root, but changes the bundle format and loses the property that a Source document is self-contained. Chunking is the less invasive root fix and helps every long document, not only Sources.Note on the measurement
context_window: 12288inopenkos.yamlis a separate lever and does not touch this: the embedder's 8192 limit isbge-m3's own, and raising the chat window changes nothing about what got embedded. Re-runningopenkos reindexwill not help either — it recomputes the same one-vector-per-document truncated embedding.Repro
Any
bundle/sources/*.mdabove ~29,000 chars reproducescos(full, first_half) == 1.0000.