GitHub 코드 검색 — Blackbird의 색인과 질의
push가 Kafka를 거쳐 blob ID 샤딩·ngram 역색인이 되기까지의 색인 경로와, 정규식 질의가 AST 재작성·전 샤드 fan-out·posting list 교집합·원문 재검사를 거쳐 결과가 되기까지의 질의 경로를 24단계로 따라간다.
단계 목록 · 키보드 ←/→ 이동, Space 재생
출처: The technology behind GitHub's new code search (GitHub Engineering) · A brief history of code search at GitHub
Blackbird는 '색인으로 정답을 찾는' 엔진이 아니라 '색인으로 후보를 좁히고 원문 재검사로 확정하는' 엔진이다 — 그리고 blob 콘텐츠 해시로 샤딩해 수천 개 포크의 중복 콘텐츠를 한 번만 색인한다.
왜 헷갈리는가
'왜 Elasticsearch를 안 썼나'가 첫 의문이다. 범용 텍스트 검색은 자연어를 전제한다 — 어간 추출과 불용어 제거는 코드에서 재앙이다(구두점·대소문자·`for` 같은 '불용어'가 전부 의미를 가진다). 정규식 검색까지 필요하니 처음부터 다시 만들었다. 또 하나, 흔히 인용되는 '640,000 QPS'는 원문 오독이다 — 원문은 쿼리당 CPU 약 100ms에서 나오는 '64코어 호스트당 약 640 QPS 상한'이다.
색인 경로 — 리포가 아니라 콘텐츠를 색인한다
push가 Kafka에 색인 이벤트로 발행되면 크롤러가 Git에서 blob을 읽고 함수·클래스 심볼을 추출해 문서를 만든다. 여기서 두 개의 규모 최적화가 겹친다. 첫째, delta indexing — 포크 원본 같은 유사 리포와 달라진 blob만 색인해 문서 수를 50% 넘게 줄였고, 전체 재색인이 36시간에서 약 18시간으로 반감했다.
둘째, 샤드 배분 키가 리포가 아니라 blob의 Git object ID, 즉 콘텐츠 해시다. 같은 내용은 항상 같은 샤드로 가므로 1만 번 포크된 파일도 정확히 한 번만 색인된다. 원본 115TB가 고유 콘텐츠 약 28TB로 줄고, 최종 인덱스는 약 25TB — 원본보다 작다. 어느 리포의 결과인지는 색인이 아니라 랭킹 단계에서 구분한다.
질의 경로 — ngram은 후보, 정답은 원문 재검사
`/arguments?/ org:rails lang:Ruby` 같은 질의는 먼저 AST로 파싱·재작성된다 — lang:Ruby는 LanguageID(326)으로, 그리고 권한 절 Or(RepoIDs(...), PublicRepo())가 자동 주입되어 볼 수 없는 리포가 후보 단계에서부터 걸러진다. 재작성된 질의는 전 샤드(64코어 머신 32대)로 fan-out된다.
각 샤드에서 정규식은 ngram 논리식이 된다 — arguments?는 arg AND rgu AND gum AND ((ume AND ment) OR uments). posting list 이터레이터가 And를 교집합으로, Or를 합집합으로 지연 순회해 후보를 좁힌 뒤, 후보 문서의 실제 내용에 정규식을 다시 돌려 위양성을 제거하고 점수를 매긴다. 쿼리 서비스가 전 샤드 결과를 집계·재정렬하고 권한을 이중 확인해 상위 100건을 돌려준다. 샤드 p99는 약 100ms다.
- `for` 같은 고빈도 구간은 trigram 대신 가중치 기반의 더 긴 sparse gram으로 색인 — posting list 선택성 회복
- 권한 필터는 후처리가 아니라 질의 절 — 그리고 집계 단계에서 이중 확인
- grep 비교: 115TB를 2,048코어로 훑으면 쿼리당 96초
색인과 질의의 분리 — 서로를 기다리지 않는 두 루프
색인은 Kafka를 사이에 둔 비동기 경로, 질의는 동기 경로다. 크롤링·색인이 밀려도 검색 지연에 간섭이 없고, 쓰기 최적화(배치·플러시)와 읽기 최적화(지연 순회·집계)를 따로 할 수 있다. 인덱스 전체는 초당 약 12만 문서 인제스트 속도로 Kafka 스트림을 재소비해 처음부터 다시 지을 수 있다 — 155억 문서에 약 18시간.
샤드 장애 시의 복제·재라우팅 세부는 공개 자료에 명시되어 있지 않다. 공개된 사실은 이벤트 스트림 기반이라 재구축 경로가 항상 열려 있다는 점까지다.
기억할 것
- 샤딩 키는 리포가 아니라 blob의 Git object ID — 중복 콘텐츠(포크)의 색인 비용이 0이 된다.
- delta indexing으로 문서 수 50%↓, 전체 재색인 36→약 18시간.
- 정규식은 ngram 논리식으로 후보를 좁히고, 원문 재검사가 위양성을 제거한다 — 색인은 후보 장치다.
- 권한은 질의 재작성 때 절(Or(RepoIDs, PublicRepo))로 주입되고 집계에서 이중 확인된다.
- 쿼리당 CPU 약 100ms → 64코어 호스트당 약 640 QPS 상한 — '640,000 QPS'는 오독이다.