Qdrant、ブランチ対応のセマンティックコード検索機能を公開
本文の状態
日本語全文を表示中
詳細モードで約11分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
Qdrant Blog
Qdrant はベクトルインデックスがブランチ情報を保持しない問題を解決し、Git の履歴構造を反映した「ブランチ対応型意味コード検索」の仕組みと実装方針を公開する。
AI深層分析を開く2026年8月4日 12:11
AI深層分析
キーポイント
バージョン不一致の問題定義
従来のベクトルインデックスは特定のブランチに紐付かないため、開発中の機能ブランチから検索してもメインブランチの最新バージョンが返され、コードの実行環境と矛盾する結果を招く。
ブランチ対応型検索の仕組み
各クエリを特定のブランチのライブビューにスコープし、そのコミット履歴と祖先からの継承分、および後続コミットによる置換分を考慮して正確なバージョンを特定する。
インデックス構築の設計方針
ファイルサイズや固定ウィンドウではなくコード構造に基づいてチャンク化し、Git ツリー内のドキュメント、設定、スキーマ、コード全体に適応可能な追跡・同期メカニズムを提案する。
構造化パーサーによるコード分割
tree-sitterなどのパーサーで関数単位ではなく、クラスやメソッドなど名前付き宣言を単位として分割することで検索精度が向上する。
ブランチ追跡におけるIDの重要性
ブランチごとのバージョンを追跡するには、コミット間を通じて安定したID(パスとシンボルの組み合わせ)が必要であり、スライディングウィンドウでは不十分である。
重要な引用
A vector index is built once, detached from any checkout. It doesn't know which branch you're on, so it answers from whatever it indexed.
Branch-aware search scopes each query to one branch's live view: its own commits, plus what it inherited from its ancestors, minus anything a later commit replaced.
For branch-aware search, identity decides.
A codebase is more than functions, so the unit is any named declaration: methods, classes, structs, enums, constants, and the rest.
編集コメントを表示
編集コメント
AI エージェントがコードベースを扱う際、文脈の正確性は極めて重要であり、ブランチ情報を無視した検索は重大な誤動作の原因となる。Qdrant が提示するこの解決策は、開発フローと AI ツールの統合における信頼性を高める重要な一歩である。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
コード検索の多くは語彙ベースです。文字列を検索したり定義にジャンプしたりしますが、これはチェックアウトされた環境で動作するため、常に開いているブランチの内容を反映します。
一方、セマンティック(意味的)なコード検索はさらに一歩進んでいます。コードベースをベクトルとしてインデックス化し、意味に基づいて検索を行います。これにより、AI エージェントに対して、長い grep ループの代わりに、必要なコンテキストをワンストップで提供することが可能になります。
しかし、このアプローチには grep にはない新たな課題が伴います。
ベクトルインデックスは一度構築されると、特定のチェックアウトとは切り離された状態になります。そのため、現在どのブランチにいるかという文脈を理解できず、インデックスに登録されている内容から回答を返すことになります。もし main ブランチからインデックスを作成した場合、1 ヶ月前の機能ブランチからのクエリに対しても、そのブランチですでに書き換えられた関数の main 版が返されてしまいます。
結果は正しく見えますが、実際には間違ったバージョンを参照していることになります。読者のブランチには存在しない関数シグネチャを指し示すようなケースです。コーディングエージェントはこの情報を現在のコードとして扱ってしまい、誤った判断を下す可能性があります。
ブランチ対応型の検索では、各クエリを「そのブランチのライブビュー」に限定します。つまり、そのブランチ固有のカムットに加え、祖先から継承された変更分を含めつつ、後続のカムットによって置き換えられた部分は除外した状態です。この考え方は、ドキュメント、設定ファイル、スキーマ、コードなど、Git ツリー内に保存されるあらゆるデータに適用可能です。
実装の詳細については Branch-Aware Search のチュートリアル をご覧ください。本記事では、その背後にある意思決定について解説します。何をインデックス化すべきか、バージョン間をまたいでチャンクを追跡する方法、そして Git との同期をどう保つかといったポイントです。
バージョン不一致の問題
Qdrant 自身の Rust コードベースにおける実際の例を見てみましょう。あるコミットで optimizer_thresholds 関数に引数が追加されましたが、その結果、同じ関数でもブランチによって解決されるバージョンが異なります。

