Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Elasticsearch no Node.js

Atualizado em: 5 de setembro de 2026

Rack de servidores processando fluxos de dados no Node.js

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/elasticsearch

Conexã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: 20

from 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-v1

Aplicações usam aliases, permitindo trocar o índice sem alterar configuração.

Reindexação sem downtime

  1. Crie products-v2 com novo mapping.
  2. Copie ou reprojete os documentos.
  3. Valide quantidade e busca.
  4. Pause ou sincronize mudanças pendentes.
  5. Troque aliases atomicamente.
  6. 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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita