ElasticSearch进阶

分值计算

首先根据用户query条件,过滤出包含指定term的doc,Field-length normfield长度越长相关度越弱

1
2
3
query "hello world" -->  hello / world / hello & world
bool --> must/must not/should --> 过滤 --> 包含 / 不包含 / 可能包含
doc --> 不打分数 --> 正或反 true or false --> 为了减少后续要计算的doc的数量,提升性能

relevance score算法:计算出一个索引中文本搜索文本之间关联匹配程度,ES使用term frequency/inverse document frequency算法简称为TF/IDF算法。Term frequency搜索文本各个词条field文本中出现次数次数越多越相关Inverse document frequency搜索文本各个词条整个索引所有文档出现次数出现次数越多越不相关

向量空间模型

vector space model向量空间模型,多个term对一个doc的总分数,es会根据查询字符串在所有doc中的评分情况,计算出一个query vectorquery向量,会给每一个doc,拿每个term计算出一个分数来。每个doc vector计算出对query vector弧度,最后基于该弧度给出一个doc相对于query中多个term的总分数,弧度越大分数越低弧度越小分数越高。若是多个term,那么就是线性代数来计算,无法用图表示。

若查询条件字符串为hello world,hello这个term,给的基于所有doc的一个评分就是3,world这个term,给的基于所有doc的一个评分就是6,则query向量[3, 6],若3个doc一个包含hello,一个包含world,一个包含hello和world,doc向量分别为[3, 0]、[0, 6]、[3, 6]。

分词器工作流程

首先进行normalization切分词语,将目标文本拆分成单个单词,同时对每个单词进行normalization时态转换单复数转换分词器recall搜索时召回率、增加能搜索到的结果的数量分词器将文本进行各种处理,最后处理好的结果才会用来建立倒排索引

1
2
3
character filter:在一段文本进行分词之前,先进行预处理,如过滤html标签(<span>hello<span> --> hello),& --> and (I&you --> I and you)
tokenizer:分词,hello you and me --> hello, you, and, me
token filter:lowercase,stop word,synonymom,liked --> like,Tom --> tom,a/the/an --> 干掉,small --> little

对于默认的standard分词器

  • standard tokenizer:以单词边界进行切分
  • standard token filter:什么都不做
  • lowercase token filter:将所有字母转换为小写
  • stop token filer:默认被禁用,移除停用词,比如a the it等等
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    POST _analyze
    {
    "analyzer": "standard",
    "text": "Set the shape to semi-transparent by calling set_trans(5)"
    }
    PUT /my_index
    {
    "settings": {
    "analysis": {
    "analyzer": {
    "es_std": {
    "type": "standard",
    "stopwords": "_english_" // 启用english停用词token filter
    }
    }
    }
    }
    }
    GET /my_index/_analyze
    {
    "analyzer": "standard",
    "text": "a dog is in the house"
    }
    GET /my_index/_analyze
    {
    "analyzer": "es_std",
    "text":"a dog is in the house"
    }

    PUT /my_index // 定制化分词器
    {
    "settings": {
    "analysis": {
    "char_filter": {"&_to_and": {"type": "mapping","mappings": ["&=> and"]}},
    "filter": {"my_stopwords": {"type": "stop","stopwords": ["the","a"]}
    },
    "analyzer": {
    "my_analyzer": {"type": "custom","char_filter": ["html_strip","&_to_and"],"tokenizer": "standard","filter": ["lowercase","my_stopwords"]}
    }
    }
    }
    }
    GET /my_index/_analyze
    {
    "text": "tom&jerry are a friend in the house, <a>, HAHA!!",
    "analyzer": "my_analyzer"
    }

IK分词器

IK分词器配置文件地址es/plugins/ik/config,ik原生最重要的是main.dicstopword.dic两个配置文件

  • IKAnalyzer.cfg.xml:用来配置自定义词库
  • main.dic:ik原生内置中文词库,总共有27万多条,会按照该文件中的词语去分词
  • quantifier.dic单位相关的词
  • suffix.dic后缀相关的词
  • surname.dic:中国姓氏
  • stopword.dic英文停用词,停用词会在分词时被干掉不会建立在倒排索引中

