documentation updates
This commit is contained in:
@@ -37,9 +37,10 @@ Fusion lives in orchestration — the service already coordinates multiple data
|
||||
sources, and fusion is a retrieval strategy, not a storage concern.
|
||||
|
||||
```
|
||||
getFusedEpisodes()
|
||||
├── getSemanticEpisodes() — Qdrant embed+search → fetch full rows by ID
|
||||
│ (existing path, unchanged)
|
||||
getFusedEpisodes(…, queryVector, …)
|
||||
├── getSemanticEpisodes(queryVector) — Qdrant search → fetch full rows by ID
|
||||
│ (query embedded ONCE upstream in assembleContext and shared with entity
|
||||
│ search — no longer embedded separately here)
|
||||
└── getFTSResults() — memory-service /episodes/search → full rows directly
|
||||
(skipped entirely if keywordWeight == 0)
|
||||
↓
|
||||
@@ -48,6 +49,10 @@ fuseEpisodeResults() — pure RRF, no I/O
|
||||
fusedEpisodes[] — top semanticLimit episodes by RRF score
|
||||
```
|
||||
|
||||
The query embedding is computed once per turn in `assembleContext` and passed
|
||||
into both fused retrieval and entity search; if embedding fails, both receive
|
||||
`null` and degrade to empty results rather than erroring.
|
||||
|
||||
### Data Shape Consistency
|
||||
|
||||
Both sides must enter fusion as `Episode[]` — full SQLite row objects with
|
||||
@@ -59,6 +64,38 @@ the same shape — and both must be filtered against `recentIds` first:
|
||||
FTS requests `semanticLimit * 2` results to provide headroom for the
|
||||
`recentIds` filter without under-serving the fusion.
|
||||
|
||||
## Query Tokenization
|
||||
|
||||
Before FTS5 sees the query, `buildFtsQuery(query)` (in
|
||||
`memory-service/src/episodic/index.js`) turns the raw message into a MATCH
|
||||
expression:
|
||||
|
||||
1. Lowercase and split on any non-letter/number (`/[^\p{L}\p{N}]+/u`, unicode-aware)
|
||||
2. Drop stopwords (a small `FTS_STOPWORDS` set of common function words) and single-character tokens
|
||||
3. Wrap each surviving token in double quotes and join with ` OR `
|
||||
|
||||
So `"How do I configure the Qdrant collection?"` becomes
|
||||
`"configure" OR "qdrant" OR "collection"`. If nothing survives (an
|
||||
all-stopword message like `"how do I do it?"`), it returns `null` and
|
||||
`searchEpisodes` returns `[]` — keyword search sits out that turn and
|
||||
semantic retrieval carries it.
|
||||
|
||||
**Why this matters:** the earlier implementation quoted the *entire* message
|
||||
as one FTS5 phrase, which required the whole string to appear verbatim in an
|
||||
episode — so keyword recall was effectively nil for conversational queries.
|
||||
Tokenizing into OR-joined terms is what makes `keywordWeight > 0` actually
|
||||
contribute anything.
|
||||
|
||||
**Injection safety:** quoting each token individually means any
|
||||
FTS5-significant token inside the user's message (a literal `OR`, `*`, `"`,
|
||||
etc.) is matched as a search term rather than parsed as an operator. This
|
||||
replaces the safety the old whole-phrase quoting provided.
|
||||
|
||||
The stopword set is deliberately conservative and tuned iteratively — high
|
||||
frequency filler (`the`, `is`, `one`, `there`, …) is dropped, but borderline
|
||||
words that can carry signal (`time`, `good`, `way`) are kept. Add to the set
|
||||
when a common word is observed producing noisy matches.
|
||||
|
||||
## FTS Session Scoping
|
||||
|
||||
Without scoping, FTS5 searches across all episodes in the database. For
|
||||
|
||||
Reference in New Issue
Block a user