官方网站: https://www.elastic.co/
下载地址:https://www.elastic.co/cn/start
全文检索
数据分类:
- 结构化数据: 固定格式,有限长度 比如mysql存的数据
- 非结构化数据:不定长,无固定格式 比如邮件,word文档,日志
- 半结构化数据: 前两者结合 比如xml,html
搜索分类:
- 结构化数据搜索: 使用关系型数据库
- 非结构化数据搜索
- 顺序扫描
- 全文检索
设想一个关于搜索的场景,假设我们要搜索一首诗句内容中带“前”字的古诗
name | content | author |
---|---|---|
静夜思 | 床前明月光,疑是地上霜。举头望明月,低头思故乡。 | 李白 |
望庐山瀑布 | 日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。 | 李白 |
… | … | … |
什么是全文检索
全文检索是指:
- 通过一个程序扫描文本中的每一个单词,针对单词建立索引,并保存该单词在文本中的位置、以及出现的次数
- 用户查询时,通过之前建立好的索引来查询,将索引中单词对应的文本位置、出现的次数返回给用户,因为有了具体文本的位置,所以就可以将具体内容读取出来了
搜索原理简单概括的话可以分为这么几步:
- 内容爬取,停顿词过滤比如一些无用的像"的",“了”之类的语气词/连接词
内容分词,提取关键词
根据关键词建立倒排索引
- 用户输入关键词进行搜索
倒排索引
索引就类似于目录,平时我们使用的都是索引,都是通过主键定位到某条数据,那么倒排索引呢,刚好相反,数据对应到主键。
这里以一个博客文章的内容为例:
正排索引(正向索引)
文章ID文章标题文章内容1浅析JAVA设计模式JAVA设计模式是每一个JAVA程序员都应该掌握的进阶知识2JAVA多线程设计模式JAVA多线程与设计模式结合
文章ID | 文章标题 | 文章内容 |
---|---|---|
1 | 浅析JAVA设计模式 | AVA设计模式是每一个JAVA程序员都应该掌握的进阶知识 |
2 | JAVA多线程设计模式 | JAVA多线程与设计模式结合 |
倒排索引(反向索引)
假如,我们有一个站内搜索的功能,通过某个关键词来搜索相关的文章,那么这个关键词可能出现在标题中,也可能出现在文章内容中,那我们将会在创建或修改文章的时候,建立一个关键词与文章的对应关系表,这种,我们可以称之为倒排索引。
like %java设计模式% java 设计模式
关键词 | 文章ID |
---|---|
JAVA | 1,2 |
设计模式 | 1,2 |
多线程 | 2 |
简单理解,正向索引是通过key找value,反向索引则是通过value找key。ES底层在检索时底层使用的就是倒排索引。
ElasticSearch简介
ElasticSearch是什么
ElasticSearch(简称ES)是一个分布式、RESTful 风格的搜索和数据分析引擎,是用Java开发并且是当前最流行的开源的企业级搜索引擎,能够达到近实时搜索,稳定,可靠,快速,安装使用方便。
客户端支持Java、.NET(C#)、PHP、Python、Ruby等多种语言。
Elasticsearch 与 Lucene 核心库竞争的优势在于:
- 完美封装了 Lucene 核心库,设计了友好的 Restful-API,开发者无需过多关注底层机制,直接开箱即用。
- 分片与副本机制,直接解决了集群下性能与高可用问题。
ES Server进程 3节点 raft (奇数节点)
数据分片 -》lucene实例 分片和副本数 1个ES节点可以有多个lucene实例。也可以指定一个索引的多个分片
ElasticSearch vs Solr
Solr 是第一个基于 Lucene 核心库功能完备的搜索引擎产品,诞生远早于 Elasticsearch。
当单纯的对已有数据进行搜索时,Solr更快。当实时建立索引时, Solr会产生io阻塞,查询性能较差, Elasticsearch具有明显的优势。
总结:
- Solr 利用 Zookeeper 进行分布式管理,而Elasticsearch 自身带有分布式协调管理功能。
- Solr 支持更多格式的数据,比如JSON、XML、CSV,而 Elasticsearch 仅支持json文件格式。
Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch。
- Solr 是传统搜索应用的有力解决方案,但 Elasticsearch更适用于新兴的实时搜索应用。
Elastic Stack介绍
在Elastic Stack之前我们听说过ELK,ELK分别是Elasticsearch,Logstash,Kibana这三款软件在一起的简称,在发展的过程中又有新的成员Beats的加入,就形成了Elastic Stack。
在Elastic Stack生态圈中Elasticsearch作为数据存储和搜索,是生态圈的基石,Kibana在上层提供用户一个可视化及操作的界面,Logstash和Beat可以对数据进行收集。在上图的右侧X-Pack部分则是Elastic公司提供的商业项目。
指标分析/日志分析:
ElasticSearch应用场景
- 站内搜索
- 日志管理与分析
- 大数据分析
- 应用性能监控
- 机器学习
国内现在有大量的公司都在使用 Elasticsearch,包括携程、滴滴、今日头条、饿了么、360安全、小米、vivo等诸多知名公司。除了搜索之外,结合Kibana、Logstash、Beats,Elastic Stack还被广泛运用在大数据近实时分析领域,包括日志分析、指标监控、信息安全等多个领域。它可以帮助你探索海量结构化、非结构化数据,按需创建可视化报表,对监控数据设置报警阈值,甚至通过使用机器学习技术,自动识别异常状况。
ElasticSearch文件目录结构
主配置文件elasticsearch.yml
-
cluster.name
当前节点所属集群名称,多个节点如果要组成同一个集群,那么集群名称一定要配置成相同。默认值elasticsearch,生产环境建议根据ES集群的使用目的修改成合适的名字。 -
node.name
当前节点名称,默认值当前节点部署所在机器的主机名,所以如果一台机器上要起多个ES节点的话,需要通过配置该属性明确指定不同的节点名称。 -
path.data
配置数据存储目录,比如索引数据等,默认值 $ES_HOME/data,生产环境下强烈建议部署到另外的安全目录,防止ES升级导致数据被误删除。 -
path.logs
配置日志存储目录,比如运行日志和集群健康信息等,默认值 $ES_HOME/logs,生产环境下强烈建议部署到另外的安全目录,防止ES升级导致数据被误删除。 -
bootstrap.memory_lock
配置ES启动时是否进行内存锁定检查,默认值true。
ES对于内存的需求比较大,一般生产环境建议配置大内存,如果内存不足,容易导致内存交换到磁盘,严重影响ES的性能。所以默认启动时进行相应大小内存的锁定,如果无法锁定则会启动失败。
非生产环境可能机器内存本身就很小,能够供给ES使用的就更小,如果该参数配置为true的话很可能导致无法锁定内存以致ES无法成功启动,此时可以修改为false。 -
network.host
配置能够访问当前节点的主机,默认值为当前节点所在机器的本机回环地址127.0.0.1 和[::1],这就导致默认情况下只能通过当前节点所在主机访问当前节点。可以配置为 0.0.0.0 ,表示所有主机均可访问。 -
http.port
配置当前ES节点对外提供服务的http端口,默认值 9200 -
discovery.seed_hosts
配置参与集群节点发现过程的主机列表,说白一点就是集群中所有节点所在的主机列表,可以是具体的IP地址,也可以是可解析的域名。 -
cluster.initial_master_nodes
配置ES集群初始化时参与master选举的节点名称列表,必须与node.name配置的一致。ES集群首次构建完成后,应该将集群中所有节点的配置文件中的cluster.initial_master_nodes配置项移除,重启集群或者将新节点加入某个已存在的集群时切记不要设置该配置项。#ES开启远程访问
network.host: 0.0.0.0
修改JVM配置
修改config/jvm.options配置文件,调整jvm堆内存大小
配置的建议
Xms和Xms设置成—样
Xmx不要超过机器内存的50%
不要超过30GB - https://www.elastic.co/cn/blog/a-heap-of-trouble
vim jvm.options
-Xms4g
-Xmx4g
启动ElasticSearch服务
Windows
直接运行elasticsearch.bat
Linux(centos7)
ES不允许使用root账号启动服务,如果你当前账号是root,则需要创建一个专有账户
#非root用户
bin/elasticsearch
# -d 后台启动
bin/elasticsearch -d
注意:es默认不能用root用户启动,生产环境建议为elasticsearch创建用户。
#为elaticsearch创建用户并赋予相应权限
adduser es
passwd es
chown -R es:es elasticsearch-17.3
如果ES服务启动异常,会有提示:
客户端Kibana安装
Kibana是一个开源分析和可视化平台,旨在与Elasticsearch协同工作。
1)下载并解压缩Kibana
下载地址:https://www.elastic.co/cn/downloads/past-releases#kibana
选择版本:7.17.3
2)修改Kibana.yml
vim config/kibana.yml
server.port: 5601
server.host: "localhost" #服务器ip
elasticsearch.hosts: ["http://localhost:9200"] #elasticsearch的访问地址
i18n.locale: "zh-CN" #Kibana汉化
3)运行Kibana
注意:kibana也需要非root用户启动
bin/kibana
#后台启动
nohup bin/kibana &
#查询kibana进程
netstat -tunlp | grep 5601
访问Kibana: http://localhost:5601/
cat API
/_cat/allocation #查看单节点的shard分配整体情况
/_cat/shards #查看各shard的详细情况
/_cat/shards/{index} #查看指定分片的详细情况
/_cat/master #查看master节点信息
/_cat/nodes #查看所有节点信息
/_cat/indices #查看集群中所有index的详细信息
/_cat/indices/{index} #查看集群中指定index的详细信息
/_cat/segments #查看各index的segment详细信息,包括segment名, 所属shard, 内存(磁盘)占用大小, 是否刷盘
/_cat/segments/{index}#查看指定index的segment详细信息
/_cat/count #查看当前集群的doc数量
/_cat/count/{index} #查看指定索引的doc数量
/_cat/recovery #查看集群内每个shard的recovery过程.调整replica。
/_cat/recovery/{index}#查看指定索引shard的recovery过程
/_cat/health #查看集群当前状态:红、黄、绿
/_cat/pending_tasks #查看当前集群的pending task
/_cat/aliases #查看集群中所有alias信息,路由配置等
/_cat/aliases/{alias} #查看指定索引的alias信息
/_cat/thread_pool #查看集群各节点内部不同类型的threadpool的统计信息,
/_cat/plugins #查看集群各个节点上的plugin信息
/_cat/fielddata #查看当前集群各个节点的fielddata内存使用情况
/_cat/fielddata/{fields} #查看指定field的内存使用情况,里面传field属性对应的值
/_cat/nodeattrs #查看单节点的自定义属性
/_cat/repositories #输出集群中注册快照存储库
/_cat/templates #输出当前正在存在的模板信息
Elasticsearch安装分词插件
Elasticsearch提供插件机制对系统进行扩展
以安装analysis-icu这个分词插件为例
在线安装
#查看已安装插件
bin/elasticsearch-plugin list
#安装插件
bin/elasticsearch-plugin install analysis-icu
#删除插件
bin/elasticsearch-plugin remove analysis-icu
注意:安装和删除完插件后,需要重启ES服务才能生效。
测试分词效果
POST _analyze
{
"analyzer":"icu_analyzer",
"text":"中华人民共和国"
}
离线安装
本地下载相应的插件,解压,然后手动上传到elasticsearch的plugins目录,然后重启ES实例就可以了。
比如ik中文分词插件:https://github.com/medcl/elasticsearch-analysis-ik
测试分词效果
#ES的默认分词设置是standard,会单字拆分
POST _analyze
{
"analyzer":"standard",
"text":"中华人民共和国"
}
#ik_smart:会做最粗粒度的拆
POST _analyze
{
"analyzer": "ik_smart",
"text": "中华人民共和国"
}
#ik_max_word:会将文本做最细粒度的拆分
POST _analyze
{
"analyzer":"ik_max_word",
"text":"中华人民共和国"
}
创建索引时可以指定IK分词器作为默认分词器
PUT /es_db
{
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
}
}
}
ik切词器扩展
ElasticSearch基本概念
关系型数据库 VS ElasticSearch
- 在7.0之前,一个 Index可以设置多个Types
- 目前Type已经被Deprecated,7.0开始,一个索引只能创建一个Type - “_doc”
- 传统关系型数据库和Elasticsearch的区别:
- Elasticsearch- Schemaless /相关性/高性能全文检索
- RDMS —事务性/ Join
索引(Index)
一个索引就是一个拥有几分相似特征的文档的集合。
比如说,可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。
一个索引由一个名字来标识(必须全部是小写字母的)
,并且当我们要对对应于这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。
文档(Document)
Elasticsearch是面向文档的,文档是所有可搜索数据的最小单位。
- 日志文件中的日志项
- 一本电影的具体信息/一张唱片的详细信息
- MP3播放器里的一首歌/一篇PDF文档中的具体内容
文档会被序列化成JSON格式,保存在Elasticsearch中
- JSON对象由字段组成
- 每个字段都有对应的字段类型(字符串/数值/布尔/日期/二进制/范围类型)
- 每个文档都有一个Unique ID
- 可以自己指定ID或者通过Elasticsearch自动生成
- 一篇文档包含了一系列字段,类似数据库表中的一条记录
- JSON文档,格式灵活,不需要预先定义格式
- 字段的类型可以指定或者通过Elasticsearch自动推算
- 支持数组/支持嵌套
文档元数据
元数据,用于标注文档的相关信息:
_index:文档所属的索引名
- _type:文档所属的类型名
- _id:文档唯—ld
_source: 文档的原始Json数据
- _version: 文档的版本号,修改删除操作_version都会自增1
- _seq_no: 和_version一样,一旦数据发生更改,数据也一直是累计的。Shard级别严格递增,保证后写入的Doc的_seq_no大于先写入的Doc的_seq_no。
- _primary_term: _primary_term主要是用来恢复数据时处理当多个文档的_seq_no一样时的冲突,避免Primary Shard上的写入被覆盖。每当Primary Shard发生重新分配时,比如重启,Primary选举等,_primary_term会递增1。
ElasticSearch索引操作
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/index.html
创建索引
索引命名必须小写,不能以下划线开头
格式: PUT /索引名称
#创建索引
PUT /es_db
#创建索引时可以设置分片数和副本数
PUT /es_db
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 2
}
}
#修改索引配置
PUT /es_db/_settings
{
"index" : {
"number_of_replicas" : 1
}
}
查询索引
格式: GET /索引名称
#查询索引
GET /es_db
#es_db是否存在
HEAD /es_db
删除索引
格式: DELETE /索引名称
DELETE /es_db
ElasticSearch文档操作
示例数据
PUT /es_db
{
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
}
}
}
PUT /es_db/_doc/1
{
"name": "张三",
"sex": 1,
"age": 25,
"address": "广州天河公园",
"remark": "java developer"
}
PUT /es_db/_doc/2
{
"name": "李四",
"sex": 1,
"age": 28,
"address": "广州荔湾大厦",
"remark": "java assistant"
}
PUT /es_db/_doc/3
{
"name": "王五",
"sex": 0,
"age": 26,
"address": "广州白云山公园",
"remark": "php developer"
}
PUT /es_db/_doc/4
{
"name": "赵六",
"sex": 0,
"age": 22,
"address": "长沙橘子洲",
"remark": "python assistant"
}
PUT /es_db/_doc/5
{
"name": "张龙",
"sex": 0,
"age": 19,
"address": "长沙麓谷企业广场",
"remark": "java architect assistant"
}
PUT /es_db/_doc/6
{
"name": "赵虎",
"sex": 1,
"age": 32,
"address": "长沙麓谷兴工国际产业园",
"remark": "java architect"
}
添加(索引)文档
格式: [PUT | POST] /索引名称/[_doc | _create ]/id
# 创建文档,指定id
# 如果id不存在,创建新的文档,否则先删除现有文档,再创建新的文档,版本会增加
PUT /es_db/_doc/1
{
"name": "张三",
"sex": 1,
"age": 25,
"address": "广州天河公园",
"remark": "java developer"
}
#创建文档,ES生成id
POST /es_db/_doc
{
"name": "张三",
"sex": 1,
"age": 25,
"address": "广州天河公园",
"remark": "java developer"
}
注意:POST和PUT都能起到创建/更新的作用,PUT需要对一个具体的资源进行操作也就是要确定id才能进行更新/创建
,而POST是可以针对整个资源集合进行操作的,如果不写id就由ES生成一个唯一id进行创建新文档,如果填了id那就针对这个id的文档进行创建/更新
Create -如果ID已经存在,会失败
修改文档
- 全量更新,整个json都会替换,格式: [PUT | POST] /索引名称/_doc/id
如果文档存在,现有文档会被删除,新的文档会被索引
全量更新,替换整个json
PUT /es_db/_doc/1
{
"name": "张三",
"sex": 1,
"age": 25
}
#查询文档
GET /es_db/_doc/1
使用_update部分更新,格式: POST /索引名称/_update/id
update不会删除原来的文档,而是实现真正的数据更新
# 部分更新:在原有文档上更新
# Update -文档必须已经存在,更新只会对相应字段做增量修改
POST /es_db/_update/1
{
"doc": {
"age": 28
}
}
#查询文档
GET /es_db/_doc/1
使用 _update_by_query 更新文档
POST /es_db/_update_by_query
{
"query": {
"match": {
"_id": 1
}
},
"script": {
"source": "ctx._source.age = 30"
}
}
并发场景下修改文档
_seq_no和_primary_term是对_version的优化,7.X版本的ES默认使用这种方式控制版本,所以当在高并发环境下使用乐观锁机制修改文档时,要带上当前文档的_seq_no和_primary_term进行更新:
POST /es_db/_doc/2?if_seq_no=21&if_primary_term=6
{
"name": "李四xxx"
}
如果版本号不对,会抛出版本冲突异常,如下图:
查询文档
根据id查询文档,格式: GET /索引名称/_doc/id
GET /es_db/_doc/1
条件查询 _search,格式: /索引名称/_doc/_search
# 查询前10条文档
GET /es_db/_doc/_search
ES Search API提供了两种条件查询搜索方式:
REST风格的请求URI,直接将参数带过去
封装到request body中,这种方式可以定义更加易读的JSON格式
#通过URI搜索,使用“q”指定查询字符串,“query string syntax” KV键值对
#条件查询, 如要查询age等于28岁的 _search?q=*:***
GET /es_db/_doc/_search?q=age:28
#范围查询, 如要查询age在25至26岁之间的 _search?q=***[** TO **] 注意: TO 必须为大写
GET /es_db/_doc/_search?q=age[25 TO 26]
#查询年龄小于等于28岁的 :<=
GET /es_db/_doc/_search?q=age:<=28
#查询年龄大于28前的 :>
GET /es_db/_doc/_search?q=age:>28
#分页查询 from=*&size=*
GET /es_db/_doc/_search?q=age[25 TO 26]&from=0&size=1
#对查询结果只输出某些字段 _source=字段,字段
GET /es_db/_doc/_search?_source=name,age
#对查询结果排序 sort=字段:desc/asc
GET /es_db/_doc/_search?sort=age:desc
通过请求体的搜索方式会在第二节课中详细讲解(DSL)
GET /es_db/_search
{
"query": {
"match": {
"address": "广州白云"
}
}
}
删除文档
格式: DELETE /索引名称/_doc/id
DELETE /es_db/_doc/1
ElasticSearch文档批量操作
批量操作可以减少网络连接所产生的开销,提升性能
支持在一次API调用中,对不同的索引进行操作
- 可以在URI中指定Index,也可以在请求的Payload中进行
操作中单条操作失败,并不会影响其他操作
- 返回结果包括了每一条操作执行的结果
批量写入
批量对文档进行写操作是通过_bulk的API来实现的
- 请求方式:POST
- 请求地址:_bulk
- 请求参数:通过_bulk操作文档,一般至少有两行参数(或偶数行参数)
- 第一行参数为指定操作的类型及操作的对象(index,type和id)
- 第二行参数才是操作的数据
参数类似于:
{"actionName":{"_index":"indexName", "_type":"typeName","_id":"id"}}
{"field1":"value1", "field2":"value2"}
-
actionName:表示操作类型,主要有create,index,delete和update
-
批量创建文档create
POST _bulk {"create":{"_index":"article", "_type":"_doc", "_id":3}} {"id":3,"title":"fox老师","content":"fox老师666","tags":["java", "面向对象"],"create_time":1554015482530} {"create":{"_index":"article", "_type":"_doc", "_id":4}} {"id":4,"title":"mark老师","content":"mark老师NB","tags":["java", "面向对象"],
普通创建或全量替换index
POST _bulk
{"index":{"_index":"article", "_type":"_doc", "_id":3}}
{"id":3,"title":"北京","content":"北京666","tags":["景点", "万里长城"],"create_time":1554015482530}
{"index":{"_index":"article", "_type":"_doc", "_id":4}}
{"id":4,"title":"上海","content":"上海NB","tags":["景点", "东方明珠"],"create_time":1554015482530}
- 如果原文档不存在,则是创建
- 如果原文档存在,则是替换(全量修改原文档)
批量删除delete
POST _bulk
{"delete":{"_index":"article", "_type":"_doc", "_id":3}}
{"delete":{"_index":"article", "_type":"_doc", "_id":4}}
批量修改update
POST _bulk
{"update":{"_index":"article", "_type":"_doc", "_id":3}}
{"doc":{"title":"ES大法必修内功"}}
{"update":{"_index":"article", "_type":"_doc", "_id":4}}
{"doc":{"create_time":1554018421008}}
组合应用
POST _bulk
{"create":{"_index":"article", "_type":"_doc", "_id":3}}
{"id":3,"title":"fox老师","content":"fox老师666","tags":["java", "面向对象"],"create_time":1554015482530}
{"delete":{"_index":"article", "_type":"_doc", "_id":3}}
{"update":{"_index":"article", "_type":"_doc", "_id":4}}
{"doc":{"create_time":1554018421008}}
批量读取
es的批量查询可以使用mget和msearch两种。其中mget是需要我们知道它的id,可以指定不同的index,也可以指定返回值source。msearch可以通过字段查询来进行一个批量的查找。
_mget
#可以通过ID批量获取不同index和type的数据
GET _mget
{
"docs": [
{
"_index": "es_db",
"_id": 1
},
{
"_index": "article",
"_id": 4
}
]
}
#可以通过ID批量获取es_db的数据
GET /es_db/_mget
{
"docs": [
{
"_id": 1
},
{
"_id": 4
}
]
}
#简化后
GET /es_db/_mget
{
"ids":["1","2"]
}
_msearch
在_msearch中,请求格式和bulk类似。查询一条数据需要两个对象,第一个设置index和type,第二个设置查询语句。查询语句和search相同。如果只是查询一个index,我们可以在url中带上index,这样,如果查该index可以直接用空对象表示。
GET /es_db/_msearch
{}
{"query" : {"match_all" : {}}, "from" : 0, "size" : 2}
{"index" : "article"}
{"query" : {"match_all" : {}}}
ElasticSearch 高级查询语法Query DSL
倒排索引
索引就类似于目录,平时我们使用的都是索引,都是通过主键定位到某条数据,那么倒排索引呢,刚好相反,数据对应到主键。
为了进一步提升索引的效率,ES 在 term 的基础上利用 term 的前缀或者后缀构建了 term index, 用于对 term 本身进行索引,ES 实际的索引结构如下图所示:
这样当我们去搜索某个关键词时,ES 首先根据它的前缀或者后缀迅速缩小关键词的在 term dictionary 中的范围,大大减少了磁盘IO的次数。
- 单词词典(Term Dictionary) :记录所有文档的单词,记录单词到倒排列表的关联关系
- 常用字典数据结构:https://www.cnblogs.com/LBSer/p/4119841.html
- 倒排列表(Posting List)-记录了单词对应的文档结合,由倒排索引项组成
- 倒排索引项(Posting):
- 文档ID
- 词频TF–该单词在文档中出现的次数,用于相关性评分
- 位置(Position)-单词在文档中分词的位置。用于短语搜索(match phrase query)
- 偏移(Offset)-记录单词的开始结束位置,实现高亮显示
Elasticsearch 的JSON文档中的每个字段,都有自己的倒排索引
。
可以指定对某些字段不做索引:
- 优点︰节省存储空间
- 缺点: 字段无法被搜索
Elasticsearch 的JSON文档中的每个字段,都有自己的倒排索引
。
可以指定对某些字段不做索引:
- 优点︰节省存储空间
- 缺点: 字段无法被搜索
文档映射Mapping
Mapping类似数据库中的schema的定义
,作用如下:
- 定义索引中的字段的名称
- 定义字段的数据类型,例如字符串,数字,布尔等
- 字段,倒排索引的相关配置(Analyzer)
ES中Mapping映射可以分为动态映射和静态映射
。
动态映射:
在关系数据库中,需要事先创建数据库,然后在该数据库下创建数据表,并创建表字段、类型、长度、主键等,最后才能基于表插入数据。而Elasticsearch中不需要定义Mapping映射(即关系型数据库的表、字段等),在文档写入Elasticsearch时,会根据文档字段自动识别类型,这种机制称之为动态映射
。
静态映射:
静态映射是在Elasticsearch中也可以事先定义好映射,包含文档的各字段类型、分词器等,这种方式称之为静态映射。
动态映射(Dynamic Mapping)的机制,使得我们无需手动定义Mappings,Elasticsearch会自动根据文档信息,推算出字段的类型。但是有时候会推算的不对,例如地理位置信息。当类型如果设置不对时,会导致一些功能无法正常运行,例如Range查询
Dynamic Mapping类型自动识别:
示例
#删除原索引
DELETE /user
#创建文档(ES根据数据类型, 会自动创建映射)
PUT /user/_doc/1
{
"name":"yuyang",
"age":18,
"address":"北京"
}
#获取文档映射
GET /user/_mapping
后期更改Mapping的字段类型?
-
新增加字段
- dynamic设为true时,一旦有新增字段的文档写入,Mapping 也同时被更新
- dynamic设为false,Mapping 不会被更新,新增字段的数据无法被索引,但是信息会出现在_source中
- dynamic设置成strict(严格控制策略),文档写入失败,抛出异常
| |true | false | strict |
|–|–|–|–|
|文档可索引 | yes |yes | no|
|字段可索引 | yes |no | no|
|Mapping被更新 | yes |no | no|
-
对已有字段,一旦已经有数据写入,就不再支持修改字段定义
- Lucene 实现的倒排索引,一旦生成后,就不允许修改
- 如果希望改变字段类型,可以利用 reindex API,重建索引
原因:
-
如果修改了字段的数据类型,会导致已被索引的数据无法被搜索
-
但是如果是增加新的字段,就不会有这样的影响
测试
PUT /user
{
"mappings": {
"dynamic": "strict",
"properties": {
"name": {
"type": "text"
},
"address": {
"type": "object",
"dynamic": "true"
}
}
}
}
# 插入文档报错,原因为age为新增字段,会抛出异常
PUT /user/_doc/1
{
"name":"fox",
"age":32,
"address":{
"province":"浙江",
"city":"杭州"
}
}
dynamic设置成strict,新增age字段导致文档插入失败
修改dynamic后再次插入文档成功
#修改daynamic
PUT /user/_mapping
{
"dynamic":true
}
对已有字段的mapping修改
具体方法:
1)如果要推倒现有的映射, 你得重新建立一个静态索引
2)然后把之前索引里的数据导入到新的索引里
3)删除原创建的索引
4)为新索引起个别名, 为原索引名
PUT /user2
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"address": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
POST _reindex
{
"source": {
"index": "user"
},
"dest": {
"index": "user2"
}
}
DELETE /user
PUT /user2/_alias/user
GET /user
常用Mapping参数配置
- index: 控制当前字段是否被索引,默认为true。
如果设置为false,该字段不可被搜索
DELETE /user
PUT /user
{
"mappings" : {
"properties" : {
"address" : {
"type" : "text",
"index": false
},
"age" : {
"type" : "long"
},
"name" : {
"type" : "text"
}
}
}
}
PUT /user/_doc/1
{
"name":"yuyang",
"address":"杭州西湖公园",
"age":18
}
GET /user
GET /user/_search
{
"query": {
"match": {
"address": "杭州"
}
}
}
- 有四种不同基本的index options配置,控制倒排索引记录的内容:
- docs : 记录doc id
- freqs:记录doc id 和term frequencies(词频)
- positions: 记录doc id / term frequencies / term position
- offsets: doc id / term frequencies / term posistion / character offsets
text类型默认记录postions,其他默认为 docs。记录内容越多,占用存储空间越大
DELETE /user
PUT /user
{
"mappings" : {
"properties" : {
"address" : {
"type" : "text",
"index_options": "offsets"
},
"age" : {
"type" : "long"
},
"name" : {
"type" : "text"
}
}
}
}
null_value: 需要对Null值进行搜索,只有keyword类型支持设计Null_Value
DELETE /user
PUT /user
{
"mappings" : {
"properties" : {
"address" : {
"type" : "keyword",
"null_value": "NULL"
},
"age" : {
"type" : "long"
},
"name" : {
"type" : "text"
}
}
}
}
PUT /user/_doc/1
{
"name":"yuyang",
"age":18,
"address":null
}
GET /user/_search
{
"query": {
"match": {
"address": "NULL"
}
}
}
- copy_to设置:将字段的数值拷贝到目标字段,满足一些特定的搜索需求。copy_to的目标字段不出现在_source中。
# 设置copy_to
DELETE /address
PUT /address
{
"mappings" : {
"properties" : {
"province" : {
"type" : "keyword",
"copy_to": "full_address"
},
"city" : {
"type" : "text",
"copy_to": "full_address"
}
}
},
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
}
}
}
PUT /address/_bulk
{ "index": { "_id": "1"} }
{"province": "湖南","city": "长沙"}
{ "index": { "_id": "2"} }
{"province": "湖南","city": "常德"}
{ "index": { "_id": "3"} }
{"province": "广东","city": "广州"}
{ "index": { "_id": "4"} }
{"province": "湖南","city": "邵阳"}
GET /address/_search
{
"query": {
"match": {
"full_address": {
"query": "湖南常德",
"operator": "and"
}
}
}
}
Index Template
Index Templates可以帮助你设定Mappings和Settings,并按照一定的规则,自动匹配到新创建的索引之上
- 模版仅在一个索引被新创建时,才会产生作用。修改模版不会影响已创建的索引
- 你可以设定多个索引模版,这些设置会被“merge”在一起
- 你可以指定“order”的数值,控制“merging”的过程
PUT /_template/template_default
{
"index_patterns": ["*"],
"order": 0,
"version": 1,
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
}
}
PUT /_template/template_test
{
"index_patterns": ["test*"],
"order": 1,
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1
},
"mappings": {
"date_detection": false,
"numeric_detection": true
}
}
lndex Template的工作方式
当一个索引被新创建时:
- 应用Elasticsearch 默认的settings 和mappings
- 应用order数值低的lndex Template 中的设定
- 应用order高的 Index Template 中的设定,之前的设定会被覆盖
- 应用创建索引时,用户所指定的Settings和 Mappings,并覆盖之前模版中的设定
#查看template信息
GET /_template/template_default
GET /_template/temp*
PUT /testtemplate/_doc/1
{
"orderNo": 1,
"createDate": "2022/01/01"
}
GET /testtemplate/_mapping
GET /testtemplate/_settings
PUT /testmy
{
"mappings": {
"date_detection": true
}
}
PUT /testmy/_doc/1
{
"orderNo": 1,
"createDate": "2022/01/01"
}
GET /testmy/_mapping
Dynamic Template
Dynamic Tempate定义在某个索引的Mapping中。
#Dynaminc Mapping 根据类型和字段名
DELETE my_index
PUT my_index/_doc/1
{
"firstName":"Ruan",
"isVIP":"true"
}
GET my_index/_mapping
DELETE my_index
PUT my_index
{
"mappings": {
"dynamic_templates": [
{
"strings_as_boolean": {
"match_mapping_type": "string",
"match":"is*",
"mapping": {
"type": "boolean"
}
}
},
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
]
}
}
#结合路径
PUT /my_test_index
{
"mappings": {
"dynamic_templates": [
{
"full_name":{
"path_match": "name.*",
"path_unmatch": "*.middle",
"mapping":{
"type": "text",
"copy_to": "full_name"
}
}
}
]
}
}
PUT /my_test_index/_doc/1
{
"name":{
"first": "John",
"middle": "Winston",
"last": "Lennon"
}
}
GET /my_test_index/_search
{
"query": {
"match": {
"full_name": "John"
}
}
}
ES高级查询Query DSL
ES中提供了一种强大的检索数据方式,这种检索方式称之为Query DSL(Domain Specified Language) , Query DSL是利用Rest API传递JSON格式的请求体(RequestBody)数据与ES进行交互,这种方式的丰富查询语法让ES检索变得更强大,更简洁。
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl.html
语法:
GET /es_db/_doc/_search {json请求体数据}
可以简化为下面写法
GET /es_db/_search {json请求体数据}
示例
#无条件查询,默认返回10条数据
GET /es_db/_search
{
"query":{
"match_all":{}
}
}
示例数据
#指定ik分词器
PUT /es_db
{
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
}
}
}
# 创建文档,指定id
PUT /es_db/_doc/1
{
"name": "张三",
"sex": 1,
"age": 25,
"address": "广州天河公园",
"remark": "java developer"
}
PUT /es_db/_doc/2
{
"name": "李四",
"sex": 1,
"age": 28,
"address": "广州荔湾大厦",
"remark": "java assistant"
}
PUT /es_db/_doc/3
{
"name": "王五",
"sex": 0,
"age": 26,
"address": "广州白云山公园",
"remark": "php developer"
}
PUT /es_db/_doc/4
{
"name": "赵六",
"sex": 0,
"age": 22,
"address": "长沙橘子洲",
"remark": "python assistant"
}
PUT /es_db/_doc/5
{
"name": "张龙",
"sex": 0,
"age": 19,
"address": "长沙麓谷企业广场",
"remark": "java architect assistant"
}
PUT /es_db/_doc/6
{
"name": "赵虎",
"sex": 1,
"age": 32,
"address": "长沙麓谷兴工国际产业园",
"remark": "java architect"
}
查询所有match_all
使用match_all,默认只会返回10条数据。
原因:_search查询默认采用的是分页查询,每页记录数size的默认值为10。
如果想显示更多数据,指定size
GET /es_db/_search
等同于
GET /es_db/_search
{
"query":{
"match_all":{}
}
}
返回指定条数size
size 关键字: 指定查询结果中返回指定条数。 默认返回值10条
GET /es_db/_search
{
"query": {
"match_all": {}
},
"size": 100
}
返回指定条数size
size 关键字: 指定查询结果中返回指定条数。 默认返回值10条
GET /es_db/_search
{
"query": {
"match_all": {}
},
"size": 100
}
size可以无限增加吗?
测试
GET /es_db/_search
{
"query": {
"match_all": {}
},
"size": 20000
}
出现异常:
异常原因:
1、查询结果的窗口太大,from + size的结果必须小于或等于10000,而当前查询结果的窗口为20000。
2、可以采用scroll api更高效的请求大量数据集。
3、查询结果的窗口的限制可以通过参数index.max_result_window进行设置。
PUT /es_db/_settings
{
"index.max_result_window" :"20000"
}
#修改现有所有的索引,但新增的索引,还是默认的10000
PUT /_all/_settings
{
"index.max_result_window" :"20000"
}
#查看所有索引中的index.max_result_window值
GET /_all/_settings/index.max_result_window
注意:参数index.max_result_window主要用来限制单次查询满足查询条件的结果窗口的大小,窗口大小由from + size共同决定。不能简单理解成查询返回给调用方的数据量。这样做主要是为了限制内存的消耗
。
比如:from为1000000,size为10,逻辑意义是从满足条件的数据中取1000000到(1000000 + 10)的记录。这时ES一定要先将(1000000 + 10)的记录(即result_window)加载到内存中,再进行分页取值的操作。尽管最后我们只取了10条数据返回给客户端,但ES进程执行查询操作的过程中确需要将(1000000 + 10)的记录都加载到内存中,可想而知对内存的消耗有多大。这也是ES中不推荐采用(from + size)方式进行深度分页的原因
。
同理,from为0,size为1000000时,ES进程执行查询操作的过程中确需要将1000000 条记录都加载到内存中再返回给调用方,也会对ES内存造成很大压力。
分页查询form
from 关键字: 用来指定起始返回位置,和size关键字连用可实现分页效果
GET /es_db/_search
{
"query": {
"match_all": {}
},
"size": 5,
"from": 0
}
深分页查询Scroll
改动index.max_result_window参数值的大小,只能解决一时的问题,当索引的数据量持续增长时,在查询全量数据时还是会出现问题。而且会增加ES服务器内存大结果集消耗完的风险。最佳实践还是根据异常提示中的采用scroll api更高效的请求大量数据集
。
#查询命令中新增scroll=1m,说明采用游标查询,保持游标查询窗口一分钟。
#这里由于测试数据量不够,所以size值设置为2。
#实际使用中为了减少游标查询的次数,可以将值适当增大,比如设置为1000。
GET /es_db/_search?scroll=1m
{
"query": { "match_all": {}},
"size": 2
}
查询结果:
除了返回前2条记录,还返回了一个游标ID值_scroll_id。
采用游标id查询:
# scroll_id 的值就是上一个请求中返回的 _scroll_id 的值
GET /_search/scroll
{
"scroll": "1m",
"scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmNwcVdjblRxUzVhZXlicG9HeU02bWcAAAAAAABmzRY2YlV3Z0o5VVNTdWJobkE5Z3MtXzJB"
}
多次根据scroll_id游标查询,直到没有数据返回则结束查询。采用游标查询索引全量数据,更安全高效,限制了单次对内存的消耗。
指定字段排序sort
注意:会让得分失效
GET /es_db/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": "desc"
}
]
}
#排序,分页
GET /es_db/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": "desc"
}
],
"from": 10,
"size": 5
}
返回指定字段_source
_source 关键字: 是一个数组,在数组中用来指定展示那些字段
GET /es_db/_search
{
"query": {
"match_all": {}
},
"_source": ["name","address"]
}
match
match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找
match支持以下参数:
- query : 指定匹配的值
- operator : 匹配条件类型
and : 条件分词后都要匹配
or : 条件分词后有一个匹配即可(默认)
- minmum_should_match : 最低匹配度,即条件在倒排索引中最低的匹配度
#模糊匹配 match 分词后or的效果
GET /es_db/_search
{
"query": {
"match": {
"address": "广州白云山公园"
}
}
}
# 分词后 and的效果
GET /es_db/_search
{
"query": {
"match": {
"address": {
"query": "广州白云山公园",
"operator": "AND"
}
}
}
}
在match中的应用: 当operator参数设置为or时,minnum_should_match参数用来控制匹配的分词的最少数量
。
# 最少匹配广州,公园两个词
GET /es_db/_search
{
"query": {
"match": {
"address": {
"query": "广州公园",
"minimum_should_match": 2
}
}
}
}
短语查询match_phrase
match_phrase查询分析文本并根据分析的文本创建一个短语查询。match_phrase 会将检索关键词分词
。match_phrase的分词结果必须在被检索字段的分词中都包含,而且顺序必须相同,而且默认必须都是连续的
。
GET /es_db/_search
{
"query": {
"match_phrase": {
"address": "广州白云山"
}
}
}
GET /es_db/_search
{
"query": {
"match_phrase": {
"address": "广州白云"
}
}
}
为什么查询广州白云山有数据,广州白云没有数据?
先查看广州白云山公园分词结果,可以知道广州和白云不是相邻的词条,中间会隔一个白云山,而match_phrase匹配的是相邻的词条,所以查询广州白云山有结果,但查询广州白云没有结果。
POST _analyze
{
"analyzer":"ik_max_word",
"text":"广州白云山"
}
#结果
{
"tokens" : [
{
"token" : "广州",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "白云山",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "白云",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "云山",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 3
}
]
}
如何解决词条间隔的问题?可以借助slop参数,slop参数告诉match_phrase查询词条能够相隔多远时仍然将文档视为匹配
。
#广州云山分词后相隔为2,可以匹配到结果
GET /es_db/_search
{
"query": {
"match_phrase": {
"address": {
"query": "广州云山",
"slop": 2
}
}
}
}
多字段查询multi_match
可以根据字段类型,决定是否使用分词查询,得分最高的在前面
GET /es_db/_search
{
"query": {
"multi_match": {
"query": "长沙张龙",
"fields": [
"address",
"name"
]
}
}
}
注意:字段类型分词,将查询条件分词之后进行查询,如果该字段不分词就会将查询条件作为整体进行查询
。
query_string
允许我们在单个查询字符串中指定AND | OR | NOT条件,同时也和 multi_match query 一样,支持多字段搜索。和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛。
注意: 查询字段分词就将查询条件分词查询,查询字段不分词将查询条件不分词查询
- 未指定字段查询
GET /es_db/_search
{
"query": {
"query_string": {
"query": "张三 OR 橘子洲"
}
}
}
- 指定单个字段查询
Query String
GET /es_db/_search
{
"query": {
"query_string": {
"default_field": "address",
"query": "白云山 OR 橘子洲"
}
}
}
- 指定多个字段查询
GET /es_db/_search
{
"query": {
"query_string": {
"fields": ["name","address"],
"query": "张三 OR (广州 AND 王五)"
}
}
}
simple_query_string
类似Query String,但是会忽略错误的语法,同时只支持部分查询语法,不支持AND OR NOT,会当作字符串处理。支持部分逻辑:
+ 替代AND
| 替代OR
- 替代NOT
#simple_query_string 默认的operator是OR
GET /es_db/_search
{
"query": {
"simple_query_string": {
"fields": ["name","address"],
"query": "广州公园",
"default_operator": "AND"
}
}
}
GET /es_db/_search
{
"query": {
"simple_query_string": {
"fields": ["name","address"],
"query": "广州 + 公园"
}
}
}
关键词查询Term
Term用来使用关键词查询(精确匹配),还可以用来查询没有被进行
分词的数据类型
。Term是表达语意的最小单位,搜索和利用统计语言模型进行自然语言处理都需要处理Term。match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找,而term会直接对关键词进行查找
。一般模糊查找的时候,多用match,而精确查找时可以使用term。
- ES中默认使用分词器为标准分词器(StandardAnalyzer),标准分词器对于英文单词分词,对于中文单字分词。
- 在ES的Mapping Type 中 keyword , date ,integer, long , double , boolean or ip 这些类型不分词,只有text类型分词。
#关键字查询 term
# 思考: 查询广州白云是否有数据,为什么?
GET /es_db/_search
{
"query":{
"term": {
"address": {
"value": "广州白云"
}
}
}
}
# 采用term精确查询, 查询字段映射类型为keyword
GET /es_db/_search
{
"query":{
"term": {
"address.keyword": {
"value": "广州白云山公园"
}
}
}
}
在ES中,Term查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项
,并且使用相关度算分公式为每个包含该词项的文档进行相关度算分。
PUT /product/_bulk
{"index":{"_id":1}}
{"productId":"xxx123","productName":"iPhone"}
{"index":{"_id":2}}
{"productId":"xxx111","productName":"iPad"}
# 思考: 查询iPhone可以查到数据吗?
GET /product/_search
{
"query":{
"term": {
"productName": {
"value": "iPhone"
}
}
}
}
GET /product/_analyze
{
"analyzer":"standard",
"text":"iPhone"
}
# 对于英文,可以考虑建立索引时忽略大小写
PUT /product
{
"settings": {
"analysis": {
"normalizer": {
"es_normalizer": {
"filter": [
"lowercase",
"asciifolding"
],
"type": "custom"
}
}
}
},
"mappings": {
"properties": {
"productId": {
"type": "text"
},
"productName": {
"type": "keyword",
"normalizer": "es_normalizer",
"index": "true"
}
}
}
}
可以通过 Constant Score 将查询转换成一个 Filtering,避免算分,并利用缓存,提高性能。
- 将Query 转成 Filter,忽略TF-IDF计算,避免相关性算分的开销
- Filter可以有效利用缓存
GET /es_db/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"address.keyword": "广州白云山公园"
}
}
}
}
}
ES中的结构化搜索
结构化搜索(Structured search)是指对结构化数据的搜索。
结构化数据:
- 日期,布尔类型和数字都是结构化的
- 文本也可以是结构化的。
- 如彩色笔可以有离散的颜色集合:红(red) 、绿(green、蓝(blue)
- 一个博客可能被标记了标签,例如,分布式(distributed)和搜索(search)
- 电商网站上的商品都有UPC(通用产品码Universal Product Code)或其他的唯一
标识,它们都需要遵从严格规定的、结构化的格式。
应用场景:对bool,日期,数字,结构化的文本可以利用term做精确匹配
GET /es_db/_search
{
"query": {
"term": {
"age": {
"value": 28
}
}
}
}
term处理多值字段,term查询是包含,不是等于
POST /employee/_bulk
{"index":{"_id":1}}
{"name":"小明","interest":["跑步","篮球"]}
{"index":{"_id":2}}
{"name":"小红","interest":["跳舞","画画"]}
{"index":{"_id":3}}
{"name":"小丽","interest":["跳舞","唱歌","跑步"]}
POST /employee/_search
{
"query": {
"term": {
"interest.keyword": {
"value": "跑步"
}
}
}
}
前缀查询prefix
它会对分词后的term进行前缀搜索。
它不会分析要搜索字符串,传入的前缀就是想要查找的前缀
- 默认状态下,前缀查询不做相关度分数计算,它只是将所有匹配的文档返回,然后赋予所有相关分数值为1。它的行为更像是一个过滤器而不是查询。两者实际的区别就是过滤器是可以被缓存的,而前缀查询不行。
prefix的原理:需要遍历所有倒排索引,并比较每个term是否已所指定的前缀开头
。
GET /es_db/_search
{
"query": {
"prefix": {
"address": {
"value": "广州"
}
}
}
}
通配符查询wildcard
通配符查询:工作原理和prefix相同,只不过它不是只比较开头,它能支持更为复杂的匹配模式。
GET /es_db/_search
{
"query": {
"wildcard": {
"address": {
"value": "*白*"
}
}
}
}
范围查询range
- range:范围关键字
- gte 大于等于
- lte 小于等于
- gt 大于
- lt 小于
- now 当前时间
POST /es_db/_search
{
"query": {
"range": {
"age": {
"gte": 25,
"lte": 28
}
}
}
}
日期range
DELETE /product
POST /product/_bulk
{"index":{"_id":1}}
{"price":100,"date":"2021-01-01","productId":"XHDK-1293"}
{"index":{"_id":2}}
{"price":200,"date":"2022-01-01","productId":"KDKE-5421"}
GET /product/_mapping
GET /product/_search
{
"query": {
"range": {
"date": {
"gte": "now-2y"
}
}
}
}
多id查询ids
ids 关键字 : 值为数组类型,用来根据一组id获取多个对应的文档
GET /es_db/_search
{
"query": {
"ids": {
"values": [1,2]
}
}
}
模糊查询fuzzy
在实际的搜索中,我们有时候会打错字,从而导致搜索不到。在Elasticsearch中,我们可以使用fuzziness属性来进行模糊查询,从而达到搜索有错别字的情形。
fuzzy 查询会用到两个很重要的参数,fuzziness,prefix_length
- fuzziness:表示输入的关键字通过几次操作可以转变成为ES库里面的对应field的字段
- 操作是指:新增一个字符,删除一个字符,修改一个字符,每次操作可以记做编辑距离为1,
- 如中文集团到中威集团编辑距离就是1,只需要修改一个字符;
- 该参数默认值为0,即不开启模糊查询。
- 如果fuzziness值在这里设置成2,会把编辑距离为2的东东集团也查出来。
- prefix_length:表示限制输入关键字和ES对应查询field的内容开头的第n个字符必须完全匹配,不允许错别字匹配
- 如这里等于1,则表示开头的字必须匹配,不匹配则不返回
- 默认值也是0
- 加大prefix_length的值可以提高效率和准确率。
GET /es_db/_search
{
"query": {
"fuzzy": {
"address": {
"value": "白运山",
"fuzziness": 1
}
}
}
}
GET /es_db/_search
{
"query": {
"match": {
"address": {
"query": "广洲",
"fuzziness": 1
}
}
}
}
注意: fuzzy 模糊查询 最大模糊错误 必须在0-2之间
- 搜索关键词长度为 2,不允许存在模糊
- 搜索关键词长度为3-5,允许1次模糊
- 搜索关键词长度大于5,允许最大2次模糊
高亮highlight
highlight 关键字: 可以让符合条件的文档中的关键词高亮。
highlight相关属性:
- pre_tags 前缀标签
- post_tags 后缀标签
- tags_schema 设置为styled可以使用内置高亮样式
- require_field_match 多字段高亮需要设置为false
示例数据
#指定ik分词器
PUT /products
{
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
}
}
}
PUT /products/_doc/1
{
"proId" : "2",
"name" : "牛仔男外套",
"desc" : "牛仔外套男装春季衣服男春装夹克修身休闲男生潮牌工装潮流头号青年春秋棒球服男 7705浅蓝常规 XL",
"timestamp" : 1576313264451,
"createTime" : "2019-12-13 12:56:56"
}
PUT /products/_doc/2
{
"proId" : "6",
"name" : "HLA海澜之家牛仔裤男",
"desc" : "HLA海澜之家牛仔裤男2019时尚有型舒适HKNAD3E109A 牛仔蓝(A9)175/82A(32)",
"timestamp" : 1576314265571,
"createTime" : "2019-12-18 15:56:56"
}
测试
GET /products/_search
{
"query": {
"term": {
"name": {
"value": "牛仔"
}
}
},
"highlight": {
"fields": {
"*":{}
}
}
}
自定义高亮html标签
可以在highlight中使用pre_tags和post_tags
GET /products/_search
{
"query": {
"term": {
"name": {
"value": "牛仔"
}
}
},
"highlight": {
"post_tags": ["</span>"],
"pre_tags": ["<span style='color:red'>"],
"fields": {
"*":{}
}
}
}
多字段高亮
GET /products/_search
{
"query": {
"term": {
"name": {
"value": "牛仔"
}
}
},
"highlight": {
"pre_tags": ["<font color='red'>"],
"post_tags": ["<font/>"],
"require_field_match": "false",
"fields": {
"name": {},
"desc": {}
}
}
}
ElasticSearch搜索技术深入与聚合查询
相关性和相关性算分
搜索是用户和搜索引擎的对话,用户关心的是搜索结果的相关性
- 是否可以找到所有相关的内容
- 有多少不相关的内容被返回了
- 文档的打分是否合理
- 结合业务需求,平衡结果排名
如何衡量相关性:
- Precision(查准率)―尽可能返回较少的无关文档
- Recall(查全率)–尽量返回较多的相关文档
- Ranking -是否能够按照相关度进行排序
相关性(Relevance)
搜索的相关性算分,描述了一个文档和查询语句匹配的程度
。ES 会对每个匹配查询条件的结果进行算分_score。打分的本质是排序,需要把最符合用户需求的文档排在前面
。ES 5之前,默认的相关性算分采用TF-IDF,现在采用BM 25。
如下例子:显而易见,查询JAVA多线程设计模式,文档id为2,3的文档的算分更高
关键词 | 文档ID |
---|---|
JAVA | 1,2,3 |
设计模式 | 1,2,3,4,5,6 |
多线程 | 2,3,7,9 |
什么是TF-IDF
TF-IDF(term frequency–inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。
-
TF-IDF被公认为是信息检索领域最重要的发明,除了在信息检索,在文献分类和其他相关领域有着非常广泛的应用。
-
IDF的概念,最早是剑桥大学的“斯巴克.琼斯”提出
- 1972年——“关键词特殊性的统计解释和它在文献检索中的应用”,但是没有从理论上解释IDF应该是用log(全部文档数/检索词出现过的文档总数),而不是其他函数,也没有做进一步的研究
- 1970,1980年代萨尔顿和罗宾逊,进行了进一步的证明和研究,并用香农信息论做了证明http://www.staff.city.ac.uk/~sb317/papers/foundations_bm25_review.pdf
-
现代搜索引擎,对TF-IDF进行了大量细微的优化
Lucene中的TF-IDF评分公式:
- TF是词频(Term Frequency)
检索词在文档中出现的频率越高,相关性也越高。 - IDF是逆向文本频率(Inverse Document Frequency)
每个检索词在索引中出现的频率,频率越高,相关性越低。
- 字段长度归一值( field-length norm)
字段的长度是多少?字段越短,字段的权重越高。检索词出现在一个内容短的 title 要比同样的词出现在一个内容长的 content 字段权重更大。
以上三个因素——词频(term frequency)、逆向文档频率(inverse document frequency)和字段长度归一值(field-length norm)——是在索引时计算并存储的,最后将它们结合在一起计算单个词在特定文档中的权重。
BM25
BM25 就是对 TF-IDF 算法的改进,对于 TF-IDF 算法,TF(t) 部分的值越大,整个公式返回的值就会越大。BM25 就针对这点进行来优化,随着TF(t) 的逐步加大,该算法的返回值会趋于一个数值。
- 从ES 5开始,默认算法改为BM 25
- 和经典的TF-IDF相比,当TF无限增加时,BM 25算分会趋于一个数值
- BM 25的公式
通过Explain API查看TF-IDF
PUT /test_score/_bulk
{"index":{"_id":1}}
{"content":"we use Elasticsearch to power the search"}
{"index":{"_id":2}}
{"content":"we like elasticsearch"}
{"index":{"_id":3}}
{"content":"Thre scoring of documents is caculated by the scoring formula"}
{"index":{"_id":4}}
{"content":"you know,for search"}
GET /test_score/_search
{
"explain": true,
"query": {
"match": {
"content": "elasticsearch"
}
}
}
Boosting
Boosting是控制相关度的一种手段。
参数boost的含义:
- 当boost > 1时,打分的权重相对性提升
- 当0 < boost <1时,打分的权重相对性降低
- 当boost <0时,贡献负分
返回匹配positive查询的文档并降低匹配negative查询的文档相似度分
。这样就可以在不排除某些文档的前提下对文档进行查询,搜索结果中存在只不过相似度分数相比正常匹配的要低;
GET /test_score/_search
{
"query": {
"boosting": {
"positive": {
"term": {
"content": "elasticsearch"
}
},
"negative": {
"term": {
"content": "like"
}
},
"negative_boost": 0.2
}
}
}
应用场景:希望包含了某项内容的结果不是不出现,而是排序靠后。
布尔查询bool Query
一个bool查询,是一个或者多个查询子句的组合
,总共包括4种子句,其中2种会影响算分,2种不影响算分。
- must: 相当于&& ,必须匹配,贡献算分
- should: 相当于|| ,选择性匹配,贡献算分
- must_not: 相当于! ,必须不能匹配,不贡献算分
- filter: 必须匹配,不贡献算法
在Elasticsearch中,有Query和 Filter两种不同的Context
- Query Context: 相关性算分
- Filter Context: 不需要算分 ,可以利用Cache,获得更好的性能
相关性并不只是全文本检索的专利,也适用于yes | no 的子句,匹配的子句越多,相关性评分
越高。如果多条查询子句被合并为一条复合查询语句,比如 bool查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。
bool查询语法
- 子查询可以任意顺序出现
- 可以嵌套多个查询
- 如果你的bool查询中,没有must条件,should中必须至少满足一条查询
GET /es_db/_search
{
"query": {
"bool": {
"must": {
"match": {
"remark": "java developer"
}
},
"filter": {
"term": {
"sex": "1"
}
},
"must_not": {
"range": {
"age": {
"gte": 30
}
}
},
"should": [
{
"term": {
"address.keyword": {
"value": "广州天河公园"
}
}
},
{
"term": {
"address.keyword": {
"value": "广州白云山公园"
}
}
}
],
"minimum_should_match": 1
}
}
}
如何解决结构化查询“包含而不是相等”的问题
测试数据
POST /employee/_bulk
{"index":{"_id":1}}
{"name":"小明","interest":["跑步","篮球"]}
{"index":{"_id":2}}
{"name":"小红","interest":["跑步"]}
{"index":{"_id":3}}
{"name":"小丽","interest":["跳舞","唱歌","跑步"]}
POST /employee/_search
{
"query": {
"term": {
"interest.keyword": {
"value": "跑步"
}
}
}
}
解决方案: 增加count字段,使用bool查询解决
- 从业务角度,按需改进Elasticsearch数据模型
POST /employee/_bulk
{"index":{"_id":1}}
{"name":"小明","interest":["跑步","篮球"],"interest_count":2}
{"index":{"_id":2}}
{"name":"小红","interest":["跑步"],"interest_count":1}
{"index":{"_id":3}}
{"name":"小丽","interest":["跳舞","唱歌","跑步"],"interest_count":3}
- 使用bool查询
# must 算分
POST /employee/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"interest.keyword": {
"value": "跑步"
}
}
},
{
"term": {
"interest_count": {
"value": 1
}
}
}
]
}
}
}
# filter不算分
POST /employee/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"interest.keyword": {
"value": "跑步"
}
}
},
{
"term": {
"interest_count": {
"value": 1
}
}
}
]
}
}
}
利用bool嵌套实现should not逻辑
GET /es_db/_search
{
"query": {
"bool": {
"must": {
"match": {
"remark": "java developer"
}
},
"should": [
{
"bool": {
"must_not": [
{
"term": {
"sex": 1
}
}
]
}
}
],
"minimum_should_match": 1
}
}
}
Boosting Query
控制字段的Boosting
Boosting是控制相关的一种手段。可以通过指定字段的boost值影响查询结果
参数boost的含义:
- 当boost > 1时,打分的权重相对性提升
- 当0 < boost <1时,打分的权重相对性降低
- 当boost <0时,贡献负分
POST /blogs/_bulk
{"index":{"_id":1}}
{"title":"Apple iPad","content":"Apple iPad,Apple iPad"}
{"index":{"_id":2}}
{"title":"Apple iPad,Apple iPad","content":"Apple iPad"}
GET /blogs/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "apple,ipad",
"boost": 1
}
}
},
{
"match": {
"content": {
"query": "apple,ipad",
"boost": 4
}
}
}
]
}
}
}
案例:要求苹果公司的产品信息优先展示
POST /news/_bulk
{"index":{"_id":1}}
{"content":"Apple Mac"}
{"index":{"_id":2}}
{"content":"Apple iPad"}
{"index":{"_id":3}}
{"content":"Apple employee like Apple Pie and Apple Juice"}
GET /news/_search
{
"query": {
"bool": {
"must": {
"match": {
"content": "apple"
}
}
}
}
}
利用must not排除不是苹果公司产品的文档
GET /news/_search
{
"query": {
"bool": {
"must": {
"match": {
"content": "apple"
}
},
"must_not": {
"match":{
"content": "pie"
}
}
}
}
}
利用negative_boost降低相关性
- negative_boost 对 negative部分query生效
- 计算评分时,boosting部分评分不修改,negative部分query乘以negative_boost值
- negative_boost取值:0-1.0,举例:0.3
对某些返回结果不满意,但又不想排除掉( must_not),可以考虑boosting query的negative_boost。
GET /news/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"content": "apple"
}
},
"negative": {
"match": {
"content": "pie"
}
},
"negative_boost": 0.2
}
}
}
单字符串多字段查询
三种场景
- 最佳字段(Best Fields)
当字段之间相互竞争,又相互关联。例如,对于博客的 title和 body这样的字段,评分来自最匹配字段 - 多数字段(Most Fields)
处理英文内容时的一种常见的手段是,在主字段( English Analyzer),抽取词干,加入同义词,以匹配更多的文档。相同的文本,加入子字段(Standard Analyzer),以提供更加精确的匹配。其他字段作为匹配文档提高相关度的信号,匹配字段越多则越好。 - 混合字段(Cross Field)
对于某些实体,例如人名,地址,图书信息。需要在多个字段中确定信息,单个字段只能作为整体的一部分。希望在任何这些列出的字段中找到尽可能多的词
最佳字段查询Dis Max Query
将任何与任一查询匹配的文档作为结果返回,采用字段上最匹配的评分最终评分返回
。
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-dis-max-query.html
测试
PUT /blogs/_doc/1
{
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}
PUT /blogs/_doc/2
{
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
POST /blogs/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
bool should的算法过程:
- 查询should语句中的两个查询
- 加和两个查询的评分
- 乘以匹配语句的总数
- 除以所有语句的总数
使用最佳字段查询dis max query
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
可以通过tie_breaker参数调整
Tier Breaker是一个介于0-1之间的浮点数。0代表使用最佳匹配;1代表所有语句同等重要。
1.获得最佳匹配语句的评分_score 。
2.将其他匹配语句的评分与tie_breaker相乘
3.对以上评分求和并规范化
POST /blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
]
}
}
}
POST /blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.2
}
}
}
Multi Match Query
最佳字段(Best Fields)搜索
Best Fields是默认类型,可以不用指定
POST /blogs/_search
{
"query": {
"multi_match": {
"type": "best_fields",
"query": "Quick pets",
"fields": ["title","body"],
"tie_breaker": 0.2
}
}
}
使用多数字段(Most Fields)搜索
案例
DELETE /titles
PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english",
"fields": {
"std": {
"type": "text",
"analyzer": "standard"
}
}
}
}
}
}
POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }
# 结果与预期不匹配
GET /titles/_search
{
"query": {
"match": {
"title": "barking dogs"
}
}
}
用广度匹配字段title包括尽可能多的文档——以提升召回率——同时又使用字段title.std 作为信号将相关度更高的文档置于结果顶部。
GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [
"title",
"title.std"
]
}
}
}
每个字段对于最终评分的贡献可以通过自定义值boost 来控制
。比如,使title 字段更为重要,这样同时也降低了其他信号字段的作用:
#增加title的权重
GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [
"title^10",
"title.std"
]
}
}
}
跨字段(Cross Field)搜索
DELETE /address
PUT /address
{
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
}
}
}
PUT /address/_bulk
{ "index": { "_id": "1"} }
{"province": "湖南","city": "长沙"}
{ "index": { "_id": "2"} }
{"province": "湖南","city": "常德"}
{ "index": { "_id": "3"} }
{"province": "广东","city": "广州"}
{ "index": { "_id": "4"} }
{"province": "湖南","city": "邵阳"}
#使用most_fields的方式结果不符合预期,不支持operator
GET /address/_search
{
"query": {
"multi_match": {
"query": "湖南常德",
"type": "most_fields",
"fields": ["province","city"]
}
}
}
# 可以使用cross_fields,支持operator
#与copy_to相比,其中一个优势就是它可以在搜索时为单个字段提升权重。
GET /address/_search
{
"query": {
"multi_match": {
"query": "湖南常德",
"type": "cross_fields",
"operator": "and",
"fields": ["province","city"]
}
}
}
可以用copy…to 解决,但是需要额外的存储空间
DELETE /address
PUT /address
{
"mappings" : {
"properties" : {
"province" : {
"type" : "keyword",
"copy_to": "full_address"
},
"city" : {
"type" : "text",
"copy_to": "full_address"
}
}
},
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
}
}
}
PUT /address/_bulk
{ "index": { "_id": "1"} }
{"province": "湖南","city": "长沙"}
{ "index": { "_id": "2"} }
{"province": "湖南","city": "常德"}
{ "index": { "_id": "3"} }
{"province": "广东","city": "广州"}
{ "index": { "_id": "4"} }
{"province": "湖南","city": "邵阳"}
GET /address/_search
{
"query": {
"match": {
"full_address": {
"query": "湖南常德",
"operator": "and"
}
}
}
}
GET /address/_search
{
"query": {
"multi_match": {
"query": "湖南常德",
"type": "most_fields",
"fields": ["province","city"]
}
}
}
ElasticSearch聚合操作
Elasticsearch除搜索以外,提供了针对ES 数据进行统计分析的功能
。**聚合(aggregations)**可以让我们极其方便的实现对数据的统计、分析、运算。例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
语法:
"aggs" : { #和query同级的关键词
"<aggregation_name>" : { #自定义的聚合名字
"<aggregation_type>" : { #聚合的定义: 不同的type+body
<aggregation_body>
}
[,"meta" : { [<meta_data_body>] } ]?
[,"aggregations" : { [<sub_aggregation>]+ } ]? #子聚合查询
}
[,"<aggregation_name_2>" : { ... } ]* #可以包含多个同级的聚合查询
}
聚合的分类
- Metric Aggregation:—些数学运算,可以对文档字段进行统计分析,类比Mysql中的 min(), max(), sum() 操作。
SELECT MIN(price), MAX(price) FROM products
#Metric聚合的DSL类比实现:
{
"aggs":{
"avg_price":{
"avg":{
"field":"price"
}
}
}
}
- Bucket Aggregation: 一些满足特定条件的文档的集合放置到一个桶里,每一个桶关联一个key,类比Mysql中的group by操作。
ELECT size COUNT(*) FROM products GROUP BY size
#bucket聚合的DSL类比实现:
{
"aggs": {
"by_size": {
"terms": {
"field": "size"
}
}
}
- Pipeline Aggregation:对其他的聚合结果进行二次聚合
示例数据
DELETE /employees
#创建索引库
PUT /employees
{
"mappings": {
"properties": {
"age":{
"type": "integer"
},
"gender":{
"type": "keyword"
},
"job":{
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 50
}
}
},
"name":{
"type": "keyword"
},
"salary":{
"type": "integer"
}
}
}
}
PUT /employees/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "Emma","age":32,"job":"Product Manager","gender":"female","salary":35000 }
{ "index" : { "_id" : "2" } }
{ "name" : "Underwood","age":41,"job":"Dev Manager","gender":"male","salary": 50000}
{ "index" : { "_id" : "3" } }
{ "name" : "Tran","age":25,"job":"Web Designer","gender":"male","salary":18000 }
{ "index" : { "_id" : "4" } }
{ "name" : "Rivera","age":26,"job":"Web Designer","gender":"female","salary": 22000}
{ "index" : { "_id" : "5" } }
{ "name" : "Rose","age":25,"job":"QA","gender":"female","salary":18000 }
{ "index" : { "_id" : "6" } }
{ "name" : "Lucy","age":31,"job":"QA","gender":"female","salary": 25000}
{ "index" : { "_id" : "7" } }
{ "name" : "Byrd","age":27,"job":"QA","gender":"male","salary":20000 }
{ "index" : { "_id" : "8" } }
{ "name" : "Foster","age":27,"job":"Java Programmer","gender":"male","salary": 20000}
{ "index" : { "_id" : "9" } }
{ "name" : "Gregory","age":32,"job":"Java Programmer","gender":"male","salary":22000 }
{ "index" : { "_id" : "10" } }
{ "name" : "Bryant","age":20,"job":"Java Programmer","gender":"male","salary": 9000}
{ "index" : { "_id" : "11" } }
{ "name" : "Jenny","age":36,"job":"Java Programmer","gender":"female","salary":38000 }
{ "index" : { "_id" : "12" } }
{ "name" : "Mcdonald","age":31,"job":"Java Programmer","gender":"male","salary": 32000}
{ "index" : { "_id" : "13" } }
{ "name" : "Jonthna","age":30,"job":"Java Programmer","gender":"female","salary":30000 }
{ "index" : { "_id" : "14" } }
{ "name" : "Marshall","age":32,"job":"Javascript Programmer","gender":"male","salary": 25000}
{ "index" : { "_id" : "15" } }
{ "name" : "King","age":33,"job":"Java Programmer","gender":"male","salary":28000 }
{ "index" : { "_id" : "16" } }
{ "name" : "Mccarthy","age":21,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : { "_id" : "17" } }
{ "name" : "Goodwin","age":25,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : { "_id" : "18" } }
{ "name" : "Catherine","age":29,"job":"Javascript Programmer","gender":"female","salary": 20000}
{ "index" : { "_id" : "19" } }
{ "name" : "Boone","age":30,"job":"DBA","gender":"male","salary": 30000}
{ "index" : { "_id" : "20" } }
{ "name" : "Kathy","age":29,"job":"DBA","gender":"female","salary": 20000}
Metric Aggregation
- 单值分析︰只输出一个分析结果
- min, max, avg, sum
- Cardinality(类似distinct Count)
- 多值分析:输出多个分析结果
- stats(统计), extended stats
- percentile (百分位), percentile rank
- top hits(排在前面的示例)
查询员工的最低最高和平均工资
#多个 Metric 聚合,找到最低最高和平均工资
POST /employees/_search
{
"size": 0,
"aggs": {
"max_salary": {
"max": {
"field": "salary"
}
},
"min_salary": {
"min": {
"field": "salary"
}
},
"avg_salary": {
"avg": {
"field": "salary"
}
}
}
}
对salary进行统计
# 一个聚合,输出多值
POST /employees/_search
{
"size": 0,
"aggs": {
"stats_salary": {
"stats": {
"field":"salary"
}
}
}
}
cardinate对搜索结果去重
POST /employees/_search
{
"size": 0,
"aggs": {
"cardinate": {
"cardinality": {
"field": "job.keyword"
}
}
}
}
Bucket Aggregation
按照一定的规则,将文档分配到不同的桶中,从而达到分类的目的
。ES提供的一些常见的 Bucket Aggregation。
- Terms,需要字段支持filedata
- keyword 默认支持fielddata
- text需要在Mapping 中开启fielddata,会按照分词后的结果进行分桶
- 数字类型
- Range / Data Range
- Histogram(直方图) / Date Histogram
- 支持嵌套: 也就在桶里再做分桶
获取job的分类信息
# 对keword 进行聚合
GET /employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword"
}
}
}
}
聚合可配置属性有:
- field:指定聚合字段
- size:指定聚合结果数量
- order:指定聚合结果排序方式
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。我们可以指定order属性,自定义聚合的排序方式:
GET /employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword",
"size": 10,
"order": {
"_count": "desc"
}
}
}
}
}
限定聚合范围
#只对salary在10000元以上的文档聚合
GET /employees/_search
{
"query": {
"range": {
"salary": {
"gte": 10000
}
}
},
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword",
"size": 10,
"order": {
"_count": "desc"
}
}
}
}
}
注意:对 Text 字段进行 terms 聚合查询,会失败抛出异常
POST /employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job"
}
}
}
}
解决办法:对 Text 字段打开 fielddata,支持terms aggregation
PUT /employees/_mapping
{
"properties" : {
"job":{
"type": "text",
"fielddata": true
}
}
}
# 对 Text 字段进行分词,分词后的terms
POST /employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job"
}
}
}
}
对job.keyword 和 job 进行 terms 聚合,分桶的总数并不一样
POST /employees/_search
{
"size": 0,
"aggs": {
"cardinate": {
"cardinality": {
"field": "job"
}
}
}
}
Range & Histogram聚合
- 按照数字的范围,进行分桶
- 在Range Aggregation中,可以自定义Key
Range 示例:按照工资的 Range 分桶
Salary Range分桶,可以自己定义 key
POST employees/_search
{
"size": 0,
"aggs": {
"salary_range": {
"range": {
"field":"salary",
"ranges":[
{
"to":10000
},
{
"from":10000,
"to":20000
},
{
"key":">20000",
"from":20000
}
]
}
}
}
}
Histogram示例:按照工资的间隔分桶
#工资0到10万,以 5000一个区间进行分桶
POST employees/_search
{
"size": 0,
"aggs": {
"salary_histrogram": {
"histogram": {
"field":"salary",
"interval":5000,
"extended_bounds":{
"min":0,
"max":100000
}
}
}
}
}
top_hits应用场景: 当获取分桶后,桶内最匹配的顶部文档列表
# 指定size,不同工种中,年纪最大的3个员工的具体信息
POST /employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword"
},
"aggs":{
"old_employee":{
"top_hits":{
"size":3,
"sort":[
{
"age":{
"order":"desc"
}
}
]
}
}
}
}
}
}
嵌套聚合示例
# 嵌套聚合1,按照工作类型分桶,并统计工资信息
POST employees/_search
{
"size": 0,
"aggs": {
"Job_salary_stats": {
"terms": {
"field": "job.keyword"
},
"aggs": {
"salary": {
"stats": {
"field": "salary"
}
}
}
}
}
}
# 多次嵌套。根据工作类型分桶,然后按照性别分桶,计算工资的统计信息
POST employees/_search
{
"size": 0,
"aggs": {
"Job_gender_stats": {
"terms": {
"field": "job.keyword"
},
"aggs": {
"gender_stats": {
"terms": {
"field": "gender"
},
"aggs": {
"salary_stats": {
"stats": {
"field": "salary"
}
}
}
}
}
}
}
}
Pipeline Aggregation
支持对聚合分析的结果,再次进行聚合分析。
Pipeline 的分析结果会输出到原结果中,根据位置的不同,分为两类:
- Sibling - 结果和现有分析结果同级
- Max,min,Avg & Sum Bucket
- Stats,Extended Status Bucket
- Percentiles Bucket
- Parent -结果内嵌到现有的聚合分析结果之中
- Derivative(求导)
- Cumultive Sum(累计求和)
- Moving Function(移动平均值 )
min_bucket示例
在员工数最多的工种里,找出平均工资最低的工种
# 平均工资最低的工种
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword",
"size": 10
},
"aggs": {
"avg_salary": {
"avg": {
"field": "salary"
}
}
}
},
"min_salary_by_job":{
"min_bucket": {
"buckets_path": "jobs>avg_salary"
}
}
}
}
- min_salary_by_job结果和jobs的聚合同级
- min_bucket求之前结果的最小值
- 通过bucket_path关键字指定路径
Stats示例
# 平均工资的统计分析
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword",
"size": 10
},
"aggs": {
"avg_salary": {
"avg": {
"field": "salary"
}
}
}
},
"stats_salary_by_job":{
"stats_bucket": {
"buckets_path": "jobs>avg_salary"
}
}
}
}
percentiles示例
# 平均工资的百分位数
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword",
"size": 10
},
"aggs": {
"avg_salary": {
"avg": {
"field": "salary"
}
}
}
},
"percentiles_salary_by_job":{
"percentiles_bucket": {
"buckets_path": "jobs>avg_salary"
}
}
}
}
Cumulative_sum示例
#Cumulative_sum 累计求和
POST employees/_search
{
"size": 0,
"aggs": {
"age": {
"histogram": {
"field": "age",
"min_doc_count": 0,
"interval": 1
},
"aggs": {
"avg_salary": {
"avg": {
"field": "salary"
}
},
"cumulative_salary":{
"cumulative_sum": {
"buckets_path": "avg_salary"
}
}
}
}
}
}
聚合的作用范围
ES聚合分析的默认作用范围是query的查询结果集,同时ES还支持以下方式改变聚合的作用范围:
- Filter
- Post Filter
- Global
#Query
POST employees/_search
{
"size": 0,
"query": {
"range": {
"age": {
"gte": 20
}
}
},
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword"
}
}
}
}
#Filter
POST employees/_search
{
"size": 0,
"aggs": {
"older_person": {
"filter":{
"range":{
"age":{
"from":35
}
}
},
"aggs":{
"jobs":{
"terms": {
"field":"job.keyword"
}
}
}},
"all_jobs": {
"terms": {
"field":"job.keyword"
}
}
}
}
#Post field. 一条语句,找出所有的job类型。还能找到聚合后符合条件的结果
POST employees/_search
{
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword"
}
}
},
"post_filter": {
"match": {
"job.keyword": "Dev Manager"
}
}
}
#global
POST employees/_search
{
"size": 0,
"query": {
"range": {
"age": {
"gte": 40
}
}
},
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword"
}
},
"all":{
"global":{},
"aggs":{
"salary_avg":{
"avg":{
"field":"salary"
}
}
}
}
}
}
排序
指定order,按照count和key进行排序:
- 默认情况,按照count降序排序
- 指定size,就能返回相应的桶
#排序 order
#count and key
POST employees/_search
{
"size": 0,
"query": {
"range": {
"age": {
"gte": 20
}
}
},
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword",
"order":[
{"_count":"asc"},
{"_key":"desc"}
]
}
}
}
}
#排序 order
#count and key
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword",
"order":[ {
"avg_salary":"desc"
}]
},
"aggs": {
"avg_salary": {
"avg": {
"field":"salary"
}
}
}
}
}
}
#排序 order
#count and key
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword",
"order":[ {
"stats_salary.min":"desc"
}]
},
"aggs": {
"stats_salary": {
"stats": {
"field":"salary"
}
}
}
}
}
}
ES聚合分析不精准原因分析
ElasticSearch在对海量数据进行聚合分析的时候会损失搜索的精准度来满足实时性的需求。
Terms聚合分析的执行流程:
不精准的原因: 数据分散到多个分片,聚合是每个分片的取 Top X,导致结果不精准
。ES 可以不每个分片Top X,而是全量聚合,但势必这会有很大的性能问题。
如何提高聚合精确度?
方案1:设置主分片为1
注意7.x版本已经默认为1。
适用场景:数据量小的小集群规模业务场景
。
方案2:调大 shard_size 值
设置 shard_size 为比较大的值,官方推荐:size*1.5+10。shard_size 值越大,结果越趋近于精准聚合结果值。此外,还可以通过show_term_doc_count_error参数显示最差情况下的错误值,用于辅助确定 shard_size 大小。
- size:是聚合结果的返回值,客户期望返回聚合排名前三,size值就是 3。
- shard_size: 每个分片上聚合的数据条数。shard_size 原则上要大于等于 size
适用场景:数据量大、分片数多的集群业务场景。
测试: 使用kibana的测试数据
DELETE my_flights
PUT my_flights
{
"settings": {
"number_of_shards": 20
},
"mappings" : {
"properties" : {
"AvgTicketPrice" : {
"type" : "float"
},
"Cancelled" : {
"type" : "boolean"
},
"Carrier" : {
"type" : "keyword"
},
"Dest" : {
"type" : "keyword"
},
"DestAirportID" : {
"type" : "keyword"
},
"DestCityName" : {
"type" : "keyword"
},
"DestCountry" : {
"type" : "keyword"
},
"DestLocation" : {
"type" : "geo_point"
},
"DestRegion" : {
"type" : "keyword"
},
"DestWeather" : {
"type" : "keyword"
},
"DistanceKilometers" : {
"type" : "float"
},
"DistanceMiles" : {
"type" : "float"
},
"FlightDelay" : {
"type" : "boolean"
},
"FlightDelayMin" : {
"type" : "integer"
},
"FlightDelayType" : {
"type" : "keyword"
},
"FlightNum" : {
"type" : "keyword"
},
"FlightTimeHour" : {
"type" : "keyword"
},
"FlightTimeMin" : {
"type" : "float"
},
"Origin" : {
"type" : "keyword"
},
"OriginAirportID" : {
"type" : "keyword"
},
"OriginCityName" : {
"type" : "keyword"
},
"OriginCountry" : {
"type" : "keyword"
},
"OriginLocation" : {
"type" : "geo_point"
},
"OriginRegion" : {
"type" : "keyword"
},
"OriginWeather" : {
"type" : "keyword"
},
"dayOfWeek" : {
"type" : "integer"
},
"timestamp" : {
"type" : "date"
}
}
}
}
POST _reindex
{
"source": {
"index": "kibana_sample_data_flights"
},
"dest": {
"index": "my_flights"
}
}
GET my_flights/_count
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs": {
"weather": {
"terms": {
"field":"OriginWeather",
"size":5,
"show_term_doc_count_error":true
}
}
}
}
GET my_flights/_search
{
"size": 0,
"aggs": {
"weather": {
"terms": {
"field":"OriginWeather",
"size":5,
"shard_size":10,
"show_term_doc_count_error":true
}
}
}
}
在Terms Aggregation的返回中有两个特殊的数值:
doc_count_error_upper_bound : 被遗漏的term 分桶,包含的文档,有可能的最大值
- sum_other_doc_count: 除了返回结果 bucket的terms以外,其他 terms 的文档总数(总数-返回的总数)
方案3:将size设置为全量值,来解决精度问题
将size设置为2的32次方减去1也就是分片支持的最大值,来解决精度问题。
原因:1.x版本,size等于 0 代表全部,高版本取消 0 值,所以设置了最大值(大于业务的全量值)。
全量带来的弊端就是:如果分片数据量极大,这样做会耗费巨大的CPU 资源来排序,而且可能会阻塞网络。
适用场景:对聚合精准度要求极高的业务场景,由于性能问题,不推荐使用。
方案4:使用Clickhouse/ Spark 进行精准聚合
适用场景:数据量非常大、聚合精度要求高、响应速度快的业务场景。
Elasticsearch 聚合性能优化
启用 eager global ordinals 提升高基数聚合性能
适用场景:高基数聚合 。高基数聚合场景中的高基数含义:一个字段包含很大比例的唯一值。
global ordinals 中文翻译成全局序号,是一种数据结构,应用场景如下:
- 基于 keyword,ip 等字段的分桶聚合,包含:terms聚合、composite 聚合等。
- 基于text 字段的分桶聚合(前提条件是:fielddata 开启)。
- 基于父子文档 Join 类型的 has_child 查询和 父聚合。
global ordinals 使用一个数值代表字段中的字符串值,然后为每一个数值分配一个 bucket(分桶)。
global ordinals 的本质是:启用 eager_global_ordinals 时,会在刷新(refresh)分片时构建全局序号。这将构建全局序号的成本从搜索阶段转移到了数据索引化(写入)阶段
。
创建索引的同时开启:eager_global_ordinals。
PUT /my-index
{
"mappings": {
"properties": {
"tags": {
"type": "keyword",
"eager_global_ordinals": true
}
}
}
注意:开启 eager_global_ordinals 会影响写入性能,因为每次刷新时都会创建新的全局序号。为了最大程度地减少由于频繁刷新建立全局序号而导致的额外开销,请调大刷新间隔 refresh_interval。
动态调整刷新频率的方法如下:
PUT my-index/_settings
{
"index": {
"refresh_interval": "30s"
}
该招数的本质是:以空间换时间。
插入数据时对索引进行预排序
Index sorting (索引排序)可用于在插入时对索引进行预排序
,而不是在查询时再对索引进行排序,这将提高范围查询(range query)和排序操作的性能。- 在 Elasticsearch 中创建新索引时,可以配置如何对每个分片内的段进行排序。
这是 Elasticsearch 6.X 之后版本才有的特性。
PUT /my_index
{
"settings": {
"index":{
"sort.field": "create_time",
"sort.order": "desc"
}
},
"mappings": {
"properties": {
"create_time":{
"type": "date"
}
}
}
}
注意:预排序将增加 Elasticsearch 写入的成本
。在某些用户特定场景下,开启索引预排序会导致大约 40%-50% 的写性能下降。也就是说,如果用户场景更关注写性能的业务,开启索引预排序不是一个很好的选择。
使用节点查询缓存
节点查询缓存(Node query cache)可用于有效缓存过滤器(filter)操作的结果
。如果多次执行同一 filter 操作,这将很有效,但是即便更改过滤器中的某一个值,也将意味着需要计算新的过滤器结果。
例如,由于 “now” 值一直在变化,因此无法缓存在过滤器上下文中使用 “now” 的查询。
那怎么使用缓存呢?通过在 now 字段上应用 datemath 格式将其四舍五入到最接近的分钟/小时等,可以使此类请求更具可缓存性,以便可以对筛选结果进行缓存。
PUT /my_index/_doc/1
{
"create_time":"2022-05-11T16:30:55.328Z"
}
#下面的示例无法使用缓存
GET /my_index/_search
{
"query":{
"constant_score": {
"filter": {
"range": {
"create_time": {
"gte": "now-1h",
"lte": "now"
}
}
}
}
}
}
# 下面的示例就可以使用节点查询缓存。
GET /my_index/_search
{
"query":{
"constant_score": {
"filter": {
"range": {
"create_time": {
"gte": "now-1h/m",
"lte": "now/m"
}
}
}
}
}
}
上述示例中的“now-1h/m” 就是 datemath 的格式。
如果当前时间 now 是:16:31:29,那么range query 将匹配 my_date 介于:15:31:00 和 15:31:59 之间的时间数据。同理,聚合的前半部分 query 中如果有基于时间查询,或者后半部分 aggs 部分中有基于时间聚合的,建议都使用 datemath 方式做缓存处理以优化性能。
使用分片请求缓存
聚合语句中,设置:size:0,就会使用分片请求缓存缓存结果
。size = 0 的含义是:只返回聚合结果,不返回查询结果。
GET /es_db/_search
{
"size": 0,
"aggs": {
"remark_agg": {
"terms": {
"field": "remark.keyword"
}
}
}
}
拆分聚合,使聚合并行化
Elasticsearch 查询条件中同时有多个条件聚合,默认情况下聚合不是并行运行的。当为每个聚合提供自己的查询并执行 msearch 时,性能会有显著提升。因此,在 CPU 资源不是瓶颈的前提下,如果想缩短响应时间,可以将多个聚合拆分为多个查询,借助:msearch 实现并行聚合
。
#常规的多条件聚合实现
GET /employees/_search
{
"size": 0,
"aggs": {
"job_agg": {
"terms": {
"field": "job.keyword"
}
},
"max_salary":{
"max": {
"field": "salary"
}
}
}
}
# msearch 拆分多个语句的聚合实现
GET _msearch
{"index":"employees"}
{"size":0,"aggs":{"job_agg":{"terms":{"field": "job.keyword"}}}}
{"index":"employees"}
{"size":0,"aggs":{"max_salary":{"max":{"field": "salary"}}}}