Project Telescope: Next-generation code search experience, ranking and retrieval for GitLab
| Status | Authors | Coach | DRIs | Owning Stage | Created |
|---|---|---|---|---|---|
| proposed |
chopras
|
ahegyi
|
chopras
|
devops analytics | 2026-09-28 |
Note:
This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. The development, release, and timing of any products, features, or functionality may change or be delayed and remain at the sole discretion of GitLab Inc. Latency and freshness figures given as objectives are engineering targets, not contractual service level agreements.
The engineering detail is in Telescope: Technical design. GitLab.com usage, cost, and telemetry figures behind this proposal are internal under the SAFE framework and live on the internal evidence page (internal only).
Summary
GitLab’s own coding agents default to grep and don’t use code search. Duo agent sessions work on branches that neither of our code indexes can see, so they search the checkout by brute force. It works like a new engineer who never used the company’s search: asked to change one setting, it opens folder after folder and reads file after file until it finds the right line, and every page it reads is metered. Loading code into the model’s context is the dominant share of the tokens an agent task consumes, and most of that loading lands on files that turn out to be irrelevant. Even a trivial change takes many model calls, and the call count barely moves with the size of the task, which means most of it is fixed overhead: locating files, reading them, re-reading them (a field-reported example is gitlab-org/gitlab#607004). Each of those tokens is metered, each round adds latency the customer feels, and when the search misses there is no human in the loop to notice. Sessions that do not complete are disproportionately the long, tool-heavy ones that keep reading without converging.
We propose to build the layer above the engines, not another engine. We call it Telescope, a retrieval plane: one request contract every caller speaks (the search bar, GLQL, a plain-English question, an agent’s tool call), one permission check, pushed into the engines rather than applied to a page of results, one completeness report on every response stating exactly what was and was not searched, and one change log keeping every index fresh from the same feed. Underneath it we fix what we have. Index every branch that matters and overlay the agent’s uncommitted work in about a second. Store each unique file once instead of once per fork. Add a symbol index so “where is this defined” and “who calls it” are millisecond lookups, and every identifier in a file or a diff becomes a link. Let an agent ask for the answer that fits its token budget in one round trip. The goal is for thirty rounds to become a handful. What that looks like: an agent asks once and gets the four functions that matter, including the one written an hour ago on its own branch; a security engineer shows a leaked key is nowhere across 15,212 of 15,212 projects and keeps a signed record of exactly what was searched; an engineer in their first week asks how billing retry works and gets three paragraphs with eight citations, the newest a merge request from last week.
GitLab is positioned to build all of it, including one piece we have not seen elsewhere. Every competitor in the positioning table has some of these capabilities. GitLab is positioned to have all of them, because GitLab holds the code, the work items, the pipelines, the vulnerabilities, the permission model that is the source of truth, and the agents in one system. Third-party tools typically synchronize a copy of a code host’s permissions; a synchronized copy can lag the source of truth, and enterprise customers ask us about that lag. We evaluate authorization against GitLab’s own permission data rather than against a synchronized copy, and every rule that reduces to a filter is applied inside the engine rather than to a page of results that came back. Counts are exact when the rules reduce to filters, and are reported as approximate when a rule needs request context the index cannot hold, so the completeness report states how permissions were evaluated and becomes a signed coverage record a security engineer can keep. As of September 2026 we are not aware of any other code host offering one. Zero results only mean something when the engine can state what it searched, and that record is the one part of the plan where GitLab would lead rather than catch up. Security leaders have asked us for it.
What else changes. Retrieval inside the merge request, which is how developers who never open the search bar start using search at all. One indexing pipeline, one published lag, and one on-call pager instead of four. Elasticsearch shrunk to the text it is good at. And, for the first time, a published latency objective for search: p95 at or under 500 ms across an organization’s code, with an error budget behind it. It is an engineering objective, not a contractual service level agreement.
Why it pays for itself. Most of the tokens an agent task consumes go to loading code into context, so better retrieval reduces the token consumption of every agent task, which customers see directly as metered usage. Google, GitHub, and Meta each built and run their own code search systems. The alternative is the default path, named first under Alternative solutions: keep spending tokens on every grep loop, and leave the retrieval layer over our customers’ code to someone else.
Positioning against peers
Capabilities as described in each product’s public documentation, as of September 2026. This is an analysis of documented capabilities, not a benchmark, and it is not a statement about the quality of any product. Corrections with a public source are welcome as merge requests.
| Capability | GitHub | Sourcegraph | Cursor and similar IDE agents | GitLab today | GitLab after this plan |
|---|---|---|---|---|---|
| Exact search across an organization | Yes; capped at 100 results, exhaustive search not supported, no report of skipped files | Yes, exhaustive mode documented | No (the open workspace only) | Limited availability; group scope only on GitLab.com; default branch; unreported caps | Yes, every relevant branch, complete or honestly partial, attested |
| Definitions and references across repositories | Partial (symbol definitions only, not references) | Yes, precise, over MCP | Within the open workspace | Partial (Knowledge Graph: cross-project definitions and import-level references, default branch only) | Yes, precise, over MCP and in the UI |
| Sees uncommitted work | No (default branch only) | Not documented | Yes (indexes the local workspace) | No | Yes (session overlay, server-side) |
| Deduplicates forks and vendored copies | Yes (content-addressed index) | Not documented | Not applicable | No | Yes, gated on the measured duplication ratio |
| Ranking beyond text match | Yes (results sorted by score; method not documented) | Yes (BM25F) | Yes (embedding-based) | No | Yes (reference ranks and an off-the-shelf reranker; training a ranking model is out of scope, see Non-Goals) |
| Token budget as a retrieval parameter | Not documented | Not documented | Not documented | No | Yes, in the contract |
| Permissions evaluated inside the index | Query rewritten with the caller’s repository set, plus a post-query permission check | Permissions synced from the code host | Not applicable (local files) | Filters in the query, plus a page-level redaction check | Native: every rule that reduces to a filter is pushed into the engine, and the response reports how it was evaluated |
| Provenance on retrieved content | Not documented | Not documented | Not documented | No | Yes |
| Code, issues, MRs, and vulnerabilities searchable under one native permission check | Partly (separate search types) | Code, commits, and diffs | Not applicable | Yes, but not searchable together | Yes |
The public documentation behind each cell is listed under Sources.
The last row matters most. Each competitor has some of these capabilities. GitLab is positioned to have all of them, because it holds the code, the issues and merge requests, the permission model that is the source of truth, and the agents in one system.
Motivation
The problem
Any improvement plan starts with measurement, and today we do not measure search end to end: not how relevant it is, not how fast it is from the caller’s point of view, and not how often or by whom it is used. There are no published SLOs or SLAs for search.
Search has become infrastructure. For a decade it was a search bar a minority of developers opened a few times a week. In the agent era it is called dozens of times in every agent session, by machines that cannot tell when an answer is incomplete. As of September 2026, the large majority of searches on GitLab.com arrive through the API rather than from a person typing in the search bar, and API search traffic has grown many times faster than the search bar over the last two years. The retrieval layer therefore decides how many tokens every agent task spends and how far every answer can be trusted. At GitLab it is owned in five pieces by one small team. We have five ways of finding things, each with its own permission logic, four indexing pipelines, four unpublished lag numbers, four pagers, and no end-to-end latency target of any kind. Both code indexes see only the default branch. No system records which branch an agent worked on, so we cannot even count how often the index is blind to it. A security engineer at a large enterprise with tens of thousands of repositories gets a list capped at about five thousand results, no count of how many repositories matched, no notice of what was skipped, and no way to keep what was found (gitlab-org/gitlab#624419). The engineer clones every repository and greps it, because that can be documented.
Six journeys we do not serve today
Each journey is a composite drawn from public issues, user interviews, and production traces. Personas are fictional. Each follows the same shape: who the person is, what happens today, what we propose, and what a good outcome looks like.
Journey 1: The security engineer who needs a result they can keep
Who. Priya runs application security at a large enterprise with tens of thousands of repositories under one group, an environment like the one described in the public issue gitlab-org/gitlab#624419.
Today. A vulnerability is disclosed in a common library. Priya searches for every use. They get a list capped at about five thousand results and no count of how many matched. They are not told that files over a megabyte were skipped, that three projects were skipped for having too many files, or that a syntax error in their pattern would produce the same empty screen as a clean miss. They cannot export what they found, so they clone every repository and run grep, because that they can document.
What we propose. A completeness report on every response: a small block that states how many results matched, how many were returned, what the index skipped and why, whether any engine was degraded, and how old the index was. An exhaustive mode that streams every match and, for anything the index skipped, scans the raw files as a queued background job and reports the coverage explicitly. A coverage record: a tamper-evident description of what was searched, over what scope, as of which index manifest (the list of every project’s indexed commit, carried as a digest), with what was skipped, that Priya can keep with their own documentation. (Tamper-evident means the record is signed by the instance, so any later change to it can be detected.) Permission-safe export.
What great looks like. Priya types the pattern and in under a second sees: 12,431 matches across 15,212 of 15,212 projects; 254 files skipped by the index and now being scanned; scan complete in 40 seconds. They export the result with its coverage record.
The hardest case is zero. Half of Priya’s searches are looking for nothing: prove that the deprecated cipher or the leaked key is nowhere. Any engine can return zero results, but only an engine that knows its own coverage can vouch for them. “No matches, 15,212 of 15,212 projects, 0 files skipped, as of index manifest 9f2c” is a statement about the index’s coverage at a point in time. (A single commit cannot describe 15,212 repositories; 9f2c is the digest of the manifest, and the full list can be produced on demand.) We are not aware of another code host offering an equivalent record as of September 2026.
Note: What the coverage record is, and is not. It describes the scope and coverage of an index at a point in time. It is not an audit attestation, not a statement about content outside the searched scope, and not a representation that any regulatory or audit standard has been met. Customers determine with their own auditors what evidence their frameworks require.
Journey 2: The coding agent that cannot see the code it is writing
Who. A Duo agent working an issue on a feature branch inside a sandbox (an isolated, GitLab-controlled environment where the agent’s checkout lives). Many run every day, and the number has grown quickly.
Today. Both code indexes see only the default branch. The agent is editing feature/payment-retry, but every search is answered from main. The functions it and its teammates wrote this week do not exist as far as the index knows. So it greps the checkout in a loop: dozens of rounds and millions of tokens per task, most of them spent loading files that turn out to be irrelevant. An incomplete search goes unnoticed, because there is no human in the loop.
Note: In plain terms. The index is a card catalog printed from the default branch. Anything written on a branch this week is not in the catalog. Indexing the branches that matter adds their pages; the session overlay is a sticky note on top listing the pages changed in the last hour. The token budget is telling the librarian how many pages you can carry back to your desk: they bring the most relevant ones that fit and say how many they left on the shelf.
What we propose. Two layers. First, index the branches that matter: default, protected, release-pattern, and open-MR source branches, using Zoekt’s branch masks so identical files across branches are stored once. (A branch mask is a small tag on each stored file version saying which branches contain it; the mechanism is explained in the technical design under Which branches.) Second, a session overlay: a tiny index of only the files that differ between the agent’s working tree and the nearest indexed ref, built in about a second and searched together with the main index. The completeness report says “includes your 37 uncommitted changes.” Where the working tree lives determines how the overlay is built; see Session overlay for the three topologies and why the sandbox case comes first. Both layers are searched through the plane’s request contract, which carries a token budget set by the caller alongside its budgets in results and time. The plane ranks the matches, packs the answer to the budget, and reports what did not fit.
Why the budget travels with the request. Only the agent knows how much context it has left and how much of it one lookup is worth; the plane cannot. So the budget is a field on the call, set by the caller: not part of the question itself and not a service-side default. Without it, indexing the right branches fixes only half of the problem: the agent can now find this week’s code, but it still gets full files back and spends its context reading them. With it, the trimming happens in the index, before anything reaches the agent (Journey 4 calls this context packing). The number is a knob per call, not a ceiling on what can be found: if the agent asks for 6,000 tokens and the honest answer needs more, the plane returns the best-ranked results that fit and reports “4 of 9 matching functions shown,” and the agent widens the budget, pages, or asks a narrower question. Today an incomplete search is silent. With a budget and a coverage report it is explicit and machine-readable.
What great looks like. The agent asks once, “where is the retry policy applied for payments,” with a 6,000-token budget on the reply, and gets the four functions that matter, including the one written an hour ago on this branch, ranked, with coverage reported. Anything that did not fit is named, so the agent asks for more instead of guessing. Thirty rounds become a handful.
Who else does this. IDE-based tools compute a content-hash tree of the developer’s workspace and re-index only changed files. We are not aware of a server-side, per-session, permission-aware equivalent over an organization’s whole corpus.
Journey 3: The developer who needs the exact string, one definition, and its callers
Who. Marco, a senior engineer on a large Ruby and Go codebase spread across two thousand projects. Also Marco three hours later, reviewing a colleague’s merge request.
Today. Marco wants to know where PaymentProcessor is defined and which files call it. What they get depends on which engine answers. For years the full-text engine matched scattered words instead of the literal string (gitlab-org/gitlab#325234, open from 2021 until Zoekt replaced it). They see the same file two hundred times because two hundred repositories vendor the same library, meaning each carries its own copy of it. There is no cross-repository, compiler-precise find-references, and reviewing the MR they cannot ask “what else calls the three functions this MR changed.” They give up and ask their editor’s assistant to grep the checkout.
What we propose. Symbol lookups as search operators: definition:, references:, and implements:, answered in milliseconds from a symbol index built from SCIP, with the same completeness report as any search. Static ranking: files and symbols that many others reference rank higher. (“Static” because the score is computed ahead of time from the structure of the code, not per query.) Deduplication: one copy of each unique file, with “also present in N other repositories you can see” as an affordance rather than N results, gated on the duplication ratio measured in the first 90 days. Search inside the work: on every merge request, a references: search scoped to the symbols the MR changed; on every vulnerability, “where else does this pattern occur.”
Note: In plain terms: text search versus a symbol lookup. Searching the text for PaymentProcessor finds every file where those characters appear: the class, its callers, a comment that mentions it, a log message, and an unrelated LegacyPaymentProcessor. A symbol index is built by tools that understand the language, so it knows PaymentProcessor is a class defined in one place and referenced directly in three others, and it can tell those apart from a lookalike name. That is what definition: and references: return. They answer one hop: where is it, and who uses it directly.
Every identifier becomes a link. The same symbol index lights up the file view and the diff: hover for a signature, click to jump to the definition, click again for callers. Google’s published case study of its internal code search found that most of its engineers use it every day, mostly to navigate and understand code; it became a habit because it is instant and every name is a link, not because its ranking is clever. Through MCP, the same lookups reach the editor, so the assistant stops grepping for answers it could look up in a millisecond.
Who else does this. GitHub’s code search returns the top 100 results by score across all of its shards, so once all code is in one index, ranking decides what a user ever sees. The open-source Aider project showed that ranking symbols by how often the rest of the codebase references them lets an AI choose the right context within a fixed budget.
Journey 4: The engineer in their first week who wants to ask the codebase a question
Who. Wei joined three days ago. Their question would take a senior colleague two minutes: “how does billing retry work here, and what changed recently?”
Today. Search returns a list of files. Duo Chat can search one repository semantically but not the organization, and its retrieval runs through a separate path with a narrower scope than the search bar. Wei asks in Slack and waits.
What we propose. Ask: a plain-English question answered with citations to code, merge requests, and issues across everything Wei may see, on the same retrieval path and permission check as the search bar and the agents. Each sentence cites a result. Underneath it, context packing: instead of the top ten files, the plane returns the smallest set of functions that answers the question, cut to the model’s token budget, with the signatures of the helpers those functions call so the reader can follow them. (A signature is the one-line declaration of a function: its name, parameters, and return type, without the body.)
Note: Example: a token budget of 6,000. Wei’s question touches ten files totaling 40,000 tokens. Context packing returns the four function bodies that answer it (about 3,500 tokens) plus the signatures of the three helpers they call (a few hundred), and lists the rest by name. Outlines first, bodies on request.
What great looks like. Three paragraphs with eight citations, the most recent a merge request from last week that changed the retry backoff. Nobody in Slack was interrupted.
Who else does this. Sourcegraph and GitHub each offer natural-language question answering over code with citations. Research from Carnegie Mellon University in 2025 found that chunking code at function and class boundaries rather than arbitrary lines improves retrieval recall and end-task success. Sourcegraph’s published evaluation reports that giving an agent server-side search over MCP instead of local grep materially raised file recall and precision.
Journey 5: The platform team building its own agents on GitLab
Who. Dana leads a platform team at a customer with 800 developers, building internal agents (a change-risk reviewer, an on-call assistant) that need to retrieve from GitLab.
Today. Dana’s agents call the REST API for issues and clone repositories to grep them. There is no way to ask “give me the code relevant to this incident, only what my agent’s service account may see, in 8,000 tokens,” no completeness signal, and no provenance on what came back (a vendored file? a fork? an issue comment by a non-member?).
What we propose. The plane’s MCP endpoint, exposing three kinds of retrieval tool: find (search), fetch within a token budget, and symbol (definition or direct references). Every returned span carries the completeness report and provenance labels (visibility, vendored or generated, author membership, age, overlay). Provenance means “where did this text come from and how much should I trust it”: a snippet from a fork or from a comment by someone outside the organization deserves more suspicion than one from a protected branch. Batched queries, so eight lookups are one round trip. These tools sit in GitLab’s MCP catalog beside the Knowledge Graph’s, with descriptions written so an agent never has to guess which one finds and which one explains. Any metering or packaging of this endpoint is decided through GitLab’s standard processes, not here.
What great looks like. Dana’s on-call agent calls find once with the incident description and a budget, gets back ranked code and the three merge requests that mention the same service, sees that two spans came from a fork and one from a comment by a non-member, and weights them accordingly. Dana’s security review is simpler because permissions were evaluated inside the index, not by their agent.
Journey 6: The operator who pays for two copies of everything
Who. Sam runs search infrastructure for GitLab.com. Sam does not have a story so much as two copies of everything to run.
Today. Elasticsearch indexes code blobs on the default branch with tokenization that never supported exact substring search. (Tokenization is how a full-text engine splits text into words before indexing it; splitting PaymentProcessor.retry into words is exactly what you do not want for code.) Zoekt indexes the same code, also default branch only, and stores every fork and vendored copy as a separate document. Four indexing pipelines, four on-call surfaces, four unpublished lag numbers. Group code search is documented as capped at 1,000 projects, and there is no global code search on GitLab.com. And Cells is arriving to ask where the single Elasticsearch cluster goes.
What we propose. Stop indexing code in Elasticsearch on GitLab.com (proposed) once Exact Code Search is generally available; any change for self-managed instances follows GitLab’s deprecation and breaking-change process. Deduplicate across repositories so each unique file is indexed once, gated on the measured duplication ratio. A durable copy of finished shards in object storage, a later decision rather than a day-one commitment: the indexing pool writes each finished (compacted) shard to a bucket and serving replicas pull it, so a lost node is a download in minutes rather than a reindex in days, and a new replica comes up without touching Gitaly. Queries never read from the bucket, only from local SSD, and the small shards written for fresh pushes go straight to the serving replicas. One change log feeding every index. A serving pool on local SSD with namespaces sharded across nodes and indexing in its own pool, so a large customer’s queries no longer run on one node’s cores. Incremental indexing already exists (a push is indexed from the last indexed commit onward) and is not part of this ask.
What great looks like. Elasticsearch holds issues, merge requests, and comments at a fraction of today’s footprint. Zoekt’s index grows with unique content rather than with forks. Adding a Cell is a configuration change. Sam has one lag number and one pager.
Who else does this. GitHub’s content-addressed index cut its content to roughly a quarter (see Why now). If GitLab.com’s duplication ratio resembles GitHub’s published one, deduplication roughly halves the Zoekt footprint and its growth. That is a measurement rather than a claim, and it happens in the first 90 days.
Why now
Three forcing functions have converged, and none of them waits for us. Agents have made retrieval the dominant consumer of tokens in Duo, and a retriever that is fast but imprecise makes the agent worse, so this is a quality problem before it is a speed problem. Cells removes the single-cluster assumption several of our paths rest on; their routing gets rebuilt either way, and the only choice is whether we rebuild it four times under a migration deadline or once behind one contract. And the market has moved onto our ground: in February 2026 Sourcegraph repositioned itself as the intelligence layer for developers and AI coding agents and sells it to large enterprises, the same market GitLab Ultimate serves. The retrieval layer decides what an agent sees. If someone else owns it over our customers’ code, GitLab risks becoming the storage tier under someone else’s intelligence layer. Enterprise search vendors such as Glean publish per-seat pricing for permission-aware retrieval over documents and chat, which shows buyers pay for it; we hold the code, the work items, and the permission model those tools can only copy.
Agents have made retrieval the dominant consumer of tokens. A public measurement from August 2026 puts a 30B-parameter OpenHands agent (an open-source coding agent) on SWE-bench Verified (a public benchmark built from real GitHub issues) at an average of 23 rounds and 631K tokens per resolved issue, dominated by grep, glob, and file-view calls. An agent that fixes one issue spends most of its time and tokens finding the code, not changing it. The same paper reports a result this plan treats as a design constraint: retrieval helped the downstream agent only above a precision threshold. A BM25 retriever (a standard keyword-ranking method) at precision 0.375 made the agent worse; a trained retriever at 0.677 helped. Precision is the share of what a retriever returns that is actually relevant. A retriever at 0.375 precision hands the agent more noise than signal. Faster retrieval that is not precise enough is a regression. The plane’s agent gate is therefore a quality gate before it is a latency gate. It is also why several leading IDE agents grep on purpose; Anticipated questions sets out where grep wins and where it stops working.
Our own agents show the same pattern. A field-reported case from July 2026 (gitlab-org/gitlab#607004) shows a Developer flow spending many model calls to replace a single string across four files, and not many more on a materially larger change. The call count barely moved with task size, which means most of it is fixed overhead: locating files, reading them, re-reading them. That overhead is what the retrieval plane removes. Across coding flows, read, list, and search calls outnumber edits several times over, and calls to GitLab’s own API are rare. The pattern shows up in sessions that do not complete too: they are the long, tool-heavy ones that keep reading without converging, and they consume a share of tokens out of proportion to their number. A search that reports what it did not cover lets the agent widen or stop instead of looping. The measured figures behind this paragraph are on the internal evidence page (internal only).
Cells changes the routing layer anyway. Cells is GitLab’s program to split GitLab.com into many independent units, each hosting a set of customer Organizations. Under Cells, an Organization is the isolation boundary and lives in exactly one Cell. Advanced Search’s single Elasticsearch cluster and the other per-engine routing assumptions have to become organization-aware. Building that routing once, behind one contract, is cheaper than patching four pipelines under a migration deadline.
The industry has moved. GitHub rebuilt its code search on a content-addressed index (one where each unique file is stored once, no matter how many repositories contain it) and deduplicated 115 TB of content to a 25 TB index. Sourcegraph pairs Zoekt for text search with a SCIP store for precise navigation and exposes both to agents over MCP. IDE-based agents index the developer’s working tree so their retrieval sees uncommitted work. Research groups and vendors are training retrievers whose only job is to find code for a downstream agent. GitLab’s differentiated position is not any one of these techniques; it is that GitLab is the system of record for the code, the work items, the pipelines, and the permission model, so retrieval here can be permission-true rather than a synchronized copy.
What today’s code search cannot answer
A system that answers yes to all five questions below is a comprehensive search engine.
- Does it search every branch a developer or agent is working on, or only the default branch?
- Does it answer in under half a second across an organization’s code, with a published tail-latency target?
- Does it say what it did not cover, in a form a security engineer can keep?
- Is the permission check evaluated inside the engine, so counts are true and no post-filtering is needed?
- Can an agent ask for the answer within a token budget, in one round trip, and get back only what it needs?
Today the answer to all five is no.
Goals
Every goal has a baseline that is measured before its target is fixed. Where the baseline does not yet exist, the first deliverable is the baseline. The “Today” column describes GitLab.com as of September 2026; baselines that come from GitLab.com telemetry are recorded on the internal evidence page (internal only).
| # | Goal | Today (GitLab.com; measured or documented) | Target | Measure, and how we will know |
|---|---|---|---|---|
| G1 | Search sees the branch being worked on | Default branch only, for both Exact Code Search and Advanced Search (open since 2023, gitlab-org/gitlab#403307); approximately 0% of non-default refs indexed, and how many sessions run on non-default refs is not recorded anywhere today | Default, protected, release, and open-MR branches, plus an ephemeral overlay of an agent’s uncommitted work; ≥ 95% of Duo agent sessions with their working ref indexed or overlaid | Share of Duo agent sessions whose working ref is indexed or overlaid; the branches_not_indexed field in every completeness report trends to empty for active branches |
| G2 | Interactive latency | No end-to-end target for search, and no end-to-end measurement. Two thresholds exist in the public GitLab.com runbooks: a Rails-level SLI at 15.52 s, inherited from Advanced Search, and a Zoekt service Apdex at 5 s satisfied and 10 s tolerated | p50 ≤ 200 ms, p95 ≤ 500 ms for interactive code queries, p99 published, measured end to end at the gateway, within a scope and load envelope set from the baseline | Gateway-measured p50, p95, and p99 for organization-scope code queries; SLO dashboard with error budget |
| G3 | Completeness | Files over 1 MB, files past the trigram limit, and projects over 500,000 files (documented limits) are skipped without notice; result lists are capped; “no results” is indistinguishable from “backend degraded” or “invalid pattern”. 0% of responses carry a completeness block | Every response states matched, returned, skipped (with reasons), degraded engines, and index age: 100% of responses (Zoekt lane within the first 90 days, then every lane); zero ambiguous empty results | Share of responses with a completeness block; share of empty results that are ambiguous |
| G4 | Permission-true results | Rails computes the caller’s authorization set per request, embeds a superset filter, and redacts the returned page; divergence is not measured | Exact predicate evaluated inside the engine for defined rule classes; any residual rule class is reported as post-filtered, never silent; 0 divergence over 30 days of shadow traffic before cutover | Divergence between index-evaluated and Rails-evaluated results on shadow traffic; residual rule classes published and reported per response |
| G5 | Freshness and removals | Four pipelines, four unpublished lags; no published removal lag | One change log; one published lag for additions and removals, ≤ 60 s p99 for both; interim lag numbers published within the first 90 days | p99 push to searchable; p99 delete or revoke to gone; lag published beside latency |
| G6 | Result quality | Not measured; no golden set, no interleaving | No regression at cutover; agent lane precision@5 above the agent gate before any agent traffic is routed | Golden-set recall@10 and precision@5; production interleaving; weekly quality report |
| G7 | Agent efficiency | Measured internally; see the baselines (internal only) | −25% loaded tokens on resolved tasks | Loaded tokens (fresh input plus cache writes) per resolved task, and rounds, on Duo’s internal evaluation harness |
| G8 | Operations | 4 indexing pipelines; 0 published lag numbers; 4 on-call surfaces | 1 / 1 / 1 | Indexing pipelines; published lag numbers; on-call surfaces |
| G9 | Footprint | Measure | Decision gate for the content-addressed layout, not a target; measured in the first 90 days | Zoekt index growth versus unique-content growth; duplication ratio on a 2% blob sample |
| G10 | Task success | Measured internally; most flows emit no success signal today | Record a success signal for every flow in the first 90 days; target set from that baseline | Share of retrieval-heavy sessions that fail, and the tokens they consume, on Duo’s evaluation harness and in production |
| G11 | Scope | Group code search documented as capped at 1,000 projects; no global code search on GitLab.com | Organization scope; global scope is out of scope by construction under Cells | Not applicable |
| G12 | Agent interface | None. Agents grep checkouts or bind to a single engine | MCP tools: find, fetch within a token budget, symbol lookup; batched; provenance-labeled |
Tokens-to-correct-context on Duo evaluations |
Reading the table: the trigram limit skips files that contain too many distinct three-character fragments (typically minified or generated code) because they bloat the index. An authorization set is the list of projects a particular user may read; today Rails computes it on every request and passes it to the engine as a coarse filter, then re-checks the returned page. Golden set and interleaving are the two standard ways of measuring whether search results are actually good; both are explained in the technical design under Ranking and quality. Recall@10 is the share of the correct answers that appear in the top ten results; precision@5 is the share of the top five results that are correct.
Non-Goals
Saying what the plan does not do is as important as saying what it does, because the fastest way to under-fund this work is to assume some other system covers it.
- A new vector database, or moving semantic search to the primary path. (Semantic search finds code by meaning rather than exact text; it stays as one lane among several.)
- Moving search into ClickHouse. Query telemetry and analytics follow the existing aggregation engines guidance; search itself does not.
- Rewriting Advanced Search for issues, merge requests, and comments. Elasticsearch stays for the text it is good at.
- Training a retrieval or ranking model within this plan’s scope. See Privacy and data handling; any such work is a separate proposal subject to GitLab’s published AI data-usage documentation.
- Modeling relationships across the software lifecycle or answering multi-hop questions (blast radius of a change across services, pipelines, and owners). That is the Knowledge Graph’s job; this plan does not depend on it, change it, or route to it.
- Global, cross-organization code search on GitLab.com. Under Cells, Organizations are isolated by design.
- History search (“since when”). It is a candidate for the next phase behind the same contract.
- Replacing Zoekt. See Telescope: the engine underneath.
Proposal
Telescope: the retrieval plane
Think of a database. You write a query that says what you want; the database decides which index to use. You never think about the data structures underneath. The retrieval plane does the same for search. Every caller asks in whatever it already speaks: a search string, a GLQL filter, a plain-English question (the Ask surface in Journey 4), a typed tool call. All of it translates into one request contract. The plane decides which engines to ask, evaluates the caller’s permissions once, inside the engines, merges the results by rank, cuts them to the caller’s budget, and reports exactly what it covered. The engines become implementation details behind a contract.
The two diagrams show the shape of the change. Today, each way of asking is wired to its own engine, and each wire carries its own permission check. In the proposed design, everything goes through the plane.
flowchart LR
subgraph today["Today: five paths, five permission implementations, four indexing pipelines"]
UI[Search UI] --> PG[(PostgreSQL)]
UI --> ES[(Elasticsearch)]
UI --> ZK[(Zoekt)]
GLQL[GLQL] --> PG
GLQL --> ES
CHAT[Duo Chat] --> SEM[(Semantic index)]
AP[Agent Platform] --> KG[(Knowledge Graph)]
AP -. "grep in the checkout" .-> WT[Working tree]
end
flowchart LR
subgraph proposed["Proposed: one plane, one contract, one completeness report"]
UI2[Search UI] --> RP
GLQL2[GLQL] --> RP
ASK[Ask] --> RP
DUO[Duo agents] --> RP
CUST[Customer agents over MCP] --> RP
RP["Retrieval plane: authorization, predicate, budget, completeness"] --> ZK2[(Zoekt: every relevant branch)]
RP --> SYM[(Symbol index: SCIP)]
RP --> ES2[(Elasticsearch: issues, MRs, comments)]
RP --> PG2[(PostgreSQL fallback)]
RP --> SEM2[(Semantic index lane)]
RP -. "per session" .-> OV[Session overlay]
end
DUO --> KG2
CUST --> KG2
KG2[("Knowledge Graph: a peer system for relationship questions, not behind the plane")]
KG2 -. "one shared SCIP symbol store" .- SYM
Figure 1. Five retrieval paths today, one plane proposed.
How this fits with what GitLab already has
GitLab already has several things with “search” or “context” in their description. None of them does what this plan does. The table lists each one, what it does well today, what it cannot do, and what this plan does with it.
| What we have | What it does today | What it does not do | What this plan does with it |
|---|---|---|---|
| Basic search (PostgreSQL) | Issues, MRs, users, projects; code within one project | Code search across a group or organization | Stays as the always-available fallback engine behind the contract |
| Advanced Search (Elasticsearch) | Ranked full-text search over issues, MRs, comments, commits, and default-branch code | Exact strings in code (it tokenizes into words); anything off the default branch | Keeps issues, MRs, and comments. Code indexing stops on GitLab.com once Exact Code Search is generally available; any change for self-managed instances follows GitLab’s deprecation and breaking-change process |
| Exact Code Search (Zoekt) | Exact substring and regular-expression search over default-branch code | Other branches; the documented size, trigram, and file-count limits (see Goals); reporting what it skipped; any published end-to-end latency target | Becomes the primary code engine: every relevant branch, deduplicated, true counts, half-second target |
| Semantic code search | Meaning-based retrieval over code chunks within one repository, for Duo | Organization scope; short keyword queries | Kept as a lane for the narrow jobs where it wins; not the primary path |
| GLQL | Structured filters over work items, MRs, pipelines, and projects; live embedded views | Code | Stays the language of work items; its filters compile to the plane’s contract instead of talking to engines directly, while sorting and aggregation stay in GLQL for now; gains code results in live views |
| Knowledge Graph (GitLab Orbit) | Models relationships across the lifecycle (which MR closed which issue, what calls what); holds cross-project definitions and import-level references from the default branch; exposes graph tools over MCP | Substring or regex search over code; ranked full text; per-push freshness; true counts under a user’s permissions | Untouched and not depended on. The plane and the graph consume one SCIP symbol store |
| Duo Chat and Agent Platform | The agents that ask the questions | Retrieval of their own; each tool binds to one engine | Become the plane’s first and largest consumers through one contract |
A lane is one engine’s ranked results, shown side by side with other engines’ results rather than blended into one list; the reason for that is explained in the technical design under Ranking and quality. SCIP is an open file format that language tools produce to describe a codebase’s symbols precisely: this name at this position defines this class, that name references it.
Every organization that has built this at scale kept the search engine and the relationship graph as separate systems and joined them in the product: a trigram or n-gram index beside a graph of code facts, with the user seeing both on one screen. This plan follows that pattern rather than inventing one.
Design and implementation details
The engineering design lives on its own page, Telescope: Technical design. It follows the path of a request and then the path of the data:
- The request contract: what a caller sends and what every response carries, including the completeness block and the coverage record.
- Authorization inside the index: three rule classes, location-level evaluation, 30 days of shadow traffic, and the rule against silence.
- Indexing policy: which branches, the session overlay and its three topologies, deduplication, and skip accounting.
- Symbols, freshness and the change log, ranking and quality, and the agent interface.
- Capacity, sizing, and hardware, the latency budget, SLOs and observability, the security and threat model, and privacy and data handling.
Legal, privacy, and security reviews follow GitLab’s existing processes: the Legal Risk Checklist (internal only) at the design and plan stages of the product development flow, a privacy review before any query-log collection, and an application security review and threat model before the authorization cutover. The uploaded-overlay topology and any model inference in ranking each trigger their own review before implementation.
Four decisions are requested of reviewers, and each is argued in the technical design:
- One SCIP symbol store with one owning team, consumed by both the plane and the Knowledge Graph, rather than two SCIP-derived stores.
- One change log, seeded from the existing Zoekt task queue and moved off the main GitLab.com database primary before branch indexing leaves dogfooding.
- A durable copy of compacted shards in object storage as a Phase 2 decision (the phases are defined in the technical design), for recovery and distribution only, never for query-time tiering.
- The content-addressed index layout as a gate decided by the duplication ratio measured in the first 90 days, not as a day-one commitment.
Alternative Solutions
Roads not taken, and why. The first row is the important one: it is what happens by default if this plan is not funded.
| Alternative | Why not, or why not yet |
|---|---|
| Incremental fixes inside each engine, no plane | Fixes the symptoms in five places; leaves five permission paths and four pipelines to re-plumb for Cells; no completeness, no contract, no agent interface. This is the default path and the reason this document exists |
| Make Elasticsearch the single engine | Exact substring and regex over code, per-branch freshness at scale, and cost per byte are structurally poor fits for a word-tokenized full-text engine; the 2021 to 2023 history of exact-match issues is the evidence |
| Make the Knowledge Graph the search layer | A graph store answers relationship queries; a text index answers where-is-it queries. Asking one to do the other means building a search engine inside a database designed for something else. Every organization that has built both at scale kept them separate and joined them in the product |
| Adopt an external code-search product | An external product typically synchronizes permissions from the code host rather than evaluating them at the source of truth, does not see GitLab work items and pipelines beside code unless separately integrated, and has to be packaged for self-managed and Dedicated. Some techniques are worth adopting (Zoekt, SCIP, MCP exposure); the product is not |
| Build a content-addressed engine now | GitHub’s engine took a dedicated team several years. The two problems it solves (cross-repository duplication and trigram selectivity) are measured, not assumed, in the first 90 days, and the contract exists so that the engine can change later without redoing the plane |
| Semantic (vector) retrieval as the primary path | Weak for exact and navigational queries, which are likely the head of the query mix; useful as a lane. The 90-day query-mix measurement decides how much of a lane |
| Search in ClickHouse | Designed for analytical aggregation, not substring and regex over code with per-push freshness; the aggregation engines guidance covers where ClickHouse belongs |
Anticipated questions
Are customers telling us to build better code search?
Customers ask for the things that search blocks, and they have been asking in writing for three years. In August 2026 an enterprise with tens of thousands of repositories opened an issue saying they want to trust GitLab code search as a replacement for a dedicated code search product, and listing why they cannot yet: results capped at 5,000 when they have 10,000 legitimate matches, non-default branches disabled, backend failures that look identical to “no results.” Non-default-branch search has been open since 2023 and is still active. Exact-match search was, in our own blog’s words, one of our most requested features, and the Zoekt beta was filled by customers asking to join. Search usage on GitLab.com is growing quickly, and not only from machines; the figures are on the internal evidence page (internal only).
Do we know if users are unhappy with code search today?
We do not measure per-user retention, zero-result rate, or relevance for search at all, and that gap is the first thing the plan fixes. But the one user we can measure is our own agents, and they route around search: coding agents rarely call GitLab’s API for retrieval and grep the checkout instead. When agents cannot find code, customers tell our field teams they hesitate to keep using them, and the field team has filed that concern.
Don’t the best coding agents grep on purpose?
Yes, and inside one checkout they are right to. Several leading agents chose grep over an index deliberately: a working tree is always fresh, grep never misreports its coverage, and a retriever that is not precise enough makes the agent worse, which is the 0.375 threshold in Why now. That is the case for grep, and this plan accepts it. Grep falls short in four situations: it searches the one repository on disk rather than the two thousand a large customer’s code lives in; it knows nothing about permissions, so anything built on it either over-shares or re-checks every result after the fact; it cannot answer “where is this defined” or “who calls it” without reading every candidate file, which is the read loop a symbol index replaces with one lookup; and when every token is metered, reading files to discover they are irrelevant is among the most expensive things an agent does. The plane does not take grep away from the agent. It gives the agent a first call that is precise enough to clear the agent gate, scoped to what the caller may see, and cut to the caller’s budget, so grep becomes the last mile inside the sandbox rather than the whole search.
Doesn’t the Knowledge Graph already do this?
It does something adjacent and valuable: it models how things across the lifecycle are connected, and it holds cross-project definitions and import-level references from the default branch. It is not a text index, so it cannot search for an exact string across an organization’s code, rank full text, return a true count under a user’s permissions, or reflect a push within a minute. The symbol index differs by being compiler-precise and one-hop-fast, which is why this document asks for one shared SCIP store rather than two.
Isn’t this just an Elasticsearch upgrade?
No. Elasticsearch stays for the text it is good at. What does not exist today is the layer above every engine: one contract, one permission predicate inside the index, one completeness report, and one set of typed tools for agents. That layer is what this proposal adds.
Why Zoekt?
We have not found a better engine available to buy or in open source. Google, GitHub, and Meta each built their own code-search systems, and none is available to license. Most of today’s limitations in GitLab’s code search are in how Zoekt is configured, deployed, and wrapped, not in Zoekt’s design; see Telescope: the engine underneath.
Sources
Public documentation behind the positioning table, as of September 2026.
- GitHub: About GitHub Code Search and The technology behind GitHub’s new code search (February 2023).
- Sourcegraph: Code Search, precise code navigation, repository permissions, Keeping it boring (and relevant) with BM25F (April 2025), and the MCP server.
- Cursor: codebase indexing.
- GitLab: Exact Code Search, Advanced Search, and the GitLab Orbit design document.
Related design documents
- Use Zoekt for code search: the engine this plan builds on.
- GitLab Orbit: the Knowledge Graph, a peer system that shares the SCIP symbol store. The Orbit team’s own code-retrieval work is in orbit-next and the code indexing design; which system owns which half of retrieval is a question this document expects to settle with that team rather than assert.
- AI Context Abstraction Layer and AI Context Management: earlier work on context retrieval for Duo.
- Retrieval Augmented Generation for GitLab Duo on self-managed and Codebase as Chat Context.
- Package a search service with GitLab.
- Cells: the isolation boundary the plane’s routing has to respect.
Authors, sponsors, and reviewers
- Author of this design document: Saurabh Chopra (
@chopras). - Executive sponsors and the reviewers of the source proposal are listed on the internal evidence page (internal only).
Telescope: the engine underneath
7bf2693c)
