1 初识 Elasticsearch

1.1 什么是 Elasticsearch

Elasticsearch 是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容

Elasticsearch 结合 kibana、Logstash、Beats,也就是 elastic stack(ELK),被广泛应用在日志数据分析、实时监控等领域

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

image-20251222222443582

1.2 Elasticsearch 的发展

Lucene 是一个 Java 语言的搜索引擎类库,是 Apache 公司的定级项目,由 DougCutting 于 1999 年研发,

官网地址:https://lucene.apache.org

Lucene 的优势:

  • 容扩展
  • 高性能(基于倒排索引)

Lucene 的缺点:

  • 只限于 Java 语言开发
  • 学习曲线陡峭
  • 不支持水平扩展

2024 年 Shay Banon 基于 Lucene 开发了 Compass

2010 年 Shay Banon 重写了 Compass,取名为 Elasticsearch

官网地址:https://www.elastic.co/cn

相比于 Lucene,Elasticsearch 具备以下优势:

  • 支持分布式,可水平扩展
  • 提供 Restful 接口,可被任何语言调用

访问https://www.elastic.co/cn/downloads/elasticsearch 可以看到,Elasticsearch 目前最新版本是 9.2.3

image-20251230231347673

1.3 倒排索引

传统数据库(MySQL)采用正向索引,比如订单表给订单 ID 创建索引,根据订单 ID 找到具体的订单数据

Elasticsearch 采用倒排索引,索引存的是词条,而不是订单 ID,搜索的时候,根据词条找到订单 ID。所以,倒排索引,会把文档中的内容分成词条再去存

下面说下什么是文档和词条:

  • 文档(document):每条数据就是一个文档。比如商品表,每个商品就是一个文档。订单表,每个订单就是一个文档
  • 词条(term):文档按照语义分成的词语

下面举例讲一下倒排索引的原理:

比如商品有有 4 条数据,每条数据有 id、title、price 三个字段。

现在先存第 1 个文档,会把 小米手机 分成 小米手机 两个词,前面说了,商品表中一条数据就是一个文档,所以文档的 id 就是 1。

所以 Elasticsearch 存储的词条就是 小米手机,文档 id 是 1,如下图所示:

image-20251222225153438

接下来存第 2 个文档,会把 华为手机 分成 华为手机华为 在词条中不存在,所以新增一个词条,文档 id 是 2,手机 在词条中已经存在了,不会重复存,而是把文档 id(也就是 2),加在 1 的后面,如下图所示:

image-20251222225751287

第 3、第 4 个文档以此类推

image-20251222225846524

那搜索的时候,是怎么搜索的呢?

image-20251222230041247

总结:

正向索引:基于文档 id 创建索引,查询词条时必须先找到文档,而后判断是否包含词条

倒排索引:对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档 id,而后获取文档

1.4 es 和 MySQL 的概念对比

1.4.1 文档

文档:Elasticsearch 是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息

文档数据会被序列化为 json 格式后存储在 Elasticsearch 中

比如下图中的商品数据,存到 es 中就是 json 格式的

image-20251222232429064

1.4.2 索引

索引(Index):相同类型的文档的集合。前面说到,一条商品数据,就是一个文档,那索引就是 MySQL 中的商品表的概念

映射(mapping):索引中文档的字段约束信息,类似表的结构约束

如下图所示:商品索引也就是商品表,用户索引也就是用户表,订单索引也就是订单表

image-20251224205202078

1.4.3 对比

MySQL Elasticsearch 说明
Table(表) Index(索引) 索引(index),就是文档的集合,类似于数据库的表(Table)
Row(行) Document(文档) 文档(Document)就是一条条的数据,类似于数据库中的行(Row)。文本都是 JSON 格式
Column(列) Field(字段) 字段(Field)就是 JSON 文档中的字段,类似于数据库中的列(Column)
Schema(约束) Mapping(映射) 映射(mapping)是索引中文档的约束,例如字段类型约束,类似数据库的表结构(Schema)
SQL DSL DSL 是 Elasticsearch 提供的 JSON 风格的请求语句,用来操作 Elasticsearch,实现 CRUD

架构上:

MySQL:擅长事务类型操作,可以确保数据的安全和一致性

Elasticsearch:擅长海量数据的搜索、分析、计算

2 安装

2.1 Docker 安装 Elasticsearch

Docker 安装 Elasticsearch 的启动脚本为:

docker run -d --name es  \
-p 9200:9200 -p 9300:9300 \
-v /data/zhuxs/databases/elasticsearch/es_data:/usr/share/elasticsearch/data \
-v /data/zhuxs/databases/elasticsearch/es_plugins:/usr/share/elasticsearch/plugins \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
docker.elastic.co/elasticsearch/elasticsearch:9.0.1
  • ES_JAVA_OPTS:表示配置 es 的 Java 堆内存大小,毕竟 es 是根据 Java 实现的
  • discovery.type=single-node:表示 es 是单点部署的,而不是集群部署
  • 9200 是暴露的 http 协议端口,供用户访问的
  • 9300 是 es 容器各个节点之间互联的端口

