- Published on
벡터 인덱스의 기본값이 HNSW에서 디스크로 바뀐 이유 — Elasticsearch bbq_disk의 트레이드오프
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며 — 기본값이 바뀌었다는 사실부터
- HNSW의 진짜 비용은 계산이 아니라 RAM이다
- BBQ는 RaBitQ에서 무엇을 가져오고 무엇을 버렸나
- DiskBBQ가 실제로 하는 일
- 정직한 한계 — 95%의 벽, 그리고 라이선스
- 벤치마크 전쟁 — 그리고 잠들어 있던 디스크
- 그래서 언제 쓰고, 언제 쓰지 말아야 하나
- 마치며
- 참고 자료
들어가며 — 기본값이 바뀌었다는 사실부터
벡터 검색 이야기는 대개 임베딩 모델이나 RAG 파이프라인에서 시작합니다. 이 글은 그 아래층, 인덱스 자체에 관한 것입니다. 그리고 시작점은 논쟁이 아니라 그냥 사실 하나입니다 — Elasticsearch의 벡터 필드 기본 인덱스 타입이 바뀌었습니다.
dense_vector 공식 레퍼런스의 기본값 표는 이렇게 흘러갑니다.
- 9.0 — float 벡터는 전부
int8_hnsw. - 9.1 이상 — 384차원 이상은
bbq_hnsw, 384차원 미만은int8_hnsw. - 9.4 이상 및 Serverless — 문서의 문장 그대로 옮기면, float 또는 bfloat16 벡터를 색인할 때 "현재 라이선스에서 사용 가능한 경우 기본 인덱스 타입은
bbq_disk"입니다.
버전 날짜를 붙이면 이 이동이 얼마나 빠른지 보입니다. 9.1은 2025-07-23, 9.2는 2025-10-21, 9.4.0은 2026-05-05에 나왔습니다(endoflife.date와 elastic/elasticsearch 릴리스 태그 기준. 이 글을 쓰는 2026년 7월 현재 최신 릴리스는 9.4.3입니다). 즉 1년이 채 안 되는 사이에 기본값이 스칼라 양자화 그래프 → 이진 양자화 그래프 → 그래프가 아예 아닌 디스크 인덱스로 두 번 옮겨갔습니다.
마지막 단계가 중요합니다. bbq_disk는 HNSW가 아닙니다. IVF(역파일 인덱스) 계열이고, 검색 구조가 RAM이 아니라 디스크에 삽니다. 20년 가까이 "근사 최근접 이웃 검색 = 그래프 인덱스"였던 기본 상식을, 적어도 기본값의 자리에서는 뒤집은 겁니다. 왜 그랬는지, 그리고 그 대가가 무엇인지가 이 글의 내용입니다.
HNSW의 진짜 비용은 계산이 아니라 RAM이다
HNSW의 매력은 분명합니다. 데이터에 대해 로그 스케일로 확장되는 빠르고 계산 효율적인 알고리즘입니다. DiskBBQ 소개 글(Benjamin Trent, John Wagster, Ignacio Vera Sequeiros, 2025-10-23)의 첫 문단도 "We absolutely love HNSW"로 시작합니다.
그런데 같은 글이 바로 대가를 지적합니다. HNSW가 제대로 작동하려면 모든 벡터가 RAM에 있어야 합니다. 양자화로 이 비용을 계속 낮춰 왔지만, 벡터가 메모리 밖으로 밀려나는 순간 성능이 급락하는 문제는 남습니다. 게다가 덜 알려진 두 번째 비용이 있습니다 — 색인을 하려면 기존 HNSW 그래프를 탐색해야 하므로, RAM 비용이 검색 때만이 아니라 색인 때도 그대로 발생합니다.
여기서 핵심 단어는 "급락"입니다. 성능이 완만하게 나빠지는 게 아니라 절벽에서 떨어집니다. Elastic이 이걸 숫자로 보여준 게 저메모리 벤치마크(John Wagster, 2025-10-23)입니다.
측정 조건 — 벤더 자체 측정입니다. 100만 벡터, 기본 병합 정책으로 약 12개 세그먼트, 두 방식 모두 BBQ 1비트 양자화 적용, 양쪽 설정을 recall 약 0.89로 맞춤. Elasticsearch에 내장된 KnnIndexTester 유틸리티를 Docker 컨테이너에 격리하고 swap을 끈 뒤 컨테이너 총 메모리를 제한하는 방식입니다. 표기는 컨테이너 RAM / JVM 힙입니다.
| RAM / 힙 | DiskBBQ 지연 | HNSW BBQ 지연 |
|---|---|---|
| 101m / 10m | 15.83ms | 실행 불가 |
| 150m / 100m | 12.13ms | 289.7ms |
| 250m / 150m | 7.46ms | 26.81ms |
| 350m / 250m | 3.65ms | 7.7ms |
| 450m / 350m | 2.38ms | 3.06ms |
| 550m / 450m | 2.41ms | 3.14ms |
550m 구간에서는 둘이 사실상 같습니다(2.41 대 3.14). 메모리가 넉넉하면 이 논쟁 자체가 성립하지 않는다는 뜻입니다. 그런데 아래로 내려갈수록 갈라집니다. 250m에서 3.6배, 150m에서는 24배 차이가 납니다. Elastic의 표현으로는 DiskBBQ는 완만하게 저하되는데(degrades gracefully), HNSW BBQ는 메모리가 극도로 제한되면 그냥 무너집니다(falls apart).
색인 쪽은 더 극적입니다. 같은 글의 색인 시간 표에서 HNSW BBQ는 1g / 850m 이하 구간에서 아예 완료되지 못했습니다(550m, 750m, 1g 세 구간 모두 측정치 없음). DiskBBQ는 같은 1g / 850m에서 82,397ms에 색인을 마쳤습니다. 이게 앞서 말한 "색인할 때도 그래프를 탐색해야 한다"는 성질의 실물입니다.
바닥은 어디인가도 재밌습니다. 저자는 힙을 10MB까지 내려서 측정했는데, 그 아래로는 log4j 초기화가 실패해서 더 못 내렸다고 합니다. 컨테이너 전체로는 101MB에서 동작했고, 그 아래로는 OS가 SIGKILL(exit 137)을 보냅니다. 참고로 같은 openjdk:24-jdk-slim-bookworm 컨테이너에서 HelloWorld 한 줄을 돌리는 데만 50MB가 필요합니다. 그러니까 DiskBBQ는 100만 벡터를 실질적으로 60MB 남짓한 여유 공간에서 15.83ms에 서빙한 셈입니다.
이 벤치마크가 말하지 않는 것. 정직하게 짚자면, 이 글은 어떤 데이터셋을 썼는지, 몇 차원인지를 본문에 명시하지 않습니다. 100만 벡터 한 규모, 한 머신에서의 측정이고, recall도 0.89 한 지점에 고정돼 있습니다. 방향성(HNSW는 메모리 절벽이 있고 IVF는 없다)은 알고리즘의 구조에서 나오는 것이라 신뢰할 만하지만, 이 표의 절대 수치를 당신의 코퍼스에 옮겨 적으면 안 됩니다.
BBQ는 RaBitQ에서 무엇을 가져오고 무엇을 버렸나
bbq_disk의 BBQ부터 정리해야 합니다. 여기가 이 글에서 가장 덜 알려진 부분입니다.
BBQ(Better Binary Quantization)는 Elasticsearch 8.16과 Lucene에 도입됐습니다(Benjamin Trent, 2024-11-11). 그리고 그 글은 출처를 명확히 밝힙니다 — 싱가포르 난양공대(NTU) 연구진이 제안한 RaBitQ라는 기법에서 얻은 통찰로부터 개발했다고요.
RaBitQ 논문(Jianyang Gao, Cheng Long, SIGMOD 2024)의 핵심 주장은 이렇습니다. D차원 벡터를 D비트 문자열로 양자화하면서, 선명한 이론적 오차 한계를 보장한다는 것. 논문의 문제의식은 PQ(곱 양자화) 계열이 경험적으로는 잘 되지만 이론적 오차 한계가 없어서 실제 데이터셋에서 실패할 수 있다는 점이었습니다. 즉 RaBitQ의 세일즈 포인트는 압축률이 아니라 보장입니다.
여기서 중요한 대목. Elastic의 BBQ 글은 원 저자들의 제안과 다른 점을 스스로 나열합니다. 그중 두 번째 항목을 그대로 옮기면 — 코드북을 무작위 회전(random rotation)하지 않기 때문에, 추정자가 알고리즘의 여러 번 호출에 걸쳐 불편(unbiased)이라는 성질을 갖지 못한다고 적혀 있습니다.
이게 무슨 뜻이냐면, RaBitQ가 이론적 보장을 얻어내는 기제가 바로 그 무작위 회전인데, BBQ는 HNSW와의 단순한 통합과 빠른 색인을 위해 그걸 뺐다는 겁니다. 나열된 차이는 다섯 가지입니다.
- 중심점(centroid)을 하나만 사용 — HNSW와 단순하게 통합하고 색인을 빠르게 하려고.
- 코드북을 무작위 회전하지 않음 → 추정자의 불편성을 잃음.
- 재점수(rescoring)가 추정된 양자화 오차에 의존하지 않음.
- 재점수를 그래프 색인 탐색 중에 하지 않고, 초기 추정 벡터 계산 이후로 미룸.
- 내적(dot product)을 완전히 구현·지원. 원 저자들은 유클리드 거리에만 집중했고, 내적은 언급만 됐을 뿐 구현·측정되지 않았음. 벡터 크기가 중요한 max-inner-product도 지원.
그러니까 BBQ는 RaBitQ가 아닙니다. 논문의 이론적 보장을 엔지니어링 편의와 맞바꾼 파생물입니다. 이 사실 자체는 잘못이 아닙니다 — Elastic이 이걸 블로그에 스스로 적어 뒀다는 점은 오히려 후한 점수를 줄 만합니다. 다만 "RaBitQ 기반이라 이론적 오차 한계가 있다"는 식으로 BBQ를 소개하는 2차 자료를 만나면, 그건 틀린 설명입니다.
그리고 재밌는 후일담이 있습니다. 9.4에서 precondition이라는 파라미터가 추가됐는데, 레퍼런스 문서의 설명은 이렇습니다 — true로 두면 색인된 벡터를 무작위 직교 사영(random orthogonal projection)으로 변환하며, 벡터 성분이 정규분포를 따르지 않을 때 정확도 개선에 도움이 될 수 있다. 기본값은 false이고, 필드 생성 후에는 바꿀 수 없습니다. 2024년에 뺐던 무작위 회전이 2026년에 옵트인 스위치로 돌아온 셈입니다.
32배가 32배가 아닌 이유
압축률도 한 번 정직하게 짚고 갑시다. BBQ 레퍼런스 문서는 BBQ가 float32 차원을 비트로 바꿔 각 벡터를 32배 압축하되, 벡터당 14바이트의 보정 데이터가 추가로 붙는다고 적습니다.
산수를 해 보면 1024차원 벡터에서 1024비트는 128바이트, 여기에 보정 14바이트를 더하면 142바이트입니다. 원본은 4096바이트니까 실제 비율은 약 29배입니다. 실제로 Jina v5 임베딩 측정 글(Jeffrey Rengifo, 2026-07-10)의 표가 정확히 이 숫자를 씁니다 — bbq는 차원당 약 0.14바이트, 1024차원에서 142바이트, 약 29배.
더 중요한 건 디스크는 줄지 않는다는 점입니다. 같은 Jina 글은 두 인덱스의 디스크 사용량이 거의 동일했다고 보고합니다. 양자화 벡터를 얹어도 원본 벡터는 재점수용으로 그대로 남기 때문입니다. dense_vector 문서도 양자화·원본을 함께 저장하는 오버헤드로 디스크가 int8은 약 25%, int4는 약 12.5%, bbq는 약 3.1% 늘어난다고 적습니다. 32배는 RAM 이야기지 스토리지 이야기가 아닙니다.
Jina 글의 헤드라인 숫자도 조건을 붙여 읽어야 합니다. recall@10이 float32 기준선 대비 0.994로 유지됐다는데(벤더 자체 측정, 5개 언어 다국어 뉴스 코퍼스, jina-embeddings-v5-text-small, 1024차원), 글 자신이 그 이유를 밝힙니다 — Jina v5가 양자화 인식 학습(quantization-aware training)을 거친 모델이기 때문입니다. 즉 1비트 양자화에 맞춰 훈련된 모델의 숫자이지, 아무 임베딩 모델에나 이식되는 결과가 아닙니다. 그리고 메모리가 12.71MB에서 0.44MB로 줄었다는 수치의 절대 규모를 보십시오. 12MB는 데모 크기입니다.
DiskBBQ가 실제로 하는 일
이제 디스크 쪽입니다. DiskBBQ는 IVF 인덱스의 진화형이고, 동작은 이렇습니다.
- 계층적 K-means로 벡터를 작은 클러스터로 분할합니다. 문서 기준
cluster_size기본값은 클러스터당 384개 벡터이고, 64에서 65536 사이로 조정할 수 있습니다. 작을수록 정확도가 오르고 성능이 떨어집니다. - 질의가 오면 대표 중심점부터 찾고, 계층의 다층 구조를 활용해 최대 2개 층의 중심점만 탐색해 탐색 공간을 제한합니다.
- 선택된 클러스터 안의 벡터들을 벌크 스코어링합니다. 벡터가 BBQ로 잘게 압축돼 있으니 한 번에 여러 블록을 메모리로 올려 빠르게 점수를 매길 수 있고, 이 연산 대부분이 오프힙에서 일어납니다.
- 벡터를 여러 중심점에 중복 배정합니다. Google의 SOAR(Spilling with Orthogonality-Amplified Residuals)의 변형을 써서, 벡터가 두 클러스터 경계에 놓일 때 특히 도움이 됩니다. 벡터가 워낙 강하게 압축돼 있어서 디스크 오버헤드는 미미하고, 대신 탐색해야 할 중심점 수가 줄어듭니다.
질의 시점 손잡이는 visit_percentage입니다. 샤드당 방문할 벡터 비율을 정하고, 낮추면 메모리와 지연이 줄고 높이면 recall이 오릅니다. 매핑 쪽 default_visit_percentage 기본값은 문서 기준 100만 벡터당 샤드당 약 1% 수준입니다.
양자화는 비대칭입니다. 기본적으로 색인 벡터는 1비트, 질의 벡터는 4비트로 양자화합니다. 질의 쪽에 비트를 더 쓰는 건 저장 비용이 안 늘면서 검색 품질이 크게 오르기 때문입니다. 9.4부터 bbq_disk에 한해 색인 벡터의 bits를 1(기본), 2, 4, 7로 바꿀 수 있고, 오버샘플링 계수가 자동으로 따라옵니다 — 1비트면 3.0배, 2비트면 1.5배, 4비트와 7비트면 오버샘플링 없음. 이건 재색인 없이 언제든 바꿀 수 있습니다.
메모리가 충분한 경우의 비교도 같은 소개 글에 있습니다. 벤더 자체 측정, 100만 벡터가 전부 메모리에 들어가는 조건입니다.
| 방식 | 색인 시간 | 지연 | recall |
|---|---|---|---|
| HNSW BBQ | 1,054,319ms | 약 3.4ms | 92% |
| DiskBBQ | 94,075ms | 약 4.0ms | 91% |
읽는 법은 이렇습니다. HNSW가 가장 유리한 조건(전부 RAM에 있음)에서도 지연 차이는 3.4 대 4.0으로 크지 않은데, 색인은 11배 넘게 차이 납니다. 그래프를 짓는 비용이 그만큼 비쌉니다. Elastic도 "모든 상황에서 100% 따라잡은 건 아니"라고 적어 뒀습니다.
정직한 한계 — 95%의 벽, 그리고 라이선스
여기가 이 글에서 가장 중요한 절입니다.
첫째, recall 천장이 있습니다. BBQ 레퍼런스 문서의 문장을 그대로 옮기면, DiskBBQ는 보통 약 95%까지의 recall 수준에서 잘 동작합니다. 예외적으로 높은 recall(99% 이상)이 필요한 경우에는 많은 클러스터를 방문해야 하고, 이는 성능에 부정적 영향을 줄 수 있습니다. 그런 경우엔 메모리 상황에 따라 bbq_hnsw 같은 HNSW 계열이 더 낫습니다.
소개 블로그도 같은 말을 더 직설적으로 합니다. 아주 높은 recall이 필요하고, 오프힙 메모리가 넉넉하거나 그 비용을 낼 의향이 있고, 색인 갱신이 드물다면 — 양자화를 곁들인 HNSW가 여전히 최선일 가능성이 높다. 반대로 95% 이하의 recall로 충분하고, 비용에 민감하고, 그래도 빠른 검색을 원한다면 DiskBBQ가 답일 수 있다.
이건 알고리즘 구조에서 나오는 한계라 튜닝으로 없앨 수 있는 종류가 아닙니다. IVF는 클러스터 단위로 가지치기를 하고, 마지막 몇 %의 recall을 얻으려면 결국 가지치기를 포기하고 점점 더 많은 클러스터를 열어야 합니다. 그 지점에서 IVF의 장점이 사라집니다.
둘째, 기본값이 라이선스에 묶여 있습니다. dense_vector 문서는 bbq_disk가 Enterprise 구독을 필요로 한다고 명시합니다. 그리고 9.4의 기본값 문장에는 "현재 라이선스에서 사용 가능한 경우"라는 조건이 붙어 있습니다.
실무적으로 이 문장의 무게를 짚고 갑시다. 같은 매핑 JSON이 라이선스 등급에 따라 다른 인덱스 타입을 만듭니다. index_options를 생략한 채 9.4 클러스터 두 대에 같은 정의를 올리면, Enterprise 쪽은 디스크 IVF를, 그 아래 등급은 bbq_hnsw를 만듭니다. 메모리 프로파일도, recall 특성도, 색인 속도도 다른 인덱스가 나오는데 매핑은 똑같이 생겼습니다. 개발 환경과 운영 환경의 라이선스가 다르면 성능 재현이 안 되는 이유가 여기 숨을 수 있습니다. 기본값에 의존하지 말고 index_options.type을 명시하는 게 안전합니다.
셋째, 자잘하지만 실재하는 제약들. bbq_disk는 element_type이 float 또는 bfloat16인 경우만 지원합니다. byte와 bit는 해당 없음이고, 이 타입들은 모든 버전에서 양자화 없는 hnsw가 기본입니다. precondition은 필드 생성 후 변경 불가입니다. 그리고 384차원 미만 데이터셋은 정확도가 덜 나오고 보정 계수 오버헤드 비율이 커질 수 있다는 게 BBQ 문서의 경고입니다(다만 e5-small 같은 낮은 차원에서도 잘 동작한 프로덕션 사례를 봤다고 덧붙입니다).
참고로 문서상 auto_calibrate라는 기능이 9.5 GA로 예고돼 있습니다. 병합 시점에 세그먼트마다 최대 17,000개 벡터를 샘플링해 k=10에서 90% recall 목표를 만족하는 가장 싼 설정(양자화 인코딩 5종, 재순위 깊이 배수 6종, 프리컨디셔닝 적용/미적용)을 자동으로 고르는 방식입니다. 다만 이 글을 쓰는 시점의 최신 릴리스는 9.4.3이므로, 이건 아직 문서에만 있는 예고편으로 읽어야 합니다.
벤치마크 전쟁 — 그리고 잠들어 있던 디스크
이제 2026년 여름의 사건입니다. 이 이야기가 재밌는 건 결론보다 방법론 때문입니다.
발단은 Elastic이 2026-06-24에 낸 글(Sachin Frayne)의 주장이었습니다 — 네트워크 연결 스토리지(NAS)에서 DiskBBQ가 Qdrant보다 최대 7배 높은 처리량을 낸다. recall 0.93~0.97 구간에서 격차가 일관되고 recall이 오를수록 벌어진다는 주장이었고, 원인으로 재점수 중 원본 벡터의 랜덤 디스크 읽기를 지목했습니다. 이 메커니즘을 기억해 두십시오. 나중에 철회되는 게 바로 이 부분입니다.
Qdrant는 자기 쪽 숫자와 설명으로 반박했습니다. io_uring 기반 비동기 디스크 스코어러가 있고, Elasticsearch는 메모리를 더 먹는다는 지적이었습니다.
그리고 2026-07-13, Elastic의 Jim Ferenczi가 반박의 반박을 냈는데, 이게 보통의 벤더 반격문이 아닙니다. 글의 규칙을 이렇게 선언합니다 — 모든 주장에는 숫자와 메커니즘이 붙어야 하고, 아니면 싣지 않는다. 그리고 그 잣대를 자기 원 글에도 들이댑니다.
Qdrant의 설정을 그대로 자기 클러스터에 세우고(2100만 벡터, 3 × m6g.large = 2 vCPU / 8GB, AWS, 고정 1만 질의, recall@100), 결과를 재현한 뒤 모든 숫자를 원인까지 추적했습니다. 결론부터 말하면 이렇습니다.
1. io_uring은 아무 일도 하지 않았습니다. 먼저 웃긴 발견이 있습니다. Docker에서 Qdrant를 돌리니 io_uring이 아예 초기화되지 않고 있었습니다 — 기본 seccomp 프로파일이 io_uring 시스템 콜을 막아서, 조용히 동기 읽기로 폴백하고 있었던 겁니다(로그: failed to initialize io_uring instance: Operation not permitted). seccomp를 풀고 제대로 켠 뒤 다시 측정한 결과가 이렇습니다.
- 비동기 스코어러 사실상 꺼짐(동기 폴백): ef=50에서 31.6 QPS
- 비동기 스코어러 켜짐(io_uring 동작 확인): ef=50에서 35.8 QPS
13% 차이이고, 글의 표현으로는 그마저도 실행 간 노이즈 범위 안입니다. 결과 전체의 공으로 지목된 기능을 켜 봤더니, 벤치마크를 한 번 더 돌린 것만큼 움직였다는 얘기입니다.
2. 이유는 디스크를 읽은 적이 없기 때문입니다. 이게 이 글의 백미입니다. 처리량 측정 구간 동안 세 노드의 블록 디바이스를 iostat으로 지켜본 결과, 읽기 처리량은 0 MB/s, 0 IOPS로 유지됐고 CPU는 60~70%에서 동시성을 올리자 100%까지 갔습니다. 병목은 양자화 거리 계산과 그래프 순회를 하는 CPU였습니다.
왜 디스크를 안 읽었나. 봉투 뒷면 계산이 답을 줍니다. 재점수는 질의당 상위 100개 후보를 읽고, 768차원 float32 벡터는 3,072바이트인데 페이지 정렬이 안 돼 있어 실제로는 8KB 페이지 읽기에 가깝습니다. 그러면 총 작업 집합은 100개 × 1만 질의 × 8KB = 클러스터 전체 8GB, 노드당 2.7GB입니다. 8GB 노드의 페이지 캐시에 여유롭게 들어갑니다. 게다가 측정 하네스는 1만 질의로 recall 패스를 한 번 다 돌린 다음 같은 1만 질의로 처리량을 잽니다. 글의 표현대로 측정은 구조적으로 이미 따뜻합니다(warm by construction).
3. 숫자를 실제로 움직인 건 세그먼트 개수였습니다. Elastic의 첫 재현은 128개 세그먼트가 나왔고 기대의 절반쯤 되는 처리량을 보였습니다. Qdrant가 공개한 결과 JSON에는 67개 세그먼트가 적혀 있었습니다. 세그먼트를 병합해 맞추자 세 지표가 함께 움직였습니다.
| 세그먼트 | recall@100 | QPS | 평균 지연 |
|---|---|---|---|
| 128 (병렬 적재, Elastic 첫 시도) | 0.9745 | 35.8 | 112ms |
| 66 (Qdrant에 맞춰 병합) | 0.9531 | 53.3 | 75ms |
| 67 (Qdrant 공개 수치) | 0.9596 | 67.2 | 59.5ms |
이 표를 읽는 법이 중요합니다. 세그먼트가 줄면 recall이 떨어집니다. 질의당 훑는 후보 총량이 줄기 때문입니다. 동시에 처리량은 오릅니다. 질의가 모든 세그먼트의 그래프로 팬아웃하니 세그먼트가 두 배면 질의당 작업도 거의 두 배가 되니까요. 즉 Elastic의 첫 시도가 recall이 높게 나온 건 튜닝을 잘해서가 아니라 세그먼트가 많아서였습니다.
그런데 세그먼트를 적게 만드는 방법은 단일 스레드로 느리게 적재하는 것입니다. 빠른 질의를 느린 적재로 사는 셈입니다. 그리고 이게 IVF와 HNSW가 진짜로 갈리는 지점이라고 글은 짚습니다. HNSW 그래프 구축은 비싸고(2 vCPU 노드에서 ef_construct=256으로 약 1.6시간, CPU 100% 고정) 질의 비용이 세그먼트 수에 민감한 반면, bbq_disk 뒤의 IVF 레이아웃은 구축이 싸고 세그먼트 수에 훨씬 덜 민감합니다. 글의 표현으로는 "벤치마크 트릭이 아니라, 정직하게 진술한 IVF 대 HNSW 트레이드오프"입니다.
4. 그리고 자기 비용도 셉니다. Elastic 쪽 지연의 상당 부분이 벡터 검색이 아니라 응답을 조립하는 페치 단계였습니다. 현재 _id가 _source·본문·벡터와 같은 저장 필드 컬럼에 살기 때문에, id 하나를 가져오려고 그 압축 블록 전체를 디컴프레서에 통과시킵니다. id를 그 컬럼에서 분리해 측정하니 Qdrant의 저세그먼트 처리량 대역으로 바로 올라섰습니다. 글의 문장 그대로 — 그 격차는 문서 검색이지 벡터 검색이 아닙니다.
| 방문 % | recall@100 | QPS (기본) | QPS (id 분리) |
|---|---|---|---|
| 1 | 0.894 | 44 | 89 |
| 2 | 0.939 | 39 | 73 |
| 3 | 0.956 | 35 | 56 |
| 5 | 0.970 | 31 | 40 |
recall 약 0.96에서 id를 분리한 Elasticsearch는 약 56 QPS로, 같은 하드웨어에서 Qdrant의 53~67과 같은 대역에 앉습니다.
5. 마지막으로 자기 원 글의 7배를 철회합니다. 정확히는 숫자가 아니라 메커니즘을 철회합니다. 7배라는 수치는 그 설정에서 진짜지만, 원 글이 그 숫자에 붙인 설명(재점수 중 원본 벡터의 랜덤 디스크 읽기가 Qdrant의 병목이었고 NAS가 이를 악화시켰다)은 같은 산수를 통과하지 못한다는 겁니다. 1라운드 노드는 RAM이 26GB로 이번 8GB 노드보다 여유가 많았으니, 8GB에서 디스크가 놀았다면 26GB에서도 거의 확실히 놀았을 거라는 논리입니다. 그럼 1라운드 Qdrant가 왜 0.97 recall에서 4.5 QPS에 머물렀는지는? 글은 모른다고 인정하고 가설만 답니다(2비트 양자화에 오버샘플링을 1로 고정했으니 recall을 올릴 손잡이가 ef밖에 없었고, 아주 큰 ef는 그 자체로 질의를 비싸게 만든다).
이 사건에서 가져갈 것
가장 중요한 문장은 결론부에 있습니다. 2100만 벡터를 8GB 노드에 올리면 어느 설계든 전부 RAM에 들어가고, 그래서 이 벤치마크는 두 설계를 구분할 수 없습니다. 흥미로운 변수를 상수로 고정해 놓고 측정한 겁니다.
즉 디스크 기반 인덱스를 디스크가 필요 없는 규모에서 측정한 벤치마크였습니다. Qdrant의 설정은 4비트 벡터를 always_ram으로 고정하고 HNSW 그래프를 RAM에 두는데, 2100만 벡터에서 노드당 약 3.6GB입니다(계산: 4비트 양자화 384바이트/벡터 = 2.69GB, m=16 HNSW 그래프 136바이트/벡터 = 0.95GB, 합 520바이트/벡터 = 3.64GB. 실측 상주 메모리는 3.4~3.9GB로 추정치에 거의 정확히 앉았습니다). 8GB 노드에 3.6GB면 남습니다. bbq_disk가 설계된 영역은 양자화 벡터와 검색 구조가 페이지 캐시를 넘어서는 순간인데, 두 글 중 누구도 거기를 측정하지 않았습니다. Elastic은 이걸 인정하고, 인덱스가 RAM에 안 들어가는 규모의 벤치마크를 만들고 있다고 적습니다.
그리고 이 글도 여전히 벤더 자체 측정입니다. 방법론은 이례적으로 정직하지만, 결론은 여전히 "빠른 기본 경로에서는 Elasticsearch가 색인도 검색도 더 빠르다"입니다. Qdrant 쪽 재현은 Qdrant의 재현 스크립트를 썼다고 밝히고 있지만, 상대 엔진의 튜닝은 언제나 자기 엔진의 튜닝보다 얕기 마련입니다. 4 vCPU / 16GB 등급은 "두 엔진을 함께 들어올리고 동일한 추세를 보여줄 뿐이라 통찰 없이 비용만 는다"는 이유로 의도적으로 돌리지 않았다고 밝히는데, 합리적인 판단이지만 검증되지 않은 주장인 것도 사실입니다.
그럼에도 이 글이 남긴 교훈은 벤더 소속과 무관하게 유효합니다. 글 자신의 요청을 옮기면 — 벤치마크가 깔끔한 배수를 건네줄 때, 그 이야기를 믿기 전에 모든 숫자를 원인까지 쫓아가라. 절반은 따뜻한 캐시이거나 세그먼트 개수다.
그래서 언제 쓰고, 언제 쓰지 말아야 하나
bbq_disk가 값을 하는 경우
- 양자화 벡터와 검색 구조를 합친 크기가 노드의 RAM·페이지 캐시를 넘어서는 규모다. 이게 유일하게 진짜 중요한 조건입니다. 안 넘으면 이 논쟁은 당신 것이 아닙니다.
- recall 95% 이하로 제품 요구사항이 충족된다.
- 색인·갱신이 잦다. HNSW의 그래프 구축 비용과 세그먼트 민감도가 아픈 지점입니다.
- 비용에 민감하다. RAM은 벡터 검색에서 가장 비싼 자원입니다.
- 임베딩 차원이 384 이상이다.
HNSW 계열에 남아야 하는 경우
- recall 99% 이상이 필요하다. 문서가 직접 다른 방식을 권합니다.
- 지연이 극단적으로 빡빡하다.
- 인덱스가 RAM에 편하게 들어가고, 그 RAM을 이미 갖고 있거나 살 의향이 있다.
- 색인이 거의 갱신되지 않는다. 그래프 구축 비용을 한 번만 내면 됩니다.
- Enterprise 구독이 없다. 이 경우 선택지 자체가 없습니다.
byte나bitelement type이 필요하다.bbq_disk가 지원하지 않습니다.
한 가지 더. 필터링된 벡터 검색이 주 워크로드라면 다른 축을 봐야 합니다. Elasticsearch 9.1은 필터를 HNSW 그래프 순회에 직접 통합하는 ACORN-1 알고리즘을 도입했고(2024년 발표된 학술 논문 기반), Elastic은 전형적으로 5배 속도 향상을 보고합니다 — 이건 HNSW 쪽 이야기입니다. 필터 선택도가 낮은 워크로드에서 IVF 계열이 어떻게 거동하는지는 이 글들이 다루지 않는 주제이고, 직접 재야 합니다.
마치며
정리하면 이렇습니다. Elasticsearch는 2026년 5월 9.4에서 float 벡터의 기본 인덱스를 디스크 기반 IVF로 바꿨습니다. 이 결정 뒤에는 하나의 판단이 있습니다 — 벡터 검색에서 진짜 비싼 자원은 CPU가 아니라 RAM이고, HNSW는 RAM이 모자랄 때 완만하게 나빠지는 게 아니라 절벽에서 떨어진다.
대가는 명확합니다. recall 95% 언저리의 천장이 있고, 그건 IVF의 구조에서 나오므로 튜닝으로 없앨 수 없습니다. 그리고 그 기본값은 Enterprise 라이선스 뒤에 있어서, 같은 매핑이 라이선스에 따라 다른 인덱스를 만듭니다.
BBQ 자체도 정직하게 볼 필요가 있습니다. RaBitQ 논문의 통찰에서 나왔지만, 논문의 세일즈 포인트였던 이론적 오차 한계를 얻어내는 무작위 회전을 빼고 시작했습니다(그리고 9.4에서 옵트인으로 일부 되돌렸습니다). 32배 압축은 RAM 이야기이고 디스크는 오히려 3.1% 늘어납니다.
마지막으로, 2026년 여름의 벤치마크 공방이 남긴 게 가장 실용적입니다. 디스크 기반 인덱스를 자랑하는 벤치마크에서 디스크는 0 IOPS로 잠들어 있었고, 비동기 I/O는 13% 움직였으며, 진짜 범인은 세그먼트 개수와 _id 페치였습니다. 그리고 Elastic 스스로 인정했듯, 두 설계가 갈리는 지점을 측정한 벤치마크는 아직 아무도 쓰지 않았습니다.
그러니 결론은 늘 같은 자리로 돌아옵니다. 당신의 인덱스가 RAM에 들어가는지부터 재십시오. 들어간다면 이 글의 대부분은 당신 문제가 아닙니다. 안 들어가기 시작할 때 — 그때가 디스크 인덱스를 꺼낼 시점이고, 그때도 남의 벤치마크가 아니라 당신의 코퍼스와 당신의 recall 목표로 재야 합니다.
참고 자료
- Dense vector field type — 기본 인덱스 타입, bbq_disk 파라미터와 라이선스 조건
- Better Binary Quantization (BBQ) 레퍼런스 — bbq_disk, 95% recall 한계, 오버샘플링, 자동 보정
- Introducing a new vector storage format: DiskBBQ (Trent, Wagster, Vera Sequeiros, 2025-10-23)
- Low-memory benchmarking in DiskBBQ and HNSW BBQ (Wagster, 2025-10-23)
- Better Binary Quantization in Lucene and Elasticsearch (Trent, 2024-11-11) — RaBitQ와의 차이 목록
- RaBitQ: Quantizing High-Dimensional Vectors with a Theoretical Error Bound for ANN Search (Gao & Long, SIGMOD 2024, arXiv 2405.12497)
- Elasticsearch DiskBBQ delivers 7x faster vector search than Qdrant on network-attached storage (Frayne, 2026-06-24) — 발단이 된 원 벤치마크
- The disk that never woke up: what actually decided our Qdrant vector search benchmark rematch (Ferenczi, 2026-07-13) — 위 글의 메커니즘을 스스로 철회한 후속편
- How BBQ shrinks Jina v5 embeddings by 29x without losing recall (Rengifo, 2026-07-10)
- Elasticsearch now with BBQ by default & ACORN for filtered vector search (9.1)
- Elasticsearch 릴리스 태그 — 버전별 출시일 확인