Published on

Pinecone·Milvus 인덱싱 느림? 차원·메트릭 튜닝

Authors

서빙은 빠른데 인덱싱만 유독 느리다면, 대부분은 “벡터의 모양(차원·정규화)”과 “인덱스가 기대하는 거리 메트릭”, 그리고 “빌드/컴팩션 작업량”이 서로 안 맞아서 생깁니다. Pinecone·Milvus 모두 내부적으로는 근사 최근접 탐색(ANN)을 위해 그래프(HNSW)나 inverted file(IVF) 계열 구조를 만들고, 이 과정은 차원 수에 비례해 비용이 증가하며, 메트릭에 따라 전처리(정규화 등)와 연산량이 달라집니다.

이 글에서는 인덱싱이 느려지는 대표적인 패턴을 “원인 → 확인 방법 → 튜닝 포인트”로 정리하고, Pinecone·Milvus에서 바로 적용 가능한 설정/코드 예제를 제공합니다.

인덱싱이 느려지는 4가지 대표 원인

1) 임베딩 차원이 과도하게 큼

차원이 커질수록 다음이 동시에 악화됩니다.

  • 업서트(Upsert) 데이터 크기 증가: 네트워크 전송량, WAL/로그, 디스크 사용량 증가
  • 인덱스 빌드 연산량 증가: 거리 계산이 O(d)로 늘어남
  • 메모리 압박: HNSW류는 특히 그래프 엣지까지 포함해 메모리 사용량이 급증

실무에서 자주 보는 케이스는 “정확도를 위해 1536/3072 차원을 그대로 쓰는데, 실제 검색은 200~400차원에서도 품질이 유지되는” 상황입니다.

권장 접근

  • 먼저 현재 임베딩 모델의 차원 d와 코사인/내적 기반인지 확인
  • 품질 지표(Recall@k, nDCG@k)와 인덱싱 시간/비용을 함께 측정
  • 필요하면 차원 축소(예: PCA) 또는 더 작은 임베딩 모델로 교체

2) 메트릭(거리 함수)과 벡터 정규화가 불일치

Pinecone·Milvus 모두 보통 cosine, dot(inner product), l2 중 하나를 씁니다. 문제는 다음 패턴에서 터집니다.

  • 코사인 유사도를 기대하면서 정규화되지 않은 벡터를 넣음
  • 내적 기반을 쓰면서 스케일이 들쑥날쑥한 벡터를 넣음
  • l2를 쓰는데 실제로는 방향(각도) 유사도가 중요한 문제

이 불일치는 검색 품질뿐 아니라, 일부 인덱스 타입에서 빌드/컴팩션 중 불필요한 분산을 만들어 인덱싱 효율을 떨어뜨릴 수 있습니다.

원칙

  • cosine을 쓸 거면 입력을 L2 normalize해서 넣고(대부분의 임베딩 모델은 이미 정규화된 경우도 있으니 확인), 저장된 벡터의 노름 분포를 점검
  • dot을 쓸 거면 정규화 여부를 설계로 고정(정규화하면 cosine과 동치)

파이썬에서 정규화 예시는 다음처럼 고정해두는 게 안전합니다.

import numpy as np

def l2_normalize(v: np.ndarray, eps: float = 1e-12) -> np.ndarray:
    norm = np.linalg.norm(v)
    return v / (norm + eps)

vec = np.array([0.2, 0.1, -0.4], dtype=np.float32)
vec = l2_normalize(vec)

3) 인덱스 타입/파라미터가 “쓰기(ingest)”에 불리하게 설정됨

Milvus는 인덱스 타입(HNSW, IVF_FLAT, IVF_PQ 등)에 따라 빌드 비용과 쓰기 경로가 크게 달라집니다. Pinecone도 pod/serverless 모드, 인덱스 타입(제품 옵션)과 리소스 크기에 따라 ingest 처리량이 달라집니다.