可通过在IKAnalyzer.cfg.xml配置文件中通过修改<entry key="ext_dict"></entry>配置内容扩展自己的词库需重启es才能生效,还可以通过修改<entry key="ext_stopwords"></entry>配置扩展停用词

每次在es扩展词典中,手动添加新词语,添加完都要重启es才能生效,非常麻烦,且es是分布式的,可能有数百个节点,不能每次都一个一个节点上面去修改。IKAnalyzer.cfg.xml配置文件中可通过<entry key="remote_ext_dict">words_location</entry><entry key="remote_ext_stopwords">words_location</entry>配置支持远程扩展字典

高亮显示

搜索中经常需要对搜索关键字高亮显示,ES默认通过添加<em></em>标签,在HTML中会变成红色,指定的field中若包含了搜索词,就会在那个field文本中,对搜索词进行红色高亮显示。highlight中的field必须跟query中field一一对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
PUT /news_website/_doc/1
{
"title": "这是我写的第一篇文章",
"content": "大家好,这是我写的第一篇文章,特别喜欢这个文章门户网站!!!"
}
GET /news_website/_doc/_search
{
"query": {
"match": {"title": "文章"}
},
"highlight": {
"fields": {"title": {}}
}
}

GET /news_website/_doc/_search
{
"query": {
"bool": {
"should": [
{"match": {"title": "文章"}},
{"match": {"content": "文章"}}
]
}
},
"highlight": {
"fields": {"title": {},"content": {}
}
}
}

默认的highlightplain highlightlucene highlight,在mapping中设置index_optionsoffsets使用posting highlight。在mapping中设置term_vectorterm_vector使用fast verctor highlight,对大于1mbfield性能更高。也可通过在查询时强制使用某种highlighter

一般情况下用plain highlight也就足够了,不需要做其他额外设置,若对高亮性能要求很高,可尝试启用posting highlight,若field值特别大超过了1M,则可用fast vector highlight

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
PUT /news_website
{
"mappings": {
"properties": {
"title": {"type": "text","analyzer": "ik_max_word"},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"index_options": "offsets"
}
}
}
}
PUT /news_website
{
"mappings": {
"properties": {
"title": {"type": "text","analyzer": "ik_max_word"},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"term_vector": "with_positions_offsets"
}
}
}
}
GET /news_website/_doc/_search
{
"query": {"match": {"content": "文章"}},
"highlight": {
"fields": {"content": {"type": "plain"}}
}
}
GET /news_website/_doc/_search
{
"query": {"match": {"content": "文章"}},
"highlight": { // 设置高亮html标签,默认是<em>标签
"pre_tags": ["<span color='red'>"],
"post_tags": ["</span>"],
"fields": {"content": {"type": "plain"}}
}
}
GET /_search
{
"query": {"match": {"content": "文章"}},
"highlight": {
"fields": {
"content": {
"fragment_size": 150, // 设置要显示出来的fragment文本长度,默认100
"number_of_fragments": 3 // 指定显示高亮fragment文本片段个数
}
}
}
}
用一个大家容易理解的SQL语法来解释,如:select count(*) from table group by column。那么group by column分组后的每组数据就是bucket。对每个分组执行的count(*)就是metric。

聚合搜索

bucket就是一个聚合搜索时的数据分组metric就是对一个bucket数据执行的统计分析metric求和最大值最小值平均值多种统计。如select count(*) from table group by column其中group by column分组后的每组数据就是bucket,每个分组执行的count(*)就是metric

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PUT /cars
{"mappings":{"properties":{"price":{"type":"long"},"color":{"type":"keyword"},"brand":{"type":"keyword"},"model":{"type":"keyword"},"sold_date":{"type":"date"},"remark":{"type":"text","analyzer":"ik_max_word"}}}}

POST /cars/_bulk
{"index":{}}
{"price":258000,"color":"金色","brand":"大众","model":"大众迈腾","sold_date":"2021-10-28","remark":"大众中档车"}
{"index":{}}
{"price":123000,"color":"金色","brand":"大众","model":"大众速腾","sold_date":"2021-11-05","remark":"大众神车"}
{"index":{}}
{"price":239800,"color":"白色","brand":"标志","model":"标志508","sold_date":"2021-05-18","remark":"标志品牌全球上市车型"}
{"index":{}}
{"price":148800,"color":"白色","brand":"标志","model":"标志408","sold_date":"2021-07-02","remark":"比较大的紧凑型车"}
{"index":{}}
{"price":1998000,"color":"黑色","brand":"大众","model":"大众辉腾","sold_date":"2021-08-19","remark":"大众最让人肝疼的车"}
{"index":{}}
{"price":218000,"color":"红色","brand":"奥迪","model":"奥迪A4","sold_date":"2021-11-05","remark":"小资车型"}
{"index":{}}
{"price":489000,"color":"黑色","brand":"奥迪","model":"奥迪A6","sold_date":"2022-01-01","remark":"政府专用?"}
{"index":{}}
{"price":1899000,"color":"黑色","brand":"奥迪","model":"奥迪A 8","sold_date":"2022-02-12","remark":"很贵的大A6。。。"}

根据color分组统计销售数量,只执行聚合分组,ES中最基础的聚合terms,相当于SQL中的count,ES中默认为分组数据做排序,使用的是doc_count数据执行降序排列。可使用_key元数据根据分组后的字段数据执行不同的排序方案,也可根据_count元数据,根据分组后的统计值执行不同的排序方案

1
2
3
4
5
6
7
8
9
10
11
GET /cars/_search
{
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {"_count": "desc"}
}
}
}
}

先根据color执行聚合分组,在此分组的基础上,对组内数据执行聚合统计,组内数据的聚合统计就是metric,同样可执行排序,因为组内有聚合统计,且对统计数据给予了命名avg_by_price,所以可根据该聚合统计数据字段名执行排序逻辑size可设置为0,表示不返回文档只返回聚合之后的数据提高查询速度,若需要这些文档也可按照实际情况进行设置。对聚合统计数据进行排序,若有多层aggs执行下钻聚合时也可根据最内层聚合数据执行排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
GET /cars/_search
{
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {"avg_by_price": "asc"}
},
"aggs": {
"avg_by_price": {"avg": {"field": "price"}}
}
}
}
}
GET /cars/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {"field": "color"},
"aggs": {
"group_by_brand": {
"terms": {"field": "brand","order": {"avg_by_price": "desc"}},
"aggs": {"avg_by_price": {"avg": {"field": "price"}}}
}
}
}
}
}

先根据color聚合分组,在组内根据brand再次聚合分组,这种操作可称为下钻分析aggs若定义比较多,则会感觉语法格式混乱,aggs语法格式有一个相对固定的结构,aggs可嵌套定义也可水平定义嵌套定义称为下钻分析水平定义就是平铺多个分组方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /index_name/type_name/_search
{
"aggs": {
"定义分组名称": {
"分组策略如:terms、avg、sum": {
"field": "根据哪一个字段分组",
"其他参数": ""
},
"aggs": {
"分组名称1": {},
"分组名称2": {}
}
}
}
}

统计不同color中的最大和最小价格、总价,聚合分析最常用的种类就是统计数量最大最小平均总计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
GET /cars/_search
{
"aggs": {
"group_by_color": {
"terms": {"field": "color"},
"aggs": {
"max_price": {"max": {"field": "price"}},
"min_price": {"min": {"field": "price"}},
"sum_price": {"sum": {"field": "price"}}
}
}
}
}
GET cars/_search // 统计不同品牌汽车中价格排名最高的车型
{
"size": 0,
"aggs": {
"group_by_brand": {
"terms": {"field": "brand"},
"aggs": {
"top_car": {
"top_hits": {
"size": 1, // 取组内多少条数据,默认为10
"sort": [{"price": {"order": "desc"}}], // 组内使用什么字段什么规则排序,默认使用_doc的asc规则排序
"_source": {"includes": ["model","price"]} // 结果中包含document中的哪些字段,默认包含全部字段
}
}
}
}
}
}

