feat: busca híbrida (vetorial + BM25 com fusão RRF) e janela de contexto
Recuperação: - keyword.py: tokenizador PT (acentos, stopwords, dígitos), BM25 Okapi com índice invertido, e reciprocal_rank_fusion com desempate determinístico - retrieval.py: VectorRetriever e HybridRetriever; fusão por posição, não por nota (cosseno 0..1 e BM25 sem teto são escalas incomparáveis) - identidade de fusão é a coordenada (arquivo,livro,capítulo,versículo): o mesmo versículo chega das duas buscas com distance diferente e o dataclass nunca os igualaria, então o RRF não premiaria o que ambas confirmam - ChatService recebe retriever injetado; referência explícita mantém precedência Contexto: - VerseChunk ganha context (janela de CONTEXT_WINDOW vizinhos por lado, dentro do livro, atravessando capítulos) e embedding_text - store grava a janela como documento e o versículo em verse_text; a UI mostra o versículo citado e o modelo recebe a janela - snapshot() nos dois stores: o BM25 é em memória e o Chroma persiste, então o arranque precisa reconstruir o índice lexical ou a busca degrada em silêncio Motivação medida em 5703 versículos reais: a busca vetorial não liga 'estou com medo do futuro' a 'não temas' (0,19, nível de par sem relação), e e5-small tem faixa de similaridade de só 0,02 — inutilizaria o piso de evidência.
This commit is contained in:
1 parent
2043d5ddcf
commit
957c73b09e
16 files changed
+1085
-45
No files matched your search
@@ -17,7 +17,7 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from .config import Settings
|
||||
from .corpus import CorpusParseError, VerseChunk, parse_usfx
|
||||
from .corpus import CorpusParseError, parse_usfx
|
||||
from .embedding import Embedder
|
||||
from .vector_store import VectorStore
|
||||
|
||||
@@ -198,7 +198,13 @@ class Indexer:
|
||||
self.report_progress(source.name, 0, 0)
|
||||
|
||||
try:
|
||||
chunks = list(parse_usfx(source, file_name=source.name))
|
||||
chunks = list(
|
||||
parse_usfx(
|
||||
source,
|
||||
file_name=source.name,
|
||||
context_window=self.settings.context_window,
|
||||
)
|
||||
)
|
||||
except CorpusParseError as exc:
|
||||
self._manifest[source.name] = _ManifestEntry(
|
||||
sha256=file_hash, state=STATE_ERROR, indexed_at=_now()
|
||||
@@ -224,7 +230,7 @@ class Indexer:
|
||||
batch_size = max(1, self.settings.index_batch_size)
|
||||
for start in range(0, total, batch_size):
|
||||
batch = chunks[start : start + batch_size]
|
||||
vectors = self.embedder.encode([chunk.text for chunk in batch])
|
||||
vectors = self.embedder.encode([chunk.embedding_text for chunk in batch])
|
||||
self.store.upsert(batch, vectors)
|
||||
self.report_progress(source.name, min(start + batch_size, total), total)
|
||||
|
||||
|
||||
Reference in new issue
Block a user