表與表之間的關聯基本上是所有業務系統都存在的,RDBMS通過外鍵實現,MongoDB通過嵌入式子文檔解決,那麼Elasticsearch怎麼解決這個問題呢?答案就是Parent-Child關聯(參考文檔)
業務場景
有一個廣告的分發系統,爲了更精準的做廣告的推送,除了自身積累的數據以外,還會從其他合作方通過數據交換(當然這些都是脫敏的數據)的方式獲取更多用戶行爲數據,例如從音樂網站獲取聽的音樂列表、從購物網站獲取最近的購物類別、從書評網站獲取最近瀏覽的圖書等等。這些來自於外部的數據,有以下幾個問題:
- 並不是每個用戶都有全部的數據,比如有些用戶只有書評和音樂信息,而有些用戶沒有任何外部信息
- 某一類外部的數據源可能包含幾個網站,比如音樂網站有A、B、C三個網站,它們提供的數據格式也並不一致
在進行廣告推送時,需要實時查詢一個用戶的信息完成精準推薦。比如實時查詢滿足下面條件的用戶:
- 最近一個月,
- 經常在早上、傍晚或者晚上連續一個小時的音樂;
- 購買過跑鞋、運動手錶等跑步裝備
- 且購買過或點評過運動類書籍
再繼續下面的(十分簡化)解決方案之前,可以先思考下
解決方案
這是典型應用大數據進行個性化精準推薦的應用場景,在省卻了數據清洗、評分等各種步驟以後,簡化爲一個查詢問題。分析可以發現數據問題的核心就是:無固定表結構,是典型的Schema-Free的NoSQL應用場景,第一個反應出來的就是MongoDB。
MongoDB
MongoDB用作以上的數據存儲,毫無疑問具有天然的優勢,可以將每個來源的數據都作爲user的一個子文檔存儲,查詢時也只是在這一個Collection上進行(可能有人會說這種方案太蠢了,的確是,不過也要看產品所處的階段)。當然這樣做的問題也顯而易見:
爲了查詢速度,索引是必須要創建的。可是因爲數據源不斷變化,那麼索引的維護就會變成一個災難。一旦忘記創建查詢,可能就會拖死整個系統。
下面當然就是主角上場了。
Elasticsearch
定調:
1. 由於字段是變化,因此必須使用動態Mapping(文檔)
2. 由於Parent-Child的關係需要創建索引(Create Index)時就確定,因此必須使用固定的Mapping(文檔)
我又檢查了上面兩條,的確是沒有說錯。
其實很簡單,在創建索引時,只需指定父子關係,無需指定其他未知字段。因爲要預先指定type的父子關係,所以就必須先確定type。這是用兩個type:user和user_action,那麼創建索引時的Mapping大致如下:
{
"mappings": {
"user": {},
"user_action": {
"_parent" : {
"type": "user"
}
}
}
}
我好像把文檔中的例子抄了一遍,不多實際情況的確是這樣。
那麼在添加文檔到索引中時,對於user就需要指定id,而user_action需要指定parent,例如:
es = Elasticsearch()
_id = 27
_user = {
'id': 27,
'name': 'Tigger Fei'
}
# 索引用戶文檔
es.index(index='user_index', doc_type='user', id=str(_id), body=_user)
# 索引用戶行爲文檔, type字段表示列表
# 音樂
_music = {
'type': 'music',
'user': 27,
'period': 'morning',
'duration': 78,
'category': 'running',
'time': '2017-01-29 12:30:00'
}
es.index(index='user_index', doc_type='user_action', parent=str(_id), body=_music)
# 圖書,
_book = {
'type': 'book,'
'user': 27,
'name': '我的第一個馬拉松',
'category': 'running',
'time': '2017-01-30 12:30:00'
}
es.index(index='user_index', doc_type='user_action', parent=str(_id), body=_book)
如何完成上面的查詢呢,如下:
POST user_index/user/_search
{
"query": {
"bool": {
"filter": [
{
"has_child": {
"type": "user_action",
"query": {
"bool": {
"filter": [
{"term": {"type": "music"}},
{"range": {"duration": {"gte": 60}}},
{"range": {
"time": {
"gte": "2017-01-07 00:00:00",
"format": "yyyy-MM-dd HH:mm:ss"
}
}},
{"term": {"category": "running"}},
{"terms": {"period": ["morning", "night"]}}
]
}
}
}
},
{
"has_child": {
"type": "user_action",
"query": {
"bool": {
"filter": [
{"range": {
"time": {
"gte": "2017-01-07 00:00:00",
"format": "yyyy-MM-dd HH:mm:ss"
}
}},
{"term": {"type": "book"}},
{"term": {"category": "running"}}
]
}
}
}
}
]
}
}
}
好了,這個簡單的解決方案就完了。