fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • langgraph 페이지로 이동
    • 사람용 CLI와 AI 에이전트용 CLI는 설계가 다르다
    • agents.md
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code 메모리: CLAUDE.md와 .claude/rules를 규칙으로 쓰는 법
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • Docling — IBM Research 의 문서 파싱 toolkit 상세 정리
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
    • OpenClaw는 context와 memory를 어떻게 관리하나 — 나만의 에이전트를 구성하는 법
    • OpenClaw vs Hermes Agent — 갈아탈까 고민하며 정리한 비교
    • 스킬 문서를 신경망처럼 학습시킨다 — Microsoft SkillOpt 분석
  • ai 페이지로 이동
    • agent 페이지로 이동
    • [초안] AI 제품 백엔드 안정성 — 지연·비용·권한·관측·도구 실패·폴백/재시도/사람 에스컬레이션
    • [초안] LLM 평가 프레임워크: 골든셋, 회귀 테스트, LLM-as-a-judge, 사람 피드백 루프
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • apartment 페이지로 이동
    • 구리 럭키아파트 24평 인테리어 레퍼런스 모음
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • [초안] API Versioning과 Backward Compatibility: 시니어 백엔드 관점 정리
    • 캐시 설계 전략 총정리
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 도메인 모델링: 주문·재고·노출의 세 축을 분리해서 설계하기
    • 커머스 주문 상태와 데이터 정합성 기본기
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] Event Sourcing과 CQRS — 상태가 아니라 변화를 저장한다는 발상
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리
    • [초안] F&B 주문/매장/픽업 상태머신 설계
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 대규모 회원과 메가 프로모션을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [초안] MSA 서비스 간 통신: Redis [Cache-Aside](../database/redis/cache-aside.md) × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] REST API 버저닝과 모바일 앱 하위 호환성 — 디지털 채널 백엔드 관점
    • [초안] Spring Batch vs Event-Driven — 같은 비동기처럼 보이지만 전혀 다른 두 패러다임
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • [초안] DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • [초안] 운영 데이터 정합성 장애 대응 — 결제 취소 누락과 중복 적재 런북
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
    • [초안] 시니어 백엔드를 위한 SLO와 Error Budget 기반 장애 대응
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS는 어떻게 안전한가 — TLS, 인증서, 그리고 termination
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 전략 패턴 / 12일 AI 웹툰 MVP
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
    • [초안] Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • mlops 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS 가 안 줄어드는 이유 — gc.collect 의 한계와 malloc_trim
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
    • [초안] Spring Security 6.x OAuth2 + JWT 상용 인증 설계 — Grant 선택, Resource Server, Refresh Rotation, 로그아웃
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • travel 페이지로 이동
    • 오사카 3박 4일 일정표: 우메다 쇼핑, USJ, 난바·도톤보리, 오사카성
  • web 페이지로 이동
    • [초안] HTTP / Cookie / Session / Token 인증 기본기 — 레거시 JSP와 모바일 API가 공존하는 백엔드 관점
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/database/Lucene 세그먼트 구조와 그 위에서 kN…
db

Lucene 세그먼트 구조와 그 위에서 kNN이 동작하는 방식

> 벡터 검색 알고리즘 입문과 HNSW 심화에서 HNSW 그래프 자체는 다뤘다. > 이 글은 그 그래프가 범용 검색엔진(OpenSearch) 안에 어떻게 얹히는가 — Lucene 세그먼트 구조 위에서 kNN이 도는 방식이다. OpenSearch를 벡터 DB로 쓸 때 성능이 왜 그렇게 나오는지는 엔진을 열어봐도 안 보인다. 답은 그 밑의 Lucene 세그먼트...

2026.07.06·7 min read·0 views

벡터 검색 알고리즘 입문과 HNSW 심화에서 HNSW 그래프 자체는 다뤘다. 이 글은 그 그래프가 범용 검색엔진(OpenSearch) 안에 어떻게 얹히는가 — Lucene 세그먼트 구조 위에서 kNN이 도는 방식이다.