histogram区间统计类似terms,也是用于bucket分组操作,是根据一个field实现数据区间分组。如以100万为一个范围,统计不同范围内车辆销售量和平均价格。使用histogram聚合时field指定价格字段price,区间范围是100万,此时ES会将price价格区间划分为: [0, 1000000), [1000000, 2000000), [2000000, 3000000)等依次类推。在划分区间同时histogram会类似terms进行数据数量统计,可通过嵌套aggs对聚合分组后的组内数据做再次聚合分析

1
2
3
4
5
6
7
8
9
10
11
12
GET /cars/_search
{
"aggs": {
"histogram_by_price": {
"histogram": {
"field": "price",
"interval": 1000000
},
"aggs": {"avg_by_price": {"avg": {"field": "price"}}}
}
}
}

date_histogram区间分组可对date类型的field执行区间聚合分组,若以月为单位,统计不同月份汽车销售数量及销售总金额。此时可使用date_histogram实现聚合分组,其中field来指定用于聚合分组的字段,interval指定区间范围,可选值有yearquartermonthweekdayhourminutesecondformat指定日期格式化min_doc_count指定每个区间最少document,若不指定默认为0,当区间范围内没有document时,也会显示bucket分组,extended_bounds指定起始时间结束时间,若不指定默认使用字段中日期最小值最大值作为起始和结束时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /cars/_search
{
"aggs": {
"histogram_by_date": {
"date_histogram": {
"field": "sold_date",
"interval": "month", // 7.X之后使用calendar_interval,指定区间范围
"format": "yyyy-MM-dd", // 指定日期格式化
"min_doc_count": 1,
"extended_bounds": {"min": "2021-01-01","max": "2022-12-31"}
},
"aggs": {"sum_by_price": {"sum": {"field": "price"}}}
}
}
}

聚合统计数据时,有时需要对比部分数据和总体数据,如统计某品牌车辆平均价格和所有车辆平均价格。global是用于定义一个全局bucket,该bucket忽略query的条件,检索所有document进行对应的聚合统计。

1
2
3
4
5
6
7
8
9
10
11
12
GET /cars/_search
{
"size": 0,
"query": {"match": {"brand": "大众"}},
"aggs": {
"volkswagen_of_avg_price": {"avg": {"field": "price"}}, // 统计某品牌车辆平均价格
"all_avg_price": { // 所有车辆平均价格
"global": {},
"aggs": {"all_of_price": {"avg": {"field": "price"}}}
}
}
}

filter也可和aggs组合使用,实现相对复杂的过滤聚合分析,filter的范围决定了其过滤的范围,将filter放在aggs内部,代表该过滤器只对query搜索得到的结果执行filter过滤。若filter放在aggs外部,过滤器则会过滤所有数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /cars/_search // filter和aggs组合使用,实现相对复杂的过滤聚合分析
{
"query": {
"constant_score": {
"filter": {"range": {"price": {"gte": 100000,"lte": 500000}}}
}
},
"aggs": {"avg_by_price": {"avg": {"field": "price"}}}
}
GET /cars/_search
{
"query": {"match": {"brand": "大众"}},
"aggs": {
"count_last_year": { // 12M/M表示12个月,1y/y表示1年,d表示天
"filter": {"range": {"sold_date": {"gte": "now-12M"}}},
"aggs": {"sum_of_price_last_year": {"sum": {"field": "price"}}}
}
}
}

数据建模

