1、實現關系型數據庫中的三范式
三范式 --> 將每個數據實體拆分為一個獨立的數據表,同時使用主外鍵關聯關系將多個數據表關聯起來 --> 確保沒有任何冗余的數據。
冗余數據,就是說,將可能會進行搜索的條件和要搜索的數據,放在一個doc中
無冗余數據優點和缺點
優點:數據不冗余,維護方便
缺點:應用層join,如果關聯數據過多,導致查詢過大,性能很差
有冗余數據優點和缺點
優點:性能高,不需要執行兩次搜索
缺點:數據冗余,維護成本高 --> 每次如果你的username變化了,同時要更新user type和blog type
(1)、構造更多測試數據
PUT /website/users/3
{
"name": "黃藥師",
"email": "huangyaoshi@sina.com",
"birthday": "1970-10-24"
}
PUT /website/blogs/3
{
"title": "我是黃藥師",
"content": "我是黃藥師啊,各位同學們!!!",
"userInfo": {
"userId": 1,
"userName": "黃藥師"
}
}
PUT /website/users/2
{
"name": "花無缺",
"email": "huawuque@sina.com",
"birthday": "1980-02-02"
}
PUT /website/blogs/4
{
"title": "花無缺的身世揭秘",
"content": "大家好,我是花無缺,所以我的身世是。。。",
"userInfo": {
"userId": 2,
"userName": "花無缺"
}
}
(2)、對每個用戶發表的博客進行分組
GET /website/blogs/_search
{
"size": 0,
"aggs": {
"group_by_username": {
"terms": {
"field": "userInfo.username.keyword"
},
"aggs": {
"top_blogs": {
"top_hits": {
"_source": {
"include": "title"
},
"size": 5
}
}
}
}
}
}
2、對類似文件系統這種的有多層級關系的數據進行建模
(1)、path_hierarchy:對文本是文件目錄形式的進行目錄分詞
例:
PUT /fs
{
"settings": {
"analysis": {
"analyzer": {
"paths":{
"tokenizer":"path_hierarchy"
}
}
}
}
}
測試:
GET /fs/_analyze
{
"analyzer": "paths",
"text": "a/b/c"
}
結果:
{
"tokens": [
{
"token": "a",
"start_offset": 0,
"end_offset": 1,
"type": "word",
"position": 0
},
{
"token": "a/b",
"start_offset": 0,
"end_offset": 3,
"type": "word",
"position": 0
},
{
"token": "a/b/c",
"start_offset": 0,
"end_offset": 5,
"type": "word",
"position": 0
}
]
}
(2)實例操作
PUT /fs/_mapping/file
{
"properties": {
"name": {
"type": "keyword"
},
"path": {
"type": "keyword",
"fields": {
"tree": {
"type": "text",
"analyzer": "paths"
}
}
}
}
}
PUT /fs/file/1
{
"name": "README.txt",
"path": "/workspace/projects/helloworld",
"contents": "這是我的第一個elasticsearch程序"
}
PUT /fs/file/2
{
"name": "README.txt",
"path": "/workspace/projects/helloworld2",
"contents": "這是我的第一個elasticsearch程序"
}
文件搜索需求:查找一份,內容包括elasticsearch,在/workspace/projects/hellworld這個目錄下的文件,以下查詢,是以不分形式去查
GET /fs/file/_search
{
"query": {
"bool": {
"must": [
{"match": {
"contents": "elasticsearch"
}},
{"constant_score": {
"filter": {
"term": {
"path": "/workspace/projects/helloworld"
}
}
}}
]
}
}
}
搜索/workspace目錄下,內容包含elasticsearch的所有的文件,如果用上面的,path改成到workspace,是查不到數據的,使用path_hierarchy的分詞就可以查出來
GET /fs/file/_search
{
"query": {
"bool": {
"must": [
{"match": {
"contents": "elasticsearch"
}},
{"constant_score": {
"filter": {
"term": {
"path.tree": "/workspace"
}
}
}}
]
}
}
}
這樣兩個都可以查出來
3、全局鎖實現悲觀鎖并發控制,就是用_create語法
PUT /fs/lock/global/_create
{}
fs: 你要上鎖的那個index
lock: 就是你指定的一個對這個index上全局鎖的一個type
global: 就是你上的全局鎖對應的這個doc的id
_create:強制必須是創建,如果/fs/lock/global這個doc已經存在,那么創建失敗,報錯
另外一個線程同時嘗試上鎖會報錯
PUT /fs/lock/global/_create
{}
全局鎖的優點和缺點
優點:操作非常簡單,非常容易使用,成本低
缺點:你直接就把整個index給上鎖了,這個時候對index中所有的doc的操作,都會被block住,導致整個系統的并發能力很低
上鎖解鎖的操作不是頻繁,然后每次上鎖之后,執行的操作的耗時不會太長,用這種方式,方便
上了鎖之后,另一個還是可以進行操作,是不是有問題?
這種鎖只對create啟作用,修改新增還是沒有用,只能靠version來按制
4、document鎖實現悲觀鎖并發控制
(1)、document鎖,是用腳本進行上鎖
document鎖,顧名思義,每次就鎖你要操作的,你要執行增刪改的那些doc,doc鎖了,其他線程就不能對這些doc執行增刪改操作了
POST /fs/lock/1/_update
{
"upsert": { "process_id": 123 },
"script": "if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';"
"params": {
"process_id": 123
}
}
/fs/lock,是固定的,就是說fs下的lock type,專門用于進行上鎖
/fs/lock/id,比如1,id其實就是你要上鎖的那個doc的id,代表了某個doc數據對應的lock(也是一個doc)
params,里面有個process_id,是你的要執行增刪改操作的進程的唯一id,很重要,會在lock中,設置對對應的doc加鎖的進程的id,這樣其他進程過來的時候,才知道,這條數據已經被別人給鎖了
assert false,不是當前進程加鎖的話,則拋出異常
ctx.op='noop',不做任何修改
(2)document鎖的完整實驗過程
上鎖:
POST /fs/lock/1/_update
{
"upsert": {"process_id":321},
"script": {
"lang": "groovy",
"file": "judge-lock",
"params": {"paocess_id":321}
}
}
釋放鎖:
POST /fs/_refresh
好像沒啥用只是同時不能_update而已,其他線程也可以增冊改????????
共享鎖,就是用_update語法,只是上鎖數據不能一樣
5、基于nested object實現博客與評論嵌套關系
(1)、為什么需要nested object
冗余數據方式的來建模,其實用的就是object類型,我們這里又要引入一種新的object類型,nested object類型
PUT /website/blogs/6
{
"title": "花無缺發表的一篇帖子",
"content": "我是花無缺,大家要不要考慮一下投資房產和買股票的事情啊。。。",
"tags": [ "投資", "理財" ],
"comments": [
{
"name": "小魚兒",
"comment": "什么股票啊?推薦一下唄",
"age": 28,
"stars": 4,
"date": "2016-09-01"
},
{
"name": "黃藥師",
"comment": "我喜歡投資房產,風,險大收益也大",
"age": 31,
"stars": 5,
"date": "2016-10-22"
}
]
}
例,查出博客評論是黃藥師并且年齡是28的
GET /website/blogs/_search
{
"query": {
"bool": {
"must": [
{"match": {
"comments.name": "黃藥師"
}},
{
"match": {
"comments.age": 28
}
}
]
}
}
}
按理是不應該出來的
(2)、object類型數據結構的底層存儲。。。
{
"title": [ "花無缺", "發表", "一篇", "帖子" ],
"content": [ "我", "是", "花無缺", "大家", "要不要", "考慮", "一下", "投資", "房產", "買", "股票", "事情" ],
"tags": [ "投資", "理財" ],
"comments.name": [ "小魚兒", "黃藥師" ],
"comments.comment": [ "什么", "股票", "推薦", "我", "喜歡", "投資", "房產", "風險", "收益", "大" ],
"comments.age": [ 28, 31 ],
"comments.stars": [ 4, 5 ],
"comments.date": [ 2016-09-01, 2016-10-22 ]
}
object類型底層數據結構,會將一個json數組中的數據,進行扁平化,所以這樣一找,整個貼子都出來了
(3)、引入nested object類型,來解決object類型底層數據結構導致的問題
修改mapping,將comments的類型從object設置為nested
PUT /website
{
"mappings": {
"blogs": {
"properties": {
"comments": {
"type": "nested",
"properties": {
"name": { "type": "string" },
"comment": { "type": "string" },
"age": { "type": "short" },
"stars": { "type": "short" },
"date": { "type": "date" }
}
}
}
}
}
}
底部是單獨存儲
{
"comments.name": [ "小魚兒" ],
"comments.comment": [ "什么", "股票", "推薦" ],
"comments.age": [ 28 ],
"comments.stars": [ 4 ],
"comments.date": [ 2014-09-01 ]
}
{
"comments.name": [ "黃藥師" ],
"comments.comment": [ "我", "喜歡", "投資", "房產", "風險", "收益", "大" ],
"comments.age": [ 31 ],
"comments.stars": [ 5 ],
"comments.date": [ 2014-10-22 ]
}
{
"title": [ "花無缺", "發表", "一篇", "帖子" ],
"body": [ "我", "是", "花無缺", "大家", "要不要", "考慮", "一下", "投資", "房產", "買", "股票", "事情" ],
"tags": [ "投資", "理財" ]
}
GET /website/blogs/_search
{
"query": {
"bool": {
"must": [
{"match": {
"title": "花無缺"
}},
{
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{"match": {
"comments.name":"黃藥師"
}},
{
"match": {
"comments.age": 28
}
}
]
}
}
}
}
]
}
}
}
這樣就查不出來了
(4)、聚合數據分析的需求1:按照評論日期進行bucket劃分,然后拿到每個月的評論的評分的平均值
GET /website/blogs/_search
{
"size": 0,
"aggs": {
"comments_path": {
"nested": {
"path": "comments"
},
"aggs": {
"group_by_date": {
"date_histogram": {
"field": "comments.date",
"interval": "month",
"format": "yyyy-MM-dd"
},
"aggs": {
"avg_stars": {
"avg": {
"field": "comments.stars"
}
}
}
}
}
}
}
}
(5)、reverse_nested,可以在聚合后使用外層的buckets進行聚合
GET /website/blogs/_search
{
"size": 0,
"aggs": {
"comments_path": {
"nested": {
"path": "comments"
},
"aggs": {
"group_age": {
"histogram": {
"field": "comments.age",
"interval": 10
},
"aggs": {
"reverse_path": {
"reverse_nested": {},
"aggs": {
"group_tags": {
"terms": {
"field": "tags.keyword"
}
}
}
}
}
}
}
}
}
}
6、及父子關系數據建模
Object及nested object的建模,有個不好的地方,就是采取的是類似冗余數據的方式,將多個數據都放在一起了,維護成本就比較高
parent child建模方式,采取的是類似于關系型數據庫的三范式類的建模,多個實體都分割開來,每個實體之間都通過一些關聯方式,進行了父子關系的關聯,各種數據不需要都放在一起,父doc和子doc分別在進行更新的時候,都不會影響對方
(1)、案例背景:研發中心員工管理案例,一個IT公司有多個研發中心,每個研發中心有多個員工
建立關系映射:父子關系建模的核心,多個type之間有父子關系,用_parent指定父type
PUT /company
{
"mappings": {
"rd_center": {},
"employee": {
"_parent": {
"type": "rd_center"
}
}
}
}
POST /company/rd_center/_bulk
{ "index": { "_id": "1" }}
{ "name": "北京研發總部", "city": "北京", "country": "中國" }
{ "index": { "_id": "2" }}
{ "name": "上海研發中心", "city": "上海", "country": "中國" }
{ "index": { "_id": "3" }}
{ "name": "硅谷人工智能實驗室", "city": "硅谷", "country": "美國" }
shard路由的時候,id=1的rd_center doc,默認會根據id進行路由,到某一個shard
PUT /company/employee/1?parent=1
{
"name": "張三",
"birthday": "1970-10-24",
"hobby": "爬山"
}
維護父子關系的核心,parent=1,指定了這個數據的父doc的id
POST /company/employee/_bulk
{ "index": { "_id": 2, "parent": "1" }}
{ "name": "李四", "birthday": "1982-05-16", "hobby": "游泳" }
{ "index": { "_id": 3, "parent": "2" }}
{ "name": "王二", "birthday": "1979-04-01", "hobby": "爬山" }
{ "index": { "_id": 4, "parent": "3" }}
{ "name": "趙五", "birthday": "1987-05-11", "hobby": "騎馬" }
(2)驗證
搜索有1980年以后出生的員工的研發中心
GET /company/rd_center/_search
{
"query": {
"has_child": {
"type": "employee",
"query": {
"range": {
"birthday": {
"gte": "1980-01-01"
}
}
}
}
}
}
搜索有名叫張三的員工的研發中心
GET /company/rd_center/_search
{
"query": {
"has_child": {
"type": "employee",
"query": {
"match": {
"name":"張三"
}
}
}
}
}
搜索有至少2個以上員工的研發中心
GET /company/rd_center/_search
{
"query": {
"has_child": {
"type": "employee",
"min_children": 2,
"query": {
"match_all": {}
}
}
}
}
搜索在中國的研發中心的員工
GET /company/employee/_search
{
"query": {
"has_parent": {
"parent_type": "rd_center",
"query": {
"term": {
"country.keyword": {
"value": "中國"
}
}
}
}
}
}
統計每個國家的有多少個員工,有那些愛好
GET /company/rd_center/_search
{
"size": 0,
"aggs": {
"group_country": {
"terms": {
"field": "country.keyword"
},
"aggs": {
"group_employee": {
"children": {
"type": "employee"
},
"aggs": {
"group_hobby": {
"terms": {
"field": "hobby.keyword"
}
}
}
}
}
}
}
}
7、祖孫三層關系的數據建模,搜索
PUT /company
{
"mappings": {
"country": {},
"rd_center": {
"_parent": {
"type": "country"
}
},
"employee": {
"_parent": {
"type": "rd_center"
}
}
}
}
country -> rd_center -> employee,祖孫三層數據模型
POST /company/country/_bulk
{ "index": { "_id": "1" }}
{ "name": "中國" }
{ "index": { "_id": "2" }}
{ "name": "美國" }
POST /company/rd_center/_bulk
{ "index": { "_id": "1", "parent": "1" }}
{ "name": "北京研發總部" }
{ "index": { "_id": "2", "parent": "1" }}
{ "name": "上海研發中心" }
{ "index": { "_id": "3", "parent": "2" }}
{ "name": "硅谷人工智能實驗室" }
PUT /company/employee/1?parent=1&routing=1
{
"name": "張三",
"dob": "1970-10-24",
"hobby": "爬山"
}
routing參數的講解,必須跟grandparent相同,否則有問題
country,用的是自己的id去路由; rd_center,parent,用的是country的id去路由; employee,如果也是僅僅指定一個parent,那么用的是rd_center的id去路由,這就導致祖孫三層數據不會在一個shard上,孫子輩兒,要手動指定routing,指定為爺爺輩兒的數據的id
搜索有爬山愛好的員工所在的國家
GET /company/country/_search
{
"query": {
"has_child": {
"type": "rd_center",
"query": {
"has_child": {
"type": "employee",
"query": {
"match": {
"hobby": "爬山"
}
}
}
}
}
}
}