目录
前言
这篇博客主要讲解ElasticSearch,由于篇幅过长,这个博客也是分为两篇,篇一主要讲解ElasticSearch的基础概念和原理,安装部署,以及索引库和文档基本操作,希望看完这篇博客能让新人对Elasticsearch有一个直观的认识。
一,Elasticsearch概述与核心概念
什么是Elasticsearch
- Elasticsearch是一款开源的分布式搜索引擎,基于Java开发,采用Lucene作为核心搜索库。其发展历程可追溯至2004年,当时Shay Banon基于Lucene开发了Compass,随后在2010年重写并命名为Elasticsearch
技术优势与生态系统
Elasticsearch具备显著的技术优势
- 分布式架构:支持水平扩展,能够处理海量数据
- RESTful接口:提供标准的HTTP API,可被任何编程语言调用
- 高性能搜索:基于倒排索引技术,查询速度极快
Elasticsearch与Kibana、Logstash、Beats共同构成ELK技术栈,被广泛应用于日志数据分析、实时监控等领域。

而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。

核心概念解析
文档和字段
- elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中

- 因此,传统数据库中的一行数据就是ES中的一个JSON文档;而数据库中每行数据都包含很多列,这些列就转换为JSON文档中的字段(Field)
索引和映射
索引
- Elasticsearch中的索引不是传统数据库中的索引,而是"表",比如商品文档、用户的文档、订单文档等,将同类型的文档集中在一起管理,成为索引。例如:

映射(mapping)
-
传统数据库中的表会有约束信息,用来定义表的结构、字段名称、类型、主键约束等等。同样的,在elasticsearch,在创建索引(表)时,也必须有这样的约束信息,在elasticsearch中,这样的约束信息被称为mapping(映射),用于定义索引中的document的字段名称、类型等结构和约束。
-
常见的Mapping属性包括:

-
需要注意的是,mapping中的index(索引),指的是为这个字段创建常规索引,便于根据该字段快速查询数据,类比于mysql中的聚集索引和非聚集索引,elasticsearch中的索引概念比较混乱,这里需要理解。
-
例如下面的json文档:
yaml{ "age": 21, "weight": 52.1, "isMarried": false, "info": "黑马程序员Java讲师", "email": "zy@itcast.cn", "score": [99.1, 99.5, 98.9], "name": { "firstName": "云", "lastName": "赵" } }对应的每个字段映射(Mapping):

mysql与elasticsearch
-
学习Elasticsearch对照MySQL,pg关系型数据库会理解的更深刻,本质上可以这样理解,Elasticsearch也是一个数据库,基于倒排索引检索存储的数据,除此之外,还提供数据分析的功能,仅此而已。Elasticsearch需要掌握以下基本概念:
MySQL/PG概念 Elasticsearch对应 说明 Table Index 索引,即文档的集合,类似数据库的表 Row Document 文档,一条条的数据,采用JSON格式 Column Field 字段,JSON文档中的属性,类似数据库列 Schema Mapping 映射,索引中 文档的字段约束信息, 字段什么名字,什么类型,需不需要倒排索引等等 SQL DSL Elasticsearch提供的JSON风格查询语句

二,倒排索引原理与分词器
倒排索引工作机制
-
讲倒排索引之前,先回顾一下mysql、pg等传统数据库的正向索引,什么是正向索引,简单来说,就是根据关键词检索数据时,依次将数据库表中的记录去匹配查询关键词条件,重点是看记录中包不包含关键词 ,将包含关键词的记录一条一条检索出来,下面这个图是一个比较经典的正向索引查新数据的过程。

-
与传统数据库的正向索引不同,Elasticsearch采用倒排索引技术,倒排索引有两个非常重要的概念:
概念 说明 文档(Document) Elasticsearch存储的数据,用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息,类比传统数据库中的Row 词条(Term) 对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条 -
倒排索引的核心思想是对文档内容进行分词,对词条创建索引,并记录词条所在文档的ID。查询时先根据词条查询到文档ID,再获取完整文档。注意,这里所说的索引不是指mysql、pg等传统数据库地索引,索引在Elasticsearch中,是类似于传统数据库的表的概念,可以这样简单理解,正向索引就是存放文档(document)的表,倒排索引是正向索引中拆分出来的词语表。

