Dois defeitos medidos no índice real (31.098 versículos), ambos na metade
lexical da busca híbrida:
1. Sem normalização morfológica, 'amar' da pergunta nunca casava com 'amai' ou
'amarás' do versículo. Para 'o que Jesus disse sobre amar os inimigos?',
Mateus 5:44 ficava em 434º no BM25 enquanto Marcos 12:34 liderava por casar
apenas 'jesus'+'disse'. keyword.py ganha um stemmer de sufixos em etapas
(estilo RSLP, sem dependência externa) com guarda de comprimento mínimo do
radical — sem ela 'deus' viraria 'd' e 'mar' viraria 'm'. Rank de Mt 5:44: 12º.
2. O BM25 indexava a janela de contexto. Os dois lados querem granularidades
opostas: a janela dá tema ao embedding, mas multiplica os termos incidentais
que o BM25 pontua. Indexando o versículo isolado, Mt 5:44 vai a 1º e os
achados só-lexicais deixam de ser aleatórios ('ansioso e não consigo dormir'
-> Jó 3:13 'teria dormido'; 'não sei que decisão tomar' -> Joel 3:14 'vale da
decisão').
Colisão conhecida do stemming: 'luto' e 'lutavam' reduzem ao mesmo radical.
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.