콘텐츠로 이동

임베딩과 벡터 운영

분류: Layer 12 - AI 시스템 & LLM 애플리케이션 | 선수지식: L11-70 (토크나이저·임베딩), L12-10 (LLM API)

임베딩과 벡터 운영 — Vector Store, ANN Index, Chunking, Hybrid Search

섹션 제목: “임베딩과 벡터 운영 — Vector Store, ANN Index, Chunking, Hybrid Search”

임베딩 운영은 텍스트·이미지·문서 조각을 의미 벡터로 바꾸고, 저장하고, 빠르게 찾고, 최신성과 권한까지 유지하는 시스템이다. RAG(Retrieval-Augmented Generation, 외부 근거를 검색해 생성 입력에 붙이는 방식)·의미 검색·추천은 “모델이 답을 잘 생성하는가” 이전에 “필요한 근거를 안전하게 찾아오는가”에서 품질이 갈린다.

처음 읽을 때는 용어를 아래 순서로 잡으면 덜 헷갈린다.

  1. Embedding(임베딩): 문장·문서·이미지를 숫자 좌표로 바꾸는 함수다. 비슷한 의미가 가까운 좌표에 오도록 학습하지만, 좌표 자체가 의미의 절대 진리는 아니다.
  2. Vector(벡터): 임베딩 함수가 내놓는 순서 있는 숫자 배열이다. 768d는 숫자가 768개인 768차원 벡터라는 뜻이며, 각 차원을 사람이 직접 해석하기보다는 벡터 사이의 관계를 사용한다.
  3. Similarity와 distance: 두 벡터가 얼마나 비슷하거나 먼지 정하는 점수 규칙이다. cosine similarity가 클수록 가깝고, cosine distance는 보통 1 - similarity라 작을수록 가깝다. 인덱스와 query가 같은 규칙·정규화 계약을 써야 한다.
  4. Vector store: 벡터와 원본 문서 ID, tenant(데이터를 격리해야 하는 조직·고객 단위), 권한, 버전, 시간 같은 metadata(벡터에 딸린 검색·필터용 속성)를 함께 저장하고 검색하는 저장소다. 단순 배열 저장소가 아니라 distance search와 filter를 같이 수행해야 한다.
  5. Chunking: 긴 문서를 검색 가능한 작은 조각으로 나누는 설계다. 검색 단위가 너무 작으면 문맥이 끊기고, 너무 크면 관련 없는 문장이 context를 오염시킨다.
  6. ANN(Approximate Nearest Neighbor): 모든 벡터를 완전탐색하지 않고 가까울 가능성이 높은 후보를 빠르게 찾는 근사 최근접 이웃 검색이다.
  7. HNSW(Hierarchical Navigable Small World): ANN의 대표 그래프 인덱스다. 벡터들을 이웃 그래프로 연결해 query가 가까운 점을 따라가며 후보를 찾는다.
  8. IVF(Inverted File Index): 벡터를 centroid(군집 중심) 주변의 칸으로 나누고 query와 가까운 칸만 탐색하는 ANN 인덱스다. 더 많은 칸을 보면 recall과 latency가 함께 오른다.
  9. Hybrid search: dense embedding 검색과 BM25(Best Matching 25, 단어 일치와 희귀도를 보는 lexical ranking) 같은 lexical search를 섞는 방식이다. RRF(Reciprocal Rank Fusion, 역순위 기반 결합)는 서로 점수 단위가 다른 ranking을 순위로 합치는 대표 방법이다.
  10. Reranker: 1차 검색 후보 top-50~100을 더 비싼 cross-encoder(query와 문서를 한 입력으로 함께 읽어 관련도를 점수화하는 모델)로 다시 정렬하는 2단계 검색이다. gold 정답을 후보에 포함시키는 recall(재현율)을 먼저 만든 뒤, 결과 중 정답 비율인 precision(정밀도)을 회복한다.
  11. Quantization: fp32(32-bit floating point) 벡터를 fp16, int8, binary처럼 더 작은 표현으로 바꿔 storage와 memory를 줄이는 압축이다. PQ(Product Quantization)는 벡터를 부분 벡터로 나누고 각 부분을 codebook ID로 근사한다.
  12. Freshness: 원본 문서가 바뀐 뒤 새 chunk와 embedding이 검색에 반영되기까지의 최신성이다. freshness가 깨지면 모델은 오래된 근거로 그럴듯한 답을 한다.
  13. Permission filter: 검색 후보를 만들기 전 tenant·ACL(Access Control List)·RBAC(Role-Based Access Control) 조건으로 권한 없는 문서를 제외하는 필터다. 응답 직전에 걸면 이미 후보와 prompt에 섞였을 수 있어 leak을 막지 못한다.

임베딩 운영이 어려운 이유는 “벡터로 저장한다” 자체가 아니라, 검색 품질·latency·storage·권한·최신성이 서로 맞물리기 때문이다.

P99는 요청의 99%가 그 시간 안에 끝나는 지연 백분위다. MRL(Matryoshka Representation Learning) 은 앞쪽 차원만 잘라도 쓸 수 있도록 표현을 학습하는 방식이고, Matryoshka embedding은 이 학습 계약을 가진 임베딩이다. 이 기능은 모델별 계약이다. Vanilla BGE-M3의 공식 dense 출력은 1024d이고 카드에는 native Matryoshka 절단이 명시되지 않는다. 반면 Voyage 4 계열은 256/512/1024/2048d 출력과 Matryoshka 절단을 공식 지원한다. 따라서 BGE-M3 1024dMRL 지원 모델의 256d를 같은 구성처럼 섞지 않는다.

운영 벽왜 생기는가대응 메커니즘없으면 보이는 실패 신호
Exact NN latency wallquery마다 모든 벡터와 거리 계산을 한다ANN, HNSW, IVF, DiskANN검색 P99가 수백 ms~초 단위로 튀고 concurrency를 못 버팀
fp32 storage wall벡터 수 × 차원 × 4 bytes가 곧 RAM·SSD 비용이 된다quantization, Matryoshka, PQ1억 벡터 × 1024d fp32 = 약 400GB
dense lexical blind spot의미 압축은 고유명사·코드 식별자·법령 번호를 흐릴 수 있다BM25, sparse, hybrid, RRF”SOC2-2026”, 함수명, 조항 번호 exact match가 빠짐
chunk/context wall검색 단위와 LLM context 단위가 다르다structural/recursive chunking, overlap정답 문장이 있어도 잘린 chunk가 근거로 쓰기 어려움
first-stage precision 부족ANN·dense는 후보를 넓게 잡지만 최종 순위까지 정확하지 않다reranker, context packingtop-100에는 정답이 있는데 top-10에 안 들어옴
stale embedding원본 수정과 embedding 재계산은 별도 파이프라인이다outbox, CDC(Change Data Capture), versioning, blue-green문서는 바뀌었는데 답변은 옛 정책을 인용
permission leak앱이 ANN 후보를 받은 뒤 권한을 거르면 이미 권한 밖 문서가 전달된다server-side tenant/ACL filter, namespace, auditHTTP 200·latency 정상인데 다른 tenant 후보가 앱에 도달

여기서 RRF는 dense 검색과 BM25처럼 서로 다른 ranking 목록을 원점수 대신 순위 기반으로 섞는 방법이다. 정확한 공식보다 “서로 단위가 다른 점수를 그냥 더하지 않는다”는 감각을 먼저 잡으면 된다.

RAG에서 LLM은 보통 마지막 생성 단계만 맡는다. 그 전에 vector store가 잘못된 chunk를 가져오거나, 오래된 version을 가져오거나, 권한 없는 문서를 가져오면 모델은 그 오류를 자연어로 매끄럽게 포장한다. 그래서 이 문서의 핵심 질문은 “어떤 벡터 DB가 좋은가”가 아니라 “어떤 실패를 줄이려고 이 메커니즘이 존재하는가”다.

첫 회독에서는 세부 인덱스와 벤더 비교를 모두 외우려 하지 않는다. 먼저 vector store -> chunking -> hybrid search -> reranker -> freshness -> permission filter가 어떤 실패를 줄이는지 잡고, DiskANN·HNSW 파라미터·object-storage-native 벤더 비교는 “선택 심화”로 표시된 부분에서 이름과 쓰임새만 확인해도 충분하다.

2.5 선행 기술의 한계 — ANN·압축·재정렬이 등장한 이유

섹션 제목: “2.5 선행 기술의 한계 — ANN·압축·재정렬이 등장한 이유”

bi-encoder 임베딩(L11-70)은 텍스트를 의미 벡터로 매핑하는 데 성공했지만, 운영 단계에서 세 가지 벽에 부딪힌다. 첫 회독에서는 아래 세 문장만 잡아도 된다.

  • Exact NN(Flat / brute force)의 latency 벽: 모든 벡터와 거리를 계산하면 데이터가 커질수록 검색 시간이 선형으로 는다. 그래서 HNSW, IVF, DiskANN 같은 ANN 인덱스가 등장했다.
  • fp32 메모리 비용 벽: 벡터 수와 차원이 커지면 RAM·SSD 비용이 곧바로 커진다. 그래서 int8, binary, PQ(Product Quantization), Matryoshka 같은 quantization과 차원 축소가 등장했다.
  • 단일 dense vector의 lexical blind spot: 고유명사·코드 식별자·법령 번호·신조어는 의미 공간에서 흐려질 수 있다. 그래서 BM25와 dense를 섞는 hybrid search, 그리고 cross-encoder reranker가 등장했다.

이 토픽의 세 축(ANN · quantization · hybrid+rerank)은 각 한계에 1:1 대응한다. 즉, “더 멋진 벡터 DB”를 외우기 전에 느린 검색, 비싼 저장, 고유명사 누락이라는 세 실패를 먼저 기억한다.

세 축은 독립 최적화가 아니다. ANN이 너무 좁게 후보를 만들면 reranker는 정답을 복구할 수 없고, quantization이 후보 순서를 크게 흔들면 hybrid가 보완해야 할 폭도 커진다. chunking이나 모델 버전이 바뀌면 같은 문서도 기본적으로 다른 좌표에 놓이므로 index 계약과 gold 평가를 함께 갱신해야 한다. 단, Voyage 4 계열처럼 벤더가 cross-model 호환을 명시한 모델군은 좁은 예외이며, 이 경우에도 차원·quantization과 retrieval 회귀를 검증한다. 결국 운영 철학은 싸고 넓은 후보 생성에서 recall을 지키고, 비싸고 좁은 재정렬에서 precision을 높이며, 권한과 version은 두 단계 모두의 경계로 강제한다는 것이다.

정량 심화: latency·storage·비용 규모감

아래 숫자는 성격이 서로 다르다. 출처에서 corpus·metric·hardware를 복원할 수 있는 값은 해당 논문의 조건부 관찰로, 그렇지 않은 값은 이 문서의 가상 planning scenario 또는 초기 측정 예로 표시한다. 어느 값도 제품의 보편 성능 계약이나 선택 cutoff가 아니다.

  • 가상 planning scenario - Flat/HNSW latency·memory: 원출처의 benchmark·embedding model·corpus·hardware를 복원할 수 없는 기존 수치를 학습용 초기 측정 예로 보존한다. 1M × 768d fp32, 200 query에서 Flat 12.4s·18.7GB·exact-candidate recall 100%, HNSW 0.95s·13.2GB·exact-candidate recall@10 0.962를 관찰했다고 가정한다. Flat의 100%는 같은 corpus·distance의 exact top-k 기준이지 gold relevance accuracy가 아니다. 이 수치로 다른 환경의 latency나 memory를 예측하면 안 된다.
  • 가상 planning scenario - HNSW throughput: default HNSW가 Flat 대비 candidate recall 96% @ throughput 44×였다는 값도 출처 조건을 복원할 수 없는 별도 초기 관찰 예다. 앞 bullet과 같은 실행에서 나온 값으로 합치거나 HNSW의 보편 배수로 인용하지 않는다.
  • DiskANN 논문의 조건부 관찰: NeurIPS 2019 논문은 텍스트 embedding model이 아니라 SIFT1B bigann의 10억 개 128차원 uint8 이미지 descriptor와 Euclidean distance를 사용했다. 10,000 query를 16개 thread로 실행한 단일 workstation 조건에서 64GB RAM과 retail SSD를 사용해 >5,000 QPS, 평균 latency <3ms, 95%+ 1-recall@1을 보고했다. 이는 해당 corpus·index·hardware의 관찰이며, DiskANN이 Vamana graph와 full-precision vector를 SSD에 두고 압축 vector를 RAM에 두는 설계를 검증한 사례다.
  • 가상 planning scenario - storage TCO: HNSW 500GB ≈ $2,000/월과 DiskANN 32GB RAM + SSD ≈ $400/월은 원출처와 가격표 시점을 복원할 수 없는 비용 가정이다. 메모리 상주 index를 SSD-resident index와 비교하는 계산 연습으로만 사용하며, DiskANN이나 특정 제품의 일반 비용으로 해석하지 않는다.
  • 계산 가능한 fp32 payload: 1B × 768d × 4 bytes의 raw vector payload는 약 3TB다. HNSW graph를 포함한 4~5TB RAM은 graph 설정에 따라 달라지는 이 문서의 용량 planning 범위다. → Quantization(§3.7)이 등장한 이유: int8은 숫자당 byte 기준 4×, packed binary는 bit 기준 32× raw payload를 줄일 수 있다. metadata·index·replica·backup은 이 비율에 포함되지 않는다.
  • 가상 planning scenario - quantization 비용·품질: 10M 기사 embedding을 fp32에서 int8로 바꿔 vector DB 비용이 **$5,600/월 → $1,400/월(75% ↓)**가 되고 사용자 검색 품질 변화가 감지되지 않았다는 값은 원출처의 모델·corpus·hardware·가격 조건을 복원할 수 없는 초기 예다. binary 단독 품질 90~98%, fp32 rescoring 후 99%+도 같은 성격의 가상 관찰 범위이며 보편 손실률이 아니다.
  • 가상 planning scenario - hybrid·reranker 개선 폭: recall@10 +10~30%는 모델·dataset·후보 폭을 고정한 초기 관찰 범위로만 사용한다. BM25로 lexical match를 보완하고 reranker가 top-100을 정렬한다는 메커니즘은 유지하되, 실제 개선 폭은 자체 gold query로 다시 측정한다.