部署起来后,浏览器访问 ip+9200,如下图所示:

image-20251224215529654

如果浏览器能访问到上图的页面,说明 es 部署成功了

其中 version 表示 es 的版本信息, build_date 表示启动时间

2.2 Docker 安装 Kibana

Kibana 可以给我们提供一个 Elasticsearch 的可视化界面,便于我们学习

kibana 是一个针对 Elasticsearch 的开源分析及可视化平台,好比 MySQL 对应的 Navicat 一样。使用 kibana 可以查询、查看并与存储在 ES 索引的数据进行交互操作,使用 kibana 可以执行高级的数据分析,并能以图表、表格和地图的形式查看数据

Docker 安装 Kibana 的脚本如下:

docker run -d --name kibana \
-e "ELASTICSEARCH_HOSTS=http://localhost:9200" \
-p 5601:5601 \
kibana:7.12.1
  • ELASTICSEARCH_HOSTS:表示设置 es 的地址

启动后,浏览器访问 ip+ 5601,如下图所示:

image-20251224220508415

页面在加载,需要等几秒。加载完之后,就会来到下图页面:

image-20251224220608882

  • Add data:表示添加数据、导入数据
  • Explore on my own:独自探索,表示自己玩

我们选择 Explore on my own 自己玩,点击后进入下图页面:

image-20251224221617768

  • Manage:表示管理
  • Dev tools:表示开发工具

点击菜单,往下滑,有一个 Dev Tools 的工具

image-20251224221749647

点击 Dev Tools ,进入下图:

image-20251224221932798

Dev tools 开发工具,可以非常方便地发送一个 DSL 请求

页面中会有一个默认请求:

  • GET:表示是 GET 请求
  • _search:表示要做一次搜索
  • query:表示查询
  • match_all:表示匹配所有数据

点击运行按钮,就会向 Elasticsearch 发送一个请求

image-20251224222534516

3 分词器

es 在创建倒排索引时需要对文档分词。在搜索时,需要对用户内容进行分词。但默认的分词规则对中文处理并不友好。

下面举个例子:

POST /_analyze
{
    "analyzer": "standard",
    "text": "黑马程序员学习java太棒了"
}
  • POST:请求方式
  • _analyze:请求路径。这里省略了 http://192.168.150.101:9200,有 kibana 帮我们补充
  • 请求参数,json 风格
    • analyzer:分词器类型,这里使用默认的 standard 分词器(标准分词器)
    • text:要分词的内容

在 kibana 中执行,analyzer 值为 english 表示使用英语分词,下图中可以看到,英文 java 被分到一个词,但是中文每个字都分成一个词

image-20251224223327944

即使使用标准分词器,analyzer 的值为 standard,效果还是一样,还是一个字一个词,说明对中文理解的不够好

image-20251224223513475

3.2 IK 分词器

处理中文分词,一般使用 IK 分词器。https://github.com/medcl/elasticsearch-analysis-ik

image-20251224224344990

点进去可以看到,IK 分词器有 2 种模式:ik_smartik_max_word

3.3 安装

IK 分词器有在线安装和离线安装,这里推荐离线安装

先下载 IK 分词器插件,下载后是一个后缀为 .zip 的压缩包,解压缩后把文件夹重命名为 ik

安装插件需要知道 elsticsearch 的 plugins 的目录位置,在启动 es 时,我们用了数据卷挂载,映射到磁盘上的目录为 /data/zhuxs/databases/elasticsearch/es_plugins ,然后把 ik 这个文件夹放到 /data/zhuxs/databases/elasticsearch/es_plugins 目录下就行

最后执行命令:docker restart es 重启容器就行

注意:使用的 ik 分词器得和 es 版本一致

3.4 测试

IK 分词器包含两种模式:

  • ik_smart:最少切分。从粗粒度切分,会从字符越来越多开始到字符越来越少去看,比如 5 个字是不是一个词,如果不是,看 4 个字是不是一个词,如果还不是,看 3 个字是不是一个词,以此类推。比如 程序员 3 个字刚好是一个词,那就不再继续往下看 程序 是不是一个词。所以分词粒度比较粗,词比较少。优势是分词少,占用内存空间也少,
  • ik_max_word:最细切分。从细粒度切分,在分词时会分更多词,比如把 程序员 分成 程序员程序 3 个词,这样分词就会分的很细。优势是由于分词更细更多,被搜索到的概率就越大。缺点是:分的词更多,更占用内存空间

IK 分词器安装好后,测试一下效果

先测试 ik_smart,如下图所示,没有再把每个字都分成一个词了,说明效果还不错

image-20251224230006356

再测试下 ik_max_word,可以看到:程序员,被分成了 程序员程序

image-20251224230259492

3.5 扩展词库

ik 分词器无法识别近几年的热门网络用语。如下图所示,把 奥力给 分成了 3 个字,说明分词器识别不了这个网络词语,有办法让 ik 分词器识别吗?有

