GitLab Code Search:在 Basic、Advanced 与 Exact 之间建立证据链
同一条查询换个分支,背后可能已经换了搜索引擎
一个 GitLab 实例准备删除 LegacyGateway。开发者在项目默认分支搜到十二处,在功能分支却只看到三处;管理员随后启用跨组搜索,结果又变成十六处。团队最初怀疑索引损坏,最后发现三次操作分别落在 Exact、Basic 和不同索引范围的 Advanced 搜索上。界面都叫“搜索”,查询语义、覆盖范围和失败方式却不同。
GitLab 的代码搜索不能只记录关键词。至少还要记录搜索类型、项目或组范围、分支、执行身份、索引状态和容量限制。只有这些状态同时可见,零命中才能从一句结论变成可以继续诊断的工程证据。
先识别真正处理请求的搜索后端,再讨论结果数量与迁移风险,才能避免把产品降级路径误判成源码事实。
先识别当前请求落在哪条路径
GitLab 按可用性为代码搜索选择 Exact、Advanced、Basic。Exact Code Search 提供精确匹配和正则模式;Exact 不可用时使用 Advanced;两种索引搜索都不可用,或者查询非默认分支时,回到 Basic。Basic 的代码搜索只在单项目中工作,不提供组级或全局代码搜索。GitLab 搜索总览列出了三类搜索的选择顺序和作用域。
这三类能力的产品状态、许可和安装责任不同:
Basic 属于 Free、Premium、Ultimate 的项目级能力,不需要外部搜索集群。Advanced 属于 Premium、Ultimate;GitLab.com 与 GitLab Dedicated 的付费订阅默认启用,Self-Managed 需要管理员另行部署 Elasticsearch 或受支持的 OpenSearch,配置连接、索引实例,再启用查询。
Exact 属于 Premium、Ultimate,当前官方状态是 Limited Availability,适用于 GitLab.com 与 Self-Managed,不应写成 GitLab Dedicated 的既定能力。Self-Managed 需要至少一个 Zoekt 节点;正式部署使用 Zoekt Helm chart 或 GitLab Operator,官方 Docker Compose 与 Ansible 入口只用于测试。
GitLab、Zoekt chart/indexer、Elasticsearch/OpenSearch 的支持组合与许可必须分别核对。GitLab 每条发行线绑定特定 Zoekt 版本;Elasticsearch 默认发行版、源码与 OpenSearch 的许可证也不是同一合同,不能因 API 兼容就当作可随意互换。升级前以目标 GitLab 版本的兼容矩阵和组织采购条款为准。
普通开发者可以从页面结果特征和 URL 参数确认类型;需要固定实验路径时,搜索 URL 使用 search_type=basic、search_type=advanced 或 search_type=zoekt。这不是绕过能力开关:后端未启用、当前作用域不支持或账号无权访问时,指定参数也不会凭空产生结果。
用 Basic 建立无外部索引的基线
在测试项目默认分支的 src/search-proof.txt 中加入唯一标记:
SEARCH_PROOF_7F3A进入项目,选择搜索,输入标记并选择 Code,显式使用 Basic 搜索。预期结果指向该文件,并只展示文件中的第一个匹配。再用三个字段逐步收敛:
SEARCH_PROOF_7F3A filename:*proof.txt
SEARCH_PROOF_7F3A path:src/
SEARCH_PROOF_7F3A extension:txtfilename: 匹配文件名,path: 匹配仓库位置,extension: 使用不带点的精确扩展名。Basic 使用精确子串匹配,适合单项目、指定分支的快速核对。它不适合跨组迁移清点,也不能因为“只显示文件中第一处”就推断文件内只有一次命中。
创建 migration/search-proof 分支,只在该分支新增 BRANCH_ONLY_PROOF。在项目搜索中选择该分支后应由 Basic 找到标记;切回默认分支则不应命中。这项反例很关键:Advanced 与 Exact 只搜索默认分支,非默认分支回落 Basic 是产品行为,不是索引自动扩展到了所有分支。
项目级 Search API 能把这条基线变成可审计输出。每次调用都需要认证;令牌只授予目标项目的最小只读权限,不能放进查询字符串、仓库或 CI 日志。glab api 示例显式固定 Basic 和分支:
glab api --hostname gitlab.example.com --method GET \
'projects/123/search' \
-f scope=blobs \
-f search='SEARCH_PROOF_7F3A path:src/' \
-f search_type=basic \
-f ref='migration/search-proof' \
--paginate预期每条记录至少带 path、ref、project_id 和匹配片段;无项目读取权时,私有项目端点返回 404,不能据此推断项目不存在。Search API 使用 offset 分页,脚本应跟随响应的下一页信息,并保存页数、执行身份与响应状态。Self-Managed 默认对已认证搜索请求设置每分钟限额且管理员可调整,出现 429 或“requested too many times”时按服务端窗口退避,不要并发重放。
Advanced 把实例内容送入全文索引
Advanced 不只搜索代码,还可覆盖提交、工作项、合并请求、评论、项目、用户和 Wiki 等作用域。代码文档进入 Elasticsearch 或 OpenSearch 的 blobs 范围,查询使用 simple_query_string 语义,支持精确、模糊、布尔、排除和部分匹配。它不是 Exact 的旧名字,也不保证任意符号都按字面子串处理。代码查询可继续使用 filename:、path:、extension: 和 blob: 等限定词,已知问题、特殊字符限制与默认分支约束见 Advanced Search 使用文档。
Self-Managed 启用顺序必须让索引先追平,再让用户查询:管理员在 Admin > Settings > Search > Advanced search 配置 URL、认证、分片和副本;先开启 Turn on indexing for advanced search,完成实例索引并检查状态;最后开启 Search with advanced search。代码搜索还受 Code search with advanced search 控制。这个字段关闭时,GitLab 会删除 Elasticsearch 中的代码数据;重新打开后必须完整重建代码索引,不能只把复选框勾回来。
几个字段直接改变系统代价和可见语料:
Pause indexing for advanced search 暂停把变化提交到索引,但变化仍被跟踪;适合迁移和重建窗口。暂停期间查询可能继续返回旧状态。Number of shards and replicas per index 改变并行度、故障恢复与磁盘占用。调整分片数通常要重建索引才生效,增加副本会近似增加完整分片副本的存储成本。
Limit the amount of namespace and project data to index 把全文索引限定到指定命名空间或项目。未指定对象时不会神奇地索引全部关联数据;有限索引还会改变全局、跨项目代码搜索的可用范围。URL、Username、Password 或 AWS OpenSearch IAM 配置决定 GitLab 后端如何访问集群。凭据属于服务身份,不应进入仓库或开发者浏览器。
Elasticsearch 和 AWS OpenSearch 不包含在 GitLab Linux package 中,官方建议与 GitLab 分开部署。具体兼容版本必须按 GitLab 版本查 Elasticsearch/OpenSearch 集成文档,因为搜索后端的支持线会变化。OpenSearch 的 VPC、域策略、细粒度访问控制与请求签名是额外边界;把服务暴露到公网再依赖一组主账号,会同时放大代码泄露和凭据滥用风险。
最小启用证据不是“搜索框能返回东西”,而是以下状态同时成立:集群连接验证通过;索引状态没有持续欠账;Sidekiq 中代码索引 worker 没有积压或反复失败;SEARCH_PROOF_7F3A 在默认分支可查;新提交的第二个标记在可接受延迟后可查;未授权账号仍看不到私有项目结果。
Exact 用 Zoekt 专门索引代码
Exact Code Search 由 Zoekt 驱动,只处理代码。它不会替代 Elasticsearch/OpenSearch 对评论、提交、工作项、合并请求、用户和 Wiki 的搜索。团队可以让 Exact 承担代码查询,同时保留 Advanced 处理其他作用域;如果不再让 Advanced 索引代码,关闭对应代码开关可以节省资源,但这会删除 Elasticsearch 中的代码索引。
Self-Managed 安装 Zoekt 后,在 Admin > Settings > Search > Exact code search 启用 Enable indexing 与 Enable searching。先开放 indexing、观察仓库状态变为 ready,再开放 searching,能避免用户在半成品索引上得出结论。官方 Zoekt 集成文档提供 Helm、Operator、状态检查、健康检查、暂停和容量估算入口。
sudo gitlab-rake gitlab:zoekt:estimate_storage
gitlab-rake gitlab:zoekt:info
gitlab-rake gitlab:zoekt:healthestimate_storage 用于规划节点磁盘;info 应显示 indexing/searching 开关、在线节点、命名空间、仓库、任务、索引状态和水位;health 返回 HEALTHY、DEGRADED 或 UNHEALTHY,并提供可用于自动化的退出码。只有节点在线而仓库仍处于 pending/failed,不能算搜索可用。
Exact 默认使用精确匹配模式,也可切换正则模式。两种模式对引号、空格和 or 的解释不同,因此迁移记录必须保存 mode。它只搜索默认分支;单文件受大小和 trigram 数限制,同一行的多次匹配会计为一个结果。Zoekt 用三字符序列建立候选索引,所以 Maximum trigrams per file 越大,能覆盖的复杂文本越多,但索引和查询成本也越高;超过限制的文件只能按文件名搜索。Maximum file size for indexing 同样决定内容是否可搜,超限文件只保留文件名。
Exact 的 Search API 已是正式 API,但 Exact 产品本身仍是 Limited Availability,这两个状态不能混写。自动化要显式固定 search_type=zoekt;GitLab 18.7 起可用 exclude_forks 和 include_archived,18.9 起可用 regex,18.11 起可用 num_context_lines,19.0 起 Exact 查询增加 repo: 过滤器。API 在省略 exclude_forks 时排除 fork,在省略 regex 时按正则解释,因此做字面清点时必须写出 exclude_forks=false 与 regex=false,并在升级记录中保存 GitLab 版本:
glab api --hostname gitlab.example.com --method GET \
'groups/42/search' \
-f scope=blobs \
-f search='LegacyGateway' \
-f search_type=zoekt \
-f exclude_forks=false \
-f include_archived=false \
-f regex=false \
--paginate这条命令的预期证据不是一个总数,而是一组带 project_id、path、ref 的候选记录。若目标实例早于对应参数版本,先删去不支持的参数并把能力缺口写入搜索合同,不能让客户端静默切回 Advanced 或 Basic。Search API还提醒 Elasticsearch 通配符语法不一定能在 Exact 正确工作;迁移查询应为不同引擎分别保存语法,而不是共用一条字符串。
还有几项容量字段值得进入架构评审:
Maximum number of files per project to be indexed:默认分支文件数超过限制的项目不会被索引,调大它会增加单项目索引时间和存储压力。Indexing CPU to tasks multiplier:提高并发可缩短积压,但会争用 CPU。Number of parallel processes per indexing task:提高单任务并行度会增加 CPU 和内存占用。
Number of replicas per namespace:提高查询可用性和分摊能力,同时增加存储。Indexing timeout per project 与失败重试间隔:太短会让大仓库反复失败,太长会让故障任务占用资源更久。Cache search results for five minutes:降低重复查询压力,却意味着权限或提交变化后要考虑短暂缓存窗口,敏感权限回收仍需同时验证源项目访问。
这些默认值和可用字段会随 GitLab/Zoekt 版本演进。生产变更要从目标实例的 gitlab:zoekt:info 读取实际值,再以仓库规模、峰值 QPS、索引追平时间和磁盘水位决定调整,不能照抄示例数字。
三个反向实验定位“为什么是零”
第一个实验检查索引暂停。在默认分支确认 SEARCH_PROOF_7F3A 可查后暂停 Advanced 或 Exact indexing,再提交 SEARCH_PROOF_AFTER_PAUSE。旧标记仍可能命中,新标记不应立即出现。恢复 indexing 并等待任务追平后,新标记应出现。证据是“旧可查、新不可查、恢复后新可查”三联状态;只看搜索服务 HTTP 正常无法发现陈旧索引。
第二个实验检查索引排除。为 Exact 准备一个超过当前文件大小或 trigram 限制的测试文件,内容放入唯一标记。预期按内容零命中,但按文件名仍可找到;gitlab:zoekt:info 给出当前限制,仓库状态仍应 ready。为 Advanced 做同类实验时,应读取实例的最大索引文件大小,而不是假定固定值。实验完成后删除大文件,避免持续占用仓库存储和索引队列。
同一轮实验还要覆盖 fork、submodule 与 LFS。fork 是独立项目;Exact API 默认排除 fork,只有显式 exclude_forks=false 才能验证 fork 自己的默认分支,Advanced 与 Basic 则按实际作用域逐项目核对。父项目中的 submodule 只保存 gitlink 和 .gitmodules,不会把子项目源码并入父项目代码索引;应解析子项目 URL 与固定提交,分别验证权限并搜索。Git LFS 在 Git 仓库里只留下文本指针,实际大对象存放在 LFS 存储,三类代码搜索都不应被当作 LFS 对象内容搜索。GitLab LFS 文档说明了这一存储边界;需要检查对象内容时,在受控克隆中获取 LFS 对象,并记录下载容量、凭据范围与清理动作。
第三个实验检查权限。账号 A 能读取私有测试项目,账号 B 不能。两者使用相同 search type、作用域和标记查询;A 应看到并打开代码,B 不应获得结果。Advanced 虽把项目放进共享索引,私有项目仍只向有项目权限的用户展示;Exact 和 Basic 也必须遵守项目访问控制。权限角色还有细粒度差异,尤其是自定义角色或 Planner/Guest 能力,实施前应按 GitLab 权限表核对目标版本,不要用“能看到项目首页”替代“能读取私有仓库代码”。
把搜索接入迁移项目,而不是接入一个数字
跨项目重构可维护一份候选清单,字段至少包括:GitLab 实例与版本、search type、group/project、默认分支提交、查询和 mode、命名空间索引范围、fork/归档策略、submodule 提交、LFS 补查状态、超限或未索引项目、执行角色、命中仓库、仓库 owner、本地补查结果。先用 Exact 或 Advanced 发现默认分支候选,再由每个 owner 在目标提交执行本地文本、符号或结构化搜索;非默认分支统一走 Basic 或本地克隆。
正向流程是“唯一正例命中 -> 分片查询低于展示或容量边界 -> owner 复核 -> 生成 PR -> 编译和测试 -> 合并后旧查询清零 -> 运行观测确认”。反向样本同时保留一个同名注释、一个生成文件和一个分支独有调用,证明规则知道哪些对象不应修改、哪些对象线上索引看不到。搜索读权限不等于推送、合并或绕过保护分支的权限;不要为了批量迁移给搜索账号写权限。
当查询需要跨多个代码宿主、完整历史、所有分支或编译器级引用关系时,GitLab 三类搜索都不是完整答案。此时应选择能同步目标宿主和权限的独立代码搜索,或在受控环境克隆精确引用后运行本地工具。选型判断可以很朴素:单项目任意分支核对用 Basic;GitLab 多内容域和全文搜索用 Advanced;GitLab 默认分支上的大规模精确/正则代码查询用 Exact;类型安全重构仍回到 LSP、编译器或 AST/LST 工具。文本索引根据 trigram 或全文倒排结构快速产生候选,AST 则解析语法节点,LSP/编译器进一步结合符号表、依赖与类型归因;只有后两者能区分许多同名符号、重载和跨文件引用,但反射、生成代码和运行时装配仍需测试与观测补证。
停用、回滚与长期治理
Advanced 的回退顺序是先停止向用户提供 advanced search,再暂停或关闭 indexing;代码查询应确认已转到 Exact(若可用)或项目级 Basic,其他 Advanced 作用域则要逐项确认可接受的降级路径,最后按数据保留决策删除索引。直接运行完整索引任务会删除并重建现有索引,必须在维护窗口、备份与恢复路径明确后执行。后端升级时先关闭 Advanced 查询、暂停 indexing,完成集群升级和 gitlab:elastic:index_and_search_validation,再恢复 indexing、确认追平,最后开放查询。
Exact 的回退先关闭 searching,让新查询不再依赖 Zoekt;再暂停 indexing,确认 Basic/Advanced 路径满足业务;最后按 Helm 或 Operator 生命周期移除节点、持久卷和服务凭据。不要先删节点再关搜索,否则路由仍可能把请求送往失效后端。测试环境可以用官方 Rake 任务禁用 Exact,但生产仍要把 chart、存储、监控和网络策略纳入同一次变更。
长期成本由两条索引链分别产生。Advanced 消耗 Elasticsearch/OpenSearch 的节点、分片、副本、Sidekiq 和网络;Exact 消耗 Zoekt 的索引节点、CPU、内存、mmap 与磁盘副本。两者并存时若 Advanced 仍索引代码,会为相同代码承担两份不同索引成本;纳入 fork 会重复索引相同或相近代码,展开 submodule 与 LFS 则把流量、工作区和敏感数据落盘成本转移到本地补查。容量评审应持续观察仓库总量、索引大小、节点水位、索引延迟、失败仓库、查询延迟、限流与重建时间,以趋势和恢复目标定阈值,不用脱离负载的固定数字冒充生产标准。
治理上给 GitLab 搜索控制面指定 owner,服务凭据进入密钥系统并定期轮换,搜索后端只接受 GitLab 受控网络访问,管理员操作进入审计与变更记录。默认分支变更、命名空间迁移、GitLab/Elasticsearch/OpenSearch/Zoekt 升级、权限模型调整或索引配置变化后,重跑正例、暂停索引反例、大文件排除和双身份权限实验。查询词、路径和截图同样可能含敏感架构信息,工单与文档只保留脱敏证据。
清理实验时删除测试标记、大文件和临时分支,撤销测试成员与临时服务凭据,删除 API 响应、submodule 工作树和 LFS 本地对象缓存,等待增量索引完成后确认三类搜索都不再返回标记。若删除了专用测试项目或 fork,还要确认 Advanced 与 Zoekt 中对应仓库记录最终消失、任务没有永久卡在 pending/failed。这样退出的是测试数据和临时权限,而不是把一个陈旧索引或敏感对象悄悄留给下一次迁移。