이 토픽이 사라지면 RAG는 brute force NN의 초 단위 latency, TB급 RAM 요구, 고유명사 검색 불가 상태로 즉시 회귀한다.

3.1 대표 운영 모델 훑기 — Vector Store 종류

섹션 제목: “3.1 대표 운영 모델 훑기 — Vector Store 종류”

Vector store는 벡터만 저장하는 테이블이 아니다. 운영에서는 최소 다섯 가지를 함께 책임진다.

  • embedding: 검색에 쓰는 숫자 벡터
  • doc_id / chunk_id: LLM에게 보여줄 원본 근거로 되돌아가기 위한 ID
  • metadata: tenant_id, acl, language, doc_type, version, updated_at
  • index: Flat, HNSW, IVF, DiskANN 같은 검색 구조
  • consistency policy: upsert, delete, version 교체, reindex 중 어떤 결과를 보장하는지

이 중 metadata filter가 특히 중요하다. vector similarity가 아무리 높아도 사용자가 볼 수 없는 문서라면 검색 후보가 되면 안 된다. “일단 검색하고 나중에 권한 필터”는 RAG에서는 위험하다. 후보가 reranker나 prompt에 들어가는 순간 권한 밖 정보가 모델 입력으로 넘어갈 수 있기 때문이다.

아래 표는 제품 순위표가 아니라 대표 운영 모델을 훑는 용도다. 첫 회독에서는 도구 이름을 외우지 말고 라이브러리, RDBMS, managed SaaS, self-host vector DB, 검색 엔진 통합, object-storage-native처럼 어떤 운영 부담을 가져가는지만 본다.

3.1 대표 Vector Store 운영 모델 비교

**FAISS** (Meta)

운영 모델: 라이브러리 (in-process)

특징: 가장 빠름. persist는 직접. 단일 머신

**pgvector** (Postgres extension)

운영 모델: RDBMS

특징: SQL·트랜잭션 통합. 차원·filter·update 부하를 Postgres와 함께 측정

**Pinecone**

운영 모델: SaaS

특징: managed, 운영 부담 없음. 비싸짐

**Weaviate**

운영 모델: self-host or cloud

특징: hybrid search 빌트인, GraphQL

**Qdrant**

운영 모델: self-host or cloud

특징: Rust 기반, payload filter 강함

**Milvus / Zilliz**

운영 모델: self-host or cloud (Zilliz)

특징: 분산 indexing·serving, shard·replica 운영

**Chroma**

운영 모델: embedded / server

특징: prototype에 편함

**Vespa**

운영 모델: self-host

특징: 검색 엔진 + vector·ranking 통합

**Elasticsearch / OpenSearch**

운영 모델: self-host or AWS

특징: 검색·분석 엔진 + vector/hybrid 통합

**Turbopuffer**

운영 모델: SaaS (object storage)

특징: object storage native, serverless 검색
  • 트랜잭션·권한 SQL 결합이 우선: pgvector. 원본 row와 vector의 원자적 갱신, 기존 backup·monitoring을 재사용하기 쉽다.
  • 검색 계층 독립 확장과 payload filter가 우선: Qdrant·Weaviate. 검색 자원과 update lifecycle을 Postgres에서 분리하고 싶을 때 비교한다.
  • pgvector와 Qdrant를 가르는 측정 축: vector 차원, filter selectivity(전체 중 조건을 통과하는 비율), update/delete rate, index까지 포함한 memory budget, 목표 동시성에서의 latency-recall curve다. 여기에 PostgreSQL 운영 ownership과 SQL·트랜잭션 통합 이익, 별도 검색 cluster를 소유할 팀과 장애 복구 경계를 함께 본다.
  • 분산 scale-out 운영 모델이 필요: Milvus 또는 Vespa를 비교한다. 단순 vector count보다 shard 재분배, 복제, 장애 복구를 직접 운영할 준비가 기준이다.
  • 운영 부담 회피: Pinecone, Zilliz, Turbopuffer
  • Postgres 생태계: pgvector + pg_search (BM25 결합)
  • 검색 엔진과 통합: Elasticsearch / OpenSearch
  • tenant별 long-tail RAG, object storage 중심 운영: Turbopuffer

vector count만으로 제품 후보나 migration 시점을 정하지 않는다. 후보마다 같은 vector 차원, filter 분포, update/delete trace, 동시성, cold/warm cache 조건을 재생하고, 허용 memory 안의 P95/P99와 latency-recall curve를 비교한다. 측정 결과와 함께 데이터·검색 계층의 운영 ownership, SQL·트랜잭션 결합 필요, backup·복구 책임이 최종 선택 기준이다.

이 선택 기준은 제품 순위표가 아니라 운영 부담의 분해다. 이미 Postgres에서 원본 문서와 권한을 관리한다면 pgvector가 단순하고, 검색팀이 OpenSearch analyzer와 dashboard를 운영 중이면 OpenSearch의 진입 비용이 낮다. 반대로 tenant별 namespace가 많고 대부분이 long-tail cold corpus라면 object-storage-native 계열의 문제의식이 맞을 수 있다. 서비스별 기능·가격·제한은 바뀌므로, 여기서는 “저장 구조와 운영 모델이 어떤 실패를 줄이는가”를 중심으로 읽는다.

선택 심화: Turbopuffer와 OpenSearch의 운영 모델 비교

OpenSearch는 원래 검색·분석 엔진이다. Lucene 기반 inverted index, analyzer, BM25, aggregation, dashboard, log/observability 생태계가 강하고, 여기에 k-NN vector search와 neural/hybrid search 기능이 붙었다. 그래서 “문서 검색 + 로그 분석 + faceted search + 운영 대시보드 + vector search”를 한 시스템에서 다루고 싶을 때 자연스럽다.

Turbopuffer는 반대로 LLM/RAG 시대의 vector·full-text first-stage retrieval 문제에서 출발한 서비스다. RAG 코퍼스가 커져도 모든 tenant와 namespace가 항상 hot하지는 않다. 기존 vector DB나 search cluster처럼 많은 데이터를 RAM/SSD에 계속 올려두면 long-tail 데이터까지 비싼 compute/storage를 점유한다. Turbopuffer의 핵심 가정은 “상태는 object storage에 싸게 두고, query compute는 stateless하게 두며, 자주 쓰는 namespace만 NVMe/memory cache로 데운다”이다.

해결 메커니즘은 세 부분이다.

  1. Object storage가 source of truth: namespace별 prefix에 WAL과 index 파일을 두고, 쓰기가 성공하면 object storage에 durably written된 것으로 본다. compute node는 상태를 덜 들고 있어 serverless·multi-tenant 운영이 쉬워진다.
  2. Warm cache로 latency 회복: 첫 query는 object storage에서 읽어 cold latency가 생길 수 있지만, 이후 같은 namespace는 NVMe/memory cache와 query routing으로 warm latency가 낮아진다. latency-sensitive 서비스는 사용자 세션 시작 시 cache warm hint를 보내는 식으로 완충한다.
  3. First-stage retrieval에 집중: vector ANN, exact kNN, BM25 full-text, sparse vector, metadata filter, multi-query/RRF(Reciprocal Rank Fusion, 여러 ranking을 합치는 순위 결합)를 제공해 “수백만 문서 → 수십~수백 후보”로 줄인다. 복잡한 rerank·business ranking은 애플리케이션 코드에서 2단계로 수행하는 쪽을 권한다.
기준TurbopufferOpenSearch선택 판단
정체성object-storage-native vector/full-text DBopen-source search/analytics suite + vector engineRAG retrieval 전용이면 Turbopuffer, 검색 플랫폼이면 OpenSearch
저장 구조object storage + NVMe/memory cacheLucene index/shard 중심 cluster 운영long-tail·cold corpus는 Turbopuffer 경제성이 좋음
검색 기능ANN, exact kNN, BM25, sparse vector, filter, multi-query/RRFBM25, analyzer, k-NN, neural query, hybrid query, search pipeline, aggregationrich query DSL·분석·faceting은 OpenSearch가 강함
운영 부담cluster/shard를 덜 관리하지만 namespace·cache·consistency trade-off 이해 필요shard sizing, JVM/heap, refresh, replica, ML node, index lifecycle 관리 필요운영팀이 이미 OpenSearch를 잘 쓰면 OpenSearch 진입 비용이 낮음
latencywarm query는 낮지만 cold query·cache miss가 tail latency가 됨provisioned cluster면 예측 가능하지만 capacity 비용을 계속 지불사용자별 namespace가 많고 접속 전 prewarm 가능하면 Turbopuffer 적합
쓰기 특성object storage WAL 덕분에 durable·high throughput, 대신 write commit latency와 async indexing 고려near real-time indexing, bulk/index refresh 튜닝 중요heavy update/delete workload는 사전 벤치 필수
2단계 ranking앱 코드에서 rerank·business logic 조립 권장search pipeline/query DSL 안에 많은 검색 로직을 넣기 쉬움검색 로직을 코드로 유지하고 싶으면 Turbopuffer 쪽이 단순

실무 결론: “이미 OpenSearch로 로그·문서 검색·분석을 운영하고 있고, vector search를 추가하고 싶다”면 OpenSearch가 자연스럽다. “B2B SaaS처럼 tenant별 namespace가 많고, 대부분은 가끔만 검색되며, RAG 후보군을 저렴하게 뽑는 것이 핵심”이면 Turbopuffer가 등장한 문제의식과 맞다. 단, Turbopuffer도 cold query, write latency, built-in reranker/embedding 부재, 상용 서비스 의존성을 감수해야 한다.

3.2 ANN (Approximate Nearest Neighbor) 인덱스

섹션 제목: “3.2 ANN (Approximate Nearest Neighbor) 인덱스”

ANN 전에 고정할 계약: vector, 정규화, 거리

섹션 제목: “ANN 전에 고정할 계약: vector, 정규화, 거리”

벡터 검색은 “의미”라는 추상어를 곧바로 검색하지 않는다. 기본 계약은 같은 임베딩 모델이 만든 query vector와 document vector를 같은 차원에 놓고, cosine·dot product·Euclidean distance 중 하나로 비교하는 것이다. 벤더가 cross-model 호환을 명시적으로 보장한 모델군만 이 기본 계약의 예외가 될 수 있다. Cosine similarity는 두 벡터의 방향을 보고 양의 배율 변화에는 불변이다. 즉 lambda > 0일 때 cos(q, lambda*x) = cos(q, x)다. 반면 **dot product(내적)**는 방향과 크기를 함께 반영하고, **Euclidean distance(L2 거리)**는 좌표 사이의 직선거리를 보므로 정규화하지 않은 벡터의 크기 변화에 따라 순위가 달라질 수 있다.

모든 query와 document를 L2 norm 1인 unit vector로 정규화하면 cosine similarity와 dot product 값이 같아져 두 검색의 순위도 같다. unit vector x, y에서는 ||x-y||^2 = 2 - 2*cos(x,y)이므로 cosine을 크게 하는 순위와 L2 또는 L2 제곱을 작게 하는 순위도 같다. 이 동치는 양쪽 벡터를 같은 방식으로 unit normalization했다는 계약 안에서만 성립한다.