image-20251225210317098

又比如,文本中出现了一些禁忌词,比如涉及一些不能播的文章,希望分词后能够禁止出现这些词,那分词器能实现这种个性化设置吗?可以

ik 分词器支持扩展词库,只需要修改 ik 分词器目录中的 config 目录中的 IKAnalyzer.cfg.xml 文件:

image-20251225212109261

ext 是扩展的意思,dist 是字典的意思。在 ext_dic 文件中,写入 奥力给,ik 分词器就能识别这个网络词语了

把某些敏感词停掉,不让用,也就是不让 ik 分词器识别并分词,怎么办?还是修改 ik 分词器目录中的 config 目录中的 IKAnalyzer.cfg.xml 文件:

image-20251225212706299

stopword 是停止词汇的意思,把敏感词写到 stopword.dic 文件中,ik 分词器就不会对它分词了

IKAnalyzer.cfg.xml 文件中默认是没配置扩展字典和停止词字典的,需要自己配置,如下图所示:

image-20251225212904353

现在配置扩展字典和停止字典,如下图所示:

image-20251225213109799

虽然我们配置了扩展字典和停止字典,但是 ext.dic 和 stopword.dic 是我们随手写的名字,这两个文件根本不存在,所以还需要新建两个文件。

在 IKAnalyzer.cfg.xml 同级的目录下新建 ext.dic 和 stopword.dic 两个文件就行

image-20251225213259143

比如在 ext.dic 文件中添加词:

image-20251225213509826

4 索引操作

4.1 mapping 属性

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/8.19/mapping-params.html

mapping 是对索引中文档的约束,mapping 属性有很多,如下图所示:

image-20251225215039622

常见的 mapping 属性包括:

  • type:字段的类型,常见的简单类型有:
    • 字符串:字符串有 2 种,一是 text(可分词的文本),二是 keyword:精确值,例如国家、品牌、ip 地址,不能拆分
    • 数值:long、integer、short、byte、double、float、
    • 布尔:boolean
    • 日期:date
    • 对象:object,表示字段是一个对象,对象中有其它属性。对象中的属性也是可以参与搜索的
    • 注意:没有数组这个类型,比如某字段是:“score”:[80 ,90 ,100],数组中的元素是 integer 类型,则 score 字段的类型也是 integer 类型
  • Index:是否创建索引,默认为 true,表示会创建倒排索引,可以参与搜索。如果不手动设置,则默认每个字段都会创建索引,每个字段都能参与搜索。但是,不是所有字段都需要参与搜索,比如图片地址字段,搜索有意义吗?因此,这类字段可以设置不创建索引
  • analyzer:使用哪种分词器,只有 text 类型才需要分词
  • properties:该字段的子字段

4.2 创建索引

ES 中通过 Restful 请求操作索引库、文档。请求内容用 DSL 语句来表示,创建索引库和 mapping 的 DSL 语法如下:

image-20251225222105443

上图中的右边可以看到,索引库中有 3 个字段:

  • info:字段类型为 text(字符串),使用 ik_smart 分词器
  • email:字段类型是 keyword(不需要分词),不创建索引
  • name:字段类型是对象,properties 表示有子字段,子字段是 firstName,类型是 keyword。上图在 name 字段少了一个属性:“type”: “object”

下面举例,创建一个名叫"heima"的索引库

image-20251225222730000

也可以直接创建索引,不带 mapping 映射

首先,执行下面的命令:

# 查看es中所有的索引
GET /_cat/indices

cat:表示查看

indices:表示索引

这个命令执行的结果如下:

image-20251231005026388

第 1 行:只是一个安全警告

第 2 - 8 行:是 es 中现有的索引。其中第三列是索引名称。这些索引是 kibana 工具连接 es 时创建的临时索引,每次启动时这些索引名称可能都不太一样,

执行命令:

GET /_cat/indices?v

v:表示显示标题

执行效果如下:

image-20251231005419974

health:健康状态。健康状态有 3 种,分别是红绿黄。green(绿)表示索引的健康状态是健康的,yellow(黄)表示索引可用,但是处于危险状态,red(红)表示索引不可用

status:索引的打开状态,默认是打开的

index:索引名称

uuid:表示索引的唯一标识

pri:索引主分片的个数

rep:索引的副本分片的个数

docs.count:每个索引下面的文档数

docs.deleted:文档删除数量

store.size:存储大小

pri.store.size:主分片的存储大小

下面直接创建一个索引:

# 创建一个商品索引
PUT /products

image-20251231010256728

acknowledged:值为 true,表示索引创建成功

shards_acknowledged:值为 true,表示分片创建成功

index:值为 products,表示索引名称是 products

再执行 GET /_cat/indices?v 看下索引有没有创建好呢?

image-20251231010544996

上图可以看到,products 索引已经创建好了,但是健康状态是 yellow,表示可用,但是危险。为什么刚创建的索引就处于危险状态呢?这是因为它的主分片和副本分片默认都是 1,而主分片和副本分片都在同一个服务器上,一旦服务器损坏,主分片和副本分片的数据都会丢失。这时候 es 就认为你做的副本(备份)是没有意义的,索引是不安全的

