Pular para o conteúdo principal

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:

  1. Entidades Relacionadas — Funcionando bem ✅
  2. 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):

  1. "Walter foi diretor do MAC e promoveu arte contemporânea"98% relevante
  2. "Zanini dirigia o MAC como ninguém na época"95% relevante
  3. "O Museu de Arte Contemporânea tinha Zanini na direção"92% relevante

📊 Estado Atual do Sistema

Dados Disponíveis (GeopoliticasVector)

RecursoQuantidadeCoberturaStatus
clio_ocr (documentos)16.431100% têm ocr_vector✅ Completo
content_markdown16.10198% dos docs✅ Texto integral
clio_ocr_segments13.969~2% dos docs (369)🔄 Em expansão
clio_entidades145.478100% têm semantic_vector✅ Completo
clio_entidades_vector0Não migrado⏳ Pendente migração
clio_ocr_vector0Não migrado⏳ Pendente migração
Relacionamentos~31.756 docsVia inFileID✅ Disponível

Dados em ClioVector (Modelo/Referência - Pendente Migração)

RecursoQuantidadeDescrição
clio_entidades_vector20.359Embeddings alternativos de entidades
clio_ocr_vector16.024Vetores 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

TipoQuantidade% Total
PESSOA67.85646.6%
ORGANIZACAO34.20823.5%
LOCAL23.57016.2%
EVENTO19.72113.5%
Outros1230.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)

CampoUsoPotencial
contentJSON com metadados do processoBaixo (estruturado)
content_markdownTexto puro transcritoAlto (busca full-text)
ocr_vectorEmbedding do documento completoAlto (busca semântica)

2. Vetores Fragmentados (clio_ocr_vector)

CampoDescriçãoPotencial
ocr_vector11/3 inicial do textoMédio (introdução/cabeçalho)
ocr_vector21/3 central do textoAlto (corpo do assunto)
ocr_vector31/3 final do textoMé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)

CampoDescriçãoPotencial
text_contentTexto do bloco (~38 chars média)Baixo (fragmentado)
segment_vectorEmbedding do blocoBaixo (sem contexto)
bbox_*Posição no layoutMédio (ordenação visual)
reading_orderSequência de leituraAlto (reconstrução)

Status: Apenas 369/16.431 docs (2%) têm segmentos.

4. Entidades e Relacionamentos (clio_entidades)

CampoDescriçãoPotencial
entity_name_textNome da entidadeAlto (busca textual)
isA_textTipo (PESSOA, ORGANIZACAO...)Alto (filtro semântico)
semantic_vectorEmbedding da entidadeAlto (busca semântica)
inFileIDDocumento(s) onde apareceCrítico (ponte doc↔entidade)
relatedTo_textEntidade relacionadaAlto (grafo)
relationshipTypeForma do relacionamentoAlto (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:

SinalDescriçãoPeso Sugerido
S_docSimilaridade vetorial do documento completo0.35
S_entQtd/score de entidades da query no documento0.30
S_relPresença de relacionamentos entre entidades0.15
S_textMatch full-text no content_markdown0.20
P_noisePenalidade 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âmetroValor SugeridoDescrição
min_score0.25Score mínimo para inclusão
high_confidence0.70Score para "muito relevante"
vec_threshold0.45Distância máxima vetorial
entity_boost1.2xMultiplicador se 3+ entidades match

⚡ Impacto em Performance

Custo por Query (Estimado)

OperaçãoTempo AtualApós Otimização
Embedding da query50ms50ms
Busca vetorial docs3-5s1-2s (com cache quente)
Busca vetorial entidades200ms200ms
Full-text search50ms50ms
Scoring compostoN/A100ms
Total4-6s1.5-2.5s

Otimizações Necessárias

  1. Cache de Entidades por Documento

    • Criar tabela doc_entity_cache ou view materializada
    • Evita JOIN pesado em cada query
  2. 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)
    );
  3. 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écnicaDescriçãoUsado PorArboreoLab
Dense RetrievalBusca vetorial com embeddingsOpenAI, AnthropicVEC_DISTANCE_COSINE
Sparse RetrievalBM25/TF-IDFElasticsearch, LuceneFULLTEXT + TF-IDF
Hybrid SearchCombinação Dense+SparsePinecone, Weaviate✅ Parâmetro alpha
Re-rankingCross-encoder no top-NCohere, Google🔄 Proposto: Multi-Signal
Query ExpansionExpandir query com sinônimosAzure Cognitive⏳ Futuro
Entity LinkingLigar entidades a KBGoogle, Metaclio_entidades + inFileID
Contextual ChunkingChunks sobrepostosLangChain, LlamaIndex🔄 clio_ocr_segments

Comparação Detalhada

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).

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

AspectoIndústria (State-of-Art)ArboreoLab AtualProposta Multi-Signal
Fusão de sinais2 (dense + sparse)24 (doc + ent + rel + text)
Re-rankingCross-encoder neuralNão implementadoScore composto local
Entity-awareRaro (apenas Google/Meta)✅ Estruturado✅ Como sinal de peso
Threshold dinâmicoAutocut (Weaviate)FixoAjustável pelo usuário
Feedback loopImplícito (clicks)Não implementadoThumbs up/down

Conclusão da Análise

Nossa estratégia Multi-Signal está ALINHADA com a indústria e tem um diferencial competitivo:

  1. Hybrid Search — Implementado (alpha parameter)
  2. Entity Linking — Diferencial (145k entidades estruturadas)
  3. Contextual Retrieval — OCR completo + segmentos
  4. 🔄 Re-ranking — Proposta substitui cross-encoder por scoring composto
  5. 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

FerramentaAbordagemLink
PineconeSeparate Dense+Sparse + Rerankdocs
WeaviateAlpha-balanced Hybrid + Autocutdocs
CohereNeural Rerank API (100+ idiomas)docs
LangChainEnsemble Retrieverdocs
LlamaIndexNode Postprocessorsdocs
MariaDBVEC_DISTANCE_* nativodocs

Papers Relevantes

  1. "ColBERT: Efficient and Effective Passage Search" — Late interaction para re-ranking
  2. "REALM: Retrieval-Augmented Language Model" — Entidades como âncoras
  3. "RAG: Retrieval-Augmented Generation" — O paper original (Lewis et al., 2020)
  4. "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

  1. Modificar tenant_search_engine.py:

    • Implementar calculate_multi_signal_score()
    • Adicionar entity detection na query
    • Criar fallback para content_markdown
  2. Criar endpoint /api/fregerag/search_v2:

    • Novo endpoint com Multi-Signal
    • Manter /search legado para rollback
    • Feature flag para migração gradual
  3. 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

  1. 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)
  2. Pipeline de reprocessamento:

    • Integrar Docling para detecção de layout
    • Gerar chunks com contexto (heading + content)
    • Vetorizar com contexto expandido
  3. 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

  1. 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
    );
  2. API de feedback:

    • POST /api/fregerag/feedback
    • Rate limiting por usuário
    • Agregação diária
  3. Cache inteligente:

    • Query com ≥3 feedbacks positivos → cache 24h
    • Query + entidade → boost permanente na entity

Estimativa: 1 semana


📊 Métricas de Sucesso

MétricaBaseline (Atual)Target (Pós-implementação)
Precision@10~40% (estimado)≥70%
Query com 0 resultados úteis~25%≤10%
Tempo médio de resposta200-400ms≤500ms (com re-ranking)
Feedback positivoN/A≥60%

🔐 Considerações de Segurança

  • Feedback vinculado a usuario_id para 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