따라서 distance metric은 구현 옵션이 아니라 데이터 계약이다. document를 cosine 기준으로 색인해 놓고 query에만 다른 정규화나 호환 보장 없는 모델을 쓰면 요청은 성공해도 순위 의미가 깨진다. 또한 cosine 0.8이 모든 모델·도메인에서 같은 품질을 뜻하지 않으며, cross-model cosine threshold가 호환성을 증명하지도 않는다. 임베딩 공간은 모델이 학습한 상대 좌표계이므로, 호환 여부는 공식 벤더 계약으로, 품질 threshold는 자체 gold dataset으로 확인한다. **Gold dataset(골드 데이터셋)**은 query마다 사람이 검수한 관련 문서와 근거를 기록해 둔 고정 평가 집합이다. 모델·index·chunking을 바꿔도 같은 질문과 정답으로 회귀를 비교하게 해준다.

작은 손계산으로 경계를 확인해 보자. query q=[1, 0], 문서 a=[0.8, 0.6], b=[0, 1]이면 세 벡터의 길이는 모두 1이다. cos(q,a)=dot(q,a)=0.8, cos(q,b)=dot(q,b)=0이고, L2(q,a)^2=0.4, L2(q,b)^2=2이므로 세 metric 모두 a를 먼저 둔다. 반면 정규화하지 않은 c=[2, 0]을 넣으면 cos(q,c)=1이라 cosine은 c를 a보다 앞에 두지만, L2(q,c)=1이고 L2(q,a)=sqrt(0.4), 약 0.632이므로 L2는 a를 c보다 앞에 둔다. dot product도 dot(q,c)=2로 크기에 영향을 받는다. 이 반례가 cosine은 양의 scale에 불변이고, cosine·dot·L2 순위 동치가 unit vector에만 성립한다는 뜻이다.

NN(Nearest Neighbor, 최근접 이웃) 검색은 query vector와 가장 가까운 문서 vector를 찾는 일이다. Exact NN은 모든 벡터와 거리를 계산하므로 정확하지만, 코퍼스가 커질수록 latency가 선형으로 증가한다. 1M개 문서, 768차원 벡터라면 query 하나마다 대략 1,000,000 × 768개의 차원 비교가 필요하다. query가 200개만 동시에 들어와도 “정확하지만 느린” 방식은 챗봇 UX의 검색 단계로 쓰기 어렵다.

**ANN(Approximate Nearest Neighbor, 근사 최근접 이웃)**은 이 벽을 낮추기 위해 등장했다. 근사의 의미는 “아무 결과나 대충 찾는다”가 아니라, 정확한 top-k를 매번 보장하지 않는 대신 가까울 가능성이 높은 후보를 훨씬 빨리 찾는다는 뜻이다. 그래서 ANN의 품질은 항상 latency와 recall@k를 같이 봐야 한다.

공통 toy corpus: candidate 폭과 filter 위치를 손으로 보기

섹션 제목: “공통 toy corpus: candidate 폭과 filter 위치를 손으로 보기”

이 문서의 실험은 아래 다섯 문서를 공통으로 사용한다. 실제 임베딩 대신 결과가 항상 같은 2차원 벡터를 써서, 프레임워크나 provider 없이도 순위를 손으로 검산한다. query는 tenant acmeSOC2-2026 감사 정책이고 q=[1,0]이다. 사람이 검수한 정답 문서 집합은 G={d1,d3}라고 두자.

IDtenant문서 요약vectorcos(q,d)gold 관련 문서인가
d1acmeSOC2-2026 증적 보관 7년[0.8, 0.6]0.800
d2betaSOC2-2026 비공개 예외[1.0, 0.0]1.000권한 밖
d3acmeSOC2-2026 감사 일정과 제출 의무[0.9, 0.1]약 0.994
d4acmeSOC2-2026 증적 통제 개요[0.7, 0.7]약 0.707아니오
d5acme환불 정책[0.0, 1.0]0.000아니오

전체에서 dense top-2를 먼저 뽑으면 [d2,d3]이다. DB executor가 ANN index scan 뒤 같은 SQL 안에서 WHERE tenant='acme'를 적용하면 애플리케이션에는 [d3] 하나만 반환된다. 권한 밖 d2가 DB 경계를 넘지는 않았으므로 이것만으로 permission leak은 아니지만, recall=1/2=50%와 requested K보다 적은 underfill이 생긴다. 반환된 행만 분모로 보는 precision_returned=1/1=100%와 요청한 두 자리를 분모로 고정하는 precision@2=1/2=50%가 갈리는 반례다.

반대로 acme를 후보 생성 전에 pre-filter한 뒤 top-2를 뽑으면 [d3,d1]이고 recall, precision_returned, precision@2가 모두 100%다. 엔진이 구조상 executor-side post-filter를 한다면 후보 폭을 2에서 4로 늘린 뒤 filter하고 top-2를 잘라 [d3,d1]을 회복할 수 있지만, 더 많은 graph 탐색과 latency를 지불한다. 이 문제와 response-side permission leak을 구분해야 한다. 애플리케이션이 [d2,d3]을 전달받아 reranker·prompt·cache·log에 넣은 뒤 최종 응답 직전에 d2를 거르면, 화면에 d2가 보이지 않아도 권한 밖 데이터가 이미 신뢰 경계를 넘어 leak이 발생한 것이다.

여기서 **recall(재현율)**은 gold 정답 중 검색한 비율이고, **precision(정밀도)**은 검색 결과 중 정답 비율이다. candidate 폭을 늘리면 보통 recall은 오르지만 오답 후보와 계산량도 늘어난다. 보안 filter는 후보 폭으로 타협할 대상이 아니며 반드시 강제하고, 언어·날짜 같은 품질 filter만 recall을 보며 완화한다.

ANN benchmark에서 자주 쓰는 exact-candidate recall@k는 이 gold relevance recall과 분모가 다르다. 같은 corpus, query vector, distance metric의 Flat exact top-k를 정답 후보 집합으로 두고 ANN이 그중 몇 개를 재현했는지 본다. 따라서 Flat은 정의상 exact-candidate recall이 100%지만, Flat top-k 자체가 사람의 gold 관련 문서를 놓쳤다면 relevance accuracy는 낮을 수 있다. 아래 Flat 100%, HNSW 95~99% 표기는 전자만 뜻하며, HNSW 범위는 §2.5 가상 planning scenario의 조건부 예시다.

작은 숫자로 감을 잡으면 다음과 같다.

Flat exact:
모든 점과 cosine 계산
exact-candidate recall@10 = 100% (같은 corpus·distance의 기준선)
데이터가 커질수록 latency = O(N)
HNSW:
이웃 그래프를 따라 가까운 후보만 탐색
exact-candidate recall@10 = 95~99% (§2.5 가상 planning scenario의 초기 목표 예시)
latency는 efSearch와 graph 품질에 좌우
알고리즘메모리latencyFlat 대비 exact-candidate recall 예시비고
Flat100% (원본)O(N)100% (정의상)brute force 기준선. 차원·동시성·SIMD·filter 조건을 측정
HNSW~1.5× 원본 (§2.5 가상 planning scenario)빠른 경우가 많음95~99% (§2.5 가상 planning scenario)graph·parameter·데이터 분포에 의존
IVF~원본k-means cluster + 탐색90~95%centroid·nprobe·데이터 분포에 의존
IVF-PQ원본의 4~32× ↓빠른 경우가 많음85~95%Product Quantization 구성과 rescoring 여부에 의존
DiskANN디스크 사용SSD 특성에 의존95%+ (§2.5 NeurIPS 2019 SIFT1B 관찰)Vamana graph·SSD·cache·query workload의 benchmark 관찰 예
SCANN메모리·디스크하드웨어에 의존95%+partition·quantization·hardware별 benchmark 관찰 예

HNSW는 “가까운 친구의 친구를 따라가면 더 가까운 점을 찾을 수 있다”는 그래프 탐색이다. 위쪽 sparse layer에서는 멀리 건너뛰고, 아래쪽 dense layer에서는 후보를 촘촘히 훑는다. centroid를 학습하는 단계가 없어 새 분포에서도 바로 build를 시작할 수 있고, 같은 메모리급 ANN 중 speed-recall 곡선이 좋은 경우가 많다. 대가는 각 vector의 이웃 edge를 보관하는 메모리, 높은 build 비용, 삽입·삭제가 누적될 때 graph 품질과 update 비용을 관리해야 한다는 점이다. 그래프 탐색이 실무에서 sublinear하게 동작하는 경우가 많아도 입력 분포와 graph 품질에 무관한 O(log N) 보장은 아니다.

IVF는 철학이 다르다. 대표 표본으로 centroid를 학습해 벡터 공간을 여러 cell로 나누고, 각 vector ID를 가장 가까운 cell의 inverted list에 넣는다. query 때는 가까운 cell을 nprobe개만 열어 그 안에서 후보를 찾는다. nprobe가 작으면 빠르지만 cell 경계 반대편의 실제 이웃을 놓칠 수 있고, 크게 하면 recall과 latency가 함께 올라 exact search에 가까워진다. HNSW의 후보 폭이 efSearch라면 IVF의 후보 폭은 주로 nprobe와 cell 크기가 만든다.

centroid training은 IVF의 장점이자 lifecycle 경계다. bulk ingest 전에 대표 표본으로 한 번 학습해 inverted list를 병렬로 채우기 쉽고, HNSW의 graph edge 메모리를 피하며, IVF-PQ처럼 compression을 결합하기도 좋다. 반면 새 tenant·언어·상품군이 들어와 vector 분포가 바뀌면 특정 cell만 비대해지거나 query가 잘못된 cell을 보게 된다. list 크기 편향, 같은 nprobe에서 recall 하락, scanned candidates 급증이 distribution drift 신호다. 이때 nprobe만 키우는 것은 임시 완화이고, 대표 표본 재수집·centroid retraining·index rebuild를 검토해야 한다.

toy corpus에서 d1과 d3이 서로 다른 IVF cell 경계에 걸렸다고 하자. nprobe=1이 d3의 cell만 보면 d1은 reranker 입력에도 들어오지 않는다. nprobe=2로 두 cell을 보면 recall이 회복되지만 조회 범위가 늘어난다. 이 예는 HNSW와 IVF의 내부 구조는 달라도, 후보 폭을 줄여 latency를 얻고 recall을 내주는 ANN 철학은 같다는 것을 보여준다.

조건HNSW가 유리한 이유IVF 계열이 유리한 이유
빠른 query와 높은 recall무학습 graph 탐색의 speed-recall 곡선이 좋은 경우가 많음충분한 nprobe가 필요해지면 탐색 cell과 후보 수가 늘어남
메모리가 빠듯함graph edge overhead 때문에 불리edge 메모리가 없고 IVF-PQ compression을 결합하기 쉬움
대규모 bulk buildgraph 연결 생성이 CPU·시간을 많이 사용centroid 학습 뒤 list assignment를 병렬 bulk 처리하기 쉬움
지속적인 insert/delete온라인 삽입은 가능하지만 graph 품질·삭제 누적 관리 필요새 데이터가 기존 분포와 비슷할 때 list append가 단순
데이터 분포가 자주 바뀜centroid retraining은 없지만 graph recall은 계속 측정centroid drift를 측정하고 주기적 retraining·rebuild가 필요
강한 압축이 핵심별도 quantization을 붙여야 함IVF-PQ로 coarse cell 탐색과 codebook compression을 함께 설계
  • M: HNSW graph의 이웃 연결 수 (16~64). 클수록 recall 가능성과 메모리·build/update 비용이 함께 오른다. 기존 graph의 M을 바꾸려면 rebuild가 필요하다.
  • efConstruction: HNSW 노드를 넣을 때 좋은 이웃을 찾는 build-time 탐색 폭 (100~500). 높이면 build가 느려지지만 graph 품질이 좋아질 수 있다. 기존 노드에는 소급되지 않으므로 전체 효과를 비교하려면 rebuild한다.
  • efSearch: query마다 유지하는 후보 폭 (50~500). query/session-time knob라 rebuild 없이 바꿀 수 있고, 늘리면 보통 recall과 latency가 함께 오른다.
  • IVF의 centroid 수·training sample·PQ codebook은 build-time 계약이라 바꾸면 retraining/rebuild가 필요하다. nprobe는 query-time knob라 rebuild 없이 latency-recall 곡선을 조절한다.

운영 실험 시작값으로 M=32, efConstruction=200, efSearch=100을 둘 수 있지만 범용 정답은 아니다. efSearch를 query-time sweep해 한계를 먼저 찾고, 그래도 graph recall이 부족할 때만 rebuild 비용을 감수하고 M·efConstruction 조합을 비교한다.

