0%

ElasticSearch 的使用

一、安装 ElasticSearch

在 dokcer 中安装 elasticsearch

1. 下载 elasticsearch 和 kibana

1
2
docker pull elasticsearch:7.6.2
docker pull kibana:7.6.2

2. 配置

1
2
3
4
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
echo "http.host: 0.0.0.0" >/mydata/elasticsearch/config/elasticsearch.yml
chmod -R 770 /mydata/elasticsearch/

3. 启动 elasticsearch

1
2
3
4
5
6
7
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.6.2

设置开机启动 elasticsearch

1
docker update elasticsearch --restart=always

4. 启动 kibana

1
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://172.17.0.3:9200 -p 5601:5601 -d kibana:7.6.2

设置开机启动 kibana

1
docker update kibana  --restart=always

5. 测试

查看 elasticsearch 版本信息:http://172.17.0.3:9200/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "0adeb7852e00",
"cluster_name": "elasticsearch",
"cluster_uuid": "9gglpP0HTfyOTRAaSe2rIg",
"version": {
"number": "7.6.2",
"build_flavor": "default",
"build_type": "docker",
"build_hash": "ef48eb35cf30adf4db14086e8aabd07ef6fb113f",
"build_date": "2020-03-26T06:34:37.794943Z",
"build_snapshot": false,
"lucene_version": "8.4.0",
"minimum_wire_compatibility_version": "6.8.0",
"minimum_index_compatibility_version": "6.0.0-beta1"
},
"tagline": "You Know, for Search"
}

访问 Kibana:http://172.17.0.3:5601/app/kibana

二、初步检索

CAT

  • GET /_cat/nodes: 查看所有节点,* 表示集群中的主节点
1
2
3
10.15.199.49 74 87  1 0.00 0.01 0.05 cdhilmrstw * es-cn-tl32bf5ty00412mma-a50144d1-0001
10.15.199.48 32 90 14 0.01 0.04 0.05 cdhilmrstw - es-cn-tl32bf5ty00412mma-a50144d1-0002
10.15.199.50 57 91 0 0.00 0.02 0.05 cdhilmrstw - es-cn-tl32bf5ty00412mma-a50144d1-0003
  • GET /_cat/health: 查看 es 健康状况,green 表示健康值正常
1
1588332616 11:30:16 elasticsearch green 1 1 3 3 0 0 0 0 - 100.0%
  • GET/_cat/master: 查看主节点
1
vfpgxbusTC6-W3C2Np31EQ 127.0.0.1 127.0.0.1 0adeb7852e00
  • GET /_cat/indices?v: 查看所有索引,等价于 mysql 数据库的 show databases
1
2
3
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .kibana_task_manager_1 KWLtjcKRRuaV9so_v15WYg 1 0 2 0 39.8kb 39.8kb
green open .kibana_1 PqK_LdUYRpWMy4fK0tMSPw 1 0 7 0 31.2kb 31.2kb

三、索引 index 和 映射 mapping

创建索引

只创建索引,映射关系 es 自己猜出来(当允许自动创建索引时):

1
2
3
4
5
6
7
PUT /index-name
{
"settings": {
"number_of_shards": 3, // 分片数
"number_of_replicas": 1 // 副本数
}
}

结果:

1
2
3
4
5
6
{
"acknowledged" : true,
"shards_acknowledged" : true, // 分片操作成功
"index" : "index_name"
}
// 注意:创建索引库的分片数默认 1 片,在 7.0.0 之前默认 5 片

创建索引同时添加 1 个文档(当允许自动创建索引时):

1
2
3
4
PUT index-name/_doc/1
{
"name": "zhangsan"
}

查和删

  • 查看索引:GET /index-name/
  • 删除索引:DELETE /index-name
  • 只查看 mapping 信息:GET bank/_mapping

创建映射

Maping 是用来提前定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。

比如:使用 mapping 来定义:

  • 哪些字符串属性应该被看做全文本属性(full text fields);
  • 哪些属性包含数字,日期或地理位置;
  • 文档中的所有属性是否都能被索引(all 配置);
  • 日期的格式;
  • 自定义映射规则来执行动态添加属性;

