二十八、elasticSearch數據建模實戰

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": "爬山"
            }
          }
        }
      }
    }
  }
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容