那有没有办法在创建索引的时候,就把它变成绿色呢?有!在创建索引的时候做一些配置,比如设置主分片数量为 1,副本数量为 0

# 创建一个索引
PUT /orders
{
    "setting": {
        "number_of_shards": 1,          //主分片数量
        "number_of_replicas": 0         //副本分片数量
    }
}

创建后可以查看一下,健康状态变成绿色了,如下图所示:

image-20251231011347099

4.3 查询索引

查看索引库语法:

GET /索引库
​
示例:
GET /heima

下面举例:

image-20251225224100546

如何查看索引的映射信息呢?

# 查看某个索引的映射信息
GET /heima/_mapping

4.4 删除索引

删除索引语法:

DELETE /索引库
DELETE /heima

4.5 修改索引

在 es 中,索引是不允许修改的,因为索引创建好后,它的 mapping 映射都创建好了,它会基于这些 mapping 去创建倒排索引。如果你要修改字段,就会导致原有的倒排索引彻底失效。不像 MySQL,创建表之后还可以修改表字段

es 虽然禁止你在索引库中修改原有的字段,但是允许你添加新的字段,语法如下:

PUT /索引库名/_mapping
{
	"properties": {
		"新字段名": {
			"type": "integer"
		}
	}
}

注意,新字段名必须要是一个全新的字段名,否则就会认为你在改字段,就会报错

下面举例:

image-20251225224338239

如果把 age 字段改成 long 类型,再添加一次,就会报错,如下图所示

image-20251225224746858

5 文档操作

5.1 添加文档

新增文档的 DSL 语法如下:

POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    }
}

比如下面想插入一个文档 id 为 1 的数据

image-20251225225507163

实战演示:

image-20251225225626770

上图可以看到,“result”: “created” 表示插入成功

5.2 查询文档

查询文档 DSL 语法如下:

GET /索引库名/_doc/文档id

示例:
GET /heima/_doc/1

上面已经插入文档成功了,现在想查询一下,如下图所示:

image-20251225230414797

_index:表示索引库是 heima

_id:表示文档 id 为 1

version:表示版本控制,每做一次文档修改,版本号都会 +1

source:表示你插入的原始文档

5.3 删除文档

删除文档的 DSL 语法如下:

DELETE /索引库名/_doc/文档id

示例:
DELETE /heima/_doc/1

image-20251225230629831

“result”: “deleted”,表示删除成功

删除之后再查一下,如下图所示:

image-20251225230740056

"found"的值变成 false 了,而且出现了 404,说明删除成功了

现在再重新插入一次,并执行查询,如下图所示,发现 version 变成 3 了,因为第一次插入,版本号为 1,删除后,版本号变成 2,再插入,版本号变成 3 了,所以每次写操作都会导致版本号不断增加

image-20251225230905620

5.4 修改文档

5.4.1 全量修改

全量修改,会删除旧文档,添加新文档,DSL 语法如下:

PUT /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2":"值2",
    // ... 略
}

细心的兄弟们会发现,这跟添加文档的 DSL 语法几乎一样,唯一的区别就是:添加文档是 POST,修改文档是 PUT

那如何删除旧文档呢?会根据文档 ID,去索引库中找到旧的文档,然后删掉。如果传的文档 ID 在索引库中不存在,就会直接新增。所以,使用 PUT 请求,如果文档 ID 存在则修改,不存在则新增,相当于可以替代 POST 请求

下面实战演示:

image-20251227201202630

上图中只修改了 email,结果显示: “result”: “updated”,表示更新成功

现在修改文档 ID 为 3 的文档,这个文档 ID 根本不存在,所以会新增,如下图所示

image-20251227201412641

上图中"result": “created”,表示新增成功。

5.4.2 新增修改

增量修改,修改指定字段值。其 DSL 语法如下:

POST /索引库名/_update/文档id
{
	"doc": {
		"字段名": "新的值"
	}
}

下面实战演示,只修改 email 字段。“result”: “updated”,表示修改成功

image-20251227204005573

5.5 总结

  • 创建文档:POST /索引库名/_doc/文档 id { json 文档 }
  • 查询文档:GET /索引库名/_doc/文档 id
  • 删除文档:DELETE /索引库名/_doc/文档 id
  • 修改文档
    • 全量修改:PUT /索引库名/_doc/文档 id { json 文档 }
    • 增量修改:POST /索引库名/_update/文档 id { “doc”: {字段} }

6 DSL 查询文档

6.1 DSL 查询分类