创建一个索引并同时定义映射信息:

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
PUT /my_index
{
"settings": {
"number_of_shards": 5, // 分片数
"number_of_replicas": 1 // 备份数
},
"mappings": {
"properties":{
"name":{
"type":"text", // 类型
"analyzer":"ik_max_word", // 指定分词器
"index":true, // 是否索引,默认就为 true
"store":false // 是否需要额外存储,一般不需要
},
"author":{
"type":"keyword"
},
"count":{
"type":"long"
},
"on-sale":{
"type":"date", // 指定时间类型的格式化方式
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"descr":{
"type":"text",
"analyzer":"ik_max_word",
"search_analyzer": "ik_smart"
}
}
},
"aliases": { // 可以自定义一个别名
"default_index": {}
}
}

添加字段

添加新的字段映射:

1
2
3
4
5
6
7
8
9
PUT /my_index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false // 不被索引,默认为 true
}
}
}

更新映射

对于已经存在的字段映射,我们不能更新。更新必须创建新的索引,进行数据迁移。

重新绑定别名 alias

1
2
3
4
5
6
7
POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index_v1", "alias": "my_index" }},
{ "add": { "index": "my_index_v2", "alias": "my_index" }}
]
}

数据迁移 reindex

先创建 new_index 的正确映射。然后使用如下方式进行数据迁移。

1
2
3
4
5
6
7
8
9
POST _reindex
{
"source":{
"index": "my_index"
},
"dest":{
"index": "new_index"
}
}

四、对文档的增删改查操作

新增和修改:

  • PUT
1
2
3
4
5
// 必须指定 id,接口幂等性。没有就创建,有就修改(覆盖式修改:全部改成提交的数据,之前有的字段,但这次没提交,会删除)
PUT /index_name/_doc/2
{
"name": "zhangsan2"
}
  • POST
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 不指定 id,是新增,id 会随机生成
POST /index_name/_doc
{
"name": "zhangsan",
"age": 18
}
// 指定 id,没有就创建,有就修改(局部)。多次提交,version 号会一直增加
POST /index_name/_doc/1
{
"name": "zhangsan",
"age": 18
}
// update 的更新会比对源数据,如多次提交相同数据,"result" 值会为 "noop",version 号只增加一次
POST /index_name/_update/1
{
"doc": {
"name": "zhangsan123"
}
}

查询一条:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /index_name/_doc/1

{
"_index" : "index_name",
"_type" : "_doc",
"_id" : "1",
"_version" : 3, // 版本号
"_seq_no" : 6, // 并发控制字段,每次更新都会 +1,用来做乐观锁
"_primary_term" : 1, // 同上,主分片重新分配,如重启,就会变化
"found" : true,
"_source" : {
"name" : "zhangsan"
}
}

乐观锁:通过 if_seq_no=6&if_primary_term=1,当序列号匹配的时候,才进行修改,否则不修改。

1
2
3
4
PUT /index_name/_doc/1?if_seq_no=6&if_primary_term=1
{
"name": "zhangsan1"
}

删除:

1
2
3
4
5
6
7
8
9
10
11
12
// 删除一个文档:不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)。
DELETE /index_name/_doc/1

// 批量删:根据匹配的条件删除多条数据
POST /index_name/_delete_by_query
{
"query": {
"match": {
"name": "zhangsan2"
}
}
}

五、bulk 批量导入

批量操作,当某一条执行失败时,其他的数据仍然能够接着执行,也就是说彼此之间是独立的。

bulk api 按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因失败,它将继续处理它后面剩余的动作。

当 bulk api 返回时,它将提供每个动作的状态(与发送的顺序相同),所以您可以检查某一个指定的动作是否失败了。

语法格式:

1
2
3
4
5
{action:{metadata}}\n
{request body}\n

{action:{metadata}}\n
{request body}\n

示例,批量新增两条数据:

1
2
3
4
5
POST /index_name/_bulk
{"index":{"_id":"1"}}
{"name":"John Doe"}
{"index":{"_id":"2"}}
{"name":"John Doe"}

