Usar Elasticsearch no Node.js permite indexar documentos e criar busca textual, filtros, agregações, autocomplete, relevância e análise de grandes volumes. O cliente oficial oferece conexão persistente, balanceamento, descoberta de nós e uma API próxima da REST API do Elasticsearch.
Elasticsearch não deve ser tratado como substituto automático do banco transacional. Em muitos sistemas, PostgreSQL ou MongoDB continuam sendo a fonte de verdade, enquanto Elasticsearch recebe uma projeção otimizada para busca. Essa separação exige sincronização, versionamento e tratamento de consistência eventual.
Neste guia, você aprenderá conexão, mappings, indexação, bulk, queries, filtros, paginação, aliases, reindexação, segurança, observabilidade, testes e integração por outbox.
Cliente oficial JavaScript
A documentação oficial do cliente JavaScript descreve mapeamento com a REST API, conexões keep-alive, load balancing, descoberta e TypeScript. Para modelagem e busca, consulte a referência oficial do Elasticsearch.
Para eventos confiáveis entre banco e índice, consulte Outbox Pattern no Node.js. Para mensagens de domínio, veja Domain Events no Node.js.
Instalação
npm install @elastic/elasticsearchConexão
import { Client } from '@elastic/elasticsearch';
const elastic = new Client({
node: process.env.ELASTICSEARCH_URL,
auth: process.env.ELASTIC_API_KEY
? { apiKey: process.env.ELASTIC_API_KEY }
: undefined,
requestTimeout: 5000,
maxRetries: 2
});Em Elastic Cloud, use as opções de cloud ID e API key recomendadas pela versão atual do cliente.
Verificando conexão
const info = await elastic.info();
logger.info({
clusterName: info.cluster_name,
version: info.version.number
}, 'Elasticsearch conectado');Não exponha informações do cluster em endpoint público.
Índice e mapping
await elastic.indices.create({
index: 'products-v1',
mappings: {
dynamic: 'strict',
properties: {
id: { type: 'keyword' },
name: {
type: 'text',
fields: {
keyword: { type: 'keyword' }
}
},
description: { type: 'text' },
categoryId: { type: 'keyword' },
priceCents: { type: 'long' },
active: { type: 'boolean' },
createdAt: { type: 'date' },
updatedAt: { type: 'date' }
}
}
});dynamic: strict rejeita campos desconhecidos e reduz crescimento acidental do mapping.
Text versus keyword
- text: conteúdo analisado para busca textual.
- keyword: valor exato para filtros, ordenação e agregações.
Um campo pode possuir os dois formatos por multi-field.
Analisadores
Analyzers transformam texto em tokens. Idioma, acentos e stemming afetam relevância. Teste com exemplos reais:
const result = await elastic.indices.analyze({
index: 'products-v1',
analyzer: 'standard',
text: 'Câmera profissional'
});Não altere analyzer de um campo existente sem reindexar.
Indexando documento
await elastic.index({
index: 'products-write',
id: product.id,
document: {
id: product.id,
name: product.name,
description: product.description,
categoryId: product.categoryId,
priceCents: product.priceCents,
active: product.active,
createdAt: product.createdAt,
updatedAt: product.updatedAt
}
});Use o ID do agregado como _id para updates idempotentes.
Refresh
Uma escrita não fica visível imediatamente até refresh. Não use refresh: true em cada documento de produção, pois aumenta custo. Em testes ou fluxos específicos, refresh: 'wait_for' pode ser apropriado.
Bulk API
const operations = products.flatMap(product => [
{
index: {
_index: 'products-write',
_id: product.id
}
},
product
]);
const response = await elastic.bulk({
refresh: false,
operations
});Bulk reduz round trips, mas cada item pode falhar independentemente.
Inspecionando falhas do bulk
if (response.errors) {
response.items.forEach((item, index) => {
const operation = item.index ?? item.update ?? item.delete;
if (operation?.error) {
logger.error({
error: operation.error,
productId: products[index].id
}, 'Falha no bulk');
}
});
}Não considere sucesso apenas pelo status HTTP do bulk.
Busca textual
const response = await elastic.search({
index: 'products-read',
query: {
multi_match: {
query: input.q,
fields: [
'name^3',
'description'
],
fuzziness: 'AUTO'
}
}
});O boost ^3 aumenta o peso do nome. Meça relevância com consultas reais.
Filtros
query: {
bool: {
must: [
{
multi_match: {
query: input.q,
fields: ['name^3', 'description']
}
}
],
filter: [
{ term: { active: true } },
{ term: { categoryId: input.categoryId } },
{
range: {
priceCents: {
gte: input.minPrice,
lte: input.maxPrice
}
}
}
]
}
}Filtros não participam do score e podem ser cacheados.
Não use query_string com entrada livre
A query string tradicional aceita sintaxe especial e pode produzir consultas caras ou comportamento inesperado. Prefira simple_query_string, match ou construção explícita.
Source filtering
const response = await elastic.search({
index: 'products-read',
_source: [
'id',
'name',
'priceCents',
'categoryId'
],
query
});Retorne apenas campos necessários.
Ordenação
sort: [
{ _score: 'desc' },
{ createdAt: 'desc' },
{ id: 'asc' }
]Ordenação em texto exige um campo keyword ou outro tipo com doc values.
Paginação simples
from: 0,
size: 20from e size funcionam para páginas rasas, mas páginas profundas exigem que shards mantenham muitos resultados.
search_after
const response = await elastic.search({
index: 'products-read',
size: 20,
sort: [
{ createdAt: 'desc' },
{ id: 'asc' }
],
search_after: cursor
});O cursor contém os valores de sort do último hit. Consulte Paginação em APIs Node.js.
Point in Time
Para navegação consistente durante mudanças, abra um Point in Time e use search_after. Feche quando terminar e defina keep_alive curto.
Agregações
const response = await elastic.search({
index: 'products-read',
size: 0,
query: { term: { active: true } },
aggs: {
categories: {
terms: {
field: 'categoryId',
size: 50
}
},
averagePrice: {
avg: { field: 'priceCents' }
}
}
});Agregações de alta cardinalidade podem consumir muita memória. Limite buckets.
Autocomplete
Opções incluem:
- search_as_you_type;
- edge n-gram;
- completion suggester;
- prefix em keyword para casos simples.
Escolha conforme atualização, relevância e tamanho.
Highlight
highlight: {
fields: {
name: {},
description: {
fragment_size: 120,
number_of_fragments: 2
}
}
}Escape o conteúdo antes de renderizar HTML.
Aliases
Use aliases para separar nome lógico e índice físico:
products-read -> products-v1
products-write -> products-v1Aplicações usam aliases, permitindo trocar o índice sem alterar configuração.
Reindexação sem downtime
- Crie
products-v2com novo mapping. - Copie ou reprojete os documentos.
- Valide quantidade e busca.
- Pause ou sincronize mudanças pendentes.
- Troque aliases atomicamente.
- Mantenha v1 para rollback temporário.
Alias swap
await elastic.indices.updateAliases({
actions: [
{ remove: { index: 'products-v1', alias: 'products-read' } },
{ add: { index: 'products-v2', alias: 'products-read' } }
]
});Elasticsearch como projeção
O banco principal confirma a operação. Um evento atualiza o índice posteriormente. A busca pode ficar brevemente desatualizada, portanto a interface deve tolerar consistência eventual.
Outbox e indexação
await unitOfWork.run(async tx => {
await tx.products.save(product);
await tx.outbox.add({
type: 'product.search-index.requested',
aggregateId: product.id,
payload: product.toSearchDocument()
});
});Um worker indexa de forma idempotente.
Versionamento externo
Eventos podem chegar fora de ordem. Inclua aggregateVersion e use controle de versão compatível com a API do Elasticsearch para impedir uma versão antiga de sobrescrever a nova.
Delete
await elastic.delete({
index: 'products-write',
id: productId
});Trate 404 como operação idempotente quando a semântica permitir.
Update parcial
await elastic.update({
index: 'products-write',
id: productId,
doc: {
active: false,
updatedAt: new Date().toISOString()
},
doc_as_upsert: true
});Para projeções completas, indexar o documento inteiro pode ser mais previsível.
Segurança
- use TLS;
- API key com privilégio mínimo;
- rede privada;
- não exponha endpoint ao navegador;
- limite índices e operações;
- rotacione credenciais;
- proteja snapshots.
Consulte Gestão de Segredos no Node.js.
Timeouts
Defina request timeout e limites nas queries. Uma busca complexa não deve consumir recursos indefinidamente.
Retries
O cliente pode repetir determinadas falhas, mas operações precisam ser idempotentes. Não repita automaticamente erros de mapping ou consultas inválidas.
Observabilidade
Monitore:
- latência p50, p95 e p99;
- timeouts;
- retries;
- bulk failures;
- rejected requests;
- heap e GC;
- shards;
- tamanho dos índices;
- refresh e merge;
- lag da projeção.
Slow logs
Configure slow logs no cluster para identificar queries e indexações caras. Redija dados sensíveis antes de compartilhar.
Tracing
Crie spans para search, bulk e index com nome do índice lógico, não com query completa.
Testes
Use Elasticsearch real em container para validar:
- mapping;
- analyzer;
- relevância;
- filtros;
- bulk;
- search_after;
- aliases;
- reindexação;
- versionamento.
Fixtures de relevância
Crie conjuntos de documentos e consultas com ordem esperada. Mudanças de analyzer ou boost precisam executar essa suíte.
Erros comuns
- Elasticsearch como banco primário sem necessidade: transações e consistência ficam difíceis.
- Mapping dinâmico sem controle: campos explodem.
- Bulk sem inspecionar itens: falhas ficam ocultas.
- from profundo: memória cresce.
- refresh em toda escrita: throughput cai.
- Query livre: custo e sintaxe ficam imprevisíveis.
- Sem alias: reindexação exige downtime.
- Sem versão: evento antigo sobrescreve novo.
Boas práticas
- Defina mappings explícitos.
- Use text e keyword corretamente.
- Faça bulk em lotes.
- Inspecione falhas individuais.
- Use filtros no bool.
- Prefira search_after.
- Use aliases.
- Sincronize por outbox.
- Monitore relevância e lag.
- Teste em cluster real.
Conclusão
Usar Elasticsearch no Node.js cria buscas textuais e agregações flexíveis com o cliente oficial. Mappings, analyzers e índices precisam ser desenhados de acordo com as consultas.
Quando Elasticsearch funciona como projeção, outbox, aliases e versionamento evitam inconsistências e downtime. Com bulk, search_after, segurança e testes de relevância, a busca pode evoluir sem comprometer o banco transacional.