-
倒排索引的搜索流程如下(以搜索"华为手机"为例),如图所示。Elasticsearch先将用户输入的查询条件进行分词,然后在词语倒排索引中进行查找,由于词条有索引(注意理解,这个索引指的是系统为倒排索引内部的"词条字典"(Term Dictionary)这个组成部分本身又建立了一层高效的索引结构(通常是 Term Index)),所以查询效率也高。
-
虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描

正向和倒排的优缺点
| 索引 | 优点 | 缺点 |
|---|---|---|
| 正向索引 | 可以给多个字段创建索引, - 根据索引字段搜索、排序速度非常快 | 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描 |
| 倒排索引 | - 根据词条搜索、模糊搜索时,速度非常快 | - 只能给词条创建索引,而不是字段, - 无法根据字段做排序 |
IK中文分词器
- 由于中文分词的复杂性,需要专门的中文分词器。IK分词器采用正向迭代最细粒度切分算法,提供两种分词模式
- ik_smart:智能切分,粗粒度
- ik_max_word:最细切分,细粒度
-
我们在调用Elasticsearch Restfull接口时,可以指定分词器的工作模式。
yamlPOST /_analyze { "analyzer": "ik_max_word", "text": "传智播客开设大学,真的泰裤辣!" } -
如果我们想要扩展IK分词器分词的基本词汇,IK分词器提供了扩展词汇的功能,我们只需要创建一个自定义的新增词语文件,然后在IK分词器配置文件中关联一下即可。

