Gerrit remains the identity and trust boundary for every search request. A logged-in user‘s query flows from Gerrit’s own UI, through a Gerrit-side component that knows the user and their allowed scope, to a separate search platform built around stock Zoekt. The search platform is only ever told a query together with the scope it's allowed to touch.
The search platform adds the pieces that stock Zoekt does not provide on its own: a scoped gateway in front of multiple Zoekt webservers, repository ID management, index generation publishing, per-node shard activation, and a control plane for routing and node health.
The full step-by-step walkthrough, for both the query path and the indexing path, is in End-to-End Flow below.
The design is built from a small number of responsibilities, split across two independent systems: one on the Gerrit side, one forming a standalone search platform.
On the Gerrit side:
config-gitweb URL can be used when Gitweb is the site‘s browser instead. If neither browser integration is available, search still works but the result cannot provide a file-view link. Also exposes, for the same allowed scope, which of the user’s readable projects and branches are currently indexed.In the search platform (independent of Gerrit, reusable by any caller that can supply a valid scope):
{server, project, refs} scope into Zoekt repository IDs and branches, ANDs that scope into the parsed Zoekt query, routes to the right shard groups, fans out to one healthy replica per touched group, and merges ranked results. Empty, missing, or fully unresolved scope returns zero results with scrubbed statistics..zoekt shards and execute the actual search. The design does not require forking Zoekt; scope is enforced by adding a BranchesRepos constraint to each query before it reaches Zoekt.This split is deliberate: the search platform never needs to understand Gerrit's users, sessions, or access rules. All Gerrit-specific knowledge stays on the Gerrit side.
Query flow — a user searches:
BranchesRepos scope, and combines that scope with the parsed user query.Three outcomes are always distinguished: normal results; “no results” (covering both a genuinely empty match and a user with no readable scope — deliberately indistinguishable, so a lack of access can never be inferred from the response); and “search unavailable” (the search platform itself could not be reached), so an outage is visible rather than mistaken for an empty result.
Coverage flow — a user checks what's indexed:
The same indexed/not-indexed status can be shown from repository and branch views by asking for coverage only for the project/branch rows the caller is already allowed to see.
Indexing flow — keeping the index current:
Which projects and branches get indexed and access-checked is a deployment decision, not a fixed requirement of this design. A deployment can start by indexing only default branches for selected projects and expand from there — to a broader active-branch set, to specific long-lived branches, or further — once it has measured the actual demand, storage cost, and access-check cost of doing so for its own repositories. Nothing in this design requires starting anywhere other than the smallest option.
Coverage configuration should be treated as data consumed by the indexing pipeline and control plane, not as code baked into either service. That lets administrators or project owners change indexed coverage without rebuilding the search platform.
Repository IDs are part of that managed data. IDs must be stable, non-zero, and never reused for another project; otherwise a query scoped to one project could accidentally match shards for another project. The control plane should be the single authority that allocates and persists IDs, and the indexer should fail closed if it cannot obtain a valid ID.
Indexed content and query load are spread across many serving nodes, and that fleet can grow, shrink, or be rebalanced independently of the Gerrit-side components — a busier search platform does not require touching Gerrit, and vice versa.
The gateway routes by repository ID to shard groups and sends each query only to groups that can contain in-scope repositories. Each shard group can have multiple replicas; the gateway chooses a healthy replica from its cached routing snapshot and marks results incomplete if a needed group has no live replica. This preserves correctness under partial failure: missing capacity can hide matches temporarily, but it cannot add out-of-scope matches.
The index rollout path is eventually consistent by design. New generations are published once, then sync agents activate them independently on each serving node. This avoids a coordinated fleet-wide restart and lets a node keep serving its previous validated shards if it cannot fetch or validate a new generation.
The visibility-awareness component scales with the number of Gerrit access groups and projects, not with the number of users or the rate of search requests, since it computes and caches an answer per group rather than per user or per request.
The largest scale risk for any given deployment is how many branches it chooses to index — a large or unevenly distributed branch count drives indexing, storage, and access-derivation cost independently of query volume, which is why branch coverage is treated as a deployment policy decision to be measured, not a fixed default (see Branch Policy).
Visibility-derivation cost and refresh interval must be tuned per deployment: measure the cost of deriving allowed scope from Gerrit's access rules, and tune how often it is refreshed against that measurement, rather than assuming one interval fits every installation. A new or previously unseen account/group adds a bounded, synchronous derivation cost to its first query — trading a slower first request for not returning empty results to a new user — and that timeout should be tuned per deployment as well.
The control plane should also be kept off the per-query hot path. Gateways should watch routing and repository-ID snapshots and serve from memory. Losing the control plane temporarily makes those snapshots stale; it should not fail every query.
Use OpenGrok as the search backend
OpenGrok indexes and authorizes at project granularity, not per branch or ref, so it has no native way to represent Gerrit‘s ref-level access model. Giving OpenGrok branch-awareness means synthesizing it externally — for example a separate working-tree checkout and index per branch, or a project+branch sharding scheme — with cost that scales with branch count, not just project count. A quick test of the Gerrit project’s 52 branches showed the difference concretely: a single Zoekt index covering all 52 branches was about 1.1 GB and took about 40 seconds to build, tagging each result with the branches that contain it and supporting branch:<name> query filtering natively; covering the same branches in OpenGrok required a full working-tree checkout per branch, about 407 MB each. OpenGrok‘s genuine strength is its code browser (cross-references, jump-to-definition, annotate, history), but Gitiles already renders files, blame, and history while enforcing Gerrit’s ACLs natively, so this design uses Zoekt for search and links results out to Gitiles for browsing instead of adopting or ACL-wrapping a second browser. Zoekt is also licensed Apache 2.0, the same as Gerrit, versus OpenGrok's CDDL.
Expose the underlying search engine directly to users
Simpler to deploy, but rejected outright: a general-purpose search engine has no concept of Gerrit sessions or access rules, so exposing it directly would mean either no access control on search results, or duplicating Gerrit's access model inside a system that was never designed to hold it.
Have the search platform enforce Gerrit's access rules itself
Would require the search platform to understand Gerrit‘s permission model in depth, risking the two implementations drifting apart over time, and tightly coupling a platform that should be able to serve any caller to one specific host application’s security model. Keeping all access-control knowledge on the Gerrit side, and having the search platform simply enforce whatever scope it's handed, keeps the two systems cleanly separated.
Fork Zoekt to add Gerrit-specific authorization
Would put Gerrit-specific concepts into the search engine and make future Zoekt upgrades harder. Stock Zoekt already has the query-level primitives needed to constrain searches by repository ID and branch, so the safer integration point is a gateway that constructs scoped Zoekt queries before fan-out.
Check every result against Gerrit after search
Would reduce the risk of stale visibility data, but it would also make search latency and load proportional to result count and Gerrit's live permission-check cost. It also does not prevent leaking aggregate information unless the search platform still avoids exposing raw match counts before filtering. The proposed design filters before search and accepts a documented, bounded staleness window for access changes.
Off-the-shelf commercial or hosted code search products
Worth evaluating independently on their own merits (Gerrit integration, access-control parity, scale, operational fit), but out of scope for this design, which assumes building and operating the search platform in-house.
Pros:
Cons:
Rough implementation phases: