【独家深度】Gitlab基于Elasticsearch的站内搜索设计

前言

通过分析Gitlab的站内搜索设计,借鉴其设计经验,来改进自己的站内搜索方案,包括领域对象划分,索引设计,权限控制设计。

这可能是国内第一篇详细解剖Gitlab站内搜索设计实现的文章。

基础背景

Gitlab的免费版本采用的是Postgresql的FTS(full text search)进行搜索。

Gitlab的白金版本才支持基于Elasticsearch的高级搜索(可以申请30天的试用license体验)

Gitlab的领域对象关系

Gitlab的索引设计

gitlab的ES索引结构

gitlab会在ES内部建立如下索引

  • gitlab-production
  • gitlab-production-commits
  • gitlab-production-issues
  • gitlab-production-merge_requests
  • gitlab-production-migrations
  • gitlab-production-notes
  • gitlab-production-users

gitlab-production是Project的索引

整个索引的mapping采用大宽表的设计,不同领域对象的字段都会平铺在一起,具有相同的document-type  (ES6/Lucene7已经解决大宽表下sparse field所导致的空间浪费问题 Space Saving Improvements in Elasticsearch 6.0 | Elastic Blog

(该索引有 project, blob, milestone, snippet, wiki_blob 领域对象)(blob是Binary Large Object的意思,这里特指Code)

它通过join的方式,将project和它的子对象建立父子关系,以便于做到权限控制

        "join_field" : {
          "type" : "join",
          "eager_global_ordinals" : true,
          "relations" : {
            "project" : [
              "note",
              "blob",
              "issue",
              "milestone",
              "wiki_blob",
              "commit",
              "merge_request"
            ]
          }
        }

关于索引的设计取舍,参考2019年gitlab的分享。

https://download.csdn.net/download/yyw794/88646899

注意:

根据Gitlab的官方描述,他们是更倾向不同领域对象采用不同的索引设计,因为搜索速度更快,重建索引更快等(也是ES官方推荐的方式)。

Gitlab采用不同领域对象放在1个索引里,仅仅是为了解决权限控制的问题。

子索引分析

索引字段一般分为3部分:

  • 对象id信息(用于唯一定位)
  • 对象基本信息(用于搜索和显示)
  • 对象父信息(用于权限控制)

以issue的索引gitlab-production-issues 为例:

对象id信息部分

issue对象包含子对象task(work_item)

索引采用的是平铺的设计层次。

每个对象包含id和iid(子对象id)和type(对象类型)3个值,来唯一定义这个对象。

对象基本信息

(略)

对象父信息

每个子索引的对象都有3个字段进行权限控制:

  • visibility_level(project的权限,父父对象权限)
  • xxx_access_level(父对象的权限 )
  • 父对象id

备注:

子对象不一定都有id,例如commit对象,就采用唯一的sha作为id(_id=${project_id}_${sha})

merge_request索引虽然有iid,但是没有发现其有子对象

merge_request有hashed_root_namespace_id字段

数据同步

gitlab的insert, update, delete操作会推送到redis的zset中(作为queue使用)。

redis的zset是有序集合,可以有效防止减少重复消息,提高ES的写效率。

sidekiq(ruby领域的异步框架)周期采用bulk api批量写ES,提高ES的写性能和保障ES集群的整体性能。(参考:Keeping Elasticsearch in Sync | Elastic Blog

搜索分析

子对象的搜索
{
    "from": 0,
    "size": 20,
    "timeout": "30s",
    "query": {
        "bool": {
            "must": [
                {
                    "simple_query_string": {
                        "query": "领域驱动",
                        "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部分:

  1. 搜索关键字逻辑(采用simple_query_string,AND逻辑,开启模糊查询)
  2. 过滤逻辑(类型为子对象类型(如milestone),且project(父对象)在权限范围内)

子对象issue搜索示例
        "_source" : {
          "id" : 2,
          "iid" : 1,
          "title" : "搜索支持相似问",
          "description" : "打开FAQ搜索时,需要加入相似问字段的搜索",
          "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"
        }

凡是涉及其他对象的字段,全部使用引用对象的id (例如,标签label_ids,存储的是标签的id,而不是具体的值)

但这样有一个问题,导致了无法做到标签搜索。(例如,给一个issue添加知识库的标签,搜索 知识库,并不能搜到 这个issue)

iid为issue的子对象。

issue本身的iid为1,添加其他子对象,iid依次递增的分配。(例如,新建task,task的iid为2,task的type为work_item)

并不是所有的issue属性都会同步到es中,例如,issue的评论,虽然包含文字,但是没有同步到es索引中。(评论的重要性低,加入搜索范围,可能会加大搜索结果噪音)

visibility_level 代表project的可见性

issues_access_level 代表issue的可访问性

权限过滤逻辑为(或的关系):

  • 查询有权限的project 且 issue的权限为可被访问
  • 项目为登录用户可见 且 issue可被所有人访问
  • 项目可被所有人可见 且 issue可被所有人访问

(作者注:可以优化为  issue可被所有人访问(20) 或 (issue需要有权限才能访问(10) 且 具有该项目的权限)

权限控制设计

权限分为

  • private(0)
  • internal(10)
  • public(20)

后面的数字代表ES里的对应权限的值(不存字符串,而是存数字,且数字采用了10的间隔,猜测为了考虑未来在中间插入的拓展)

project和它子对象都有自己独立的权限值。



project在gitlab-production中的结构为:

       
 "_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,在这个query里,充当parent_type的值

type只是本身的对象属性

user在gitlab-production-users的结构为:

        "_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"
        }

用户的权限通过namespace_ancestry_ids进行存储

通过namespace-project的id拼接方便进行权限控制。

基于父子数据建模的权限控制设计

父子数据建模使用了ES的Has Parent Query

为什么Gitlab不使用ES官方推荐的大宽表数据建模?

gitlab的搜索对象存在父子关系,且子对象也需要被独立搜索出来,因此,ES内部的子对象是独立对象存储的。

每个project下面有多种类型的子对象,每种子对象都可能数量众多。

缺点:写入操作变得繁琐

  • 如果采用大宽表设计,当project的权限改变时,该project的全部子对象的project权限属性都要同步更新,涉及面很广。
  • project每新增一个子对象时,需要查询project的属性后,再填入子对象中。

采用父子关系的数据建模

写入过程,需要额外增加route属性,保证父子对象(同一个project)都在同一个shard中。

由于是project级别的路由,因此_route值为"project-${project_id}"

父子关系的建模在什么数据量下的性能变得不可接受?(gitlab的搜索不是一个高频操作,每个project下子对象总数也不会太高)

父子关系的数据建模适合:

  • 整体的父对象不多,但是父对象内部的子对象较多的场景
  • 搜索性能要求不高

索引版本管理

具有schema_version字段,Format is YYMM (如2303),当schema改变时,这个值需要变更。

ES ID设计

ES ID设计的核心是唯一性。(_Id 字段)

ES的文档_id

两种设计思路:

  • ProjectID_项目内唯一识别字符  (用于对象的唯一识别是项目内唯一的)
  • 对象类型_对象ID (用于对象的ID是全局唯一的)

ProjectID_项目内唯一识别字符

blob的ID设计为:

${project_id}_${blob_path}

(wiki_blob采用和blob一样的设计)

commit:

${project_id_${sha}

对象类型_对象ID

project的ID设计为:

project_${project_id}

milestone的ID设计为:

milestone_${milestone_id}

snippet的ID设计为:

snippet_${snippet_id}

source内部的ID仍然使用对象自己的业务ID(_source内部的id和ES的_id的不一样,如何做到的?TODO:)

子对象ID

通过例如repository的id名为rid (有较好的可读性)

rid: repository id

一个project下面会有2个repository

1个为代码仓库,id和project一致

1个为wiki仓库,id为wiki_${project_id}

oid: blob id / wiki_blob id

附录

通过查看ES的日志,来获取gitlab实际的搜索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来强制硬删除(merge segment的过程会最终硬删除文档),但是forcemerge是一个阻塞操作,会严重影响ES的整体性能,因此只能放弃forcemerge。

不同领域对象在一个大索引 还是不同领域对象在不同索引的问题。

  • 55
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值