이번에 진행했던 멋쟁이 사자처럼 13기에서 Qdrant 데이터베이스를 이용하여 RAG 서비스(?)를 구축했던 것을 기억하기 위해 작성했다.
Qdrant는 여태 사용해왔던 MySQL과는 사용하는 방법이 조금 달랐다.
이상이 사용했던 DB들이고, 아래는 Qdrant다.
나름대로 이런 서비스들은 공식 사이트에서 다운로드할 수 있는 법이어서, 나름 생소하다면 생소한 부분이었다.
Qdrant Official Docs - Quick Start Qdrant
로컬에서는 docker run 명령어를 사용했다.
docker pull qdrant/qdrant
docker run -p 6333:6333 -p 6334:6334 \
-v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
qdrant/qdrant
-v는 Qdrant 컨테이너가 종료되어도, 기존의 저장되어있는 정보들을 저장해두고 추후 재시작해도 데이터가 남아있게 해주는 옵션이다
서버에 올린 것은 컴포즈 방식으로 올렸는데, 코드는 다음과 같다.
docker-compose.yml은 아래와 같이 작성하였다
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant-container
ports:
- "6333:6333"
- "6334:6334"
restart: always
volumes:
- qdrant-data:/qdrant/storage
volumes:
qdrant-data:

보시는 것과 같이 Qdrant를 WebUI로 확인하는 모습이다.
좌측 "Welcome"에선 샘플 데이터를 다운로드 하는 버튼이 있고, 그것을 예제로 조금 보여드리자면
(우측 JSON 데이터 위의 run하는 버튼을 누른 모습)
이런 식으로 포인트들을 확인할 수 있다.




위치가 비슷한 포인트끼리는 비슷한 이미지인 것을 볼 수 있다.
우리의 프로젝트에서는 "text-embedding-3-small" 이라는 모델을 사용했다. 잠깐 용어에 대해 짚고 가자면, embed라는 것은 해당 포인트의 의미를 Vector값으로 변환하는 것을 의미한다. 그리고 이를 수행하는 AI 모델이 벡터 임베딩 모델인 것이다.
from openai import OpenAI
openai_client = OpenAI(api_key=API_KEY)
openai_client.embeddings.create(
input=data,
model="text-embedding-3-small"
).data[0].embedding
위는 임베딩하는 모습이다.
이 때, create로 생성된 값은 오직 해당 임베딩 모델이 반환할 수 있는 차원 개수의 벡터값이므로, 이후 벡터 DB에 값을 저장하려고 하면, 그 값을 알고 있다가 저장하여야 한다.
# 벡터 임베딩 모델을 사용하여 벡터값으로 변환하고, data[0], 즉 벡터값이 있는 인덱스에서 꺼내옵니다.
vector_val = openai_client.embeddings.create(
input="아야하면우유",
model="text-embedding-3-small"
).data[0].embedding
# Qdrant는 Key-Value로 노드를 구성합니다.
# Qdrant는 구성된 정보를 "payload"로 부릅니다.
point = PointStruct (
id = 1,
payload = {
"author": "아야하면 우유"
}
)
# 이제 Qdrant에 Upsert(Update & Insert)할 차례입니다.
qdrant_client.upsert(
collection_name=COLLECTION_NAME.
vector=vector_val,
payload=point
)
생각보다 간단하다. Retrieval-Argumented-Generate 이름부터 압도됐었지만, 하고보니 간단한 것이다.
원래 이런 유사도 검색에는 몇가지 방식이 있다
두 벡터 간 직선 거리를 측정해서 계산
두 벡터가 가리키는 방향, 즉 기준점과 두 벡터 사이에서 이루는 각의 코사인 값
두 집합의 교집합 크기를 합집합 크기로 나눈 것, 즉 두 집합의 공통분모가 전체(합집합)에서 이루는 비율
이번 프로젝트를 진행하기 전까지만 해도 완전히 무지했었는데, 잠깐의 웹서핑으로 이 세가지가 가장 대표적인 유사도 검색인듯 싶었다. 그리고, 텍스트 간 유사도를 검색하는 데에는 "코사인 유사도"가 가장 성능이 좋다 라는 이야기가 있어서, 우리 프로젝트에서도 코사인 유사도 검색을 기반으로 했다.
검색하는 방식은 정말 간단한데, 아래에 바로 예시를 보이면,
# 여기에서 Qdrant와 Python이 어떻게 연결되는지 보여드리겠습니다.
# 우선 Qdrant에 접근할 수 있게, 의존성과 함수를 명시하겠습니다.
from qdrant_client import QdrantClient
from qdrant_clinet.models import Distance, VectorParams
# 로컬 Python <-> 도커 Qdrant로 통신하고 있으므로, localhost로 접근합니다.
# 컨테이너 간 통신은 host.docker.internal 등을 테스트해보시는걸 추천드립니다.
qdrant_client = QdrantClient(host="localhost", port=6333)
# Qdrant 컬렉션을 무조건 재생성합니다.
# size는 임베딩 모델이 지원하는 차원 수를 작성하시면 됩니다.
# Distance가 COSINE으로 지정되었으므로,
# 이후 search가 코사인 유사도 검색으로 이루어집니다.
qdrant_client.recreate_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(size=1536, distance=Distance.COSINE)
# 만들어져있을 때 재생성하고 싶지 않다면, 아래의 boolean 조건을 조건문으로 분기하세요.
# qdrant_client.collection_exists(COLLECTION_NAME)
# 대망의 검색입니다.
# 우선 검색값을 임베딩합니다.
vector_embed = openai_client.create(
input="왕이 넘어지면 킹콩",
model="text-embedding-3-small"
).data[0].embedding
# 그리고 이제 Qdrant에서 검색합니다.
qdrant_client.search(
collection_name=COLLECTION_NAME,
query_vector=vector_embed,
limit=1
)
Qdrant의 search 메서드는 유사도를 정렬 기준으로 해서, 가장 유사도가 높은 값을 0번으로 내림차순 저장합니다. 이 때, limit 옵션이 1이라는 것은
"가장 유사도가 높은 검색값을 구하라"
라는 뜻이 되겠다.
정말 이 페이지 하나만으로 일단 Vector DB를 사용한 RAG의 기본 중의 기본은 다 적은 것 같다. 일단 1편은 여기서 마무리 짓는 것으로 하고, 추후에 2편, 3편을 제작할 수 있으면 제작해보도록 하겠다.