Große Codebasen sicher indizieren

von Jeremy Stribling in Forschung
Große Codebasen sicher indizieren

Semantische Suche ist einer der wichtigsten Treiber der Agent-Performance. In unserer jüngsten Evaluation verbesserte sie die Antwortgenauigkeit im Durchschnitt um 12,5 %, führte zu Codeänderungen, die mit höherer Wahrscheinlichkeit in Codebasen beibehalten wurden, und steigerte die allgemeine Zufriedenheit mit den Anfragen.

Um semantische Suche zu ermöglichen, erstellt Cursor beim Öffnen eines Projekts einen durchsuchbaren Index deiner Codebasis. Bei kleinen Projekten geschieht das nahezu sofort. Aber große Repositories mit Zehntausenden von Dateien können bei naiver Indizierung Stunden in Anspruch nehmen, und semantische Suche steht erst zur Verfügung, wenn mindestens 80 % dieser Arbeit abgeschlossen sind.

Wir haben nach Möglichkeiten gesucht, die Indizierung zu beschleunigen, basierend auf der einfachen Beobachtung, dass die meisten Teams mit nahezu identischen Kopien derselben Codebasis arbeiten. Tatsächlich weisen Klone derselben Codebasis innerhalb einer Organisation über alle Nutzer hinweg im Durchschnitt eine Ähnlichkeit von 92 % auf.

Das bedeutet, dass wir, anstatt jeden Index von Grund auf neu aufzubauen, wenn jemand beitritt oder den Rechner wechselt, den bestehenden Index eines Teammitglieds sicher wiederverwenden können. Dadurch verkürzen wir die Zeit bis zur ersten Abfrage bei den größten Repos von Stunden auf Sekunden.

Den ersten Index erstellen

Cursor erstellt sein erstes Abbild der Codebasis mithilfe eines Merkle-Baums. Dadurch kann es exakt erkennen, welche Dateien und Verzeichnisse sich geändert haben, ohne alles neu verarbeiten zu müssen. Der Merkle-Baum enthält einen kryptografischen Hash jeder Datei sowie Hashes jedes Ordners, die auf den Hashes seiner untergeordneten Elemente basieren.

Kleine clientseitige Änderungen verändern nur die Hashes der bearbeiteten Datei selbst und die Hashes der übergeordneten Verzeichnisse bis zur Wurzel der Codebasis. Cursor vergleicht diese Hashes mit der Serverversion, um genau zu sehen, an welcher Stelle die beiden Merkle-Bäume auseinanderlaufen. Einträge mit unterschiedlichen Hashes werden synchronisiert. Übereinstimmende Einträge werden übersprungen. Jeder Eintrag, der auf dem Client fehlt, wird auf dem Server gelöscht, und jeder Eintrag, der auf dem Server fehlt, wird hinzugefügt. Der Sync-Prozess verändert niemals Dateien auf der Clientseite.

Der Merkle-Baum-Ansatz reduziert die Datenmenge, die bei jedem Sync übertragen werden muss, erheblich. In einem Workspace mit fünfzigtausend Dateien summieren sich allein die Dateinamen und SHA-256-Hashes auf ungefähr 3,2 MB. Ohne den Baum müssten diese Daten bei jedem Update übertragen werden. Mit dem Baum durchläuft Cursor nur die Zweige, in denen sich Hashes unterscheiden.

Wenn sich eine Datei ändert, teilt Cursor sie in syntaktische Chunks auf. Diese Chunks werden in Embeddings umgewandelt, die semantische Suche ermöglichen. Das Erzeugen von Embeddings ist der aufwendige Schritt, weshalb Cursor dies asynchron im Hintergrund erledigt.

Die meisten Änderungen lassen die meisten Chunks unverändert. Cursor speichert Embeddings basierend auf dem Chunk-Inhalt im Cache. Unveränderte Chunks werden aus dem Cache bedient, und Agent-Antworten bleiben schnell, ohne diese Kosten zur Inferenzzeit erneut zahlen zu müssen. Der resultierende Index lässt sich schnell aktualisieren und ist leicht zu warten.

