Elasticsearch 系列
内容 链接 Elasticsearch 基础操作 本文 Elasticsearch 查询操作 https://blog.yexca.net/archives/227 RestClient 基础操作 https://blog.yexca.net/archives/228 RestClient 查询操作 https://blog.yexca.net/archives/229 Elasticsearch 数据聚合 https://blog.yexca.net/archives/231 Elasticsearch 自动补全 https://blog.yexca.net/archives/232
Elasticsearch 是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。结合 kibana、Logstash、Beats,也就是 elastic stack (ELK)。被广泛应用在日志数据分析、实时监控等领域
而 Elasticsearch 是 Elastic Stack 的核心,负责存储、搜索、分析数据
Elasticsearch 底层基于 Lucene 实现,Lucene 为 Java 的一个搜索引擎类库
正向索引
传统数据库 (例如 MySQL) 采用正向索引,例如下表
id | title | price |
---|---|---|
1 | 小米手机 | 3499 |
2 | 华为手机 | 4999 |
3 | 华为小米充电器 | 49 |
4 | 小米手环 | 239 |
如果基于 id 精准查询,直接走索引会很快
但若基于 title 做模糊查询,只能逐行扫描数据,流程:
- 用户搜索 手机,数据库条件
%手机%
- 逐行获取数据,如 id 为 1 的数据
- 判断数据中的 title 是否符合条件
- 符合则放入,不符合则舍弃,下一行
随着数据量的增加,逐行扫描的效率越来越低
倒排索引
倒排索引的概念是基于 MySQL 这样的正向索引而言的
Elasticsearch 采用倒排索引,概念:
- 文档 (document):每条数据就是一个文档
- 词条 (term):文档按照语义分成的词语
创建倒排索引是对正向索引的一种特殊处理,流程:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行包括词条、词条所在文档 id、位置等信息
- 因为词条的唯一性,可以给词条创建索引,例如 hash 表结构索引
例如上例的表可以创建如下倒排索引
词条 | 文档 id |
---|---|
小米 | 1,3,4 |
手机 | 1,2 |
华为 | 2,3 |
充电器 | 3 |
手环 | 4 |
倒排索引搜索流程:
- 用户搜索
小米手机
- 对搜索内容分词,得到
小米
、手机
- 使用词条在倒排索引查找,得到包含词条的文档 id:1、2、3、4
- 使用文档 id 到正向索引中查找具体文档
文档
Elasticsearch 是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为 json 格式后存储在 Elasticsearch 中
上述正向索引表的 json 如下
{
"id": 1,
"title": "小米手机",
"price": 3499
}
{
"id": 2,
"title": "华为手机",
"price": 4999
}
{
"id": 3,
"title": "华为小米充电器",
"price": 49
}
{
"id": 4,
"title": "小米手环",
"price": 299
}
在 Json 文档中包含很多字段,类似于数据库中的列
索引与映射
索引 (index) 为相同类型的文档的集合
映射 (mapping) 为索引中文档的字段约束信息,类似表的结构约束
可以把索引当做是数据库中的表,数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射,是索引中文档的字段约束信息,类似表的结构约束
MySQL 与 Elasticsearch
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 实现
- 两者再基于某种方式,实现数据的同步,保证一致性
优缺点
正向索引:
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描
倒排索引:
- 优点:
- 根据词条搜索、模糊搜索时,速度非常快
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
安装
一般只用 Elasticsearch 即可,使用 kibana 可以提供一个 elasticsearch 的可视化界面,方便学习书写 DSL 语句
Elasticsearch
为了使 Elasticsearch 与 kibana 容器互联,可以先创建一个网络
docker network create es-net
有多种方式可以实现互联,如 docker-compose、172.17.0.1
拉取 Elasticsearch
docker pull elasticsearch:7.12.1
单点部署
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
注意修改映射目录,上述使用数据卷,部分解释
-e "cluster.name=es-docker-cluster"
:设置集群名称-e "http.host=0.0.0.0"
:监听的地址,可以外网访问-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:内存大小-e "discovery.type=single-node"
:非集群模式-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录--privileged
:授予逻辑卷访问权--network es-net
:加入一个名为es-net的网络中
访问 localhost:9200 查看返回类似下述即启动成功
{
"name" : "6747e3f712ba",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "GSLtjxiMSlyRRRW-pSzvWQ",
"version" : {
"number" : "7.12.1",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "3186837139b9c6b6d23c3200870651f10d3343b7",
"build_date" : "2021-04-20T20:56:39.040728659Z",
"build_snapshot" : false,
"lucene_version" : "8.8.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
kibana
拉取同版本镜像
docker pull kibana:7.12.1
运行
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
其中 -e ELASTICSEARCH_HOSTS=http://es:9200"
:设置 elasticsearch 的地址,因为 kibana 已经与 elasticsearch 在一个网络,因此可以用容器名直接访问 elasticsearch
kibana 启动一般比较慢,需要多等一会,可以查看日志,若出现端口号则启动成功
docker logs -f kibana
访问 localhost:5601 查看结果
IK 分词器
es 在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好,例如测试
# 测试分词
POST /_analyze
{
"analyzer": "standard",
"text": "初次使用 Elasticsearch"
}
语法说明
- POST:请求方式
- /_analyze:请求路径。这里省略了 http://localhost:9200 ,由 kibana 补充
- 请求参数使用 JSON
- analyzer:分词器类型,默认为 standard
- text:需要分词的内容
结果为
{
"tokens" : [
{
"token" : "初",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "次",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "使",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "用",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "elasticsearch",
"start_offset" : 5,
"end_offset" : 18,
"type" : "<ALPHANUM>",
"position" : 4
}
]
}
可以看到分词效果非常不好,处理中文分词,一般会使用IK分词器
IK 分词器 Github: https://github.com/medcl/elasticsearch-analysis-ik
在线安装
注意安装版本与 es 对应
# 进入容器内部
docker exec -it elasticsearch /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
#退出
exit
#重启容器
docker restart elasticsearch
离线安装
安装插件需要知道 elasticsearch 的 plugins 目录位置,上述使用数据卷挂载到本地,可使用下面命令查看
docker volume inspect es-plugins
输出的 JSON 的 Mountpoint
即为目录
将从 Github 下载的压缩包解压后文件夹重命名为 ik
,放到 plugins 目录下
重启容器
docker restart es
测试效果
IK 分词器有两种模式
- ik_smart:最少切分
- ik_max_word:最细切分
还是上例
# 测试IK分词
POST /_analyze
{
"analyzer": "ik_smart",
"text": "初次使用 Elasticsearch"
}
结果
{
"tokens" : [
{
"token" : "初次",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "使用",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "elasticsearch",
"start_offset" : 5,
"end_offset" : 18,
"type" : "ENGLISH",
"position" : 2
}
]
}
该例两种分词模式结果相同,可使用其他更长语句测试结果
拓展词库
随着互联网发展,会不断涌现新词语,在原有的词汇列表中并不存在,所以词汇列表也需要不断更新。若拓展 IK 词库,只需要修改 ik 目录 config 目录中的 IKAnalyzer.cfg.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>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">stopwords.dic</entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
如上将拓展词放在 ./ext.dic
,禁止词放在 ./stopwords.dic
禁止词可以放一些无意义的词,如 的、啊 等
配置好后重启 es
DSL 索引库操作
索引库就类似数据库表,要向 es 中存储数据,必须先创建“库”和“表”
mapping 映射属性
mapping 是对索引库中文档的约束,常见的 mapping 属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
创建索引库
- 请求方式:PUT
- 请求路径:/索引库名,可以自定义
- 请求参数:mapping 映射
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// code
}
}
}
例如
# 创建索引库
PUT /hello
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": false
},
"name": {
"properties": {
"firstName": {
"type": "keyword"
},
"lastName": {
"type": "keyword"
}
}
}
}
}
}
运行后返回类似即成功
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "hello"
}
查询索引库
-
请求方式:GET
-
请求路径:/索引库名
-
请求参数:无
格式
GET /索引库名
例如
# 查看索引库
GET /hello
结果
{
"hello" : {
"aliases" : { },
"mappings" : {
"properties" : {
"email" : {
"type" : "keyword",
"index" : false
},
"info" : {
"type" : "text",
"analyzer" : "ik_smart"
},
"name" : {
"properties" : {
"firstName" : {
"type" : "keyword"
},
"lastName" : {
"type" : "keyword"
}
}
}
}
},
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "1",
"blocks" : {
"read_only_allow_delete" : "true"
},
"provided_name" : "hello",
"creation_date" : "1703683379263",
"number_of_replicas" : "1",
"uuid" : "zn-kPdsETZeFcB0nXK79hg",
"version" : {
"created" : "7120199"
}
}
}
}
}
修改索引库
索引库和 mapping 一旦创建,不允许修改,但可以添加字段
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
例如
# 新加字段
PUT /hello/_mapping
{
"properties": {
"age": {
"type": "integer",
"index": false
}
}
}
如果遇到 read-only-allow-delete
类似错误,产生原因为磁盘剩余空间不足 5%,可用以下请求解决
PUT _settings
{
"index": {
"blocks": {
"read_only_allow_delete": "false"
}
}
}
删除索引库
-
请求方式:DELETE
-
请求路径:/索引库名
-
请求参数:无
格式
DELETE /索引库名
例如
DELETE /hello
结果
{
"acknowledged" : true
}
索引库操作总结
- 创建索引库:PUT /索引库名
- 查询索引库:GET /索引库名
- 删除索引库:DELETE /索引库名
- 添加字段:PUT /索引库名/_mapping
DSL 文档操作
新增文档
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// code
}
例子
# 添加文档
PUT /hello/_doc/1
{
"info": "hello es",
"email": "[email protected]",
"name": {
"firstName": "yexca",
"lastName": "Dale"
}
}
结果
{
"_index" : "hello",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
查询文档
GET /{索引库名称}/_doc/{id}
例子
# 查询文档
GEt /hello/_doc/1
结果
{
"_index" : "hello",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"info" : "hello es",
"email" : "[email protected]",
"name" : {
"firstName" : "yexca",
"lastName" : "Dale"
}
}
}
修改文档
修改有两种方式,全量修改与增量修改
全量修改
全量修改是覆盖原来的文档,其本质是:
- 根据指定的 id 删除文档
- 新增一个相同 id 的文档
如果 id 不存在,也会执行第二步,也就从修改变成新增了 (覆盖写)
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// code
}
例如
# 修改-全量修改
PUT /hello/_doc/1
{
"info": "hello es",
"email": "[email protected]",
"name": {
"firstName": "yexca",
"lastName": "Dale"
}
}
结果
{
"_index" : "hello",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
查询可发现邮箱已经修改
增量修改
增量修改是只修改指定 id 匹配的文档中的部分字段
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
例如
# 修改-增量修改
POST /hello/_update/1
{
"doc": {
"email": "[email protected]"
}
}
结果
{
"_index" : "hello",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
查询可发现邮箱已经修改
删除文档
DELETE /{索引库名}/_doc/id值
例如
# 删除文档
DELETE /hello/_doc/1
结果
{
"_index" : "hello",
"_type" : "_doc",
"_id" : "1",
// 因为中间我修改了其他的,版本号变高了
"_version" : 8,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 7,
"_primary_term" : 1
}
文档操作总结
- 创建文档:POST /{索引库名}/_doc/文档id { json文档 }
- 查询文档:GET /{索引库名}/_doc/文档id
- 删除文档:DELETE /{索引库名}/_doc/文档id
- 修改文档:
- 全量修改:PUT /{索引库名}/_doc/文档id { json文档 }
- 增量修改:POST /{索引库名}/_update/文档id { “doc”: {字段}}