대규모 코드베이스를 안전하게 인덱싱하기

작성자 Jeremy Stribling작성일 리서치
대규모 코드베이스를 안전하게 인덱싱하기

시맨틱 검색은 Agent 성능을 끌어올리는 가장 큰 요인 중 하나입니다. 최근 평가에서 평균 응답 정확도를 평균 12.5% 향상시켰고, 코드베이스에 더 오래 유지되는 코드 변경을 만들어 냈으며, 전체 요청 만족도도 높였습니다.

시맨틱 검색을 제공하기 위해 Cursor는 프로젝트를 열 때 코드베이스의 검색 가능한 인덱스를 생성합니다. 작은 프로젝트에서는 거의 즉시 완료됩니다. 하지만 수만 개 파일을 가진 대규모 저장소는 단순하게 인덱싱하면 처리에 몇 시간이 걸릴 수 있고, 그 작업의 최소 80%가 끝나기 전까지는 시맨틱 검색을 사용할 수 없습니다.

우리는 대부분의 팀이 거의 동일한 코드베이스 사본으로 작업한다는 단순한 관찰에서 출발해 인덱싱을 가속화할 방법을 찾았습니다. 실제로 동일한 코드베이스의 클론들은 한 조직 내 사용자들 사이에서 평균 92%의 유사도를 보입니다.

이는 누군가 팀에 합류하거나 머신을 바꿀 때마다 매번 인덱스를 처음부터 다시 구축하는 대신, 팀원의 기존 인덱스를 안전하게 재사용할 수 있다는 뜻입니다. 이렇게 하면 가장 큰 저장소에서도 첫 쿼리까지 걸리는 시간을 몇 시간에서 몇 초로 줄일 수 있습니다.

첫 인덱스 구축하기

Cursor는 코드베이스에 대한 첫 번째 뷰를 만들 때 Merkle tree를 사용합니다. 이를 통해 전체를 다시 처리하지 않고도 정확히 어떤 파일과 디렉터리가 변경되었는지 감지할 수 있습니다. Merkle tree에는 모든 파일의 암호학적 해시와 함께, 자식의 해시들을 기반으로 계산된 각 폴더의 해시가 포함됩니다.

클라이언트 측에서 작은 수정이 발생하면, 수정된 파일 자체의 해시와 코드베이스 루트까지 상위 디렉터리들의 해시만 변경됩니다. Cursor는 이 해시들을 서버 버전과 비교해 두 Merkle tree가 정확히 어디에서 달라지는지 확인합니다. 해시가 다른 항목은 동기화되고, 일치하는 항목은 건너뜁니다. 클라이언트에만 존재하는 항목은 서버에서 삭제되고, 서버에만 존재하는 항목은 서버에 추가됩니다. 동기화 과정에서 클라이언트 측 파일은 절대 수정되지 않습니다.

Merkle tree 방식을 사용하면 각 동기화마다 전송해야 하는 데이터 양을 크게 줄일 수 있습니다. 5만 개의 파일이 있는 워크스페이스에서는 파일 이름과 SHA-256 해시만 합쳐도 대략 3.2MB에 달합니다. 트리가 없다면 업데이트할 때마다 이 데이터를 전부 전송해야 합니다. 트리를 사용하면 Cursor는 해시가 다른 브랜치만 순회하면 됩니다.

파일이 변경되면 Cursor는 파일을 구문 단위 청크로 나눕니다. 이 청크들은 시맨틱 검색을 가능하게 하는 임베딩(embedding)으로 변환됩니다. 임베딩 생성은 비용이 큰 단계이기 때문에, Cursor는 이를 백그라운드에서 비동기적으로 수행합니다.

대부분의 편집은 대부분의 청크를 그대로 남겨둡니다. Cursor는 청크 내용 기준으로 임베딩을 캐시합니다. 변경되지 않은 청크는 캐시를 그대로 사용하므로, 추론 시점에 그 비용을 다시 지불하지 않고도 에이전트 응답 속도를 빠르게 유지할 수 있습니다. 이렇게 만들어진 인덱스는 업데이트가 빠르고 유지 비용이 낮습니다.

