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).
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
LIKEvs 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
LIKEvsMATCHsob carga semelhante. - Sempre fixe o mesmo termo, o mesmo
LIMITe 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 (
furadeira→furadeira OR parafusadeiravia 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
- Escolha três termos: um raro (
orbitcam), um comum (usb), um composto ("chave de fenda"). - Rode LIKE e FTS para cada um com
.timer on. - Anote hits (ids) e se a ordem FTS faz sentido para um comprador.
- 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.