本ブランチ(main)には新しい引数が含まれていますが、リリースブランチ(release/v1.15)ではその変更が適用される前に分岐しているため、該当しません。変更前に分岐して関数を編集した機能ブランチには、図に示されているような第 3 のバージョンが存在します。
リリースブランチ上のエージェントは短い呼び出しを必要としますが、main ブランチから構築されたベクトルインデックスは長い呼び出しを返してしまいます。同じ関数でもブランチによって異なる実装があり、複数の答えが存在する状況では、単純なインデックスではそれらを区別することができません。
インデックス化する対象の選定
最初の判断は、どの部分を「チャンク」として扱うかです。つまり、1 つのポイントとして認識される範囲をどう定義するかです。ここには 2 つの重要な考え方があります。
サイズではなく構造でチャンク化する。 全ファイル単位でベクトル化すると多くの関数が混ざり合い、ちょっとした編集でもファイル全体が再バージョンされてしまいます。固定サイズのウィンドウでは関数が半分に切れてしまう可能性があります。tree-sitter などのパーサーを使ってコードの構造に沿って分割すれば、これらの問題を防ぎ、検索精度も向上します(詳細は cAST を参照)。コードベースは関数だけではありません。単位となるのはメソッド、クラス、構造体、列挙型、定数など、名付けられた宣言すべてです。設定ファイルやドキュメントといった非コード部分は、それぞれのセクションごとに分割します。Qdrant の code-search tutorial ではこの手順を詳しく解説しています。
ブランチ対応検索では「同一性」が鍵となります。 ブランチ対応検索は、各バージョンを一意の ID で追跡するため、チャンクには安定した識別子が必要です。名付けられた宣言にはそれが備わっています。つまり、コミット間を通じてたどれるパスとシンボルです。スライディングウィンドウ方式ではこの同一性を保証できないため、ここでは名前付きユニットの方が適しています。
バージョンを跨ぐ関数の追跡
ブランチ対応検索では、各チャンクのバージョンを追跡します。「どのブランチで記述されたか」「どの後のコミットで置き換えられたか」です。これにはバージョン間を通じて安定した同一性が必要です。ファイルの場合の識別子はパスですが、関数であれば「パス+修飾されたシンボル名」(例:optimizers_builder.rs::optimizer_thresholds)となります。
この同一性を定義するのは、一見すると簡単そうに見えて実は三つのケースが絡み合っています。
オーバーロードと重複する名前。 同じ裸の名前は、シグネチャでオーバーロードされたメソッドや、異なる型間で同じ名前を共有するメソッドなど、複数の別々の宣言にマッピングされることがよくあります。パスと裸の名前だけでは一意ではありません。名前のみではなく、パーサーまたは言語サーバーから取得した、名前空間(または型)とシグネチャを含む完全修飾されたシンボルIDを使用してください。
移動。 別のファイルへ移動する関数はパスが変更されるため、パスベースのIDでは新しいものとして扱われ、履歴が失われます。系譜(リンケージ)が重要であれば、Git が移動を検出する方法のように、パスだけでなくコンテンツの類似性によってIDを解決する必要があります。
名前の変更。 名前の変更は「削除+追加」と見なされます。これは最もシンプルなオプションですが、類似性によって検出し、バージョン履歴を引き継ぐことも可能です。
これは Git 自体がヒューリスティック(経験則)でしか解決できない問題と同じです。誠実なアプローチは、安定したIDを選択し、名前の変更や移動をどこまで追跡するかを決め、その境界線を文書化することです。
Git とインデックスの同期を保つ
Qdrant が保持するのは派生されたインデックスであり、Git が唯一の真実源(ソース・オブ・トゥルース)です。コレクションはブランチ履歴を再生することで構築されますが、生のコミットグラフではなく、ブランチイベントログから再生するのが理想です。
各ポイントの ID をブランチ、コミット、チャンクの識別子から導出すれば、その再生は冪等性を持ちます。つまり、コミットの識別子が保たれていれば、再ビルドしても重複せずきれいに上書きされます。ただし、リベースやフォースプッシュはこの前提を崩します。これらはコミットの識別子を改ざんする行為だからです。影響を受けたブランチのポイントを一時的に削除してから再生するか、古いバージョンが残り続けるリスクがあります。
マージや並列書き込みについてはそれぞれ個別の判断が必要ですが、ここでは核心となるメカニズムを明確にするため、これらのケースは対象外とします。
リポジトリが大きくなるにつれて、2 つの要素が異なるペースでスケールします。ストレージ容量は編集の数に比例して増えます。上書きされたバージョンもポイントとして残るため、長期存在するブランチでは履歴が蓄積されていきます。ただし、プルーニング(不要データの削除)はブランチを認識した上で実行されます。あるブランチが置換したバージョンでも、以前に分岐した別のブランチにとってはまだ有効な場合があるからです。したがって、どのブランチからも参照されなくなったバージョンのみを削除します。
一方、フィルターのサイズは小さく保たれます。その大きさはインデックス化されたコーパス全体ではなく、ブランチの系譜(ラインジ)の深さに依存するため、履歴が深くてもフィルター自体はコンパクトに収まります。
仕組みの可視化
各バージョンは、そのコードを書いたブランチ名、ブランチ固有のコミット番号、そして後続のどのコミットによって上書きされたかというレコードを含む小さなペイロードを持つ「ポイント」として扱われます。単一のペイロードフィルタがブランチの祖先をトレースし、現在有効なバージョンのみを保持して、既に上書きされたものを除外します。セマンティック検索はこの上に重ねられ、クエリ実行時にメタデータフィルタとベクトル検索を組み合わせて行います。これが実務における「コンポーザブル・リトリーバル(合成可能な検索)」の具体的な姿です。
実際に動作を確認するために、Qdrant 自身の lib/collection ソースコードから 141 個の関数を抽出し、ローカルの Qdrant インスタンスにインデックス化しました。各関数は 1 ポイントとして登録され、それぞれのポイントには前述のペイロード(ブランチ名、コミット番号、上書き記録)が付与されています。