대표적인 실수는 다음입니다.

  • HNSW에서 M, efConstruction을 너무 크게 잡음 → 빌드가 과도하게 느려짐
  • IVF에서 nlist를 너무 크게 잡음 → 클러스터링/빌드 비용 증가, 작은 데이터에 과도
  • PQ/압축을 너무 이르게 적용 → 빌드 단계가 무거워지고 품질 튜닝도 어려워짐

튜닝 방향

  • “대량 적재 초기”에는 빌드 비용이 낮은 구성을 먼저 선택하고, 적재 후 인덱스를 재빌드/전환
  • HNSW는 efConstruction을 낮춰 ingest를 살리고, 검색 시 ef를 올려 품질을 확보하는 식으로 분리

4) 배치 크기·동시성·컴팩션(세그먼트 병합) 병목

인덱싱이 느린데 CPU는 놀고 있고 네트워크만 바쁘다면 보통 업서트 호출이 너무 잦거나 배치가 너무 작음이 원인입니다. 반대로 CPU/IO가 100%인데 처리량이 낮으면 컴팩션/인덱스 빌드가 뒤에서 밀리는 상태일 수 있습니다.

Milvus는 세그먼트/컴팩션/인덱스 빌드가 얽혀서, 작은 배치로 계속 넣으면 세그먼트가 잘게 쪼개져 병합 비용이 커집니다. Pinecone도 작은 upsert가 누적되면 오버헤드가 커집니다.

Pinecone: 인덱싱 느릴 때 실전 체크리스트

1) 벡터 크기와 업서트 페이로드부터 줄이기

  • 불필요한 metadata를 과도하게 넣지 말고, 검색에 필요한 필드만 남기기
  • 임베딩 차원을 줄일 수 있으면 최우선으로 검토(비용/속도에 즉시 반영)

2) 업서트 배치/동시성 표준화

아래는 Pinecone 업서트에서 흔히 쓰는 배치 패턴입니다(개념 예시). 핵심은 너무 작은 요청을 연속으로 보내지 말고, 적절한 배치와 제한된 동시성으로 안정적인 처리량을 만드는 것입니다.

from concurrent.futures import ThreadPoolExecutor

BATCH = 200
MAX_WORKERS = 8

# vectors: [(id, vector, metadata), ...]

def chunks(xs, size):
    for i in range(0, len(xs), size):
        yield xs[i:i+size]

def upsert_batch(index, batch):
    # index.upsert(vectors=batch)
    return len(batch)

# with ThreadPoolExecutor(max_workers=MAX_WORKERS) as ex:
#     futures = [ex.submit(upsert_batch, index, b) for b in chunks(vectors, BATCH)]
#     total = sum(f.result() for f in futures)

운영에서는 429/throttling, 타임아웃, 재시도 정책이 함께 필요합니다. Bedrock 같은 관리형 API에서도 비슷하게 호출량이 병목이 되며, 쿼터/스로틀링 진단이 중요합니다. 호출 제한과 재시도 전략은 AWS Bedrock InvokeModel 403·Throttling 해결 - IAM·VPC·쿼터처럼 “제한이 걸리는 지점부터 확인”하는 방식이 그대로 적용됩니다.

3) 메트릭과 정규화 일관성 유지

  • cosine을 선택했으면 입력 벡터를 정규화한 뒤 업서트
  • dot으로 통일하면 정규화 여부를 고정하고(정규화하면 코사인과 동일), 재현 가능한 파이프라인 구성

4) 인덱싱 상태를 “기다리는 것”과 “막힌 것”을 구분

Pinecone은 내부적으로 인덱싱이 비동기적으로 진행될 수 있어, 업서트가 성공해도 “즉시 검색에서 안 잡히는” 구간이 생길 수 있습니다. 이때는 다음을 분리해서 봐야 합니다.

  • 업서트 요청 처리량이 낮다(클라이언트/네트워크/배치 문제)
  • 업서트는 빠른데 검색 반영이 늦다(인덱싱 백로그)

