人们说数据是新的黄金。但要在短时间内通过庞大的数据集来满足消费者的需求,对于后端开发人员来说仍然是一个难题。
传统的数据库查询方法往往无法快速获得准确的搜索结果。不过幸运的是,Elasticsearch为这个问题提供了解决方案。
在这篇文章中,我将向您介绍如何使用Elasticsearch来提升数据库搜索和分析的功能,同时还能保持效率。
进行本教程学习之前,需要准备以下条件:
-
一个Node.js开发环境
-
基本的后端开发知识
好了,让我们开始吧。但首先,什么是Elasticsearch呢?
目录
什么是Elasticsearch?
Elasticsearch是由Apache开发的搜索引擎,它能够对单词和短语进行索引处理,从而提供先进的文本搜索及向量搜索功能。此外,它还具备搜索分析以及自动补全等功能。
需要注意的是,尽管Elasticsearch提供了索引功能,但它本身并不属于数据库——这一点与常见的数据库是相同的。
在实际情况中,还有其他一些流行的替代工具可供选择,例如Algolia、OpenSearch以及MeiliSearch等。
Elasticsearch核心术语
在这一节中,我们将介绍一些在Elasticsearch中常用的术语。为了帮助您更好地理解这些概念,我会引用一些常见的数据库相关术语来进行说明。
-
索引:索引是用于存储数据的区域,它可以被视为Elasticsearch中的“数据库”。与传统的数据库一样,索引也具有唯一性等特性。
-
文档:文档是索引中存储的最小信息单位。它的结构与基于MongoDB的文档相同,也类似于SQL数据库中的行。
-
得分:Elasticsearch会生成得分值,用来表示搜索查询与存储在索引中的数据之间的相关性程度。
-
分词工具:这些工具会将输入到Elasticsearch引擎中的非结构化数据转换成结构化的数据格式,以便进一步进行处理和存储。
-
过滤器:过滤器是一组用于修改分词结果指令,这些指令可能包括删除填充字符、调整大小写等操作。
-
批量索引:批量索引是指一次性对多个文档进行索引处理。当需要对已经存在内容的数据库进行索引时,通常会使用这种方式。
映射规则:映射规则定义了文档和字段在Elasticsearch索引中的存储方式。
分析器:当数据被发送到Elasticsearch引擎进行索引处理时,首先会经过分析器的处理。这一过程通过过滤器和支持分词的工具来实现。
聚合器:聚合器能够对索引中存储的数据进行深入分析,从而生成有用的数据洞察。这是Elasticsearch引擎的一大优势,MongoDB的聚合器也提供了类似的功能。
如何设置Elasticsearch
在本教程中,我们将在本地机器上使用Elasticsearch的可安装软件。当然,也存在在线托管版本的Elasticsearch,使用它们也同样非常方便。
这里提供了关于如何在Windows系统上设置Elasticsearch的详细说明。对于非Windows用户来说,也可以在Linux/Mac OS系统中安装Elasticsearch,或者使用Docker来部署它。
注意:对于Windows用户来说,请确保以管理员身份运行Elasticsearch程序,这样才能避免安装过程中出现错误。
安装成功后,你可以通过访问localhost:9200来测试Elasticsearch是否能够正常工作。这个地址是Elasticsearch的默认本地端点。此时你会在屏幕上看到类似下图所示的成功提示信息:

完成这些步骤后,我们就可以继续设置我们的项目,并将ElasticSearch集成到演示项目中去了。
如何设置演示项目
为了方便进行本教程的学习,我们将使用一个用Node Express JS构建的、已经开发完成的论坛后端应用程序。项目的链接如下。
要启动并运行这个项目,请克隆相应的代码包,然后运行
npm start
在本教程中,MySQL将被作为默认数据库使用。接下来我们进入下一节内容吧。
如何在你的项目中设置Elasticsearch
现有的演示项目是一个论坛后端应用,它允许用户发布文本内容,并通过分类主题来开展讨论。
Elasticsearch能够帮助用户快速筛选这些帖子和讨论主题,从而利用特定的关键词准确找到所需信息。这种方式比使用传统的数据库搜索查询更为高效,因为后者往往操作起来比较繁琐。
要设置Elasticsearch,请首先安装npm包中的Elasticsearch插件。具体操作方法是在你的项目目录中运行以下命令:
npm install @elastic/elasticsearch
安装成功后,创建一个config.js文件,在其中配置用于连接Elasticsearch应用的各项参数。
const { Client } = require('@elastic/elasticsearch');
const esClient = new Client({
node: 'http://localhost:9200',
auth: {
username: process.env.ELASTICSEARCH_USERNAME,
password: process.env.ELASTICSEARCH_PASSWORD
},
maxRetries: 5,
requestTimeout: 60000,
tls: {
rejectUnauthorized: process.env.NODE_ENV !== 'development'
}
});
module.exports = esClient;
要在后端应用程序中访问并使用Elasticsearch的功能,您需要设置和配置相应的驱动程序。具体细节在上述配置文件中有所说明。
如前所述,Elasticsearch运行在localhost:9200端口上。因此,您的Elasticsearch节点会连接到这个本地端口。在线托管的Elasticsearch节点在类似情况下也能正常工作。
在配置文件中,您还需要提供访问Elasticsearch所需的认证信息。用户名和密码需要通过Auth对象进行设置。如果您是在本地运行Elasticsearch,除非启用了安全功能,否则可能不需要进行认证。
在这里,MaxRetries表示尝试访问Elasticsearch时允许的最大失败次数。我们将其设置为5次。而requestTimeout则表示如果请求在指定时间内未被处理,系统将自动终止该请求所花费的时间(单位为毫秒)。
配置文件设置完成后,在后端应用程序启动时,您需要导入这些配置信息并初始化Elasticsearch客户端。
如何在Elasticsearch中操作索引
在开始充分利用Elasticsearch的功能之前,我们首先需要在项目后端对其进行定制,以便使其能够满足我们的需求。这包括在Elasticsearch引擎中创建一个索引,用于存储所有提交到后端应用程序的数据。
const esClient = require('./config');
const setupIndex = async () => {
try {
const indexExists = await esClient.indices.exists({
index: INDEX_NAME
});
if (indexExists) {
console.log(`索引 "${INDEX_NAME}" 已经存在`);
return;
}
await esClientindices.create({
index: INDEX_NAME,
...indexMapping
});
console.log(`索引 "${INDEX_NAME}" 已创建`);
} catch (err) {
console.error(err);
throw err;
}
};
上述代码演示了如何创建一个新的索引。首先,需要调用setupIndex()函数,在该函数中指定索引的名称。Elasticsearch会检查该名称是否已经存在。
如果索引名称已经存在,函数会终止执行(以避免重复创建相同的索引);但如果名称不存在,系统就会使用该名称创建一个新的索引,并同时设置相应的索引映射规则(我们稍后会进一步讨论这些规则)。
成功创建索引后,您会在应用程序的控制台中看到相应的成功提示信息。
如何删除索引
过了一段时间后,某个索引可能就不再需要了,这时您就可以将其从Elasticsearch中删除。
const deleteIndex = async () => {
try {
await esClientindices.delete({ index: INDEX_NAME });
console.log(`${INDEX_NAME} 已被删除`);
} catch (err) {
console.error("删除索引时出现错误:", err);
}
};
如何删除索引中的帖子
有时,帖子会被删除或修改。此外,用户也可能会被封禁,在这种情况下,您就需要将他们的内容从存储的数据库中移除。
在这种情况下,您需要确保这些内容真正被删除——也就是说,既要从数据库中删除,也要从Elasticsearch的索引中删除。
const deletePost = async (postId) => {
try {
await esClient.delete({
index: INDEX_NAME,
id: postId.toString(),
});
console.log("帖子已成功删除");
return { success: true, postId };
} catch (err) {
console.error(err);
throw err;
}
};
如何为帖子创建索引
在设置好了Elasticsearch索引之后,您就需要将添加到数据库中的帖子自动纳入该索引中。
const transformPostToESDoc = (post) => {
return {
id: post.id,
title: post.title,
content: post.body,
author: post.author,
category: post.category,
tags: post.tags,
views: post.views || 0,
published_at: post.created_at
};
const indexPost = async (postId) => {
try {
const postRepo = await getPostRepo();
const post = await postRepo.findOne({ where: { id: postId } });
if (!post) {
throw new Error("帖子不存在");
}
const esDocument = transformPostToESDoc(post);
await esClient.index({
index: INDEX_NAME,
id: post.id.toString(),
document: esDocument
});
console.log("帖子已成功创建索引");
return { success: true, postId };
} catch (err) {
console.error(err);
throw err;
}
};
要被创建索引的帖子必须具有唯一的ID。为了方便使用,我们采用了常规数据库中默认存在的唯一标识机制;当然,您也可以使用UUID库来生成唯一的帖子ID。
const indexPost = async (postId) => {
// ... 其他代码 ...
const transformPostToESDoc = (post) => {
// ... 其他代码 ...
随后,这些帖子信息会被作为要被索引的文档传递给esClient.index()函数。同时,我们还设置了相应的错误处理机制,以防止在操作失败时导致应用程序崩溃。
如何定义Elasticsearch映射规则
Elasticsearch的映射规则决定了数据是如何被存储和索引的。这些规则指定了每个字段的数据类型,同时也规定了文本在搜索时应该如何被分析处理。
在下面的示例中,我们将定义一种索引配置方案,该配置包括用于自动完成的自定义分析器,以及针对每个帖子字段(如标题、内容和作者)所设置的映射规则。const indexMapping = {
settings: {
analysis: {
analyzer: {
autocomplete: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase', 'autocomplete_filter']
},
autocomplete_search: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase']
}
},
filter: {
autocomplete_filter: {
type: 'edge_ngram',
min_gram: 2,
max_gram: 10
}
}
}
},
mappings: {
properties: {
id: { type: 'integer' },
title: {
type: 'text',
analyzer: 'autocomplete',
search_analyzer: 'autocomplete_search',
fields: {
keyword: { type: 'keyword' },
standard: { type: 'text' }
}
},
content: {
type: 'text',
analyzer: 'standard'
},
category: {
type: 'keyword'
},
tags: { type: 'keyword' },
author: {
type: 'text',
fields: {
keyword: { type: 'keyword' }
}
},
views: { type: 'integer' },
published_at: { type: 'date' }
}
}
};
indexMapping对象定义了Elasticsearch应如何存储和处理您的数据。它由两个主要部分组成:settings和mappings。
mappings部分定义了您的文档结构。每个字段(如title、content或author)都具有某种类型,例如text、keyword、integer或date。这些类型告诉Elasticsearch如何存储和搜索这些字段。
对于文本字段,我们还可以定义分析器。分析器决定了在索引和搜索过程中文本是如何被分解成更小片段(即词元)的。
在settings部分,我们为自动完成功能定义了一个自定义分析器。该分析器使用edge_ngram过滤器来生成部分匹配的结果,这样用户就可以在输入内容的过程中实时看到搜索结果。我们还定义了另一个search_analyzer,以确保搜索查询能够被正确处理。
综上所述,这些设置使您能够在保持搜索结果准确性和高效性的同时,实现自动完成功能等其他特性。
搜索功能的实现
为了实现您的搜索功能,您需要构建相应的API。这包括开发业务逻辑服务以及定义API路由。您还需要使用GET请求,并将搜索词作为查询参数传递;系统返回的结果将以JSON格式呈现。
接下来,您需要实现搜索结果展示的相关功能。在这种情况下,您会利用搜索引擎的功能在索引中查找指定的短语。为了减少接收不必要的信息,建议您采用分页技术来处理搜索结果。搜索查询将由索引名称、用于控制返回哪些结果的分页参数(from和size),以及预期结果的最大规模组成。此外,您还需要附加一个查询对象,该对象会指定Elasticsearch引擎应使用的搜索方式。
const searchElastic = async (query, page = 1, size = 10) => {
const searchQuery = {
index: INDEX_NAME,
from: (page - 1) * size,
size,
query: {
bool: {
must: [
{
multi_match: {
query,
fields: ["title^3", "content"],
type: "best_fields",
fuzziness: "AUTO"
}
}
]
}
}
};
const result = await esClient.search(searchQuery);
return result.hitshits;
};
在上面的代码中,这个函数的名称是searchElastic。要执行这个函数,需要传递三个参数:size、page和query。
size参数指定了每次搜索时要返回的最大文档数量。默认值可以是任何整数。
查询中使用multi_match子句来同时在多个字段中进行搜索,例如title和content。title^3这种写法会优先匹配标题字段中的内容,使得这些匹配结果比其他字段的匹配结果更具相关性。
我们还添加了一个must子句,用于定义文档必须满足的条件才能被纳入搜索结果中。
搜索结果通常会根据它们与查询内容的关联程度来进行排序。
完整代码
通过以上步骤,你已经完成了本教程的学习,并配置好了Elasticsearch,使其能够对你数据库中的帖子进行索引。以下是完整的代码:
- Elasticsearch客户端(config.js):
const { Client } = require('@elastic/elasticsearch');
const esClient = new Client({
node: 'http://localhost:9200',
auth: {
username: process.env.ELASTICSEARCH_USERNAME,
password: process.env.ELASTICSEARCH_PASSWORD
},
maxRetries: 5,
requestTimeout: 60000,
tls: {
rejectUnauthorized: process.env.NODE_ENV !== 'development'
}
});
module.exports = esClient;
- 索引映射配置:
const indexMapping = {
settings: {
analysis: {
analyzer: {
autocomplete: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase', 'autocomplete_filter']
},
autocomplete_search: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase']
}
},
filter: {
autocomplete_filter: {
type: 'edge_ngram',
min_gram: 2,
max_gram: 10
}
}
}
},
mappings: {
properties: {
id: { type: 'integer' },
title: {
type: 'text',
analyzer: 'autocomplete',
search_analyzer: 'autocomplete_search',
fields: {
keyword: { type: 'keyword' },
standard: { type: 'text' }
}
},
content: {
type: 'text',
analyzer: 'standard'
},
category: {
type: 'keyword'
},
tags: { type: 'keyword' },
author: {
type: 'text',
fields: {
keyword: { type: 'keyword' }
}
},
views: { type: 'integer' },
published_at: { type: 'date' }
}
}
};
- 创建索引:
const setupIndex = async () => {
try {
const indexExists = await esClient.indices.exists({
index: INDEX_NAME
});
if (indexExists) {
console.log(`索引 "${INDEX_NAME}" 已经存在`);
return;
}
await esClientindices.create({
index: INDEX_NAME,
...indexMapping
});
console.log(`索引 "${INDEX_NAME}" 已创建`);
} catch (err) {
console.error(err);
throw err;
}
};
- 删除索引:
const deleteIndex = async () => {
try {
await esClient.indices.delete({ index: INDEX_NAME });
console.log(`${INDEX_NAME} 已被删除");
} catch (err) {
console.error("删除索引时出现错误:", err);
}
};
- 删除文档:
const deletePost = async (postId) => {
try {
await esClient.delete({
index: INDEX_NAME,
id: postId.toString()
});
console.log("文档已成功删除");
return { success: true, postId };
} catch (err) {
console.error(err);
throw err;
}
};
- 转换并索引文档:
const transformPostToESDoc = (post) => {
return {
id: post.id,
title: post.title,
content: post.body,
author: post.author,
category: post.category,
tags: post.tags,
views: post.views || 0,
published_at: post.created_at
};
const indexPost = async (postId) => {
try {
const postRepo = await getPostRepo();
const post = await postRepo.findOne({ where: { id: postId } });
if (!post) {
throw new Error("文档不存在");
}
const esDocument = transformPostToESDoc(post);
await esClient.index({
index: INDEX_NAME,
id: post.id.toString(),
document: esDocument
});
console.log("文档已成功索引");
return { success: true,postId };
} catch (err) {
console.error(err);
throw err;
}
};
- 搜索功能:
const searchElastic = async (query, page = 1, size = 10) => {
const searchQuery = {
index: INDEX_NAME,
from: (page - 1) * size,
size,
query: {
bool: {
must: [
{
multi_match: {
query,
fields: ["title^3", "content"],
type: "best_fields",
fuzziness: "AUTO"
}
}
]
}
}
};
const result = await esClient.search(searchQuery);
return result.hitshits;
};
总结
现在您已经了解了如何使用Elasticsearch来提升您的Web应用程序中的搜索功能。Elasticsearch具有很强的通用性,因此您可以将其应用于各种编程语言和框架中。此外,它拥有庞大的社区支持,这些社区资源提供了许多有用的用户指南,从而帮助您更轻松地开始使用Elasticsearch。
要想进一步发挥Elasticsearch的强大功能,你可以探索ELK技术栈中的其他工具(包括Elasticsearch、Log Stash和Kibana),这些工具能够帮助你为数据生成高质量的数据可视化报表,尤其是对于企业级应用而言。
结论
在当今的Web应用中,一个快速且可靠的搜索引擎是必不可少的。Elasticsearch正是实现这一目标的理想选择。
如果你想阅读更多有助于提升你的技术水平的文章,欢迎访问我的网站。继续努力学习吧!