초기 실험 트리거 (silent failure 예방):

  • HNSW graph와 vector를 합친 실측 memory가 예산을 넘으면 DiskANN 또는 IVF-PQ를 비교한다. 코퍼스 raw payload × 1.5는 §2.5 가상 planning scenario의 graph 설정별 초기 용량 추정 예시일 뿐이며, 그대로 강행했을 때 OOM 또는 swap과 P99 급등이 나타나는지 RSS·page fault로 확인한다.
  • exact-candidate recall@10이 초기 목표 0.9보다 낮으면 efSearch를 100→200처럼 sweep하고, 그래도 미달이면 M 16→32 조합을 rebuild해 비교할 수 있다. 숫자는 시작점이며, efSearch를 올려 latency만 증가하면 embedding·chunking·hybrid·filter를 분리한다.
  • Flat과 ANN의 경계는 vector count가 아니라 차원, SIMD·GPU 사용, cold/warm memory, 동시성, filter selectivity, latency budget으로 정한다. 같은 workload trace에서 Flat 기준선이 budget을 만족하면 index 복잡성을 미룰 수 있고, 넘으면 HNSW·IVF·DiskANN의 latency-recall curve를 비교한다.

Chunking은 원본 문서를 embedding과 검색에 맞는 조각으로 나누는 일이다. 임베딩 모델은 보통 문장 또는 짧은 문서 조각을 하나의 vector로 압축한다. 30페이지 정책 문서 전체를 한 vector로 만들면 여러 주제가 한 좌표에 섞이고, 한 문장을 너무 잘게 자르면 LLM이 답변에 필요한 조건과 예외를 잃는다.

worked example로 보면 더 쉽다.

원문:
환불은 결제 후 7일 안에 가능하다.
단, 이미 사용한 크레딧은 환불 대상에서 제외한다.
엔터프라이즈 계약은 별도 약관을 따른다.
나쁜 chunk:
"환불은 결제 후 7일 안에 가능하다."
좋은 chunk:
"환불은 결제 후 7일 안에 가능하다. 단, 이미 사용한 크레딧은 제외한다."

나쁜 chunk는 recall에는 걸릴 수 있지만, 답변에는 필요한 예외 조건이 빠진다. 반대로 여러 섹션을 한 chunk에 넣으면 “환불” query에 계약, 보안, 결제 실패 문장까지 같이 들어와 context precision이 떨어진다. chunking은 검색 recall과 LLM context precision 사이의 균형점이다.

chunking은 저장 전처리로 끝나지 않고 검색 단위를 정의한다. gold dataset의 정답이 문서 ID만 가리키면 어떤 chunk가 근거 문장을 포함해야 하는지 평가할 수 없으므로, 가능하면 query -> relevant_doc_id -> evidence span을 함께 기록한다. 그러면 작은 chunk는 evidence span을 잘라 먹는지, 큰 chunk는 관련 없는 문장을 얼마나 함께 싣는지 분리해서 볼 수 있다.

또 하나의 반례는 overlap을 무조건 늘리는 것이다. 800토큰 chunk에 400토큰 overlap을 주면 경계 문맥은 보존되지만 같은 근거가 여러 chunk로 복제된다. top-5가 사실상 같은 문단의 복사본으로 채워지면 문서 recall은 높아 보여도 정보 다양성과 context precision은 낮다. overlap은 경계 손실을 줄이는 보험이지, dedupe와 chunk identity 없이 늘릴 무료 품질 노브가 아니다.

방식설명장점단점
Fixed-sizeN 토큰 단위 (예: 512)단순, 빠름의미 단위 깨짐
Recursiveparagraph → sentence → token 순 분할의미 구조 부분 보존LangChain RecursiveCharacterTextSplitter 표준
Semantic임베딩 거리 기반 분할의미 단위 보존비용 ↑, latency ↑
Structuralmarkdown heading·HTML 구조문서 구조 보존구조 없는 텍스트엔 무력
Sliding windowoverlap 두고 슬라이드경계 정보 보존중복 ↑
  • 일반 텍스트: chunk 5121024 토큰, overlap 50100
  • 코드: 함수·class 단위 (structural)
  • 표·리스트: 행 단위 또는 entire structural unit 보존
  • 한국어: 토큰 효율을 감안한 시작점으로 영어 대비 ~50% chunk size를 두되, 실제 tokenizer의 token 수로 다시 정한다.

숫자 감각도 같이 잡아야 한다. 800토큰 chunk를 top-10으로 그대로 넣으면 검색 context만 8,000토큰이다. 여기에 system prompt, user query, citation format, 이전 대화가 붙으면 16k context 모델에서도 여유가 빠르게 줄어든다. 그래서 production에서는 retrieve top-100 → rerank top-10 → context top-5~7처럼 후보군과 최종 context를 분리한다.

Late chunking(Jina, 2024~2025 계열)은 문서 전체를 먼저 encoder에 통과시킨 뒤 token-level 표현에서 chunk vector를 추출하는 접근이다. chunk 경계 밖의 앞뒤 문맥을 어느 정도 반영할 수 있어 context-aware chunking에 가깝다. 단, 비용과 모델 지원 여부가 붙으므로 “기본 chunking 대체재”라기보다 긴 문서에서 경계 손실이 실제 품질 병목일 때 검토한다.

Dense search는 문장의 의미를 벡터 거리로 찾는다. “휴가 신청 절차”와 “연차 요청 방법”처럼 표현이 달라도 의미가 가까운 문서를 잘 찾는다. 하지만 고유명사, 법령 번호, 에러 코드, 함수명처럼 정확한 문자열이 중요한 query에서는 의미 압축이 오히려 단서를 흐릴 수 있다.

BM25는 query 단어와 문서 단어가 얼마나 잘 겹치는지, 단어가 얼마나 희귀한지, 문서가 얼마나 긴지를 고려하는 lexical ranking이다. 의미가 비슷한 paraphrase에는 약하지만, "SOC2-2026", "ERR_AUTH_401", "Article 17"처럼 exact match가 중요한 단서에는 강하다.

BM25가 단순 문자열 포함 검사와 다른 이유는 세 가지다. 여러 문서에 흔한 단어보다 드문 단어의 IDF(Inverse Document Frequency, 역문서 빈도)를 크게 보고, 한 문서에 같은 단어가 반복돼도 점수를 무한히 선형 증가시키지 않으며, 긴 문서가 단어를 많이 포함했다는 이유만으로 유리해지지 않도록 길이를 보정한다. 그래서 정책은 거의 모든 문서에 있어 약한 신호이고 SOC2-2026은 드물어 강한 신호가 된다. 반대로 연차 요청 방법으로 휴가 신청 절차를 찾는 paraphrase에는 단어 겹침이 적어 dense search가 더 강하다.

Hybrid search는 둘 중 하나를 고르는 것이 아니라 서로의 blind spot을 보완한다.

1. BM25 (lexical) → top-100
2. Dense embedding (semantic) → top-100
3. RRF (Reciprocal Rank Fusion): score = Σ 1/(k + rank_i)
4. 합쳐서 top-K 반환

작은 반례:

querydense만 썼을 때 생길 수 있는 실패BM25가 보완하는 단서
SOC2-2026 보관 정책”보안 감사 정책” 일반 문서가 위로 옴SOC2-2026 exact match
getUserById timeout”사용자 조회 지연” 문서와 섞임함수명 getUserById
제17조 환불 예외환불 일반 가이드가 위로 옴제17조와 “예외” exact term
ERR_BILLING_042billing 관련 FAQ만 찾고 코드 누락에러 코드 문자열

공통 toy corpus에서도 dense 순위는 [d2,d3,d1,d4,d5]이고 권한 pre-filter 뒤에는 [d3,d1,d4,d5]다. lexical 순위는 exact term 수에 따라 [d1,d4,d3,d5]로 둔다. k=60이면 d1은 dense 2위·lexical 1위라 1/62+1/61=0.032522, d3은 dense 1위·lexical 3위라 1/61+1/63=0.032266이다. 따라서 d1의 RRF 점수가 엄격하게 더 크며, 동률을 입력 순서로 깨서 개선처럼 보인 결과가 아니다. 같은 점수가 생길 수 있는 일반 경우에는 fixture처럼 document ID 오름차순을 deterministic tie-break로 둔다. 기본 RRF는 각 검색기의 reciprocal rank를 가중치 없이 더하므로 모든 검색기에 같은 weight를 준다. k는 검색기별 weight가 아니라 순위 기여 곡선의 모양을 정한다. 작을수록 상위 순위 사이의 점수 차이가 가팔라져 상위권 영향이 커지고, 클수록 순위 차이가 평탄해진다. RRF의 목적은 한 검색기를 승자로 정하는 것이 아니라 원점수 단위가 다른 검색기들의 순위를 사용해 서로 다른 누락 원인을 가진 후보 합집합을 안정적으로 만드는 것이다.

  • RRF k: 보통 60. 무가중 합은 검색기마다 같은 weight를 주며, k를 낮추면 상위 순위의 기울기가 가팔라지고 높이면 순위 기여가 평탄해진다.
  • 가중치 조정: 정규화한 dense·BM25 score를 α·score_dense + (1-α)·score_bm25로 합치거나, weighted RRF를 쓴다. raw score는 단위가 달라 그대로 더하지 않는다.
  • BGE-M3 통합 모드: 한 모델이 dense + sparse + ColBERT 동시 출력 → 셋을 RRF로 결합 (L11-70 §3.8)
  • Elasticsearch / OpenSearch / Weaviate / Vespa: hybrid 빌트인
  • 일반 자연어 QA: dense 0.7 / BM25 0.3
  • 법률·의학·고유명사 多: dense 0.4 / BM25 0.6
  • 코드 검색: dense 0.3 / BM25 0.7 (식별자 매칭이 중요)

semantic retrieval 품질은 한 번에 한 기법으로 해결되지 않는다. production에서는 recall을 먼저 확보하고, rerank·filter로 precision을 회복하는 순서가 안정적이다.

1. Query rewriting: 사용자 표현을 문서 표현으로 정규화
2. Hybrid search: dense가 놓치는 고유명사·식별자·숫자를 BM25로 보완
3. Metadata filter: tenant, permission, product, language, updated_at 범위 제한
4. Rerank: top-50~100 후보를 cross-encoder로 정렬
5. Context packing: parent chunk, dedupe, 최신 문서 우선순위 적용
  • Query rewriting: “그거 비용은?” 같은 후속 질문을 “RAG 아키텍처 운영 비용은?”처럼 독립 검색어로 만든다. chat history를 무조건 붙이면 noise가 늘므로 최근 intent와 entity만 보존한다.
  • Hybrid: dense recall이 부족한 도메인(한국어 고유명사, 코드, 법령 번호)에서 기본값. RRF로 넓게 합친 뒤 reranker가 정리한다.
  • Metadata filter: 품질 기법이면서 보안 기법이다. tenant_id, ACL 같은 보안 조건은 권한 밖 후보가 DB 또는 검색 서비스의 신뢰 경계를 넘기 전에 강제한다. DB executor가 ANN index scan 뒤 같은 query의 WHERE를 적용하면 leak은 막아도 underfill과 recall 손실이 생길 수 있다. 반면 앱이 권한 밖 후보를 받은 뒤 reranker·prompt·응답 직전에 거르는 response-side filter는 이미 데이터가 전달됐으므로 permission leak이다. doc_type, language, updated_at 같은 품질 조건은 별도로 측정하고 필요하면 fallback으로 완화한다.
  • Recall / precision trade-off: top-K를 키우면 recall은 오르지만 context precision과 generation 품질은 떨어진다. 권장 흐름은 retrieve top-100 → rerank top-10 → context top-5~7이다.
  • 최신성 가중치: 정책·가격·장애 문서는 cosine score만으로 정렬하면 오래된 문서가 이길 수 있다. rerank 후 updated_at decay 또는 version filter를 적용한다.

Reranker는 1차 검색이 만든 후보를 query와 함께 다시 읽고 관련도를 재점수화하는 모델이다. bi-encoder는 query와 document를 따로 벡터화해 미리 색인할 수 있어 빠르지만, query-document 상호작용을 하나의 거리로 압축한다. cross-encoder reranker는 query와 document를 함께 넣어 “이 query에 이 문서가 답인가”를 직접 판단하므로 정확하지만 후보마다 추론 비용이 든다.

Stage 1 (retrieve): bi-encoder로 top-100 후보 (10ms)
Stage 2 (rerank): cross-encoder reranker로 top-10 (50~100ms)

예를 들어 query가 “엔터프라이즈 환불 예외”이고 1차 검색 top-3이 아래와 같다고 하자.

1. "일반 환불은 결제 후 7일 안에 가능하다"
2. "엔터프라이즈 계약은 별도 약관을 따른다"
3. "크레딧 사용분은 환불 대상에서 제외한다"

dense retrieve는 세 문서를 모두 관련 후보로 잡을 수 있지만, 최종 답변에 가장 중요한 문서는 2번일 가능성이 크다. reranker는 query와 후보 문장을 함께 읽고 2번을 위로 올린다. 단, 2번이 top-100 안에 없었다면 reranker는 찾을 수 없다. 그래서 순서는 항상 recall 확보 → rerank로 precision 회복이다.

