유사도 검색

벡터 데이터베이스의 핵심은 '쿼리 벡터와 가장 가까운 벡터를 찾아내는 일'이에요. Qdrant에서도 이 가장 기본적인 검색이 Query API라는 하나의 통일된 인터페이스로 제공되고 있어요. 실제 세계에서 가까운 대상들이 벡터 공간에서도 가깝게 놓이도록 신경망이 대상을 변환한다는 점을 기억하면, 이 검색이 왜 '유사도 검색'이라 불리는지 자연스럽게 이해돼요.

출처: 공식문서 - Search

Query API가 지원하는 검색 종류

v1.10.0부터 사용 가능

Qdrant는 모든 종류의 검색·탐색 요청에 Query API 하나를 사용해요. query 파라미터에 무엇을 넣느냐에 따라 Qdrant가 다른 검색 전략을 고르게 되는데, 대표적인 종류는 이렇게 정리돼요:

쿼리 설명
Nearest Neighbors Search 벡터 유사도 검색, 일명 k-NN
Search By Id 이미 저장된 벡터로 검색 — 임베딩 모델 추론 생략
Recommendations 긍정·부정 예시를 함께 제공
Discovery Search 컨텍스트로 검색을 가이드 (one-shot 훈련셋처럼)
Scroll 선택적 필터로 모든 포인트 가져오기
Grouping 특정 필드 기준으로 결과 그룹화
Order By payload 키 기준으로 정렬
Hybrid Search 여러 쿼리를 결합해 더 나은 결과
Multi-Stage Search 큰 임베딩을 위한 성능 최적화
Random Sampling 컬렉션에서 무작위 포인트 추출

거리 메트릭 (Metrics)

벡터 간 유사도를 측정하는 방식, 즉 메트릭을 Qdrant에서는 여러 가지로 지원해요. 어떤 메트릭을 쓸지는 얻은 벡터와 인코더 훈련 방식에 따라 달라져요.

  • Dot product: Dot
  • Cosine similarity: Cosine
  • Euclidean distance: Euclid
  • Manhattan distance: Manhattan (v1.7부터)

유사도 학습 모델에서 가장 흔한 메트릭은 코사인이에요. Qdrant는 이 코사인 메트릭을 두 단계로 나눠 계산해 더 빠른 검색 속도를 얻어요. 첫 단계는 벡터를 컬렉션에 추가할 때 정규화하는 것인데, 벡터마다 딱 한 번만 일어나요. 두 번째 단계는 벡터를 비교하는 것인데, 정규화 덕분에 dot product와 동일해져서 SIMD로 아주 빠르게 처리할 수 있어요.

Search API

검색 쿼리 예시를 볼게요. 아래에서 [0.2, 0.1, 0.9, 0.7]와 유사한 벡터를 찾고 있어요. limit(별명 top)는 가져올 가장 유사한 결과의 개수예요.

{
  "result": [
    { "id": 10, "score": 0.81 },
    { "id": 14, "score": 0.75 },
    { "id": 11, "score": 0.73 }
  ],
  "status": "ok",
  "time": 0.001
}

resultscore 기준으로 정렬된 포인트 id 목록이에요. 기본적으로 payload와 벡터 데이터는 결과에 포함되지 않으며, with_vectors, with_payload 파라미터로 포함시킬 수 있어요.

params 키 아래에는 검색의 커스텀 파라미터를 넣을 수 있어요:

  • hnsw_ef - HNSW 알고리즘의 ef 파라미터
  • exact - 근사 검색(ANN)을 쓰지 않겠다는 옵션. true면 전체 스캔으로 정확한 결과를 찾느라 오래 걸릴 수 있어요.
  • indexed_only - 벡터 인덱스가 아직 안 만들어진 세그먼트에서 검색을 끄는 옵션
  • quantization - 양자화 관련 파라미터
  • acorn - ACORN 검색 알고리즘 관련 파라미터
  • idf - 희소 벡터 IDF 통계를 어떤 집단에서 계산할지

filter가 지정되면 그 조건을 만족하는 포인트만 검색 대상이 돼요. 필터링 성능을 위해선 필터링할 필드에 payload 인덱스를 만들어 두는 게 좋아요.

Score 기준 결과 필터링