如下设计一个用户document数据类型,其中包含一个地址数据的数组,该设计方式相对复杂,但在管理数据时更加的灵活。但也有明显的缺陷,针对地址数据做数据搜索时,经常会搜索出不必要的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
PUT /user_index
{
"mappings": {
"properties": {
"login_name": {"type": "keyword"},
"age ": {"type": "short"},
"address": {
"properties": {
"province": {"type": "keyword"},
"city": {"type": "keyword"},
"street": {"type": "keyword"}
}
}
}
}
}
PUT /user_index/_doc/1
{
"login_name": "jack",
"age": 25,
"address": [
{"province": "北京","city": "北京","street": "枫林三路"},
{"province": "天津","city": "天津","street": "华夏路"}
]
}
PUT /user_index/_doc/2
{
"login_name": "rose",
"age": 21,
"address": [
{"province": "河北","city": "廊坊","street": "燕郊经济开发区"},
{"province": "天津","city": "天津","street": "华夏路"}
]
}
GET /user_index/_search
{
"query": {
"bool": {
"must": [
{"match": {"address.province": "北京"}},
{"match": {"address.city": "天津"}}
]
}
}
}

可使用nested object作为地址数组的集体类型可解决上述问题,且搜索时需要使用nested对应的搜索语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
PUT /user_index
{
"mappings": {
"properties": {
"login_name": {"type": "keyword"},
"age ": {"type": "short"},
"address": {
"type": "nested",
"properties": {
"province": {"type": "keyword"},
"city": {"type": "keyword"},
"street": {"type": "keyword"}
}
}
}
}
}
GET /user_index/_search
{
"query": {"bool": {"must": [
{"nested": {"path": "address",
"query": {
"bool": {"must": [
{"match": {"address.province": "北京"}},
{"match": {"address.city": "北京"}}
]}
}}
}]
}}
}

普通数组数据在ES中会被扁平化处理nested object数据类型ES在保存时不会扁平化处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{ // 普通数组
  "login_name" : "jack",
  "address.province" : [ "北京", "天津" ],
  "address.city" : [ "北京", "天津" ]
  "address.street" : [ "枫林三路", "华夏路" ]
}
// nested数据
{
  "login_name" : "jack"
}
{
  "address.province" : "北京",
  "address.city" : "北京"
  "address.street" : "枫林三路"
}
{
  "address.province" : "天津",
  "address.city" : "天津",
  "address.street" : "华夏路",
}

nested object建模缺点是采取的是类似冗余数据的方式,将多个数据放在一起,维护成本比较高,每次更新需要重新索引整个对象,包括根对象和嵌套对象。ES提供类似关系型数据库Join的实现,使用Join数据类型实现父子关系,从而分离两个文档对象。

更新父文档无需重新索引整个子文档,子文档被新增,更改和删除也不会影响到父文档和其他子文档,父子关系元数据映射,用于确保查询时高性能,但是有一个限制父子数据包括映射其关联关系的元数据必须存在于一个shard ,搜索父子关系数据时,不用跨分片性能高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
PUT my_blogs
{
"mappings": {
"properties": {
"blog_comments_relation": {
"type": "join", // 指明join类型
"relations": { // 声明父子关系
"blog": "comment" // blog为父文档名称,comment为子文档名称
}
},
"content": {"type": "text"},
"title": {"type": "keyword"}
}
}
}
PUT my_blogs/_doc/blog1 // blog1为父文档id
{
"title": "Learning Elasticsearch",
"content": "learning ELK is happy",
"blog_comments_relation": { // 声明文档类型
"name": "blog"
}
}
PUT my_blogs/_doc/blog2 // blog2为父文档id
{
"title": "Learning Hadoop",
"content": "learning Hadoop",
"blog_comments_relation": { // 声明文档类型
"name": "blog"
}
}
// 父文档和子文档必须存在相同的分片上, 当指定文档时候,必须指定它的父文档ID
PUT my_blogs/_doc/comment1?routing=blog1 // 使用route参数来保证,分配到相同分片
{
"comment": "I am learning ELK",
"username": "Jack",
"blog_comments_relation": {
"name": "comment",
"parent": "blog1"
}
}
PUT my_blogs/_doc/comment2?routing=blog2 // comment2为子文档id,blog2为父文档id
{
"comment": "I like Hadoop!!!!!",
"username": "Jack",
"blog_comments_relation": {
"name": "comment",
"parent": "blog2"
}
}
PUT my_blogs/_doc/comment3?routing=blog2
{
"comment": "Hello Hadoop",
"username": "Bob",
"blog_comments_relation": {
"name": "comment",
"parent": "blog2"
}
}