OpenSearch를 벡터 DB로 쓸 때 성능이 왜 그렇게 나오는지는 엔진을 열어봐도 안 보인다. 답은 그 밑의 Lucene 세그먼트 구조에 있다. 한 줄로 요약하면 이렇다 — 벡터 인덱스는 세그먼트에 종속된다. 세그먼트마다 HNSW 그래프가 하나씩 불변으로 만들어지고, kNN 쿼리는 샤드 안 모든 세그먼트 그래프로 fan-out 해서 각 top-k를 병합한다. 이 한 문장이 "세그먼트가 많으면 느리다", "force merge로 빨라진다", "OpenSearch 벡터 검색 QPS 천장" 을 전부 설명한다.

먼저 가져갈 결론

  • Lucene 인덱스는 불변 세그먼트의 모음이다. 문서를 넣으면 새 세그먼트가 생기고, 지우면 기존 세그먼트에 삭제 표시만 남는다.
  • 벡터 인덱스(HNSW 그래프)는 세그먼트마다 1개씩 만들어진다. 샤드에 세그먼트가 N개면 그래프도 N개다.
  • kNN 쿼리는 샤드 안 모든 세그먼트 그래프를 각각 탐색해 세그먼트별 top-k를 뽑고, 그걸 병합해 전역 top-k를 낸다. 그래서 세그먼트가 많을수록 느려진다.
  • force merge는 세그먼트를 소수(보통 1개)로 강제 병합하는 작업이다. 그래프가 1개로 줄어 검색이 빨라져, 읽기 전용 벡터 인덱스에 공식 권장된다.
  • 이 구조가 우리 벤치에서 본 OpenSearch QPS 특성의 근본 원리다 — 단일 샤드 + force merge로 그래프 1개를 만든 뒤 측정했고, 그 위에서 쿼리 병렬성이 병목을 만든다.

1부. Lucene 세그먼트 구조

inverted index — 세그먼트의 알맹이

Lucene은 문서를 넣을 때 텍스트를 term으로 분석해 "term → 그 term을 가진 문서 목록(posting list)" 매핑을 만든다. 이게 inverted index다 — 문서에서 단어를 찾는 게 아니라, 단어에서 문서를 역방향으로 찾는다. 그리고 Lucene 인덱스는 이 inverted index를 통째로 하나 두는 게 아니라, 작은 inverted index 여러 개의 모음이다. 그 하나하나가 세그먼트다.

세그먼트는 불변이다

세그먼트는 한번 쓰이면 고쳐지지 않는다(immutable).

  • 문서 추가 — 기존 세그먼트를 건드리지 않고 새 세그먼트에 쓴다.
  • 문서 수정·삭제 — 원래 세그먼트 안에서 삭제됨(tombstone)으로 표시만 한다. 실제로 지우지 않는다.

불변이라 얻는 게 크다. 락 없이 여러 스레드가 동시에 읽을 수 있고, OS 페이지 캐시에 안전하게 올라가 한번 캐시되면 무효화될 일이 없다. 벡터 검색에서 이 불변성이 그대로 그래프에 이어진다(2부).

refresh — 새 세그먼트가 태어나는 순간

들어온 문서는 먼저 RAM 버퍼에 쌓인다. 이 상태에서는 아직 검색되지 않는다. refresh 시점에 버퍼를 flush 해서 OS 페이지 캐시에 새 세그먼트를 만들고, 그때부터 그 문서가 검색된다. refresh_interval 기본값이 1초라, 문서를 넣고 약 1초 뒤 검색된다 — 이것이 준실시간(near-real-time) 검색의 정체다. 간격을 늘리거나 끄면 작은 세그먼트가 덜 생겨 색인 처리량이 오른다. 대량 적재 때 refresh를 끄는 게 표준 튜닝인 이유다.

merge — 작은 세그먼트를 큰 것으로

refresh가 계속 작은 세그먼트를 찍어내므로, 백그라운드에서 이들을 큰 세그먼트로 합친다. Lucene 기본 정책은 TieredMergePolicy로, 크기가 비슷하거나 삭제 표시 비율이 높은 세그먼트를 골라 합친다. 병합은 선택된 세그먼트를 읽어 삭제 문서를 빼고 남은 문서만 새 큰 세그먼트로 다시 쓴 뒤, 원본 작은 세그먼트를 지운다. 이때 비로소 tombstone이 실제로 회수된다. 세그먼트 수가 줄어 검색이 열어볼 대상이 적어진다.

force merge — 세그먼트를 강제로 줄이기

