一、ES概述
1.1、ES介绍
ElasticSearch简称ES,是一个基于Lucene开发的 分布式的全文搜索引擎,它可以近实时的存储、检索数据。
而Lucene是一个开源的全文检索引擎的工具包,他本身不是一个完整的全文检索引擎。
ES官网文档地址:https://www.elastic.co/guide/en/elasticsearch/reference/7.3/index.html
1.2、ES基本概念
ES和关系型数据库对比:
关系型数据库(Mysql) ElasticSeach 数据库(database) 索引(index) 表(tables) 类型(types) 行(rows) 文档(documnets) 列字段(columns) 字段(fields)
- 集群(Cluster):是一组具有相同集群名的节点集合
- 节点(Node):一个ES集群由多个节点组成,一个节点就是集群中的一个服务器。
- 分片(Shard): 一个索引的数据保存在多个分片中,每个分片都是一个完整的Lucene实例,对相同索引可以进行水平扩展。
- 副本(Replicas):就是分片的复制,提高数据的可靠性。
- 索引(Index):由具有相同结构(field)的文档(document)组成集合。每个索引都有自己的mapping定义,用于定义字段名和类型。
- 类型(Type):类型是文档的逻辑容器(在ES7中使用默认的_doc作为唯一的type,在后续版本中将被彻底删除)
- 文档(Document):存储在ES的数据文档,JSON格式,由字段field组成。
如下所示,一个由三个节点组成的集群,示意图:
二、ES基本原理
2.1、ES架构
2.2、ES索引
索引是为了快速检索数据的一种数据结构,对于我们熟悉的Mysql,采用的是B+树(Innodb引擎),而ES采用的是一种倒排索引的数据结构。
- 正排索引
是以ID查询对应的数据记录,是一种key-value的查询过程。
倒排索引(Inverted Index)
一个倒排索引由文档中所有不重复词的列表构成,对于其中的每个词,有一个包含它的文档列表。
假如有以下两个文档:
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,先将每个文档进行分词,创建一个包含所有不重复词条的排序列表,并列出每个词出现在哪个文档中。结果如下:
假如现在,要查询quick brown,只需要查找出包含每个词条的文档:
如上所述,两个文档都匹配,但是第一个比第二个匹配度更高。
2.3、分词和分词器
分词就是将文本转换成一系列单词的过程,也叫做文本分析,在ES中成为Analysis。
分词器(Analysis)组成如下:
组成 功能 Character Filter 对字符串进行预处理,比如去除html标记符等 Tokenizer 将原始文本按照一定的规则切分为单个词条 Token Filters 针对tokenizer处理的单词进行再加工,
比如转换为小写、删除或新增等。分词器调用顺序过程如下:
如下是ES自建的分词器:
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/analysis-analyzers.html
2.4、建立索引和搜索过程
建索引:数据入库时,需要经过分词后,建立正排和倒排索引,并持久化到硬盘中。
搜索:搜索的时候,同样需要经过分词,再根据分词后的结果到正排和倒排索引中查找匹配的文档。
2.4、IK分词器
IK分词器:是一个中文分词器,并且支持自定义字典。他支持两种分词算法ik_max_word和ik_smart。
- ik_max_word:会将文本做最细粒度的拆分
- ik_smart:会将文本做最粗粒度的拆分
一般来说,对于文档入库时,我们希望文本分词拆的越细越好,这样搜索的时候更能匹配到内容。而搜索的时候,我们希望匹配度越高越好。所以一般入库使用ik_max_word算法,而查询的时候使用ik_smart算法。
三、ES基本操作
ES提供了REST风格的API,用于操作数据。
3.1 、文档API
3.1.1、新增
- 单条新增:
POST /content/_doc/123456 { "aid":"V123456", "type":"t800000", "time":1583824758000, "data":{"keyword":"占某某"} }
- 批量新增
POST /_bulk {"index": {"_index": "content", "_id": "11"}} {"aid":"V123456","type":"t800000","time":1583824758000,"data":{"keyword":"占某某11"}} {"index": {"_index": "content", "_id": "22"}} {"aid":"V123456","type":"t800000","time":1583824758000,"data":{"keyword":"占某某22"}}
3.1.2、删除
指定ID删除单条数据
如下,删除指定id=11的数据:
DELETE /content/_doc/11
根据条件删除指定数据
如下,通过_delete_by_query,删除aid="V123456"的数据:
POST /content/_delete_by_query { "query":{ "term":{ "aid":"V123456" } } }
如下,通过_delete_by_query,删除 _id为11和33的数据:
POST /content/_delete_by_query { "query":{ "terms":{ "_id":["11","33"] } } }
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/docs-delete-by-query.html
3.1.3、修改
- 使用脚本更新指定id的文档
POST /content/_update/11 { "script" : { "source": "ctx._source.time= params.time", "lang": "painless", "params" : { "time" : "2020-12-08 16:00:00" } } }
- doc部分文档字段更新
POST /content/_update/11 { "doc" : { "time" : "2020-12-08 16:00:00" } }
- upsert更新
假如文档内容不存在,则将内容upsert元素作为新数据插入,否则更新操作执行script
POST /content/_update/11 { "script" : { "source": "ctx._source.time= params.time", "lang": "painless", "params" : { "time": "2020-12-11 16:00:00" } }, "upsert" : { "aid": "V123456", "type": "t800000", "time": "2020-12-12 16:00:00", "data": { "keyword": "占某某2" } } }
- update_by_query更新
根据条件进行更新
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/docs-update.html
3.1.4、其他REST接口
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/rest-apis.html
3.2、DSL查询
DSL:Domain Specific Language(特定领域语言)的缩写。
ES提供基于JSON的查询功能,将DSL查询看做AST树(Abstract Syntax Tree),他包含两部分:
- Leaf query clauses:叶子节点查询子句在特定字段中查找特定值,如match、term、或者range查询等
- Compound query clauses:复合查询子句包装了其他叶子或复合查询,用于逻辑方式组合的多个查询,如bool、dis_max查询等
3.2.1、全文查询
全文查询可以搜索已被分析的文本字段,只能找到相似的文档,并给出对应文档的评分,评分越高,则相似度越高。
- match查询
match:模糊匹配,需要指定字段名,输入会进行分词,分词后再进行匹配。
POST /content/_search { "from": 0, "size": 20, "query": { "match": { "data.keyword": "红色占某某" } } }
并且match查询,可以通过operator来进行boolean逻辑匹配控制,值有:or、and
POST /content/_search { "from": 0, "size": 20, "query": { "match": { "data.keyword": { "query": "红色占某某", "operator": "and" } } } }
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-match-query.html
- match_bool_prefix查询
match_bool_prefix会解析检索词,然后生成一个bool复合检索语句。如果检索词由很多个token构成,除了最后一个会进行prefix匹配,其他的会进行term匹配。
POST /content/_search { "query": { "match_bool_prefix": { "data.keyword": { "query": "quick brown f" } } } }
等价于:
GET /_search { "query": { "bool" : { "should": [ { "term": { "message": "quick" }}, { "term": { "message": "brown" }}, { "prefix": { "message": "f"}} ] } } }
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-match-bool-prefix-query.html
- multi_phrase查询
词组匹配会先解析检索词,并且标注出每个的token相对位置,搜索匹配的字段的必须包含所有的检索词的token,并且他们的相对位置也要和检索词里面相同。
POST /content/_search { "query": { "match_phrase": { "data.keyword": { "query": "红色占某某" } } } } 以上查询只能匹配:“红色占某某”,但不能匹配“占某某红色”
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-match-query-phrase.html
- multi_match查询
multi_match:多字段模糊查询,和match类似都是模糊查询,但multi_match可以指定多字段进行模糊查询。
POST /content/_search { "query": { "multi_match": { "query": "V123456", "fields": [ "data.keyword", "aid" ] } } }
- match_phrase_prefix查询
match_phrase_prefix相当于是结合了match_bool_prefix和match_phrase。ES会先解析检索词,分成很多个token,然后除去最后一个token,对其他的token进行match_phrase的匹配,即全部都要匹配并且相对位置相同;对于最后一个token,需要进行前缀匹配并且匹配的这个单词在前面的match_phrase匹配的结果的后面。
POST /content/_search { "query": { "match_phrase_prefix": { "data.keyword": { "query": "红色 连" } } } }
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-match-query-phrase-prefix.html
- query_string查询
query_string:和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛(当然query_string也支持指定字段查询)。
POST /content/_search { "query": { "query_string": { "query": "占某某" } } }
3.2.2、复合查询
bool多条件查询
bool查询就是由一个或多个bool子句组成复合查询,由上述term、match等组合而成。
有以下几种组合类型:
Occur Description must 相对于条件and,必须同时满足多条件 filter 子句(查询)必须出现在匹配的文档中。
但是,与查询分数不同的是,忽略该分数。
Filter子句在过滤器上下文中执行,这意味着计分被忽略,
并且子句被视为用于缓存。
(对于我们查询不需要按评分排序的场景,一般都用filter,效率更高)should 相当于条件or,只要有一个或部分条件满足 must_not 非,不满足条件 POST /content/_search { "query": { "bool": { "must": [ { "term": { "aid":"V123456" } }, { "term": { "data.keyword":"占某某" } } ] } } } POST /content/_search { "query": { "bool": { "filter": [ { "term": { "aid":"V123456" } }, { "term": { "data.keyword":"占某某" } } ] } } }
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-bool-query.html
boosting查询
boosting 查询可以用来有效地降级能匹配给定查询的结果。 与 bool 查询中的“not”子句不同,这仍然会选择包含非预期条款的文档,但会降低其总分。
constant score查询
此查询可对另一个查询进行包装,并简单地返回一个与该过滤器中每个文档的查询提升相等的常量分数
disjunction max查询
此查询生成由其子查询生成的文档的并集,并为每个文档分配由任意子查询生成的该文档的最大分数,以及任何其他匹配子查询的分数增量即 tie breaking 属性。
function score查询
function_score 允许你修改一个查询检索文档的分数。
3.2.3、术语查询
- exists非空值查询
查询原始字段中为非空值的文档。
null、[]、[null]以及指定字段完全丢失的情况,均为空值文档。对于""空字符串,不属于空值。
POST /content/_search { "query": { "exists": { "field": "aid" } } }
反过来查询空值文档,可以结合must_not使用:
POST /content/_search { "query": { "bool": { "must_not": { "exists": { "field": "aid" } } } } }
- fuzzy模糊查询
查询相似的文档。fuzzy搜索会对输入的搜索文本进行纠错,再进行搜索。fuzzy与term类似,是包含关系,并且不会对输入进行分词。Elasticsearch使用
Levenshtein edit distance
来测量相似性或模糊性。编辑距离是将一个术语转换为另一个术语所需的单字符更改数。这些变化可包括:
- Changing a character (box → fox)
- Removing a character (black → lack)
- Inserting a character (sic → sick)
- Transposing two adjacent characters (act → cat)
例如,模糊查询sic:
POST /content/_search { "query": { "fuzzy": { "data.keyword": { "value": "sic" } } } }
- ids查询
查询指定id列表的文档。
POST /content/_search { "query": { "ids" : { "values" : ["11", "22"] } } }
- prefix前缀查询
根据指定的前缀进行查询。
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-prefix-query.html
- range范围查询
根据取值范围进行查询
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-range-query.html
- regexp正则表达式查询
根据正则表达式匹配查询
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-regexp-query.html
- term值匹配查询
term:精确查询,不会对输入做分词,如果输入的是"占某某",则直接查询"占某某",如果输入的是"红色占某某",则直接查询"红色占某某"。同时要明确term是包含操作,而不是等值。
POST /content/_search { "from": 0, "size": 20, "query": { "term": { "data.keyword": "红色占某某" } } }
当查找一个精确值的时候,我们不希望对查询进行评分计算。只希望对文档进行包括或排除的计算,所以我们会使用 constant_score 查询以非评分模式来执行 term 查询并以一作为统一评分。constant_score查询为该过滤器匹配的所有文档分配1.0的分数
POST /content/_search { "from": 0, "size": 20, "query": { "constant_score": { "filter": { "term": { "data.keyword": "占某某" } } } } }
- terms多字段值匹配查询
terms类似term查询,terms支持多个值查询,是或的关系。
POST /content/_search { "from": 0, "size": 20, "query": { "terms": { "data.keyword": ["占某某","红色占某某"] } } }
- terms set查询
terms_set 支持文档级匹配查询限制,terms_set 与terms query语句很类似,区别在于terms_set 可以做到细粒度查询控制,每个文档中可以指定一个数值类型的字段用来控制匹配的term数。
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-terms-set-query.html
- wildcard通配符查询
支持的通配符:
- ? 匹配任何单个字符
- * 匹配0个或者多个字符
POST /content/_search { "query": { "wildcard": { "data.keyword": { "value": "红色*" } } } }
四、ES进阶操作
4.1、脚本
ES API脚本语法格式如下:
"script": { "lang": "...", --> 1 "source" | "id": "...", --> 2 "params": { ... } --> 3 } 1. lang: 编写脚本所用的语言,默认为`painless` 2. source,id: 脚本本身,可以指定为内联脚本的源,也可以指定为存储脚本的ID 3. params: 传给脚本参数
4.1.1、访问文档字段和特殊变量
更新脚本
在update、update-by-query或reindex API中使用的脚本能够访问ctx以下变量:
变量 描述 ctx._source
访问文档_source字段 ctx.op
文档操作项: index
ordelete
ctx._index
etc访问文档元字段(meta fields),有些可能是只读属性的 例如,update_by_query通过脚本更新文档的time:
POST /content/_update_by_query { "query": { "term": { "aid": "V123456" } }, "script" : { "source": "ctx._source.time= params.time", "lang": "painless", "params" : { "time" : "2020-12-08 16:00:00" } } }
再举例,通过脚本,删除aid="000001"并且time<="2020-12-07 16:00:00"的文档:
POST /content/_update_by_query { "query": { "bool": { "must": [{ "term": { "aid": "000001" } }, { "range": { "time": { "lte": "2020-12-07 16:00:00" } } }] } }, "script" : { "source": "ctx.op = 'delete'", "lang": "painless", "params" : {} } }
搜索和聚合脚本
可以通过脚本使用doc-values、stored字段或_source字段访问以下这些字段值:
- 访问score
- 访问doc values
- 访问stored字段和_source
TODO
4.1.2、脚本语言介绍
- painless scripting language
painless是专门设计给ES使用的,是ES默认使用的脚本语言。
- lucene expressions language
Lucene的表达式将JavaScript表达式编译为字节码。它们设计用于高性能的自定义排名和排序功能,并且默认情况下启用内联和存储脚本。
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/modules-scripting-expression.html
- 使用脚本引擎实现高级脚本
https://www.elastic.co/guide/en/elasticsearch/reference/7.3/modules-scripting-engine.html
4.2、折叠查询
使用折叠参数根据字段值折叠搜索结果。也就是我们通常理解的按特定字段进行合并去重。
字段折叠
使用collapse折叠查询结果。如下获取索引aid折叠(去重)的结果:
GET /content/_search { "query": { "match_all": {} }, "collapse":{ "field":"aid" }, "_source": ["aid","type"] }
展开折叠结果
使用inner_hits 参数展开折叠的结果。如下获取索引aid折叠后的,并展开折叠后的按照createDate排序后的前两条数据:
GET /content/_search { "query": { "match_all": {} }, "collapse":{ "field":"aid", "inner_hits":{ "name":"top_two_rated", "size":2, "sort":[{ "createDate":"desc" }] } }, "_source": ["aid","type"] }
二级折叠
注意二级折叠里面不允许使用inner_hits
GET /idx_agcontent_*/_search { "query": { "match_all": {} }, "collapse":{ "field":"type", "inner_hits":{ "name":"top_two_rated", "collapse":{ "field":"aid" }, "size":3 } }, "_source": ["aid","type"] }
4.3、聚合操作
4.3.1、度量聚合
度量聚合以某种方式从正在聚合的文档中提取的值来计算度量。这些值通常从文档的字段中提取(使用字段数据),但也可以使用脚本生成。大多数度量是简单的数学运算(例如最小值、平均值、最大值,还有汇总),这些是通过文档的值来计算的。
- 值计数聚合(Value Count Aggregation)
统计某个字段值的个数,value_count不会去重值,因此即使字段具有重复值(或者脚本为单个文档生成多个相同的值),每个值都会单独计算。
例如,统计指定字段aid总数(不包含aid为null的数据):
POST /content/_search { "aggs": { "aid_count": { "value_count": { "field": "aid" } } }, "size": 0 }
如下,用
painless
脚本语言将 script 参数解释为内联脚本进行值统计。POST /content/_search { "aggs": { "aid_count": { "value_count": { "script": { "source": "doc['aid']" } } } }, "size": 0 }
- 基数聚合(Cardinality Aggregation)
用于计算不同值的近似计数。值可以从文档中的特定字段中提取,也可以由脚本生成。
例如,统计指定字段aid去重后总数(不包含aid为null的数据):
POST /content/_search { "aggs": { "aid_count": { "cardinality": { "field": "aid" } } }, "size": 0 }
同时这种聚合操作支持
precision_threshold
参数选项,precision_threshold选项允许用内存交换精度,并定义一个唯一计数,低于此计数的计数值将接近精确。超过这个值,计数可能会变得模糊一些。支持的最大值为40000,高于此数的阈值将与阈值40000的效果相同。默认值为3000。基数聚合是基于
hyperloglog++
算法实现的。
- 均值聚合(Avg Aggregation)
统计均值
- 最大值聚合(Max Aggregation)
统计最大值
- 最小值聚合(Min Aggregation)
统计最小值
其他相关聚合不再一一介绍,详情查看官网文档。
4.3.2、桶聚合
桶聚合不像度量聚合那样计算字段上的度量,而是创建文档桶。桶可以理解为是满足特定条件文档的集合。
- 术语聚合(Terms Aggregation)
根据字段值项分组聚合。
例如,获取aid值聚合,并根据计数进行降序排序:
POST /content/_search { "aggs": { "aid_value": { "terms": { "field": "aid", "order": { "_count": "desc" }, "size": 3 } } }, "size": 0 }
术语聚合不支持一个文档中多字段聚合。原因在于术语聚合本身并不收集字符串值,而是使用
全局序数(global ordinals)
来生成字段中所有唯一值的列表。全局顺序导致一个重要的性能提升,这导致不可能跨多个字段。 有以下两种方法可用于跨多个字段执行术语聚合:
Script:
使用脚本从多字段检索术语。这会将全局序数优化,并且这个比从单个字段收集术语要慢,但是可以更加灵活的实现相关搜索。
copy_to field:
如果提前知道要从两个或多个字段收集术语,那么在映射中使用copy_to在索引时创建一个新的专用字段,其中包含两个字段的值。可以在此单个字段上聚合,这将受益于全局序数优化。
如下,使用脚本,获取aid+type唯一值的聚合:
POST /content/_search { "aggs": { "app_count": { "terms": { "script": { "inline": "doc['aid'].value +'-split-'+ doc['type'].value" }, "order": { "_count": "desc" }, "size": 1000 } } }, "size": 0 }
术语聚合可以包含子聚合,例如:
POST /content/_search { "aggs": { "type_value": { "terms": { "field": "type", "order": { "_count": "desc" } }, "aggs": { "aid_value": { "terms": { "field": "aid", "size": 5 } } } } }, "size": 0 }