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
@@ -12,9 +12,16 @@ Containerizado com Docker Compose: frontend React, backend FastAPI, ChromaDB e
|
||||
## Como funciona
|
||||
|
||||
```
|
||||
./corpus/*.usfx.xml ──▶ parser USFX ──▶ embeddings ──▶ ChromaDB
|
||||
│
|
||||
navegador ◀── SSE ── FastAPI ◀── busca + validação de citações
|
||||
./corpus/*.usfx.xml ──▶ parser USFX ──▶ versículo + janela de contexto
|
||||
│
|
||||
┌───────────────┴───────────────┐
|
||||
embeddings (MiniLM) BM25 (palavras)
|
||||
│ │
|
||||
ChromaDB índice em memória
|
||||
└───────────────┬───────────────┘
|
||||
fusão RRF
|
||||
│
|
||||
navegador ◀── SSE ── FastAPI ◀────┴── validação de citações
|
||||
```
|
||||
|
||||
**Fidelidade ao texto.** A resposta só é gerada quando há evidência recuperada do
|
||||
@@ -28,6 +35,26 @@ reconstrução de capítulos a partir de marcadores de página, nem ruído de
|
||||
digitalização — a referência exibida é exatamente a coordenada de onde o texto foi
|
||||
lido.
|
||||
|
||||
**Busca híbrida.** Vetores e palavras erram de modos diferentes, então os dois lados
|
||||
são combinados. Medido no corpus real, com 5.703 versículos e 6 perguntas pastorais:
|
||||
|
||||
- A busca 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 alguma). Modelos de
|
||||
paráfrase casam formulações parecidas, não pergunta e resposta.
|
||||
- A mesma busca acerta em cheio quando há vocabulário em comum: "o que Jesus disse
|
||||
sobre amar os inimigos?" → Mateus 5:44 no top-1 (0,683).
|
||||
- O BM25 cobre o lado lexical e ainda pega termos que o embedding dilui.
|
||||
|
||||
A fusão é por **posição** nos dois rankings (Reciprocal Rank Fusion), não por nota:
|
||||
cosseno vive em 0..1 e BM25 não tem teto, então normalizar um contra o outro seria
|
||||
arbitrário. `MIN_SIMILARITY` continua valendo como piso do lado vetorial.
|
||||
|
||||
**Janela de contexto.** Um versículo isolado tem ~20 palavras — curto demais para o
|
||||
embedding captar o tema. Cada chunk indexa o versículo junto com `CONTEXT_WINDOW`
|
||||
vizinhos de cada lado (dentro do mesmo livro, atravessando capítulos). A referência
|
||||
exibida continua sendo a do versículo central, e é o texto **dele** que aparece na
|
||||
interface; a janela inteira é o que vai para o modelo como evidência.
|
||||
|
||||
---
|
||||
|
||||
## Corpus
|
||||
@@ -161,7 +188,8 @@ Todas as variáveis ficam no `.env`. As principais:
|
||||
| `LLM_PROVIDER` | `ollama` | `ollama`, `openai`, `groq`, `anthropic` ou `stub` |
|
||||
| `EMBEDDING_MODEL` | `paraphrase-multilingual-MiniLM-L12-v2` | `all-MiniLM-L6-v2` é menor e mais rápido |
|
||||
| `TOP_K` | `6` | Quantos trechos são recuperados por pergunta |
|
||||
| `MIN_SIMILARITY` | `0.35` | Corte de relevância; abaixo disso o sistema responde que o texto não aborda o assunto |
|
||||
| `MIN_SIMILARITY` | `0.35` | Piso do lado vetorial; abaixo disso o trecho só entra se o BM25 o encontrar |
|
||||
| `CONTEXT_WINDOW` | `2` | Vizinhos indexados com cada versículo (0 desliga a janela) |
|
||||
| `INDEX_BATCH_SIZE` | `512` | Versículos gravados no banco por lote |
|
||||
| `CORPUS_DIR` | `/data/corpus` | Diretório dos arquivos USFX (montado de `./corpus`) |
|
||||
| `REINDEX_TOKEN` | vazio | Se preenchido, `/api/reindex` exige o header `X-Reindex-Token` |
|
||||
@@ -173,9 +201,23 @@ Todas as variáveis ficam no `.env`. As principais:
|
||||
## Solução de problemas
|
||||
|
||||
**O chat responde que o texto "não aborda diretamente o assunto".**
|
||||
O índice pode estar vazio (`/api/status`) ou a pergunta pode estar longe do corpus.
|
||||
Reduza `MIN_SIMILARITY` para 0.25, ou aumente `TOP_K` para 10. Se a pergunta citar
|
||||
uma referência explícita, a busca é determinística e não depende do limiar.
|
||||
Nenhum dos dois lados da busca encontrou nada acima do piso. Confira `/api/status`
|
||||
(índice vazio?), reduza `MIN_SIMILARITY` para 0.25 ou aumente `TOP_K` para 10. Se a
|
||||
pergunta citar uma referência explícita (`João 3:16`), a busca é determinística por
|
||||
metadados e não depende de limiar nenhum.
|
||||
|
||||
**As respostas citam versículos que parecem fora do tema.**
|
||||
Verifique em `/api/status` o campo `model`. Se aparecer
|
||||
`hash-determinístico (sem semântica)` em vez do nome do modelo configurado, o
|
||||
embedding **não** está usando o modelo real — o campo `errors` traz o motivo. Isso
|
||||
acontece quando `torch`/`transformers`/`sentence-transformers` saem de alinhamento;
|
||||
as versões estão pinadas em `backend/requirements.txt` e no `Dockerfile` exatamente
|
||||
por isso. Depois de corrigir, reindexe: os vetores gravados são do embedder antigo.
|
||||
|
||||
**A busca por palavra não está funcionando.**
|
||||
O índice BM25 vive em memória e é reconstruído no arranque e após cada indexação.
|
||||
Confira no log a linha `Índice de palavras-chave construído com N trechos`. Sem ela,
|
||||
a recuperação degrada para só-vetorial.
|
||||
|
||||
**"nenhum versículo pôde ser lido" em `/api/status`.**
|
||||
O arquivo em `./corpus` não é um USFX válido, ou não contém `<book>`. Confira com:
|
||||
@@ -210,8 +252,10 @@ indicado em `OLLAMA_MODEL` na primeira execução. Para outro modelo:
|
||||
│ ├── app/
|
||||
│ │ ├── config.py # settings via pydantic-settings
|
||||
│ │ ├── models.py # schemas da API
|
||||
│ │ ├── corpus.py # USFX → versículos + nomes canônicos dos livros
|
||||
│ │ ├── corpus.py # USFX → versículos + janela de contexto + nomes canônicos
|
||||
│ │ ├── embedding.py # sentence-transformers
|
||||
│ │ ├── keyword.py # tokenizador PT, BM25 e fusão RRF
|
||||
│ │ ├── retrieval.py # recuperadores vetorial e híbrido
|
||||
│ │ ├── vector_store.py # ChromaDB (+ versão em memória para testes)
|
||||
│ │ ├── indexer.py # descoberta, manifesto incremental, progresso
|
||||
│ │ ├── llm.py # adaptadores Ollama/OpenAI/Groq/Anthropic/stub
|
||||
|
||||
Reference in new issue
Block a user