示例:复杂一些的:对整个索引执行批量操作

1
2
3
4
5
6
7
8
9
10
11
12
POST /_bulk
# 删一个
{"delete":{"_index":"index_name","_id":"1"}}
# 创建一个
{"create":{"_index":"index_name","_id":"2"}}
{"title":"my first blog"}
# 创建一个
{"index":{"_index":"index_name_2", "_id":"123"}}
{"title":"my second blog"}
# 更新
{"update":{"_index":"index_name_2","_id":"123"}}
{"doc":{"title":"my updated blog post"}}}

六、检索 query

测试数据样本:https://gitee.com/xlh_blog/common_content/blob/master/es测试数据.json

批量导入

1
2
3
4
POST bank/account/_bulk
...
...
...

Query DSL 查询语法

Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL(domain-specific language 领域特定语言)。这个被称为 Query DSL。该查询语言非常全面,并且刚开始的时候感觉有点复杂,真正学好它的方法是从一些基础的示例开始的。

1
2
3
4
5
6
7
8
9
10
11
12
# 第一种:参数在 url 中
GET /bank/_search?q=*&sort=account_number:asc

# 第二种(建议):参数在 body 中
GET /bank/_search
{
"query": {"match_all": {} },
"sort": [
{"account_number": "asc"},
{"balance":"desc"}
]
}

分页查询 from size

使用 fromsize 可以指定查询

1
2
3
4
5
6
7
8
9
10
GET /bank/_search
{
"query": {"match_all": {} },
"sort": [
{"account_number": "asc"},
{"balance":"desc"}
],
"from": 20,
"size": 10
}

返回部分字段 source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET bank/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 5,
"sort": [
{
"account_number": {
"order": "desc"
}
}
],
"_source": ["balance","firstname"]
}

精确检索 term & terms

term 和 terms的异同:查询机制是一样,都不会将指定的查询关键字进行分词,直接去分词库中匹配,找到相应文档内容。

  • 区别:
    • term 一次匹配一个值,比如 where name = "张三"
    • terms 一次可以同时匹配多个值,比如where name = "张三" and name= "李四"

注意:应避免使用 termtext 类型进行检索,应使用 match

全文检索字段用 match, 其他非 text 字段匹配,用 term