同じクエリを各ブランチごとにスコープして実行すると、そのブランチに対応するバージョンが返されます。
Query: "how are segment optimizer thresholds for indexing and memmap computed"
main -> optimizer_thresholds(&self, num_indexing_threads, deferred_internal_id)
release -> optimizer_thresholds(&self, num_indexing_threads)
1 つのクエリで 1 つのコレクションから、2 つのブランチそれぞれの結果を取得できます。各結果は、該当ブランチで動作しているバージョンにスコープされています。英語の質問とコードのマッチングはエンベディングモデルの役割であり、フィルタリングの役目ではありません。今回は汎用モデルでも機能しましたが、自然言語によるコード検索を一般化して行う場合は、コード埋め込みモデルを使用するか、各チャンクにシンボル名やシグネチャ情報を追加してエンリッチメントを行うことを推奨します。
Build It
Branch-Aware Search のチュートリアル では、単一ファイルから始まり、クエリごとに可視性フィルターを適用してマルチブランチコレクションを構築するまでの実装手順を、段階を追って解説しています。この例では理解しやすい小規模なドキュメントコーパスを対象にしていますが、同様のフィルターはコード検索にもそのまま適用可能です。
セマンティック・コード検索が初めてという方は、まず Semantic Search for Code をご覧になり、その上でブランチ対応機能を追加することをお勧めします。
このパターン全体を、無料の Qdrant Cloud クラスター上で実行できます。これにより、各ブランチがアクセスできる正確なバージョンごとにクエリをスコープ限定することが可能になります。
原文を表示
Most code search is lexical. You grep a string or jump to a definition, and because it runs on your checkout, it always reflects the branch you have open. Semantic code search goes further: you index the codebase as vectors and search by meaning, which is how you hand an AI agent the right context in one lookup instead of a long grep loop. But it brings a problem grep never has.
A vector index is built once, detached from any checkout. It doesn’t know which branch you’re on, so it answers from whatever it indexed. Build it from main, and a query from a month-old feature branch still gets main’s version of a function that branch already rewrote. The result looks right, but it’s the wrong version: a function signature the reader’s branch doesn’t have. A coding agent will reference it as if it were current.
Branch-aware search scopes each query to one branch’s live view: its own commits, plus what it inherited from its ancestors, minus anything a later commit replaced. The same idea fits anything kept in a Git tree, whether docs, configuration, schemas, or code. The Branch-Aware Search tutorial covers the implementation. This post covers the decisions behind it: what to index, how to track a chunk across versions, and how to keep the index in sync with Git.
The Wrong-Version Problem
Here is a real change from Qdrant’s own Rust codebase. The function optimizer_thresholds gained an argument in one commit, so the same function resolves differently depending on the branch.

