Estágio 02 · 02-15
LockedQuase todo produto sério tem busca. Listagem de pedidos, catálogo, base de conhecimento, suporte ao cliente, descobrimento de produto. LIKE '%foo%' em Postgres não escala, sem índice apropriado, vira full scan; com pg_trgm vai um pouco; full-text com tsvector vai melhor; mas pra ranking sério, distância de edit, sinônimos, multilingual, faceting, autocomplete, fuzzy match, relevance tuning, você precisa de search engine dedicado.
E hoje, busca semântica (vector search via embeddings) entrou no mainstream, não substitui keyword, complementa. RAG (04-10) e busca híbrida BM25+vetor são padrão.
Este módulo é information retrieval por dentro: o que é inverted index, como tokenizer afeta tudo, o que é TF-IDF, BM25, vector embeddings, ANN search (HNSW), e como Elasticsearch/OpenSearch/Meilisearch/Typesense funcionam por baixo. Plus quando NÃO usar search engine (Postgres FTS basta) e quando obrigatório.
Estrutura central. Para cada term (palavra), lista de documentos que contêm:
"pedido" → [doc1, doc7, doc42]
"entrega" → [doc7, doc99]
Versão real armazena também posições (para phrase queries), term frequency (para ranking), e document length.
Build: tokenize, normalize, indexa cada term → docID. Search: pega query, tokenize do mesmo jeito, intersecta postings lists. Postings comprimidas (variable-byte, FOR delta encoding) economizam disco. Skip lists aceleram intersect.
Pipeline de análise transforma texto em terms:
Linguagem importa. Stemmer pra português ≠ inglês (Snowball, RSLP). Multi-language: index por idioma ou usar field por idioma.
Edge case: stemmer agressivo perde precisão. Exemplo "universal" e "universidade" colapsam. Stemmer brasileiro RSLP é agressivo; Snowball é conservador.
Termo frequente no doc + raro no corpus = relevante. Score:
log(N / df_t), N = total docs, df_t = docs com o term.Ingênuo, mas conceitualmente claro.
Refinamento de TF-IDF (Robertson/Spärck Jones, 1994). Default em Elasticsearch e maioria dos engines. Fórmula:
score(q, d) = Σ_t IDF(t) * (TF(t,d) * (k1+1)) / (TF(t,d) + k1 * (1 - b + b * |d|/avgdl))
k1 (geralmente 1.2): controla saturation de TF (term repetido 100x não vale 100x mais).b (geralmente 0.75): normaliza por document length (docs curtos não vencem por densidade).avgdl: tamanho médio dos docs.Tunar k1/b por corpus às vezes ajuda. Maioria usa default.
LLMs (e modelos menores tipo all-MiniLM, multilingual-e5) transformam texto em vetor (384-1536 dims). Similar texto → vetores próximos. Distância: cosine, dot product, L2.
Use case: busca semântica ("preciso de comida pra cachorro pequeno" casa com "ração para cães raça pequena" mesmo sem palavra comum).
Limitações: vetores capturam semântica geral, perdem nuance específica (números, IDs). Híbrido vence.
Brute-force O(N) é proibitivo a milhões. ANN (Approximate Nearest Neighbor) tradeia recall por velocidade.
Algoritmos:
Trade-off: build cost, memória, recall@k, latência.
Estado da arte. Estratégias:
score = Σ 1/(k + rank_i) por sistema. Simples, bom default.α * BM25 + (1-α) * cosine. Ajustar α por dataset.Elasticsearch 8+, OpenSearch, Vespa, Weaviate suportam híbrido nativamente.
tsvector + GIN index é viável até dezenas de milhões de docs com queries simples. Tem stemming via dicionários (portuguese, english). Sem facet builtin, sem rerank, sem fuzzy.
pg_trgm dá fuzzy via trigram similarity. pgvector dá ANN (HNSW desde 0.5).
Quando Postgres basta: busca em texto simples, < 10M docs, sem multi-language complicado, sem facets aninhados, sem ranking sofisticado. Quando não basta: catálogo grande com facets/agregações, autocomplete fuzzy, relevância tunada por boosting, suggesters.
ES (Elastic) é o engine dominante. OpenSearch é fork open-source da AWS após mudança de licença em 2021.
Architecture: cluster de nodes, shards (cada index dividido), replicas (read scaling + HA). Queries via REST/JSON DSL.
Recursos chave:
Custos: cluster ES não é barato. Em apps pequenos, overkill.
Alternativas focadas em DX e instant search:
Trade-off vs ES: menos flexível em aggregations e ranking custom, mas muito mais simples. Cabe num container.
Open-source da Yahoo. Combina indexing + serving + ML inference. Usado em scale (Yahoo, Spotify). Mais complexo, mas state-of-art em ranking ML-driven.
Onde indexar? Patterns:
Reindex completo: ES suporta _reindex, alias swap (zero-downtime: cria index novo, popula, troca alias).
Métricas:
1/rank do primeiro relevante.Tunar com judgments (humanos rotulam relevância) ou click models (cliques implicam relevância). Não tune relevância só com intuição.
Tipos:
Latência alvo < 100ms; precisa cache + debounce no client.
Estratégias:
title.pt, title.en, indexar com analisadores diferentes. Query field do idioma.Facets agregam contagens por categoria (category, brand, price_range). ES terms agg. Importante pra UX de catálogo.
Filtros usam filter context (cacheable, sem score). Query: match name + filter price_range + filter in_stock.
PDFs, Office docs precisam extract antes de indexar. Tools: Apache Tika, Unstructured. Pre-process: cleanup, dedupe, chunking (pra embeddings).
Chunking pra RAG: tamanho 200-800 tokens, overlap 10-20%, respeita boundaries (parágrafo, sentença). Métricas dependem disso.
Você precisa, sem consultar:
k1, b, e por que TF satura.Estender a Logística com busca de pedidos full-text + híbrida usando Meilisearch (ou OpenSearch) + pgvector pra semantic.
orders insere em outbox_orders_indexer. Worker Node consome e envia pra Meilisearch.id, tenant_id, customer_name, items[], delivery_address, status, created_at, notes.notes + items. Persiste vetor em pgvector.GET /search?q=...&filters=...&hybrid=true retorna { hits, facets, query_id, latency_ms }.npm run eval calcula precision@10, MRR, NDCG@10. Roda contra Meilisearch puro, vector puro, híbrido, compara.orders_v2, popula, troca alias. Demonstre.