1
2
3
4
5
6
7
8
9
// term
GET bank/_search
{
"query": {
"term": {
"balance": 32838
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// terms
GET bank/_search
{
"query": {
"terms": {
"lastname.keyword": [
"Bates",
"Duke"
]
}
}
}

查询 match

match 查询属于高层查询,他会根据你查询的字段类型不一样,采用不同的查询方式。

  • 如果查询的是日期或者是数值的话,他会将你基于的字符串查询内容转换为日期或者数值对待。
  • 如果查询的内容是一个不能被分词的内容(keyword) , match 查询不会对你指定的查询关键字进行分词。
  • 如果查询的内容是一个可以被分词的内容 (text) , match 会将你指定的查询内容根据一定的方式去分词,去分词库中匹配指定的内容。

match 查询,实际底层就是多个 term 查询,将多个 term 查询的结果给你封装到了一起。

1
2
3
4
5
6
7
8
GET bank/_search
{
"query": {
"match": {
"account_number": 20 // 如果目标字段是数值,就是精确查询。(写成 "20" 也可以)
}
}
}
1
2
3
4
5
6
7
GET bank/_search
{
"query": {
"match": {
"address": "kings place" // 如果目标字段是字符,会先分词,再做匹配查询,一个词匹配就算,最后通过评分排名
}
}

查询全部 match_all

查询全部内容,不指定任何查询条件。

1
2
3
4
5
6
POST /bank/_search
{
"query": {
"match_all": {}
}
}

布尔 match

基于一个 Field 匹配的内容,采用 and 或者 or 的方式连接

1
2
3
4
5
6
7
8
9
10
11
POST /bank/_search
{
"query": {
"match": {
"address": {
"query": "490 Madison" // address 中包含 490 和 Madison 的
, "operator": "and" // and 或 or
}
}
}
}

多字段匹配(or)multi_match

multi_match 针对多个 field 进行检索,多个 field 对应一个 text

1
2
3
4
5
6
7
8
9
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill", // 查询关键字,如果是词会先分词
"fields": ["address", "city"] // 在这些字段中有一个匹配就行,or
}
}
}

短语匹配 match_phrase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET bank/_search
{
"query": {
"match_phrase": {
"address": "kings place" // 不会分词,匹配这个整句。
}
}
}

// 上下的区别:kings place stree 上面的匹配,下面的不匹配

GET bank/_search
{
"query": {
"match": {
"address.keyword": "kings place" // 精确匹配,一模一样才行
}
}
}

多条件查询 bool

mustmust_notshould

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 bank/_search
{
"query": {
"bool": { // 指有多个条件
"must": [ // 必须都满足,and
{"match": {
"gender": "F"
}},
{"match": {
"address": "Madison"
}}
],
"must_not": [ // 必须都不满足,not
{"match": {
"age": 38
}}
],
"should": [ // 最好满足,只对得分有影响,不能代表 or
{"match": {
"lastname": "Lynch"
}}
]
}
}
}

查询 filter

  • query: 根据你的查询条件,去计算文档的匹配度得到一个分数,并且根据分数进行排序,不会做缓存的。
  • filter: 根据你的查询条件去查询文档,不去计算分数,而且 filter 会对经常被过滤的数据进行缓存。

如果对结果没有得分排序需求,使用 filter 的效率更高

ES系列之利用filter让你的查询效率飞起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /party-huya-gift/_search
{
"query": { // 虽然用了 query,但不是 query 查询,过滤出的结果得分都为 0
"bool": {
"filter": [
{
"term": {
"gift_name": "虎粮"
}
},
{
"term": {
"gift_total": "100"
}
}
]
}
}
}

filter 也可以对 query 查询的结果过滤,和 mustshould 一起使用,不影响得分

  • filter 对得分没有任何影响,而 mustshould 会影响得分
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
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"gender": "F"
}},
{"match": {
"address": "Madison"
}}
],
"must_not": [
{"match": {
"age": 38
}}
],
"should": [
{"match": {
"lastname": "Lynch"
}}
],
"filter": {
"range": {
"age": {
"gte": 23,
"lte": 35
}
}
}
}
}
}

对上面的例子分析下:

  • query 参数表示整个语句是处于 query context 中
  • boolmatch 语句被用在 query context 中,也就是说它们会计算每个文档的匹配度(_score)
  • filter 参数则表示这个子查询处于 filter context 中
  • filter 语句中的 termrange 语句用在 filter context 中,它们只起到过滤的作用,并不会计算文档的得分。

直接批量查询 ids

对 id 直接进行批量查询

1
2
3
4
5
6
7
8
POST /bank/_search
{
"query": {
"ids": {
"values": [1,2,3]
}
}
}

范围查询 range

只针对数值类型,对某一个 Field 进行大于或者小于的范围指定。

1
2
3
4
5
6
7
8
9
10
11
POST /party-huya-gift/_search
{
"query": {
"range": {
"price_total": {
"gte": 2000, // 售价大于等于 2000,小于等于 3000 的礼物
"lte": 3000
}
}
}
}

前缀匹配 prefix

匹配前缀,支持在 keyword 类型的字段中查找

Ps: prefix, fuzzy, wildcard 和 regexp 查询效率相对比较低,要求效率比较高时,避免去使用

1
2
3
4
5
6
7
8
9
10
POST /bank/_search
{
"query": {
"prefix": {
"address": {
"value": "16"
}
}
}
}

查询(模糊) fuzzy

模糊查询,我们输入字符的大概,ЕS 就可以去根据输入的内容大概去匹配一下结果。

可以有个别的错别字,结果不是稳定的

1
2
3
4
5
6
7
8
9
10
11
12
POST /bank/_search
{
"query": {
"fuzzy": {
"name": {
"value": "盒马先生" // 也能搜到”盒马鲜生“
// 如果是 ”河马先生“,差的太多,就搜不到了
, "prefix_length": 3 // 数据量大的话,可以要求前几个字符是完全匹配的
}
}
}
}