_forcemerge는 병합을 수동으로 강제해 세그먼트 수를 목표치(max_num_segments, 흔히 1)까지 줄인다. 세그먼트가 적을수록 읽기가 빨라지므로, 더 이상 쓰기가 없는 읽기 전용 인덱스에 주로 쓴다.

주의할 점이 있다.

  • 1개로 합치는 동안 샤드 저장 공간이 일시적으로 약 2배가 된다.
  • 계속 쓰기가 있는 인덱스에 돌리면, 합쳐 만든 큰 세그먼트가 다시 병합 대상이 되며 비용만 든다. 그래서 read-only일 때만 권장된다.

shard와 segment — 계층 관계

정리하면 계층은 이렇다 — 인덱스 = 샤드 여러 개, 샤드 1개 = Lucene 인덱스 1개 = 세그먼트 여러 개. 쿼리는 인덱스의 모든 샤드로 뿌려지고(coordinating 노드가 취합), 각 샤드 안에서는 그 샤드의 모든 세그먼트를 훑어 부분 결과를 모은 뒤 병합한다. 이 "샤드로 fan-out, 세그먼트로 다시 fan-out" 구조가 벡터 검색에서 그대로 성능을 좌우한다.

2부. kNN 플러그인이 세그먼트 위에서 도는 방식

세그먼트마다 그래프 1개

핵심 사실이다. 벡터 인덱스(HNSW 그래프)는 세그먼트마다 별도로 만들어진다. OpenSearch 공식 문서는 native 인덱스가 "knn_vector 필드 × 세그먼트 조합마다 1개" 생긴다고 적는다. Elastic도 "세그먼트마다 HNSW 그래프를 탐색한다"고 명시한다.

즉 한 샤드에 세그먼트가 N개면 그 샤드의 HNSW 그래프도 N개다. 세그먼트가 불변이니 그래프도 세그먼트에 묶여 불변이고, 새 데이터는 새 세그먼트의 새 그래프로 들어간다.

쿼리는 전 세그먼트로 fan-out 후 병합

하나의 kNN 쿼리는 각 세그먼트의 그래프를 각각 탐색해 세그먼트별 top-k를 구한 뒤, 이들을 병합해 전역 top-k를 만든다. 그래서 세그먼트가 많으면 느려진다. 이유가 두 가지다.

  • 탐색 횟수 증가 — HNSW 탐색 비용은 벡터 수에 로그로 는다. 같은 벡터를 여러 작은 그래프로 쪼개면, 벡터 100개짜리 그래프 5개를 탐색하는 게 벡터 500개짜리 그래프 1개보다 총비용이 크다.
  • 낭비 증가 — 벡터가 세그먼트별로 의미 있게 나뉜 게 아니라서, 각 그래프에서 전역 top-k에 들지도 못할 이웃까지 탐색하는 낭비가 생긴다.

force merge가 벡터 검색을 빠르게 하는 이유

세그먼트를 1개로 force merge 하면 HNSW 그래프도 1개가 된다. fan-out이 사라져 검색이 빨라진다. OpenSearch 공식 문서가 이걸 직접 권장한다 — "샤드당 세그먼트 1개가 검색 지연 관점에서 최적"이라고.

단 조건이 붙는다. "초기 대량 적재 1회 → 이후 검색 전용" 워크로드에만 권장된다. force merge 시 벡터 자료구조가 다시 만들어지지만(rebuild), 그 뒤로는 그래프가 1개로 안정돼 fan-out이 없어지기 때문이다. 운영 함정도 하나 있다 — 병합을 돌리면 기존 그래프가 native 메모리에서 내려가고 새 그래프가 올라온다. warmup으로 미리 올려둔 그래프도 병합으로 무효화되니, warmup 대상 인덱스에는 병합을 돌리지 않는다.

엔진 — faiss / nmslib / lucene

세 엔진 모두 "세그먼트마다 그래프 1개" 구조는 공유한다. 차이는 그래프를 누가 만들고 어디에 두느냐다.

  • faiss — 현재 기본 엔진. 대규모에 권장. native 라이브러리가 세그먼트별 인덱스 파일을 만들고 PQ(양자화)·on-disk 모드를 지원한다.
  • nmslib — 원조 엔진. OpenSearch 2.19에서 deprecated, 3.0부터 신규 인덱스 생성 차단.
  • lucene — Lucene 코덱이 세그먼트당 벡터·그래프 파일을 만든다. 인덱스 크기가 가장 작고 필터링에 유리하나, 초기 버전 기준 아주 높은 recall은 약했다.

