ES的数据结构与DSL语法
本篇章将围绕ES的基本功能、实现原理与性能优化这三个方面,使读者能够逐渐深入了解ES的特点与能力。本篇所用的版本为7.10.2同kibana版本,需要注意的是,es与kibana至少在大版本上要保持一致,否则功能上会有兼容性问题。
ES的特征
-
分布式:ES是分布式的开源搜索和分析引擎,基于Apache Lucene开发而成,适用于所有类型的数据,包括文字、数字、地理空间、结构化和非结构化数据
1.1. 分布式架构:
之后的章节里,会逐步讲解ES的架构内容。1.2 集群(Cluster)
1.3 节点(Node)
1.4 副本(Replica):副本,replica主要用来保证高可用(故障转移)、数据备份、增强高吞吐的并行搜索
1.5 分片(Shard):是非常重要的点,它允许ES跨分片(可以是多个节点)分布和并行操作,从而提高性能与吞吐量 -
存储方式:不同于数据库中的行列存储形式,ES将信息存储为JSON文档的数据结构
-
全文检索:全文检索功能是ES广泛运用的主要原因,由于分布式的特点,存储文档会分布整个集群中,因此搜索效率非常高
数据结构部分
ES通过倒排索引的数据结构做到快速索引和全文检索,该结构支持非常快速的全文本搜索。
一般情况下,ES默认对每个字段中的所有数据建立索引,并且每个索引字段都具备专有的优化数据结构
例如,文本字段是存储在倒排索引中,而数字或地理信息的字段存储在BKD树中,BKD树可以理解为多维度的树形结构。
数据类型 | 数据结构 |
---|---|
text/keyword | 倒排索引 |
数字/地理位置 | BKD树 |
正向索引与倒排索引
定义
倒排索引的英文是Inverted index (link),是有转换的意思。被翻译成倒排我觉得有两个原因:
- 由于倒排索引不同于关系型数据库,通过id去查找内容,而在ES中是通过keyword,也就是文档内容去查找具体文件
- ES中还有个正排索引Forword Index表中记录文档中每个字的位置信息,查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档。
结构
下面针对两种索引举例(index_01: this is elasticsearch / index_02:elasticsearch query)
倒排索引
关键词 | 文档id |
---|---|
this | index_01 |
is | index_01 |
elasticsearch | index_01, index_02 |
query | index_02 |
正排索引
文档id | 关键词 |
---|---|
index_01 | this, is, elasticsearch |
index_02 | elasticsearch, query |
分词(keyword)
ES对于文档内容或者某一段文本,都会进行分词,也就是把一段话中的单词进行拆分。分词的效果将直接影响倒排索引的搜索能力。
所以选择合适的分词是十分重要的。对于不同语言,分词器也是不同的。比如英文会使用Standard, ngram等,中文可选ik分词器,常用有ik_smart_max_word。
这里以英文举例查看不同分词器的效果
在ngram_tokenizer的格式中,可以设置最大最小步长,
// An highlighted block
"tokenizer": {
"my_tokenizer": {
"type": "ngram",
"min_gram": 1,
"max_gram": 2,
"token_chars": [
"letter",
"digit"
]
}
}
以"Qu Cdn"为例,使用ngram分词
// An highlighted block
{
"tokenizer": "ngram",
"text": "Qu Cdn"
}
以下是分词的返回结果,可以看到颗粒度限制在1-2的长度,甚至还包括空格
// An highlighted block
{
"tokens": [
{
"token": "Q",
"start_offset": 0,
"end_offset": 1,
"type": "word",
"position": 0
},
{
"token": "Qu",
"start_offset": 0,
"end_offset": 2,
"type": "word",
"position": 1
},
{
"token": "u",
"start_offset": 1,
"end_offset": 2,
"type": "word",
"position": 2
},
{
"token": "u ",
"start_offset": 1,
"end_offset": 3,
"type": "word",
"position": 3
},
{
"token": " ",
"start_offset": 2,
"end_offset": 3,
"type": "word",
"position": 4
},
{
"token": " C",
"start_offset": 2,
"end_offset": 4,
"type": "word",
"position": 5
},
{
"token": "C",
"start_offset": 3,
"end_offset": 4,
"type": "word",
"position": 6
},
{
"token": "Cd",
"start_offset": 3,
"end_offset": 5,
"type": "word",
"position": 7
},
{
"token": "d",
"start_offset": 4,
"end_offset": 5,
"type": "word",
"position": 8
},
{
"token": "dn",
"start_offset": 4,
"end_offset": 6,
"type": "word",
"position": 9
},
{
"token": "n",
"start_offset": 5,
"end_offset": 6,
"type": "word",
"position": 10
}
]
}
使用Standard分词情况,返回的结果就是按空格分开
// An highlighted block
{
"tokens": [
{
"token": "Qu",
"start_offset": 0,
"end_offset": 2,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "Cdn",
"start_offset": 3,
"end_offset": 6,
"type": "<ALPHANUM>",
"position": 1
}
]
}
分词器的不同,ES创建的倒排索引结构也将不同,进而编写的搜索条件也将不同。
这里需要考虑的是,分词器的分词颗粒度越细,倒排索引的结构也就越大。之后就是匹配度与性能的鱼和熊掌的问题。
所以要根据实际业务选择合适的分词器,下一部分将展示不同的elasticsearch-dsl语句。
DSL语句
同其他类型的数据库一样,ES也有自己的query语言,其搜索结构都是由json串组合构成请求体发送。
常用DSL语句
索引部分
创建索引
创建索引最关键的部分就是settings跟mappings的部分,settings设置包括分片副本,分词器等内容
mappings就是index所包含的映射结构与内容,比如mappings中可以是否为动态映射。
PUT /index03
{
"settings": {
"analysis": {
"analyzer": {
"ngram_analyzer": {
"tokenizer": "ngram_tokenizer",
"filter": ["lowercase"]
}
},
"tokenizer": {
"ngram_tokenizer": {
"type": "ngram",
"min_gram": 3,
"max_gram": 3
}
}
},
"mappings":{
"dynamic": false,
"properties":{
"object":{
"attr":{
"name": {"type": "keyword"},
"site": {"type": "long"},
"filepath": {"type": "text"},
"filename": {"type": "keyword"},
"date": {"type": "keyword"}
}
}
}
}
}
}
需要注意的是,如果一个index中field太多会导致映射爆炸的问题,所以需要设定index.mapping.total_fileds_limit属性
查看es所有索引
GET /_cat/indices?v
查看单个索引
GET /index03
删除索引
DELETE /index03
文档部分
创建文档
POST /index03/_doc/7
{
"name" : "kibana",
"site" : 212121,
"date" : "20230101",
"filepath" : "/a/b/k/d/e/f",
"filename" : "nothing"
}
批量执行
POST /index03/_doc/_bulk
{"index":{"_id":7}}
{"name":"elastic","site":24,"filepath":"/a/b/k/d/e/f"}
{"index":{"_id":8}}
{"name":"search","site":-11,"filepath":"/a/b/k/d/e/f"}
{"index":{"_id":9}}
{"name":"stack","site":0,"filepath":"/a/b/k/d/e/f"}
查询文档
GET /index03/_doc/7
返回值
{
"_index" : "index03",
"_type" : "_doc",
"_id" : "7",
"_version" : 1,
"_seq_no" : 6,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "kibana",
"site" : 212121,
"date" : "20230101",
"filepath" : "/a/b/k/d/e/f",
"filename" : "nothing"
}
}
查询操作DSL
DSL Query的分类
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
查询所有:查询出所有数据,一般测试用。例如:match_all
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
match_query, multi_match_query
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
ids, range, term
地理(geo)查询:根据经纬度查询。例如:
geo_distance, geo_bounding_box
复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
bool, function_score
查询验证
这是比较重要的一个点,有些时候语句执行出错,需要排查是数据问题,还是搜索语法的问题。
比如我这里举例在文档里找indexname为killbill的数据,但实际没有indexname这个field(类似mysql的column)
GET /index03/_doc/_validate/query?explain
{
"query":{
"match":{
"indexname":"killbill"
}
}
}
如果语法是正确的话,会返回
{
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"valid" : true,
"explanations" : [
{
"index" : "index03",
"valid" : true,
"explanation" : """+MatchNoDocsQuery("unmapped fields [indexname]") #*:*"""
}
]
}
这里显示语法检查是正确的,并且会提示mappings里面没有indexname这个field
是一个非常好用的语法
条件查询
这里主要介绍几种查询的语法格式,相同语法格式会合并,就是替换关键词就行。比如match可以换成match_phrase,语法格式上没有区别,只是query下的搜索关键词不同。
match/match_phrase(完全匹配)
match:分词后搜索。比如分词后有N个分词,只要匹配上其中一个就可以返回数据了
match_phrase:分词后搜索。与match不同的是,match_phase分出来的词必须全部在搜索结果中,且位置顺序是一样的。
GET /index03/_doc/_search
{
"query":{
"match":{
"name":"AFIND"
}
}
}
match_all并且排序
GET /index03/_search
{
"query":{
"match_all":{}
},
"sort":{
"site":{
"order":"desc"
}
}
}
多条件查询
GET /index03/_search
{
"query":{
"bool":{ # 表示需要进行条件过滤
"must":[{ #表示必须满足下面的条件,并且参与计算分值,常用的子句还有should,表示“或”的意思
"wildcard":{
"filename":"*bill"
}
},{
"match":{
"site": 10086
}
}]
}
}
}
聚合分页查询
需要注意的是,在聚合中,只能针对整型类的数据类型(byte,short,integer,long)
GET /index03/_search
{
"aggs":{ #表示聚合操作
"site_group":{ #聚合后分组名称,可以随便起
"terms":{ #表示分组操作,也可以使用avg来求平均值
"field":"site" #表示对哪一个字段进行分组
}
}
},
"size":0 #表示不查询原始数据,只查询分组结果
}
结语
本篇内容主要介绍了ES的入门知识点与常用操作的语法,之后会基于其内核Lucene以及分布式架构展开并且深入,并设计性能提升方案。