通配查询 wildcard

和 MySQL 中的 like 是一个套路,可以在查询时,在字符串中指定通配符 * 和占位符 ?

1
2
3
4
5
6
7
8
9
10
POST /party-huya-gift/_search
{
"query": {
"wildcard": {
"send_name": {
"value": "我是一颗???" // ? 是占位符,“???" 可以匹配任意 3 个,比如“我是一颗小虎牙”,但是不能匹配“我是一颗糖牙”
}
}
}
}

正则查询 regexp

通过你编写的正则表达式去匹配内容。

1
2
3
4
5
6
7
8
POST /party-huya-gift/_search
{
"query": {
"regexp": {
"room_id": "1314[0-9]+" // 编写正则,匹配 room_id 字段为 1314 开头的数字
}
}
}

深分页 Scroll

We no longer recommend using the scroll API for deep pagination. If you need to preserve the index state while paging through more than 10,000 hits, use the search_after parameter with a point in time (PIT).
我们不再建议使用 scroll API 进行深度分页。如果在分页超过 10000 次点击时需要保留索引状态,请使用带有时间点(PIT)的 search_after 参数。
search_after 请看 http://arganzheng.life/deep-pagination-in-elasticsearch.html

ES 对 from + size 有限制的,from + size 之和 不能大于 1W,超过后效率会十分低下。而 Scroll 可以被用来检索大量的结果(甚至所有的结果)。

原理:

from + size 在 ES 查询数据的方式:

  • 第一步先将用户指定的关键进行分词。
  • 第二步将词汇去分词库中进行检索,得到多个文档的 id。
  • 第三步去各个分片中去拉取指定的数据。耗时较长。
  • 第四步将数据根据 score 进行排序。耗时较长。
  • 第五步根据 from 的值,将查询到的数据舍弃一部分。
  • 第六步返回结果。

scroll + size 在 ES 查询数据的方式:

  • 第一步现将用户指定的关键进行分词。
  • 第二步将词汇去分词库中进行检索,得到多个文档的 id。
  • 第三步将文档的 id 存放在一个 Es 的上下文中(也就是内存中)。
  • 第四步根据你指定的 size 的个数去 Es 中检索指定个数的数据,拿完数据的文档 id,会从上下文中移除。
  • 第五步如果需要下一页数据,直接去 Es 的上下文中,找后续内容。
  • 第六步循环第四步和第五步

注意:Scroll 的查询方式,不适合做实时的查询,因为是在内存中循环查询

为了使用 scroll 查询,需要指定 scroll 参数,这个参数会告诉 Elasticsearch 将 search context 保存多久。例如:?scroll=1m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /party-huya-gift/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 2, // 只看 2 条
"sort": [ // 如有需要,也可以排序
{
"price_total": {
"order": "desc"
}
}
]
}

根据 scroll_id,继续查下一个数据

1
2
3
4
5
6
POST /_search/scroll
{
// 根据第一步得到的 scorll_id 去指定
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoAxZPSU5JTlpUdFRGcVV2OThOWjhSR1Z3AAAAAAALmugWdTROSEJ6enRUV2E5VTd6Nkg2djdHURZSUFJRSzc4R1FQQ2tpMC1mdnFobVVnAAAAAAANVNwWNEtzM3VSZDBTbHEtM0R4STdVM2YwURZSUFJRSzc4R1FQQ2tpMC1mdnFobVVnAAAAAAANVN0WNEtzM3VSZDBTbHEtM0R4STdVM2YwUQ==",
"scroll": "1s" // scorll 信息的生存时间
}

删除 scrol1 在 ES 上下文中的数据

1
DELETE /_search/scroll/FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoAxZPSU5JTlpUdFRGcVV2OThOWjhSR1Z3AAAAAAALmugWdTROSEJ6enRUV2E5VTd6Nkg2djdHURZSUFJRSzc4R1FQQ2tpMC1mdnFobVVnAAAAAAANVNwWNEtzM3VSZDBTbHEtM0R4STdVM2YwURZSUFJRSzc4R1FQQ2tpMC1mdnFobVVnAAAAAAANVN0WNEtzM3VSZDBTbHEtM0R4STdVM2YwUQ==