재사용할 최적의 인덱스 찾기

위의 인덱싱 파이프라인은 코드베이스가 Cursor에 새로 추가될 때 모든 파일을 업로드합니다. 하지만 조직 내의 새 사용자는 그 전체 과정을 다시 거칠 필요가 없습니다.

새 사용자가 합류하면 클라이언트는 새 코드베이스에 대해 Merkle 트리를 계산하고, 그 트리로부터 유사도 해시(simhash)라고 불리는 값을 도출합니다. 이는 코드베이스 내 파일 콘텐츠 해시들의 요약 역할을 하는 단일 값입니다.

클라이언트는 simhash를 서버로 업로드합니다. 서버는 이를 벡터로 사용해, 동일한 팀(또는 동일 사용자)의 다른 모든 인덱스에 대해 현재 존재하는 simhash들로 구성된 벡터 데이터베이스에서 검색을 수행합니다. 벡터 데이터베이스가 반환한 각 결과에 대해, 해당 값이 클라이언트의 유사도 해시와 임계값 이상의 유사도를 보이는지 확인합니다. 조건을 만족하면, 그 인덱스를 새 코드베이스의 초기 인덱스로 사용합니다.

이 복사는 백그라운드에서 진행됩니다. 그동안 클라이언트는 복사 중인 원본 인덱스를 대상으로 새로운 시맨틱 검색을 수행할 수 있어, 클라이언트 입장에서는 첫 질의까지 걸리는 시간(time-to-first-query)이 매우 짧습니다.

단, 이는 두 가지 제약이 모두 충족될 때만 동작합니다. 복사된 인덱스와 다르더라도, 결과는 항상 사용자의 로컬 코드베이스를 반영해야 합니다. 그리고 클라이언트는 로컬에 존재하지 않는 코드에 대한 결과를 절대 볼 수 없어야 합니다.

접근 증명

코드베이스의 사본 간에 파일이 누출되지 않도록 보장하기 위해 Merkle tree의 암호학적 특성을 재사용합니다.

트리의 각 노드는 그 아래에 있는 콘텐츠의 암호학적 해시입니다. 해당 파일을 실제로 가지고 있어야만 그 해시를 계산할 수 있습니다. 워크스페이스가 복사된 인덱스에서 시작될 때, 클라이언트는 similarity hash와 함께 자신의 Merkle tree 전체를 업로드합니다. 이렇게 해서 코드베이스의 각 암호화된 경로에 하나의 해시가 연결됩니다.

서버는 이 트리를 콘텐츠 증명의 집합으로 저장합니다. 검색 시 서버는 이 해시들을 클라이언트의 트리와 대조해 결과를 필터링합니다. 클라이언트가 어떤 파일을 가지고 있음을 증명하지 못하면 해당 결과는 삭제됩니다.

이 방식으로 클라이언트는 즉시 쿼리를 수행할 수 있고, 복사된 인덱스와 공유하는 코드에 대해서만 결과를 볼 수 있습니다. 백그라운드 동기화는 나머지 차이들을 조정합니다. 클라이언트와 서버의 Merkle tree 루트가 일치하면, 서버는 콘텐츠 증명을 삭제하고 이후 쿼리는 완전히 동기화된 인덱스를 기준으로 실행됩니다.

더 빠른 온보딩

팀원의 인덱스를 재사용하면 모든 규모의 레포지토리에서 초기 설정 시간이 크게 단축됩니다. 레포지토리가 클수록 이 효과는 더욱 커집니다:

  • 중앙값 레포지토리의 경우, 첫 쿼리까지 걸리는 시간이 7.87초에서 525밀리초로 줄어듭니다

  • 상위 90퍼센타일에서는 2.82분에서 1.87초로 줄어듭니다

  • 상위 99퍼센타일에서는 4.03시간에서 21초로 줄어듭니다.

이 변화로 반복 작업의 주요 원인이 제거되고, Cursor가 매우 큰 코드베이스도 몇 시간 단위가 아니라 몇 초 만에 이해할 수 있게 됩니다.

카테고리: 리서치

작성자: Jeremy Stribling

대규모 코드베이스를 안전하게 인덱싱하기 · Cursor