-
在IKAnalyzer.cfg.xml配置文件内容添加:
xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 扩展配置</comment> <!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典--> <entry key="ext_dict">ext.dic</entry> </properties>
三,索引库操作
-
创建索引库, 请求方式:PUT,请求参数:mapping映射
yamlPUT /索引库名称 { "mappings": { "properties": { "字段名":{ "type": "text", "analyzer": "ik_smart" }, "字段名2":{ "type": "keyword", "index": "false" }, "字段名3":{ "properties": { "子字段": { "type": "keyword" } } }, // ...略 } } } -
查询索引库:GET /索引库名
-
删除索引库:DELETE /索引库名
-
修改索引库
-
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
-
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段,或者更新索引库的基础属性。
yamlPUT /索引库名/_mapping { "properties": { "新字段名":{ "type": "integer" } } }
-
四,文档操作
新增文档
-
语法如下:
yamlPOST /索引库名/_doc/文档id { "字段1": "值1", "字段2": "值2", "字段3": { "子属性1": "值3", "子属性2": "值4" }, }示例:

查询文档
-
语法如下:
yamlGET /{索引库名称}/_doc/{id}示例:

删除文档
删除使用DELETE请求,同样,需要根据id进行删除:
yaml
DELETE /{索引库名}/_doc/id值
修改文档
修改文档也有两种方式:
- 全量修改:直接覆盖原来的文档
- 局部修改:修改文档中的部分字段
全量修改
-
全量修改是覆盖原来的文档,本质上是删除原来的文档,然后新增一个相同id的文档,如果id不存在,也会新增一个新的文档。
-
语法如下:
yamlPUT /{索引库名}/_doc/文档id { "字段1": "值1", "字段2": "值2", // ... 略 }
局部修改
-
局部修改是只修改指定id匹配的文档中的部分字段
-
语法:
yamlPOST /{索引库名}/_update/文档id { "doc": { "字段名": "新的值", } }
批处理
-
Elasticsearch中允许通过一次请求中携带多次文档操作,也就是批量处理,语法格式如下:
yamlPOST _bulk { "index" : { "_index" : "test", "_id" : "1" } } { "field1" : "value1" } { "delete" : { "_index" : "test", "_id" : "2" } } { "create" : { "_index" : "test", "_id" : "3" } } { "field1" : "value3" } { "update" : {"_id" : "1", "_index" : "test"} } { "doc" : {"field2" : "value2"} } -
其中,index表示新增操作。
五,DSL查询
- 在实际开发中,Elasticsearch的查询远比简单的文档ID精确查询要复杂得多。就像传统数据库业务中不能仅依赖主键查询一样,我们需要处理各种复杂的查询场景。这时就需要使用Elasticsearch的查询语言DSL来构建复杂的查询语句,其功能类似于关系型数据库中的SQL语句。
- Elasticsearch提供了基于JSON的DSL(Domain Specific Language)语句以JSON格式来定义查询条件。
基本结构
Elasticsearch的查询可以分为两大类:
- 叶子查询: 简单查询,或者叫单条件查询,一般是在特定的字段里查询特定值。
- 复合查询:逻辑组合多个叶子查询,一般实际开发中多以复合查询为主。
DSL查询语句结构是固定的,如下:
yaml
GET /{索引库名}/_search
{
"query": {
"查询类型": {
// .. 查询条件
}
}
}
Elasticsearch返回的查询结果也是固定的,各字段含义如下,其中检索的原始数据在_source字段中,Elasticsearch默认的返回数量是10条文档。

叶子查询
- 叶子查询是单条件的简单查询,叶子查询常用的有三大类,每一大类又有多个具体的类型。

全文检索查询
-
会对用户输入内容进行分词,然后去倒排索引库中匹配,将查询出来的文档整体计算与输入内容的匹配度,最后根据匹配度排序返回结果。
-
match查询:在单个字段上进行全文检索,语法
yamlGET /{索引库名}/_search { "query": { "match": { "字段名": "搜索条件" } } } -
multi_match查询:允许同时在多个字段上进行全文检索,但参与查询的字段越多,性能通常越差。
yamlGET /{索引库名}/_search { "query": { "multi_match": { "query": "搜索条件", "fields": ["字段1", "字段2"] } } }
精确查询
-
不会对搜索条件分词,而是直接精确匹配。推荐用于查找 keyword、数值、日期、布尔等类型字段。如果查询字段是一个分词字段(text类型),Elasticsearch不会将输入内容分词,而是将输入内容整体与拆分 的词条索引库(表)匹配,进而查询出文档
-
term查询:根据词条进行精确匹配。
yamlGET /{索引库名}/_search { "query": { "term": { "字段名": { "value": "输入词条" } } } } -
range查询:根据数值或日期范围进行查询。
yamlGET /{索引库名}/_search { "query": { "range": { "字段名": { "gte": {最小值}, "lte": {最大值} } } } } -
ids查询: 根据id数组查询文档
复合查询
-
复合查询是一个或多个叶子查询的组合,通过逻辑运算符来构建复杂查询,也称为bool查询
-
bool查询支持的逻辑运算有:
运算符 说明 must 必须匹配每个子查询,类似"与" should 满足其中一个子查询即可,类似"或" must_not 必须不匹配,不参与算分,类似"非" filter 必须匹配,不参与算分 -
需要解释下这个算分的过程,如果一个DSL复合查询语句,有must、should、must_not、filter四种运算符,Elasticsearch会先执行must_not、filter这两种不算分的运算,将不满足条件的记录过滤掉,然后执行must匹配,将过滤后的文档集中验证must条件,这个过程时算分的,最后再对满足should条件的文档进行相关性加分,大概是这样的算分过程。
-
需要注意的是,算分是基于文档与查询条件的匹配程度,而不是与输入词语的简单匹配
-
一个经典的复合查询语句如下:
yamlGET /items/_search { "query": { "bool": { "must": [ {"match": {"name": "手机"}} ], "should": [ {"term": {"brand": { "value": "vivo" }}}, {"term": {"brand": { "value": "小米" }}} ], "must_not": [ {"range": {"price": {"gte": 2500}}} ], "filter": [ {"range": {"price": {"lte": 1000}}} ] } } }
排序
-
elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
-
支持多字段排序,语法如下:
yamlGET /indexName/_search { "query": { "match_all": {} }, "sort": [ { "FIELD": "desc" // 排序字段和排序方式ASC、DESC } ] }
分页
-
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
-
基础分页:elasticsearch中通过修改from、size参数来控制要返回的分页结果:
yamlGET /items/_search { "query": { "match_all": {} }, "from": 0, // 分页开始的位置,默认为0 "size": 10, // 每页文档数量,默认10 "sort": [ { "price": { "order": "desc" } } ] } -
深度分页 ,跟传统数据库一样,elasticsearch 也存在深度分页问题,讲深度分页之前,先说下elasticsearch 的数据存储方式,跟传统数据库一样,如果文档数量过大,elasticsearch 会将所有文档存储在多个分片(实例)上,如果有排序分页的查询需求,比如
from 990 size 10,传统分页需要将每个分片的文档,先排序,再取前1000个文档到本地,再进行排序,然后向后数到990,将接下来的10条数据返回结果。

-
因此,如果当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求
-
为了解决这个问题,elasticsearch提供了
search after这个解决方案,分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。 -
还是这个
from 990 size 10这个分页需求,search after 会分批将小批量(size)抽取到本地分片,进行排序,然后将排序最后一个的数据作为标签,向各个分片发出查询请求,检索值大于(或者小于)这个标签的数据(再来size),循环这个过程,直到定位到分页的位置
高亮显示
-
elasticsearch可以将关键词加上HTML标签,以帮助前端对关键词进行渲染或者高亮显示。
-
基本语法如下 :
yamlGET /{索引库名}/_search { "query": { "match": { "搜索字段": "搜索关键字" } }, "highlight": { "fields": { "高亮字段名称": { "pre_tags": "<em>", "post_tags": "</em>" } } } } -
示例如下:

数据聚合
- 类比于传统数据库的聚合函数、分组等数据分析功能,elasticsearch也可以极其方便的实现对数据的统计、分析、运算。
- 聚合运算常用的主要有两类,分组(桶聚合)和函数运算(度量聚合)。

Bucket桶聚合
-
例如我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category值一样的放在同一组,属于Bucket聚合中的Term聚合
yamlGET /items/_search { "size": 0, # 设置size为0,就是结果中就不包含文档,只包含聚合结果 "aggs": { "category_agg": { # 给聚合起个名字 "terms": { # 聚合实际类型 "field": "category", "size": 20 # 只返回20条聚合结果 } } } } -
下面是返回的结果

带条件的Bucket桶聚合
-
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以通过query添加查询条件,elasticsearch会将满足条件的文档筛选出来,然后进一步做聚合操作。
yamlGET /items/_search { "query": { "bool": { "filter": [ { "term": { "category": "手机" } }, { "range": { "price": { "gte": 300000 } } } ] } }, "size": 0, "aggs": { "brand_agg": { "terms": { "field": "brand", "size": 20 } } } } -
查询返回的结果如下:
yaml{ "took" : 2, "timed_out" : false, "hits" : { "total" : { "value" : 13, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "brand_agg" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "华为", "doc_count" : 7 }, { "key" : "Apple", "doc_count" : 5 }, { "key" : "小米", "doc_count" : 1 } ] } } }
Metric聚合(度量聚合)
-
度量聚合是桶聚合后,对每个桶内的文档做分析统计,返回结果。
yamlGET /items/_search { "query": { "bool": { "filter": [ { "term": { "category": "手机" } }, { "range": { "price": { "gte": 300000 } } } ] } }, "size": 0, "aggs": { "brand_agg": { "terms": { "field": "brand", "size": 20 }, "aggs": { # 聚合的内部新加了一个aggs参数。这个聚合就是brand_agg的子聚合,会对brand_agg形成的每个桶中的文档分别统计 "stats_meric": { # 自己定义的聚合名称 "stats": { "field": "price" } } } } } } -
下面是返回结果 的说明

-
此外,我们可以定义聚合返回的顺序