POST my_blogs/_search // 查询所有文档
{}
GET my_blogs/_doc/blog2 // 根据父文档ID查看
POST my_blogs/_search // parent_id查询,返回所有相关子文档
{
"query": {
"parent_id": {"type": "comment","id": "blog2"}
}
}
POST my_blogs/_search // has_child查询,返回父文档
{
"query": {
"has_child": {
"type": "comment",
"query": {"match": {"username": "Jack"}}
}
}
}
POST my_blogs/_search // has_parent查询,返回相关的子文档
{
"query": {
"has_parent": {
"parent_type": "blog",
"query": {"match": {"title": "Learning Hadoop"}}
}
}
}
PUT my_blogs/_doc/comment3?routing=blog2 //更新子文档不会影响到父文档
{
"comment": "Hello Hadoop??",
"blog_comments_relation": {
"name": "comment",
"parent": "blog2"
}
}

文件系统数据

若需要使用文件路径搜索内容,只需要为其中的字段定义一个特殊的path_hierarchy分词器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PUT /codes
{
"settings": {"analysis": {"analyzer": {"path_analyzer": {"tokenizer": "path_hierarchy"}}}},
"mappings": {
"properties": {
"fileName": {"type": "keyword"},
"path": {
"type": "text",
"analyzer": "path_analyzer",
"fields": {"keyword": {"type": "text","analyzer": "standard"}}
},
"content": {"type": "text","analyzer": "standard"}
}
}
}
PUT /codes/_doc/1
{
"fileName": "HelloWorld.java",
"path": "/com/eleven/first",
"content": "package com.eleven.first; public class HelloWorld { // some code... }"
}
GET /codes/_search
{
"query": {"match": {"path": "/com"}}
}
GET /codes/_analyze
{
"text": "/a/b/c/d",
"field": "path"
}
GET /codes/_search
{
"query": {"match": {"path.keyword": "/com"}}
}

GET /codes/_search
{
"query": {"bool": {"should": [
{"match": {"path": "/com"}},
{"match": {"path.keyword": "/com/eleven"}}
]}}
}

Scroll分页

使用fromsize方式查询1W以内的数据都OK,但若数据比较多时会出现性能问题。ES做了一个限制不允许查询1W条以后的数据。若要查询1W条以后的数据,可使用ES中提供的scroll游标来查询。

在进行大量分页时,每次分页都需要将要查询数据进行重新排序,这样非常浪费性能。使用scroll游标将要用的数据一次性排序好,然后分批取出。性能要比from + size好得多,使用scroll查询后,排序后的数据会保持一定的时间,后续分页查询都从该快照取数据。响应结果中会返回_scroll_id,第二次查询直接使用_scroll_id来查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /es_db/_search?scroll=1m  // 让排序的数据保持1分钟
{
"query": {
"multi_match": {
"query": "广州长沙张三",
"fields": ["address","name"]
}
},
"size": 100
}
GET _search/scroll?scroll=1m
{
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFnJKUnZmX1pIVGVpM05TWDBQX0JJeXcAAAAAAAaeghZDUkdZN1FJNVIwYUJhYUxvNWVxd1Rn"
}

SQL支持

