Guia Prático de CQRS + Meilisearch: Chega de travar o MySQL com buscas textuais
Por Que LIKE '%termo%' é uma Armadilha de Performance (e Como o CQRS Resolve Isso)
No desenvolvimento de software, é extremamente comum iniciarmos sistemas utilizando bancos de dados relacionais tradicionais, como MySQL ou PostgreSQL, para gerenciar todas as operações da aplicação.
Quando surge a necessidade de criar uma barra de pesquisa, a reação instintiva da maioria dos desenvolvedores é recorrer à cláusula SQL:
SELECT * FROM products
WHERE name LIKE '%termo%';
No ambiente de desenvolvimento — ou com poucos milhares de registros — essa abordagem funciona de forma aceitável.
No entanto, à medida que o volume de dados cresce para centenas de milhares de linhas e o tráfego de usuários simultâneos aumenta, essa simples linha de código se transforma em uma das maiores armadilhas de performance de uma infraestrutura.
A Anatomia do Problema: Por Que o Banco Relacional “Chora”?
Para entender o gargalo, precisamos olhar para como os bancos relacionais organizam seus índices.
Motores como o InnoDB (MySQL) utilizam uma estrutura chamada Árvore B+ (B+ Tree) para indexação.
Essa estrutura é otimizada para:
- Buscas exatas
WHERE id = 42
- Ordenações
ORDER BY created_at
- Consultas por intervalo
WHERE price BETWEEN 100 AND 500
O Problema com %termo%
Quando realizamos uma busca com caractere coringa no início:
WHERE name LIKE '%matrix%'
quebramos completamente a lógica de navegação da Árvore B+.
O banco não consegue prever quais caracteres iniciam o texto, então ele se torna incapaz de navegar pelo índice.
A única alternativa restante é realizar um:
Full Table Scan
Ou seja, o banco precisa:
- Ler cada registro do disco
- Carregar para memória
- Verificar manualmente se contém o termo pesquisado
Complexidade Algorítmica
Com índices normais:
$$ O(\log N) $$
ou até próximo de constante.
Com %termo%:
$$ O(N) $$
Isso significa que o custo cresce linearmente com a quantidade de registros.
Se tivermos:
- 500 mil registros
- 50 usuários simultâneos
- buscas constantes
o servidor sofrerá:
- explosão de I/O de disco
- saturação de CPU
- lentidão generalizada
- risco real de timeout e indisponibilidade
Outro Problema: Busca Rígida
Bancos relacionais exigem correspondência literal.
Se o usuário digitar:
matrx
ao invés de:
matrix
o retorno será:
0 resultados
Isso gera frustração imediata.
A Solução Arquitetural: CQRS
CQRS significa:
Command Query Responsibility Segregation
A ideia é separar:
- Command → escrita
- Query → leitura
Arquitetura
┌──────────────┐
│ Aplicação │
└──────┬───────┘
│
┌───────────────┴───────────────┐
▼ ▼
(Escrita) (Leitura)
┌───────────┐ ┌──────────────┐
│ MySQL │ │ Meilisearch │
└───────────┘ └──────────────┘
Lado de Escrita (Commands)
O MySQL continua sendo o banco transacional principal.
Responsável por:
- integridade referencial
- transações ACID
- consistência forte
- persistência oficial dos dados
Exemplo:
INSERT INTO products (...)
UPDATE orders SET status = 'paid'
Lado de Leitura (Queries)
As buscas textuais pesadas são delegadas a motores especializados como:
- Meilisearch
- Elasticsearch
- Typesense
Esses motores mantêm uma cópia otimizada para pesquisa textual.
Exemplo:
GET /search?q=matrix
Benefício Imediato
Ao remover buscas textuais do MySQL:
✅ O banco relacional fica livre para transações críticas
✅ Menor contenção de recursos
✅ Escalabilidade horizontal
✅ Redução drástica de gargalos
O sistema deixa de correr risco de cair porque muitos usuários estão usando a barra de busca ao mesmo tempo.
Por Que o Meilisearch é Tão Rápido?
O Meilisearch utiliza Índice Invertido.
Funciona como o índice remissivo de um livro:
Ao invés de ler o livro inteiro procurando uma palavra, você consulta o índice e descobre exatamente onde ela está.
Exemplo Conceitual
Ao indexar:
"Matrix Reloaded"
"Matrix Revolutions"
"John Wick"
O índice invertido vira algo próximo disso:
{
"matrix": [1, 2],
"reloaded": [1],
"revolutions": [2],
"john": [3],
"wick": [3]
}
Buscar "matrix" é praticamente instantâneo.
Performance
Enquanto uma busca SQL pesada pode levar:
- 100ms
- 300ms
- 500ms
No Meilisearch normalmente temos:
- < 2ms
Mesmo com milhões de documentos.
Recursos Nativos de UX
Tolerância a Erros
Se o usuário digitar:
matrx
o sistema entende:
matrix
Isso é possível usando distância de edição
(baseada em Levenshtein).
Search-as-you-type
Resultados aparecem a cada tecla:
m
ma
mat
matr
matri
matrix
Experiência instantânea.
Ranking de Relevância
Você pode priorizar campos.
Exemplo:
Dar mais peso para:
- título
do que para:
- descrição
Exemplo:
{
"rankingRules": [
"title",
"description"
]
}
Consistência Eventual
Ao adotar CQRS, aceitamos que os dados existem em dois lugares:
- MySQL
- Meilisearch
Isso introduz:
Consistência Eventual
Ou seja:
Um produto pode ser salvo no MySQL e aparecer na busca alguns milissegundos depois.
Estratégias de Sincronização
Sincronização Direta
A API grava e indexa logo depois:
$productRepository->save($product);
$meili->index('products')->addDocuments([$product]);
Simples, porém acoplado.
Mensageria
Usando:
- RabbitMQ
- Kafka
Fluxo:
Produto criado
↓
Evento publicado
↓
Consumer indexa no Meilisearch
Mais resiliente.
CDC (Change Data Capture)
Ferramentas monitoram mudanças no banco em tempo real:
- Debezium
- Maxwell
- Canal
Sincronização automática e desacoplada.
Resultado Final
A arquitetura se torna:
- elástica
- desacoplada
- resiliente
- escalável
Capaz de suportar milhões de buscas sem comprometer a saúde do banco transacional.
Conclusão
Se sua aplicação possui:
- catálogo grande
- busca textual intensa
- muitos usuários simultâneos
então usar:
LIKE '%termo%'
não é apenas uma má prática.
É uma bomba-relógio arquitetural.
A combinação:
MySQL + CQRS + Meilisearch
transforma a busca em uma operação especializada, rápida e preparada para escala real.