Elasticsearch 提供了基于 JSON 的 DSL(Domain Specific Language)来定义查询。常见的查询类型包括:

  • 查询所有:查出所有数据,一般测试用。例如:match_all。但是不是真的查出所有,而是不加限制的意思,实际上会分页,每页查 20 条
  • 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。有 match_querymulti_match_query
  • 精确查询:根据精确词条值查询数据,一般是查找 keyword、数值、日期、boolean 等类型字段。例如
    • ids:根据 id 精确匹配
    • range:根据数值范围查询
    • term:按照数据的值查询
  • 地理(geo)查询:根据经纬度查询,有 geo_distancegeo_bounding_box
  • 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
    • bool:利用布尔这种逻辑运算将其他条件组合起来
    • function_socre:控制相关度算分

DSL 的基本语法如下:

GET /indexName/_search
{
    "query": {
        "查询类型": {
            "查询条件": "条件值"
        }
    }
}

indexName:索引库的名称

_search:固定写法,表示要搜索

比如查询所有的 DSL 语法就是这样的:

GET /indexName/_search
{
    "query": {
        "match_all": {
        }
    }
}

注意:查询文档的 DSL 是:GET /indexName/_search,这是一种简写,全写可以这样写:GET /indexName/_doc/_search

6.2 DSL 基本语法

6.2.1 查询所有

上一节已经讲了查询所有的 DSL 语法,下面实战演示:

image-20251227222043083

took:花费多长时间,单位是毫秒

time_out:是否超时

shards:当前索引的分片信息

hits:命中的数据、查询的结果

hits -> total:搜索到的总条数

hits -> max_score:文档的相关性得分

hits -> hits:是一个数组,存的是一个个文档对象

hits -> hits -> _source:原始的文档信息

6.2.2 全文检索查询

全文检索查询,会对用户输入内容分词,常用于搜索框检索。其 DSL 语法如下:

GET /indexName/_search
{
	"query": {
		"match": {
			"字段名": "字段值"
		}
	}
}

image-20251227223310787

如上图所示,会对值"外滩如家"进行分词,然后对 all 字段进行检索。all 字段的值中,包含外滩或者如家,只要能匹配到一个,就能检索出来

全文检索还有一种检索方式:multi_match,与 match 查询类似,只不过允许同时查询多个字段,但是查询的字段越多,查询性能越差。其 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "multi_match": {
            "query": "字段值",
            "fields": ["字段1", "字段2"]
        }
    }
}

查询的效果就是:比如查询 A、B、C 三个字段,这样有一个字段的值为"字段值"的分词,就能检索到。下面实战演示:

image-20251227224121405

上图中,对 外滩如家 进行分词,比如分成 外滩如家,然后搜索了 3 个字段,只要有一个字段的值中包含了 外滩 或者 如家,就能匹配到

DSL

6.2.3 精确查询

精确查询一般是查找 keyword、数值、日期、boolean 等类型字段,这些类型有个共同特点:她们的值是一个不可分割的整体。所以不会对搜索条件分词。常见的有:

  • term:根据词条精确值查询
  • range:根据值的范围查

term 查询的 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "term": {
            "字段名": {
                "value": "字段值"
            }
        }
    }
}

比如精确搜索 city 为上海的文档,如下图所示:

image-20251227225418059

注意:如果要搜索的字段是 keword 类型,则只能精确地按照全部内容搜索,比如 city 字段是 keword 类型,想搜上海,那只能搜到 city=上海 的记录,搜不到 city=上海浦东 的记录。如果要搜索的字段是 text 类型,则按照分词之后的词来搜,比如 city 的值为 上海浦东,被分成 2 个词 上海浦东,则按照上图搜索,也能搜到这条数据

6.2.4 范围查询

range 查询的 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "range": {
            "字段名": {
                "gte": "起始值",   //gte表示大于等于
                "lte": "结束值",   //lte表示小于等于
            }
        }
    }
}

比如像查询价格大于 100,小于等于 300 的文档,如下图所示:

image-20251227225943767

gte:大于等于,如果只要大于呢?那就是 gt

lte:小于等于,如果只要小于呢?那就是 lt

6.2.5 前缀查询

前缀查询:查询以某某某为开头的数据。其 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "prefix": {
            "字段名": {
                "value": "字段值"
            }
        }
    }
}

下面查询 title 字段以 “小” 开头的数据

image-20260108210720023

6.2.6 通配符查询

wildcard 关键字:通配符查询,?用来匹配一个任意字符,* 用来匹配多个任意字符。其 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "wildcard": {
            "字段名": {
                "value": "字段值*"
            }
        }
    }
}

示例如下:

image-20260108211152941

6.2.7 多 id 查询[ids]

ids 关键字:值为数组类型,用来根据一组 id 获取多个对应的文档。其 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "ids": {
            "values": ["aaa", "bbb"]
        }
    }
}

6.2.8 模糊查询

fuzzy 关键字:用来模糊查询含有指定关键字的文档。其 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "fuzzy": {
            "字段名": "要搜索的值"
        }
    }
}

注意:fuzzy 模糊查询,最大模糊错误必须在 0 - 2 之间

  • 搜索关键词长度为 2,不允许存在模糊
  • 搜索关键词长度为 3-5,允许一次模糊
  • 搜索关键词长度大于 5,允许最大模糊

示例如下:

image-20260108212326456