ES SQL允许执行SQL查询REST接口命令行JDBC等都可使用SQL来进行数据检索数据聚合。特点:

  • 本地集成:ES SQL是专门为ES构建的,每个SQL查询都根据底层存储对相关节点有效执行
  • 无额外要求:不依赖其他硬件、进程、运行时库,可直接运行在ES集群上
  • 轻量且高效:像SQL那样简洁、高效地完成查询
1
2
3
4
5
6
7
8
SELECT select_expr [, ...]
[ FROM table_name ]
[ WHERE condition ]
[ GROUP BY grouping_element [, ...] ]
[ HAVING condition]
[ ORDER BY expression [ ASC | DESC ] [, ...] ]
[ LIMIT [ count ] ]
[ PIVOT ( aggregation_expr FOR column IN ( value [ [ AS ] alias ] [, ...] ) ) ]

目前FROM只支持单表不支持JOIN不支持较复杂的子查询format表示指定返回数据类型,支持的类型有逗号分隔csvjson、制表符分隔符tsvtxtyaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /_sql?format=json
{
"query": "SELECT * FROM es_db limit 1"
}
GET /_sql/translate // 将SQL转换为DSL
{
"query": "SELECT * FROM es_db limit 1"
}
GET /_sql?format=json // field_exp匹配字段,constant_exp匹配常量表达式,
{ // 检索address包含广州和name中包含张三的用户
"query": "select * from es_db where MATCH(address, '广州') or MATCH(name, '张三') limit 10"
}
GET /_sql?format=txt // 统计分组
{
"query": "select age, count(*) as age_cnt from es_db group by age"
}

模本搜索

模板搜索可将一些搜索进行模板化,每次执行该搜索就直接调用模板,传入一些参数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
GET /cars/_search/template // 简单定义参数并传递
{
"source": {
"query": {"match": {"remark": "{{kw}}"}},
"size": "{{size}}"
},
"params": {"kw": "大众","size": 2}
}
GET cars/_search/template // toJson方式传递参数
{
"source": """{ "query": { "match": {{#toJson}}parameter{{/toJson}} }}""",
"params": {
"parameter": {"remark": "大众"}
}
}
GET cars/_search/template // json方式传递参数
{
"source": {"query": {"match": {
"remark": "{{#join delimiter=' '}}kw{{/join delimiter=' '}}"
}}},
"params": {"kw": ["大众","标致"]}
}
GET cars/_search/template
{
"source": {"query": {"range": {"price": {
"gte": "{{start}}",
"lte": "{{end}}{{^end}}200000{{/end}}" // 默认值定义
}}}},
"params": {"start": 100000,"end": 140000}
}

记录template实现重复调用可使用Mustache语言作为搜索请求预处理,它提供模板通过键值对来替换模板中的变量。把脚本存储在本地磁盘中,默认位置为elasticsearch\config\scripts,通过引用脚本名称进行使用

1
2
3
4
5
6
7
8
9
10
11
12
13
POST _scripts/test // test为脚本id
{
"script": {
"lang": "mustache", // 指定mustache语言
"source": {"query": {"match": {"remark": "{{kw}}"}}}
}
}
GET cars/_search/template
{
"id": "test", // 指定调用脚本的id
"params": {"kw": "大众"}
}
DELETE _scripts/test // 删除脚本id为test的脚本

suggest search(completion suggest)建议搜索搜索建议,也可叫做自动完成,类似百度中搜索联想提示功能。ES实现suggest性能非常高,其构建的不是倒排索引也不是正排索引,是纯粹用于前缀搜索的一种特殊数据结构,且会全部放在内存中,所以suggest search进行前缀搜索提示性能是非常高。需要使用suggest时候,必须在定义index时为其mapping指定开启suggest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
PUT /movie
{
"mappings": {
"properties": {"title": {"type": "text","analyzer": "ik_max_word",
"fields": {"suggest": {"type": "completion","analyzer": "ik_max_word"}}
},
"content": {"type": "text","analyzer": "ik_max_word"}
}
}
}
PUT /movie/_doc/1
{
"title": "西游记电影系列",
"content": "西游记之月光宝盒将与2021年进行......"
}
PUT /movie/_doc/2
{
"title": "西游记文学系列",
"content": "某知名网络小说作家已经完成了大话西游同名小说的出版"
}
PUT /movie/_doc/3
{
"title": "西游记之大话西游手游",
"content": "网易游戏近日出品了大话西游经典IP的手游,正在火爆内测中"
}
GET /movie/_search
{
"suggest": {
"my-suggest": {
"prefix": "西游记",
"completion": {"field": "title.suggest"}
}
}
}