payload 필터링 외에도 유사도 점수가 낮은 결과를 걸러낼 때가 있어요. 이때는 검색 쿼리의 score_threshold 파라미터를 쓰면 주어진 점수보다 나쁜 결과를 모두 제외해요. 주의할 점은 사용하는 메트릭에 따라 더 낮은 점수일 수도, 더 높은 점수일 수도 있다는 거예요. 예를 들어 Euclidean 메트릭에서 더 높은 점수는 더 멀리 떨어졌다는 뜻이므로 제외 대상이 돼요.

희소 벡터와 밀집 벡터의 차이

컬렉션이 희소 벡터로 만들어졌다면 검색에 쓸 희소 벡터의 이름을 지정해야 해요. 밀집 벡터와 희소 벡터 검색에는 중요한 차이가 있어요:

항목 Sparse Query Dense Query
스코어링 메트릭 기본 Dot product (지정 불필요) Distance 지원 메트릭 (Dot, Cosine 등)
검색 방식 Qdrant에서 항상 정확 HNSW는 근사 NN
반환 동작 쿼리 벡터와 같은 인덱스에만 0이 아닌 값을 갖는 벡터 반환 limit 개수만큼 벡터 반환

일반적으로 검색 속도는 쿼리 벡터의 0이 아닌 값 개수에 비례해요.

ACORN 검색 알고리즘

v1.16.0부터 사용 가능

여러 개의 엄격한 payload 필터를 조합할 때 기존 필터 가능 인덱스로는 정확도가 부족할 수 있어요. 이럴 때 쓰는 게 ACORN 검색 알고리즘이에요. 기존 HNSW 알고리즘의 확장으로, 그래프 순회 중 직접 이웃(첫 번째 홉)이 필터링됐을 때 이웃의 이웃(두 번째 홉)까지 탐색해서 정확도를 높여요. 성능 대신 정확도를 얻는 방식이죠.

ACORN은 기본적으로 비활성화돼요. enable 플래그로 켜면, 추정 필터 선택도(selectivity)가 임계값 아래일 때 조건부로 활성화돼요. max_selectivity 값으로 이 임계값을 조절하는데, 0.0이면 절대 안 쓰고 1.0이면 항상 쓰며 기본값은 0.4예요. ACORN은 전형적으로 2~10배 느리지만 제한적인 필터에서 재현율을 높이므로, 정확도 개선이 성능 비용을 정당화하는 시점을 고르는 게 핵심이에요.

Batch Search API

배치 검색 API는 단일 요청으로 여러 검색을 수행하게 해줘요. n개의 배치 검색 요청은 n개의 개별 검색 요청과 동일하죠. 장점은 네트워크 연결이 줄어든다는 점, 그리고 쿼리 플래너가 같은 filter를 쓰는 요청을 감지·최적화해 복잡한 필터에서 지연 시간을 크게 줄여준다는 점이에요.

Query by ID

벡터를 입력으로 써야 할 때마다 포인트 ID를 대신 쓸 수 있어요. 기본적으로 해당 id의 기본 벡터를 가져와 쿼리 벡터로 사용하죠. using 파라미터를 지정하면 그 이름의 벡터를 쓰고, lookup_from 파라미터를 두면 다른 컬렉션의 ID를 참조할 수 있어요. 이때 가져온 벡터는 using 벡터의 특성과 일치해야 해요.

Pagination

Search와 recommendation API는 offset 파라미터로 처음 몇 개 결과를 건너뛸 수 있어요. 예를 들어 offset 100에 10개를 요청하면 11번째 페이지를 가져오는 것과 같죠. 다만 HNSW는 근사 검색이라 순위가 요청마다 조금씩 흔들릴 수 있고, offset으로 페이지네이션하면 같은 포인트가 여러 페이지에 나오거나 빠질 수도 있어요. 이를 우회하는 방법은 세 가지예요:

  • 클라이언트 측 페이지네이션 — 한 번에 큰 배치(예: 상위 100개)를 받아 클라이언트에서 10개씩 페이징. 왕복이 줄고 중복이 없지만 지연이 늘어나요.
  • 정확 검색 — HNSW를 우회해 모든 벡터를 스캔하면 안정·결정적 순서로 반환돼 offset 페이지네이션이 정확히 동작해요. 다만 지연이 커서 작은 컬렉션에서만 실용적이에요.
  • 본 ID 제외 — 이후 페이지에서 이전 페이지에 모은 모든 포인트 ID를 담은 must_not: has_id 필터를 추가해 이미 본 포인트를 제외해요. 페이지마다 제외 목록을 키워가며 반복하는데, 앞으로 순차 페이징에 잘 맞고 임의 페이지로 건너뛰기엔 부적합해요.