6.2.9 过滤查询

过滤查询,其实准确来说,ES 中的查询操作分为 2 种:查询(query)和过滤(filter)。查询即是之前提到的 query 查询,它默认会计算出每个返回文档的得分,然后根据得分排序。而**过滤(filter)**只会筛选出符合的文档,并不计算得分,而且它可以缓存文档。所以,单从性能考虑,过滤比查询更快。换句话说,过滤适合在大范围筛选数据,而查询则适合精确匹配数据。一般应用时,应先使用过滤操作过滤数据,然后使用查询匹配数据

image-20260110220838846

为什么过滤性能更高?query 查询是精确查询,会计算每个文档的得分,然后根据得分排序。而计算得分是一个非常复杂的过程。如果在过滤查询的基础之上做查询,会大大减少 query 查询去 ES 中查询数据的数据量,而且在此基础上计算得分,会更快

使用 filter 的 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "bool": {
            "must": [
                {"match_all": {}}    //查询条件
            ],
            "filter": {...}          //过滤条件
        }
    }
}

注意:在执行 filter 和 query 时,先执行 filter,再执行 query。ES 会自动缓存经常使用的过滤器,以加快性能

常见的过滤类型有:term、terms、ranage、exists、ids 等

先用 term 做示例:

先用 term 查询 description 字段值为 好吃 的数据,可以搜到好几条数据,如下所示:

image-20260110221931621

现在先过滤出 description 字段值为 浣熊 的数据,然后在此基础上查询 description 字段值为 好吃 的数据,只能搜索到一条数据,如下图所示:

image-20260110222232522

再用 terms 做示例,terms 表示根据多个条件过滤,如下图所示

image-20260110222728040

现在用 range 做示例,range 表示范围过滤,下图是先过滤出价格在 0-10 之间的数据,再在此基础上查询 description 字段值为 豆腐 的数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

用 exists 做示例,exists 表示存在,过滤存在指定字段,获取字段不为空的索引记录使用。下图就是先过滤出带 aaaa 这个字段的数据,注意是根据字段名来过滤,而不是字段值

image-20260110223216064

最后用 ids 示例,ids 表示过滤出 id 字段值为指定值的字段,比如下图是先过滤出 id 字段值为 1、2、3

image-20260110223557648

6.2.10 地理查询

根据经纬度查询,

  • geo_bounding_box:查询 geo_point 值落在某个矩形范围的所有文档
  • geo_distance:查询到指定中心点小于某个距离值的所有文档

geo_bounding_box 查询的 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "geo_bounding_box": {
            "字段名": {
                "top_left": {
                    "lat": "经度值"
                    "lon": "维度值"
                },
                "bottom_right": {
                    "lat": "经度值"
                    "lon": "维度值"
                }
            }
        }
    }
}

下面实战演示:

上图中有 2 个点,第一个点就是 top_left,第二个点是 bottom_right

那搜索的效果就是:在这 2 个点之间,画一条横线和竖线,形成一个矩阵。矩阵范围内的点就能被检索到

image-20251227231112386

geo_distance 查询的 DSL 语法如下:

GET /indexName/_search
{
    "query": {
        "geo_distance": {
            "distance": "15km",    //半径
            "字段名":"31.21, 121.5" //经纬度,以这经纬度为中心点
        }
    }
}

其检索效果是,搜索以指定的点为中心点,半径为 15 公里的圆的内部的所有点,如下图所示:

image-20251227231716022

6.2.11 复合查询

6.2.5.1 算分函数查询

复合(compound)查询:将其它简单查询组合起来,实现更复杂的搜索逻辑

  • fuction_score:算分函数查询,可以控制文档相关性算法,控制文档排名。例如百度竞价。使用 Function Score Query,可以修改文档的相关性算分(query score),根据新得到的算分排序。其 DSL 语法如下:

image-20251228193105576

下面是一个案例

image-20251228193241981

6.2.5.2 布尔查询

布尔查询(Boolean Query)是一个或多个查询子句的组合。子查询的组合方式有:

  • must:必须匹配每个子查询,类似 “与”,表示必须满足
  • should:选择性匹配子查询,类似 “或”
  • must_not:必须不匹配,不参与算分,类似 “非”
  • filter:必须匹配,不参与算分,看上去跟 must 一样,但是底层不一样

es 在搜索的时候,不仅要看文档是否匹配,还要看文档和关键字之间的相关度,也就是打分,分值越高越靠前。打分有复杂的算分函数,每做一次算分会消耗一些资源。如果子查询比较多,每一个都参与算分,就会影响性能。如果使用 must_not、filter,不参与算分,它们只会返回满足还是不满足、是或否,所以会提升性能

下面是一个案例:关键词搜索需要参与算分,所以关键词使用 must

image-20251228195233043

再来一个案例:

image-20251228195432904

示例 1:查询 id 必须为 1 的数据

image-20260108212800971

示例 2:查询 id 为 1,且 title 为 “小浣熊” 的数据

image-20260108213030320