후자는 리소스 스케일(인덱스 크기/용량)이나 데이터 형태(차원/메타데이터/필터)가 원인일 가능성이 큽니다.

Milvus: 인덱싱 느릴 때 튜닝 포인트

1) 컬렉션 스키마와 메트릭을 먼저 고정

Milvus는 컬렉션 생성 시 차원과 메트릭이 고정되는 경우가 많습니다. 차원/메트릭 변경은 재적재를 의미하므로 초기에 결정해야 합니다.

# 개념 예시: pymilvus 사용
from pymilvus import FieldSchema, CollectionSchema, DataType, Collection

dim = 768
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=False),
    FieldSchema(name="vec", dtype=DataType.FLOAT_VECTOR, dim=dim),
]

schema = CollectionSchema(fields, description="docs")
col = Collection(name="docs", schema=schema)

메트릭은 인덱스 파라미터에서 설정합니다(예: metric_type). 코사인 유사도를 원하면 Milvus 설정에서 COSINE을 쓰거나, 정규화 후 IP를 쓰는 전략을 택합니다.

2) 초기 적재 단계에서는 “가벼운 인덱스” 또는 인덱스 지연 생성

대량 적재 시점에 무거운 인덱스를 즉시 만들면, insert와 index build가 경쟁하면서 전체가 느려집니다.

실무 패턴:

  • 초기에는 FLAT(브루트포스) 또는 인덱스 생성 지연
  • 데이터 적재 완료 후 create_index
  • 이후 검색 부하에 맞춰 HNSW 또는 IVF_*로 전환
index_params = {
    "index_type": "HNSW",
    "metric_type": "COSINE",
    "params": {"M": 16, "efConstruction": 80},
}

col.create_index(field_name="vec", index_params=index_params)
col.load()

튜닝 감각(경험칙):

  • M을 올리면 그래프가 촘촘해져 품질은 오르지만 빌드/메모리가 증가
  • efConstruction을 올리면 빌드 품질은 오르지만 인덱싱 시간이 증가

인덱싱이 느리다면 먼저 efConstruction을 낮추고, 검색 시 ef(search param)를 올려서 품질을 맞추는 접근이 안전합니다.

3) IVF 계열은 nlist를 데이터 규모에 맞추기

IVF는 클러스터 수(nlist)가 핵심입니다.

  • nlist가 너무 작으면 검색 후보가 커져 검색이 느려질 수 있음
  • nlist가 너무 크면 인덱스 빌드가 느려지고, 데이터가 적을 때는 오히려 품질/속도 모두 손해

일반적으로는 데이터 건수 N에 대해 nlistsqrt(N) 근처에서 시작해 실험합니다. 예를 들어 N=1,000,000이면 nlist≈1000부터 A/B로 측정합니다.

4) 컴팩션/세그먼트 관리: 작은 배치 삽입을 피하기

Milvus는 작은 insert가 반복되면 세그먼트가 쪼개지고, 컴팩션이 뒤에서 비용을 크게 먹습니다.

권장:

  • insert는 가능한 한 큰 배치
  • 주기적으로 컴팩션 상태와 백로그를 모니터링
  • “인덱싱이 느리다”가 사실은 “컴팩션이 밀려서 검색 반영이 늦다”인 경우가 많음

데이터가 누적되며 저장 구조가 비대해져 성능이 악화되는 현상은 DB에서도 흔합니다. 테이블/스토리지 관리 관점은 PostgreSQL VACUUM 안 돌면 테이블 폭증 해결법과 비슷하게, “백그라운드 정리 작업이 밀리면 결국 전체가 느려진다”는 구조로 이해하면 진단이 쉬워집니다.

차원 줄이기: 품질을 잃지 않고 인덱싱을 살리는 방법

1) PCA로 768에서 384로 줄이는 예시

임베딩 모델을 바꾸기 어렵다면, 오프라인에서 PCA를 학습해 차원을 줄일 수 있습니다.