Grouping API

여러 포인트가 같은 항목에 속할 때, 같은 항목이 결과에 중복으로 나오는 걸 피하려면 특정 필드로 결과를 그룹화할 수 있어요. 대표적인 예가 큰 문서를 여러 청크로 나눈 경우인데, 문서 ID로 그룹화하면 문서 단위로 결과를 볼 수 있어요. group_by 파라미터에 쓰는 필드의 payload에 전용 인덱스를 만들어두면 성능이 좋아져요. 제한 사항으로는 group_by에 keyword와 integer payload만 지원되고, 그룹화 사용 시 offset 페이지네이션은 허용되지 않아요.

Lookup in Groups

그룹의 포인트들이 제목·초록처럼 큰 공유 필드를 갖는다면, 그 데이터를 모든 포인트에 복사하면 저장 공간이 부풀고 공유 필드가 바뀔 때마다 모든 청크를 다시 써야 해요. with_lookup이 이 문제를 해결해요. 공유 데이터를 별도 컬렉션에 한 번만 저장하고, 쿼리 시점에 groups API로 각 그룹에 붙여주는 방식이죠.

lookup은 점 ID 기준의 단순 조인(join)이지 벡터 검색이 아니에요. 몇 가지 주의점이 있어요:

  • documents 컬렉션에 group_by 값과 일치하는 id의 포인트가 이미 있어야 해요.
  • id 타입이 일치해야 해요. 문자열 group_by 값에 정수 포인트 id를 쓰면 에러 없이 빈 lookup 필드가 반환돼요.
  • documents에 일치 포인트가 없는 그룹 id도 빈 lookup 필드를 받아요.

with_lookup="documents" 축약형은 서버 기본값(with_payload=True, with_vectors=False)을 써서 문서 벡터를 반환하지 않아요. 벡터까지 받아야 한다면 명시적인 WithLookup(...) 형태를 쓰세요. 2만 문서에 문서당 약 24개 청크가 있고 문서 수준 데이터가 ~3KB라면, 청크마다 복사하면 ~1.4GB지만 documents 컬렉션에 한 번만 저장하면 ~60MB로 줄어들어요.

Random Sampling

v1.11.0부터 사용 가능

디버깅·테스트·탐색의 시작점으로 컬렉션에서 무작위 포인트를 추출할 때 쓸 수 있어요. Universal Query API의 일부라 일반 검색 API와 같은 방식으로 씁니다. 쿼리 간 재현 가능한 샘플이 필요하다면(재현율 평가나 train/test 분할) random sampling 대신 slice 필터 조건을 쓰세요 — 항상 같은 부분집합을 반환해요.

Query Planning

검색에 쓰는 필터에 따라 쿼리는 여러 실행 시나리오 중 하나로 동작해요. Qdrant는 가용한 인덱스, 조건의 복잡도, 필터링 결과의 카디널리티에 따라 실행 옵션을 고르는데, 이 과정을 query planning이라 불러요. 전략 선택은 휴리스틱에 크게 의존하고 릴리스마다 달라질 수 있지만, 일반 원칙은 다음과 같아요:

  • 세그먼트마다 독립적으로 플래닝 수행
  • 포인트 수가 임계값보다 적으면 전체 스캔 선호
  • 전략 선택 전에 필터링 결과의 카디널리티 추정
  • 카디널리티가 임계값 아래면 payload 인덱스로 포인트 검색
  • 카디널리티가 임계값 위면 필터 가능 벡터 인덱스 사용
  • 선택도(비율)는 낮지만 카디널리티(양)는 높으면 ACORN 사용

임계값은 configuration 파일과 컬렉션마다 조정할 수 있어요.

더 알아보기 (Learn more)