핵심 대비 — faiss/nmslib는 native 라이브러리가 별도 세그먼트 파일을 만들고, lucene 엔진은 Lucene 벡터 저장에 직접 통합된다.

그래프는 JVM 힙 밖에 산다

HNSW 그래프가 어디에 상주하는지도 엔진에 따라 갈린다.

  • faiss / nmslib — 그래프 전체를 native 메모리(off-heap)에 로드한다. 로딩량은 knn.memory.circuit_breaker.limit(기본 노드 메모리의 50%)로 제한된다. 한 머신에 담을 벡터 수는 off-heap 용량의 함수다. 세그먼트 파일이 처음 검색될 때 올라오며 warmup으로 미리 올릴 수 있다.
  • lucene — 그래프를 통째로 native로 올리지 않고, 코덱 파일을 OS 페이지 캐시를 통해 읽어 힙 밖에서 쓴다.

어느 쪽이든 원칙은 같다 — 그래프는 JVM 힙이 아니라 힙 밖에 둔다(힙은 다른 검색·집계용). 이 얘기는 OpenSearch를 벡터 DB로 굴리며 알게 된 것에서 실측으로 다뤘다.

쿼리 병렬성 — 엔진·버전에 따라 갈린다

한 kNN 쿼리가 세그먼트를 병렬로 탐색하는지, 순차로 하는지는 엔진·버전에 따라 다르므로 단정하면 안 된다.

  • Lucene / Elasticsearch — 처음엔 세그먼트를 한 번에 하나씩 순차 탐색했으나, Lucene 9.7 계열부터 "스레드풀에 여유가 있으면 세그먼트당 최대 스레드 1개"로 병렬화해 지연을 절반으로 줄였다.
  • OpenSearch — 세그먼트당 스레드 자동 병렬이 Elasticsearch만큼 기본화돼 있지 않다(별도로 켜는 concurrent segment search 기능이 있다). 정확한 기본값·설정명은 대상 버전 문서로 확인하는 게 안전하다.

병렬이 되든 안 되든 force merge로 세그먼트를 줄이는 최적화는 여전히 유효하다 — 그래프 수와 병합 낭비 자체가 줄기 때문이다.

우리 벤치와 연결 — OpenSearch QPS 특성의 근본

앞선 대규모 벤치에서 OpenSearch의 QPS가 다른 전용 벡터 DB보다 낮게 나온 게 이 구조로 설명된다.

  • 벤치는 단일 샤드 + force merge로 세그먼트를 1개(그래프 1개)로 만든 뒤 측정했다. 이건 최적 조건이다 — fan-out을 없앤 상태.
  • 그런데도 QPS 천장이 낮았던 건, 그래프가 1개면 그 그래프를 탐색하는 데 쿼리당 스레드가 사실상 1개로 묶여, 동시성을 올려도 단일 그래프 탐색이 병목이 되기 때문이다.
  • 샤드를 늘리면 그래프가 샤드 수만큼 늘어 쿼리가 샤드 병렬로 퍼지지만, 이번엔 fan-out 병합 비용과 recall 관리가 따라온다.

즉 OpenSearch 벡터 검색 튜닝은 "세그먼트를 줄여 그래프를 모으기"와 "샤드를 늘려 병렬을 얻기" 사이의 절충이다. 이 절충 구조 자체가 범용 검색엔진에 벡터를 얹었을 때의 본질적 특성이고, 전용 벡터 DB(Milvus 등)와 갈리는 지점이다(OpenSearch vs Milvus).

정리 — 체크리스트

OpenSearch를 벡터 DB로 운영·튜닝할 때 이 구조에서 나오는 판단 기준이다.

  • 읽기 전용 벡터 인덱스면 대량 적재 → force merge(세그먼트 1개) → warmup 순으로 안정화한다.
  • 계속 쓰기가 있으면 force merge를 돌리지 않는다(그래프 재생성·지연 변동 비용).
  • 대량 적재 중에는 refresh_interval을 늘리거나 꺼서 작은 세그먼트 난립을 막는다.
  • 벡터가 커지면 메모리는 힙이 아니라 native(off-heap)를 본다 — circuit_breaker.limit과 실제 벡터 크기를 같이 본다.
  • QPS가 안 오르면 샤드 수(그래프 수)와 동시성, 쿼리당 스레드 모델을 함께 본다. 세그먼트 1개는 지연에는 최적이지만 단일 그래프가 병목이 될 수 있다.