示例 3:如果是 should,满足一个条件就行,比如查询 id 为 1,或者 title 为 “浣熊”

image-20260108213213771

示例 4:must_not,表示都不能,不能满足任何一个,比如查询 id 不为 1,且 title 不为 “浣熊” 的数据

image-20260108214009247

6.2.12 多字段查询

GET /indexName/_search
{
    "query": {
        "multi_match": {
            "query": "要搜索的字段值",
            "fields": ["字段1", "字段2"]
        }
    }
}

注意:字段类型分词,将查询条件分词之后进行查询该字段,如果该字段不分词就会将查询条件作为整体进行查询

示例如下:

image-20260108214800823

搜的是 “浣猫”,但是因为会分词,把 “浣猫” 分成 “浣” 和 “猫”,然后拿这 2 个字去搜索,所以 title 和 description 字段值中,带有 “浣” 字就能搜索到

6.2.13 默认字段分词查询

GET /indexName/_search
{
    "query": {
        "query_string": {
            "default_field": "字段名",
            "query": "字段值"
        }
    }
}

注意:查询字段分词,就将查询条件分词查询,查询字段不分词,则将查询条件不分词查询

示例如下:

image-20260108215756903

6.3 搜索结果处理

6.3.1 排序

Elasticsearch 支持对搜索结果排序,默认是根据相关度算分(_score)来排序,可以排序字段类型有:keyword 类型、数值类型、地理坐标类型、日期类型等。其 DSL 语法如下:

GET /indexName/_search
{
	"query":{
		"match_all":{}
	},
	"sort": [
		"字段名": "desc"  //排序字段和排序方式ASC、DESC
	]
}

sort 里面是一个数组,意味着可以根据多个字段排序。多字段排序时,先按照第一个字段排序,第一个字段相等,再按第二个字段排序。下面是按照地理位置排序:

GET /indexName/_search
{
	"query":{
		"match_all":{}
	},
	"sort": [
		"_geo_distance": {
			"字段名":"经度,维度",
			"order":"asc",
			"unit":"km"    //单位是km
		}
	]
}

注意:一旦排序,文档的_score 字段(也就是得分)就会为空

6.3.2 分页

Elasticsearch 默认情况下只返回 top10 的数据,而如果要查询更多数据就需要修改分页参数了。通过修改 from、size 参数来控制要返回的分页结果,其 DSL 语法如下:

GET /indexName/_search
{
	"query":{
		"match_all":{}
	},
	"from": 990,      //分页开始的位置,默认是0
	"size":10,		  //期望获取的文档总数
	"sort": [
		{
			"price": "asc"
		}
	]
}

但是分页跟 MySQL 一样,都是先获取前 1000 条数据,截取 990-1000 这 10 个文档。所以也有深分页的问题。ES 设定结果集查询的上限是 10000

针对深分页,es 提供了 2 种解决方案:

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据,也就是 MySQL 中的,比如上一页最大 id 是 100,那下一页就查询 where id > 100。缺点是只能一页页地查。官方推荐使用的方式
  • scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用

6.3.3 高亮

高亮:就是在搜索结果中把搜索关键字突出显示

原理:将搜索结果中的关键字用标签标记出来,在页面中给标签添加 CSS 样式

其 DSL 语法如下:

GET /indexName/_search
{
	"query":{
		"match":{                //注意,这里一定不能使用match_all
			"字段名": "关键字" 
		}
	},
	"highlight": {
		"fields": {				//指定要高亮的字段
			"字段名":{			  //高亮字段和搜索字段必须一致
				"pre_tags": "<em>",			//用来标记高亮字段的前置标签
				"post_tags": "</em>",		//用例标记高亮字段的后置标签
			}
		}
	}
}

示例如下,查询所有字段

image-20260108220226159

也可以自定义标签

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意:高亮并没有修改原文档,而是把高亮结果放在 highlight 字段中

image-20260108220426524

6.3.4 返回指定字段[_source]

_source 关键字:是一个数组,在数组中用来指定展示那些字段

GET /indexName/_search
{
	"query":{
		"match_all":{}
	},
	"_source": ["字段名1", "字段名2"]
}

7 数据聚合

7.1 聚合种类

聚合(aggregations):可以实现对文档数据的统计、分析、运算。聚合常见的有 3 类:

  • 桶(Budget)聚合:用来对文档做分组。类似于 MySQL 的 group by
    • TermAggregation:按照文档字段值分组
    • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一个月为一组
  • 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等。只能根据数值字段做聚合
    • Avg:求平均值
    • Max:求最大值
    • Min:求最小值
    • Stats:同时求 max、min、avg、sum 等
  • 管道(pipeline)聚合:其它聚合的结果为基础做聚合

7.2 DSL 实现聚合

注意:text 类型是不支持聚合的

7.2.1 DSL 实现 Budget 聚合

比如,统计酒店品牌有几种。DSL 语法如下:

GET /indexName/_search
{
	"size": 0,						//搜索时分页的参数,为0表示显示的文档数量为0,表示结果中不包含文档,只包含聚合结果
	"aggs": {						//定义聚合
		"brandAgg": {				//给聚合起个名字,这是自定义的
			"terms": {				//聚合类型,按照品牌值聚合,所以选择term
				"field": "brand",	//参与聚合的字段
				"size": 20			//希望获取的聚合结果数量
			}
		}
	}
}

下面实战演示:

image-20251228220254231

上图中可以看到,自定义的聚合名称,在结果中展示了出来。key 是字段值,也就是品牌名称,doc_count 是文档数量

默认情况下,Budget 聚合会统计 Budget 内的文档数量,记为_count,并且按照 _count 降序排序,我们可以修改结果排序方式:

GET /indexName/_search
{
	"size": 0,						//搜索时分页的参数,为0表示显示的文档数量为0,表示结果中不包含文档,只包含聚合结果
	"aggs": {						//定义聚合
		"brandAgg": {				//给聚合起个名字,这是自定义的
			"terms": {				//聚合类型,按照品牌值聚合,所以选择term
				"field": "brand",	//参与聚合的字段
				"size": 20,			//希望获取的聚合结果数量
				"order": {
					"_count": "asc"  //按照 _count 升序排序
				}
			}
		}
	}
}

默认情况下,Budget 聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加 query 条件即可。

GET /indexName/_search
{	
    "query": {
    	"range": {
    		"price": {
    			"lte": 200			//只对200元以下的文档聚合
    		}
    	}
    }
	"size": 0,						//搜索时分页的参数,为0表示显示的文档数量为0,表示结果中不包含文档,只包含聚合结果
	"aggs": {						//定义聚合
		"brandAgg": {				//给聚合起个名字,这是自定义的
			"terms": {				//聚合类型,按照品牌值聚合,所以选择term
				"field": "brand",	//参与聚合的字段
				"size": 20,			//希望获取的聚合结果数量
				"order": {
					"_count": "asc"  //按照 _count 升序排序
				}
			}
		}
	}
}

7.2.2 DSL 实现 Metrics 聚合

例如,我们要获取每个品牌的用户评分的 min、max、avg 等值。我们可以利用 stats 聚合,其 DSL 语法如下:

GET /indexName/_search
{
	"size": 0,
	"aggs": {
		"brandAgg": {
			"terms": {
				"field": "brand",
				"size": 20
			},
			"aggs": {						//是brands聚合的子聚合,也就是分组后对每组分别计算
				"score_stats": {			//聚合名称
					"stats":  {				//聚合类型,这里stats可以计算min、max、avg等
						"field": "score"	//聚合字段,这里是score
					}
				}
			}
		}
	}
}

7.3 自动补全

什么是自动补全?如下图所示:我输入 s,立马就出现跟 s 有关的搜索项供我们选择

image-20251229221420985

7.3.1 拼音分词器

要实现根据字母做补全,就必须对文档按照拼音分词。在 Github 上恰好有 Elasticsearch 的拼音分词插件。地址是:https://github.com/medcl/elasticsearch-analysis-pinyin

安装方式跟 ik 分词器一样,分三步:解压、上传到 Elasticsearch 的 es-plugins 目录、重启 Elasticsearch

image-20251229222052998

上图是解压后,文件夹重命名为 py,表示拼音。然后上传到 es-plugins 目录下

下面实战演示:可以看到拼音分词器把中文都分成了拼音,甚至有把每个中文的首字母拼在一起

image-20251229222312602

7.4 数据同步

Elasticsearch 如何与 MySQL 保持数据同步呢?有以下几种方式:

  1. 同步调用,写入 MySQL 后,立马写入 es。业务耦合性太强,不推荐
  2. MQ 异步,写入 MySQL 后,发送消息到 MQ,消费者负责把数据写到 es 中。虽然复杂度上升了,但是解耦了
  3. 监听 MySQL 的 binlog,使用 canal

8 集群 Cluster

8.1 相关概念

8.1.1 集群

一个集群就是由一个或多个节点组织在一起,它们共同持有你整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,这个名字默认就是 elasticsearch。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群

为什么要引入集群?因为单节点存在以下问题:

  • 单节点故障问题。比如自然灾害、进程崩溃等导致单节点出现故障
  • 存在单节点并发压力
  • 存在单节点物理上限问题,比如单节点最多能存 2T 的数据,随着业务量增多,很快这 2T 的磁盘不够用

8.1.2 节点

一个节点是你进群中的一个服务器,作为集群的一部分,它存储你的数据,参与集群的索引和搜索功能。和集群类似,一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点

8.1.3 索引

一组相似文档的集合

8.1.4 映射

用来定义索引存储文档的结构,如:字段、类型等

8.1.5 文档

索引中一条记录,可以被索引的最小单元

8.1.6 分片

Elasticsearch 提供了将索引划分成多份的能力,这些份就叫做分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的 “索引”,这个 "索引"可以被放置到集群中的任何节点上

8.1.7 复制

Index 的分片中一份或多份副本

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