배치 검색으로 벡터 검색 최적화하기
배치 검색으로 벡터 검색 최적화하기 (Mastering Batch Search for Vector Optimization)
벡터를 한 번에 하나씩, 요청을 하나씩 보내다 보면 어느 순간 "이거 좀 비효율적인데?" 싶은 순간이 찾아와요. 특히 네트워크가 느린 환경에서는 여러 개의 검색 요청을 따로따로 보내는 것만으로도 꽤 큰 오버헤드가 생기거든요. 이 글에서는 Qdrant 0.10.0에 새로 들어온 **배치 검색(batch search)**이라는 기능으로 이런 고민을 어떻게 덜 수 있는지, 예제 코드와 함께 차근차근 살펴볼게요.
출처: 공식문서
배치 검색은 왜 필요할까?
Qdrant 0.10.0 릴리스에는 일상적인 작업을 단순화해 주는 기능들이 많이 추가됐어요. 그에 맞춰 클라이언트 라이브러리의 인터페이스도 조금씩 바뀌었죠. 그중 하나가 바로 여러 개의 벡터로 컬렉션을 한 번에 조회할 수 있는 배치 검색 메커니즘이에요.
검색을 하다 보면 서로 관련 없는 여러 작업을 동시에 처리해야 하는 상황이 자주 생겨요. 예전에는 그럴 때 Qdrant API에 요청을 여러 개 따로 보내는 수밖에 없었어요. 그런데 병렬 요청이 많아지면 네트워크 오버헤드가 커지고, 연결 속도가 느린 환경에서는 전체 프로세스가 눈에 띄게 느려지죠.
이제 배치 검색 덕분에 그런 걱정은 하지 않아도 돼요. Qdrant가 여러 검색 요청을 API 호출 하나에 담아서 그 요청들을 가장 최적화된 방식으로 처리해 줘요.
배치 검색으로 벡터 검색을 최적화하는 예제
공식 Python 클라이언트를 써서 배치 검색을 실제 애플리케이션에 어떻게 통합하는지 보여드릴게요. Qdrant 0.10.0에서 인터페이스가 몇 가지 바뀌었으니, 단계별로 함께 따라가 볼게요.
Step 1: 컬렉션 만들기
첫 단계는 컬렉션을 만들 때 설정을 지정하는 거예요. 최소한 벡터 크기와 벡터 간 유사도를 측정하는 데 쓰는 **거리 함수(distance function)**는 정해 줘야 해요.
from qdrant_client import QdrantClient
from qdrant_client.conversions.common_types import VectorParams
client = QdrantClient("localhost", 6333)
if not client.collection_exists('test_collection'):
client.create_collection(
collection_name="test_collection",
vectors_config=VectorParams(size=4, distance=Distance.EUCLID),
)
Step 2: 벡터 업로드하기
컬렉션이 준비됐으면 이제 벡터를 몇 개 넣어 볼게요. 예제니까 아주 간단한 벡터 몇 개만 사용할게요.
vectors = [
[.1, .0, .0, .0],
[.0, .1, .0, .0],
[.0, .0, .1, .0],
[.0, .0, .0, .1],
[.1, .0, .1, .0],
[.0, .1, .0, .1],
[.1, .1, .0, .0],
[.0, .0, .1, .1],
[.1, .1, .1, .1],
]
client.upload_collection(
collection_name="test_collection",
vectors=vectors,
)
Step 3: 단일 요청으로 배치 검색하기
컬렉션에 데이터가 들어왔으니 이제 유사한 벡터를 찾을 준비가 됐어요. 여기서는 두 가지를 동시에 해 보고 싶다고 가정해 볼게요. 하나는 특정 벡터와 가장 유사한 데이터베이스 항목과의 거리를 찾는 것이고, 다른 하나는 또 다른 벡터 쿼리에 대해 가장 유사한 객체 두 개를 찾는 거예요. 0.9 버전까지는 이 두 작업을 위해 API를 두 번 호출해야 했어요. 이제는 두 요청을 한 번에 보낼 수 있어요:
results = client.search_batch(
collection_name="test_collection",
requests=[
SearchRequest(
vector=[0., 0., 2., 0.],
limit=1,
),
SearchRequest(
vector=[0., 0., 0., 0.01],
with_vector=True,
limit=2,
)
]
)
# Out: [
# [ScoredPoint(id=2, version=0, score=1.9,
# payload=None, vector=None)],
# [ScoredPoint(id=3, version=0, score=0.09,
# payload=None, vector=[0.0, 0.0, 0.0, 0.1]),
# ScoredPoint(id=1, version=0, score=0.10049876,
# payload=None, vector=[0.0, 0.1, 0.0, 0.0])]
# ]
SearchRequest 클래스의 각 인스턴스는 자기만의 검색 파라미터를 가질 수 있어요. 벡터 쿼리뿐 아니라 추가 필터도 지정할 수 있죠. 응답은 각 요청에 대한 개별 결과의 리스트로 돌아와요. 만약 요청 중 하나라도 잘못된 형식이면 예외가 던져져요. 즉 모든 요청이 통과하거나, 아무것도 통과하지 못하거나, 둘 중 하나예요.
이게 전부예요! 이제 여러 요청을 직접 관리할 필요가 없어요. Qdrant가 내부적으로 다 처리해 줍니다.
배치 검색 벤치마크
배치 검색은 애플리케이션에 통합하기 꽤 쉬워요. 그래도 전환을 결정하기 전에 숫자를 먼저 보고 싶다면, 다음 네 가지 옵션을 비교해 보는 게 좋아요:
- 데이터베이스를 순차적으로 조회하기.
- 개별 요청을 여러 스레드/프로세스로 보내기.
- Qdrant의 배치 검색을 단일 요청으로 활용하기.
- 병렬 처리와 배치 검색을 결합하기.
이를 위해 우리는 glove-25-angular 데이터셋의 벡터로 좀 더 풍성한 포인트 컬렉션을 만들었어요. ANN 비교에서 꽤 흔하게 쓰이는 선택이죠. Qdrant를 어떻게 벤치마크했는지 자세히 보고 싶다면 Gist를 확인해 보세요.
그 결과
우리는 10000개의 테스트 벡터로 벤치마크를 5회 실행하고 결과를 평균냈어요. 아래 숫자는 모든 시도의 평균값이에요:
- 순차 검색: 225.9초
- 배치 검색: 208.0초
- 멀티프로세싱 검색 (8개 프로세스): 194.2초
- 멀티프로세싱 배치 검색 (8개 프로세스, 배치 크기 10): 148.9초
구체적인 하드웨어에 따라 결과는 달라질 수 있어요. 그래도 언뜻 보기에 배치 검색이 시간을 꽤 아껴 준다는 건 분명해 보여요.
추가 개선은 분산 배포 환경에서 이뤄질 수 있어요. Qdrant가 과도한 inter-cluster 요청을 만들지 않아도 되기 때문이죠. 게다가 요청들이 같은 필터 조건을 공유한다면, 쿼리 최적화기가 그 필터를 배치 요청들 사이에서 재사용할 수 있어요.
정리
배치 검색을 쓰면 서로 다른 쿼리들을 단일 API 호출에 담아서, 단일 응답으로 결과를 받을 수 있어요. 지금까지 Qdrant로 연속된 쿼리를 하나씩 보내느라 애를 먹었다면, 새로운 배치 검색 메서드로 쉽게 전환해서 애플리케이션 코드를 단순화할 수 있어요. 벤치마크에서 봤듯이, 남는 네트워크 오버헤드와 필터 재사용 가능성까지 고려하지 않아도 Qdrant와의 상호작용을 30% 이상 빠르게 만들 수 있어요!