참고 링크

  • Force merge API (OpenSearch)
  • k-NN Performance tuning (OpenSearch)
  • Methods and engines (OpenSearch)
  • Multi-graph vector search (Elastic)
  • Vector search improvements in Elasticsearch & Lucene (Elastic)
  • Choose the k-NN algorithm for billion-scale (AWS)
on this page
  • 01먼저 가져갈 결론
  • 021부. Lucene 세그먼트 구조
  • inverted index — 세그먼트의 알맹이
  • 세그먼트는 불변이다
  • refresh — 새 세그먼트가 태어나는 순간
  • merge — 작은 세그먼트를 큰 것으로
  • force merge — 세그먼트를 강제로 줄이기
  • shard와 segment — 계층 관계
  • 032부. kNN 플러그인이 세그먼트 위에서 도는 방식
  • 세그먼트마다 그래프 1개
  • 쿼리는 전 세그먼트로 fan-out 후 병합
  • force merge가 벡터 검색을 빠르게 하는 이유
  • 엔진 — faiss / nmslib / lucene
  • 그래프는 JVM 힙 밖에 산다
  • 쿼리 병렬성 — 엔진·버전에 따라 갈린다
  • 04우리 벤치와 연결 — OpenSearch QPS 특성의 근본
  • 05정리 — 체크리스트
  • 06참고 링크
tags
#심화

이런 글도

  • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    > OpenSearch vs Milvus 비교의 "한국어 처리" 절을 먼저 읽으면 좋다. > 이 글은 그 한 문단을 형태소 분석기 수준까지 파고든 심화판이다. 벡터 DB를 OpenSearch에서 Milvus로 옮기는 걸 검토하면서 계속 걸린 게 하나 있었다. 의미 기반 dense 벡터는 임베딩 모델이 만드니 DB를 바꿔도 같다. 그런데 하이브리드 검색의 나...
    🗄️ db
    db
    2026.07.06
  • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    벡터 DB 어떻게 고를까에서 기능·운영 관점으로 5종을 비교했다면, 이 글은 그 다음 단계다 — 표면 기능 밑에서 실제로 무엇이 다른지를 뜯어본다. 벡터 DB 후보들을 비교하다 보면 표면 기능은 다 비슷해 보인다. 다섯 제품 모두 HNSW 로 ANN 검색을 제공하고, 다 메타데이터 필터링을 한다. 그런데 막상 운영에 올리면 메모리가 터지는 지점, 노드를 늘...
    🗄️ db
    db
    2026.06.25
  • Vespa 아키텍처 입문 — 왜 강력한데 학습 곡선이 가파른가
    벡터 DB 어떻게 고를까에서 "Vespa 는 강력하지만 학습 곡선이 가팔라서, 대규모 서빙·복잡한 ML 랭킹이 필요한 게 아니면 과한 선택이 되기 쉽다"라고 짧게 정리했었다. 정확히 뭐가 강력하고 뭐가 가파른지 근거를 파봤다. 결론부터 말하면, Vespa 는 서버 사이드에서 임의 랭킹 함수와 ML 모델 추론을 인라인 실행하는 유일한 엔진이다. 그 표현력을...
    🗄️ db
    db
    2026.07.02
  • Qdrant 벡터 데이터베이스 입문 — "컴포넌트가 단순하다"는 게 정확히 뭔가
    벡터 DB 어떻게 고를까에서 "가장 가볍게 운영하고 싶다 → Qdrant. 컴포넌트가 단순하다"라고만 짧게 썼었다. 정확히 뭐가 단순한지, Milvus 대비 무엇이 빠지는지 근거 없이는 판단할 수 없다는 피드백을 받고 다시 파봤다. 결론부터 말하면, Qdrant는 Milvus 가 별도 프로세스로 떼어낸 역할들을 전부 하나의 Rust 바이너리 안으로 흡수했다...
    🗄️ db
    db
    2026.07.02

댓글 (0)