Como usar o LlamaIndex para indexar e consultar documentos privados

1. Introdução ao LlamaIndex e contextualização no ecossistema RAG

O LlamaIndex (anteriormente GPT Index) é um framework especializado na construção de sistemas de Retrieval-Augmented Generation (RAG) para documentos privados. Diferentemente do LangChain, que oferece uma abordagem mais genérica para orquestração de LLMs, o LlamaIndex foca exclusivamente na indexação eficiente e consulta de dados estruturados e não estruturados.

Principais diferenciais:
- Pipeline otimizado: do parsing ao armazenamento vetorial em poucas linhas de código
- Múltiplas estratégias de indexação: VectorStoreIndex, SummaryIndex, KeywordTableIndex
- Controle granular: metadados, filtros e estratégias de chunking personalizadas

Cenários típicos incluem: contratos confidenciais, bases de conhecimento internas, documentação técnica proprietária e dados regulatórios.

2. Configuração do ambiente e instalação do LlamaIndex

Para garantir privacidade total, utilizaremos modelos locais. Instale as dependências essenciais:

pip install llama-index-core llama-index-embeddings-huggingface llama-index-llms-ollama

Para processamento de PDFs e DOCXs, adicione:

pip install pypdf python-docx markdown

Configuração inicial do ambiente:

import logging
import os
from llama_index.core import Settings

# Logs para depuração
logging.basicConfig(level=logging.INFO)

# Configuração para execução 100% local
Settings.llm = None  # Será configurado com Ollama posteriormente
Settings.embed_model = "local:BAAI/bge-small-en-v1.5"

3. Carregamento e parsing de documentos privados

O LlamaIndex suporta múltiplos formatos nativamente. Exemplo de carregamento de um contrato em PDF:

from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter

# Carregar documentos de uma pasta segura
reader = SimpleDirectoryReader(
    input_dir="./documentos_privados",
    recursive=True,
    required_exts=[".pdf", ".docx", ".txt", ".md"]
)

documents = reader.load_data()

# Estratégia de chunking: 512 tokens com sobreposição de 128
parser = SentenceSplitter(
    chunk_size=512,
    chunk_overlap=128,
    separator=" ",
    paragraph_separator="\n\n"
)

nodes = parser.get_nodes_from_documents(documents)
print(f"Total de chunks criados: {len(nodes)}")

Tratamento de metadados sensíveis:

for node in nodes:
    # Remover metadados sensíveis antes da indexação
    if "email" in node.metadata:
        del node.metadata["email"]
    if "cpf" in node.metadata:
        del node.metadata["cpf"]

4. Criação de embeddings e indexação dos documentos

Seleção de modelo de embedding local (HuggingFace) para máxima privacidade:

from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.core.storage.docstore import SimpleDocumentStore

# Modelo de embedding local (sem envio de dados para nuvem)
embed_model = HuggingFaceEmbedding(
    model_name="BAAI/bge-small-en-v1.5",
    device="cpu"  # ou "cuda" se tiver GPU
)

# Criação do índice vetorial
index = VectorStoreIndex(
    nodes=nodes,
    embed_model=embed_model,
    show_progress=True
)

# Persistência do índice em disco
index.storage_context.persist(persist_dir="./indice_privado")
print("Índice salvo em ./indice_privado")

Estrutura dos índices disponíveis:
- VectorStoreIndex: melhor para similaridade semântica
- SummaryIndex: ideal para sumarização de documentos longos
- KeywordTableIndex: útil para buscas por palavras-chave exatas

5. Implementação do mecanismo de consulta (Query Engine)

Configuração do query engine com parâmetros otimizados:

from llama_index.core import QueryBundle
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.retrievers import VectorIndexRetriever

# Configuração do retriever
retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=5,  # Recupera os 5 chunks mais similares
    filters=None,  # Pode adicionar filtros de metadados
)

# Query engine básico
query_engine = RetrieverQueryEngine.from_args(
    retriever=retriever,
    response_mode="compact",  # "tree_summarize", "refine", "compact"
    verbose=True
)

# Exemplo de consulta
response = query_engine.query("Quais são as cláusulas de rescisão do contrato?")
print(response)

