feat: stemming português no BM25 e índice lexical sobre o versículo

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.
This commit is contained in:
omp committed 2026-11-02 12:39:54 -03:00
1 parent 957c73b09e
commit 8f5e098d8f
5 files changed
+226 -15

No files matched your search

@@ -3,6 +3,42 @@
**Data:** 2026-11-01
**Status:** Proposed
> ## Atualização (2026-11-02): ingestão por corpus estruturado
>
> As seções que descrevem **PDF → OCR → parser de índice → reconstrução de
> capítulo/versículo** foram substituídas. O resto do desenho (FastAPI, ChromaDB,
> validação de citações, SSE, Docker Compose, provedores de LLM) segue válido.
>
> **O que mudou:** a entrada passou a ser XML [USFX](https://ebible.org/usfx/) em
> domínio público (`corpus/por-almeida.usfx.xml`, Almeida, 66 livros, 31.098
> versículos), versionado no repositório.
>
> **Por quê:** o PDF exigia OCR das 3.364 páginas e a camada textual é corrompida,
> então cada referência era *reconstruída* a partir de texto ruidoso. Com o corpus
> estruturado a referência é a coordenada de onde o texto foi lido. Medido:
> indexação de 31.098 versículos em ~30 min contra 17–67 min de OCR, sem Tesseract,
> sem PyMuPDF e sem cache de páginas.
>
> **Duas falhas encontradas no caminho**, ambas silenciadas por projeto anterior:
> 1. `create_app` criava o `VectorStore` antes do embedder; como o store default
> para `HashEmbedder`, o modelo configurado **nunca** era carregado em produção
> e a busca rodava sem semântica nenhuma.
> 2. `transformers` não estava pinado: a versão 5.x exige `torch>=2.6`, e o
> Dockerfile pinava 2.5.1 — o `sentence-transformers` desativava o PyTorch e o
> fallback silencioso caía no mesmo `HashEmbedder`.
>
> `/api/status` agora reporta o embedder **em uso** (não o do `.env`) e qualquer
> fallback aparece em `errors`.
>
> **Busca:** passou a híbrida (vetorial + BM25, fusão por RRF) e cada chunk indexa o
> versículo com uma janela de vizinhos. Medido em 5.703 versículos reais: a busca
> puramente vetorial não liga "estou com medo do futuro" a "não temas, porque eu sou
> contigo" (similaridade 0,19, no nível de um par sem relação), enquanto acerta
> "amar os inimigos" → Mateus 5:44 (0,683). Os dois lados erram de modos diferentes.
> `intfloat/multilingual-e5-small` foi descartado apesar de treinado para
> recuperação: a faixa de similaridade dele tem 0,02 de largura (0,853–0,873), o que
> inutilizaria o piso de evidência do qual a garantia de fidelidade depende.
## 1. Intenção
O Pastor Online é uma aplicação web local, containerizada, que lê PDFs da Bíblia,