前言
通過分析Gitlab的站內(nèi)搜索設(shè)計(jì),借鑒其設(shè)計(jì)經(jīng)驗(yàn),來改進(jìn)自己的站內(nèi)搜索方案,包括領(lǐng)域?qū)ο髣澐?,索引設(shè)計(jì),權(quán)限控制設(shè)計(jì)。
這可能是國內(nèi)第一篇詳細(xì)解剖Gitlab站內(nèi)搜索設(shè)計(jì)實(shí)現(xiàn)的文章。
基礎(chǔ)背景
Gitlab的免費(fèi)版本采用的是Postgresql的FTS(full text search)進(jìn)行搜索。
Gitlab的白金版本才支持基于Elasticsearch的高級(jí)搜索(可以申請(qǐng)30天的試用license體驗(yàn))
Gitlab的領(lǐng)域?qū)ο箨P(guān)系
Gitlab的索引設(shè)計(jì)
gitlab的ES索引結(jié)構(gòu)
gitlab會(huì)在ES內(nèi)部建立如下索引
- gitlab-production
- gitlab-production-commits
- gitlab-production-issues
- gitlab-production-merge_requests
- gitlab-production-migrations
- gitlab-production-notes
- gitlab-production-users
gitlab-production是Project的索引
整個(gè)索引的mapping采用大寬表的設(shè)計(jì),不同領(lǐng)域?qū)ο蟮淖侄味紩?huì)平鋪在一起,具有相同的document-type??(ES6/Lucene7已經(jīng)解決大寬表下sparse field所導(dǎo)致的空間浪費(fèi)問題?Space Saving Improvements in Elasticsearch 6.0 | Elastic Blog)
(該索引有?project, blob, milestone, snippet, wiki_blob 領(lǐng)域?qū)ο螅╞lob是Binary Large Object的意思,這里特指Code)
它通過join的方式,將project和它的子對(duì)象建立父子關(guān)系,以便于做到權(quán)限控制
? ? ? ? "join_field" : {
? ? ? ? ? "type" : "join",
? ? ? ? ? "eager_global_ordinals" : true,
? ? ? ? ? "relations" : {
? ? ? ? ? ? "project" : [
? ? ? ? ? ? ? "note",
? ? ? ? ? ? ? "blob",
? ? ? ? ? ? ? "issue",
? ? ? ? ? ? ? "milestone",
? ? ? ? ? ? ? "wiki_blob",
? ? ? ? ? ? ? "commit",
? ? ? ? ? ? ? "merge_request"
? ? ? ? ? ? ]
? ? ? ? ? }
? ? ? ? }
關(guān)于索引的設(shè)計(jì)取舍,參考2019年gitlab的分享。
?https://download.csdn.net/download/yyw794/88646899
注意:
根據(jù)Gitlab的官方描述,他們是更傾向不同領(lǐng)域?qū)ο蟛捎貌煌乃饕O(shè)計(jì),因?yàn)樗阉魉俣雀?,重建索引更快等(也是ES官方推薦的方式)。
Gitlab采用不同領(lǐng)域?qū)ο蠓旁?個(gè)索引里,僅僅是為了解決權(quán)限控制的問題。
子索引分析
索引字段一般分為3部分:
- 對(duì)象id信息(用于唯一定位)
- 對(duì)象基本信息(用于搜索和顯示)
- 對(duì)象父信息(用于權(quán)限控制)
以issue的索引gitlab-production-issues 為例:
對(duì)象id信息部分
issue對(duì)象包含子對(duì)象task(work_item)
索引采用的是平鋪的設(shè)計(jì)層次。
每個(gè)對(duì)象包含id和iid(子對(duì)象id)和type(對(duì)象類型)3個(gè)值,來唯一定義這個(gè)對(duì)象。
對(duì)象基本信息
(略)
對(duì)象父信息
每個(gè)子索引的對(duì)象都有3個(gè)字段進(jìn)行權(quán)限控制:
- visibility_level(project的權(quán)限,父父對(duì)象權(quán)限)
- xxx_access_level(父對(duì)象的權(quán)限 )
- 父對(duì)象id
備注:
子對(duì)象不一定都有id,例如commit對(duì)象,就采用唯一的sha作為id(_id=${project_id}_${sha})
merge_request索引雖然有iid,但是沒有發(fā)現(xiàn)其有子對(duì)象
merge_request有hashed_root_namespace_id字段
數(shù)據(jù)同步
gitlab的insert, update, delete操作會(huì)推送到redis的zset中(作為queue使用)。
redis的zset是有序集合,可以有效防止減少重復(fù)消息,提高ES的寫效率。
sidekiq(ruby領(lǐng)域的異步框架)周期采用bulk api批量寫ES,提高ES的寫性能和保障ES集群的整體性能。(參考:Keeping Elasticsearch in Sync | Elastic Blog)
搜索分析
子對(duì)象的搜索
{
"from": 0,
"size": 20,
"timeout": "30s",
"query": {
"bool": {
"must": [
{
"simple_query_string": {
"query": "領(lǐng)域驅(qū)動(dòng)",
"fields": [
"title^2.0",
"description^1.0"
],
"flags": -1,
"default_operator": "and",
"lenient": true,
"analyze_wildcard": false,
"auto_generate_synonyms_phrase_query": true,
"fuzzy_prefix_length": 0,
"fuzzy_max_expansions": 50,
"fuzzy_transpositions": true,
"boost": 1.0,
"_name": "milestone:match:search_terms"
}
}
],
"filter": [
{
"term": {
"type": {
"value": "milestone",
"boost": 1.0,
"_name": "doc:is_a:milestone"
}
}
},
{
"has_parent": {
"query": {
"bool": {
"should": [
{
"bool": {
"filter": [
{
"term": {
"visibility_level": {
"value": 0,
"boost": 1.0,
"_name": "milestone:related:project:any"
}
}
},
{
"terms": {
"issues_access_level": [
20,
10
],
"boost": 1.0,
"_name": "milestone:related:project:issues:enabled_or_private"
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
},
{
"bool": {
"filter": [
{
"term": {
"visibility_level": {
"value": 0,
"boost": 1.0,
"_name": "milestone:related:project:any"
}
}
},
{
"terms": {
"merge_requests_access_level": [
20,
10
],
"boost": 1.0,
"_name": "milestone:related:project:merge_requests:enabled_or_private"
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
},
{
"bool": {
"filter": [
{
"term": {
"visibility_level": {
"value": 10,
"boost": 1.0,
"_name": "milestone:related:project:visibility:10"
}
}
},
{
"terms": {
"issues_access_level": [
20,
10
],
"boost": 1.0,
"_name": "milestone:related:project:visibility:10:issues:access_level:enabled_or_private"
}
}
],
"adjust_pure_negative": true,
"boost": 1.0,
"_name": "milestone:related:project:visibility:10:issues:access_level"
}
},
{
"bool": {
"filter": [
{
"term": {
"visibility_level": {
"value": 10,
"boost": 1.0,
"_name": "milestone:related:project:visibility:10"
}
}
},
{
"terms": {
"merge_requests_access_level": [
20,
10
],
"boost": 1.0,
"_name": "milestone:related:project:visibility:10:merge_requests:access_level:enabled_or_private"
}
}
],
"adjust_pure_negative": true,
"boost": 1.0,
"_name": "milestone:related:project:visibility:10:merge_requests:access_level"
}
},
{
"bool": {
"filter": [
{
"term": {
"visibility_level": {
"value": 20,
"boost": 1.0,
"_name": "milestone:related:project:visibility:20"
}
}
},
{
"terms": {
"issues_access_level": [
20,
10
],
"boost": 1.0,
"_name": "milestone:related:project:visibility:20:issues:access_level:enabled_or_private"
}
}
],
"adjust_pure_negative": true,
"boost": 1.0,
"_name": "milestone:related:project:visibility:20:issues:access_level"
}
},
{
"bool": {
"filter": [
{
"term": {
"visibility_level": {
"value": 20,
"boost": 1.0,
"_name": "milestone:related:project:visibility:20"
}
}
},
{
"terms": {
"merge_requests_access_level": [
20,
10
],
"boost": 1.0,
"_name": "milestone:related:project:visibility:20:merge_requests:access_level:enabled_or_private"
}
}
],
"adjust_pure_negative": true,
"boost": 1.0,
"_name": "milestone:related:project:visibility:20:merge_requests:access_level"
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
},
"parent_type": "project",
"score": false,
"ignore_unmapped": false,
"boost": 1.0,
"_name": "milestone:related:project"
}
}
],
"adjust_pure_negative": true,
"boost": 1.0
}
},
"highlight": {
"pre_tags": [
"gitlabelasticsearch→"
],
"post_tags": [
"←gitlabelasticsearch"
],
"number_of_fragments": 0,
"fields": {
"title": {},
"description": {}
}
}
}
?
搜索分為2部分:
- 搜索關(guān)鍵字邏輯(采用simple_query_string,AND邏輯,開啟模糊查詢)
- 過濾邏輯(類型為子對(duì)象類型(如milestone),且project(父對(duì)象)在權(quán)限范圍內(nèi))
子對(duì)象issue搜索示例
? ? ? ? "_source" : {
? ? ? ? ? "id" : 2,
? ? ? ? ? "iid" : 1,
? ? ? ? ? "title" : "搜索支持相似問",
? ? ? ? ? "description" : "打開FAQ搜索時(shí),需要加入相似問字段的搜索",
? ? ? ? ? "created_at" : "2023-04-14T08:28:38.119Z",
? ? ? ? ? "updated_at" : "2023-04-14T08:28:38.119Z",
? ? ? ? ? "state" : "opened",
? ? ? ? ? "project_id" : 3,
? ? ? ? ? "author_id" : 3,
? ? ? ? ? "confidential" : false,
? ? ? ? ? "schema_version" : 2302,
? ? ? ? ? "assignee_id" : [
? ? ? ? ? ? 3
? ? ? ? ? ],
? ? ? ? ? "hidden" : false,
? ? ? ? ? "visibility_level" : 0,
? ? ? ? ? "issues_access_level" : 10,
? ? ? ? ? "upvotes" : 1,
? ? ? ? ? "namespace_ancestry_ids" : "8-",
? ? ? ? ? "label_ids" : [
? ? ? ? ? ? "2"
? ? ? ? ? ],
? ? ? ? ? "type" : "issue"
? ? ? ? }
凡是涉及其他對(duì)象的字段,全部使用引用對(duì)象的id (例如,標(biāo)簽label_ids,存儲(chǔ)的是標(biāo)簽的id,而不是具體的值)
但這樣有一個(gè)問題,導(dǎo)致了無法做到標(biāo)簽搜索。(例如,給一個(gè)issue添加知識(shí)庫的標(biāo)簽,搜索 知識(shí)庫,并不能搜到 這個(gè)issue)
iid為issue的子對(duì)象。
issue本身的iid為1,添加其他子對(duì)象,iid依次遞增的分配。(例如,新建task,task的iid為2,task的type為work_item)
并不是所有的issue屬性都會(huì)同步到es中,例如,issue的評(píng)論,雖然包含文字,但是沒有同步到es索引中。(評(píng)論的重要性低,加入搜索范圍,可能會(huì)加大搜索結(jié)果噪音)
visibility_level 代表project的可見性
issues_access_level 代表issue的可訪問性
權(quán)限過濾邏輯為(或的關(guān)系):
- 查詢有權(quán)限的project 且 issue的權(quán)限為可被訪問
- 項(xiàng)目為登錄用戶可見 且 issue可被所有人訪問
- 項(xiàng)目可被所有人可見 且 issue可被所有人訪問
(作者注:可以優(yōu)化為? issue可被所有人訪問(20) 或 (issue需要有權(quán)限才能訪問(10) 且 具有該項(xiàng)目的權(quán)限)
權(quán)限控制設(shè)計(jì)
權(quán)限分為
- private(0)
- internal(10)
- public(20)
后面的數(shù)字代表ES里的對(duì)應(yīng)權(quán)限的值(不存字符串,而是存數(shù)字,且數(shù)字采用了10的間隔,猜測(cè)為了考慮未來在中間插入的拓展)
project和它子對(duì)象都有自己獨(dú)立的權(quán)限值。
?
project在gitlab-production中的結(jié)構(gòu)為:
? ? ? ?
"_source" : {
? ? ? ? ? "id" : 3,
? ? ? ? ? "name" : "eim-search",
? ? ? ? ? "path" : "eim-search",
? ? ? ? ? "description" : null,
? ? ? ? ? "namespace_id" : 8,
? ? ? ? ? "created_at" : "2023-04-14T02:53:42.747Z",
? ? ? ? ? "updated_at" : "2023-04-14T02:53:44.471Z",
? ? ? ? ? "archived" : false,
? ? ? ? ? "visibility_level" : 0,
? ? ? ? ? "last_activity_at" : "2023-04-14T02:53:42.747Z",
? ? ? ? ? "name_with_namespace" : "platform / eim-search",
? ? ? ? ? "path_with_namespace" : "platform/eim-search",
? ? ? ? ? "join_field" : "project",
? ? ? ? ? "type" : "project",
? ? ? ? ? "schema_version" : 2301,
? ? ? ? ? "traversal_ids" : "8-p3-",
? ? ? ? ? "issues_access_level" : 20,
? ? ? ? ? "merge_requests_access_level" : 20,
? ? ? ? ? "snippets_access_level" : 20,
? ? ? ? ? "wiki_access_level" : 20,
? ? ? ? ? "repository_access_level" : 20
? ? ? ? }
namespace_id 就是group的id(namespace=group)
traversal_ids 通過namespace_id和project_id 拼接而成
join_field 和 type 雖然都被賦予了相同的值,但是作用不一樣。
join_field 是用于has parent query,在這個(gè)query里,充當(dāng)parent_type的值
type只是本身的對(duì)象屬性
user在gitlab-production-users的結(jié)構(gòu)為:
? ? ? ? "_source" : {
? ? ? ? ? "id" : 3,
? ? ? ? ? "username" : "yanyongwen",
? ? ? ? ? "email" : "yyw794@126.com",
? ? ? ? ? "public_email" : null,
? ? ? ? ? "name" : "yyw794",
? ? ? ? ? "created_at" : "2023-04-14T02:34:14.040Z",
? ? ? ? ? "updated_at" : "2023-04-14T02:53:04.822Z",
? ? ? ? ? "admin" : false,
? ? ? ? ? "state" : "active",
? ? ? ? ? "organization" : "",
? ? ? ? ? "timezone" : null,
? ? ? ? ? "external" : false,
? ? ? ? ? "in_forbidden_state" : false,
? ? ? ? ? "status" : null,
? ? ? ? ? "status_emoji" : null,
? ? ? ? ? "busy" : false,
? ? ? ? ? "namespace_ancestry_ids" : [
? ? ? ? ? ? "2-p2-",
? ? ? ? ? ? "8-"
? ? ? ? ? ],
? ? ? ? ? "schema_version" : 2210,
? ? ? ? ? "type" : "user"
? ? ? ? }
用戶的權(quán)限通過namespace_ancestry_ids進(jìn)行存儲(chǔ)
通過namespace-project的id拼接方便進(jìn)行權(quán)限控制。
基于父子數(shù)據(jù)建模的權(quán)限控制設(shè)計(jì)
父子數(shù)據(jù)建模使用了ES的Has Parent Query
為什么Gitlab不使用ES官方推薦的大寬表數(shù)據(jù)建模?
gitlab的搜索對(duì)象存在父子關(guān)系,且子對(duì)象也需要被獨(dú)立搜索出來,因此,ES內(nèi)部的子對(duì)象是獨(dú)立對(duì)象存儲(chǔ)的。
每個(gè)project下面有多種類型的子對(duì)象,每種子對(duì)象都可能數(shù)量眾多。
缺點(diǎn):寫入操作變得繁瑣
- 如果采用大寬表設(shè)計(jì),當(dāng)project的權(quán)限改變時(shí),該project的全部子對(duì)象的project權(quán)限屬性都要同步更新,涉及面很廣。
- project每新增一個(gè)子對(duì)象時(shí),需要查詢project的屬性后,再填入子對(duì)象中。
采用父子關(guān)系的數(shù)據(jù)建模
寫入過程,需要額外增加route屬性,保證父子對(duì)象(同一個(gè)project)都在同一個(gè)shard中。
由于是project級(jí)別的路由,因此_route值為"project-${project_id}"
父子關(guān)系的建模在什么數(shù)據(jù)量下的性能變得不可接受?(gitlab的搜索不是一個(gè)高頻操作,每個(gè)project下子對(duì)象總數(shù)也不會(huì)太高)
父子關(guān)系的數(shù)據(jù)建模適合:
- 整體的父對(duì)象不多,但是父對(duì)象內(nèi)部的子對(duì)象較多的場(chǎng)景
- 搜索性能要求不高
索引版本管理
具有schema_version字段,F(xiàn)ormat is YYMM (如2303),當(dāng)schema改變時(shí),這個(gè)值需要變更。
ES ID設(shè)計(jì)
ES ID設(shè)計(jì)的核心是唯一性。(_Id 字段)
ES的文檔_id
兩種設(shè)計(jì)思路:
- ProjectID_項(xiàng)目?jī)?nèi)唯一識(shí)別字符? (用于對(duì)象的唯一識(shí)別是項(xiàng)目?jī)?nèi)唯一的)
- 對(duì)象類型_對(duì)象ID (用于對(duì)象的ID是全局唯一的)
ProjectID_項(xiàng)目?jī)?nèi)唯一識(shí)別字符
blob的ID設(shè)計(jì)為:
${project_id}_${blob_path}
(wiki_blob采用和blob一樣的設(shè)計(jì))
commit:
${project_id_${sha}
對(duì)象類型_對(duì)象ID
project的ID設(shè)計(jì)為:
project_${project_id}
milestone的ID設(shè)計(jì)為:
milestone_${milestone_id}
snippet的ID設(shè)計(jì)為:
snippet_${snippet_id}
source內(nèi)部的ID仍然使用對(duì)象自己的業(yè)務(wù)ID(_source內(nèi)部的id和ES的_id的不一樣,如何做到的?TODO:)
子對(duì)象ID
通過例如repository的id名為rid (有較好的可讀性)
rid: repository id
一個(gè)project下面會(huì)有2個(gè)repository
1個(gè)為代碼倉庫,id和project一致
1個(gè)為wiki倉庫,id為wiki_${project_id}
oid: blob id / wiki_blob id
附錄
通過查看ES的日志,來獲取gitlab實(shí)際的搜索query。
基于admin的query json
https://download.csdn.net/download/yyw794/88646878
gitlab在es中碰過的坑:
Lessons from our journey to enable global code search with Elasticsearch on GitLab.com
Update: The challenge of enabling Elasticsearch on GitLab.com
Update: Elasticsearch lessons learnt for Advanced Global Search 2020-04-28
減少索引體積
由于ES的delete是軟刪除,gitlab最初采用forcemerge來強(qiáng)制硬刪除(merge segment的過程會(huì)最終硬刪除文檔),但是forcemerge是一個(gè)阻塞操作,會(huì)嚴(yán)重影響ES的整體性能,因此只能放棄forcemerge。文章來源:http://www.zghlxwxcb.cn/news/detail-775067.html
不同領(lǐng)域?qū)ο笤谝粋€(gè)大索引 還是不同領(lǐng)域?qū)ο笤诓煌饕膯栴}。文章來源地址http://www.zghlxwxcb.cn/news/detail-775067.html
- 不存在空間占用區(qū)別。ES6引入了sparse field功能,使得1個(gè)大索引的稀疏字段并不會(huì)浪費(fèi)額外的空間
- 獨(dú)立索引具有重建索引時(shí)的速度優(yōu)勢(shì) (Elasticsearch: return to using a separate index per document type (#3217) · Issues · GitLab.org / GitLab · GitLab)
到了這里,關(guān)于【獨(dú)家深度】Gitlab基于Elasticsearch的站內(nèi)搜索設(shè)計(jì)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!