Motor de Busca RAG - Instruções de Implementação
Documento de Especificação Técnica
Data: 05 de Janeiro de 2026
Última Atualização: 08 de Janeiro de 2026
Autor: Engenheiro Sênior ArboreoLab
Executor: Técnico Sênior de Software
Componente Alvo: Armazenamento.vue
📋 Sumário Executivo
Este documento contém as instruções para o Motor de Busca RAG (Retrieval Augmented Generation) no componente Armazenamento.vue.
✅ Status de Implementação: COMPLETO
| Pilar | Tecnologia | Status | Observação |
|---|---|---|---|
| 1. Servidor Python | FastAPI + Gunicorn (4 workers) | ✅ COMPLETO | Gerenciado por PM2 |
| 2. Armazenamento Vetorial | MariaDB VECTOR + TF-IDF em memória | ✅ COMPLETO | 145.478 entidades, 2.527 segmentos |
| 3. Interface de Busca | Parâmetro Alpha no Frontend | ✅ COMPLETO | Toggle text/hybrid/semantic implementado |
| 4. Embeddings API | Endpoint /api/embeddings | ✅ COMPLETO | Geração em batch para OCR callback |
| 5. Connection Pooling | 8 conexões/tenant | ✅ COMPLETO | Configurável via env vars |
🚀 Servidor em Produção
# FastAPI: http://127.0.0.1:8768
# Proxy Node.js: http://127.0.0.1:3000/api/fregerag/*
# Health Check
curl http://127.0.0.1:8768/health
# Testar Busca
curl -X POST http://127.0.0.1:8768/api/fregerag/search \
-H "Content-Type: application/json" \
-d '{"query": "televisões do brasil", "alpha": 0.5}'
# Testar Embeddings API
curl -X POST http://127.0.0.1:8768/api/embeddings \
-H "Content-Type: application/json" \
-d '{"texts": ["texto 1", "texto 2"], "batch_size": 128}'
# Status do Endpoint de Embeddings
curl http://127.0.0.1:8768/api/embeddings/status
# Gerenciamento do Servidor (produção)
# O Motor RAG é gerenciado via PM2 (porta 8768) usando o ecosystem em /home/arboreolab/Clio/node/ecosystem.config.js
# Iniciar/reiniciar apenas o Motor RAG
pm2 start /home/arboreolab/Clio/node/ecosystem.config.js --only motor-rag
pm2 restart motor-rag
# Ver logs
pm2 logs motor-rag
📊 Métricas de Produção
| Métrica | Valor |
|---|---|
| Entidades carregadas | 145.478 |
| Segmentos OCR carregados | 2.527 |
| Termos TF-IDF (entidades) | 70.289 |
| Termos TF-IDF (segmentos) | 2.820 |
| Modelo de embeddings | paraphrase-multilingual-mpnet-base-v2 (768d) |
| Tempo de resposta (busca) | menos de 500ms após warm-up |
| Tempo de resposta (embeddings) | ~100ms para batch de 10 textos |
🔌 API de Embeddings para OCR
Fluxo de Integração com Worker OCR
Worker macOS (OCR)
│
▼ callback com blocks[]
workerRoute.js
│
├─► Coletar textos: [full_text, block1.text, block2.text, ...]
│
▼ POST /api/embeddings
Motor RAG (FastAPI)
│
├─► EmbeddingsService.encode(texts, batch_size=128)
│
▼ retorna embeddings[[768], [768], ...]
workerRoute.js
│
├─► INSERT clio_ocr (ocr_vector = docVector)
└─► INSERT clio_ocr_segments (segment_vector = segmentEmbeddings[i])
Endpoint: POST /api/embeddings
Gera embeddings para uma lista de textos em batch.
Request:
{
"texts": ["Documento sobre arte moderna", "Relatório técnico 2024"],
"batch_size": 128
}
Response:
{
"success": true,
"embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]],
"count": 2,
"dimension": 768,
"processing_time_ms": 108.1
}
Endpoint: GET /api/embeddings/status
Verifica se o serviço de embeddings está pronto.
Response:
{
"ready": true,
"model_loaded": true,
"model_name": "sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
"dimension": 768
}
Integração em workerRoute.js
O workerRoute.js foi modificado para:
- Chamar
/api/embeddingscom todos os textos (documento + segmentos) - Usar embeddings reais em vez de vetores zerados
- Fallback para zeros se Motor RAG estiver indisponível
// Função adicionada em workerRoute.js
async function generateEmbeddings(texts, batchSize = 128) {
const MOTOR_RAG_URL = process.env.MOTOR_RAG_URL || 'http://127.0.0.1:8768';
try {
const response = await axios.post(`${MOTOR_RAG_URL}/api/embeddings`, {
texts, batch_size: batchSize
}, { timeout: 60000 });
return response.data.embeddings;
} catch (error) {
// Fallback: retornar vetores de zeros
return texts.map(() => Array(768).fill(0));
}
}
🏗️ Arquitetura Alvo
┌─────────────────────────────────────────────────────────────────────┐
│ Armazenamento.vue │
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │
│ │ Search Input │───►│ searchType │───►│ Results View │ │
│ │ + Alpha Slider │ │ text│semantic│ │ │ - Files │ │
│ └─────────────────┘ │ hybrid │ │ - Entities │ │
│ └─────────────────┘ │ - Report │ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ useBuscaArquivos.ts / useFregeRAG.ts │
│ │
│ if (searchType === 'text') → POST /api/busca/arquivos │
│ if (searchType === 'semantic') → POST /api/fregerag/search │
│ if (searchType === 'hybrid') → Merge results (alpha-weighted) │
└─────────────────────────────────────────────────────────────────────┘
│
┌────────────────────┴────────────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ buscaArquivosRoute.js │ │ FastAPI Server :8768 │
│ - Full-Text Search │ │ - Gunicorn + 4 workers │
│ - MySQL MATCH AGAINST │ │ - Hot-loaded model │
└──────────────────────────┘ └──────────────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ MariaDB (ClioVector) │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ clio_ocr │ │ clio_entidades │ │
│ │ - content_markdown│ │ - semantic_vector │ │
│ │ - ocr_vector │ │ - entity_name_text│ │
│ │ - FULLTEXT index │ │ - VECTOR index │ │
│ └────────────────────┘ └────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
🔌 Connection Pooling & Performance
📚 Documentação Completa: Para configuração detalhada do MariaDB, consulte: Configuração do MariaDB para ArboreoLab
Pool de Conexões Multi-Tenant
O motor-rag utiliza pools dinâmicos por tenant para gerenciar conexões com o MariaDB:
# motorRag/services/tenant_manager.py
pool_size = int(os.environ.get('FREGERAG_TENANT_POOL_SIZE', '8'))
wait_seconds = float(os.environ.get('FREGERAG_POOL_WAIT_SECONDS', '2.0'))
pool = mysql.connector.pooling.MySQLConnectionPool(
pool_name=f"{tenant}_pool",
pool_size=pool_size,
...
)
Variáveis de Ambiente (PM2)
| Variável | Default | Descrição |
|---|---|---|
FREGERAG_TENANT_POOL_SIZE | 8 | Conexões por tenant |
FREGERAG_POOL_WAIT_SECONDS | 2.0 | Timeout para obter conexão do pool |
Configuradas em /home/arboreolab/Clio/node/ecosystem.config.js:
{
name: 'motor-rag',
// ...
env: {
FREGERAG_TENANT_POOL_SIZE: '8',
FREGERAG_POOL_WAIT_SECONDS: '2.0',
}
}
Tratamento de Pool Exhausted
Se todas as conexões estiverem em uso:
- Retry com wait: Aguarda
FREGERAG_POOL_WAIT_SECONDSantes de falhar - HTTP 503: Retorna status de backpressure (não 500)
- Log de warning: Registra para monitoramento
# motorRag/services/tenant_search_engine.py
try:
conn = engine.tenant_manager.get_connection(tenant)
except PoolError:
time.sleep(wait_seconds) # Retry
conn = engine.tenant_manager.get_connection(tenant) # Se falhar, propaga
Benchmark de Performance (07/01/2026)
| Concorrência | p50 | p90 | RPS |
|---|---|---|---|
| 1 | 22.9s | 29.1s | 0.04 |
| 2 | 27.5s | 38.4s | 0.06 |
Gargalo: Latência dominada por busca vetorial no MariaDB (disk I/O).
Mitigação: Buffer pool de 18GB cacheia vetores após warmup.
📁 Estrutura de Arquivos
Pasta Principal: /estudos/1_funcionais/fregeRAG_v1/motorRag/
IMPORTANTE: Todos os scripts Python do Motor RAG ficam nesta pasta.
Não alterar scripts existentes em outras pastas.
motorRag/ # [NOVA] Pasta principal do Motor RAG
├── README.md # Documentação do módulo
├── requirements.txt # Dependências Python específicas
├── __init__.py # Package marker
│
├── config/ # Configurações
│ ├── __init__.py
│ └── settings.py # Carregamento de config.ini
│
├── server/ # Servidor FastAPI
│ ├── __init__.py
│ ├── main.py # Entry point (Gunicorn + uvicorn workers)
│ ├── routes/ # Endpoints da API
│ │ ├── __init__.py
│ │ ├── search.py # POST /api/fregerag/search (retorna 503 se pool esgotado)
│ │ ├── health.py # GET /health
│ │ ├── reindex.py # POST /api/fregerag/reindex
│ │ └── embeddings.py # POST /api/embeddings (geração em batch)
│ └── middleware/ # Middlewares
│ ├── __init__.py
│ └── cors.py # Configuração CORS
│
├── services/ # Lógica de negócio
│ ├── __init__.py
│ ├── embeddings.py # SentenceTransformer wrapper
│ ├── search_engine.py # Busca híbrida (TF-IDF + Semântica)
│ ├── tenant_manager.py # [07/01] Pool de conexões multi-tenant
│ ├── tenant_search_engine.py # [07/01] Busca com retry para pool exhausted
│ ├── tfidf_engine.py # Indexação TF-IDF
│ └── database.py # Conexão MariaDB VECTOR
│
├── models/ # Modelos Pydantic
│ ├── __init__.py
│ ├── requests.py # Request models
│ └── responses.py # Response models
│
└── utils/ # Utilitários
├── __init__.py
└── logger.py # Configuração de logging
Arquivos Frontend (Clio)
Clio/iface-frontend-vuejs/src/
├── composables/
│ └── useFregeRAG.ts # [CRIAR] Composable RAG
└── views/Armazenamento/
└── Armazenamento.vue # [MODIFICAR] Adicionar toggle + alpha
Arquivos Backend Node.js (Clio)
Clio/node/backend/routes/
├── fregeRAGRoute.js # [MODIFICAR] HTTP ao invés de spawn
└── workerRoute.js # [MODIFICADO] Integração com /api/embeddings
Clio/node/
└── ecosystem.config.js # [MODIFICAR] Adicionar processo fregerag
Arquivos de Banco de Dados
Clio/
└── ddl_motor_rag.sql # [CRIAR] DDL para backfill vectors
Referência de Configuração
estudos/1_funcionais/fregeRAG_v1/
└── config/
└── config.ini # [EXISTENTE] Credenciais DB (usado por motorRag)
🔧 FASE 1: FastAPI Persistente (Semana 1)
1.1 Estrutura Modular do Servidor
Arquitetura implementada: O servidor FastAPI está organizado em módulos dentro de
/estudos/1_funcionais/fregeRAG_v1/motorRag/
Entry Point: motorRag/server/main.py
#!/usr/bin/env python3
"""
Motor RAG - FastAPI Persistent Server
Entry point com lifespan para carregamento no startup
"""
import uvicorn
from contextlib import asynccontextmanager
from fastapi import FastAPI
from motorRag.config.settings import get_settings
from motorRag.services.embeddings import EmbeddingsService
from motorRag.services.search_engine import SearchEngine
from motorRag.services.database import DatabaseService
from motorRag.server.routes import search, health, reindex
from motorRag.server.middleware.cors import configure_cors
from motorRag.utils.logger import get_logger
logger = get_logger(__name__)
settings = get_settings()
# Estado global compartilhado
state = {
"embeddings_service": None,
"search_engine": None,
"database_service": None,
"is_ready": False
}
# ============================================================================
# LIFESPAN: CARREGAMENTO NO STARTUP
# ============================================================================
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Carrega modelo e serviços UMA VEZ no startup."""
logger.info("🚀 Iniciando Motor RAG...")
try:
# 1. Inicializar serviços
state["embeddings_service"] = EmbeddingsService()
state["database_service"] = DatabaseService(settings.database)
state["search_engine"] = SearchEngine(
embeddings=state["embeddings_service"],
database=state["database_service"]
)
# 2. Pré-carregar modelo (elimina cold start)
await state["embeddings_service"].initialize()
state["is_ready"] = True
logger.info("🎉 Motor RAG pronto!")
except Exception as e:
logger.error(f"❌ Erro no startup: {e}")
raise
yield
logger.info("👋 Encerrando Motor RAG...")
if state["database_service"]:
await state["database_service"].close()
def create_app() -> FastAPI:
"""Factory function para criar a aplicação."""
app = FastAPI(
title="Motor RAG API",
description="Motor de Busca RAG para ArboreoLab Clio",
version="1.0.0",
lifespan=lifespan
)
# Middleware
configure_cors(app)
# Injetar estado nos routers
search.set_state(state)
health.set_state(state)
reindex.set_state(state)
# Registrar rotas
app.include_router(health.router)
app.include_router(search.router)
app.include_router(reindex.router)
return app
app = create_app()
if __name__ == "__main__":
uvicorn.run(
"motorRag.server.main:app",
host=settings.server.host,
port=settings.server.port,
reload=settings.server.reload
)
Modelos Pydantic: motorRag/models/requests.py
"""Modelos de Request para a API"""
from typing import Optional
from pydantic import BaseModel, Field
class SearchRequest(BaseModel):
"""Request para busca semântica/híbrida"""
query: str = Field(..., min_length=2, description="Termo de busca")
alpha: float = Field(0.5, ge=0.0, le=1.0, description="Peso TF-IDF vs Semântico")
threshold: float = Field(0.2, ge=0.0, le=1.0, description="Score mínimo")
top_k: int = Field(20, ge=1, le=100, description="Máximo de resultados")
search_type: str = Field("hybrid", description="semantic | tfidf | hybrid")
file_type: Optional[str] = Field(None, description="pdf | image | all")
generate_report: bool = Field(False, description="Gerar relatório LLM")
class ReindexRequest(BaseModel):
"""Request para reindexação"""
table: str = Field(..., description="clio_ocr | clio_entidades | all")
force: bool = Field(False, description="Forçar reindexação completa")
Modelos Pydantic: motorRag/models/responses.py
"""Modelos de Response para a API"""
from typing import Optional, List, Dict
from pydantic import BaseModel
class EntityResult(BaseModel):
"""Resultado de entidade encontrada"""
id: str
name: str
type: str
score: float
relations: List[Dict[str, str]] = []
documents: List[str] = []
class DocumentResult(BaseModel):
"""Resultado de documento encontrado"""
workflow_id: int
name: str
score: float
snippet: str
web_view_link: Optional[str] = None
entidades: List[Dict[str, str]] = []
class SearchResponse(BaseModel):
"""Response da busca"""
success: bool
query: str
search_type: str
alpha: float
entities: List[EntityResult] = []
documents: List[DocumentResult] = []
total_entities: int = 0
total_documents: int = 0
report: Optional[str] = None
processing_time_ms: float = 0
class HealthResponse(BaseModel):
"""Response do health check"""
status: str
model_loaded: bool
index_loaded: bool
version: str = "1.0.0"
Serviço de Busca: motorRag/services/search_engine.py
"""
Search Engine - Orquestra busca híbrida (TF-IDF + Semântica)
"""
from typing import List, Dict, Any
from motorRag.services.embeddings import EmbeddingsService
from motorRag.services.database import DatabaseService
from motorRag.services.tfidf_engine import TFIDFEngine
from motorRag.utils.logger import get_logger
logger = get_logger(__name__)
class SearchEngine:
def __init__(
self,
embeddings: EmbeddingsService,
database: DatabaseService
):
self.embeddings = embeddings
self.database = database
self.tfidf_entities = TFIDFEngine()
self.tfidf_documents = TFIDFEngine()
async def search_entities(
self,
query: str,
alpha: float = 0.5,
threshold: float = 0.2,
top_k: int = 20
) -> List[Dict[str, Any]]:
"""
Busca híbrida em entidades.
score = (alpha × TF-IDF) + ((1-alpha) × semântico)
"""
# 1. Gerar embedding da query
query_embedding = self.embeddings.encode(query)
# 2. Buscar via MariaDB VECTOR
results = await self.database.hybrid_search(
table="clio_entidades",
query_text=query,
query_embedding=query_embedding,
alpha=alpha,
limit=top_k
)
# 3. Filtrar por threshold
return [r for r in results if r['score'] >= threshold]
async def search_documents(
self,
query: str,
alpha: float = 0.5,
threshold: float = 0.2,
top_k: int = 20
) -> List[Dict[str, Any]]:
"""Busca híbrida em documentos OCR."""
query_embedding = self.embeddings.encode(query)
results = await self.database.hybrid_search(
table="clio_ocr",
query_text=query,
query_embedding=query_embedding,
alpha=alpha,
limit=top_k
)
return [r for r in results if r['score'] >= threshold]
Rota de Busca: motorRag/server/routes/search.py
"""POST /api/fregerag/search"""
import time
from fastapi import APIRouter, HTTPException
from motorRag.models.requests import SearchRequest
from motorRag.models.responses import SearchResponse, EntityResult, DocumentResult
router = APIRouter()
_state = {}
def set_state(state: dict):
global _state
_state = state
@router.post("/api/fregerag/search", response_model=SearchResponse)
async def search(request: SearchRequest):
"""Busca híbrida com alpha configurável."""
if not _state.get("is_ready"):
raise HTTPException(status_code=503, detail="Server still loading")
start_time = time.time()
search_engine = _state["search_engine"]
try:
# Buscar entidades
entities_raw = await search_engine.search_entities(
query=request.query,
alpha=request.alpha,
threshold=request.threshold,
top_k=request.top_k
)
entities = [
EntityResult(
id=str(e['id']),
name=e['entity_name'],
type=e.get('isA', 'UNKNOWN'),
score=round(e['score'] * 100, 2),
relations=e.get('relations', []),
documents=e.get('inFileID', [])
)
for e in entities_raw
]
# Buscar documentos
docs_raw = await search_engine.search_documents(
query=request.query,
alpha=request.alpha,
threshold=request.threshold,
top_k=request.top_k
)
documents = [
DocumentResult(
workflow_id=d['workflow_id'],
name=d['name'],
score=round(d['score'] * 100, 2),
snippet=d.get('snippet', ''),
web_view_link=d.get('webViewLink')
)
for d in docs_raw
]
processing_time = (time.time() - start_time) * 1000
return SearchResponse(
success=True,
query=request.query,
search_type=request.search_type,
alpha=request.alpha,
entities=entities,
documents=documents,
total_entities=len(entities),
total_documents=len(documents),
processing_time_ms=round(processing_time, 2)
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
1.2 Configurar PM2
Arquivo: Clio/node/ecosystem.config.js (adicionar ao existente)
// Adicionar ao array de apps existente:
{
name: 'motor-rag',
script: '/home/arboreolab/estudos/.venv/bin/uvicorn',
args: 'motorRag.server.main:app --host 127.0.0.1 --port 8768',
cwd: '/home/arboreolab/estudos/1_funcionais/fregeRAG_v1',
interpreter: 'none',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '2G',
env: {
PYTHONUNBUFFERED: '1',
TOKENIZERS_PARALLELISM: 'true',
PYTHONPATH: '/home/arboreolab/estudos/1_funcionais/fregeRAG_v1'
}
}
1.3 Comandos de Deploy
# 1. Instalar dependências Python
cd /home/arboreolab/estudos
source .venv/bin/activate
pip install -r /home/arboreolab/estudos/1_funcionais/fregeRAG_v1/motorRag/requirements.txt
# 2. Testar servidor manualmente
cd /home/arboreolab/estudos/1_funcionais/fregeRAG_v1
python -m motorRag.server.main
# 3. Verificar health
curl http://localhost:8768/health
# 4. Testar busca
curl -X POST http://localhost:8768/api/fregerag/search \
-H "Content-Type: application/json" \
-d '{"query": "Di Cavalcanti", "alpha": 0.5}'
# 5. Adicionar ao PM2
pm2 start ecosystem.config.js --only motor-rag
pm2 save
1.4 Modificar fregeRAGRoute.js
Arquivo: Clio/node/backend/routes/fregeRAGRoute.js
Substituir spawn por HTTP:
// ANTES (spawn por request - 8-14s cold start)
// const pythonProcess = spawn(PYTHON_PATH, args);
// DEPOIS (HTTP para FastAPI - menos de 1s)
const axios = require('axios');
const FREGERAG_URL = process.env.FREGERAG_URL || 'http://127.0.0.1:8768';
router.post('/search', verifyJwt, async (req, res) => {
try {
const { query, alpha = 0.5, top_k = 20, search_type = 'hybrid' } = req.body;
const response = await axios.post(`${FREGERAG_URL}/api/fregerag/search`, {
query,
alpha,
top_k,
search_type
}, {
timeout: 60000 // 60s é suficiente agora
});
res.json(response.data);
} catch (error) {
console.error('FregeRAG Error:', error.message);
// Fallback: tentar spawn se FastAPI estiver down
if (error.code === 'ECONNREFUSED') {
return res.status(503).json({
success: false,
message: 'FregeRAG service unavailable',
fallback: 'Use Full-Text search'
});
}
res.status(error.response?.status || 500).json({
success: false,
message: error.message
});
}
});
🔧 FASE 2: MariaDB VECTOR (Semana 2)
2.1 DDL para Índices Vetoriais
Arquivo: Clio/ddl_motor_rag.sql
-- ============================================================================
-- DDL: Motor de Busca RAG - Índices Vetoriais
-- ============================================================================
-- Autor: Engenheiro Sênior ArboreoLab
-- Data: 05 de Janeiro de 2026
-- Banco: ClioVector (projeto específico)
-- ============================================================================
-- 1. Verificar se coluna ocr_vector existe em clio_ocr
SET @col_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'clio_ocr'
AND COLUMN_NAME = 'ocr_vector'
);
-- Adicionar coluna se não existir (768 dimensões para paraphrase-multilingual-mpnet)
SET @sql = IF(@col_exists = 0,
'ALTER TABLE `clio_ocr` ADD COLUMN `ocr_vector` VECTOR(768) NULL AFTER `content_markdown`',
'SELECT "Column ocr_vector already exists"'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2. Criar índice vetorial para busca por similaridade
CREATE INDEX IF NOT EXISTS `idx_ocr_vec` ON `clio_ocr` (`ocr_vector`) USING VECTOR;
-- 3. Coluna para versão do modelo de embedding (para re-geração futura)
SET @col_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'clio_ocr'
AND COLUMN_NAME = 'embedding_model_version'
);
SET @sql = IF(@col_exists = 0,
'ALTER TABLE `clio_ocr` ADD COLUMN `embedding_model_version` VARCHAR(100) NULL AFTER `ocr_vector`',
'SELECT "Column embedding_model_version already exists"'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 4. Mesmo para clio_entidades
SET @col_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'clio_entidades'
AND COLUMN_NAME = 'semantic_vector'
);
SET @sql = IF(@col_exists = 0,
'ALTER TABLE `clio_entidades` ADD COLUMN `semantic_vector` VECTOR(768) NULL',
'SELECT "Column semantic_vector already exists"'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
CREATE INDEX IF NOT EXISTS `idx_ent_vec` ON `clio_entidades` (`semantic_vector`) USING VECTOR;
-- ============================================================================
-- QUERIES DE EXEMPLO PARA BUSCA HÍBRIDA
-- ============================================================================
-- Busca semântica pura (usar em Python após gerar embedding da query)
-- SELECT
-- id, name, content_markdown,
-- VEC_DISTANCE(ocr_vector, VEC_FROM_TEXT(@query_embedding_json)) AS distance
-- FROM clio_ocr
-- WHERE ocr_vector IS NOT NULL
-- ORDER BY distance ASC
-- LIMIT 20;
-- Busca híbrida (Full-Text + Semântica)
-- SELECT
-- id, name,
-- (
-- (@alpha * COALESCE(MATCH(content_markdown) AGAINST(@query IN NATURAL LANGUAGE MODE), 0)) +
-- ((1 - @alpha) * (1 - VEC_DISTANCE(ocr_vector, VEC_FROM_TEXT(@query_embedding_json))))
-- ) AS hybrid_score
-- FROM clio_ocr
-- WHERE content_markdown IS NOT NULL
-- ORDER BY hybrid_score DESC
-- LIMIT 50;
2.2 Script de Backfill
Arquivo: estudos/1_funcionais/fregeRAG_v1/migracoesdedb/backfill_mariadb_vectors.py
#!/usr/bin/env python3
"""
Backfill de vetores para MariaDB VECTOR
Migra de arquivos .npy para colunas vetoriais no banco
"""
import json
import mysql.connector
from mysql.connector import pooling
from sentence_transformers import SentenceTransformer
from tqdm import tqdm
import configparser
from pathlib import Path
# Carregar configuração
config = configparser.ConfigParser()
config.read(Path(__file__).parent.parent / 'config' / 'config.ini')
DB_CONFIG = {
'host': config.get('database', 'host'),
'user': config.get('database', 'user'),
'password': config.get('database', 'password'),
'database': config.get('database', 'database'),
'pool_size': 5
}
MODEL_NAME = "sentence-transformers/paraphrase-multilingual-mpnet-base-v2"
BATCH_SIZE = 100
def backfill_ocr_vectors():
"""Gera e salva embeddings para clio_ocr"""
print("🚀 Iniciando backfill de vetores OCR...")
# Carregar modelo
print("📦 Carregando modelo...")
model = SentenceTransformer(MODEL_NAME)
# Conectar ao banco
pool = pooling.MySQLConnectionPool(**DB_CONFIG)
conn = pool.get_connection()
cursor = conn.cursor(dictionary=True)
try:
# Contar registros sem vetor
cursor.execute("""
SELECT COUNT(*) as total
FROM clio_ocr
WHERE content_markdown IS NOT NULL
AND content_markdown != ''
AND (ocr_vector IS NULL OR embedding_model_version != %s)
""", (MODEL_NAME,))
total = cursor.fetchone()['total']
print(f"📊 {total} registros para processar")
if total == 0:
print("✅ Nenhum registro pendente")
return
# Processar em batches
offset = 0
processed = 0
with tqdm(total=total, desc="Gerando embeddings") as pbar:
while offset < total:
cursor.execute("""
SELECT id, content_markdown
FROM clio_ocr
WHERE content_markdown IS NOT NULL
AND content_markdown != ''
AND (ocr_vector IS NULL OR embedding_model_version != %s)
LIMIT %s OFFSET %s
""", (MODEL_NAME, BATCH_SIZE, offset))
rows = cursor.fetchall()
if not rows:
break
# Gerar embeddings em batch
texts = [row['content_markdown'][:8000] for row in rows] # Limitar tamanho
embeddings = model.encode(texts, show_progress_bar=False)
# Atualizar banco
for row, embedding in zip(rows, embeddings):
embedding_json = json.dumps(embedding.tolist())
cursor.execute("""
UPDATE clio_ocr
SET ocr_vector = VEC_FROM_TEXT(%s),
embedding_model_version = %s
WHERE id = %s
""", (embedding_json, MODEL_NAME, row['id']))
conn.commit()
processed += len(rows)
offset += BATCH_SIZE
pbar.update(len(rows))
print(f"✅ Backfill concluído: {processed} registros")
finally:
cursor.close()
conn.close()
def backfill_entity_vectors():
"""Gera e salva embeddings para clio_entidades"""
print("🚀 Iniciando backfill de vetores de entidades...")
# Similar ao acima, adaptado para clio_entidades
# TODO: Implementar
if __name__ == '__main__':
backfill_ocr_vectors()
backfill_entity_vectors()
🔧 FASE 3: Frontend com Alpha (Semana 3)
3.1 Criar Composable useFregeRAG.ts
Arquivo: Clio/iface-frontend-vuejs/src/composables/useFregeRAG.ts
/**
* useFregeRAG - Composable para busca semântica/híbrida
*
* Integra com FastAPI server para busca RAG com parâmetro alpha configurável.
*/
import { ref, computed } from 'vue'
import axiosInstance from '@/api/axios'
// ============================================================================
// TIPOS
// ============================================================================
export interface EntityResult {
id: string
name: string
type: string
score: number
relations: Array<{ relation: string; target: string; targetType: string }>
documents: string[]
}
export interface DocumentResult {
workflow_id: number
name: string
score: number
snippet: string
web_view_link: string | null
entidades: Array<{ nome: string; tipo: string }>
}
export interface RAGSearchOptions {
alpha?: number // 0.0-1.0, default 0.5
threshold?: number // Score mínimo, default 0.2
top_k?: number // Máximo resultados, default 20
search_type?: 'semantic' | 'tfidf' | 'hybrid'
file_type?: 'pdf' | 'image' | 'all'
generate_report?: boolean
}
export interface RAGSearchResponse {
success: boolean
query: string
search_type: string
alpha: number
entities: EntityResult[]
documents: DocumentResult[]
total_entities: number
total_documents: number
report: string | null
processing_time_ms: number
}
export type AlphaPreset = 'semantic' | 'balanced' | 'keyword'
// ============================================================================
// COMPOSABLE
// ============================================================================
export function useFregeRAG() {
// Estado
const entities = ref<EntityResult[]>([])
const documents = ref<DocumentResult[]>([])
const report = ref<string | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
const lastQuery = ref('')
const processingTime = ref(0)
// Parâmetros configuráveis
const alpha = ref(0.5)
const alphaPreset = ref<AlphaPreset>('balanced')
const searchType = ref<'semantic' | 'tfidf' | 'hybrid'>('hybrid')
// Presets de alpha
const ALPHA_PRESETS: Record<AlphaPreset, { alpha: number; label: string; description: string }> = {
semantic: {
alpha: 0.2,
label: '🧠 Conceitual',
description: 'Encontra sinônimos e conceitos relacionados'
},
balanced: {
alpha: 0.5,
label: '⚖️ Balanceado',
description: 'Equilíbrio entre termos e significado'
},
keyword: {
alpha: 0.8,
label: '🔤 Palavras',
description: 'Prioriza correspondência exata de termos'
}
}
// Computed
const hasResults = computed(() => entities.value.length > 0 || documents.value.length > 0)
const currentPreset = computed(() => ALPHA_PRESETS[alphaPreset.value])
const alphaLabel = computed(() => {
const semantic = Math.round((1 - alpha.value) * 100)
const tfidf = Math.round(alpha.value * 100)
return `${semantic}% semântico / ${tfidf}% textual`
})
// Métodos
const setAlphaPreset = (preset: AlphaPreset) => {
alphaPreset.value = preset
alpha.value = ALPHA_PRESETS[preset].alpha
// Persistir preferência
localStorage.setItem('fregerag_alpha_preset', preset)
}
const setCustomAlpha = (value: number) => {
alpha.value = value
alphaPreset.value = 'balanced' // Volta para "custom" implicitamente
}
const search = async (query: string, options: RAGSearchOptions = {}): Promise<RAGSearchResponse | null> => {
if (!query || query.trim().length < 2) {
error.value = 'Query deve ter pelo menos 2 caracteres'
return null
}
isLoading.value = true
error.value = null
lastQuery.value = query
try {
const response = await axiosInstance.post<RAGSearchResponse>('/api/fregerag/search', {
query: query.trim(),
alpha: options.alpha ?? alpha.value,
threshold: options.threshold ?? 0.2,
top_k: options.top_k ?? 20,
search_type: options.search_type ?? searchType.value,
file_type: options.file_type,
generate_report: options.generate_report ?? false
})
if (response.data.success) {
entities.value = response.data.entities
documents.value = response.data.documents
report.value = response.data.report
processingTime.value = response.data.processing_time_ms
return response.data
} else {
throw new Error('Search failed')
}
} catch (err: any) {
console.error('FregeRAG search error:', err)
if (err.response?.status === 503) {
error.value = 'Serviço de busca semântica indisponível. Tente Full-Text.'
} else {
error.value = err.message || 'Erro na busca semântica'
}
return null
} finally {
isLoading.value = false
}
}
const clearResults = () => {
entities.value = []
documents.value = []
report.value = null
error.value = null
lastQuery.value = ''
}
// Inicialização: restaurar preferência salva
const savedPreset = localStorage.getItem('fregerag_alpha_preset') as AlphaPreset | null
if (savedPreset && ALPHA_PRESETS[savedPreset]) {
setAlphaPreset(savedPreset)
}
return {
// Estado
entities,
documents,
report,
isLoading,
error,
lastQuery,
processingTime,
// Parâmetros
alpha,
alphaPreset,
searchType,
// Computed
hasResults,
currentPreset,
alphaLabel,
ALPHA_PRESETS,
// Métodos
search,
clearResults,
setAlphaPreset,
setCustomAlpha
}
}
3.2 Modificar Armazenamento.vue
Adicionar toggle de tipo de busca e presets de alpha na barra de busca existente.
Modificações no template (após linha ~250, seção de controles):
<!-- Adicionar após "Modo de Busca" dropdown -->
<!-- Tipo de Busca: Text vs RAG -->
<div class="btn-group btn-group-sm ms-2" role="group" aria-label="Tipo de busca">
<button
type="button"
class="btn"
:class="tipoRag === 'text' ? 'btn-outline-secondary active' : 'btn-outline-secondary'"
@click="setTipoRag('text')"
title="Busca por palavras-chave (Full-Text)"
>
<i class="bi bi-type me-1"></i>Texto
</button>
<button
type="button"
class="btn"
:class="tipoRag === 'hybrid' ? 'btn-primary active' : 'btn-outline-primary'"
@click="setTipoRag('hybrid')"
title="Busca híbrida (texto + semântica)"
>
<i class="bi bi-stars me-1"></i>Híbrido
</button>
<button
type="button"
class="btn"
:class="tipoRag === 'semantic' ? 'btn-info active' : 'btn-outline-info'"
@click="setTipoRag('semantic')"
title="Busca por conceitos (RAG)"
>
<i class="bi bi-diagram-3 me-1"></i>Semântico
</button>
</div>
<!-- Presets de Alpha (visível quando não é 'text') -->
<div v-if="tipoRag !== 'text'" class="btn-group btn-group-sm ms-2" role="group">
<button
v-for="(preset, key) in ragPresets"
:key="key"
type="button"
class="btn"
:class="alphaPreset === key ? 'btn-warning' : 'btn-outline-warning'"
@click="setAlphaPreset(key)"
:title="preset.description"
>
{{ preset.label }}
</button>
</div>
<!-- Alpha Slider (opcional, toggle) -->
<button
v-if="tipoRag !== 'text'"
class="btn btn-sm btn-link text-muted ms-1"
@click="showAlphaSlider = !showAlphaSlider"
title="Ajuste fino do peso semântico"
>
<i class="bi bi-sliders2"></i>
</button>
<!-- Slider expandido -->
<div v-if="showAlphaSlider && tipoRag !== 'text'" class="alpha-slider-container mt-2 p-2 bg-light rounded">
<div class="d-flex align-items-center gap-2">
<span class="small text-muted">🧠</span>
<input
type="range"
class="form-range flex-grow-1"
v-model.number="alphaValue"
min="0"
max="1"
step="0.1"
@change="setCustomAlpha(alphaValue)"
>
<span class="small text-muted">🔤</span>
</div>
<div class="text-center small text-muted">
{{ Math.round((1 - alphaValue) * 100) }}% semântico / {{ Math.round(alphaValue * 100) }}% textual
</div>
</div>
Modificações no script setup:
// Adicionar imports
import { useFregeRAG } from '@/composables/useFregeRAG'
// Após outros composables
const {
entities: ragEntities,
documents: ragDocuments,
isLoading: isRagLoading,
error: ragError,
alpha: alphaValue,
alphaPreset,
ALPHA_PRESETS: ragPresets,
search: ragSearch,
clearResults: clearRagResults,
setAlphaPreset,
setCustomAlpha
} = useFregeRAG()
// Estado do tipo de busca
const tipoRag = ref<'text' | 'semantic' | 'hybrid'>('text')
const showAlphaSlider = ref(false)
// Método para alternar tipo
const setTipoRag = (tipo: 'text' | 'semantic' | 'hybrid') => {
tipoRag.value = tipo
localStorage.setItem('armazenamento_tipo_rag', tipo)
// Re-executar busca se houver query
if (searchQuery.value.trim().length >= 2) {
handleSearch()
}
}
// Modificar handleSearch para suportar RAG
const handleSearch = async () => {
if (searchDebounceTimer.value) {
clearTimeout(searchDebounceTimer.value)
}
if (searchQuery.value.trim().length >= 2) {
searchDebounceTimer.value = setTimeout(async () => {
viewMode.value = 'search'
if (tipoRag.value === 'text') {
// Busca Full-Text existente
await buscar(searchQuery.value)
} else {
// Busca RAG (semantic ou hybrid)
await ragSearch(searchQuery.value, {
search_type: tipoRag.value
})
}
}, 500)
} else if (searchQuery.value.trim().length === 0) {
viewMode.value = 'files'
limparResultados()
clearRagResults()
}
}
// Restaurar preferência ao montar
onMounted(() => {
const savedTipo = localStorage.getItem('armazenamento_tipo_rag') as typeof tipoRag.value
if (savedTipo) {
tipoRag.value = savedTipo
}
})
🔧 Manutenção de Entidades DESCRICAO
Visão Geral (Implementado em 08/01/2026)
Entidades com relationshipType="DESCRICAO" são consolidadas automaticamente via UPSERT no MariaDB.
Quando um novo parecer menciona uma entidade já existente, o inFileID é adicionado ao registro existente
em vez de criar um novo registro duplicado.
Estrutura Implementada
| Componente | Localização | Descrição |
|---|---|---|
| UNIQUE KEY | clio_entidades.idx_unique_descricao | Constraint para UPSERT |
| Coluna Gerada | clio_entidades.unique_key_descricao | CONCAT(entity_name, '|', isA) |
| UPSERT Python | enviar_db/entidadesV2.py | INSERT ... ON DUPLICATE KEY UPDATE |
| Migração SQL | migracoesdedb/002_upsert_descricao.sql | Schema com generated columns |
⚠️ Manutenção Periódica Recomendada
O mecanismo UPSERT usa CONCAT_WS para agregar inFileIDs. Reprocessamentos do mesmo documento
podem gerar IDs duplicados dentro do campo CSV. Para limpeza periódica:
# Localização do script
cd /home/arboreolab/estudos/1_funcionais/fregeRAG_v1/enviar_db/
# 1. Simular consolidação (dry-run)
python consolidar_entidades_descricao.py --dry-run
# 2. Executar consolidação real
python consolidar_entidades_descricao.py
# 3. Para outro tenant específico
python consolidar_entidades_descricao.py --database GeopoliticasVector
Frequência Recomendada: Mensalmente ou após reprocessamentos em massa.
Arquivos de Referência
enviar_db/entidadesV2.py- Contém docstring com instruções detalhadasenviar_db/consolidar_entidades_descricao.py- Script de consolidaçãogerar_entidades/gerarEntespagina.py- Pipeline que usa entidadesV2.py
✅ Checklist de Validação
Fase 1 - FastAPI ✅ COMPLETO
- Servidor inicia sem erros (
python -m motorRag.server.main) - Health check retorna status
healthy(curl http://localhost:8768/health) - Busca retorna resultados em menos de 1s (após warm-up)
- Servidor rodando via PM2 (com resurrect no boot via systemd
pm2-root) - Node.js consegue chamar FastAPI via HTTP (proxy em
/api/fregerag/*)
Fase 2 - MariaDB VECTOR ✅ COMPLETO
- Dados carregados do banco: 145.478 entidades, 2.527 segmentos
- TF-IDF construído em memória: 70.289 termos (entidades) + 2.820 termos (segmentos)
- Modelo de embeddings carregado: paraphrase-multilingual-mpnet-base-v2 (768d)
- Busca híbrida (TF-IDF + Semântica) funcionando
- Schema ClioVector sincronizado com GeopoliticasVector
Fase 3 - Frontend ✅ COMPLETO
- Toggle entre Text/Hybrid/Semantic funciona
- Presets de alpha alteram comportamento da busca
- Slider de alpha permite ajuste fino
- Preferências são persistidas em localStorage
- Resultados RAG são exibidos corretamente (entidades + documentos)
- Composable
useFregeRAG.tsimplementado - Integração com
Armazenamento.vuecompleta
📞 Pontos de Contato
Decisões Arquiteturais: Consultar Engenheiro Sênior
Implementação: Executar conforme este documento
Dúvidas sobre TypeScript/Vue: Verificar padrões existentes no projeto
Comando de Ativação do Técnico: "Atuar como Técnico Sênior ArboreoLab."