On main, it carries the new argument. On release/v1.15, cut before that change, it never did. A feature branch that forked before the change and edited the function would carry a third version, like the one the diagram sketches. An agent on the release branch needs the shorter call, but a vector index built from main returns the longer one. One function, several branches, several answers, and a plain index can’t tell them apart.
Choosing What to Index
The first decision is the chunk: what becomes one point. Two ideas carry most of the way.
Chunk by structure, not by size. Whole-file vectors blur many functions together and re-version the whole file on any edit. Fixed-size windows cut functions in half. Splitting along the code’s structure with a parser like tree-sitter avoids both, and it measurably helps retrieval (see cAST). A codebase is more than functions, so the unit is any named declaration: methods, classes, structs, enums, constants, and the rest, with non-code like config or docs split by their own sections. Qdrant’s code-search tutorial walks through it.
For branch-aware search, identity decides. Branch-aware search tracks each version by identity, so the chunk needs a stable one. A named declaration has it: a path plus symbol you can follow across commits. A sliding window doesn’t, so named units are the better choice here.
Tracking a Function Across Versions
Branch-aware search tracks each chunk’s versions: which branch wrote it, and which later commit replaced it. That needs a stable identity across versions. For a file, the identity is its path. For a function, it is the path plus the qualified symbol name, like optimizers_builder.rs::optimizer_thresholds.
Three cases make that identity harder than it looks.
Overloads and repeated names. The same bare name often maps to several distinct declarations: methods overloaded by signature, or methods that share a name across different types. Path plus bare name isn’t unique. Use a fully qualified symbol identity from a parser or language server (namespace or type, plus signature), not the name alone.
Moves. A function that moves to another file changes its path, so a path-based identity treats it as new and loses its history. If lineage matters, resolve identity by content similarity, the way Git detects moves, rather than by path alone.
Renames. A rename looks like a delete plus an add. You either accept that, which is the simplest option, or you detect it by similarity and carry the version history across.
This is the same problem Git itself only solves with heuristics. The honest approach is to pick a stable identity, decide how far you will chase renames and moves, and document where the line sits.
Keeping the Index in Sync with Git
Qdrant holds a derived index; Git is the source of truth. You build the collection by replaying branch history, ideally from a branch event log rather than the raw commit graph. Deriving each point’s ID from its branch, commit, and chunk identity makes that replay idempotent: as long as commit identity holds, a rebuild overwrites cleanly instead of duplicating. Rebases and force-pushes break that, since they rewrite commit identity: delete the affected branch’s points before replaying, or stale versions linger. Merges and concurrent writers are each their own decision; the tutorial scopes them out to keep the core mechanic clear.
Two things scale differently as the repo grows. Storage grows with edits: every superseded version stays as a point, so a long-lived branch accumulates history. Pruning is branch-aware, though: a version one branch replaced may still be live for a branch that forked earlier, so only drop versions no active branch can see. The filter, by contrast, stays small: its size tracks a branch’s lineage depth, not the indexed corpus, so even a deep history produces a compact filter.
Seeing It Work
Each version becomes one point with a small payload: the branch that wrote it, a per-branch commit number, and a record of which later commit replaced it. A single payload filter walks the branch’s ancestry, keeps the versions in its live view, and drops anything superseded. The semantic query rides on top: a metadata filter and a vector query combined at query time, which is what composable retrieval means in practice.
To see it in action, we indexed 141 functions from Qdrant’s own lib/collection source into a local Qdrant instance, one point per function. Every point carries that payload: its branch, commit number, and overwrite record.

The same query, scoped to each branch, returns that branch’s version:
Query: "how are segment optimizer thresholds for indexing and memmap computed"
main -> optimizer_thresholds(&self, num_indexing_threads, deferred_internal_id)
release -> optimizer_thresholds(&self, num_indexing_threads)
One query, one collection, two branches, each result scoped to the version that branch runs. Matching an English question to code is the embedding’s job, not the filter’s: a general-purpose model worked here because the query shares vocabulary with the function, but for natural-language code search in general, use a code embedding model or enrich each chunk with its symbol name and signature.
Build It
The Branch-Aware Search tutorial walks through the full implementation step by step, from a single file to a multi-branch collection with the visibility filter that scopes every query. It builds the example on a small documentation corpus, where the mechanics are easiest to follow, but the same filter applies unchanged to code. New to semantic code search? Start with Semantic Search for Code, then layer branch-awareness on top.
You can run the whole pattern on a free Qdrant Cloud cluster, scoping every query to the exact version a branch sees.
AI算出
技術分析ainew評価標準
AI エージェントへの文脈提供を改善する Qdrant のブランチ対応検索機能は、単なる製品アップデートではなく、コードベースのベクトルインデックス化と Git 履歴の同期という具体的な技術的課題に対する解決策を示しており、開発者にとって実装知見として価値が高い。
6つの評価軸を見る
- AI関連度
- 75
- 情報源の信頼性
- 25
- 新規性
- 75
- 調べる価値
- 75
- 重複の少なさ
- 100
- 日本での有用性
- 25
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み