正确使用 JavaScript 的 Elasticsearch,第一部分
讲解如何用 JavaScript 创建可投入生产的 Elasticsearch 后端。 探索如何使用 JavaScript 与 Elasticsearch,遵循客户端/服务器最佳实践,搭建包含多个搜索端点的服务器,用于查询 Elasticsearch 文档。
本文是系列文章的第一篇,介绍如何使用 JavaScript 使用 Elasticsearch。在本系列中,您将学习如何在 JavaScript 环境中使用 Elasticsearch 的基础知识,并回顾创建搜索应用程序的最相关功能和最佳实践。最后,您将了解使用 JavaScript 运行 Elasticsearch 所需的一切。
在第一部分中,我们将回顾
您可以 在这里查看示例的源代码 。
什么是 Elasticsearch Node.js 客户端?
Elasticsearch Node.js 客户端是一个 JavaScript 库,它将 Elasticsearch API 的 HTTP REST 调用放到了 JavaScript 中。这样就能更轻松地处理和使用帮助程序,简化批量编制文档索引等任务。
环境
前端、后端还是无服务器?
要使用 JavaScript 客户端创建搜索应用程序,我们至少需要两个组件:Elasticsearch 集群和运行客户端的 JavaScript 运行时。
JavaScript 客户端支持所有 Elasticsearch 解决方案(云、on-prem 和 Serverless),它们之间没有重大区别,因为客户端内部会处理所有变化,所以你不必担心使用哪一种。
不过,JavaScript 运行时必须从服务器运行,而不能直接从浏览器运行。

这是因为从浏览器调用 Elasticsearch 时,用户可能会获得敏感信息,如集群 API 密钥、主机或查询本身。Elasticsearch 建议永远不要将集群直接暴露在互联网上 ,而是使用一个中间层来抽象所有这些信息,这样用户只能看到参数。您可以在这里了解更多相关信息。
我们建议使用这样的模式:

在这种情况下,客户端只向服务器发送搜索条件和验证密钥,而服务器则完全控制查询和与 Elasticsearch 的通信。
连接客户端
首先,按照以下步骤创建一个 API 密钥。
按照前面的示例,我们将创建一个简单的 Express 服务器,并使用 Node.JS 服务器的客户端连接到该服务器。
我们将使用 NPM 初始化项目,并安装 Elasticsearch 客户端和Express。后者是一个在 Node.js 中调用服务器的库。使用 Express,我们可以通过 HTTP 与后端交互。
让我们初始化项目:
npm init -y
安装依赖项:
npm install @elastic/elasticsearch express split2 dotenv
让我来为你分析一下:
@elastic/elasticsearch:它是 Node.js 的官方客户端
快递:它将使我们能够运行一个轻量级的 nodejs 服务器,以暴露 Elasticsearch
split2: 将文本行分割成数据流。每次处理一行 ndjson 文件时非常有用
dotenv:允许我们使用 .env 管理环境变量文件
创建 .env文件,并添加以下几行:
ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"这样,我们就可以使用dotenv 软件包导入这些变量。
创建server.js 文件:
const express = require("express");
const bodyParser = require("body-parser");
const { Client } = require("@elastic/elasticsearch");
require("dotenv").config(); //environment variables setup
const ELASTICSEARCH_ENDPOINT = process.env.ELASTICSEARCH_ENDPOINT;
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY;
const PORT = 3000;
const app = express();
app.listen(PORT, () => {
console.log("Server running on port", PORT);
});
app.use(bodyParser.json());
let esClient = new Client({
node: ELASTICSEARCH_ENDPOINT,
auth: { apiKey: ELASTICSEARCH_API_KEY },
});
app.get("/ping", async (req, res) => {
try {
const result = await esClient.info();
res.status(200).json({
success: true,
clusterInfo: result,
});
} catch (error) {
console.error("Error getting Elasticsearch info:", error);
res.status(500).json({
success: false,
clusterInfo: null,
error: error.message,
});
}
});这段代码设置了一个基本的 Express.js 服务器,该服务器监听端口 3000,并使用 API 密钥进行身份验证,连接到 Elasticsearch 集群。它包括一个 /ping 端点,通过 GET 请求访问时,可使用 Elasticsearch 客户端的.info() 方法查询 Elasticsearch 集群的基本信息。
如果查询成功,会以 JSON 格式返回群集信息;否则会返回错误信息。服务器还使用 body-parser 中间件来处理 JSON 请求体。
运行文件,启动服务器:
node server.js
答案应该是这样的
Server running on port 3000现在,让我们查阅端点/ping ,检查 Elasticsearch 集群的状态。
curl http://localhost:3000/ping
{
"success": true,
"clusterInfo": {
"name": "instance-0000000000",
"cluster_name": "61b7e19eec204d59855f5e019acd2689",
"cluster_uuid": "BIfvfLM0RJWRK_bDCY5ldg",
"version": {
"number": "9.0.0",
"build_flavor": "default",
"build_type": "docker",
"build_hash": "112859b85d50de2a7e63f73c8fc70b99eea24291",
"build_date": "2025-04-08T15:13:46.049795831Z",
"build_snapshot": false,
"lucene_version": "10.1.0",
"minimum_wire_compatibility_version": "8.18.0",
"minimum_index_compatibility_version": "8.0.0"
},
"tagline": "You Know, for Search"
}
}编制文件索引
一旦连接起来,我们就可以使用语义_文本(用于语义搜索)和文本(用于全文查询)等映射对文档进行索引。有了这两种字段类型,我们还可以进行混合搜索。
我们将创建一个新的load.js 文件来生成映射并上传文件。
Elasticsearch 客户端
我们首先需要对客户端进行实例化和身份验证:
const { Client } = require("@elastic/elasticsearch");
const ELASTICSEARCH_ENDPOINT = "cluster/project_endpoint";
const ELASTICSEARCH_API_KEY = "apiKey";
const esClient = new Client({
node: ELASTICSEARCH_ENDPOINT,
auth: { apiKey: ELASTICSEARCH_API_KEY },
});语义映射
我们将创建一个包含兽医院数据的索引。我们将保存主人、宠物和访问详情的信息。
我们要进行全文搜索的数据,如名称和描述,将以文本形式存储。类别中的数据,如动物的种类或品种,将以关键字的形式存储。
此外,我们还将把所有字段的值复制到一个 semantic_text 字段中,以便也能针对这些信息运行语义搜索。
const INDEX_NAME = "vet-visits";
const createMappings = async (indexName, mapping) => {
try {
const body = await esClient.indices.create({
index: indexName,
body: {
mappings: mapping,
},
});
console.log("Index created successfully:", body);
} catch (error) {
console.error("Error creating mapping:", error);
}
};
await createMappings(INDEX_NAME, {
properties: {
owner_name: {
type: "text",
copy_to: "semantic_field",
},
pet_name: {
type: "text",
copy_to: "semantic_field",
},
species: {
type: "keyword",
copy_to: "semantic_field",
},
breed: {
type: "keyword",
copy_to: "semantic_field",
},
vaccination_history: {
type: "keyword",
copy_to: "semantic_field",
},
visit_details: {
type: "text",
copy_to: "semantic_field",
},
semantic_field: {
type: "semantic_text",
},
},
});批量助手
客户端的另一个优势是,我们可以使用批量助手来分批建立索引。通过批量辅助器,我们可以轻松处理并发、重试等问题,以及如何处理通过函数成功或失败的每个文档。
该助手的一个吸引人的特点是可以使用数据流。该功能允许您逐行发送文件,而不是将整个文件存储在内存中并一次性发送到 Elasticsearch。
要将数据上传到 Elasticsearch,请在项目根目录下创建名为 data.ndjson 的文件,并添加以下信息(也可以从此处下载包含数据集的文件):
{"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."}
{"owner_name":"Marco Rivera","pet_name":"Milo","species":"Cat","breed":"Siamese","vaccination_history":["Rabies","Feline Leukemia"],"visit_details":"Slight eye irritation, prescribed eye drops."}
{"owner_name":"Sandra Lee","pet_name":"Pickles","species":"Guinea Pig","breed":"Mixed","vaccination_history":[],"visit_details":"Loss of appetite, recommended dietary changes."}
{"owner_name":"Jake Thompson","pet_name":"Luna","species":"Dog","breed":"Labrador Mix","vaccination_history":["Rabies","Bordetella"],"visit_details":"Mild ear infection, cleaning and antibiotics given."}
{"owner_name":"Emily Chen","pet_name":"Ziggy","species":"Cat","breed":"Mixed","vaccination_history":["Rabies","Feline Calicivirus"],"visit_details":"Vaccination update and routine physical."}
{"owner_name":"Tomás Herrera","pet_name":"Rex","species":"Dog","breed":"German Shepherd","vaccination_history":["Rabies","Parvovirus","Leptospirosis"],"visit_details":"Follow-up for previous leg strain, improving well."}
{"owner_name":"Nina Park","pet_name":"Coco","species":"Ferret","breed":"Mixed","vaccination_history":["Rabies"],"visit_details":"Slight weight loss; advised new diet."}
{"owner_name":"Leo Martínez","pet_name":"Simba","species":"Cat","breed":"Maine Coon","vaccination_history":["Rabies","Feline Panleukopenia"],"visit_details":"Dental cleaning. Minor tartar buildup removed."}
{"owner_name":"Rachel Green","pet_name":"Rocky","species":"Dog","breed":"Bulldog Mix","vaccination_history":["Rabies","Parvovirus"],"visit_details":"Skin rash, antihistamines prescribed."}
{"owner_name":"Daniel Kim","pet_name":"Mochi","species":"Rabbit","breed":"Mixed","vaccination_history":[],"visit_details":"Nail trimming and general health check. No issues."}我们使用 split2 对文件行进行流式处理,而批量助手则将它们发送到 Elasticsearch。
const { createReadStream } = require("fs");
const split = require("split2");
const indexData = async (filePath, indexName) => {
try {
console.log(`Indexing data from ${filePath} into ${indexName}...`);
const result = await esClient.helpers.bulk({
datasource: createReadStream(filePath).pipe(split()),
onDocument: () => {
return {
index: { _index: indexName },
};
},
onDrop(doc) {
console.error("Error processing document:", doc);
},
});
console.log("Bulk indexing successful elements:", result.items.length);
} catch (error) {
console.error("Error indexing data:", error);
throw error;
}
};
await indexData("./data.ndjson", INDEX_NAME);上面的代码读取 .ndjson文件,并使用helpers.bulk 方法将每个 JSON 对象批量索引到指定的 Elasticsearch 索引中。它使用createReadStream 和split2 对文件进行流式处理,为每个文件设置索引元数据,并记录处理失败的文件。完成后,它会记录成功索引的项目数。
除indexData 功能外,您还可以使用 Kibana 直接通过用户界面上传文件,并使用上传数据文件用户界面。
我们运行文件,将文件上传到 Elasticsearch 集群。
node load.js
Creating mappings for index vet-visits...
Index created successfully: { acknowledged: true, shards_acknowledged: true, index: 'vet-visits' }
Indexing data from ./data.ndjson into vet-visits...
Bulk indexing completed. Total documents: 10, Failed: 0在 Elasticsearch 中搜索数据
回到server.js 文件,我们将创建不同的端点来执行词法、语义或混合搜索。
简而言之,这些类型的搜索并不相互排斥,而是取决于您需要回答的问题类型。
查询类型 | 用例 | 问题示例 |
|---|---|---|
词法查询 | 问题中的单词或词根很可能出现在索引文件中。问题与文件之间的标记相似性。 | 我在找一件蓝色运动 T 恤。 |
语义查询 | 问题中的词语不可能出现在文件中。问题与文件之间的概念相似性。 | 我在寻找适合寒冷天气穿的衣服。 |
混合搜索 | 问题包含词汇和/或语义成分。问题与文档之间的标记和语义相似性。 | 我想为海滩婚礼找一件 S 码的礼服。 |
问题的词汇 部分很可能是标题和说明的一部分,或者是类别名称,而语义 部分则是与这些领域相关的概念。蓝色可能是一个类别名称或描述的一部分,海滩婚礼不太可能是,但可以与亚麻服装在语义上相关。
词法查询 (/search/lexic?q=<query_term>)
词法搜索也称全文搜索,是指基于标记的相似性进行搜索;也就是说,经过分析后,将返回包含搜索标记的文档。
您可以点击此处查看我们的词法搜索实践教程。
app.get("/search/lexic", async (req, res) => {
const { q } = req.query;
const INDEX_NAME = "vet-visits";
try {
const result = await esClient.search({
index: INDEX_NAME,
size: 5,
body: {
query: {
multi_match: {
query: q,
fields: ["owner_name", "pet_name", "visit_details"],
},
},
},
});
res.status(200).json({
success: true,
results: result.hits.hits
});
} catch (error) {
console.error("Error performing search:", error);
res.status(500).json({
success: false,
results: null,
error: error.message,
});
}
});我们测试:修剪指甲
curl http://localhost:3000/search/lexic?q=nail%20trimming请回答:
{
"success": true,
"results": [
{
"_index": "vet-visits",
"_id": "-RY6RJYBLe2GoFQ6-9n9",
"_score": 2.7075968,
"_source": {
"pet_name": "Mochi",
"owner_name": "Daniel Kim",
"species": "Rabbit",
"visit_details": "Nail trimming and general health check. No issues.",
"breed": "Mixed",
"vaccination_history": []
}
},
{
"_index": "vet-visits",
"_id": "8BY6RJYBLe2GoFQ6-9n9",
"_score": 2.560356,
"_source": {
"pet_name": "Buddy",
"owner_name": "Alice Johnson",
"species": "Dog",
"visit_details": "Annual check-up and nail trimming. Healthy and active.",
"breed": "Golden Retriever",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Distemper"
]
}
}
]
}语义查询 (/search/semantic?q=<query_term>)
语义搜索与词汇搜索不同,它通过矢量搜索找到与搜索词含义相似的结果。
您可以点击这里查看我们的语义搜索实践教程。
app.get("/search/semantic", async (req, res) => {
const { q } = req.query;
const INDEX_NAME = "vet-visits";
try {
const result = await esClient.search({
index: INDEX_NAME,
size: 5,
body: {
query: {
semantic: {
field: "semantic_field",
query: q
},
},
},
});
res.status(200).json({
success: true,
results: result.hits.hits,
});
} catch (error) {
console.error("Error performing search:", error);
res.status(500).json({
success: false,
results: null,
error: error.message,
});
}
});我们进行测试:谁做了修脚?
curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?请回答:
{
"success": true,
"results": [
{
"_index": "vet-visits",
"_id": "-RY6RJYBLe2GoFQ6-9n9",
"_score": 4.861466,
"_source": {
"owner_name": "Daniel Kim",
"pet_name": "Mochi",
"species": "Rabbit",
"breed": "Mixed",
"vaccination_history": [],
"visit_details": "Nail trimming and general health check. No issues."
}
},
{
"_index": "vet-visits",
"_id": "8BY6RJYBLe2GoFQ6-9n9",
"_score": 4.7152824,
"_source": {
"pet_name": "Buddy",
"owner_name": "Alice Johnson",
"species": "Dog",
"visit_details": "Annual check-up and nail trimming. Healthy and active.",
"breed": "Golden Retriever",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Distemper"
]
}
},
{
"_index": "vet-visits",
"_id": "9RY6RJYBLe2GoFQ6-9n9",
"_score": 1.6717153,
"_source": {
"pet_name": "Rex",
"owner_name": "Tomás Herrera",
"species": "Dog",
"visit_details": "Follow-up for previous leg strain, improving well.",
"breed": "German Shepherd",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Leptospirosis"
]
}
},
{
"_index": "vet-visits",
"_id": "9xY6RJYBLe2GoFQ6-9n9",
"_score": 1.5600781,
"_source": {
"pet_name": "Simba",
"owner_name": "Leo Martínez",
"species": "Cat",
"visit_details": "Dental cleaning. Minor tartar buildup removed.",
"breed": "Maine Coon",
"vaccination_history": [
"Rabies",
"Feline Panleukopenia"
]
}
},
{
"_index": "vet-visits",
"_id": "-BY6RJYBLe2GoFQ6-9n9",
"_score": 1.2696637,
"_source": {
"pet_name": "Rocky",
"owner_name": "Rachel Green",
"species": "Dog",
"visit_details": "Skin rash, antihistamines prescribed.",
"breed": "Bulldog Mix",
"vaccination_history": [
"Rabies",
"Parvovirus"
]
}
}
]
}混合查询 (/search/hybrid?q=<query_term>)
混合搜索允许我们将语义搜索和词法搜索结合起来,从而获得两全其美的效果:既能获得标记搜索的精确性,又能获得语义搜索的意义接近性。
app.get("/search/hybrid", async (req, res) => {
const { q } = req.query;
const INDEX_NAME = "vet-visits";
try {
const result = await esClient.search({
index: INDEX_NAME,
body: {
retriever: {
rrf: {
retrievers: [
{
standard: {
query: {
bool: {
must: {
multi_match: {
query: q,
fields: ["owner_name", "pet_name", "visit_details"],
},
},
},
},
},
},
{
standard: {
query: {
bool: {
must: {
semantic: {
field: "semantic_field",
query: q,
},
},
},
},
},
},
],
},
},
size: 5,
},
});
res.status(200).json({
success: true,
results: result.hits.hits,
});
} catch (error) {
console.error("Error performing search:", error);
res.status(500).json({
success: false,
results: null,
error: error.message,
});
}
});我们以 "谁做了修脚或牙科治疗?"
curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment响应:
{
"success": true,
"results": [
{
"_index": "vet-visits",
"_id": "9xY6RJYBLe2GoFQ6-9n9",
"_score": 0.032522473,
"_source": {
"pet_name": "Simba",
"owner_name": "Leo Martínez",
"species": "Cat",
"visit_details": "Dental cleaning. Minor tartar buildup removed.",
"breed": "Maine Coon",
"vaccination_history": [
"Rabies",
"Feline Panleukopenia"
]
}
},
{
"_index": "vet-visits",
"_id": "-RY6RJYBLe2GoFQ6-9n9",
"_score": 0.016393442,
"_source": {
"pet_name": "Mochi",
"owner_name": "Daniel Kim",
"species": "Rabbit",
"visit_details": "Nail trimming and general health check. No issues.",
"breed": "Mixed",
"vaccination_history": []
}
},
{
"_index": "vet-visits",
"_id": "8BY6RJYBLe2GoFQ6-9n9",
"_score": 0.015873017,
"_source": {
"pet_name": "Buddy",
"owner_name": "Alice Johnson",
"species": "Dog",
"visit_details": "Annual check-up and nail trimming. Healthy and active.",
"breed": "Golden Retriever",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Distemper"
]
}
},
{
"_index": "vet-visits",
"_id": "9RY6RJYBLe2GoFQ6-9n9",
"_score": 0.015625,
"_source": {
"pet_name": "Rex",
"owner_name": "Tomás Herrera",
"species": "Dog",
"visit_details": "Follow-up for previous leg strain, improving well.",
"breed": "German Shepherd",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Leptospirosis"
]
}
},
{
"_index": "vet-visits",
"_id": "8xY6RJYBLe2GoFQ6-9n9",
"_score": 0.015384615,
"_source": {
"pet_name": "Luna",
"owner_name": "Jake Thompson",
"species": "Dog",
"visit_details": "Mild ear infection, cleaning and antibiotics given.",
"breed": "Labrador Mix",
"vaccination_history": [
"Rabies",
"Bordetella"
]
}
}
]
}结论
在本系列的第一部分中,我们介绍了如何按照客户端/服务器最佳实践设置环境并创建带有不同搜索端点的服务器,以查询 Elasticsearch 文档。查看我们系列的第二部分,您将了解生产最佳实践以及如何在无服务器环境中运行 Elasticsearch Node.js 客户端。
常见问题
什么是 Node.js 客户端?
Node.js 客户端是一个 JavaScript 库,用于将 Elasticsearch API 的 HTTP REST 调用封装成 JavaScript 接口。它还提供了一些辅助方法,可简化批量索引文档等任务。
为何应使用服务器端的 Node.js 环境,而不是在前端直接调用 Elasticsearch?
安全性是其主要优势。在后端环境(例如使用 Express 的 Node.js)中运行客户端,可避免集群 API 密钥、端点 URL 及内部查询逻辑等敏感信息暴露在浏览器端。
在 Node.js 中使用 Elasticsearch “Bulk Helper” 有哪些优势?
在 Node.js 中使用 Elasticsearch “Bulk Helper” 有哪些优势? 批量索引:它自动处理按组而非逐个对文档进行索引的复杂性。 流式支持:借助 split2 等工具,您可以对 .ndjson 等文件进行流式读取并按行处理。。这样一来,无需将整个数据集加载到服务器内存即可处理海量文件。

