Elasticsearch中如何進行排序
背景
最近去兄弟部門的新自定義查詢項目組搬磚,項目使用Elasticsearch進行數據的檢索和查詢。每一個查詢頁面都需要根據選擇的字段進行排序,以為是一個比較簡單的需求,其實實現起來還是比較復雜的。這里進行一個總結,加深一下記憶。
前置知識
-
Elasticsearch是什么?
Elasticsearch 簡稱ES,是一個全文搜索引擎,可以實現類似百度搜索的功能。但她不僅僅能進行全文檢索,還可以實現PB級數據的近實時分析和精確查找,還可以作GIS數據庫,進行AI機器學習,功能非常強大。 -
ES的數據模型
ES中常用嵌套文檔和父子文檔兩種方法進行數據建模,多層父子文檔還可以形成祖孫文檔。但是父子文檔是一種不推薦的建模方式,這種方式有很多的局限性。如果傳統關系型數據庫的建模方法是通過“三范式”進行規范化,那么ES的建模方法就是反范式,進行反規范化。關系型數據庫的數據是表格模型,ES是JSON樹狀模型。

ES中排序的分類
根據建模方法的不同,ES中排序分為以下幾種,每種都有不同的排序寫法和一些限制:
- 嵌套文檔-根據主文檔字段排序
- 嵌套文檔-根據內嵌文檔字段排序
- 父子文檔-查父文檔然后根據子文檔排序
- 父子文檔-查子文檔然后根據父文檔排序
- 更復雜的情況,父子文檔里又嵌套了文檔,然后根據嵌套文檔字段進行排序。
下面分別對其中某幾種情況和中文字段的排序,進行測試說明(ES 5.5.x)。
測試數據准備
首先,設置索引類型字段映射
PUT /test_sort
{
"mappings": {
"zf":{
"properties": {
"id":{"type": "keyword"},
"name":{"type": "keyword"},
"age":{"type": "integer"},
"shgx":{"type": "nested"}
}
}
}
}
然后新建測試數據
PUT /test_sort/zf/1
{
"id":1,
"name":"張三",
"age":18,
"shgx":[{
"id":1,
"name":"老張",
"age":50,
"gx":"父親"
},{
"id":2,
"name":"張二",
"age":22,
"gx":"哥哥"
}]
}
PUT /test_sort/zf/2
{
"id":2,
"name":"李四",
"age":25,
"shgx":[{
"id":3,
"name":"李五",
"age":23,
"gx":"弟弟"
}]
}
- 嵌套文檔-根據主文檔字段排序
根據zf主文檔age字段倒敘排列,直接加sort子語句就可以
POST /test_sort/zf/_search
{
"query": {
"match_all": {
}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"_source": {"include": ["id","name","age"]}
}
結果:
"hits": [
{
"_index": "test_sort",
"_type": "zf",
"_id": "2",
"_score": null,
"_source": {
"name": "李四",
"id": 2,
"age": 25
},
"sort": [
25
]
},
{
"_index": "test_sort",
"_type": "zf",
"_id": "1",
"_score": null,
"_source": {
"name": "張三",
"id": 1,
"age": 18
},
"sort": [
18
]
}
]
- 嵌套文檔-根據內嵌文檔字段排序
根據age小於50歲的親屬排序,理論上李四應該排第一位,因為50歲以下的親屬,李五最大。 憑直覺先這樣寫:
POST /test_sort/zf/_search
{
"query": {
"nested": {
"path": "shgx",
"query": {
"range": {
"shgx.age": {
"lt": 50
}
}
}
}
},
"sort": [
{
"shgx.age": {
"nested_path": "shgx",
"order": "desc"
}
}
]
}
看結果:
"hits": [
{
"_index": "test_sort",
"_type": "zf",
"_id": "1",
"_score": null,
"_source": {
"id": 1,
"name": "張三",
"age": 18,
"shgx": [
{
"id": 1,
"name": "老張",
"age": 50,
"gx": "父親"
},
{
"id": 2,
"name": "張二",
"age": 22,
"gx": "哥哥"
}
]
},
"sort": [
50
]
},
{
"_index": "test_sort",
"_type": "zf",
"_id": "2",
"_score": null,
"_source": {
"id": 2,
"name": "李四",
"age": 25,
"shgx": [
{
"id": 3,
"name": "李五",
"age": 23,
"gx": "弟弟"
}
]
},
"sort": [
23
]
}
]
非常重要!結果是錯誤的。這是因為嵌套文檔是作為主文檔的一部分返回的,在主查詢中的嵌套文檔的過濾條件並不能把不符合條件的內部嵌套文檔過濾掉,返回的還是整個文檔(主文檔+完整的嵌套文檔)。以至於按嵌套文檔字段排序時,還是按照全部的嵌套文檔進行排序的。要正確的實現排序,就要把主查詢中有關嵌套文檔的查詢條件,在排序中再寫一遍。
正確的寫法:
POST /test_sort/zf/_search
{
"query": {
"nested": {
"path": "shgx",
"query": {
"range": {
"shgx.age": {
"lt": 50
}
}
}
}
},
"sort": [
{
"shgx.age": {
"nested_path": "shgx",
"order": "desc",
"nested_filter": {
"range": {
"shgx.age": {
"lt": 50
}
}
}
}
}
]
}
- 父子文檔-查父文檔-根據子文檔排序
構造測試數據,首先設置父子文檔的映射關系
PUT /test_sort_2
{
"mappings": {
"zf_parent":{
"properties": {
"id":{"type": "keyword"},
"name":{"type": "keyword"},
"age":{"type": "integer"}
}
},
"shgx":{
"_parent": {
"type": "zf_partent"
}
}
}
}
然后,添加數據。
PUT /test_sort_2/zf_parent/1
{
"id":1,
"name":"張三",
"age":18
}
PUT /test_sort_2/zf_parent/2
{
"id":2,
"name":"李四",
"age":25
}
PUT /test_sort_2/shgx/1?parent=1
{
"id":1,
"name":"老張",
"age":50,
"gx":"父親"
}
PUT /test_sort_2/shgx/2?parent=1
{
"id":2,
"name":"張二",
"age":22,
"gx":"哥哥"
}
PUT /test_sort_2/shgx/3?parent=2
{
"id":3,
"name":"李五",
"age":23,
"gx":"弟弟"
}
然后,根據age小於50歲的親屬排序,升序的話張三應該是第一位。
POST /test_sort_3/zf_parent/_search
{
"query": {
"has_child": {
"type": "shgx",
"query": {
"range": {
"age": {
"lt": 50
}
}
},
"inner_hits": {
"name": "ZfShgx",
"sort": [
{
"age": {
"order": "asc"
}
}
]
}
}
}
}
查看排序結果:
{
"_index": "test_sort_3",
"_type": "zf_parent",
"_id": "2",
"_score": 1,
"_source": {
"id": 2,
"name": "李四",
"age": 25
},
"inner_hits": {
"ZfShgx": {
"hits": {
"total": 1,
"max_score": null,
"hits": [
{
"_type": "shgx",
"_id": "3",
"_score": null,
"_routing": "2",
"_parent": "2",
"_source": {
"id": 3,
"name": "李五",
"age": 23,
"gx": "弟弟"
},
"sort": [
23
]
}
]
}
}
}
},
{
"_index": "test_sort_3",
"_type": "zf_parent",
"_id": "1",
"_score": 1,
"_source": {
"id": 1,
"name": "張三",
"age": 18
},
"inner_hits": {
"ZfShgx": {
"hits": {
"total": 1,
"max_score": null,
"hits": [
{
"_type": "shgx",
"_id": "2",
"_score": null,
"_routing": "1",
"_parent": "1",
"_source": {
"id": 2,
"name": "張二",
"age": 22,
"gx": "哥哥"
},
"sort": [
22
]
}
]
}
}
}
}
結果是錯誤的,李四在前,查看官方文檔的父子文檔時不能直接用子文檔排序父文檔,或者用父文檔排序子文檔。那有沒有解決辦法呢?有一個曲線救國的方案,使用function_score通過子文檔的評分來影響父文檔的順序,但是評分算法很難做到精准控制順序。
中文字符排序
在項目中發現,中文字符在ES中的順序和在關系型數據庫中的順序不一致。經查是因為ES是用的unicode的字節碼做排序的。即先對字符(包括漢字)轉換成byte[]數組,然后對字節數組進行排序。這種排序規則對ASIC碼(英文)是有效的,但對於中文等亞洲國家的字符不適用,怎么辦呢?有兩種解決辦法:
- 第一種,做拼音冗余。即在向ES同步數據時候,同步程序將漢字字段同時轉換成拼音,在ES里專門用於漢字排序。如:
#插入信息
POST /test/star/1
{
"xm": "劉德華",
"xm_pinyin": "liudehua"
}
POST /test/star/2
{
"xm": "張惠妹",
"xm_pinyin": "zhanghuimei"
}
# 查詢排序
POST /test/star/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"xm_pinyin": {
"order": "desc"
}
}
]
}
- 第二種,使用ICU分詞插件。使用插件提供的icu_collation_keyword 映射類型實現中文排序。
PUT test2
{
"mappings": {
"star": {
"properties": {
"xm": {
"type": "text",
"fields": {
"sort": {
"type": "icu_collation_keyword",
"index": false,
"language": "zh",
"country": "CN"
}
}
}
}
}
}
}
POST /test2/star/_search
{
"query": {
"match_all": { }
},
"sort": "xm.sort"
}
結論
- 嵌套文檔-根據主文檔字段排序時,可以使用sort語句直接排序,無限制。
- 嵌套文檔-根據嵌套文檔字段排序時,必須在sort子句里把所有嵌套相關的查詢條件,在sort里重新寫一邊,排序才正確。
- 父子文檔-查父文檔根據子文檔排序時,不能根據子文檔排序父文檔,反之亦然。
- 數據模型的復雜程度決定了排序的復雜程度,排序的復雜程度隨着模型的復雜程度成指數級增加。
- 中文字符可以通過做拼音冗余和使用ICU插件來實現排序。
