Storage Híbrido: Kuzu + LMDB + HNSW para IA que Lembra
Três bancos de dados para uma única aplicação parece exagero até você perceber que cada um resolve um problema completamente diferente. Preciso de queries de grafo ("quais entidades se relacionam com Python?"), acesso key-value ultrarrápido ("qual a distribuição de probabilidade desta crença específica?"), e busca por similaridade vetorial ("qual mundo é mais parecido com esta mensagem?"). Nenhum banco sozinho faz as três coisas bem. A alternativa é combinar três especializados atrás de uma fachada simples.
A arquitetura que montei usa Kuzu (grafo embeddable) para relacionamentos, LMDB (key-value memory-mapped) para distribuições de probabilidade, e HNSW via hnswlib (índice vetorial) para busca por similaridade. Cada camada faz o que faz melhor, e o código consumidor acessa tudo via uma única classe BeliefStore que esconde a complexidade.
Guia de tópicos:
- Por Que Três Bancos e Não Um
- Kuzu: Grafo Embeddable para Relacionamentos
- LMDB: Distribuições em Microsegundos
- HNSW: Busca Vetorial Sublinear
- A Fachada que Unifica Tudo
- Fallbacks: Quando uma Dependência Não Está Disponível
- Exemplo Prático em Python
- Considerações Finais
Por Que Três Bancos e Não Um
PostgreSQL com pgvector poderia fazer tudo? Tecnicamente sim. Mas com trade-offs enormes para o meu caso de uso. Queries de grafo em SQL são joins recursivos lentos. Acesso a blobs binários (distribuições serializadas) tem overhead de protocolo de rede. E busca vetorial em pgvector, embora funcional, não tem a performance de um índice HNSW dedicado para datasets de centenas a milhares de vetores.
A decisão foi pragmática: cada banco é o melhor na sua categoria para o volume de dados do projeto. Kuzu roda in-process sem servidor. LMDB é o key-value mais rápido que existe para leitura (memory-mapped, zero-copy). HNSW via hnswlib faz busca vetorial com latência de microsegundos. E todos rodam localmente sem infraestrutura externa.
Kuzu: Grafo Embeddable para Relacionamentos
Kuzu armazena a estrutura do grafo: quais mundos existem, quais crenças cada mundo tem (sujeito, verbo, objeto dominante, confiança), e quais arestas causais conectam crenças. É consultado quando preciso de visão relacional: "todos os mundos", "todas as crenças de um mundo", "arestas causais para propagação".
A vantagem sobre Neo4j é que roda embeddable (sem servidor separado, sem Docker, sem porta aberta). É uma biblioteca que você importa e usa. Para um assistente pessoal que roda na máquina do usuário, isso é essencial. A desvantagem é que não escala para bilhões de nós, mas para o meu caso nunca será problema.
LMDB: Distribuições em Microsegundos
LMDB armazena as distribuições de probabilidade de cada crença. A chave é "world_id:subject:verb" e o valor é a distribuição serializada com msgpack. O acesso é memory-mapped: leituras são literalmente um pointer dereference no mmap do OS, sem syscall, sem cópia de memória.
Configuração: 4GB de map_size (espaço virtual, não uso real), writemap=True (writes direto no mmap), map_async=True (sync assíncrono para throughput). Batch writes no flush: em vez de uma transação por tripla, todas as triplas dirty são escritas em uma única transação.
HNSW: Busca Vetorial Sublinear
O índice HNSW permite encontrar os mundos mais similares a um vetor de query em tempo O(log n). Para 100 mundos a diferença é pequena vs linear. Para 10.000, é a diferença entre 1ms e 100ms por busca.
O índice é persistido em disco e carregado na inicialização. Suporta inserção, remoção via tombstones, e compactação periódica. Vetores são normalizados antes de inserir para que inner product = cosine similarity.
A Fachada que Unifica Tudo
A classe BeliefStore inicializa os três backends e expõe como atributos: store.graph (Kuzu), store.dist (LMDB), store.ann (HNSW). O close() fecha os três na ordem correta: HNSW (salva índice), LMDB (sync final), Kuzu (fecha conexão). Nenhum dado perdido no shutdown.
Fallbacks: Quando uma Dependência Não Está Disponível
Nem todo ambiente tem Kuzu, LMDB e hnswlib instalados. O sistema detecta na inicialização quais estão disponíveis e usa fallbacks: dict in-memory para LMDB, lista linear para HNSW, dict para Kuzu. Funcionalidade idêntica, só performance degrada. Permite rodar em CI/CD ou containers mínimos sem dependências opcionais.
Exemplo Prático em Python
import numpy as np
import os
class BeliefStore:
"""Unified facade over three storage backends."""
def __init__(self, base_path="./belief_store"):
os.makedirs(base_path, exist_ok=True)
self.graph = GraphStore() # Relationships
self.dist = DistributionStore() # Probability distributions
self.ann = VectorIndex() # Similarity search
def close(self):
self.ann.save()
print("BeliefStore closed.")
class GraphStore:
"""In-memory graph (production would use Kuzu)."""
def __init__(self):
self._worlds = {}
self._beliefs = {}
def upsert_world(self, world_id, topic, access_count):
self._worlds[world_id] = {"topic": topic, "access_count": access_count}
def get_all_world_ids(self):
return list(self._worlds.keys())
def get_beliefs(self, world_id):
return self._beliefs.get(world_id, [])
def add_belief(self, world_id, s, v, o, confidence):
if world_id not in self._beliefs:
self._beliefs[world_id] = []
self._beliefs[world_id].append({"s": s, "v": v, "o": o, "c": confidence})
class DistributionStore:
"""In-memory key-value (production would use LMDB)."""
def __init__(self):
self._data = {}
def put(self, world_id, subject, verb, distribution):
key = f"{world_id}:{subject}:{verb}"
self._data[key] = distribution
def get(self, world_id, subject, verb):
return self._data.get(f"{world_id}:{subject}:{verb}")
def put_batch(self, items):
for world_id, subject, verb, dist in items:
self.put(world_id, subject, verb, dist)
class VectorIndex:
"""In-memory vector index (production would use hnswlib)."""
def __init__(self):
self._vectors = {}
def add(self, item_id, vector):
self._vectors[item_id] = np.array(vector, dtype=float)
def search(self, query, top_k=5):
query = np.array(query, dtype=float)
results = []
for item_id, vec in self._vectors.items():
n = np.linalg.norm(query) * np.linalg.norm(vec)
sim = float(np.dot(query, vec) / n) if n > 0 else 0
results.append((item_id, sim))
return sorted(results, key=lambda x: -x[1])[:top_k]
def save(self):
pass # Production: persist to disk
# Usage demo
store = BeliefStore()
# Graph layer: store world metadata
store.graph.upsert_world("w1", "Programação Python", access_count=15)
store.graph.add_belief("w1", "python", "tipo", "linguagem", 0.92)
# Distribution layer: store probability distribution
store.dist.put("w1", "python", "criador", {"guido": 0.88, "unknown": 0.12})
# Vector layer: store and search centroids
store.ann.add("w1", np.random.randn(384))
store.ann.add("w2", np.random.randn(384))
query = np.random.randn(384)
results = store.ann.search(query, top_k=2)
print("Nearest worlds:", [(wid, f"{sim:.3f}") for wid, sim in results])
# Retrieve distribution
dist = store.dist.get("w1", "python", "criador")
print(f"Python criador distribution: {dist}")
store.close()
Considerações Finais
Storage híbrido não é over-engineering quando cada camada resolve um problema diferente. A chave é manter a interface simples (uma fachada que esconde a complexidade) e ter fallbacks para ambientes sem todas as dependências. O código consumidor fica limpo, e cada banco opera no que faz melhor.
O padrão é replicável: identifique os padrões de acesso do seu sistema (grafo? key-value? vetorial? time-series?), escolha o melhor banco para cada, e unifique atrás de uma fachada. Polyglot persistence com interface simples.
Links indicativos:
- Kuzu: https://kuzudb.com/
- LMDB: https://www.symas.com/lmdb
- hnswlib: https://github.com/nmslib/hnswlib
- Polyglot Persistence: https://martinfowler.com/bliki/PolyglotPersistence.html