candidate 폭에도 상한과 하한이 있다. top-10만 rerank하면 1차 검색 11위의 정답은 영원히 사라지고, top-1000을 rerank하면 문서마다 cross-encoder 추론을 해 latency와 비용이 급증한다. 따라서 retrieve N은 reranker 모델의 설정값이 아니라 exact/Flat 기준 recall 곡선과 latency budget이 만나는 지점이다. first-stage recall@100은 충분한데 최종 precision@10이 낮을 때 reranker를 의심하고, recall@100부터 낮다면 ANN·hybrid·chunking·filter를 먼저 고친다.

  • BGE-reranker-v2-m3 (open, 다국어)
  • Voyage rerank-2.5 / 2.5-lite (현재 권장 API 후보)
  • Cohere Rerank 3.5 (API)
  • Jina Reranker v2 (다국어)
  • MS MARCO MiniLM-L6: 가장 작고 빠른 baseline

초기 관찰 예: 일부 공개 benchmark와 사내 평가에서는 reranker 뒤 recall@10이 10~30% 개선되기도 한다. 개선 폭은 embedding·reranker 모델, 언어·도메인, first-stage 후보 폭, gold 정의에 의존하므로 고정 ROI 계약이 아니다. 같은 top-N 후보와 자체 gold query에서 precision@10·NDCG@10·추가 latency를 함께 측정한다.

3.6 Multi-Vector / Late Interaction (ColBERT)

섹션 제목: “3.6 Multi-Vector / Late Interaction (ColBERT)”

문서 하나 = 단일 vector 대신 token별 vector 집합.

ColBERT: 각 query token이 각 document token과 max 유사도, 합 = 점수
  • 장점: bi-encoder보다 정확, cross-encoder보다 빠름
  • 단점: storage 多 (token 수만큼 vector)
  • 운영 도구: ColBERT-v2, BGE-M3 multi-vector mode, RAGatouille
  • 사용처: 정확도가 중요한 검색, code search

3.7 Quantization과 차원 축소 (재방문)

섹션 제목: “3.7 Quantization과 차원 축소 (재방문)”

L11-70 §3.10·3.11을 운영 시점으로.

임베딩 검색의 storage 공식은 단순하다.

저장 공간 = vector_count × dimension × bytes_per_number

그래서 1B × 768d × fp32(4 bytes)는 원본 벡터만 약 3TB다. HNSW graph와 metadata, replica까지 포함하면 “벡터를 저장했다”가 아니라 “RAM과 SSD 예산을 잠갔다”에 가까워진다. Quantization은 숫자 하나를 더 적은 bit로 표현해 이 벽을 낮추고, 차원 축소는 숫자 개수 자체를 줄인다. int8 같은 scalar quantization은 각 숫자의 표현 정밀도를 낮추고, **PQ(Product Quantization)**는 vector를 여러 부분 벡터로 나눈 뒤 각 부분을 가까운 codebook centroid ID로 저장한다. 둘 다 원래 좌표를 근사하므로 storage 절감은 거리 순서가 일부 뒤집힐 수 있다는 대가와 교환한다.

1536d fp32 → 256d 1-bit1536×32/256=192×는 Matryoshka 절단과 binary quantization을 함께 지원하는 compatible model에서, bit가 빈틈없이 pack된다고 가정한 raw vector payload의 최대 이론 절감값이다. metadata, HNSW·IVF index, alignment, 원본 fp32 rescore vector, replica와 backup은 계산에 들어 있지 않다. 따라서 192×를 전체 storage나 월 비용 절감으로 읽으면 안 된다.

핵심 오해는 “압축하면 무조건 싸고 좋다”이다. 압축은 1차 후보를 싸게 많이 뽑는 데 강하지만, 단일 단계 검색의 최종 판단까지 맡기면 작은 의미 차이가 사라질 수 있다. 압축 벡터 단독압축 벡터로 넓게 retrieve → 원본 fp32 또는 reranker로 verify를 둘 다 후보로 두고, 같은 gold query에서 recall·latency·storage 곡선을 비교해 더 단순한 구성이 SLO를 만족하면 단일 단계를 유지한다.

toy corpus에서 d1의 cosine 0.800과 d4의 약 0.707은 여유가 있지만, 실제 top-k 경계에서 두 문서 점수가 0.801과 0.800이라면 int8 반올림만으로 순서가 바뀔 수 있다. top-1000 후보에는 둘 다 남아 fp32 rescoring으로 회복할 수 있지만, 압축 벡터 top-1만 반환하면 정답을 영구히 잃는다. quantization 평가는 평균 cosine 오차보다 gold 정답이 후보 집합에서 빠지는가를 recall@k로 봐야 한다.

기법raw vector payload 절감공개 benchmark의 품질 변화 예판단 기준
fp32 → fp16~0인 사례model·distance 구현과 자체 recall로 확인
fp32 → int8<1%인 사례top-k 경계 query slice와 rescore 필요 여부 확인
fp32 → binary (1bit)32×~5% 손실 사례후보를 넓게 뽑고 fp32 rescoring 전후를 비교
Matryoshka 256d4× (vs 1024d), 6× (vs 1536d)가상 초기 관찰 1~3% 손실MRL을 명시한 모델의 공식 차원과 자체 gold recall 확인
PQ (Product Quant)4~32× (구성)5~15% 손실 사례codebook·subvector 수·rescore 폭별 latency-recall 측정
  • 2-stage retrieval 후보: binary로 1차 (top-1000) → fp32로 rescore (top-10). 이 패턴은 columnar DB의 “bloom filter로 1차 page skip → 원본 page에서 verify”와 동형이고, anti-spam 시스템의 “MinHash 후보군 추출 → 정밀 비교”와도 같다. 2단계는 근사·고압축 후보 생성에서 잃은 recall을 정밀 단계가 비용 대비 충분히 회복할 때 채택한다. Binary 단독이 recall·latency SLO를 만족하거나 fp32의 추가 recall이 비용을 정당화하지 못하면 단일 단계를 고른다.
  • Cohere가 공개한 표현 형식: Embed v3는 int8과 packed binary 출력을 제공하고, 1024차원 float32를 1024bit로 pack하면 raw payload가 32× 줄어든다. 이는 byte 계산으로 확인 가능한 표현 크기이며, blog의 검색 속도·품질 수치는 hardware 조건이 충분히 공개되지 않아 이 문서의 보편 benchmark로 사용하지 않는다 (Cohere int8/binary blog).
  • 가상 비용 사례 재사용: §2.5의 10M 기사, $5,600/월 → $1,400/월(75% ↓), 품질 변화 미감지 값은 출처가 복원된 Cohere 고객 사례가 아니라 이 문서의 planning scenario다. 실제 의사결정에서는 같은 모델·corpus·hardware·가격표로 fp32와 int8을 다시 측정한다.
  • 언제 쓰면 안 되는가 (Inversion): ① uncompressed baseline이 이미 memory·latency budget을 만족하면 quantization 복잡성의 이익이 작다. 이 경계는 vector count가 아니라 차원·hardware·동시성·filter와 budget 측정으로 정한다. ② reranker가 없는 단일 단계 binary 검색에서 가상 초기 관찰의 recall 손실이 5%+였더라도 그 수치 하나로 fp32 단계를 추가하지 않는다. Binary 단독과 fp32 rescore의 후보 폭별 recall·latency 곡선을 측정해 채택 여부를 정하며, 후보 손실을 검증하지 않은 단독 도입도 피한다. ③ Matryoshka 절단은 compatible model이 공식 지원하는 nesting 차원을 우선한다. 예를 들어 Voyage 4 계열은 2048/1024/512/256d를 지원하지만, BGE-M3 공식 카드는 1024d dense 출력만 명시한다. MRL이 명시되지 않은 모델의 앞 256차원을 자르는 일은 별도 사후 실험이며 native 지원으로 부르지 않는다. 중간·임의 차원도 반드시 품질이 폭락하는 것은 아니지만, 자체 gold dataset의 recall로 검증한 뒤 사용한다.

Freshness는 원본 문서 변경이 검색 결과에 반영되기까지의 시간과 정확성이다. RAG에서는 “문서 DB는 최신인데 vector store는 옛날”이 매우 흔하다. 모델은 검색된 근거가 최신인지 스스로 알지 못하므로, stale embedding은 자연어로 그럴듯하게 포장된 오래된 답변이 된다.

갱신은 단순히 UPDATE vector 문제가 아니다. 원본 문서가 바뀌면 chunk 경계가 바뀔 수 있고, chunk hash가 바뀌면 embedding도 바뀌며, 이전 version vector는 검색에서 제외돼야 한다. 또한 검색 중인 query와 reindex job이 동시에 돌 때 어떤 version을 볼지 정해야 한다.

  • Append-only: 새 문서만 추가, 옛날 것은 그대로. 가장 단순
  • Soft delete + reindex: 삭제된 문서는 metadata 마킹, 주기적 reindex
  • Versioning: 문서마다 version 키. 검색 후 latest만
  • CDC (Change Data Capture): DB의 변경을 Kafka·event stream으로 → 임베딩 파이프라인 자동 갱신

각 패턴의 실패 모드는 다르다. append-only는 삭제와 수정에 약하고, soft delete는 orphan vector cleanup이 필요하며, versioning은 검색 후 latest filter만으로는 오래된 chunk가 top-k를 점유할 수 있다. freshness가 제품 요구사항이면 “매일 새벽 reindex”보다 변경 이벤트를 잃지 않는 파이프라인을 먼저 설계해야 한다.

**Stale index(오래된 인덱스)**는 현재 원본 version이 아직 색인되지 않았거나 폐기한 version이 계속 검색되는 상태를 통칭한다. stale embedding은 그중 벡터 표현이 오래된 경우이고, chunk 목록·metadata·ANN graph가 원본과 어긋난 경우도 stale index다. HNSW처럼 쓰기와 삭제가 누적되는 구조에서는 soft-delete 표식만 늘고 graph가 즉시 조밀하게 재구성되지 않을 수 있으므로, freshness lag뿐 아니라 deleted ratio와 rebuild 후 recall 차이도 본다.

갱신의 핵심 철학은 vector를 원본과 별개인 재생성 가능한 파생 인덱스로 보는 것이다. 원본 DB commit은 성공했는데 embedding 호출이 실패할 수 있으므로 둘을 하나의 원자적 쓰기라고 가정하지 않는다. 대신 변경 사실을 잃지 않는 outbox/CDC, 중복 처리에도 같은 결과를 만드는 idempotency key, old/new index를 동시에 검증하는 blue-green 전환으로 불일치를 드러내고 복구한다.

예를 들어 d1의 보관 기간이 7년에서 5년으로 바뀌었다고 하자. 새 chunk를 version=2로 넣었지만 version=1을 후보 단계에서 제외하지 않으면 두 문서가 동시에 top-k를 차지한다. 답변 직전에 latest 하나만 남겨도 old chunk가 후보 자리를 선점해 다른 관련 문서 d3을 밀어낼 수 있다. 따라서 version filter도 tenant filter처럼 후보 생성 단계에 적용하고, 전환 중에는 source_version, embedding_model_version, chunker_version을 함께 기록해야 stale 원인을 구분할 수 있다.

CDC / Outbox 기반 freshness architecture

섹션 제목: “CDC / Outbox 기반 freshness architecture”

문서 원본이 DB·CMS·S3·Git 등 여러 곳에 있으면 “문서가 바뀌었는데 벡터는 옛날 것”이 가장 흔한 장애가 된다. RAG freshness는 batch cron보다 변경 이벤트를 잃지 않는 파이프라인으로 설계한다.

source DB/CMS commit
→ outbox row 또는 CDC event
→ embedding job queue
→ chunk 재생성 + embedding
→ vector upsert(new version)
→ old version soft delete
→ freshness lag metric 기록
  • Outbox: 업무 트랜잭션과 document_changed 이벤트를 같은 DB commit에 넣는다. 이벤트 발행 실패로 색인이 누락되는 문제를 줄인다.
  • CDC: Debezium·Kafka 등으로 변경 로그를 읽어 embedding queue에 넣는다. 원본 DB를 polling하지 않아도 되고, 장애 후 offset부터 재처리 가능하다.
  • Idempotency key: doc_id + version + chunk_hash로 upsert를 멱등화한다. 같은 이벤트가 두 번 와도 vector가 중복 생성되면 안 된다.
  • Freshness SLA(Service Level Agreement): “문서 수정 후 5분 안에 검색 반영”처럼 lag 목표를 둔다. 모니터링은 event_time → indexed_at p95/p99로 본다.
  • Blue-green index: chunking 변경이나 cross-model 호환 보장이 없는 임베딩 모델 변경은 partial upsert가 아니라 새 index를 만들고 shadow query로 검증한 뒤 alias를 전환한다. 공식 호환 모델군 안의 교체도 dimension·dtype/quantization·distance 계약과 retrieval 회귀를 확인한 뒤 전환한다.
  • Stale embedding: 문서 수정됐지만 임베딩이 옛것 → 잘못된 답변
  • Orphan vectors: 원본 문서 삭제됐는데 vector 남음
  • 임베딩 모델 변경: 명시적 cross-model 호환 보장이 없으면 전체 reindex가 필요하다. 보장이 있으면 재사용 후보가 되지만, dimension·dtype/quantization·distance 계약과 retrieval 평가를 통과해야 한다.
  • Tokenizer drift: chunk를 다시 만들 때 토크나이저 차이로 chunk 경계 변화