delete-by-query 根据term, match等查询方式去删除大量的文档 Ps:如果你需要删除的内容,是index下的大部分数据,推荐创建一个全新的index,将保留的文档内容,添加 到全新的索引

匹配删除 delete-by-query

根据 termmatchrange 等查询方式去删除大量的文档。

Ps: 如果你需要删除的内容,是 index 下的大部分数据,推荐创建一个全新的 index,将保留的文档内容,添加到全新的索引。

1
2
3
4
5
6
7
8
9
10
11
POST /party-huya-gift/_delete_by_query
{
"query": {
"range": {
"gift_total": {
"gte": 202,
"lte": 203
}
}
}
}

调整得分的查询 boosting

boosting 查询可以帮助我们去影响查询后的 score。

  • positive: 只有匹配上 positive 的查询的内容,才会被放到返回的结果集中。
  • negative: 如果匹配上 positive 并且也匹配上了 negative,就可以降低这样文档的 score。
  • negative_boost: 指定系数,必须小于 1.0(经过测试,也可以比 1 高)

关于查询时,分数是如何计算的,有几个参考值:

  • 搜索的关键字在文档中出现的频次越高,分数就越高
  • 指定的文档内容越短,分数就越高
  • 我们在搜索时,指定的关键字也会被分词,这个被分词的内容,被分词库匹配的个数越多,分数越高
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /party-huya-gift/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"send_name": "阿纠纠" // 名字是“阿纠纠”的
}
},
"negative": {
"match": {
"gift_name": "虎粮" // 礼物名字匹配虎粮的
}
},
"negative_boost": 0.2 // 得分系数乘 0.2,排名会降低
}
}
}

高亮查询

高亮查询就是你用户输入的关键字,以一定的特殊样式展示给用户,让用户知道为什么这个结果被检索出来。

高亮展示的数据,本身就是文档中的一个 Field,单独将 Field 以 highlight 的形式返回给你。

ES 提供了一个 highlight 属性,和 query 同级别的。

  • fragment_size: 指定高亮数据展示多少个字符回来。
  • pre_tags: 指定前缀标签,举个栗子 <font color="red">
  • post_tags: 指定后缀标签,举个栗子 </font>
  • fields: 指定哪个 Field 以高亮形式返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /party-huya-danmaku/_search
{
"query": {
"match": {
"content": "这也"
}
},
"highlight": {
"fields": {
"content": {}
},
"fragment_size": 3,
"pre_tags": "<font color='red'>",
"post_tags": "</font>"
}
}

七、聚合 Aggregation

聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL Group by 和 SQL 聚合函数。在 elasticsearch 中,执行搜索返回 hits(命中结果),并且同时返回聚合结果,把以响应中的所有 hits(命中结果)分隔开的能力。这是非常强大且有效的,你可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用一次简洁和简化的 API 来避免网络往返。

  • terms: 看有多少种可能性
  • avg: 平均值

示例:搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,

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
GET bank/_search
{
"query": {
"match": {
"address": "mill" // address 中包含 mill 的
}
},
"aggs": { // 做聚合的固定语法
"ageAgg": { // 自定义一个名字,在结果中返回
"terms": { // 指定聚合类型为:terms,看有多少种可能性
"field": "age", // 指定字段
"size": 10 // 如果有 100 中可能,只要 10 个
}
},
"ageAvg": {
"avg": { // 聚合类型:avg 平均数
"field": "age" // 平均年龄
}
},
"balanceAvg":{
"avg": {
"field": "balance" // 平均工资
}
}
},
"size": 0 // 不要原始数据,只要统计结果
}

查询结果:

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
{
"took" : 7,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4, // 一共搜索到 4 条记录
"relation" : "eq"
},
"max_score" : 5.4032025,
"hits" : []
},
"aggregations" : { // 主要看这里,聚合后的信息
"ageAgg" : { // 年龄分布
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 38, // 38 岁的有 2 个
"doc_count" : 2
},
{
"key" : 28,
"doc_count" : 1
},
{
"key" : 32,
"doc_count" : 1
}
]
},
"ageAvg" : { // 平均年龄
"value" : 34.0
},
"balanceAvg" : { // 平均工资
"value" : 25208.0
}
}
}

