索引:
第一部分 Query-Then-Fetch
第二部分 相关性算分
第三部分 sorting-doc-values-field
第四部分 分页与遍历
1.fromsize
2.scroll
3.search_after
第一部分 Query-Then-Fetch
Search在执行的时候实际上分为两个步骤运作(称为Query-Then-Fetch):
- Query
- Fetch
Query阶段
- node3在接收到用户的search请求后,会先进行Query阶段(此时是Coordinating Node的角色)
- node3在6个主副分片中随机选择3个分片,并发送search request
- 被选中的3个分片会分别执行查询并排序,返回from+size个文档ID和排序值
- node3整合3个分片返回的from+size个文档Id,根据排序值排序后选取from到from+size的文档Id
Fetch阶段
node3根据Query阶段获取的文档Id列表去对应的shard上获取文档的详情数据
- node3向相关的分片发送multi_get 请求
- 3个分片返回文档详细数据
- node3拼接返回的结果并返回给客户
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 |
DELETE test_search_relevance PUT test_search_relevance { "settings": { "index":{ "number_of_shards":1 } } } POST test_search_relevance/doc { "name":"hello" } POST test_search_relevance/doc { "name":"hello,world" } POST test_search_relevance/doc { "name":"hello,world!a beautiful world" } GET test_search_relevance/_search GET test_search_relevance GET test_search_relevance/_search { "explain": false, "query": { "match":{ "name":"hello" } } } GET test_search_relevance/_search?search_type=dfs_query_then_fetch { "query": { "match":{ "name":"hello" } } } |
第二部分 相关性算分
- 相关系算分在shard于shard间是相对独立的,也就意味着同一个term的IDF等值在不同shard上是不同的。文档的相关性算分和它所处的shard相关。
- 在文档数量不多时,会导致相关性算分严重不准的情况发生。
解决方法:
- 设置分片数为1个,从根本上解决问题,在文档数量不多的时候比如几千万,可以考虑该方案。
- 使用DFS Query-then-Fetch查询方式
DFS Query-then-Fetch是在拿到所有文档后再从新完整的计算一次相关性算分,耗费更多的cpu和内存,执行性能也比较低下,一般不建议使用。使用方式如下:
第三部分 排序
ES默认使用相关性算分来排序,如果想改变排序规则可以使用sort,
EXAMPLE 按照出生日期倒序排列:
给定多个排序条件,先按照birth倒序排列,如果birth相同按照相关性得分倒序排列,如果相关系得分也相同那么就按照文档id倒序排:
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 |
# sorting # score is null here GET test_search_index/_search { "query":{ "match": { "username": "alfred" } }, "sort":{ "birth":"desc" } } GET test_search_index/_search { "query":{ "match": { "username": "alfred" } }, "sort": [ { "birth": "desc" }, { "_score": "desc" }, { "_doc": "desc" } ] } PUT test_search_index/doc/0 { "username":"aaa" } DELETE test_search_index/doc/2 GET test_search_index GET test_search_index/_search { "sort": [ { "username": "asc" } ] } GET test_search_index/_search { "sort": [ { "username": "desc" }, { "_id": "desc" } ] } DELETE test_search_index/doc/5 PUT test_search_index/doc/5 { "username":"alfred junior zoo" } DELETE test_search_index/doc/5 GET test_search_index/_search { "sort":{ "username.keyword":"desc" } } |
按照字符串排序
比较特殊,因为es有text和keyword类型,针对text类型排序,如果直接使用sort字符串名会报错,要使用keyword
排序的过程是指是对字段原始内容排序的过程,这个过程中倒排索引无法发挥作用,需要用到正排索引,也就是通过文档Id和字段可以快速得到字段原始内容。
es对此提供了两种实现方式:
- fielddata默认禁用
- doc value默认禁用,除了text类型
fielddata的启用api:
- 此时字符串是按照分词后的term排序,往往结果很难符合预期
- 一般是在对分词做聚合分析的时候开启
- 可以随时开启或关闭
Doc value默认启用,就可以在创建的时候关闭,明确知道不需要按照这个字段来排序或聚合分析的时候可以关闭,如果后面要再开启doc value需要做reindex操作
可以通过该字段获取fielddata或者doc values中存储的内容
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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# fielddata PUT test_search_index/_mapping/doc { "properties": { "job":{ "type":"text", "fielddata": true } } } # error when enable fielddata in other type PUT test_search_index/_mapping/doc { "properties": { "username":{ "type":"text", "fielddata": false }, "age":{ "type":"long", "fielddata": true } } } GET test_search_index/ # doc values DELETE test_doc_values PUT test_doc_values GET test_doc_values PUT test_doc_values/_mapping/doc { "properties": { "username": { "type": "keyword", "doc_values": true }, "hobby": { "type": "keyword" } } } PUT test_doc_values1/ { "mappings": { "doc": { "properties": { "username": { "type": "keyword", "doc_values": false } } } } } PUT test_doc_values/doc/1 { "username":"alfred", "hobby":"basketball" } GET test_doc_values/_search { "sort":"username" } GET test_doc_values/_search { "sort":"hobby" } # can be used to get original field value for not stored field PUT test_search_index/_mapping/doc { "properties": { "username":{ "type":"text", "fielddata": true } } } GET test_search_index/_search { "docvalue_fields": [ "username", "username.keyword", "age" ] } PUT test_search_index/_mapping/doc { "properties": { "username":{ "type":"text", "fielddata": true } } } |
第四部分 分页与遍历
使用场景:
1.from/size
这是最常用的分页方案:
- from 指明开始位置
- size 指明获取总数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# pagination GET test_search_index/_search { "from":4, "size":2 } #total_page=(total+page_size-1)/page_size GET test_search_index/_search { "from":9998, "size":2 } |
深度分页问题(这就是百度或者google所有不能把页面全部获取出来的原因):
在数据分片存储的情况下如何获取前1000个文档
- 获取9900-1000的文档时,会在每个分片上都先获取1000个文档,然后再由Coordinating Node聚合所有分片的结果后再排序选取前1000个文档(因为数据分散存储了,Coordinating Node是没有全局的概念的)
- 页数越多,处理文档就越多,占用内存越多,耗时越长。尽量避免深度分页,es通过index.max_result_window限定最多到10000条数据
2.scroll
遍历文档集的api,以快照的方式来避免深度分页的问题
- 不能用来做实时搜索,因为数据不是实时的
- 尽量不要使用复杂的sort条件,使用_doc最高效
- 使用稍嫌复杂
Scroll过程:
第一步 发起一个scroll search:
es在收到该请求后会根据查询条件穿件文档Id合集的快照
5m就是快照的有效期,5分钟,超过5分钟就失效了,size是返回的文档数。
第二步 调用scroll search 的 api,获取文档的集合,如下图所示:
不断的迭代调用直到返回hits.hits数组为空时停止
注意:过多的scroll调用会占用大量的内存,可以通过clear api删除过多的快照
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 |
# scroll GET test_search_index/_search?scroll=5m { "size":1 } GET _search/scroll GET test_search_index/_search # new doc can not be searched PUT test_search_index/doc/10 { "username":"doc10" } DELETE test_search_index/doc/10 POST _search/scroll { "scroll" : "5m", "scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAABswWX3FLSTZFOF9URFdqWHlvX3gtYmhtdw==" } DELETE _search/scroll/_all |
3.search_after
避免深度分页的性能问题,提供实时的下一页文档获取功能
- 缺点是不能使用from参数,即不能指定页数
- 只能下一页,不能上一页
- 使用简单,性能极高
第一步 正常搜索,但是要制定sort的值,并保证值唯一
第二步 使用上一步最后一个文档的sort值进行查询
执行原理 -> 如何避免深度分页问题?
通过唯一排序值定位将每次要处理的文档数都控制在size内
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# search_after GET test_search_index/_search { "size":1, "sort":{ "age":"desc", "_id":"desc" } } GET test_search_index/_search { "size":1, "search_after":[23,"4"], "sort":{ "age":"desc", "_id":"desc" } } |