import numpy as np
from sklearn.decomposition import PCA

# train_vectors: (num_samples, dim)
train_vectors = np.load("train_vectors.npy").astype(np.float32)

pca = PCA(n_components=384, svd_solver="randomized")
pca.fit(train_vectors)

# apply to new vectors
vec = np.load("one_vector.npy").astype(np.float32)
vec_384 = pca.transform(vec.reshape(1, -1))[0].astype(np.float32)

주의점:

  • 코사인 기반을 쓴다면 PCA 적용 후 정규화를 다시 수행하는 것이 안전
  • PCA는 도메인에 따라 품질이 크게 달라지므로, 반드시 리트리벌 평가(Recall@k 등)를 함께 측정

2) “검색 품질 지표”를 먼저 정하고 튜닝하기

인덱싱/검색 튜닝은 체감으로 하면 끝이 없습니다. 아래 중 최소 1개는 고정하세요.

  • Recall@10: 정답 문서가 상위 10개에 포함되는 비율
  • MRR: 정답이 얼마나 위에 나오는지
  • nDCG@k: 랭킹 품질

그리고 “차원/메트릭/인덱스 파라미터/배치”를 바꿀 때마다 같은 평가셋으로 비교합니다.

메트릭 선택 가이드: cosine vs dot vs l2

  • cosine: 문장 임베딩/텍스트 검색에서 가장 흔함. 방향 유사도에 강함. 정규화가 핵심.
  • dot(IP): 추천/랭킹에서 스케일 자체가 의미 있을 때 유리. 정규화하면 코사인과 동일.
  • l2: 좌표 공간에서의 거리 자체가 의미 있을 때(일부 비전/센서). 텍스트 임베딩에서는 상대적으로 덜 사용.

실무 팁:

  • 텍스트 임베딩은 대개 cosine 또는 “정규화 + dot” 중 하나로 표준화하면 사고가 줄어듭니다.

운영에서 자주 터지는 함정 6가지

  1. 벡터 차원은 큰데, 실제로는 필터/메타데이터가 검색의 대부분을 결정함
  2. cosine인데 정규화가 파이프라인 일부에서 누락(배치 작업, 재색인 작업 등)
  3. 너무 작은 upsert/insert로 세그먼트/오버헤드 폭증
  4. 인덱스 파라미터를 “검색 품질” 기준으로만 올려서 빌드가 감당 불가
  5. 초기 적재부터 압축(PQ 등)을 적용해 튜닝 난이도와 빌드 비용이 동시에 증가
  6. 인덱싱 지연을 장애로 오인해 재시도를 과도하게 걸어 더 느려짐(스로틀링 악화)

에이전트/파이프라인이 재시도를 과하게 걸거나 루프를 돌며 동일 데이터를 반복 업서트하는 경우도 실제로 있습니다. RAG/에이전트 운영에서 이런 “무한 반복 요청”을 막는 관점은 LangChain 에이전트 무한루프·툴난사 차단법에서 소개한 방어 전략과 유사합니다.

결론: 가장 효과 큰 순서로 튜닝하라

인덱싱이 느릴 때 효과가 큰 순서대로 정리하면 다음과 같습니다.

  1. 차원 줄이기(가능하면 모델 교체 또는 PCA)로 연산/전송/메모리 비용 자체를 낮춘다
  2. 메트릭과 정규화를 일관되게 맞춰 품질과 인덱싱 효율을 안정화한다
  3. 배치 크기·동시성을 표준화해 오버헤드를 줄이고 처리량을 끌어올린다
  4. 인덱스 파라미터는 “쓰기 단계”와 “검색 단계”를 분리해 튜닝한다(빌드는 가볍게, 검색에서 보정)
  5. Milvus라면 세그먼트/컴팩션을 반드시 모니터링하고, 작은 배치 삽입을 피한다

이 순서대로만 점검해도, Pinecone·Milvus에서 인덱싱 병목의 대부분은 재현 가능하게 해결됩니다.