再复杂一点,子聚合示例:先按照年龄聚合,并且算出每个年龄段中的平均薪资

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": { // 使用 terms 聚合类型,看年龄分布
"field": "age",
"size": 3
},
"aggs": { // 注意:子聚合,和 terms 同级
"balanceAvg": {
"avg": {
"field": "balance" // 在每个年龄中计算平均薪资
}
}
}
}
},
"size": 0
}

查看结果:

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
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1000,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"ageAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 820,
"buckets" : [
{
"key" : 31, // 61 个 31 岁的
"doc_count" : 61,
"balanceAvg" : { // 他们的平均薪资
"value" : 28312.918032786885
}
},
{
"key" : 39, // 60 个 39 岁的
"doc_count" : 60,
"balanceAvg" : { // 他们的平均薪资
"value" : 25269.583333333332
}
},
{
"key" : 26,
"doc_count" : 59,
"balanceAvg" : {
"value" : 23194.813559322032
}
}
]
}
}
}

子聚合内多个条件示例:查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资,以及这个年龄段的总体平均薪资

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
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": { // 年龄分布
"terms": {
"field": "age",
"size": 2
},
"aggs": { // 在每个年龄分布内,做子聚合
"genderAgg": { // 子聚合 1:性别分布
"terms": {
"field": "gender.keyword",
"size": 2
},
"aggs": { // 子聚合 1 的子聚合
"gander_balanceAvg": { // 每种性别的平均工资
"avg": {
"field": "balance"
}
}
}
},
"balanceAvg":{ // 子聚合 2:平均工资
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}

查看结果:

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
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1000,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"ageAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 879,
"buckets" : [
{
"key" : 31, // 61 个 31 岁的
"doc_count" : 61,
"genderAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "M", // 其中女的 35 人
"doc_count" : 35,
"gander_balanceAvg" : { // 这 35 个女人的平均工资
"value" : 29565.628571428573
}
},
{
"key" : "F", // 而男的 26 人
"doc_count" : 26,
"gander_balanceAvg" : {
"value" : 26626.576923076922
}
}
]
},
"balanceAvg" : { // 这 61 个 31 岁的人的平均工资
"value" : 28312.918032786885
}
},
{
"key" : 39,
"doc_count" : 60,
"genderAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "F",
"doc_count" : 38,
"gander_balanceAvg" : {
"value" : 26348.684210526317
}
},
{
"key" : "M",
"doc_count" : 22,
"gander_balanceAvg" : {
"value" : 23405.68181818182
}
}
]
},
"balanceAvg" : {
"value" : 25269.583333333332
}
}
]
}
}
}

去重计数查询

去重计数,即 Cardinality, 第一步先将返回的文档中的一个指定的 field 进行去重,统计一共有多少条

1
2
3
4
5
6
7
8
9
10
POST /party-huya-gift/_search
{
"aggs": {
"gift_name_count": {
"cardinality": {
"field": "gift_name"
}
}
}
}

结果有 29 种:

1
2
3
4
5
"aggregations" : {
"gift_name_count" : {
"value" : 29
}
}

范围统计 range

统计一定范围内出现的文档个数,比如,针对某一个 Field 的值在 0~100100~200200~300 之间文档出现的个数分别是多少。

范围统计有可以针对普通的数值(range),针对时间类型(date_range),针对 ip 类型(ip-range)都可以做相应的统计。

