コンテンツへスキップ

大規模コードベースを安全にインデックス化する

by Jeremy Stribling研究
大規模コードベースを安全にインデックス化する

セマンティック検索は、エージェントのパフォーマンスを左右する最大の要因のひとつです。最近の評価では、平均で 12.5% の応答精度向上につながり、コードベースに残りやすいコード変更を生成し、リクエスト全体の満足度も向上させました。

セマンティック検索を実現するために、Cursor はプロジェクトを開いたときにコードベースの検索可能なインデックスを構築します。小規模なプロジェクトでは、これはほぼ瞬時に完了します。しかし、数万ファイル規模の大規模リポジトリを単純にインデックスしようとすると処理に数時間かかる場合があり、その作業の少なくとも 80% が終わるまでセマンティック検索は利用できません。

私たちは、「ほとんどのチームは同じコードベースのほぼ同一のコピーで作業している」というシンプルな事実に基づいて、インデックス作成を高速化する方法を探しました。実際、同じコードベースのクローンは、同一組織内のユーザー間で平均 92% の類似度があります。

これはつまり、新しいメンバーが参加したりマシンを切り替えたりするたびに、毎回インデックスを一から作り直すのではなく、チームメイトの既存インデックスを安全に再利用できるということです。これにより、最大規模のリポジトリでも、最初のクエリが実行できるまでの時間を数時間から数秒に短縮できます。

最初のインデックスの構築

Cursor はコードベースの最初のビューを Merkle tree を使って構築します。これにより、すべてを再処理せずに、どのファイルやディレクトリが変更されたかを正確に検出できます。Merkle tree には各ファイルの暗号学的ハッシュと、その子要素のハッシュに基づいて計算される各フォルダのハッシュが含まれます。

クライアント側で小さな編集が行われると、変更されたファイル自体のハッシュと、コードベースのルートまでの親ディレクトリのハッシュだけが変わります。Cursor はそれらのハッシュをサーバー側のバージョンと比較し、2 つの Merkle tree がどこで分岐しているかを正確に特定します。ハッシュが異なるエントリは同期され、一致するエントリはスキップされます。クライアントに存在しないエントリはサーバーから削除され、サーバーに存在しないエントリは追加されます。同期プロセスがクライアント側のファイルを変更することは決してありません。

Merkle tree のアプローチにより、各同期で転送が必要なデータ量が大幅に削減されます。5 万ファイルあるワークスペースでは、ファイル名と SHA-256 ハッシュだけでおよそ 3.2 MB になります。ツリーがなければ、そのデータを更新のたびに転送する必要があります。ツリーがあれば、Cursor はハッシュが異なる枝だけをたどります。

ファイルが変更されると、Cursor はそのファイルを構文単位のチャンクに分割します。これらのチャンクは、セマンティック検索を可能にする埋め込み (embedding) に変換されます。埋め込みの生成はコストの高いステップであり、そのため Cursor はこれをバックグラウンドで非同期に実行します。

多くの編集では、ほとんどのチャンクは変更されません。Cursor はチャンクの内容ごとに埋め込みをキャッシュします。変更されていないチャンクはキャッシュにヒットし、推論時にそのコストを再度支払うことなく、エージェントの応答を高速に保てます。こうして得られるインデックスは、更新も速く、保守も軽量です。

再利用に最適なインデックスの特定

上記のインデックス作成パイプラインは、コードベースが Cursor にとって新しい場合、すべてのファイルをアップロードします。ただし、組織内の新しいユーザーは、そのプロセス全体を経る必要はありません。

新しいユーザーが参加したとき、クライアントは新しいコードベースに対して Merkle tree を計算し、そのツリーから類似度ハッシュ(simhash)と呼ばれる値を導出します。これは、そのコードベース内のファイルコンテンツハッシュを要約した単一の値です。

クライアントは simhash をサーバーにアップロードします。サーバーはそれをベクターとして使用し、同じチーム(または同一ユーザー)内の他のすべてのインデックスについて、現在のすべての simhash から構成されるベクターデータベースを検索します。ベクターデータベースから返される各結果について、その類似度ハッシュがクライアントのものと、所定のしきい値を超える類似度で一致するかどうかを確認します。一致する場合、そのインデックスを新しいコードベースの初期インデックスとして使用します。

このコピー処理はバックグラウンドで行われます。その間も、クライアントはコピー元のオリジナルインデックスに対して新しいセマンティック検索を実行できるため、クライアント側の初回クエリまでの時間を非常に短くできます。

ただし、これは 2 つの制約が成り立つ場合にのみ機能します。コピー元インデックスと異なる場合でも、結果はユーザーのローカルコードベースを反映している必要があります。そして、クライアントは、自分がすでに持っていないコードに対する結果を決して見ることができてはなりません。

アクセス証明

コードベースのコピー間でファイルが漏洩しないよう保証するために、Merkle tree の暗号学的な特性を再利用します。

ツリー内の各ノードは、その直下にあるコンテンツの暗号学的ハッシュです。そのハッシュは、対応するファイルを実際に保持している場合にのみ計算できます。ワークスペースがコピーされたインデックスから起動されると、クライアントは類似度ハッシュとともに自身の Merkle tree 全体をアップロードします。これにより、コードベース内の各暗号化されたパスにハッシュが関連付けられます。

サーバーはこのツリーをコンテンツの証明情報の集合として保存します。検索時に、サーバーはそれらのハッシュをクライアント側のツリーと照合して結果をフィルタリングします。クライアントがあるファイルを保持していることを証明できない場合、その結果は除外されます。

これにより、クライアントはすぐにクエリを実行でき、コピーされたインデックスと共有しているコードに対する結果だけを確認できます。バックグラウンド同期が残りの差分を突き合わせます。クライアントとサーバーの Merkle tree のルートが一致すると、サーバーはコンテンツの証明情報を削除し、その後のクエリは完全に同期されたインデックスに対して実行されます。

より高速なオンボーディング

チームメンバーのインデックスを再利用することで、あらゆる規模のリポジトリでセットアップ時間が短縮されます。リポジトリが大きくなるほど、その効果も大きくなります:

  • 中央値のリポジトリでは、最初のクエリまでの時間が 7.87 秒から 525 ミリ秒に短縮されます

  • 上位 10% のリポジトリでは、2.82 分から 1.87 秒に短縮されます

  • 上位 1% の非常に大きなリポジトリでは、4.03 時間から 21 秒に短縮されます。

これらの変更により、大きな重複作業の原因が取り除かれ、Cursor は非常に大規模なコードベースでも、数時間ではなく数秒で把握できるようになります。

分類: 研究

作成者: Jeremy Stribling

大規模コードベースを安全にインデックス化する · Cursor