Filtros de metadados para consultas específicas:

from llama_index.core.vector_stores import MetadataFilters, ExactMatchFilter

filters = MetadataFilters(
    filters=[
        ExactMatchFilter(key="categoria", value="contrato"),
        ExactMatchFilter(key="ano", value="2024")
    ]
)

retriever_filtrado = VectorIndexRetriever(
    index=index,
    similarity_top_k=3,
    filters=filters
)

6. Garantia de privacidade e segurança dos dados

Execução 100% local com Ollama para LLM:

from llama_index.llms.ollama import Ollama

# LLM local via Ollama (sem envio de dados para internet)
llm = Ollama(
    model="llama3.1:8b",  # ou "mistral", "gemma:2b"
    temperature=0.1,
    request_timeout=60.0
)

# Atualizar o query engine com LLM local
query_engine = RetrieverQueryEngine.from_args(
    retriever=retriever,
    llm=llm,
    response_mode="compact"
)

Sanitização adicional de documentos:

import re

def sanitizar_documento(texto):
    # Remover padrões de dados sensíveis
    texto = re.sub(r'\b\d{3}\.\d{3}\.\d{3}-\d{2}\b', '[CPF REMOVIDO]', texto)  # CPF
    texto = re.sub(r'\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b', '[EMAIL REMOVIDO]', texto)  # Email
    return texto

# Aplicar antes da indexação
for node in nodes:
    node.text = sanitizar_documento(node.text)

Controle de acesso por níveis:

# Adicionar metadados de permissão
for node in nodes:
    node.metadata["nivel_acesso"] = "confidencial"  # ou "restrito", "publico"

7. Otimização e boas práticas para documentos privados

Compressão de índice para grandes volumes:

from llama_index.core.indices.postprocessor import SentenceTransformerRerank

# Re-ranking para melhorar precisão
rerank = SentenceTransformerRerank(
    model="cross-encoder/ms-marco-MiniLM-L-2-v2",
    top_n=3
)

query_engine = RetrieverQueryEngine.from_args(
    retriever=retriever,
    node_postprocessors=[rerank],
    llm=llm
)

Caching de consultas frequentes:

from functools import lru_cache

@lru_cache(maxsize=100)
def consulta_cacheada(pergunta: str) -> str:
    return str(query_engine.query(pergunta))

# Uso
resposta = consulta_cacheada("Qual o valor do contrato?")

8. Exemplo prático completo: do documento à consulta funcional

Código completo para indexar e consultar um contrato confidencial:

import os
from llama_index.core import (
    SimpleDirectoryReader,
    VectorStoreIndex,
    Settings
)
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.ollama import Ollama

# 1. Configuração
Settings.embed_model = HuggingFaceEmbedding(
    model_name="BAAI/bge-small-en-v1.5"
)
Settings.llm = Ollama(model="llama3.1:8b", temperature=0.1)

# 2. Carregamento
reader = SimpleDirectoryReader(input_dir="./contratos")
documents = reader.load_data()

# 3. Parsing
parser = SentenceSplitter(chunk_size=512, chunk_overlap=128)
nodes = parser.get_nodes_from_documents(documents)

# 4. Indexação
index = VectorStoreIndex(nodes=nodes)
index.storage_context.persist(persist_dir="./indice_contratos")

# 5. Consulta
query_engine = index.as_query_engine(
    similarity_top_k=5,
    response_mode="compact"
)

# Teste
pergunta = "Qual a data de vigência do contrato?"
resposta = query_engine.query(pergunta)
print(f"Pergunta: {pergunta}")
print(f"Resposta: {resposta}")

Para integrar com Streamlit:

# app.py (Streamlit)
import streamlit as st
from llama_index.core import StorageContext, load_index_from_storage

st.title("Consulta de Documentos Privados")

# Carregar índice persistido
storage_context = StorageContext.from_defaults(persist_dir="./indice_contratos")
index = load_index_from_storage(storage_context)

pergunta = st.text_input("Digite sua pergunta sobre os contratos:")
if pergunta:
    query_engine = index.as_query_engine()
    resposta = query_engine.query(pergunta)
    st.write(resposta.response)

Referências