검색 품질은 production traffic의 클릭만으로 판단하기 어렵다. 클릭은 순위와 UI에 영향받고, 권한 밖 문서는 클릭되기 전에 노출 자체가 사고다. 먼저 버전 관리되는 gold dataset으로 offline 회귀를 재고, 실제 traffic에서는 latency·freshness·권한 audit·사용자 행동을 함께 본다.

  • Recall@k: gold 정답 문서 중 top-k에 들어온 비율. 검색한 정답 수 / 전체 정답 수
  • Precision_returned: 실제 반환된 결과만 분모로 보는 비율. 검색한 정답 수 / returned. underfill을 벌점으로 보지 않으므로 단독 품질 지표로 쓰지 않는다.
  • Precision@k: top-k 결과 중 gold 정답인 비율. 검색한 정답 수 / k
  • MRR (Mean Reciprocal Rank): 각 query에서 첫 relevant result의 rank를 역수로 바꾼 뒤 query 전체에서 평균낸 값. MRR = (1/|Q|) · Σ_q 1/rank_q이며 relevant result가 없으면 그 query의 기여는 0이다.
  • NDCG@k(Normalized Discounted Cumulative Gain): 관련도와 순위 위치를 함께 고려한 품질 지표
  • Hit@k: top-k 안에 적어도 1개 정답

공통 toy corpus의 executor-side post-filter 결과 [d3]은 반환된 한 건만 분모로 보는 precision_returned가 100%여서 좋아 보이지만 recall은 50%다. precision@2처럼 요청한 두 자리를 분모로 고정하면 빈 한 자리도 실패로 보아 50%다. 반대로 후보를 [d3,d1,d4,d5]까지 넓히면 recall은 100%지만 precision@4는 50%다. 이 때문에 first stage는 returned K와 recall@50·@100으로 누락을 보고, rerank와 context 단계는 precision@10·@5와 NDCG로 순서를 본다. 한 지표만 최적화하면 결과는 깨끗하지만 정답을 놓치는 검색 또는 정답은 있으나 noise가 너무 많은 context가 된다.

  • query latency P50/P95/P99
  • recall@k (gold dataset로)
  • index size, vector count
  • cache hit ratio (자주 쓰는 query는 cache)
  • 임베딩 API 호출 비용 / drift
  • Ragas (L11-80): faithfulness·context recall·context precision·answer relevance
  • TruLens: RAG triad
  • DeepEval: 자동 평가
  • 자체 gold dataset + Promptfoo

운영자가 자주 만나는 함정이다. 공통점은 HTTP 에러가 나지 않는다는 점이다. 검색은 200으로 응답하고 latency도 정상인데, recall·freshness·권한이 조용히 깨진다. 그래서 명령어보다 먼저 “어떤 현상이 어떤 개념 실패를 뜻하는가”를 읽어야 한다.

  • Chunk size 부적절: 너무 작아 context 부족, 너무 커 noise 多
  • Hybrid 가중치 미튜닝: dense만 쓰면 고유명사·법령 검색 약함
  • Reranker 누락: top-10 정렬이 엉망
  • 임베딩 모델 mismatch: query·document를 호환 보장 없는 다른 모델로 임베딩한다. 에러는 안 나며, 서로 다른 모델의 좌표계는 차원이 같아도 기본적으로 호환되지 않는다. 모델 간 vector cosine threshold로 호환성을 시험하지 않는다. 감지: 검색 대상의 model_version별 vector 수를 집계하고, 공식 모델 문서에 source/target cross-model 호환 계약이 있는지 확인한다. 보장이 없으면 새 모델로 대상 corpus 전체를 별도 index에 재임베딩하고, 같은 원문 query도 모델별로 따로 임베딩해 각자의 index에서 retrieval 평가를 수행한다. Voyage 4 계열처럼 보장이 있으면 기존 vector 재사용을 실험할 수 있지만, 동일한 지원 dimension, dtype/quantization, 정규화·distance 계약을 확인하고 recall@k·nDCG@k·latency를 검증한 뒤 cutover한다.
  • Cold start: 새 문서 임베딩 큐 처리 지연 → 검색 안 됨. 감지: 임베딩 큐 lag(p99 > 분 단위)과 “최근 24h 신규 문서 검색 hit rate”를 동시 모니터링 — 어느 한쪽만 보면 놓친다.
  • Storage 폭증: 의도한 양보다 vector 수 多 (중복 문서 등). 감지: vector_count / source_doc_count 비율이 평소(예: 5±1 chunks/doc) 대비 2× 이상 튀면 중복 chunking 또는 reindex 누적이 원인.
  • Executor-side ANN post-filter underfill: DB나 검색 엔진 내부에서 ANN 후보를 먼저 제한하고 WHERE tenant_id=...를 나중에 적용해 LIMIT K보다 적게 반환한다. 권한 밖 row가 앱으로 전달되지 않았다면 permission leak은 아니지만, recall과 returned K가 조용히 낮아진다. 감지: requested K 대비 returned K, exact/pre-filter 기준 recall, iterative scan 전후를 비교한다. 복구: 더 넓은 탐색, iterative scan, partial index, partition·namespace를 검토한다.
  • Response-side permission leak (가장 silent): 앱이 권한 없는 후보를 받아 reranker·prompt·cache·log에 전달한 뒤 최종 응답 직전에만 거른다. 화면에서 숨겨도 신뢰 경계를 넘은 시점에 leak이다. HTTP 200, recall·latency 메트릭이 정상이라 자동 알람이 안 걸릴 수 있다. 감지: 검색 서비스 응답 직후 assert all(doc.tenant_id == ctx.tenant_id for doc in results)를 강제하고 tenant cross-leak audit를 돌린다. 복구: 메타데이터 tenant_id NOT NULL, tenant/ACL server-side filter, partial index·partition·namespace를 적용한다.
  • Over-filtering: metadata filter가 너무 강해 정답 문서가 후보군에 들어오지 않음. 감지: 필터 적용 전 recall@50과 필터 적용 후 recall@50을 분리 측정. 복구: 필수 보안 필터와 품질 필터(language, date, doc_type)를 분리하고, 품질 필터는 fallback 완화 규칙을 둔다.

executor-side underfill, response-side permission leak, over-filtering은 모두 filter 주변에서 보이지만 원인과 경계가 다르다. executor-side underfill은 DB 안에서 권한 조건을 지켰어도 ANN 탐색 폭 때문에 결과 수와 recall이 줄어드는 문제다. response-side leak은 권한 검사가 앱 전달 이후라 기밀성이 깨진 문제다. over-filtering은 language, doc_type, updated_at 같은 품질 조건이 과도한 문제다. 보안 필터(tenant_id, ACL)는 절대 완화하지 않고, 품질 필터만 recall을 보며 fallback으로 완화한다.

3.11 자체 sweep을 시작하기 위한 가상 관찰 표

섹션 제목: “3.11 자체 sweep을 시작하기 위한 가상 관찰 표”

아래 값은 자체 sweep을 시작하기 위한 파라미터 예다. 인덱스 선택은 vector count 구간이 아니라 차원, hardware, 동시성, filter selectivity, update 비율, memory budget에서 측정한 latency-recall curve로 정한다. 같은 corpus·distance의 Flat으로 exact-candidate recall을 만들고, 별도의 gold relevance와 latency를 함께 측정한다.

기법초기 관찰·실험 시작점깨지는 조건
Flat (exact)동일 workload의 exact-candidate recall 100% 기준선P95/P99·CPU가 budget을 넘을 때 ANN과 비교
HNSW M=16graph memory와 build/update 비용 측정 시작값차원·graph memory·filter·update rate가 예산을 넘으면 IVF·DiskANN 비교
HNSW efSearch=100candidate recall ~95% 관찰 예자체 latency-recall 곡선에서 ef=50~500을 sweep하고 평탄점 선택
IVF큰 데이터셋·bulk build 비교 후보작은 데이터에서 HNSW가 더 나은 사례도 있으므로 build·latency-recall 곡선으로 비교
DiskANN메모리 부족 환경SSD tail latency·update/build 특성이 병목이거나 초저지연 P99 요구가 엄격할 때 검증 필요
RRF (k=60)무가중 합의 시작값 예k가 작으면 상위 순위 영향이 가팔라지고, 크면 순위 기여가 평탄해짐
BGE-M3 통합공식 1024d에서 다국어·dense+sparse+multi-vector 운영native 256d 절단으로 간주하지 않고, 단일 dense만 필요하면 multi-mode 오버헤드도 비교
Rerankertop-100 → top-10은 시작 구성 예모델·hardware별 추가 latency가 budget을 넘거나 first-stage recall이 낮으면 구성 재검토
보안 metadata filtertenant·ACL 강제executor post-filter면 underfill, 앱에서만 걸면 leak
품질 metadata filter언어·문서 유형·시간 제한과도하면 recall 손실, fallback 기준 없으면 정답 누락
Binary embeddingraw payload 32×, 가상 초기 관찰에서 품질 ~95%모델·corpus·hardware별로 후보 폭과 fp32 rescore 전후를 다시 측정
Matryoshka 256dVoyage 4처럼 명시적 지원 모델의 시작점. 1024d 대비 raw payload 4×모델 공식 차원 우선, 가상 1~3% 손실 관찰도 자체 gold recall로 재검증

3.12 Silent Failure 시나리오와 복구

섹션 제목: “3.12 Silent Failure 시나리오와 복구”

아래 임계값은 alert·sweep을 시작하기 위한 예시다. 평상시 baseline과 SLO가 다르면 50%+, 200ms, 90%, 100→200 같은 숫자도 함께 바꾼다.

증상정량 시그널원인복구
Stale embedding검색 결과 옛 정보문서 수정 후 reindex 누락CDC + 임베딩 큐 자동화
Orphan vectorsvector count > document count삭제 미반영soft delete + 주기적 cleanup
모델 버전 변경 후 검색 깨짐새 경로 retrieval 회귀비호환 공간 또는 차원·표현 불일치호환 계약 확인, 없으면 전체 재임베딩·cutover
ANN post-filter underfillreturned K·recall 동시 하락index scan 뒤 DB WHEREiterative scan, partial index·partition
Permission leak앱에 다른 tenant 후보 전달됨response-side filterserver-side tenant/ACL filter, audit
Over-filteringfilter 후 recall@50 급락language/date/doc_type 과도함보안 필터와 품질 필터 분리, fallback
Hybrid 가중치 실수dense·BM25 한쪽만 적용RRF 또는 α 미설정도메인별 가중치 default 표준화
Reranker latency 초과P99 > 200mstop-100 너무 많음top-50으로 줄임, 작은 reranker (MiniLM)
HNSW recall 낮음recall@10 < 90%efSearch 부족efSearch ↑(100→200), M ↑(16→32)

예: pgvector HNSW + tenant filter 결과 누락 진단

섹션 제목: “예: pgvector HNSW + tenant filter 결과 누락 진단”

학습해야 할 원리는 한 문장이다. DB executor가 approximate index로 제한된 후보를 탐색한 뒤 같은 query 안에서 WHERE tenant_id = ...를 적용하면 LIMIT 10보다 적게 반환할 수 있다. 권한 밖 row가 DB에서 앱으로 나오지 않았다면 response-side permission leak은 아니지만, 검색 시스템은 성공한 것처럼 보인 채 RAG의 후보 수와 recall을 잃는다.

조건부 관찰 예:
live row 중 tenant=acme 비율이 약 10%
tenant와 vector 이웃 분포가 대략 독립이고 acme live row가 10개 이상
hnsw.ef_search = 40, iterative scan 비활성
graph·분포·dead tuple·scan limit 영향이 작다면 통과 건수 직관은 40×10%≈4
한 실행에서 LIMIT 10인데 returned=4를 관찰할 수 있음

진단 순서는 명령어보다 개념으로 기억한다.

  1. LIMIT K보다 적게 반환되는지 본다.
  2. ef_search 또는 iterative scan을 키웠을 때 K개로 회복되는지 본다. returned=10은 충분한 tenant row와 탐색 한도 안에서 qualifying row를 찾았을 때의 조건부 관찰이지 deterministic expected가 아니다.
  3. 회복되면 후보 폭 문제이므로 recall과 latency 곡선을 같이 측정한다.
  4. 회복되지 않으면 tenant별 partial index, partition, namespace 분리, payload index 누락을 의심한다.

실제 psql 관찰 명령은 선택 부록 A에 접어 둔다. 본문 결론은 두 가지다. 권한 필터는 response 직전이 아니라 DB·검색 서비스 경계에서 강제하고, DB executor 내부에서 index scan 뒤 filter되는 구현은 별도의 underfill·recall 문제로 측정한다.

