了解 Node.js 中的 Elasticsearch 查询主体构建器
作者:Alexander Nnakwue ✏️
介绍
Elasticsearch 查询主体构建器是一个查询 DSL(领域特定语言)或客户端,它在原始 Elasticsearch 查询之上提供了一个 API 层。它使全文搜索数据查询和复杂数据聚合更加便捷、轻松,并且在语法方面也更加简洁。
在本教程中,我们将学习如何使用构建器语法编写查询,从而获得比原始 Elasticsearch 查询更多的优势。这是因为原始查询很容易变得冗长、结构混乱、不符合惯用语法,甚至容易出错。
我们将利用查询构建器库elastic-builder来实现这一目标。根据其文档,它是一个用于快速构建复杂搜索查询和聚合请求体的工具。此外,它符合 Elasticsearch 原生查询的 API 规范标准,并且没有任何性能瓶颈。
简而言之,这意味着我们可以使用构建器语法编写查询,这些查询与 Elasticsearch 原生提供的查询等效。不用担心——我们将在本教程的后续步骤中学习并理解构建器语法。
首先,让我们来看一个简单的通用汽车查询示例,以了解为什么使用 ES 查询构建器可以更轻松地查询 Elasticsearch 数据,以及它如何有助于加快开发生命周期。
{
"query": {
"bool": {
"must": {
"match": {
"Origin": "USA"
}
},
"filter": {
"range": {
"Cylinders": {
"gte": 4,
"lte": 6
}
}
},
"must_not": {
"range": {
"Horsepower": {
"gte": 75
}
}
},
"should": {
"term": {
"Name": "ford"
}
}
}
}
}
综上所述,我们正在查询一辆产地为美国的汽车,并设置了以下筛选条件:发动机气缸数大于等于 4 或小于等于 6;马力范围不大于等于 75;最后,汽车名称必须为福特。
现在,编写这类查询存在以下问题:
- 他们过于啰嗦。
- 它们容易出现语法错误,这可能是由于字段嵌套不当造成的。
- 随着时间的推移,它们可能难以维护,甚至难以进行细微的增量更改。例如,知道在哪里添加另一个筛选器或查询字段可能会变得令人困惑。
- 对于开发团队的新成员来说,这些可能很难掌握。
- 对于更复杂的查询,编写它们既不有趣也不吸引人。
现在考虑使用构建器语法实现的等效查询,如下所示:
esb.requestBodySearch()
.query(
esb.boolQuery()
.must(esb.matchQuery('Origin', 'USA'))
.filter(esb.rangeQuery('Cylinders').gte(4).lte(6))
.should(esb.termQuery('Name', 'ford'))
.mustNot(esb.rangeQuery('Horsepower').gte(75))
)
上面的查询与我们之前审查的原始 ES 查询完全相同,而且正如我们所看到的,这更加直观和有意为之。
这里我们使用了requestBodySearchelastic-builder 的 API。这个 API 可以帮助我们构建查询语句,以流畅、自然、地道的方式清晰地表达和解释我们的意图。这些查询语句也非常易读,而且我们可以根据实际情况添加更多字段,从而获得完全不同的查询结果。
先决条件
为了更轻松地学习本教程,我建议您先阅读这篇关于 Elasticsearch 和 Node.js入门的教程。请注意,只有当您之前没有使用 Elasticsearch 的经验,或者想要复习一下相关知识时,才需要这样做。否则,您应该能够轻松地学习本教程。
首先,请确保您的计算机上已安装 Node.js 和 npm。此外,如果您打算在本地运行 Elasticsearch,我建议您下载并安装 Elasticsearch 二进制文件。不过,在本教程中,我们将使用 Elastic Cloud 来设置 Elasticsearch,您可以免费试用 Elastic Cloud 14 天。
完成所有设置后(例如选择云提供商和区域,因为它是托管服务),您应该会获得一个用户名(很可能是 `<username>` elastic)、一个 `<configuration>` password、一个 `<configuration>`host和一个 `<configuration>` port。请注意,我们稍后需要这些凭据或密钥才能连接到我们的 ES 集群。
虽然用户界面相当直观,但为了让您更直观地了解这些参数的位置,以下是一些截图,指出了您应该查看的位置。
第一张截图展示了 Elasticsearchuser以及我们可以在哪里找到password或生成新密码。第二张截图显示了一个链接,我们可以轻松复制密码elasticsearch endpoint url。完成这些设置后,我们应该就可以开始使用了,不过我们还打算探索堆栈中的其他 Elasticsearch 服务,例如 Kibana。
您可以查看更多关于 Kibana和整个 Elastic 技术栈的信息。接下来,让我们先明确一下我们将要构建的内容。
启动我们的应用程序
在本教程中,我们将构建几个 API 端点,以演示如何对存储在 Elasticsearch 集群中的数据执行全文搜索查询。当然,我们将使用构建器语法来构建查询,并将其与原始 ES 查询进行比较。
我们可以创建一个新的项目文件夹,并随意命名。和往常一样,在创建一个新的 Node.js 项目之前,我们需要npm init在项目目录下运行命令。这将创建一个新package.json文件。
接下来,我们可以安装应用程序依赖项。本项目所需的依赖项包括:官方的 Node.js Elasticsearch 客户端、elastic-builder 库、Express、body-parser 和 dotenv 包。
要安装它们,我们可以在终端/命令提示符中运行以下命令:
npm install @elastic/elasticsearch body-parser dotenv elastic-builder express --save
安装完成后,我们的package.json文件应该如下所示:
{
"name": "logrocket_elasticsearch_tutorial",
"version": "1.0.0",
"description": "LogRocket ElasticSearch Tutorial with ES Builder",
"main": "index.js",
"scripts": {
"start": "node ./app/server.js"
},
"author": "Alexander Nnakwue",
"license": "ISC",
"dependencies": {
"@elastic/elasticsearch": "^7.4.0",
"body-parser": "^1.19.0",
"dotenv": "^8.2.0",
"elastic-builder": "^2.4.0",
"express": "^4.17.1"
}
}
现在我们将创建所有必要的文件和文件夹。请注意,start脚本基于server.js文件的相对路径。首先,请确保您位于项目目录中,然后运行脚本mkdir app创建一个名为 `<filename>` 的新文件夹app。
创建app文件夹后,我们可以进入该文件夹并创建所有必要的文件,如下图所示。此外,我们还可以继续在项目根目录中创建所有其他文件,如下图所示。
下一步是建立与 Elasticsearch 集群的连接。为此,我们需要创建一个.env文件来存储所有环境变量或密钥。该sample.env文件的内容与我们的配置完全一致.env。文件内容如下:
ELASTICSEARCH_USERNAME=username
ELASTICSEARCH_PASSWORD=password
ELASTICSEARCH_HOST=host
ELASTICSEARCH_PORT=port
APP_PORT= 3004
ELASTICSEARCH_INDEX=index
ELASTICSEARCH_TYPE=type
我们可以复制这些参数,.env在项目根目录下创建一个文件,并填写真实的凭据。之后,我们就可以创建config.js文件了,该文件应该能够访问在我们新创建的.env文件中定义或添加的变量。
该config.js文件应包含以下JSON:
const result = require('dotenv').config();
module.exports= {
es_host: process.env.ELASTICSEARCH_HOST,
es_pass: process.env.ELASTICSEARCH_PASSWORD,
es_port: process.env.ELASTICSEARCH_PORT,
es_user:process.env.ELASTICSEARCH_USERNAME,
es_index:process.env.ELASTICSEARCH_INDEX,
es_type:process.env.ELASTICSEARCH_TYPE,
app_port: process.env.APP_PORT
};
if (result.error) {
console.log(result.error, "[Error Parsing env variables!]");
throw result.error;
};
// console.log(result.parsed, '[Parsed env variables!]');
如我们所见,我们能够访问.env文件中的变量,并使用不同的变量名存储它们。另请注意,我们还添加了Elasticsearch 连接所需的`<variables>` app_port、 `<variables> es_index`、 `<variables>` 和其他变量。es_type
现在,让我们使用这些参数连接到 Elasticsearch 集群。为此,我们可以将以下内容复制到esConfig.js文件中:
'use strict'
const { Client } = require('@elastic/elasticsearch');
const config = require('./config');
const client = new Client({ node: `https://${config.es_user}:${config.es_pass}@${config.es_host}:${config.es_port}`});
module.exports.esClient= client;
在这里,我们添加了对官方 Elasticsearch Node.js 客户端库的引用,然后使用我们config.js之前创建的文件中的内容来实例化一个新的 ES 客户端连接到我们的集群。
将数据写入我们的 ES 集群
现在集群已经搭建完毕,我们可以创建一个新文件,其中包含要写入 Elasticsearch 索引的 JSON 数据。dataToEs.json如果之前没有创建过该文件,现在可以创建它。该文件的内容来自GitHub 上的这个项目。它主要包含我们将根据连接到集群所需的参数写入 ES 索引的 JSON 数据集。
完成上述步骤后,我们可以创建一个utility.js文件,其中包含创建 ES 索引所需的函数;根据可用字段及其各自的数据类型为我们的数据集创建一个新的映射;最后,将 JSON 数据写入我们在集群上创建的索引。
请注意,Elasticsearch 默认是无模式的,但我们可以预先定义自己的模式,以便为数据定义标准结构和格式。当然,这样做也有其自身的优势,例如数据统一性等等。现在让我们来了解一下这个文件中的内容utility.js:
const fs = require('fs');
const esconfig = require('./esConfig');
const client = esconfig.esClient;
const data = JSON.parse(fs.readFileSync(__dirname + '/dataToEs.json'));
const config = require('./config');
const index= config.es_index;
const type = config.es_type;
async function writeCarDataToEs(index, data){
for (let i = 0; i < data.length; i++ ) {
await client.create({
refresh: true,
index: index,
id: i,
body: data[i]
}, function(error, response) {
if (error) {
console.error("Failed to import data", error);
return;
}
else {
console.log("Successfully imported data", data[i]);
}
});
}
};
async function createCarMapping (index, type) {
const carSchema = {
"Acceleration": {
"type": "long"
},
"Cylinders": {
"type": "long"
},
"Displacement": {
"type": "long"
},
"Horsepower": {
"type": "long"
},
"Miles_per_Gallon": {
"type": "long"
},
"Name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"Origin": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"Weight_in_lbs": {
"type": "long"
},
"Year": {
"type": "date"
}
}
return client.indices.putMapping({index, type, body:{properties:carSchema}});
}
module.exports = {
async resetIndex(){
if (client.indices.exists({ index })) {
client.indices.delete({ index });
}
client.indices.create({ index });
createCarMapping(client, index, type);
writeCarDataToEs(index, data);
}
};
在上面的文件中,我们首先动态读取dataToEs.json之前提到的文件中的 JSON 数据。如上所示,我们使用了 Node.js 的原生文件系统包。
我们还利用了__dirname获取当前模块目录名称的方法,并将数据集的相对文件路径附加到该目录名称后。此外,我们还导入了对 ES 客户端连接的引用。第一个函数writeCarDataToEs会遍历整个 JSON 数据集并将其写入 Elasticsearch 索引。
需要注意的是,对于非常大的数据集,我们应该使用 ES bulkAPI 而不是 GitHub createAPI。不过,就我们目前的使用场景而言,这种方法应该足够了。要了解如何使用 ES API,您可以查看此 GitHub 仓库bulk中提供的官方示例。
之后,我们就可以为数据创建映射,这些映射代表了预期的数据类型和格式。我们通过调用putMappingAPI 并传递索引、类型和 JSON 正文来实现这一点。
最后,我们创建了一个函数resetIndex,该函数会检查我们尝试创建的索引是否已存在,如果已存在,则将其删除。否则,我们会创建一个新索引,索引名称由我们从环境变量中传递而来;我们会为 JSON 数据集创建映射,然后调用该writeDataToEs()函数,该函数会根据已指定的映射将数据写入索引。
现在我们可以继续创建我们的server.js文件,它基本上是一个简单的 Express 服务器。
const express = require('express');
const bodyParser = require('body-parser')
require("dotenv").config();
require("./utility").resetIndex();
const app = express();
const esconfig = require('./esConfig');
const client = esconfig.esClient;
const router = require("./router");
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use("/",router);
app.set('port', process.env.APP_PORT || 3000);
client.ping({}, function(error)
{ if (error) {
console.log('ES Cluster is down', error);
} else {
console.log('ES Cluster is up!');
}
});
app.listen(app.get('port'), ()=>{
console.log(`Express server listening on port, ${app.get('port')}`);
} );
resetIndex()这里,我们从文件中导入函数utility.js,这样在启动应用时它就会自动运行。我们可以注释掉这条导入语句,因为后续应用重启时不需要它,毕竟索引、映射和数据应该已经在 ES 集群中创建并配置好了。
编写 elastic-builder 查询
现在我们可以开始编写数据查询了。首先,我们编写一个多重匹配查询,该查询匹配汽车的名称和产地,同时其重量大于或等于某个特定数值(rangeQuery)。我们可以查看service.js文件来了解此查询的工作原理:
async fetchMatchMultipleQuery(origin, name,weight){
const requestBody = esb.requestBodySearch()
.query(
esb.boolQuery()
.must([
esb.matchQuery(
'Origin', origin,
),
(
esb.matchQuery(
'Name', name,
)
),
])
.filter(esb.rangeQuery('Weight_in_lbs').gte(weight))
)
return client.search({index: index, body: requestBody.toJSON()});
}
通过上述函数,我们可以很清楚地了解我们的目标。该查询是一个布尔查询,要求匹配来自特定产地且名称特定的车辆。此外,我们还使用范围查询来筛选车辆,要求车辆重量必须大于或等于我们指定的重量值。
顺便一提,我们来看一下上述查询的等效原始查询:
{
"bool": {
"must": [
{
"match": {
"Origin": "https://elastic-builder.js.org"
}
},
{
"match": {
"Name": "name"
}
}
],
"filter": {
"range": {
"Weight_in_lbs": {
"gte": "weight"
}
}
}
}
}
正如我们所见,由于查询嵌套层级很深(我们之前已经指出过),这种方法很容易出错。现在我们有了直观的了解,接下来让我们了解实际调用此 API 的流程。
首先,请查看该services.js文件。该文件处理所有与使用构建器语法构建查询以及调用 ES 客户端实际执行这些查询相关的操作。此外,在该文件中,我们还可以找到上面提到的相同函数。
该controller.js文件负责根据routes.js文件中指定的应用程序路由来路由我们的请求。当请求被路由时,该controller.js文件中的函数会调用其他services.js文件中的函数。
我们用一个简单的例子来说明这一点。对于上面定义的查询,controller文件中对应的调用如下所示:
async fetchMatchMultipleQuery(req,res) {
const origin = req.query.Origin;
const name = req.query.Name;
const weight = req.query.Weight_in_lbs;
try {
const result = await Services.fetchMatchMultipleQuery(origin, name, weight);
const data = result.body.hits.hits.map((car)=>{
return {
id: car._id,
data: car._source
}
})
res.json({status_code: 200, success: true, data: data, messsage: "fetch match query for multiple requests successful!" });
} catch (err) {
res.json({status_code: 500, success: false, data: [], message: err});
}
}
随后,该调用的路由信息包含在以下routes.js文件中:
routes.route("/search-by-multiple").get(controller.fetchMatchMultipleQuery);
测试我们的实现
现在我们可以开始测试我们的实现。首先,让我们运行命令启动服务器npm start。然后我们可以访问此 URL,使用提供的过滤器运行查询:name,,origin和weight_in_lbs。
http://localhost:3000/search-by-multiple?Name=ford&Origin=USA&Weight_in_lbs=3000
请注意,上述请求是一个GET请求,URL 后面的参数是查询参数,用于获取我们所需的筛选结果。API 调用的结果如下所示:
{
"status_code": 200,
"success": true,
"data": [
{
"id": "221",
"data": {
"Name": "ford f108",
"Miles_per_Gallon": 13,
"Cylinders": 8,
"Displacement": 302,
"Horsepower": 130,
"Weight_in_lbs": 3870,
"Acceleration": 15,
"Year": "1976-01-01",
"Origin": "USA"
}
},
{
"id": "99",
"data": {
"Name": "ford ltd",
"Miles_per_Gallon": 13,
"Cylinders": 8,
"Displacement": 351,
"Horsepower": 158,
"Weight_in_lbs": 4363,
"Acceleration": 13,
"Year": "1973-01-01",
"Origin": "USA"
}
},
{
"id": "235",
"data": {
"Name": "ford granada",
"Miles_per_Gallon": 18.5,
"Cylinders": 6,
"Displacement": 250,
"Horsepower": 98,
"Weight_in_lbs": 3525,
"Acceleration": 19,
"Year": "1977-01-01",
"Origin": "USA"
}
},
{
"id": "31",
"data": {
"Name": "ford f250",
"Miles_per_Gallon": 10,
"Cylinders": 8,
"Displacement": 360,
"Horsepower": 215,
"Weight_in_lbs": 4615,
"Acceleration": 14,
"Year": "1970-01-01",
"Origin": "USA"
}
},
"messsage": "fetch match query for multiple requests successful!"
}
请注意,为了简洁起见,上述查询结果已被截断。在本地运行此查询时,您应该会得到完整的结果。不用担心,POSTMAN 上的集合链接在此。您可以复制链接,将其导入到您的 POSTMAN 环境中进行测试。
下面显示了该文件的全部代码services.js,其中包含对集群中数据发出的所有查询:
const esconfig = require('./esConfig');
const client = esconfig.esClient;
const config = require('./config');
const index = config.es_index;
const esb = require('elastic-builder'); //the builder
module.exports = {
async search(){
const requestBody = esb.requestBodySearch()
.query(esb.matchAllQuery())
.size(10)
.from(1);
return client.search({index: index, body: requestBody.toJSON()});
},
async filterCarsByYearMade(param) {
const requestBody = esb.requestBodySearch()
.query(
esb.boolQuery()
.must(esb.matchAllQuery())
.filter(esb.rangeQuery('Year').gte(param).lte(param))
)
.from(1)
.size(5);
return client.search({index: index, body: requestBody.toJSON()});
},
async filterCarsByName(param) {
const requestBody = esb.requestBodySearch()
.query(
esb.termQuery('Name', param))
.sort(esb.sort('Year', 'asc')
)
.from(1)
.size(10);
return client.search({index: index, body: requestBody.toJSON()});
},
async fetchCarByName(param) {
const requestBody = esb.requestBodySearch()
.query(
esb.boolQuery()
.must(esb.matchPhraseQuery('Name', param))
);
return client.search({index: index, body: requestBody.toJSON()});
},
async fetchMatchMultipleQuery(origin, name,weight){
const requestBody = esb.requestBodySearch()
.query(
esb.boolQuery()
.must([
esb.matchQuery(
'Origin', origin,
),
(
esb.matchQuery(
'Name', name,
)
),
])
.filter(esb.rangeQuery('Weight_in_lbs').gte(weight))
)
return client.search({index: index, body: requestBody.toJSON()});
},
async aggregateQuery(origin,cylinder,name,horsePower) {
const requestBody = esb.requestBodySearch()
.query(
esb.boolQuery()
.must(esb.matchQuery('Origin', origin))
.filter(esb.rangeQuery('Cylinders').gte(cylinder))
.should(esb.termQuery('Name', name))
.mustNot(esb.rangeQuery('Horsepower').gte(horsePower))
// .agg(esb.avgAggregation('avg_miles', 'Miles_per_Gallon'))
)
return client.search({index: index, body: requestBody.toJSON()});
},
};
如上所示,这些查询语句清晰易懂。我们使用了构建器库提供的matchQuery ` <query>` rangeQuery、termQuery`<query>`、matchPhraseQuery`<query>`、`<query> boolQuery` 和 `<query>`matchAllQuery查询语句。如需了解其他可用查询语句及其使用方法,请参阅elastic-builder 文档的查询部分。
sort顾名思义,该命令会根据查询结果的顺序(升序或降序)进行排序。`and` 参数from则size有助于通过分页显示返回结果来控制数据的输出。
此外,该文件的代码controller.js如下所示:
const Services = require('./services');
module.exports = {
async search(req, res) {
try {
const result = await Services.search();
const data = result.body.hits.hits.map((car)=>{
return {
id: car._id,
data: car._source
}
})
res.json({ status_code: 200, success: true, data: data, message: "Cars data successfully fetched!" });
} catch (err) {
res.json({ status_code: 500, success: false, data: [], message: err});
}
},
async filterCarsByYearMade(req, res) {
let {year} = req.query;
try {
const result = await Services.filterCarsByYearMade(year);
const data = result.body.hits.hits.map((car)=>{
return {
id: car._id,
data: car._source
}
})
res.json({ status_code: 200, success: true, data: data, message: "Filter Cars by year made data fetched successfully" });
} catch (err) {
res.json({ status_code: 500, success: false, data: [], message: err});
}
},
async filterCarsByName(req,res) {
let param = req.query.Name;
try {
const result = await Services.filterCarsByName(param);
const data = result.body.hits.hits.map((car)=>{
return {
id: car._id,
data: car._source
}
})
res.json({status_code: 200, success: true, data:data , message: "Filter cars by name data fetched successfully!" });
} catch (err) {
res.json({ status_code: 500, success: false, data: [], message: err});
}
},
async filterCarByName(req,res) {
const param = req.query.Name;
try {
const result = await Services.fetchCarByName(param);
const data = result.body.hits.hits.map((car)=>{
return {
id: car._id,
data: car._source
}
})
res.json({ status_code: 200, success: true, data: data , message: "Filter a car by name query data fetched successfully!"});
} catch (err) {
res.json({ status_code: 500, success: false, data: [], message: err});
}
},
async fetchMatchMultipleQuery(req,res) {
const origin = req.query.Origin;
const name = req.query.Name;
const weight = req.query.Weight_in_lbs;
try {
const result = await Services.fetchMatchMultipleQuery(origin, name, weight);
const data = result.body.hits.hits.map((car)=>{
return {
id: car._id,
data: car._source
}
})
res.json({status_code: 200, success: true, data: data, messsage: "fetch match query for multiple requests successful!" });
} catch (err) {
res.json({status_code: 500, success: false, data: [], message: err});
}
},
async aggregateQuery(req,res) {
const origin = req.query.Origin;
const cylinder = req.query.Cylinder;
const name = req.query.Name;
const horsePower = req.query.Horsepower;
try {
const result = await Services.aggregateQuery(origin, cylinder, name, horsePower);
const data = result.body.hits.hits.map((car)=>{
return {
id: car._id,
data: car._source
}
})
res.json({ status_code: 200, success: true, data: data, message: "Data successfully fetched!" });
} catch (err) {
res.json({ status_code: 500, success: false, data: [], message: err});
}
},
}
上述文件包含调用我们文件并帮助路由请求的代码services.js。如我们所见,对于上面的每个查询,我们都对返回的数据进行映射,并仅输出 `and`id和 ` _sourcefields` 字段。
routes.js下面显示了文件中所有查询的路由:
const express = require("express");
const controller = require("./controller");
const routes = express.Router();
routes.route("/search-all").get(controller.search);
routes.route("/search-by-year").get(controller.filterCarsByYearMade);
routes.route("/search-by-name").get(controller.filterCarsByName);
routes.route("/search-by-name-single").get(controller.filterCarByName);
routes.route("/search-by-multiple").get(controller.fetchMatchMultipleQuery);
routes.route("/seach-avg-query").get(controller.aggregateQuery);
module.exports = routes;
此文件用于调用和路由文件中提供的所有函数controller.js。请注意,此项目的全部代码都可以在 GitHub 上找到。
结论
如果要在搜索密集型应用中实现数据聚合、指标分析、复杂过滤和全文搜索功能,Elasticsearch 是必不可少的。更棒的是,我们能够更进一步,在利用 Node.js 和 Elasticsearch 构建自定义搜索引擎的基础上,通过构建器语法扩展查询,从而为数据集编写更高级的查询和过滤规则。
虽然市面上还有其他构建器库,但 elastic-builder 非常可靠、稳定,并且语法清晰易读,易于理解。
如果您对本文有任何疑问、反馈或评论,请随时在下方评论区留言或通过Twitter联系我。感谢您的阅读!
编者按:发现本文有误?您可以在这里找到正确版本。
插件:LogRocket,一款用于 Web 应用的 DVR

LogRocket是一款前端日志工具,可让您重现问题,如同在您自己的浏览器中发生一样。无需猜测错误原因,也无需用户提供屏幕截图和日志转储,LogRocket 即可让您重现会话,快速了解问题所在。它与任何框架的应用程序完美兼容,并提供插件来记录来自 Redux、Vuex 和 @ngrx/store 的额外上下文信息。
除了记录 Redux 操作和状态之外,LogRocket 还会记录控制台日志、JavaScript 错误、堆栈跟踪、包含标头和正文的网络请求/响应、浏览器元数据以及自定义日志。它还会对 DOM 进行插桩,记录页面上的 HTML 和 CSS,即使是最复杂的单页应用程序,也能生成像素级精确的视频。
免费试用。
本文《了解 Node.js 中的 Elasticsearch 查询主体构建器》最初发表于LogRocket 博客。
文章来源:https://dev.to/bnevilleoneill/understanding-elasticsearch-query-body-builder-in-node-js-2jfl