1. 数值统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST /party-huya-gift/_search
{
"aggs": {
"price_agg": {
"range": {
"field": "price_total",
"ranges": [
{
"from": 0,
"to": 500
},
{
"from": 500,
"to": 1000
},
{
"from": 1000,
"to": 10000
}
]
}
}
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"aggregations" : {
"price_agg" : {
"buckets" : [
{
"key" : "0.0-500.0",
"from" : 0.0,
"to" : 500.0,
"doc_count" : 936388
},
{
"key" : "500.0-1000.0",
"from" : 500.0,
"to" : 1000.0,
"doc_count" : 5663
},
{
"key" : "1000.0-10000.0",
"from" : 1000.0,
"to" : 10000.0,
"doc_count" : 410
}
]
}
}

时间范围统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /party-huya-gift/_search
{
"aggs": {
"create_time_agg": {
"date_range": {
"field": "create_time",
"ranges": [
{
"from": "now-2d",
"to": "now-1d"
},
{
"from": "now-1d",
"to": "now"
}
]
}
}
},
"size": 0
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"aggregations" : {
"create_time_agg" : {
"buckets" : [
{
"key" : "1633068433.307-1633154833.307",
"from" : 1.633068433307E12,
"from_as_string" : "1633068433.307",
"to" : 1.633154833307E12,
"to_as_string" : "1633154833.307",
"doc_count" : 168548
},
{
"key" : "1633154833.307-1633241233.307",
"from" : 1.633154833307E12,
"from_as_string" : "1633154833.307",
"to" : 1.633241233307E12,
"to_as_string" : "1633241233.307",
"doc_count" : 192609
}
]
}
}

统计聚合查询

使用 extended_stats,他可以帮你查询指定 Field 的最大值,最小值,平均值,平方和。。。

1
2
3
4
5
6
7
8
9
10
11
POST /party-huya-gift/_search
{
"aggs": {
"gift_count_agg": {
"extended_stats": {
"field": "gift_total"
}
}
},
"size": 0
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"aggregations" : {
"gift_count_agg" : {
"count" : 949932,
"min" : 1.0,
"max" : 2500.0,
"avg" : 8.680017095960553,
"sum" : 8245426.0,
"sum_of_squares" : 4.59571672E8,
"variance" : 408.4515874362822,
"variance_population" : 408.4515874362822,
"variance_sampling" : 408.4520174165517,
"std_deviation" : 20.210185240028903,
"std_deviation_population" : 20.210185240028903,
"std_deviation_sampling" : 20.21019587773834,
"std_deviation_bounds" : {
"upper" : 49.100387576018356,
"lower" : -31.740353384097254,
"upper_population" : 49.100387576018356,
"lower_population" : -31.740353384097254,
"upper_sampling" : 49.10040885143723,
"lower_sampling" : -31.740374659516128
}
}
}

八、分词

ik 分词

默认的分词器对中文支持不好

1
2
3
4
5
POST _analyze
{
"analyzer": "standard",
"text": "这是一句话"
}

结果:

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
{
"tokens" : [
{
"token" : "这",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "一",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "句",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "话",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 4
}
]
}

使用 ik 分词器,对中文友好:

1
2
3
4
5
POST _analyze
{
"analyzer": "ik_smart",
"text":"这是一句话"
}

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"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
}
]
}
  • ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民大会堂,人民大会,大会堂”,会穷尽各种可能的组合;
  • ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国,人民大会堂”。

ik 分词器的使用

存储时,使用 ik_max_word,尽量多的和其他组成倒排索引。而搜索时,使用 ik_smart,避免过度分词后搜索到太多无用数据

1
2
3
4
5
6
7
8
9
10
11
12
PUT /my_index 
{
"mappings": {
"properties": {
"text": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}
}

自定义词库

修改 /usr/share/elasticsearch/plugins/ik/config 中的 IKAnalyzer.cfg.xml /usr/share/elasticsearch/plugins/ik/config

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置 </comment>
<!-- 用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!-- 用户可以在这里配置自己的扩展停止词字典 -->
<entry key="ext_stopwords"></entry>
<!-- 用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://#/es/fenci.txt</entry>
<!-- 用户可以在这里配置远程扩展停止词字典 -->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

修改完成后,需要重启 elasticsearch 容器,否则修改不生效。

更新完成后,es 只会对于新增的数据用更新分词。历史数据是不会重新分词的。如果想要历史数据重新分词,需要执行:

1
POST my_index/_update_by_query?conflicts=proceed