3.13 임베딩 운영의 일반 매핑 (Transferable Pattern)

섹션 제목: “3.13 임베딩 운영의 일반 매핑 (Transferable Pattern)”

벡터 검색 시스템 = “근사 검색 + 정확 재정렬 + 압축”. 다른 검색 시스템과 같은 패턴.

임베딩 운영 구성요소일반 시스템 매핑
Vector store (FAISS, pgvector)DB·search engine (Elasticsearch)
ANN index (HNSW)DB index (B-tree, LSM-tree)
Chunkingdocument partitioning, sharding
Hybrid search (BM25 + dense)full-text + structured query
RRF (Reciprocal Rank Fusion)weighted vote, ensemble ranking
Cross-encoder rerank2-stage cache lookup, allowlist + verify
Quantization (int8, binary)columnar compression, bloom filter
Stale embeddingcache invalidation, MV refresh
Permission filterrow-level security, RBAC

일반 공식: “근사 검색(빠름·낮은 정확) → 정확 재정렬(느림·정확)“의 2단계 패턴이 RAG·검색·추천·anti-spam 시스템 전반에 공통이다. 이 위에 갱신·권한·압축이 추가된다.

3.14 통합 운영 시나리오 — 사내 RAG vector store 결정 (예시)

섹션 제목: “3.14 통합 운영 시나리오 — 사내 RAG vector store 결정 (예시)”
가상 workload (vendor-independent): 사내 정책 1M 문서, 1024d, tenant filter selectivity 중앙값 5%, 하루 update/delete 1%, index memory budget 8GB, P95 < 80ms, gold relevance recall@10 > 0.8, 한국어
- raw vector payload: 1M × 1024d × fp32 = 4GB
선택지:
A. pgvector + HNSW: PostgreSQL 트랜잭션·권한 SQL 통합 후보
- 가상 관찰: warm cache, efSearch=100에서 latency P95 ~50ms
B. Qdrant self-host: 검색 계층 독립 확장·payload filter 후보
C. pgvector + BGE-M3 hybrid (공식 1024d dense + sparse + RRF):
- 가상 관찰: 한국어 고유명사·법령 정확도 개선, latency +20ms
- 경계: vanilla BGE-M3를 256d Matryoshka 모델로 취급하지 않음
D. pgvector + Voyage 4 계열 256d binary + BM25 hybrid + 2-stage rerank:
- Voyage가 256d와 binary 출력을 공식 지원하는 별도 후보
- raw vector payload: 1024d fp32 4GB → 256d 1-bit 약 32MB (128×↓)
- 가상 관찰: 이 fixture의 gold relevance recall@10 0.84
가상 선택: D. 같은 filter·update trace로 A와 B를 비교했을 때 둘 다 latency-recall·memory budget을 만족했다는 가정 아래, 원본 정책과 권한의 PostgreSQL 트랜잭션 경로를 재사용하는 이점으로 pgvector를 고른다. 임베딩 후보는 BGE-M3를 잘라 쓴 것이 아니라 공식 256d·binary 계약이 있는 Voyage 4 계열이다.
대안 비선택: A 단독은 lexical 보완 없음, B는 이 가상 조건에서 성능 탈락이 아니라 PostgreSQL과 운영 계층을 분리할 필요가 아직 작음, C는 self-host·multi-mode 장점이 있지만 공식 1024d raw payload가 이 가상 memory 목표보다 큼.
silent failure 모니터링 (§3.12):
- stale embedding: CDC 큐
- permission leak: tenant_id metadata filter
- reranker latency: top-50 cap
결과 (가상): raw vector payload 약 32MB, P95 75ms, gold relevance recall@10 0.84.

이 가상 workload의 측정 조건은 1M chunk, BGE-M3 baseline의 1024d와 Voyage 4 후보의 256d, warm cache, 100개 gold query, top-100 retrieve → top-10 rerank다. 이 숫자는 용량 계산과 재현 조건일 뿐 제품 선호를 만들지 않는다. 실제 후보 비교에서는 vector 차원, cold cache, tenant별 namespace 크기와 filter selectivity, update/delete rate, index 포함 memory budget, reranker latency를 고정한 뒤 pgvector와 Qdrant의 latency-recall curve를 재측정한다.

§3.1 vector store + §3.4 hybrid + §3.7 quantization + §3.11 깨지는 조건 + §3.12 silent failure 모두 적용.

  • RAG (L12-40 깊이 다룸)
  • 의미 검색 (사내 문서, FAQ)
  • 추천 시스템 (사용자·아이템 임베딩)
  • 중복 탐지 (문서·이미지)
  • 클러스터링·토픽 분석
  • 코드 검색

엔지니어가 임베딩 인프라를 운영할 때 다음에 도움된다.

  • Vector store 선택: pgvector·Qdrant를 vector 차원, filter selectivity, update rate, memory budget, latency-recall curve, 운영 ownership, SQL·트랜잭션 통합 필요로 비교한다.
  • HNSW 튜닝: M·efConstruction·efSearch sweep으로 recall vs latency 최적
  • Hybrid + Reranker 패턴: 한국어·고유명사 운영에 가장 큰 ROI
  • Quantization 비용 절감: int8 4×·binary 32×는 raw payload 비율이고, 품질·속도는 모델·benchmark·hardware별로 재측정한다.
  • 갱신 파이프라인: CDC + 임베딩 큐 + reindex 자동화
  • 모니터링 dashboard: recall@k, latency P99, vector count, drift
개념 A개념 B차이점
Flat (exact)HNSW (ANN)같은 corpus·distance의 exact-candidate recall 100% 기준선 vs ANN의 조건부 recall
HNSWIVF그래프 vs cluster 기반. HNSW가 일반적으로 빠름·정확
Bi-encoderCross-encoder1단계 (빠름·낮은 정확) vs 2단계 (느림·정확)
Bi-encoderColBERT (multi-vec)단일 vector vs token별 vector + late interaction
DenseBM25 (sparse)semantic vs lexical. 두 누락이 공존할 때 hybrid 유리
Fixed chunkSemantic chunk단순·빠름 vs 의미 단위 보존·비용 ↑
Recall@kNDCG@k들어왔는가 vs 순위까지 고려
FAISSpgvector라이브러리 (in-process) vs Postgres extension (SQL)

7. 체크리스트 체크

  • vector·embedding·cosine/dot/L2의 차이와 정규화 계약을 손계산으로 설명할 수 있다
  • pgvector·Qdrant를 차원·filter selectivity·update rate·memory budget·latency-recall curve·운영 ownership·SQL 통합으로 비교할 수 있다
  • HNSW vs IVF vs DiskANN의 차이와 선택 기준을 설명할 수 있다
  • HNSW의 M·efConstruction·efSearch와 IVF의 nprobe가 recall·latency를 바꾸는 방향을 말할 수 있다
  • Chunking 5종(fixed/recursive/semantic/structural/sliding)과 한국어 chunk size 권장을 설명할 수 있다
  • Hybrid search (BM25 + dense + RRF)의 도메인별 가중치를 적용할 수 있다
  • Query rewriting·metadata filter·rerank를 recall/precision trade-off 기준으로 조합할 수 있다
  • Reranker의 recall@10 +10~30%를 모델·dataset·후보 폭이 고정된 초기 관찰로 해석하고 자체 gold 평가를 설계할 수 있다
  • Quantization의 raw payload 비율과 benchmark별 품질 손실을 구분하고 2-stage retrieval(binary→fp32 rescore)을 실험할 수 있다
  • CDC/outbox 기반 freshness 파이프라인과 stale embedding 복구 흐름을 설명할 수 있다
  • freshness 실패(stale, orphan, model 변경, tokenizer drift)와 권한/필터 실패(permission leak, over-filtering)를 구분할 수 있다
  • gold dataset으로 recall·precision을 계산하고 candidate 폭과 filter 위치를 비교할 수 있다
  • Vector store: FAISS, pgvector, Pinecone, Weaviate, Qdrant, Milvus, Chroma, Vespa, Elasticsearch, Turbopuffer
  • ANN 인덱스: HNSW, IVF, IVF-PQ, DiskANN, SCANN, ANNoy, NSG
  • Chunking: fixed, recursive, semantic, structural, sliding, late chunking, contextual chunking
  • Hybrid: BM25, RRF (Reciprocal Rank Fusion), SPLADE (learned sparse), ColBERT (late interaction)
  • Semantic retrieval 품질: query rewriting, metadata pre-filter, recall/precision trade-off, freshness boosting, context packing
  • Reranker: BGE-reranker-v2-m3, Voyage rerank-2.5, Cohere Rerank 3.5, Jina Reranker, MiniLM
  • Quantization: int8, binary, PQ (Product Quantization), Matryoshka, OPQ
  • 운영: CDC, outbox pattern, Kafka, Debezium, Airflow, Prefect, dbt + embedding pipeline, blue-green index

9. 실험 설계 — 한 gold dataset으로 원인을 분리하기

섹션 제목: “9. 실험 설계 — 한 gold dataset으로 원인을 분리하기”

좋은 retrieval 실험은 도구 설치 체크가 아니라 한 변수만 바꾸고 같은 query·gold·부하에서 결과를 비교하는 일이다. 먼저 2050개 query의 작은 gold dataset으로 평가 코드가 맞는지 확인하고, 도메인과 실패 유형을 대표하는 100500개로 늘린다. query마다 relevant document와 evidence span, tenant, 허용 version을 기록하면 품질·권한·freshness를 한 기준에서 볼 수 있다.

실험 순서는 아래처럼 고정한다.

  1. Flat exact search로 거리와 top-k의 기준선을 만든다.
  2. ANN·filter·hybrid·reranker·quantization을 한 번에 하나씩 추가한다.
  3. 각 단계에서 recall·precision·latency·storage를 함께 기록한다.
  4. 마지막에 chunking이나 모델을 바꾼다. 둘은 전체 좌표와 gold evidence 단위를 바꾸므로 앞 실험과 섞지 않는다.
실험 축고정할 것바꿀 것함께 볼 지표결론으로 연결하는 법
Exact vs ANN같은 1만 vector·query·distanceFlat, HNSW, IVFexact-candidate recall@10, gold relevance recall@10, P50/P95Flat의 exact-candidate recall 100%와 gold relevance를 분리한다. HNSW 1ms 미만·99%는 예상치일 뿐이다
HNSW 후보 폭M, corpus, queryefSearch=50,100,200,500recall@10과 latency 곡선recall이 평평해진 뒤 latency만 늘면 model·chunking·hybrid를 의심한다
IVF 후보 폭centroid와 corpusnproberecall@10, scanned candidatescell 경계 누락이 회복되는 최소 nprobe를 찾는다
Filter 위치toy corpus와 gold G={d1,d3}server pre-filter, executor post-filter, response-side filterreturned K, recall@K, 앱 전달 전 cross-tenant countexecutor underfill과 response-side leak을 분리하고 품질 filter만 fallback을 허용한다
Chunking같은 PDF와 queryfixed-512, recursive, semantic; 256/512/1024 tokenevidence recall, context precision, 중복률정답 span 절단과 top-k 중복을 분리해 본다
Hybrid같은 dense/BM25 후보RRF k=60, dense:BM25 0.3:0.70.7:0.3recall@10, exact-term hit, NDCG@10한국어 법률·코드처럼 exact term이 많을수록 lexical 기여를 따로 본다
Reranker같은 first-stage top-100cross-encoder 유무, top-50/100 후보recall@100, precision@10, 추가 latencyrecall@100이 낮으면 reranker가 아니라 1차 검색을 고친다
Quantization같은 1,000 vector와 fp32 exact 결과fp32, int8, binary, Voyage 4 같은 명시적 MRL 모델의 256drecall@10, raw payload, total storage, rescore latencyBGE-M3 1024d는 별도 기준선이다. 192×는 1536d fp32→256d 1-bit raw payload 최대 이론값이다
갱신같은 100개 source document50% 수정 후 reindex/upsert, 중복·역순 event 100건freshness lag, stale/orphan/duplicate countdoc_id+version+chunk_hash 멱등성과 old version 제외를 별도로 검증한다

이 표의 핵심은 각 기법을 “좋아졌다”로 평가하지 않는 것이다. ANN은 latency를 줄이는 대신 exact 결과 대비 얼마나 잃었는지, reranker는 같은 후보 안에서 precision을 얼마나 회복했는지, quantization은 bytes를 줄이는 대신 후보 경계를 얼마나 흔들었는지 역할별로 묻는다.

