Elasticsearch em JavaScript da maneira correta, parte II
Conheça as práticas recomendadas de produção e como executar o cliente Elasticsearch Node.js em ambientes serverless para reduzir erros na codificação.
Esta é a segunda parte da nossa série sobre Elasticsearch em JavaScript. Na primeira parte, aprendemos como configurar nosso ambiente corretamente, configurar o cliente Node.js, indexar dados e realizar buscas. Nesta segunda parte, aprenderemos como implementar as melhores práticas de produção e executar o cliente Elasticsearch Node.js em ambientes Serverless.
Analisaremos:
Você pode conferir o código-fonte com os exemplos aqui.
Melhores práticas de produção
Tratamento de erros no Elasticsearch
Uma funcionalidade útil do cliente Elasticsearch em Node.js é que ele expõe objetos para os possíveis erros no Elasticsearch, permitindo que você os valide e trate de diferentes maneiras.
Para ver todos, execute o seguinte comando:
const { errors } = require('@elastic/elasticsearch')
console.log(errors)Vamos voltar ao exemplo de pesquisa e tratar de alguns dos possíveis erros:
app.get("/search/lexic", async (req, res) => {
....
} catch (error) {
if (error instanceof errors.ResponseError) {
let errorMessage =
"Response error!, query malformed or server down, contact the administrator!";
if (error.body.error.type === "parsing_exception") {
errorMessage = "Query malformed, make sure mappings are set correctly";
}
res.status(error.meta.statusCode).json({
erroStatus: error.meta.statusCode,
success: false,
results: null,
error: errorMessage,
});
}
res.status(500).json({
success: false,
results: null,
error: error.message,
});
}
});ResponseError em particular, ocorrerá quando a resposta for 4xx ou 5xx, o que significa que a solicitação está incorreta ou o servidor não está disponível.
Podemos testar esse tipo de erro gerando consultas incorretas, como tentar fazer uma consulta de termo em um campo do tipo texto:
Erro padrão:
{
"success": false,
"results": null,
"error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}Erro personalizado:
{
"erroStatus": 400,
"success": false,
"results": null,
"error": "Response error!, query malformed or server down; contact the administrator!"
}Também podemos capturar e lidar com cada tipo de erro de uma determinada maneira. Por exemplo, podemos adicionar lógica de repetição em um TimeoutError.
app.get("/search/semantic", async (req, res) => {
try {
...
} catch (error) {
if (error instanceof errors.TimeoutError) {
// Retry logic...
res.status(error.meta.statusCode).json({
erroStatus: error.meta.statusCode,
success: false,
results: null,
error:
"The request took more than 10s after 3 retries. Try again later.",
});
}
}
});Teste
Os testes são essenciais para garantir a estabilidade do aplicativo. Para testar o código de forma isolada do Elasticsearch, podemos usar a biblioteca elasticsearch-js-mock ao criar nosso cluster.
Esta biblioteca permite instanciar um cliente muito semelhante ao real, mas que responderá à nossa configuração substituindo apenas a camada HTTP do cliente por uma camada simulada, mantendo o restante igual ao original.
Vamos instalar a biblioteca mocks e o AVA para testes automatizados.
npm install @elastic/elasticsearch-mock
npm install --save-dev ava
Vamos configurar o arquivo package.json para executar os testes. Certifique-se de que esteja assim:
"type": "module",
"scripts": {
"test": "ava"
},
"devDependencies": {
"ava": "^5.0.0"
}Vamos agora criar um arquivo test.js e instalar nosso cliente de simulação:
const { Client } = require('@elastic/elasticsearch')
const Mock = require('@elastic/elasticsearch-mock')
const mock = new Mock()
const client = new Client({
node: 'http://localhost:9200',
Connection: mock.getConnection()
})Agora, adicione uma simulação para pesquisa semântica:
function createSemanticSearchMock(query, indexName) {
mock.add(
{
method: "POST",
path: `/${indexName}/_search`,
body: {
query: {
semantic: {
field: "semantic_field",
query: query,
},
},
},
},
() => {
return {
hits: {
total: { value: 2, relation: "eq" },
hits: [
{
_id: "1",
_score: 0.9,
_source: {
owner_name: "Alice Johnson",
pet_name: "Buddy",
species: "Dog",
breed: "Golden Retriever",
vaccination_history: ["Rabies", "Parvovirus", "Distemper"],
visit_details:
"Annual check-up and nail trimming. Healthy and active.",
},
},
{
_id: "2",
_score: 0.7,
_source: {
owner_name: "Daniel Kim",
pet_name: "Mochi",
species: "Rabbit",
breed: "Mixed",
vaccination_history: [],
visit_details:
"Nail trimming and general health check. No issues.",
},
},
],
},
};
}
);
}Agora podemos criar um teste para o nosso código, garantindo que a parte do Elasticsearch sempre retorne os mesmos resultados:
import test from 'ava';
test("performSemanticSearch must return formatted results correctly", async (t) => {
const indexName = "vet-visits";
const query = "Which pets had nail trimming?";
createSemanticSearchMock(query, indexName);
async function performSemanticSearch(esClient, q, indexName = "vet-visits") {
try {
const result = await esClient.search({
index: indexName,
body: {
query: {
semantic: {
field: "semantic_field",
query: q,
},
},
},
});
return {
success: true,
results: result.hits.hits,
};
} catch (error) {
if (error instanceof errors.TimeoutError) {
return {
success: false,
results: null,
error: error.body.error.reason,
};
}
return {
success: false,
results: null,
error: error.message,
};
}
}
const result = await performSemanticSearch(esClient, query, indexName);
t.true(result.success, "The search must be successful");
t.true(Array.isArray(result.results), "The results must be an array");
if (result.results.length > 0) {
t.true(
"_source" in result.results[0],
"Each result must have a _source property"
);
t.true(
"pet_name" in result.results[0]._source,
"Results must include the pet_name field"
);
t.true(
"visit_details" in result.results[0]._source,
"Results must include the visit_details field"
);
}
});Vamos executar os testes.
npm run test

Pronto! A partir de agora, podemos testar nosso aplicativo focando 100% no código e não em fatores externos.
Ambientes sem servidor
Como executar o cliente no Elastic Serverless
Já abordamos a execução do Elasticsearch na nuvem ou em infraestrutura local; no entanto, o cliente Node.js também oferece suporte a conexões com o Elastic Cloud Serverless.
O Elastic Cloud Serverless permite que você crie um projeto onde não precisa se preocupar com a infraestrutura, já que a Elastic cuida disso internamente, e você só precisa se preocupar com os dados que deseja indexar e por quanto tempo deseja ter acesso a eles.
Do ponto de vista da utilização, o Serverless separa o processamento do armazenamento, proporcionando recursos de escalonamento automático tanto para pesquisa quanto para indexação. Isso permite que você cultive apenas os recursos de que realmente precisa.
O cliente realiza as seguintes adaptações para se conectar ao Serverless:
Desativa a detecção de pacotes e ignora quaisquer opções relacionadas a ela.
Ignora todos os nós passados na configuração, exceto o primeiro, e ignora quaisquer opções de filtragem e seleção de nós.
Habilita a compressão e o método `TLSv1_2_method` (igual à configuração para o Elastic Cloud).
Adiciona um cabeçalho HTTP `elastic-api-version` a todas as requisições.
Utiliza `CloudConnectionPool` por padrão em vez de `WeightedConnectionPool`.
Desativa os cabeçalhos `content-type` e `accept` fornecidos pelo fornecedor, em favor dos tipos MIME padrão.
Para conectar seu projeto sem servidor, você precisa usar o parâmetro serverMode: serverless.
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
node: 'ELASTICSEARCH_ENDPOINT',
auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
serverMode: "serverless",
});Como executar o cliente em um ambiente de função como serviço
No exemplo, usamos um servidor Node.js, mas você também pode se conectar usando um ambiente de função como serviço com funções como AWS Lambda, GCP Run, etc.
'use strict'
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
// client initialisation
})
exports.handler = async function (event, context) {
// use the client
}Outro exemplo é conectar-se a serviços como o Vercel, que também é serverless. Você pode conferir este exemplo completo de como fazer isso, mas a parte mais relevante do endpoint de pesquisa se parece com isto:
const response = await client.search(
{
index: INDEX,
// You could directly send from the browser
// the Elasticsearch's query DSL, but it will
// expose you to the risk that a malicious user
// could overload your cluster by crafting
// expensive queries.
query: {
match: { field: req.body.text },
},
},
{
headers: {
Authorization: `ApiKey ${token}`,
},
}
);Este endpoint reside na pasta /api e é executado a partir do lado do servidor, de forma que o cliente só tenha controle sobre o parâmetro “texto” que corresponde ao termo de pesquisa.
A implicação de usar a função como serviço é que, ao contrário de um servidor que funciona 24 horas por dia, 7 dias por semana, as funções apenas ativam a máquina que executa a função e, assim que ela termina, a máquina entra em modo de repouso para consumir menos recursos.
Essa configuração pode ser conveniente se o aplicativo não receber muitas solicitações; caso contrário, os custos podem ser elevados. Você também precisa levar em consideração o ciclo de vida das funções e os tempos de execução (que, em alguns casos, podem ser de apenas alguns segundos).
Conclusão
Neste artigo, aprendemos como lidar com erros, o que é crucial em ambientes de produção. Também abordamos os testes da nossa aplicação enquanto simulávamos o serviço Elasticsearch, o que proporciona testes confiáveis independentemente do estado do cluster e nos permite focar no nosso código.
Por fim, demonstramos como criar uma infraestrutura totalmente sem servidor, provisionando tanto o Elastic Cloud Serverless quanto um aplicativo Vercel.

