Laboratório: busca de e-commerce com SQLite, D1 e Workers

Análise de performance e melhorias do lab

Comparar LIKE vs FTS5, medir latência e lista de verificação de evolução (sinônimos, cache Redis, OpenSearch).

Intermediário 45 min 32 pontos Leitura 0%

Nesta aula você vai

  • Comparar planos e custo de LIKE vs FTS5 no seed
  • Medir latência de forma simples e interpretar resultados
  • Priorizar melhorias: pesos, cache, sinônimos e quando migrar a OpenSearch

Análise de performance e melhorias do lab

Objetivos

Nesta aula você vai:

  • Comparar planos e custo de LIKE vs FTS5 no seed
  • Medir latência de forma simples e interpretar resultados
  • Priorizar melhorias: pesos, cache, sinônimos e quando migrar a OpenSearch

Introdução

O endpoint funciona; agora o laboratório fecha com evidência (plano + tempo) e um roteiro de evolução realista. Em dezoito produtos a diferença absoluta é pequena — o valor está em saber o que medir quando o catálogo crescer e quando o FTS embutido deixa de bastar.

Conteúdo

LIKE vs FTS5 lado a lado

LIKE (antipadrão de substring):

SELECT id, name, price_cents
FROM products
WHERE name LIKE '%usb%'
   OR description LIKE '%usb%'
   OR tags LIKE '%usb%';

FTS5:

SELECT p.id, p.name, p.price_cents, bm25(products_fts) AS score
FROM products_fts
JOIN products p ON p.id = products_fts.rowid
WHERE products_fts MATCH 'usb'
ORDER BY bm25(products_fts);

Diferenças qualitativas:

Aspecto LIKE %termo% FTS5 MATCH
Unidade Substring Token / frase / prefixo
Ranking Ordem de inserção / PK BM25 (relevância)
Acentos Depende de collation/LIKE Tokenizer remove_diacritics
Plano típico Scan da tabela base Consulta ao índice invertido
Prefixo para autocomplete LIKE 'tecla%' (só se o prefixo for fixo no início) tecla* no FTS

Medir: EXPLAIN QUERY PLAN e timing

EXPLAIN QUERY PLAN
SELECT id, name FROM products
WHERE name LIKE '%furadeira%' OR description LIKE '%furadeira%';

EXPLAIN QUERY PLAN
SELECT p.id, p.name
FROM products_fts
JOIN products p ON p.id = products_fts.rowid
WHERE products_fts MATCH 'furadeira';

No SQLite CLI:

.timer on

No Worker (aproximação de latência ponta a ponta):

const t0 = Date.now();
const { results } = await env.DB.prepare(/* ... */).bind(ftsQuery, 20).all();
const ms = Date.now() - t0;
return Response.json({ results, latencyMs: ms });

Interpretação honesta:

  • Seed de dezenas de linhas: use o plano, não a disputa de microssegundos.
  • Em staging com milhares/milhões de SKUs: compare P50/P95 de LIKE vs MATCH sob carga semelhante.
  • Sempre fixe o mesmo termo, o mesmo LIMIT e o mesmo ambiente (local vs remote D1).

Melhorias incrementais do lab

1. Boost de título (pesos BM25)

Já usado: bm25(products_fts, 10.0, 1.0, 2.0, 3.0). Ajuste empiricamente: se livros “ganham” demais via descrição longa, aumente o peso de name ou tags.

Alternativa: duas consultas (name MATCH vs corpo) e fusão de score no Worker — mais flexível, mais código.

2. Cache de consultas quentes

Termos repetidos (carregador, teclado) podem ir para:

  • Cache em memória do isolate (curto TTL; cuidado com inconsistência multi-isolate);
  • Redis/KV com chave search:v1:{hash(q)} e TTL de 30–120 s;
  • Invalidação (ou TTL curto) após ingestão de catálogo.

Cache depois de estabilizar sanitização e ranking — senão você congela bugs.

3. Sinônimos

FTS5 não traz tesauro rico como PostgreSQL. Opções no lab:

  • Expandir tags no seed (powerbank + carregador);
  • Expandir a query no Worker (furadeirafuradeira OR parafusadeira via mapa estático);
  • Em produção maior: motor externo com synonym graph.

4. Quando graduar para OpenSearch (ou Meilisearch)

Considere sair do D1/SQLite FTS quando:

  • Facets, typo-tolerance agressivo e sinônimos complexos forem requisito de produto;
  • Volume de QPS e tamanho de índice ultrapassarem conforto do D1;
  • Equipe precisar de pipelines de ingestão, analyzers e dashboards de relevância.

Até lá, FTS5 + filtros relacionais costuma ser a escolha correta para o escopo deste lab.

Exemplos práticos

Checklist de aceite do laboratório

[ ] Schema products + products_fts + 3 triggers aplicados no D1 local
[ ] Seed com ≥15 produtos e ≥3 categorias
[ ] MATCH termo, frase e prefixo validados no sqlite3 ou d1 execute
[ ] GET /search?q= retorna JSON com score e snippet
[ ] q vazio / inválido → results [] (sem 500)
[ ] q acima do limite → 400
[ ] prepare/bind usado (sem concatenar MATCH)
[ ] EXPLAIN QUERY PLAN documentado para LIKE e FTS5
[ ] bm25 com pesos de coluna justificados em uma frase
[ ] Lista de 3 melhorias futuras priorizadas (ex.: cache, sinônimos, facets)

Experimento mínimo de comparação

  1. Escolha três termos: um raro (orbitcam), um comum (usb), um composto ("chave de fenda").
  2. Rode LIKE e FTS para cada um com .timer on.
  3. Anote hits (ids) e se a ordem FTS faz sentido para um comprador.
  4. Registre uma conclusão em uma linha: “Para este corpus, FTS entrega ranking útil; LIKE só filtra substring.”

Problemas e como resolver

Problema Causa Mitigação
“FTS é mais lento” no seed Ruído de medição / cold start Repetir; olhar plano; escalar dados
Cache serve resultado velho TTL alto pós-update TTL curto ou invalidação por versão de catálogo
Sinônimos poluem ranking Expansão OR ampla demais Mapa pequeno; pesos; testes manuais
Pressa por OpenSearch Feature creep Checklist: só migre com requisito claro

Resumo

Feche o laboratório com evidência (EXPLAIN + medição de tempo) e uma lista de verificação de aceite. O LIKE ensina o problema; o FTS5 resolve tokenização e ranking no D1; pesos, cache e sinônimos estendem o desenho; o OpenSearch entra quando a busca vira produto central com requisitos que o SQLite não cobre bem. Com isso, a matéria prática do módulo está completa — valide no simulado.