Elasticsearch 入门指南:从零到一构建搜索引擎
在成功部署了 Elasticsearch 和 Kibana 后,如果还有不会安装的可以查看这篇文章。学会安装仅仅只是一个开始,真正的魔力在于如何使用这个工具,本文将深入实践,专注实现两大目标:为一个论坛项目构建搜索引擎
本文将通过大量的实例,讲解 Elasticsearch 的核心概念和操作方法,所有示例都可以在 Kibana 的开发工具中直接运行
理解 Elasticsearch 的核心概念
在开始实践之前,理解这三个核心概念至关重要:
- 索引 (Index)
- 可以理解为一个数据表(Table)。例如,可以创建一个名为 blog_posts 的索引来存放所有的博客文章,再创建一个名为 logs-nginx-prod 的索引来存放所有的 Nginx 日志。
- 文档 (Document)
- 可以理解为数据库中的一行记录。一个文档就是一条 JSON 格式的数据。例如,一篇博客文章(包含标题、内容、作者等信息)就是一个文档,一条日志记录也是一个文档。
- 映射 (Mapping)
- 可以理解为数据库中的表结构 (Schema)。映射定义了索引中文档的字段及其类型。例如,title 字段是可分词的文本 (text),author_id 字段是精确值 (keyword),publish_date 字段是日期 (date)。正确的映射是实现高效、准确搜索的基础
构建搜索引擎
这里是将 Elasticsearch 集成到项目中的核心环节
0. 安装 IK 分词器插件
为 Elasticsearch 安装中文分词器是实现优秀中文搜索效果最关键的一步。在众多中文分词器中,IK Analyzer 是最流行、最经典的选择之一。
现在安装 IK 分词器只要一个命令就可以完成插件的直接下载和安装(最后的版本和你的 Elasticsearch 保持一致):
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/8.8.1
之后重启 Elasticsearch 完成插件的安装:
sudo systemctl restart elasticsearch
IK 分词器主要提供了两种分词模式:
ik_smart
:智能分词。它会做最粗粒度的拆分,适合“词语”搜索。ik_max_word
:最细粒度分词。它会穷尽所有可能的词语组合,适合“全文”搜索,索引时使用可以提高召回率。
1. 设计文章索引(创建 Mapping)
在 Kibana 开发工具中运行以下命令,创建一个名为 blog_posts 的索引:
PUT /blog_posts
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": {
"analyzer": {
"default": {
"type": "ik_max_word"
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"author_name": {
"type": "keyword"
},
"tags": {
"type": "keyword"
},
"publish_date": {
"type": "date"
},
"view_count": {
"type": "integer"
}
}
}
}
解读:
- settings: 我们在这里将默认的中文分词器设置为 ik_max_word。
- title 和 content: 设置为 text 类型,表示这两个字段需要被分词以支持全文搜索。
- author_name 和 tags: 设置为 keyword 类型,表示它们是精确值,用于过滤、排序和聚合(例如,查找某位作者的所有文章,或统计某个标签下的文章数)。
- publish_date: 设置为 date 类型,用于按时间范围搜索或排序。
- view_count: 设置为 integer 类型,用于按浏览量排序。
2. 索引数据(添加、查看、更新、删除文档)
现在,我们可以向这个索引中添加数据了
- 添加一篇新文章(Create)
PUT /blog_posts/_doc/1 //PUT 需要指定 ID
{
"title": "我的第一篇 Elasticsearch 博客",
"content": "Elasticsearch 是一个功能强大的分布式搜索引擎,非常适合用于全文搜索和日志分析。",
"author_name": "张三",
"tags": ["Elasticsearch", "新手入门"],
"publish_date": "2025-07-05T12:00:00Z",
"view_count": 100
}
POST /blog_posts/_doc //POST 不需要指定 ID
{
"title": "我的第二篇博客,关于Kibana",
"content": "Kibana 是一个用于数据可视化的强大工具...",
"author_name": "李四",
"tags": ["Kibana", "数据可视化"],
"publish_date": "2025-07-06T14:00:00Z",
"view_count": 50
}
PUT 与 POST 之间的对比
方法 (Method) | URL 格式 (Format) | ID 由谁提供? | 行为 (Behavior) | 适用场景 |
---|---|---|---|---|
PUT |
/<index>/_doc/{ID} |
用户指定 | 创建或完全覆盖 | 有外部唯一ID,需要幂等性操作 |
POST |
/<index>/_doc |
ES自动生成 | 总是创建新文档 | 无唯一ID的数据,如日志、事件 |
POST (_create ) |
/<index>/_create/{ID} |
用户指定 | 如果ID已存在则失败 | 防止意外覆盖,确保只创建 |
- 查看这篇文章(Read)
GET /blog_posts/_doc/1
- 更新这篇文章(Update)
POST /blog_posts/_update/1
{
"doc": {
"view_count": 101
}
}
- 删除这篇文章(Delete)
DELETE /blog_posts/_doc/1
3. 执行搜索(Query DSL)
这是 Elasticsearch 最强大的部分。所有的搜索请求通过一种名为 Query DSL 的JSON的结构来定义
3.1. 核心概念
3.1.1 查询上下文 vs 过滤上下文
这是理解 Query DSL 的第一性原理
- 查询上下文 (Query Context)
- 核心任务: 回答 “这份文档有多匹配这个查询条件?”
- 工作机制: 它会进行复杂的计算,为每个匹配的文档生成一个相关性得分 (_score)。得分越高,意味着文档与查询越相关,排名越靠前。
- 适用场景: 所有需要评估“相关度”的场景,尤其是全文搜索。例如,搜索博客标题、内容、商品描述等。
- 典型子句: match, multi_match, query_string 等。
- 过滤上下文 (Filter Context)
- 核心任务: 回答 “这份文档是否匹配这个查询条件?” (一个简单的“是”或“否”)
- 工作机制: 它只做精确的匹配判断,不计算任何相关性得分。因此,它的性能远高于查询上下文,并且其结果可以被 Elasticsearch 高效地缓存起来,供后续查询复用。
- 适用场景: 所有“是/否”判断的场景。例如,“状态是否为‘已发布’?”、“标签是否精确等于‘技术’?”、“价格是否在 100 到 200 之间?”。
- 典型子句: term, terms, range, exists 等,它们必须被包裹在 bool 查询的 filter 子句中。
黄金法则: 只要一个查询条件不需要影响相关性得分,就永远、必须将它放在过滤上下文中。这是优化查询性能的首要原则。
3.1.2 相关性得分(_score)
_score 是 Elasticsearch 用来衡量文档与查询相关程度的浮点数。它的计算基于复杂的文本相关性算法(早期是 TF-IDF,现在主要是 BM25)。这里是影响得分的三个关键因素:
- 词频 (Term Frequency): 查询词在文档中出现的次数越多,得分越高。
- 逆文档频率 (Inverse Document Frequency): 查询词在整个索引的所有文档中越稀有,得分越高(例如,“Elasticsearch”比“的”更有价值)。
- 字段长度 (Field Length): 字段越短,得分越高(例如,在短标题中匹配到词,比在长篇内容中匹配到更重要)。
关于相关性的控制可以参考这篇文章:相关性控制的艺术
3.2 全文查询(Full-text Queries)
用于搜索被分析过的文本字段。
-
match: 标准的全文搜索,会对查询词进行分词。
{ "query":{ "match": { "content": "分布式 搜索引擎" } } }
-
match_phrase: 短语匹配,要求所有词项都出现,且顺序一致。
{ "query": { "match_phrase": { "content": "分布式搜索引擎" } } }
-
match_phrase_prefix: “边输入边搜索”的利器,它会匹配短语的前缀。
// 用户输入 "分布式 搜" 时,可以匹配到 "分布式 搜索引擎"{ "query": { "match_phrase_prefix": { "content": "分布式 搜" } } }
-
query_string / simple_query_string: 允许用户输入复杂的 Lucene 查询语法(如 AND, OR, *),功能强大但有风险。simple_query_string 更安全,会忽略语法错误。
{ "query": { "query_string": { "default_field": "content", "query": "(指南 AND Elasticsearch) OR Java" } } }
在
content
字段中查找满足以下条件的文档: 要么,文档中同时包含了‘指南’和‘Elasticsearch’这两个词; 要么,文档中包含了‘Java’这个词。 -
multi_match: 在多个字段中执行相同的 match 查询,并支持字段权重和多种匹配策略
{ "query": { "multi_match": { "query": "Elasticsearch", "fields": [ "title^3", "content" ], //^3的意思是计算相关性时重要性为3倍 "type": "best_fields", "tie_breaker": 0.3, "operator": "or" } } }
在
title
和content
字段中搜索关键词‘Elasticsearch’,并且在计算相关性排名时,将title
字段的重要性视为content
字段的三倍。-
type
参数:这决定了多个字段之间的得分是如何计算和组合的,这对于优化多字段搜索很重要-
best_fields
:最佳字段策略,这是默认类型。它会分别计算每个字段的得分,然后取最高的那个得分作为文档的最终得分。这被称为“Disjunction Max”或“Dis-Max”查询。- 适用场景:字段内容互斥(如 title 和 content 存储不同的信息)
tie_breaker
:type
为"best_fields"
时可选,设置其他字段得分的权重(0-1),避免忽略次要字段
-
most_fields
:多数字段策略。它会分别计算每个字段的得分,然后将所有字段的得分相加作为最终得分。- 适用场景:字段内容相似或互补(如 product_name 和 brand 存储相同的信息)。例如,搜索“Apple”,无论它出现在 product_name 还是 brand 字段,都应该被认为是强相关的。
-
cross_fields
:跨字段策略。它将多个字段视为一个大字段来分析和匹配,它对于处理那些被拆分到不同字段的实体(如人名,地址)非常有效。虽然cross_fields
在查询时很方便,但在很多场景下,一个性能更好、逻辑更清晰的替代方案是在索引时就将多个字段的内容合并到一个新字段中(例如使用copy_to
),然后对这个组合字段进行简单的match
查询。- 必须配合
operator:"and"
:确保所有查询词都出现。 - 需求:搜索 “Will Smith”。
如果用 best_fields 或 most_fields,ES 会分别搜索 "Will" 和 "Smith",然后组合结果,可能会错误地匹配到 "Will Johnson" 和 "John Smith"。 而 cross_fields 会将这两个字段视为一体,正确地寻找同时包含 "Will" 和 "Smith" 的文档。
{ "fields": ["first_name", "last_name"], "type": "cross_fields", "operator": "and" // 必须所有词出现(可跨字段) }
- 必须配合
-
pharse
&pharse_prefix
:短语和前缀策略-
phrase
: 要求所有查询词项在字段中都出现,并且保持严格的顺序和位置。 -
phrase_prefix
: 与phrase
类似,但允许最后一个词项是前缀。 -
适用场景:
phrase
: 跨字段精确匹配一个完整短语。phrase_prefix
: 实现跨多字段的“搜索即输入”自动补全功能。
-
slop
参数: 在这两种类型下,可以使用slop
参数来增加短语匹配的灵活性,允许词语之间有一定的距离或顺序变化。- 我们可以通过一个简单的查询
"quick fox"
来看看不同slop
值的效果:
slop
值查询 "quick fox"
能否匹配 "quick brown fox"
?能否匹配 "fox quick"
?解释 0 (默认) match_phrase
否 否 只匹配严格的 "quick fox"
。1 slop: 1
是 否 "quick"
和"fox"
之间可以有1个未知词。但不足以交换顺序。2 slop: 2
是 是 允许中间有2个词(如 "quick little brown fox"
)。同时,交换两个相邻词的位置也需要2次移动("quick"右移1位,"fox"左移1位),所以slop: 2
可以匹配到"fox quick"
。3 slop: 3
是 是 允许中间有3个词,或者更复杂的词序变化。 - 我们可以通过一个简单的查询
-
-
bool_prefix
:现代前缀匹配策略。这是phrase_prefix
的现代、高效替代方案。它将除最后一个词以外的所有词作为term
查询,最后一个词作为prefix
查询,然后用bool
查询组合起来。- 优点: 对词序不敏感,性能更高,更符合现代用户无序输入关键词进行即时搜索的习惯。是实现自动补全功能的首选,是
phrase_prefix
的上位替代。
- 优点: 对词序不敏感,性能更高,更符合现代用户无序输入关键词进行即时搜索的习惯。是实现自动补全功能的首选,是
-
-
operator
参数:or
(默认):匹配任意一个词即可and
:必须匹配所有的词(推荐在cross_fields
中使用)
-
3.3 布尔查询(bool)
bool 查询是构建复杂逻辑的“瑞士军刀”,它允许您像搭积木一样,将多个子查询组合起来。
- must: (逻辑与 AND)
- 所有子句都必须匹配。
- 在查询上下文中执行,会影响最终的 _score。
{
"query": {
"bool": {
"must": [
{ "match": { "title": "Elasticsearch" } },
{ "match": { "content": "性能优化" } }
]
}
}
}
只有那些标题中包含 "Elasticsearch" 并且内容中也包含 "性能优化" 的文章才会被返回。两个条件都匹配的文档,其 _score
会比较高。
- filter: (逻辑与 AND)
- 所有子句都必须匹配。
- 在过滤上下文中执行,不影响得分,性能更高,可被缓存。
{
"query": {
"bool": {
"filter": [
{ "term": { "author_name.keyword": "张三" } },
{ "term": { "status": "published" } }
]
}
}
}
返回所有精确匹配这两个条件的文章。因为不计算得分,所以返回结果的 _score
可能都是 0
或 1
。这个查询速度会非常快。
- must_not: (逻辑非 NOT)
- 所有子句都绝不能匹配。
- 在过滤上下文中执行。
{
"query": {
"bool": {
"must": [
{ "match": { "content": "Java" } }
],
"must_not": [
{ "term": { "tags": "obsolete" } }
]
}
}
}
返回所有内容包含“Java”的文章,但如果某篇文章的 tags
字段中包含了“obsolete”这个词,它将被从结果中排除。
- should: (逻辑或 OR)
- 至少有一个子句应该匹配。
- 每匹配一个,都会增加 _score。当 bool 查询中没有 must 或 filter 子句时,should 子句中至少要有一个匹配成功。
{
"query": {
"bool": {
"must": [ // 基础条件:内容必须包含“教程”
{ "match": { "content": "教程" } }
],
"should": [ // 加分项:满足任一条件,得分更高
{ "term": { "tags": "官方文档" } },
{ "term": { "tags": "最佳实践" } }
]
}
}
}
所有内容包含“教程”的文章都会被返回。其中,那些同时带有“官方文档”或“最佳实践”标签的文章,其 _score
会更高,因此会排在结果列表的前面。
如果 bool 语句中只有 should,那么这个 should 必须要实现一个;而如果有 must/must_not 存在,那么 should 就是一个加分项,可以一个条件都不满足
- minimum_should_match:
- 一个非常有用的参数,用于指定 should 子句中至少需要匹配成功的数量或百分比。
- 例如,"minimum_should_match": 2 表示 should 列表中的条件至少要满足两个。
{
"query": {
"bool": {
"should": [
{ "match": { "content": "Elasticsearch" } },
{ "match": { "content": "Kibana" } },
{ "match": { "content": "Logstash" } }
],
"minimum_should_match": 2 // 关键参数:should列表中的3个条件,至少要满足2个
}
}
}
只有那些内容中同时包含了(Elasticsearch 和 Kibana)、(Elasticsearch 和 Logstash)或(Kibana 和 Logstash)的文章才会被返回。只包含其中一个词的文章将被过滤掉。
3.4 词项级别查询(Term-level Queries)
用于对未分词的 keyword, numeric, date 等字段进行精确匹配。强烈建议将它们放在 filter 上下文中。
-
term: 匹配单个精确值。
{ "query": { "bool": { "filter": [ {"term": { "tags":"Java" } }, {"term": { "content":"String" } } ] } } }
-
terms: 匹配一个值列表中的任意一个。
{ "query": { "bool": { "filter": { "terms": { "tags": ["Java", "Elasticsearch"] } } } } }
-
range: 范围查询。
{ "query": { "bool": { "filter": { "range": { "view_count": { "gte": 1000, "lte": 10000 } } } } } }
gte
: Greater Than or Equal to (大于或等于≥
)gt
: Greater Than (大于>
)lte
: Less Than or Equal to (小于或等于≤
)lt
: Less Than (小于<
) -
exists: 查找某个字段存在的文档。
{ "query": { "bool": { "filter": { "exists": { "field": "author.name" } } } } }
exists
查询是一个简单而强大的工具,它不关心字段的值是什么,只关心字段是否存在且有值。它通常被放在filter
上下文中以获得最佳性能,并且通过与must_not
结合,可以灵活地实现对数据存在性的双向筛选。 -
prefix: 前缀查询。
{ "query": { "prefix": { "author.name": "李" } } }
-
wildcard / regexp: 通配符和正则表达式查询。功能强大,但性能极差,应避免在大型数据集上使用。
3.5 关联关系查询 (Joining Queries)
用于处理 nested 或 parent-join 关系的数据。
-
nested: 查询嵌套对象数组。
// 查询包含“由王五发表且点赞数大于10”的评论的文章{ "query": { "nested": { "path": "comments", "query": { "bool": { "must": [ {"match": { "comments.username": "王五" } }, {"range": { "comments.votes": { "gt": 10 } } ] } } } } }
-
has_child / has_parent: 基于父子关系进行查询。
// 查询所有拥有“内容包含Elasticsearch的回答”的问题{ "query": { "has_child": { "type": "answer", "query": { "match": { "content": "Elasticsearch" } } } } }
对于关联关系查询,有更加详细的文章介绍:关联关系查询指南
3.6 相似性查询
在搭建搜索引擎时,有一个非常常见的需求是:“猜你喜欢”功能,它能以一篇或多篇文档作为“样本”,分析它们的关键词,然后基于这些关键词去索引库中查找其他与之相似的文章
-
工作原理:
-
分析样本: ES 会分析你提供的样本文档(比如用户正在阅读的文章)的指定字段(如
title
和content
)。 -
提取关键词: 它会从样本文档中挑选出有代表性的、重要的词项。
-
构建新查询: 利用这些关键词,它会在内部动态地构建一个
bool
查询,去寻找其他也包含这些关键词的文档。 -
评分返回: 最终,匹配关键词越多的文档,其相关性得分 (
_score
) 越高,排名越靠前。
-
-
示例:为 ID 为 1 文章查找 3 篇最相似的文章
GET /blog_posts/_search
{
"query": {
"more_like_this": {
"fields": ["title", "content"],
"like": [
{
"_index": "blog_posts",
"_id": "1"
}
],
"min_term_freq": 1,
"min_doc_freq": 1
}
},
"size": 3
}
fields
: 指定从 title
和 content
这两个字段中提取关键词来判断相似度。
like
: 指定样本文档。这里我们直接引用了 blog_posts
索引下 _id
为 1
的文档。
min_term_freq
: 一个词在样本文档中至少出现1次才被考虑。
min_doc_freq
: 一个词在整个索引中至少出现1次才被考虑。
3.7 结果高亮
高亮显示是提升搜索用户体验最直观的功能,它能在返回的文本中,用指定的 HTML 标签包裹住匹配到的关键词
-
基本用法:
在查询的顶层添加 highlight 对象,并制定需要高亮的字段
GET /blog_posts/_search
{
"query": { "match": { "content": "搜索引擎" } },
"highlight": {
"fields": {
"content": {}
}
}
}
-
自定义高亮标签:
如果想用加粗的
<b>
标签,而不是斜体的<em>
标签,"highlight"部分可以这样写:
"highlight": {
"fields": {
"content": {
"pre_tags": ["<b>"],
"post_tags": ["</b>"]
}
}
}
- 自定义片段大小和数量:
当字段内容很长时,ES 不会返回整个字段,而是返回包含关键词的“片段”。- fragment_size: 每个片段的字符长度,默认约 100
- number_of_fragments: 返回的片段最大数量,默认 5。如果设为 0,则会高亮整个字段内容(适用于字段较短的情况)
"highlight": {
"fields": {
"content": {
"fragment_size": 200,
"number_of_fragments": 1
}
}
}
Query DSL 是一个庞大而精妙的体系,学习它的最好方式就是在 Kibana 开发工具中不断地实践,修改与观察!
评论区
请登录后发表评论