地理位置搜索

ES支持地理位置搜索聚合分析,可实现在指定区域内搜索数据搜索指定地点附近的数据聚合分析指定地点附近的数据等操作。ES中若使用地理位置搜索,必须提供一个特殊的字段类型geo_point,用于指定地理位置坐标点。

新增一个基于geo_point类型数据,可使用多种方式。多种类型描述geo_point类型字段时,在搜索数据时显示格式和录入格式是统一的任何数据描述geo_point类型字段,都适用地理位置搜索

数据范围要求纬度范围-90~90之间,经度范围-180~180之间,经纬度数据都是浮点数数字串,最大精度为小数点后7位latitude纬度longitude经度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PUT /hotel_app
{
"mappings": {
"properties": {
"pin": {"type": "geo_point"},
"name": {"type": "text","analyzer": "ik_max_word"}
}
}
}
PUT /hotel_app/_doc/1
{
"name": "七天连锁酒店",
"pin": {"lat": 40.12,"lon": -71.34}
}
PUT /hotel_app/_doc/2
{
"name": "维多利亚大酒店",
"pin": "40.99, -70.81"
}
PUT /hotel_app/_doc/3
{
"name": " 红树林宾馆",
"pin": [40,-73.81] // 基于数组:依次定义经度、纬度,不推荐使用
}

矩形范围搜索传入top_leftbottom_right坐标点是有固定要求的,top_left从西北向东南Bottom_right从东南向西北,且top_left纬度应大于bottom_righttop_left经度应小于bottom_right多边形范围搜索对传入若干点坐标顺序没有任何要求,只要传入若干地理位置坐标点,即可形成多边形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GET /hotel_app/_doc/_search // 矩形搜索
{
"query": {
"geo_bounding_box": {
"pin": {
"top_left": {"lat": 41.73,"lon": -74.1},
"bottom_right": {"lat": 40.01,"lon": -70.12}
}
}
}
}
GET /hotel_app/_doc/_search // 多边形搜索
{
"query": {
"geo_polygon": {
"pin": {
"points": [
{"lat": 40.73,"lon": -74.1},
{"lat": 40.01,"lon": -71.12},
{"lat": 50.56,"lon": -90.58}
]
}
}
}
}

Distance距离的单位,常用米m和千米km,建议使用filter来过滤geo_point数据,因为geo_point数据相关度评分计算比较耗时。使用query来搜索geo_point数据效率相对会慢一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /hotel_app/_doc/_search
{
"query": {
"bool": {
"filter": {
"geo_distance": {"distance": "200km","pin": {"lat": 40,"lon": -70}}
}
}
}
}
GET hotel_app/_search
{
"query": {
"geo_distance": {
"distance": "90km",
"pin": {"lat": 40.55,"lon": -71.12}
}
}
}

聚合统计某位置附近区域内的数据,unit是距离单位,常用单位有米m,千米km,英里mi,distance_type是统计算法:sloppy_arc默认算法arc最高精度plane最高效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /hotel_app/_doc/_search
{
"size": 0,
"aggs": {
"agg_by_pin": {
"geo_distance": {
"distance_type": "arc",
"field": "pin",
"origin": {"lat": 40,"lon": -70},
"unit": "mi",
"ranges": [ // 聚合统计分别距离某位置80英里,300英里,1000英里范围内的数据数量
{"to": 80},
{"from": 80,"to": 300},
{"from": 300,"to": 1000}
]
}
}
}
}