관측 결과먼저 세울 가설다음 분리 실험
HNSW recall이 낮다efSearch가 작거나 graph 연결이 부족하다efSearch를 먼저 올리고, 부족하면 M을 올려 latency와 같이 본다
efSearch를 올려도 recall이 안 오른다ANN보다 embedding·chunking·filter가 병목이다Flat exact도 낮은지, filter 전 recall이 높은지 확인
Hybrid가 dense보다 나쁘다BM25 analyzer가 한국어를 잘못 자르거나 가중치가 크다dense/BM25 단독 순위와 token 분석 결과를 분리한다
LIMIT K보다 적게 반환된다executor-side ANN post-filter가 후보를 소진했다exact/pre-filter 기준과 iterative scan 전후 returned K를 비교한다
앱에 다른 tenant 후보가 전달된다response-side까지 보안 filter가 지연됐다tenant/ACL을 DB·검색 서비스 경계에서 강제하고 audit한다
품질 filter 뒤 정답이 사라진다language/date/doc_type 조건이 과도하다tenant/ACL은 유지하고 품질 조건만 단계적으로 완화한다
Reranker 효과가 미미하다bi-encoder가 이미 강하거나 reranker가 domain mismatch다first-stage recall@100과 top-10 순위 변화, 추가 latency를 비교한다
Binary 뒤 품질이 폭락한다압축 top-k가 너무 좁아 fp32가 복구할 후보가 없다binary 후보 폭을 늘리고 fp32 rescore를 붙인다
최신 문서와 옛 문서가 함께 나온다version filter가 후보 생성 뒤에 적용된다version pre-filter와 blue-green index 결과를 비교한다

외부 패키지 없이 공통 toy corpus의 거리·filter·RRF·모델 mismatch를 재현하는 코드는 선택 부록 B에 둔다. 실제 DB별 진단은 선택 부록 A처럼 개념을 이해한 뒤 필요한 경우에만 펼친다.

선택 부록 A. pgvector tenant filter 진단 명령

섹션 제목: “선택 부록 A. pgvector tenant filter 진단 명령”
HNSW 후보 폭과 tenant filter가 만나 LIMIT보다 적게 반환되는지 확인

pgvector 공식 문서 기준으로 HNSW 같은 approximate index에서는 index scan 뒤에 WHERE filter가 적용될 수 있다. live row 중 tenant가 10%이고 tenant가 vector 이웃 분포와 대략 독립이라면 hnsw.ef_search=40에서 40×10%=4는 underfill의 평균적 직관이 된다. 그러나 ef_search가 정확히 40개 row를 반환한다는 뜻은 아니며 결과 수는 graph 연결, query 주변 vector 분포, tenant별 row 수, dead tuple·MVCC visibility, iterative scan 설정과 hnsw.max_scan_tuples 같은 scan limit에 따라 달라진다.

Terminal window
psql "$DATABASE_URL" -XAtc "
SET hnsw.ef_search = 40;
SELECT 'returned=' || COUNT(*)
FROM (
SELECT id
FROM rag_chunks
WHERE tenant_id = 'acme'
ORDER BY embedding <=> '[0.1,0.2,0.3]'::vector
LIMIT 10
) s;"

조건부 관찰 예:

SET
returned=4

이 값은 fixture처럼 결정적이지 않다. 같은 SQL에서도 tenant vector가 query 주변에 몰려 있으면 10개를 채울 수 있고, 반대로 graph가 나쁘거나 dead tuple이 많거나 scan limit에 먼저 닿으면 4개보다 적을 수 있다. LIMIT 10인데 10개 미만이면 tenant filter가 선택적이거나 approximate 탐색 폭이 부족하거나 tenant별 index/partition이 빠진 가설을 세우고, 먼저 후보 폭을 늘려 비교한다.

hnsw.iterative_scan은 pgvector 0.8.0 이상에서만 사용할 수 있다. 아래 두 번째 비교를 실행하기 전에 설치된 extension version을 확인하고, extversion이 0.8.0 미만이면 iterative scan을 설정하지 말고 pgvector를 먼저 업그레이드한다.

Terminal window
psql "$DATABASE_URL" -XAtc "SELECT extversion FROM pg_extension WHERE extname = 'vector';"
psql "$DATABASE_URL" -XAtc "
SET hnsw.iterative_scan = strict_order;
SET hnsw.ef_search = 200;
SELECT 'returned=' || COUNT(*)
FROM (
SELECT id
FROM rag_chunks
WHERE tenant_id = 'acme'
ORDER BY embedding <=> '[0.1,0.2,0.3]'::vector
LIMIT 10
) s;"

충분한 acme live row가 있고 확대된 탐색이 scan limit 안에서 그 row를 찾았을 때의 조건부 관찰 예:

SET
SET
returned=10

returned=10은 보장값이 아니다. qualifying tenant row 자체가 10개 미만이거나, graph·분포·dead tuple 때문에 탐색이 비효율적이거나, iterative scan이 hnsw.max_scan_tuples·memory limit에 닿으면 여전히 underfill될 수 있다. 10개로 회복되면 ef_search/iterative scan과 latency를 함께 측정한다. 여전히 부족하면 tenant가 소수라면 partial HNSW index(WHERE tenant_id = 'acme'), tenant가 많다면 partition/namespace 분리를 검토한다. Qdrant 계열에서는 tenant_id payload index를 먼저 만들고 ingest 후 만든 경우 HNSW rebuild까지 확인한다.

선택 부록 B. deterministic toy corpus 실행

섹션 제목: “선택 부록 B. deterministic toy corpus 실행”
외부 패키지 없이 distance, filter 위치, RRF, model version 확인

본문의 손계산을 코드로 검산하고 싶을 때만 실행한다. 이 코드는 실제 embedding 품질이나 BM25 구현을 대신하지 않는다. 숫자와 corpus를 고정해 DB executor post-filter는 높은 precision_returned에도 recall과 precision@K를 잃을 수 있다, response-side filter는 권한 밖 후보를 앱에 전달한다, 비동률 RRF는 dense와 다른 1위를 만들 수 있다, 동률 RRF는 명시한 ID tie-break로 결정해야 한다, 미승인 model version 혼합은 별도 실패다라는 다섯 경계를 재현한다.

Terminal window
python3 - <<'PY'
from math import sqrt
docs = [
{"id": "d1", "tenant": "acme", "model": "embed-v2", "text": "SOC2-2026 evidence retention 7 years", "v": [0.8, 0.6]},
{"id": "d2", "tenant": "beta", "model": "embed-v2", "text": "SOC2-2026 private exception", "v": [1.0, 0.0]},
{"id": "d3", "tenant": "acme", "model": "embed-v2", "text": "SOC2-2026 audit schedule submission", "v": [0.9, 0.1]},
{"id": "d4", "tenant": "acme", "model": "embed-v1", "text": "SOC2-2026 evidence control overview", "v": [0.7, 0.7]},
{"id": "d5", "tenant": "acme", "model": "embed-v2", "text": "refund policy", "v": [0.0, 1.0]},
]
query = {"tenant": "acme", "text": "SOC2-2026 evidence retention", "v": [1.0, 0.0]}
gold = {"d1", "d3"}
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
return dot / (sqrt(sum(x * x for x in a)) * sqrt(sum(y * y for y in b)))
def dense(rows):
return sorted(rows, key=lambda d: (-cosine(query["v"], d["v"]), d["id"]))
def lexical_score(row):
terms = set(query["text"].lower().replace("-", " ").split())
words = set(row["text"].lower().replace("-", " ").split())
return len(terms & words)
def rrf(rows, k=60):
dense_rank = {d["id"]: i + 1 for i, d in enumerate(dense(rows))}
lexical = sorted(rows, key=lambda d: (-lexical_score(d), d["id"]))
lex_rank = {d["id"]: i + 1 for i, d in enumerate(lexical)}
scores = {
d["id"]: 1 / (k + dense_rank[d["id"]]) + 1 / (k + lex_rank[d["id"]])
for d in rows
}
ranked = sorted(rows, key=lambda d: (-scores[d["id"]], d["id"]))
return ranked, scores
def rrf_from_rankings(rankings, k=60):
rank_maps = [
{doc_id: index + 1 for index, doc_id in enumerate(ranking)}
for ranking in rankings
]
ids = sorted({doc_id for ranking in rankings for doc_id in ranking})
scores = {
doc_id: sum(1 / (k + ranks[doc_id]) for ranks in rank_maps if doc_id in ranks)
for doc_id in ids
}
return sorted(ids, key=lambda doc_id: (-scores[doc_id], doc_id)), scores
def metrics(rows, k):
ids = {d["id"] for d in rows}
hits = len(ids & gold)
return {
"recall": hits / len(gold),
"precision_returned": hits / len(rows) if rows else 0.0,
f"precision@{k}": hits / k,
}
ann_top2 = dense(docs)[:2]
db_post = [d for d in ann_top2 if d["tenant"] == query["tenant"]]
allowed = [d for d in docs if d["tenant"] == query["tenant"]]
pre = dense(allowed)[:2]
hybrid, rrf_scores = rrf(allowed)
tie_rankings = [["z", "a"], ["a", "z"]]
tie_result, tie_scores = rrf_from_rankings(tie_rankings)
assert tie_scores["a"] == tie_scores["z"]
assert tie_result == ["a", "z"], tie_result
print("db_post_filter_top2", [d["id"] for d in db_post], metrics(db_post, 2))
print("pre_filter_top2", [d["id"] for d in pre], metrics(pre, 2))
print("response_side_candidates_top2", [d["id"] for d in ann_top2])
print("response_side_cross_tenant", [d["id"] for d in ann_top2 if d["tenant"] != query["tenant"]])
print("dense_top1", dense(allowed)[0]["id"])
print("rrf_d1_d3", {doc_id: round(rrf_scores[doc_id], 6) for doc_id in ("d1", "d3")})
print("hybrid_top1", hybrid[0]["id"])
print("rrf_tie_break", tie_result)
print("model_versions", sorted({d["model"] for d in allowed}))
PY

예상 출력:

db_post_filter_top2 ['d3'] {'recall': 0.5, 'precision_returned': 1.0, 'precision@2': 0.5}
pre_filter_top2 ['d3', 'd1'] {'recall': 1.0, 'precision_returned': 1.0, 'precision@2': 1.0}
response_side_candidates_top2 ['d2', 'd3']
response_side_cross_tenant ['d2']
dense_top1 d3
rrf_d1_d3 {'d1': 0.032522, 'd3': 0.032266}
hybrid_top1 d1
rrf_tie_break ['a', 'z']
model_versions ['embed-v1', 'embed-v2']

db_post_filter_top2가 하나만 남는 것은 DB 내부 후보 생성과 filter 순서가 만든 underfill이다. response_side_cross_tenant=['d2']는 앱이 권한 밖 후보를 받은 별도의 permission leak이다. d1과 d3의 RRF 점수는 동률이 아니므로 hybrid_top1=d1은 stable sort의 우연이 아니라 lexical 신호가 실제로 순위를 바꾼 결과다. 반대로 입력 ranking [z,a][a,z]에서는 a와 z의 RRF 점수가 실제로 같고, assertion이 document ID 오름차순인 ['a','z']를 결정적 결과로 강제한다. model_versions가 둘인 것은 또 다른 문제다. 먼저 공식 cross-model 호환 계약을 확인한다. 보장이 없으면 새 모델로 corpus 전체를 별도 index에 재임베딩하고, 원문 query를 old/new 모델로 각각 임베딩해 대응 index에서 shadow retrieval을 수행한 뒤 단일 모델로 cutover한다. Voyage 4처럼 명시적 보장이 있으면 새 모델 query와 기존 호환 vector를 함께 쓰는 경로도 평가할 수 있지만, dimension·quantization·distance 계약과 retrieval 품질을 먼저 검증한다. 어느 경우에도 cross-model cosine threshold만으로 호환을 판정하지 않는다. 이 원인들을 하나의 “검색 품질 저하”로 묶으면 filter 폭만 키우거나 권한 경계를 놓치거나 불완전한 reindex를 하는 잘못된 복구를 선택할 수 있다.

  1. Vector store는 차원·filter selectivity·update rate·memory budget·latency-recall curve와 운영 ownership·SQL 통합 요구로 고른다. vector count 구간을 제품 cutoff로 쓰지 않는다.
  2. Flat 100%는 같은 corpus·distance의 exact-candidate recall 기준선이지 gold relevance accuracy가 아니다. HNSW의 efSearch와 IVF의 nprobe는 latency와 candidate recall을 맞바꾼다.
  3. Chunking·BM25+dense hybrid·pre-filter·Reranker는 recall을 먼저 만들고 precision과 권한을 회복하는 경로다.
  4. int8 4×·binary 32×와 compatible Matryoshka 조합의 192×는 raw vector payload의 최대 이론 비율이다. 품질 손실과 metadata·index·replica 포함 total storage/cost는 별도 측정한다.
  5. 운영 silent failure는 stale index, orphan vector, model/tokenizer drift, permission leak, over-filtering이며 HTTP 200만으로 드러나지 않는다. 임베딩 공간은 기본적으로 모델 간 비호환이고, 공식 호환 보장이 없는 교체는 전체 재임베딩이 필요하다.

최종 수정: 2026-07-14