Performance, antipadrões e decisão em produção

Antipadrões e mitigações

LIKE leading wildcard, FTS sem índice, sync atrasado, queries muito amplas e como corrigir.

Intermediário 45 min 33 pontos Leitura 0%

Nesta aula você vai

  • Identificar antipadrões clássicos de busca textual em produção
  • Aplicar mitigações concretas (índices, sanitize, sync, limites)
  • Priorizar correções pelo impacto em P95 e custo operacional

Antipadrões e mitigações

Objetivos

Nesta aula você vai:

  • Identificar antipadrões clássicos de busca textual em produção
  • Aplicar mitigações concretas (índices, sanitize, sync, limites)
  • Priorizar correções pelo impacto em P95 e custo operacional

Introdução

A maioria das crises de busca não vem de “falta de Elasticsearch”, e sim de padrões que escalam mal: LIKE '%x%', FTS declarado mas não usado, índice dessincronizado, ou queries que batem em meio corpus. Esta aula cataloga os vilões e o remédio.

Conteúdo

1. LIKE com leading wildcard

-- Antipadrão
SELECT * FROM articles WHERE body LIKE '%cloudflare%';

Isso impede uso eficiente de B-tree e vira scan. Mitigações:

  • FTS5 / FULLTEXT / tsvector / text index.
  • Trigram (pg_trgm, FTS5 trigram) quando substring é requisito real.
  • Normalizar e buscar por tokens prefix (cloud*) quando UX permitir.

2. “Temos FTS” mas a app ainda usa LIKE

Índice existe; código legado não. Mitigação: sinalizador de funcionalidade (feature flag), buscar chamadas LIKE '%', migrar endpoints um a um, apagar o caminho lento.

3. External content sem triggers / sync atrasado

No D1/SQLite com content='articles', sem triggers o índice mente. Em Cassandra→OpenSearch, lag não monitorado entrega resultados fantasmas.

Mitigações:

  • Triggers INSERT/UPDATE/DELETE (FTS5).
  • CDC/outbox + métrica de lag.
  • Job de reconciliação periódica (diff ids).

4. Queries excessivamente amplas

-- Antipadrão: OR de mil termos / prefixo de 1 letra
MATCH 'a*'

Mitigações:

  • Mínimo de 2–3 caracteres no autocomplete.
  • Limite de tokens (ex.: 8–10).
  • AND default em vez de OR amplo.
  • LIMIT baixo; timeout no Worker.
  • Stopwords e rejeição de queries vazias.

5. Concatenar input cru na sintaxe FTS

Usuário digita " ou AND e quebra a query ou amplia demais. Mitigação: whitelist de tokens, aspas em cada termo, escape conforme o motor.

6. OFFSET profundo

LIMIT 20 OFFSET 100000;

Mitigações: cap de páginas, keyset, “mostrar mais” com top-K, cursor opaco.

7. Reindex em horário de pico / tokenizer mudado sem rebuild

Mudar tokenizer sem rebuild deixa índice inconsistente com a expectativa. Mitigação: janela de manutenção, índice novo + alias swap (OpenSearch), ou rebuild FTS controlado.

8. Um cluster de busca para tudo sem isolamento

Logs + busca de produto no mesmo cluster se atropelam. Mitigação: índices/clusters separados; quotas; filas de indexação.

9. Tratar busca vetorial como substituto direto do FTS

Embeddings sem filtro lexical geram “sopa semântica” irrelevante. Mitigação: híbrido (FTS top-K + re-rank vetorial) e avaliação offline de qualidade.

Priorização

Sintoma Antipadrão provável Prioridade
CPU alta em SELECT LIKE %x% / query ampla P0
Resultados errados Sync/triggers P0
P95 ruim só página 20+ OFFSET P1
Erros de sintaxe FTS Input cru P1
Relevância fraca Só OR / sem pesos P2

Exemplos práticos

// Antes: inseguro e amplo
const bad = `* FROM articles_fts WHERE articles_fts MATCH '${userInput}'`;

// Depois: tokens seguros + AND + limite
function safeMatch(userInput) {
  const tokens = String(userInput)
    .toLowerCase()
    .normalize('NFD')
    .replace(/\p{M}/gu, '')
    .replace(/[^\p{L}\p{N}\s]/gu, ' ')
    .trim()
    .split(/\s+/)
    .filter((t) => t.length >= 2)
    .slice(0, 8);
  if (!tokens.length) return null;
  return tokens.map((t) => `"${t}"`).join(' AND ');
}
-- Mitigação D1: garantir sync
CREATE TRIGGER articles_ai AFTER INSERT ON articles BEGIN
  INSERT INTO articles_fts(rowid, title, body)
  VALUES (new.id, new.title, new.body);
END;
-- (+ UPDATE/DELETE como na aula de D1)
-- Substituir LIKE leading wildcard
-- Antes: WHERE title LIKE '%d1%'
-- Depois:
SELECT rowid, title FROM articles_fts
WHERE articles_fts MATCH 'title: d1*'
ORDER BY bm25(articles_fts)
LIMIT 20;

Problemas e como resolver

Problema Causa Mitigação
Timeout intermitente Query ampla + carga Limites + cache + P95 alert
“Não acha o doc novo” Lag / sem trigger Sync + lag metric
XSS no highlight HTML sem escape Escape + marcadores controlados
Custo de cloud explode Reindex contínuo mal feito Batch, partial update, aliases

Resumo

Elimine LIKE '%…%', proteja a sintaxe FTS, sincronize o índice, limite amplitude e profundidade de página. Com antipadrões sob controle, a checklist de arquitetura (próxima aula) decide FTS no banco versus motor dedicado com critérios objetivos.