Den besten Index zur Wiederverwendung finden

Die oben beschriebene Indizierungspipeline lädt jede Datei hoch, wenn eine Codebasis neu in Cursor ist. Neue Nutzer innerhalb einer Organisation müssen diesen gesamten Prozess jedoch nicht erneut durchlaufen.

Wenn ein neuer Nutzer hinzukommt, berechnet der Client den Merkle-Baum für eine neue Codebasis und leitet daraus einen Wert ab, der als Similarity Hash (simhash) bezeichnet wird. Dies ist ein einzelner Wert, der als Zusammenfassung der Inhalts-Hashes der Dateien in der Codebasis fungiert.

Der Client lädt den Simhash an den Server hoch. Der Server nutzt ihn dann als Vektor, um in einer Vektordatenbank zu suchen, die aus allen anderen aktuellen Simhashes aller anderen Indizes in Cursor im selben Team (oder vom selben Nutzer) wie der Client besteht. Für jedes Ergebnis, das von der Vektordatenbank zurückgegeben wird, prüfen wir, ob es dem Similarity Hash des Clients über einem Schwellenwert entspricht. Falls ja, verwenden wir diesen Index als initialen Index für die neue Codebasis.

Diese Kopie erfolgt im Hintergrund. In der Zwischenzeit kann der Client neue semantische Suchen auf dem ursprünglichen Index ausführen, der kopiert wird, was zu einer sehr kurzen Time-to-first-query für den Client führt.

Das funktioniert jedoch nur, wenn zwei Bedingungen erfüllt sind. Die Ergebnisse müssen die lokale Codebasis des Nutzers widerspiegeln, selbst wenn sie sich vom kopierten Index unterscheidet. Und der Client darf niemals Ergebnisse für Code sehen, den er nicht bereits besitzt.

Nachweis des Zugriffs

Um sicherzustellen, dass Dateien nicht zwischen Kopien der Codebasis versehentlich offengelegt werden, machen wir uns die kryptografischen Eigenschaften des Merkle-Baums zunutze.

Jeder Knoten im Baum ist ein kryptografischer Hash des Inhalts unter ihm. Diesen Hash können Sie nur berechnen, wenn Sie die Datei haben. Wenn ein Workspace von einem kopierten Index startet, lädt der Client seinen vollständigen Merkle-Baum zusammen mit dem Ähnlichkeitshash hoch. Dadurch wird jedem verschlüsselten Pfad in der Codebasis ein Hash zugeordnet.

Der Server speichert diesen Baum als eine Menge von Inhaltsnachweisen. Während der Suche filtert der Server die Ergebnisse, indem er diese Hashes mit dem Baum des Clients abgleicht. Wenn der Client nicht nachweisen kann, dass er eine Datei besitzt, wird das Ergebnis verworfen.

So kann der Client sofort Abfragen stellen und sieht nur Ergebnisse für Code, den er mit dem kopierten Index teilt. Die Hintergrundsynchronisierung gleicht die verbleibenden Unterschiede ab. Sobald die Wurzeln der Merkle-Bäume von Client und Server übereinstimmen, löscht der Server die Inhaltsnachweise und zukünftige Abfragen laufen gegen den vollständig synchronisierten Index.

Schnellere Einarbeitung

Die Wiederverwendung der Indizes von Teammitgliedern verkürzt die Einrichtungszeit für Repositories jeder Größe. Der Effekt verstärkt sich mit der Größe des Repositories:

  • Für das Median-Repository sinkt die Zeit bis zur ersten Abfrage von 7,87 Sekunden auf 525 Millisekunden

  • Beim 90. Perzentil fällt sie von 2,82 Minuten auf 1,87 Sekunden

  • Beim 99. Perzentil fällt sie von 4,03 Stunden auf 21 Sekunden.

Diese Änderungen beseitigen eine zentrale Quelle für wiederholte Arbeit und ermöglichen es Cursor, selbst sehr große Codebasen in Sekunden statt in Stunden zu verstehen.

Kategorisiert unter: Forschung

Autor: Jeremy Stribling

Große Codebasen sicher indizieren · Cursor