Armazenamentos Especializados
Um fã digita "tayler swift sp dezembro" na caixa de busca da BoxOffice. O marketing quer "ingressos vendidos por minuto, por cidade, nos últimos dois anos" toda manhã. A página do evento mostra um pôster de 2 MB para milhões de visitantes. O assistente de suporte precisa achar o parágrafo da política de reembolso que responde "posso ter meu dinheiro de volta se o show mudar de data?" — uma frase que quase não compartilha palavras com o texto da política.
Um banco de dados relacional consegue fazer uma versão fraca de cada uma dessas coisas, e muitas vezes esse é o começo certo. Mas cada uma tem um motor projetado para ela: um mecanismo de busca, um armazenamento de objetos, um data warehouse e um banco de dados vetorial. Os quatro guardam dados derivados (veja a visão geral): cópias do sistema de registro, remodeladas para um tipo de pergunta.
Como os dados derivados fluem
O sistema de registro continua nos bancos relacionais dos serviços. As mudanças saem para os armazenamentos especializados por captura de dados de mudança (change data capture, CDC): um conector lê o log do banco (o WAL do PostgreSQL, o binlog do MySQL) e publica cada insert, update e delete como um evento.
flowchart LR
DB[("catalog + order<br/>PostgreSQL")] -->|"WAL"| CDC["conector CDC<br/>(Debezium)"]
CDC --> K[["stream de eventos<br/>(Kafka)"]]
K --> IDX["indexador"] --> S[("índice de busca")]
K --> ELT["carregador"] --> W[("data warehouse")]
K --> EMB["worker de embeddings"] --> V[("vector DB")]
UP["upload do pôster"] --> O[("armazenamento de objetos")] Por que não fazer o serviço escrever no banco e no índice de busca diretamente (uma escrita dupla, dual write)? Porque as duas escritas podem falhar de forma independente ou chegar em ordens diferentes sob concorrência, deixando os armazenamentos permanentemente inconsistentes. O CDC (ou o transactional outbox, veja Padrões de mensageria) oferece uma única fonte ordenada de mudanças, e qualquer armazenamento derivado pode ser reconstruído reproduzindo-a.
Mecanismos de busca
Por que LIKE '%taylor%' não é busca
SELECT id, title FROM events
WHERE title ILIKE '%tayler%' AND title ILIKE '%dezembro%';
Ela varre a tabela inteira (um % no início anula os índices B-tree), não encontra nada para o erro de digitação "tayler", não sabe que "SP" significa "São Paulo", ignora acentos e plurais e devolve os resultados em ordem arbitrária. Os usuários esperam tolerância a erros de digitação, ranking por relevância, autocomplete por prefixo e facetas ("filtrar por cidade, data, gênero") em dezenas de milissegundos.
O índice invertido
Um mecanismo de busca constrói um índice invertido (inverted index): para cada termo, a lista (lista de postings) dos documentos que o contêm — o índice remissivo no fim de um livro1.
| Termo | Postings (ids de eventos) |
|---|---|
taylor | 42, 77 |
swift | 42 |
sao | 42, 51, 77 |
paulo | 42, 51, 77 |
stadium | 42, 51 |
Uma consulta faz a interseção ou a união de listas de postings, o que é rápido porque elas são ordenadas e comprimidas. Antes de ser indexado, o texto passa por um analisador:
- Tokenizar:
"Taylor Swift — São Paulo"→Taylor,Swift,São,Paulo. - Normalizar: minúsculas e remoção de acentos →
taylor,swift,sao,paulo. - Stemming (específico do idioma):
shows→show,ingressos→ingress. - Sinônimos:
sp↔sao paulo.
O mesmo analisador roda sobre a consulta, então "SÃO PAULO" e "sao paulo" casam com os mesmos termos. O casamento aproximado (fuzzy matching) aceita termos dentro de uma distância de edição (tayler → taylor, distância 1), e edge n-grams ou completion suggesters indexam prefixos (tay, tayl, …) para o autocomplete.
Ranking
Os documentos que casam recebem uma pontuação de relevância. O padrão no Elasticsearch, no OpenSearch e no Lucene é o BM252, que cresce com a frequência de um termo da consulta no documento (com retornos decrescentes), diminui para termos comuns a todos os documentos (show, ingresso) e normaliza pelo tamanho do documento. Os produtos então combinam esse valor com sinais de negócio: popularidade, proximidade da data, disponibilidade de ingressos.
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "tayler swift sp dezembro",
"fields": ["title^3", "lineup^2", "city", "description"],
"fuzziness": "AUTO"
}
},
"functions": [
{ "field_value_factor": { "field": "popularity", "modifier": "log1p" } }
]
}
},
"aggs": { "by_city": { "terms": { "field": "city.keyword" } } }
}
ALTER TABLE events ADD COLUMN search tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('portuguese', unaccent_immutable(title)), 'A') ||
setweight(to_tsvector('portuguese', unaccent_immutable(coalesce(description, ''))), 'B')
) STORED;
CREATE INDEX events_search_idx ON events USING GIN (search);
SELECT id, title, ts_rank(search, q) AS rank
FROM events, websearch_to_tsquery('portuguese', 'taylor swift dezembro') q
WHERE search @@ q
ORDER BY rank DESC
LIMIT 20;
-- unaccent_immutable: an IMMUTABLE wrapper around unaccent(), required in generated columns
Operando um cluster de busca
- Um índice é dividido em shards (fixados na criação, veja Particionamento) com réplicas para disponibilidade e vazão de leitura. Uma consulta se espalha para uma cópia de cada shard e combina os melhores resultados — scatter/gather.
- A busca é quase em tempo real: documentos novos ficam pesquisáveis após um refresh (1 segundo por padrão no Elasticsearch), e o pipeline de CDC acrescenta seu próprio atraso. O evento que a casa de shows acabou de criar pode levar alguns segundos para aparecer — aceitável para busca, não para inventário.
- O índice é derivado: quando o mapeamento ou o analisador muda, construa um índice novo a partir do stream ou de um snapshot e troque um alias para ele.
Experimente. O cenário search-autocomplete coloca um serviço de busca e um cache atrás do gateway. Rode-o e observe que fração das teclas digitadas o cache absorve (prefixos populares como "tay" se repetem muito); depois aumente a carga e veja qual componente satura primeiro quando o cache está frio.
O projeto completo — tries de prefixo com top-k, debouncing no cliente, cache por prefixo — está no estudo de caso de autocomplete de busca.
Armazenamento de objetos
O armazenamento de objetos (object storage) guarda blobs — imagens, vídeos, PDFs, backups, arquivos de data lake — como objetos imutáveis em buckets, endereçados por uma chave e acessados por HTTP (PUT, GET, DELETE, LIST)3. O espaço de nomes é plano: posters/42/hero.webp parece um caminho, mas não existem diretórios de verdade.
| Propriedade | Armazenamento de objetos | Armazenamento em bloco (um disco) | Banco de dados |
|---|---|---|---|
| Unidade | objeto inteiro (de bytes a terabytes) | blocos de 4 KiB | linhas |
| Acesso | API HTTP, de qualquer lugar | montado por uma máquina | linguagem de consulta |
| Atualização | substitui o objeto inteiro | no lugar | no lugar, transacional |
| Escala | praticamente ilimitada | um volume | um cluster |
| Custo por GB | o menor, com camadas frias | médio | o maior |
| Durabilidade | o Amazon S3 é projetado para 99,999999999% (11 noves) ao ano, guardando cópias em várias instalações | um volume; replique você mesmo | sua replicação e seus backups |
Desde 2020, o Amazon S3 oferece consistência forte de leitura após escrita para todas as operações, e as outras grandes nuvens também. O que o armazenamento de objetos não faz: anexar dados a um objeto, renomear de forma barata (renomear = copiar + apagar), travar (lock) ou consultar o conteúdo dos objetos (exceto por motores que leem arquivos, como um warehouse).
O padrão que mantém os blobs longe dos seus servidores é a URL pré-assinada (presigned URL): o serviço autoriza o upload, mas os bytes vão direto do navegador para o bucket.
sequenceDiagram
participant V as Admin da casa de shows (navegador)
participant C as serviço catalog
participant S as Armazenamento de objetos
participant W as Worker de miniaturas
V->>C: POST /events/42/poster (nome, tipo, tamanho)
C->>C: verifica permissão, escolhe a chave posters/42/hero.webp
C-->>V: URL PUT pré-assinada (válida por 5 min)
V->>S: PUT posters/42/hero.webp (2 MB)
S-->>V: 200 OK
S->>W: evento: objeto criado
W->>S: PUT posters/42/hero-400w.webp
Note over C,S: o banco guarda só a chave,<br/>a CDN serve as imagens import boto3
s3 = boto3.client("s3")
def poster_upload_url(event_id: int) -> str:
return s3.generate_presigned_url(
"put_object",
Params={"Bucket": "boxoffice-media",
"Key": f"posters/{event_id}/hero.webp",
"ContentType": "image/webp"},
ExpiresIn=300, # seconds
)
As leituras passam pela CDN (veja DNS & CDN) com tempos de cache longos e chaves versionadas (hero.v3.webp), de modo que o bucket vê uma fração mínima do tráfego. Regras de ciclo de vida movem objetos antigos para camadas mais baratas (acesso infrequente, arquivo) e apagam os temporários automaticamente; o upload multipart envia arquivos grandes em partes paralelas que podem ser reenviadas individualmente.
Amazon S3 (classes de armazenamento Standard, Intelligent-Tiering, Glacier), CloudFront como CDN.
Google Cloud Storage (Standard, Nearline, Coldline, Archive), Cloud CDN.
Azure Blob Storage (camadas Hot, Cool, Cold, Archive), Azure Front Door.
Servidores compatíveis com S3, como Ceph Object Gateway, SeaweedFS ou Garage — úteis em laboratórios e em ambientes on-premises; a API do S3 é o padrão de fato.
Data warehouses e lakehouses
OLTP vs OLAP
Os serviços rodam OLTP (online transaction processing): muitas leituras e escritas pequenas de linhas individuais, por chave, com transações. Os analistas rodam OLAP (online analytical processing): poucas consultas enormes que agregam milhões de linhas sobre poucas colunas. Eles querem coisas opostas do armazenamento4:
| OLTP (inventory, order) | OLAP (warehouse) | |
|---|---|---|
| Consulta típica | UPDATE seats … WHERE event_id = 42 AND seat_id = 'A-12' | SELECT city, date_trunc('minute', sold_at), count(*) … GROUP BY 1, 2 sobre 2 anos |
| Linhas tocadas | poucas, via índice | de milhões a bilhões, varridas |
| Colunas tocadas | a maior parte da linha | poucas entre muitas |
| Meta de latência | milissegundos | de segundos a minutos |
| Quem escreve | a aplicação, o tempo todo | cargas em lote ou em streaming |
| Layout | orientado a linhas | orientado a colunas |
| Dados | estado atual | histórico, muitas vezes só de inserção |
Por que colunas
Um armazenamento colunar (column store) guarda cada coluna em seus próprios arquivos. Uma consulta que lê 3 de 30 colunas lê cerca de um décimo dos bytes, e uma coluna de valores parecidos (city, status) comprime extremamente bem (codificação por dicionário e run-length)5. Com números ilustrativos para o clickstream da BoxOffice — 20 GB/dia por dois anos ≈ 15 TB brutos:
| Layout | Bytes lidos por uma consulta sobre 3 de 30 colunas |
|---|---|
| Armazenamento por linhas | ~15 TB (todas as linhas, todas as colunas) |
| Armazenamento colunar | ~1,5 TB (3 colunas de 30) |
| Colunar + compressão (suponha 5–10×) | ~150–300 GB |
| + poda de partições (só os últimos 30 dias) | alguns GB |
Essa diferença — somada à varredura em paralelo em muitas máquinas — é o motivo pelo qual um warehouse responde em segundos o que levaria horas no banco OLTP (enquanto prejudica o checkout).
Modelagem: o esquema estrela
Warehouses costumam ser modelados como um esquema estrela (star schema): uma tabela fato central de eventos (uma linha por ingresso vendido) com chaves estrangeiras para tabelas dimensão que os descrevem.
erDiagram
FACT_TICKET_SALES }o--|| DIM_EVENT : "event_key"
FACT_TICKET_SALES }o--|| DIM_VENUE : "venue_key"
FACT_TICKET_SALES }o--|| DIM_DATE : "date_key"
FACT_TICKET_SALES }o--|| DIM_CUSTOMER : "customer_key"
FACT_TICKET_SALES {
bigint ticket_id
timestamp sold_at
int price_cents
string channel
}
DIM_EVENT {
bigint event_key
string title
string genre
}
DIM_VENUE {
bigint venue_key
string city
string country
} SELECT v.city,
date_trunc('minute', f.sold_at) AS minute,
count(*) AS tickets,
sum(f.price_cents) / 100.0 AS revenue
FROM fact_ticket_sales f
JOIN dim_venue v ON v.venue_key = f.venue_key
WHERE f.sold_at >= '2027-03-01 10:00' AND f.sold_at < '2027-03-01 12:00'
GROUP BY 1, 2
ORDER BY 2, 1;
Colocando dados dentro: ETL, ELT, streaming
| Abordagem | Como | Atualidade | Ferramentas típicas |
|---|---|---|---|
| ETL (extract, transform, load) | transforma fora, carrega tabelas limpas | horas (lote) | Spark, jobs próprios, Airflow |
| ELT (extract, load, transform) | carrega os dados brutos e transforma dentro do warehouse com SQL | de horas a minutos | Fivetran / Airbyte + dbt |
| CDC em streaming | eventos de mudança anexados continuamente | de segundos a minutos | Debezium + Kafka, ingestão em streaming das nuvens |
Lakes e lakehouses
Um data lake guarda arquivos brutos — geralmente Parquet, um formato de arquivo colunar — no armazenamento de objetos, de forma barata e em qualquer formato. A fraqueza de um lake simples é que os arquivos não têm transações nem esquema. Formatos de tabela abertos — Apache Iceberg, Delta Lake, Apache Hudi — acrescentam um log de transações, evolução de esquema e time travel sobre esses arquivos, de modo que vários motores (Spark, Trino, Flink, DuckDB, warehouses das nuvens) consultam as mesmas tabelas. Essa combinação é o lakehouse6.
Amazon Redshift (warehouse), Athena (SQL sobre o S3), S3 Tables / Iceberg.
BigQuery (warehouse serverless, também sobre tabelas Iceberg no Cloud Storage).
Microsoft Fabric / Azure Synapse, Azure Databricks.
ClickHouse, DuckDB (in-process), Apache Spark, Trino; Snowflake e Databricks rodam nas três nuvens.
Bancos de dados vetoriais
Embeddings: significado como geometria
Um modelo de embeddings transforma um trecho de texto (ou uma imagem) em um vetor de números — tipicamente de centenas a alguns milhares de dimensões — de modo que textos com significado parecido fiquem próximos uns dos outros. "Posso ter meu dinheiro de volta se o show mudar de data?" e "Reembolso de eventos remarcados" compartilham poucas palavras, mas têm embeddings vizinhos. A proximidade costuma ser medida pela similaridade de cosseno:
Isso viabiliza a busca semântica e a etapa de recuperação do RAG (retrieval-augmented generation, geração aumentada por recuperação), em que o assistente da BoxOffice busca os trechos de política mais relevantes e os entrega ao modelo de linguagem (veja Sistemas de IA — RAG e memória).
flowchart LR
Q["pergunta do fã"] --> E["gera o embedding<br/>(modelo de embeddings)"]
E --> V[("vector DB<br/>top-k mais próximos")]
V --> RR["reranking<br/>(opcional)"]
RR --> L["prompt do LLM:<br/>pergunta + trechos"]
L --> A["resposta com fontes"] Busca exata vs aproximada
A resposta exata compara a consulta com todos os vetores: para 10 milhões de vetores de 1.024 dimensões, são cerca de 10 bilhões de multiplicações-e-somas por consulta — aceitável offline, lento demais para tráfego interativo. Bancos de dados vetoriais usam índices de vizinho mais próximo aproximado (approximate nearest neighbour, ANN) que olham uma pequena fração dos vetores e aceitam perder alguns vizinhos verdadeiros. A medida de qualidade é o recall@k: dos k vizinhos verdadeiros mais próximos, quantos o índice devolveu?
- IVF (inverted file): agrupa os vetores em clusters (por exemplo, com k-means); na consulta, varre só os
nprobeclusters mais próximos dela. - HNSW (hierarchical navigable small world)7: um grafo em camadas no qual cada vetor se liga aos seus vizinhos próximos; a busca começa na camada esparsa do topo e caminha gulosamente em direção à consulta, camada por camada. Ótimo equilíbrio entre recall e latência, ao custo de memória para o grafo e inserções mais lentas. Parâmetros:
m(ligações por nó),ef_constructioneef_search(a largura da busca — o botão de recall na hora da consulta). - Quantização (product quantization, escalar ou binária) comprime os vetores 4–32× para caber mais em memória, trocando um pouco de recall.
Um experimento pequeno e real torna o trade-off visível: 20.000 vetores sintéticos de 64 dimensões em torno de 200 tópicos, um índice IVF com um bucket por tópico e 60 consultas. Cada ponto é um valor de nprobe:
Comparar cerca de 8% dos vetores já devolve aproximadamente 9 dos 10 vizinhos verdadeiros; cada ponto extra de recall custa mais trabalho. Embeddings reais e o HNSW se comportam de forma diferente nos detalhes, mas o formato — uma curva ajustável de recall versus custo — é o mesmo, e é uma decisão de produto: no RAG, um reranker depois da etapa ANN perdoa um recall um pouco menor.
Onde guardar os vetores
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE kb_chunks (
id BIGSERIAL PRIMARY KEY,
doc_id TEXT NOT NULL, -- 'refund-policy'
content TEXT NOT NULL,
embedding vector(1024) NOT NULL
);
CREATE INDEX kb_chunks_embedding_idx
ON kb_chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
SET hnsw.ef_search = 40; -- recall knob at query time
SELECT doc_id, content
FROM kb_chunks
ORDER BY embedding <=> :question_embedding -- cosine distance
LIMIT 5;
| Opção | Boa quando | Cuidado com |
|---|---|---|
| pgvector no PostgreSQL existente | até milhões de vetores; você quer filtros, joins e transações com o resto dos dados | tempo de construção do índice e memória no primário; ajuste ef_search |
| kNN do mecanismo de busca (Elasticsearch, OpenSearch) | você já opera um; busca híbrida (BM25 + vetores) em uma única consulta | memória por shard |
| Vector DB dedicado (Qdrant, Weaviate, Milvus, Pinecone) | centenas de milhões de vetores, filtragem pesada, multi-tenant | mais um sistema para operar e manter sincronizado |
| Busca vetorial no Redis / Valkey | conjuntos pequenos e quentes com latência muito baixa | custo de memória |
Dimensionamento, como exemplo resolvido: 100 mil trechos × 1.024 dimensões × 4 bytes (float32) ≈ 400 MB de vetores brutos, mais o grafo HNSW (com m = 16, algumas centenas de bytes por vetor) — cabe com folga no banco do catalog. Com 100 milhões de vetores (≈ 400 GB brutos), quantização ou um cluster dedicado passam a valer a discussão.
No código do BoxOffice: pgvector no PostgreSQL que já temos (lab 12)
O assistente do Hands-on 12 — RAG fica com a primeira linha da tabela: seus vetores moram no mesmo PostgreSQL que todo o resto (a imagem do lab é o PostgreSQL 17 com pgvector), no seu próprio schema assistant, com um índice HNSW por distância de cosseno. Os números explicam por que não é preciso um banco vetorial dedicado: o lab gera embeddings de 300 eventos com 768 dimensões (cerca de 1 MB de floats), e mesmo os 200.000 eventos do catálogo dariam cerca de 600 MB. Um sistema a menos para operar e manter sincronizado é o motivo; o preço é que a construção do índice e as consultas vetoriais dividem o primário com as reservas.
SCHEMA = f"""
CREATE EXTENSION IF NOT EXISTS vector;
CREATE SCHEMA IF NOT EXISTS assistant;
CREATE TABLE IF NOT EXISTS assistant.documents (
id TEXT PRIMARY KEY, -- e.g. "event:123"
kind TEXT NOT NULL, -- "event" (add "faq", "policy" ... later)
title TEXT NOT NULL,
content TEXT NOT NULL, -- the text that was embedded
embedding vector({settings.embed_dim}) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- approximate nearest neighbours (HNSW) on cosine distance, the metric of the embedding model
CREATE INDEX IF NOT EXISTS documents_embedding_hnsw
ON assistant.documents USING hnsw (embedding vector_cosine_ops);
"""
Escolhendo entre eles
| Armazenamento | Responde | Alimentado por | Atualidade | Não serve para |
|---|---|---|---|---|
| Mecanismo de busca | "quais documentos casam com estas palavras, ranqueados?" | CDC / indexador | ~segundos | sistema de registro, transações |
| Armazenamento de objetos | "me dê este blob" | upload direto (URL pré-assinada) | imediata | registros pequenos, consultas dentro dos objetos |
| Data warehouse / lakehouse | "agregue o histórico de milhões de linhas" | ELT / CDC em streaming | de minutos a horas | servir o caminho quente da aplicação |
| Banco de dados vetorial | "o que é mais próximo em significado?" | worker de embeddings | ~segundos | casamentos exatos, filtros estruturados sozinhos |
BoxOffice: os armazenamentos especializados no lugar
- Busca: índice OpenSearch de eventos, alimentado pelo Debezium a partir do banco do catalog; analisador de português, sinônimo
sp↔sao paulo, casamento aproximado, reforço por popularidade; prefixos do autocomplete em cache. - Armazenamento de objetos: pôsteres enviados com URLs pré-assinadas, redimensionados por um worker, servidos pela CDN com chaves versionadas; PDFs de ingressos gerados sob demanda e guardados por 1 ano por uma regra de ciclo de vida.
- Warehouse: eventos de pedidos e de clickstream chegam a tabelas Iceberg no armazenamento de objetos; o dbt constrói o esquema estrela; o painel de "vendas por minuto" lê o warehouse, nunca o banco do order.
- Vetores: a base de conhecimento do assistente (políticas, FAQs, descrições de eventos) no pgvector, em um PostgreSQL próprio, com índice HNSW; um worker gera os embeddings de novo sempre que um documento muda.
Na entrevista
- Para qualquer requisito de "busca", diga índice invertido e como ele é mantido em sincronia (CDC, consistência eventual de segundos) — e que o índice é um dado derivado que você pode reconstruir.
- Imagens, vídeos e arquivos → armazenamento de objetos + CDN + URLs pré-assinadas; o banco guarda só chaves e metadados.
- "Analytics" ou "relatórios" → OLAP em um warehouse, armazenamento colunar, alimentado por CDC/ELT — "nunca no primário OLTP".
- Busca semântica ou RAG → embeddings + índice ANN (HNSW), e mencione recall vs latência e a opção do pgvector antes de um produto dedicado.
Boas práticas
- Versione o índice de busca e troque com um alias — construa
events_v2, preencha-o e troque o alias de forma atômica; reverter é trocar de volta. - Meça o atraso de cada armazenamento derivado — segundos atrás da fonte da verdade, como métrica com alerta, para que "a busca mostra eventos esgotados" seja pego por um gráfico, não pelos fãs.
- Dê aos blobs chaves imutáveis e versionadas —
posters/9f3a….jpgcomCache-Control: immutable; uma imagem alterada ganha uma chave nova, então nenhum cache precisa ser expurgado. - Defina regras de ciclo de vida no armazenamento de objetos — mova os PDFs de ingressos para uma classe mais barata depois do show e apague uploads abandonados depois de um dia; um armazenamento que só cresce é uma conta que só cresce.
- Avalie a recuperação com um conjunto fixo de perguntas antes de trocar o modelo ou o índice — as mesmas perguntas, a mesma métrica, antes e depois. Na BoxOffice:
scripts/eval_retrieval.pyno lab 12.
Armadilhas comuns
- Escritas duplas da aplicação no banco e no índice: eles se desalinham; use CDC ou um outbox.
- Tratar o índice de busca como fonte da verdade: uma mudança de mapeamento ou um índice corrompido precisa poder ser corrigido com uma reconstrução.
- Servir blobs pelos servidores de aplicação: cada byte de imagem custa CPU e banda da aplicação; use URLs pré-assinadas e a CDN.
- Relatórios no banco de produção: varreduras longas competem com o checkout; use o warehouse (ou pelo menos uma réplica).
- Vetores sem um plano de regeração: quando o modelo de embeddings muda, todos os vetores precisam ser recalculados — vetores antigos e novos não são comparáveis.
Como se conecta
flowchart LR
SOR["sistema de registro<br/>sql"] -->|"CDC, outbox"| IDX["índice invertido<br/>search"]
SOR -->|"CDC, ELT"| DWH["warehouse, lakehouse<br/>OLAP, colunas"]
SOR -->|"só as chaves"| OBJ["object storage<br/>URLs pré-assinadas"]
SOR -->|"embeddings"| VEC["banco vetorial<br/>ANN, HNSW"]
IDX --> RANK["ranqueamento, autocomplete<br/>→ Estudos de caso"]
OBJ --> CDN["servido pela CDN<br/>→ Borda"]
DWH --> ANA["analytics fora<br/>do primário OLTP"]
VEC --> RAG["memória RAG<br/>→ Sistemas de IA"]
SOR -.-> STREAM["streams de mudanças<br/>→ Mensageria"] Componentes da paleta: search · objstore · dwh · vectordb (os quatro armazenamentos derivados — reconstruíveis a partir da fonte) · sql (o sistema de registro; pgvector antes de um produto dedicado) · cdn (na frente do object store) · kafka (CDC com Debezium para o índice e o warehouse) · analytics (lê o warehouse, nunca o primário) · memory (a camada de recuperação do assistente). Cenários: search-autocomplete, video-streaming, ai-assistant, event-fanout. Continua em: Borda — DNS e CDN, Mensageria — padrões (outbox, CDC), Sistemas de IA — RAG e memória, Estudo de caso: autocomplete de busca, Estudo de caso: streaming de vídeo.
-
MANNING, C. D.; RAGHAVAN, P.; SCHÜTZE, H. Introduction to Information Retrieval. Cambridge University Press, 2008. ↩
-
ROBERTSON, S.; ZARAGOZA, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 3(4), 2009. ↩
-
Amazon Web Services. Amazon S3 User Guide — modelo de consistência de dados e durabilidade. ↩
-
KIMBALL, R.; ROSS, M. The Data Warehouse Toolkit. 3. ed. Wiley, 2013. ↩
-
STONEBRAKER, M. et al. C-Store: A Column-oriented DBMS. VLDB, 2005. ↩
-
ARMBRUST, M. et al. Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics. CIDR, 2021. ↩
-
MALKOV, Y. A.; YASHUNIN, D. A. Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs. IEEE TPAMI, 42(4), 2020 (arXiv:1603.09320, 2016). ↩