Relatório Estratégico: Scoring de Busca RAG
Data: 08 de Janeiro de 2026
Autor: Engenheiro Sênior ArboreoLab
Status: Análise e Propostas para Avaliação
Objetivo: Melhorar ranking de documentos na busca semântica/híbrida
📋 Sumário Executivo
O Problema
A busca híbrida atual retorna:
- Entidades Relacionadas — Funcionando bem ✅
- Documentos Relacionados — Mostra apenas segmentos (trechos curtos ~38 caracteres)
O resultado de documentos está "empobrecido" porque:
- Segmentos são fragmentos de layout (células, linhas, blocos visuais)
- Não representam o contexto semântico completo do documento
- O score vetorial de um segmento não reflete a relevância do documento inteiro
O Objetivo
"Encontrar documentos fortemente relacionados aos termos da busca"
Exemplo de busca: "Quem foi o diretor do MAC Walter Zanini"
Resultado ideal (ranqueado por relevância contextual):
- "Walter foi diretor do MAC e promoveu arte contemporânea" — 98% relevante
- "Zanini dirigia o MAC como ninguém na época" — 95% relevante
- "O Museu de Arte Contemporânea tinha Zanini na direção" — 92% relevante
📊 Estado Atual do Sistema
Dados Disponíveis (GeopoliticasVector)
| Recurso | Quantidade | Cobertura | Status |
|---|---|---|---|
| clio_ocr (documentos) | 16.431 | 100% têm ocr_vector | ✅ Completo |
| content_markdown | 16.101 | 98% dos docs | ✅ Texto integral |
| clio_ocr_segments | 13.969 | ~2% dos docs (369) | 🔄 Em expansão |
| clio_entidades | 145.478 | 100% têm semantic_vector | ✅ Completo |
| clio_entidades_vector | 0 | Não migrado | ⏳ Pendente migração |
| clio_ocr_vector | 0 | Não migrado | ⏳ Pendente migração |
| Relacionamentos | ~31.756 docs | Via inFileID | ✅ Disponível |
Dados em ClioVector (Modelo/Referência - Pendente Migração)
| Recurso | Quantidade | Descrição |
|---|---|---|
| clio_entidades_vector | 20.359 | Embeddings alternativos de entidades |
| clio_ocr_vector | 16.024 | Vetores fragmentados (1/3, 2/3, 3/3) |
Nota: Estas tabelas contêm vetores experimentais que ainda não foram migrados para GeopoliticasVector.
Distribuição de Entidades
| Tipo | Quantidade | % Total |
|---|---|---|
| PESSOA | 67.856 | 46.6% |
| ORGANIZACAO | 34.208 | 23.5% |
| LOCAL | 23.570 | 16.2% |
| EVENTO | 19.721 | 13.5% |
| Outros | 123 | 0.2% |
Estrutura de Vetores por Documento
┌────────────────────────────────────────────────────────────────┐
│ DOCUMENTO OCR │
├────────────────────────────────────────────────────────────────┤
│ clio_ocr.ocr_vector (768d) │
│ └─ Embedding do content_markdown completo │
│ │
│ clio_ocr_vector (legado - ClioVector) │
│ ├─ ocr_vector → Embedding completo │
│ ├─ ocr_vector1 → 1/3 inicial (contexto de abertura) │
│ ├─ ocr_vector2 → 1/3 meio (corpo principal) │
│ └─ ocr_vector3 → 1/3 final (fechamento/conclusão) │
│ │
│ clio_ocr_segments (Layout-Aware) │
│ └─ segment_vector[] → Embedding de cada bloco visual │
│ │
│ clio_entidades (via inFileID) │
│ └─ semantic_vector[] → Embedding de cada entidade │
│ │
│ clio_entidades_vector (legado - ClioVector) │
│ └─ entity_vector → Embedding alternativo da entidade │
│ ⚠️ 20.359 registros em ClioVector, não migrado ainda │
└────────────────────────────────────────────────────────────────┘
🎯 Recursos Disponíveis para Scoring
1. Conteúdo Integral do OCR (clio_ocr)
| Campo | Uso | Potencial |
|---|---|---|
content | JSON com metadados do processo | Baixo (estruturado) |
content_markdown | Texto puro transcrito | Alto (busca full-text) |
ocr_vector | Embedding do documento completo | Alto (busca semântica) |
2. Vetores Fragmentados (clio_ocr_vector)
| Campo | Descrição | Potencial |
|---|---|---|
ocr_vector1 | 1/3 inicial do texto | Médio (introdução/cabeçalho) |
ocr_vector2 | 1/3 central do texto | Alto (corpo do assunto) |
ocr_vector3 | 1/3 final do texto | Médio (conclusão/assinatura) |
Hipótese Original: Reduzir ruído buscando partes específicas. Problema: Ainda não implementado/integrado ao motor atual.
3. Segmentos Layout-Aware (clio_ocr_segments)
| Campo | Descrição | Potencial |
|---|---|---|
text_content | Texto do bloco (~38 chars média) | Baixo (fragmentado) |
segment_vector | Embedding do bloco | Baixo (sem contexto) |
bbox_* | Posição no layout | Médio (ordenação visual) |
reading_order | Sequência de leitura | Alto (reconstrução) |
Status: Apenas 369/16.431 docs (2%) têm segmentos.
4. Entidades e Relacionamentos (clio_entidades)
| Campo | Descrição | Potencial |
|---|---|---|
entity_name_text | Nome da entidade | Alto (busca textual) |
isA_text | Tipo (PESSOA, ORGANIZACAO...) | Alto (filtro semântico) |
semantic_vector | Embedding da entidade | Alto (busca semântica) |
inFileID | Documento(s) onde aparece | Crítico (ponte doc↔entidade) |
relatedTo_text | Entidade relacionada | Alto (grafo) |
relationshipType | Forma do relacionamento | Alto (contexto) |
🧠 Propostas de Estratégia de Scoring
Estratégia A: Multi-Signal Scoring (Recomendada)
Princípio: Combinar múltiplos sinais para score composto
SCORE_FINAL = w1·S_doc + w2·S_ent + w3·S_rel + w4·S_text - P_noise
Componentes do Score:
| Sinal | Descrição | Peso Sugerido |
|---|---|---|
| S_doc | Similaridade vetorial do documento completo | 0.35 |
| S_ent | Qtd/score de entidades da query no documento | 0.30 |
| S_rel | Presença de relacionamentos entre entidades | 0.15 |
| S_text | Match full-text no content_markdown | 0.20 |
| P_noise | Penalidade por baixa densidade de matches | -0.10 |
Fluxo de Execução:
1. QUERY: "Quem foi o diretor do MAC Walter Zanini"
│
2. EXTRAÇÃO DE ENTIDADES (via NER ou busca vetorial)
├─► "MAC" (ORGANIZACAO)
├─► "Walter Zanini" (PESSOA)
└─► "diretor" (RELACAO)
│
3. BUSCA VETORIAL DE ENTIDADES
│ SELECT * FROM clio_entidades
│ WHERE VEC_DISTANCE_COSINE(semantic_vector, query_vec) < threshold
│ └─► Retorna entidades similares + seus inFileID
│
4. BUSCA VETORIAL DE DOCUMENTOS
│ SELECT * FROM clio_ocr
│ WHERE VEC_DISTANCE_COSINE(ocr_vector, query_vec) < threshold
│ └─► Retorna docs similares à query completa
│
5. SCORING COMPOSTO
│ Para cada documento:
│ ├─► S_doc = 1 - distance_cosine(doc.ocr_vector, query_vec)
│ ├─► S_ent = COUNT(entidades_query IN doc.entidades) / total_ent_query
│ ├─► S_rel = COUNT(relacionamentos_query IN doc.relacionamentos) / max_rel
│ ├─► S_text = MATCH(content_markdown) AGAINST (query_terms)
│ └─► SCORE = weighted_sum - penalty_if_sparse
│
6. RANKING & THRESHOLD
│ ORDER BY SCORE DESC
│ WHERE SCORE >= min_threshold (ajustável)
│
7. RETORNO COM CONTEXTO
└─► Documentos ranqueados com snippets contextuais
Estratégia B: Entity-Centric Retrieval
Princípio: Documentos são ranqueados pela "densidade" de entidades relevantes
def score_document(doc_id, query_entities):
doc_entities = get_entities_for_document(doc_id)
# Contagem de matches
matched = [e for e in query_entities if e in doc_entities]
# Score base por cobertura
coverage = len(matched) / len(query_entities)
# Boost por relacionamentos
relations = get_relations_between(matched)
relation_boost = min(len(relations) * 0.1, 0.3)
# Penalidade por documento muito genérico
if len(doc_entities) > 50:
specificity_penalty = 0.1
else:
specificity_penalty = 0
return coverage + relation_boost - specificity_penalty
Vantagem: Muito preciso para queries com entidades claras. Desvantagem: Falha em queries conceituais ("arte contemporânea brasileira").
Estratégia C: Hierarchical Retrieval (Dois Estágios)
Princípio: Primeiro encontra candidatos, depois refina
ESTÁGIO 1: Recall Alto (busca ampla)
────────────────────────────────────
├─► Busca vetorial de documentos (top 100)
├─► Busca de entidades relacionadas (top 50)
└─► União de candidatos
ESTÁGIO 2: Precision Alta (re-ranking)
────────────────────────────────────
├─► Para cada candidato:
│ ├─► Calcular S_doc (similaridade vetorial)
│ ├─► Calcular S_ent (cobertura de entidades)
│ ├─► Calcular S_rel (relacionamentos)
│ └─► Cross-check: entidades do doc confirmam query?
├─► Aplicar threshold mínimo
└─► Retornar top_k ranqueado
Estratégia D: Segment Aggregation (Futuro)
Princípio: Agregar scores de segmentos para documento
-- Quando segmentos estiverem completos (16k+ docs)
SELECT
o.id,
o.name,
AVG(1 - VEC_DISTANCE_COSINE(s.segment_vector, query_vec)) as avg_seg_score,
MAX(1 - VEC_DISTANCE_COSINE(s.segment_vector, query_vec)) as max_seg_score,
COUNT(DISTINCT s.id) as matching_segments
FROM clio_ocr o
JOIN clio_ocr_segments s ON s.ocr_id = o.id
WHERE VEC_DISTANCE_COSINE(s.segment_vector, query_vec) < 0.5
GROUP BY o.id
ORDER BY (avg_seg_score * 0.3 + max_seg_score * 0.7) DESC
Status: Viável quando segmentos cobrirem >50% dos documentos.
📐 Fórmula de Score Proposta (Estratégia A)
Score Composto
def calculate_composite_score(doc, query, query_entities, query_vec):
"""
Calcula score composto para um documento.
Pesos ajustáveis via configuração.
"""
# Pesos (configuráveis)
W_DOC = 0.35 # Similaridade vetorial do documento
W_ENT = 0.30 # Cobertura de entidades
W_REL = 0.15 # Relacionamentos
W_TEXT = 0.20 # Full-text match
# 1. Score Vetorial do Documento
doc_distance = vec_distance_cosine(doc.ocr_vector, query_vec)
S_doc = 1 - doc_distance # Converte distância para similaridade
# 2. Score de Entidades
doc_entities = get_entities_for_document(doc.workflow_id)
matched_entities = [e for e in query_entities if e in doc_entities]
S_ent = len(matched_entities) / len(query_entities) if query_entities else 0
# 3. Score de Relacionamentos
relations = get_relations_in_document(doc.workflow_id, query_entities)
S_rel = min(len(relations) / 3, 1.0) # Cap em 1.0
# 4. Score Full-Text
# Normalizado entre 0-1 baseado no match score do MariaDB
S_text = fulltext_match_score(doc.content_markdown, query) / 100
# 5. Penalidade por Ruído
# Documentos muito genéricos (muitas entidades, baixa especificidade)
if len(doc_entities) > 100 and S_ent < 0.5:
P_noise = 0.1
else:
P_noise = 0
# Score Final
score = (W_DOC * S_doc +
W_ENT * S_ent +
W_REL * S_rel +
W_TEXT * S_text -
P_noise)
return min(max(score, 0), 1) # Clamp entre 0-1
Configuração de Thresholds
| Parâmetro | Valor Sugerido | Descrição |
|---|---|---|
min_score | 0.25 | Score mínimo para inclusão |
high_confidence | 0.70 | Score para "muito relevante" |
vec_threshold | 0.45 | Distância máxima vetorial |
entity_boost | 1.2x | Multiplicador se 3+ entidades match |
⚡ Impacto em Performance
Custo por Query (Estimado)
| Operação | Tempo Atual | Após Otimização |
|---|---|---|
| Embedding da query | 50ms | 50ms |
| Busca vetorial docs | 3-5s | 1-2s (com cache quente) |
| Busca vetorial entidades | 200ms | 200ms |
| Full-text search | 50ms | 50ms |
| Scoring composto | N/A | 100ms |
| Total | 4-6s | 1.5-2.5s |
Otimizações Necessárias
-
Cache de Entidades por Documento
- Criar tabela
doc_entity_cacheou view materializada - Evita JOIN pesado em cada query
- Criar tabela
-
Pré-computar Entity→Document Mapping
CREATE TABLE doc_entity_map (
doc_id BIGINT,
entity_id INT,
entity_name VARCHAR(255),
entity_type VARCHAR(50),
INDEX idx_doc (doc_id),
INDEX idx_entity (entity_id)
); -
Limitar Candidatos no Primeiro Estágio
- Buscar top 100 por vetorial
- Aplicar scoring composto apenas nesses 100
🔬 Análise Comparativa com a Indústria
RAG Pipelines Modernos — Estado da Arte
| Técnica | Descrição | Usado Por | ArboreoLab |
|---|---|---|---|
| Dense Retrieval | Busca vetorial com embeddings | OpenAI, Anthropic | ✅ VEC_DISTANCE_COSINE |
| Sparse Retrieval | BM25/TF-IDF | Elasticsearch, Lucene | ✅ FULLTEXT + TF-IDF |
| Hybrid Search | Combinação Dense+Sparse | Pinecone, Weaviate | ✅ Parâmetro alpha |
| Re-ranking | Cross-encoder no top-N | Cohere, Google | 🔄 Proposto: Multi-Signal |
| Query Expansion | Expandir query com sinônimos | Azure Cognitive | ⏳ Futuro |
| Entity Linking | Ligar entidades a KB | Google, Meta | ✅ clio_entidades + inFileID |
| Contextual Chunking | Chunks sobrepostos | LangChain, LlamaIndex | 🔄 clio_ocr_segments |
Comparação Detalhada
Pinecone (Hybrid Search)
Abordagem: Índices separados Dense + Sparse, depois merge + rerank.
1. Busca Dense (top 40) → Similaridade semântica
2. Busca Sparse (top 40) → Match lexical BM25
3. Merge + Deduplicate → 52 resultados únicos
4. Rerank (bge-reranker-v2-m3) → top 10 finais
Nosso Diferencial: Temos entidades estruturadas como sinal adicional que Pinecone não tem nativamente. Isso nos permite scoring baseado em relacionamentos semânticos reais (não apenas similaridade de texto).
Weaviate (Hybrid Search)
Abordagem: Parâmetro alpha para balancear vector vs BM25.
response = collection.query.hybrid(
query="food",
alpha=0.5, # 0=keyword, 1=vector
fusion_type=HybridFusion.RELATIVE_SCORE
)
Nosso Diferencial: Nossa estratégia Multi-Signal vai além do alpha binário. Combinamos:
- Vetor do documento completo
- Entidades extraídas via NER
- Relacionamentos entre entidades
- Full-text match
Cohere (Rerank API)
Abordagem: Cross-encoder neural para re-ranking de candidatos.
results = co.rerank(
model="rerank-v4.0-pro",
query=query,
documents=docs,
top_n=10
)
Nosso Diferencial:
- Não dependemos de API externa (custo por query)
- Usamos conhecimento estruturado (entidades + relacionamentos)
- Latência menor (sem chamada de rede adicional)
Avaliação: Onde Estamos vs Onde Queremos Chegar
| Aspecto | Indústria (State-of-Art) | ArboreoLab Atual | Proposta Multi-Signal |
|---|---|---|---|
| Fusão de sinais | 2 (dense + sparse) | 2 | 4 (doc + ent + rel + text) |
| Re-ranking | Cross-encoder neural | Não implementado | Score composto local |
| Entity-aware | Raro (apenas Google/Meta) | ✅ Estruturado | ✅ Como sinal de peso |
| Threshold dinâmico | Autocut (Weaviate) | Fixo | Ajustável pelo usuário |
| Feedback loop | Implícito (clicks) | Não implementado | Thumbs up/down |
Conclusão da Análise
Nossa estratégia Multi-Signal está ALINHADA com a indústria e tem um diferencial competitivo:
- ✅ Hybrid Search — Implementado (alpha parameter)
- ✅ Entity Linking — Diferencial (145k entidades estruturadas)
- ✅ Contextual Retrieval — OCR completo + segmentos
- 🔄 Re-ranking — Proposta substitui cross-encoder por scoring composto
- ⏳ Feedback Loop — A implementar
Vantagem ArboreoLab: Conhecimento estruturado de domínio (entidades de arte, museologia, história) que sistemas genéricos não possuem.
Ferramentas de Referência
| Ferramenta | Abordagem | Link |
|---|---|---|
| Pinecone | Separate Dense+Sparse + Rerank | docs |
| Weaviate | Alpha-balanced Hybrid + Autocut | docs |
| Cohere | Neural Rerank API (100+ idiomas) | docs |
| LangChain | Ensemble Retriever | docs |
| LlamaIndex | Node Postprocessors | docs |
| MariaDB | VEC_DISTANCE_* nativo | docs |
Papers Relevantes
- "ColBERT: Efficient and Effective Passage Search" — Late interaction para re-ranking
- "REALM: Retrieval-Augmented Language Model" — Entidades como âncoras
- "RAG: Retrieval-Augmented Generation" — O paper original (Lewis et al., 2020)
- "Dense Passage Retrieval for Open-Domain QA" — DPR da Meta
🎯 Decisões Estratégicas do Diretor
Documentado em 08/01/2026
1. Trade-off Precision vs Recall
Decisão: ✅ PRECISÃO com controle ajustável pelo usuário
Implementação:
- Default: resultados precisos (threshold mais alto)
- Interface permite usuário aumentar limite quando quiser ver mais resultados
- Menos resultados, porém mais relevantes > muitos resultados vagos
2. Migrar ocr_vector1/2/3 ou Novo Esquema?
Decisão: ✅ NOVO ESQUEMA — Layout-Aware Segments
Implementação:
- Não migrar vetores fragmentados existentes
- Implementar novos segmentos com marcadores de seção (Heading, Parágrafo, Lista, Tabela)
- Usar Docling/LayoutParser para chunks inteligentes
- Metadados preservados: posição, hierarquia, contexto
3. Pesos Fixos ou Configuráveis?
Decisão: ✅ HÍBRIDO — Default otimizado + Controle exposto
Implementação:
- Backend define pesos otimizados baseados em análise
- Frontend expõe slider para ajuste fino pelo usuário avançado
- Presets: "Preciso" (entity-heavy), "Exploratório" (recall alto)
4. Feedback Loop
Decisão: ✅ IMPLEMENTAR com critérios para cache
Implementação:
- Interface: Thumbs up/down + opção "útil?"
- Backend registra: query → resultado → feedback
- Critérios para cache:
- Query idêntica com feedback positivo múltiplo → cachear
- Query + entidade detectada → criar boost permanente
- Métricas: taxa de sucesso por tipo de query
🛠️ Próximos Passos de Implementação
Fase 1: Multi-Signal Scoring (Prioridade Alta)
Objetivo: Melhorar ranking sem mudar schema
-
Modificar
tenant_search_engine.py:- Implementar
calculate_multi_signal_score() - Adicionar entity detection na query
- Criar fallback para
content_markdown
- Implementar
-
Criar endpoint
/api/fregerag/search_v2:- Novo endpoint com Multi-Signal
- Manter
/searchlegado para rollback - Feature flag para migração gradual
-
Atualizar
useFregeRAG.ts:- Usar novo endpoint
- Adicionar controle de threshold no UI
Estimativa: 2-3 dias de desenvolvimento
Fase 2: Layout-Aware Segments (Prioridade Média)
Objetivo: Chunks inteligentes para melhor contexto
-
Definir novo schema:
ALTER TABLE clio_ocr_segments ADD COLUMN
segment_type ENUM('heading', 'paragraph', 'list', 'table', 'caption'),
parent_heading_id INT,
position_in_doc INT,
semantic_context VARCHAR(500) -
Pipeline de reprocessamento:
- Integrar Docling para detecção de layout
- Gerar chunks com contexto (heading + content)
- Vetorizar com contexto expandido
-
Atualizar busca:
- Score considera segment_type
- Headings recebem boost
- Relacionamento parent→child para contexto
Estimativa: 1-2 semanas
Fase 3: Feedback Loop (Prioridade Média)
Objetivo: Aprendizado contínuo baseado em uso
-
Criar tabela
search_feedback:CREATE TABLE search_feedback (
id INT AUTO_INCREMENT PRIMARY KEY,
query_hash CHAR(64),
query_text TEXT,
result_doc_id INT,
result_rank INT,
feedback ENUM('positive', 'negative'),
user_id INT,
created_at TIMESTAMP
); -
API de feedback:
-
POST /api/fregerag/feedback - Rate limiting por usuário
- Agregação diária
-
-
Cache inteligente:
- Query com ≥3 feedbacks positivos → cache 24h
- Query + entidade → boost permanente na entity
Estimativa: 1 semana
📊 Métricas de Sucesso
| Métrica | Baseline (Atual) | Target (Pós-implementação) |
|---|---|---|
| Precision@10 | ~40% (estimado) | ≥70% |
| Query com 0 resultados úteis | ~25% | ≤10% |
| Tempo médio de resposta | 200-400ms | ≤500ms (com re-ranking) |
| Feedback positivo | N/A | ≥60% |
🔐 Considerações de Segurança
- Feedback vinculado a
usuario_idpara evitar spam - Rate limiting no endpoint de busca (prevenir scraping)
- Query sanitization para prevenir injection via embedding
- Isolamento de tenant mantido em todas as queries
Relatório preparado pelo Engenheiro Sênior ArboreoLab
Validado pelo Diretor Técnico em 08/01/2026
Próxima revisão: após implementação da Fase 1