이 튜토리얼은 Damilola Oladele에 의해 작성되었습니다.
본 튜토리얼에서는 Python을 사용하여 MongoDB Atlas Vector Search 인덱스를 필드별로 정의하고, 생성하며, 검증하는 방법을 보여줍니다. 이 인덱스가 검색할 실제 데이터를 제공하기 위해, 자체 텍스트 문서에 대한 질문에 답변하는 데 AI를 사용하는 작은 도구도 구축합니다.
이 튜토리얼을 통해 다음 내용을 학습하게 됩니다:
- Voyage AI의 무료 티어(free tier)를 사용하여 벡터 임베딩(vector embeddings)을 생성하고 저장하는 방법.
- MongoDB Atlas Vector Search 인덱스가 필드별로 어떻게 작동하는지: 차원(dimensions), 유사성(similarity), 양자화(quantization), 그리고 인덱싱 방식(indexing method).
- PyMongo를 사용하여 Vector Search 인덱스를 생성하는 방법과, 코드가 상태(status) 대신
queryable을 확인해야 하는 이유.
$vectorSearch 쿼리를 사용하여 인덱스를 검증하는 방법.
- 인덱스를 사용하여 자체 문서로부터 AI 기반 답변을 생성하는 방법.
- Vector Search 인덱스를 편집, 모니터링 및 삭제하는 방법.
이 튜토리얼의 모든 코드 샘플은 GitHub 저장소에서 찾을 수 있습니다.
사전 준비 사항 (Prerequisites)
시작하기 전에 다음 항목들을 준비했는지 확인하세요:
- Python 3.10 이상.
- 무료 M0 클러스터가 있는 MongoDB Atlas 계정.
Database > Connect > Drivers에서 가져온 Atlas 연결 문자열(connection string).
Security > Network Access에서 추가한 현재 IP 주소.
- 클러스터와 동일한 Atlas 프로젝트 내의
AI Model APIs에서 생성한 Voyage AI 모델 API 키. API 키를 얻으려면 다음 단계를 따르세요:
1. M0 클러스터와 동일한 Atlas 계정에 로그인합니다.
2. Atlas 프로젝트에서 왼쪽 탐색 메뉴의 *AI Model APIs*를 선택합니다.
3. *Create model API key*를 선택합니다.
...
MongoDB 벡터 검색 인덱스란 무엇이며 왜 필요한가요?
설정 과정에 들어가기 전에 벡터 인덱스가 무엇인지 아는 것이 도움이 됩니다. 일반적인 MongoDB 인덱스는 "이것과 의미가 유사한 문서를 찾아라"라는 질문에 답할 수 없습니다. 오직 정확하거나 범위로 지정된 값만 일치시킬 수 있을 뿐, 의미적 근접성(semantic closeness)은 처리하지 못합니다.
벡터 인덱스는 이를 다르게 해결합니다. 이는 텍스트 조각의 의미를 포착하는 임베딩 모델(embedding model)이 생성한 숫자 리스트인 임베딩(embeddings)을 기반으로 작동합니다. 유사한 의미를 가진 두 구절은 단어 하나도 공유하지 않더라도, 그 숫자 공간에서 서로 가깝게 위치하는 임베딩을 생성합니다.
높은 수준에서 볼 때, MongoDB Vector Search는 이러한 임베딩에 대해 근사 최근접 이웃 검색(approximate nearest neighbor search)을 수행합니다.
mongot은 Atlas가 검색(Search) 및 벡터 검색 인덱스를 구축하고 제공하는 별도의 프로세스입니다. 이는 문서를 저장하는 프로세스인 mongod와는 다릅니다. mongot은 백그라운드에서 인덱스를 구축하고 제공합니다. 이것이 바로 벡터 검색 인덱스에 "생성됨(created)"과 "쿼리 가능함(queryable)"이라는 두 가지 다른 시점이 존재하는 이유입니다. 이 튜토리얼에서는 나중에 직접 생성할 때 이를 깊이 있게 다룰 것입니다.
나머지 튜토리얼은 하나의 경로를 따릅니다. 먼저 문서를 임베딩하고 저장한 다음, 그 저장된 임베딩 위에 인덱스를 구축하는 방식입니다.
프로젝트 설정 및 Atlas 연결
이 섹션에서는 로컬 언어 모델, Python 환경, 그리고 코드를 Atlas에 연결하는 파일을 준비합니다.
프로젝트 폴더를 만들고 이동하세요:
mkdir vector-search-index-tutorial
cd vector-search-index-tutorial
언어 모델 가져오기
Ollama는 언어 모델을 사용자의 자체 기기에서 실행하므로 요청이 컴퓨터를 벗어나지 않습니다.
Ollama가 설치되었는지 확인하세요:
Ollama 데스크톱 애플리케이션을 사용하는 경우, 계속하기 전에 이를 실행하세요. 대신 명령줄에서 Ollama를 사용하는 경우, 별도의 터미널에서 서버를 시작하세요:
이 터미널은 켜둔 상태로 유지합니다. 두 번째 터미널을 열고 모델을 가져오세요:
cd vector-search-index-tutorial
ollama pull llama3.2:3b
가져오기가 성공했는지 확인하세요:
목록에 llama3.2:3b가 나타나야 합니다.
가상 환경 생성하기
가상 환경을 생성한 다음 활성화합니다:
python3 -m venv venv
source venv/bin/activate
Windows에서는 python -m venv venv로 생성하고, 대신 venv\Scripts\activate로 활성화하세요.
패키지 설치하기
튜토리얼의 의존성을 설치합니다:
pip install pymongo voyageai ollama python-dotenv
앞서 명령으로 설치한 ollama 패키지는 이미 설정된 Ollama 애플리케이션을 호출하는 Python 클라이언트 라이브러리입니다. 이는 애플리케이션 자체와는 별도의 설치이며, 마찬가지로 pymongo가 MongoDB 자체라기보다는 MongoDB를 위한 Python 클라이언트인 것과 같습니다.
환경 파일에 연결 정보 추가하기
프로젝트 폴더에 .env라는 파일을 만들고 다음 내용을 추가합니다:
MONGODB_URI="<your-connection-string>"
VOYAGE_API_KEY="<your-voyage-api-key>"
플레이스홀더를 실제 값으로 대체하세요. python-dotenv는 이 두 가지 값을 런타임에 환경 변수로 로드합니다.
설정 파일 생성하기
config.py 파일을 만들고 다음 코드를 추가하여 프로젝트의 상수들을 저장하고 .env 파일을 로드하게 합니다:
import os
from dotenv import load_dotenv
...
데이터베이스 접근 파일 생성하기
db.py 파일을 만들고 다음 코드를 추가하여 get_client()와 get_collection()을 정의합니다. 이 함수들은 다른 모든 파일에서 재사용되는 헬퍼(helper)입니다:
from functools import cache
from pymongo import MongoClient
...
클라이언트(Client)는 자체 연결 풀을 관리하므로 프로그램은 단 하나만 필요합니다. @cache 데코레이터는 get_client()가 처음 호출될 때 클라이언트를 생성하고 이후 모든 호출에서 동일한 클라이언트를 반환하도록 합니다.
Voyage AI를 사용하여 임베딩 생성하기
임베딩(embed) 호출은 텍스트를 입력받아 고정 길이의 벡터를 출력합니다. 이 튜토리얼에서는 각 문서를 더 작은 구절, 즉 청크(chunk)로 분할하고 각 청크를 임베딩합니다. 다음 섹션에서는 분할하는 방법을 다룹니다.
이 튜토리얼은 기본값이 1,024 차원인 voyage-4를 사용합니다. Voyage 모델은 또한 유연한 출력 크기를 지원합니다. 나중에 속도를 위해 크기를 조정하고 싶다면 유용합니다. 이 튜토리얼에서는 기본값을 유지합니다.
각 임베딩 호출은 input_type도 설정합니다. Voyage는 텍스트가 저장되는 경우(input_type="document")인지 검색 쿼리로 사용되는 경우(input_type="query")인지에 따라 다른 작업별 프롬프트를 적용합니다. 문서 임베딩과 쿼리 임베딩은 여전히 동일한 임베딩 공간에 위치하므로, 어떤 방식으로든 검색을 위해 비교 가능하게 유지됩니다. 서로 다른 프롬프트는 오직 검색 품질을 향상시키기 위한 것입니다.
결제 수단이 등록되지 않은 계정은 낮은 속도 제한(rate limit)을 받습니다: 분당 3회 요청 및 분당 10,000 토큰입니다. 단일 문서의 전체 청크 목록은 한 번의 호출만으로 쉽게 이 한도를 초과할 수 있으므로, embed_documents()는 요청을 배치로 처리하고 속도를 조절해야 합니다.
embeddings.py 파일을 생성하고 다음 코드를 추가합니다:
import time
import voyageai
...
embed_documents()는 청크 목록을 30개 단위의 배치로 분할합니다. 이는 무료 등급에서도 10,000 토큰 한도보다 훨씬 낮습니다. 또한 호출마다 21초 동안 일시 중지됩니다. 이를 통해 전체 데이터 수집(ingest) 실행이 파일 하나가 아닌 모든 파일을 통틀어 분당 3회 요청 미만으로 유지됩니다.
embed_query()는 검색 시점에 단일 질문을 임베딩합니다. 작은 요청 한 번으로는 어느 쪽의 한도에도 근접하지 못합니다.
문서와 그 임베딩으로 분할 및 저장하기
청크(chunk)가 전체 문서보다 더 나은 결과를 가져옵니다. 모델이 질문을 전체 파일에 대조하는 것보다 집중된 구절과 일치시키는 것이 더 정확합니다. 청크는 또한 인용이 전체 문서가 아닌 특정 구절을 가리킬 수 있게 합니다. 이 튜토리얼에서는 청킹(chunking)을 간단하게 유지합니다. 청킹에 대해 더 자세히 알아보려면 How to Build an AI PDF Analyzer with MongoDB, RAG, and Ollama를 참조하세요.
저장된 각 청크는 임베딩(embedding)이 첨부되면 다음과 같은 형태가 됩니다:
{
"source_file": "notes.txt",
"chunk_index": 0,
...
기존에 있던 두 함수 다음에 db.py 파일에 다음 save_chunks() 헬퍼를 추가하세요:
def save_chunks(filename, chunks, embeddings):
collection = get_collection()
for index, (chunk, embedding) in enumerate(zip(chunks, embeddings)):
...
필터는 source_file과 chunk_index를 기준으로 일치시켜 각 청크를 식별합니다. upsert=True이기 때문에, 이스트(ingest) 스크립트를 다시 실행해도 중복을 삽입하는 대신 일치하는 청크를 업데이트합니다. 마지막의 delete_many()는 이전 실행에서 남아 있는 청크들을 제거하여, 파일이 이전에 비해 더 적은 수의 청크를 생성할 때 유용합니다.
ingest.py 파일을 만들고 다음 코드를 추가하세요:
import os
from db import save_chunks
...
chunk_text()는 파일의 빈 줄을 기준으로 단락으로 분할한 다음, 전체 단락들을 최대 CHUNK_SIZE 크기의 청크로 그룹화합니다. 청크는 절대로 단어나 문장을 나누지 않으며, 어떤 텍스트도 두 개의 청크에 나타나지 않습니다. CHUNK_SIZE보다 긴 단일 단락은 그 자체로 하나의 청크가 됩니다.
sample_documents 폴더에 몇 개의 .txt 파일을 넣고 스크립트를 실행하세요. 무료 체험 등급에서는 각 파일의 크기를 수천 단어 이내로 유지하여, 단일 파일의 청크들이 한 배치(batch)에 들어오도록 하세요. 샘플 문서는 이 튜토리얼의 코드 샘플을 호스팅하는 GitHub 저장소에서 찾을 수 있습니다.
터미널에서 다음과 유사한 응답을 받게 될 것입니다:
Ingested 25 chunks from a_modest_proposal.txt.
Ingested 13 chunks from declaration_of_independence.txt.
Ingested 17 chunks from the_cask_of_amontillado.txt.
Atlas UI를 사용하거나 mongosh에서 간단한 카운트 쿼리를 통해 청크가 Atlas에 저장되었는지 확인할 수도 있습니다.
벡터 검색 인덱스 생성하기
MongoDB Vector Search 인덱스는 mongot에게 저장된 임베딩(embedding)을 빠르고 유사도 검색할 수 있도록 어떻게 구성해야 하는지 알려줍니다. mongot은 모든 문서를 스캔하는 대신, 임베딩 필드에 걸쳐 검색 가능한 구조를 구축합니다. 이 인덱스 정의는 mongot이 해당 구조를 어떻게 구축하고 검색할지를 제어합니다.
정의문의 fields 배열에는 인덱싱되는 각 필드에 대한 항목이 포함됩니다. 이 튜토리얼에서는 두 개의 필드를 인덱싱합니다:
embedding: 각 청크의 임베딩을 담는 vector 필드입니다.
source_file: 쿼리가 결과를 특정 문서로 좁힐 수 있게 하는 filter 필드입니다.
벡터 필드는 가장 많은 설정이 필요합니다. 그 type은 vector이며, path는 임베딩을 포함하는 문서 필드의 이름을 지정합니다:
{
"type": "vector",
"path": "embedding"
...
벡터 필드에 대한 네 가지 추가 설정은 mongot이 임베딩을 저장하고 비교하는 방식을 제어합니다:
numDimensions: 각 임베딩의 크기입니다.
similarity: 인덱스가 두 벡터 간의 근접성을 측정하는 방식입니다.
quantization: 저장된 각 벡터가 유지하는 정밀도 수준입니다.
indexingMethod: 인덱스가 검색하는 구조입니다.
첫 번째 설정인 numDimensions는 임베딩 모델 출력의 차원 수와 정확히 일치해야 합니다. 이 튜토리얼에서는 1,024차원의 임베딩을 생성하는 voyage-4를 사용합니다. 값이 일치하지 않으면 Atlas가 인덱스 정의를 거부하거나 쿼리가 실패합니다.
벡터 크기를 설정한 후, similarity는 인덱스가 두 벡터를 비교하는 방식을 결정합니다. 이 값은 세 가지 중 하나를 허용합니다:
euclidean: 벡터 간의 거리를 측정합니다.
cosine: 벡터 간의 각도를 측정합니다.
dotProduct: 정규화된(normalized) 벡터에 적합하며, MongoDB 자체 참고 예제에서 Voyage 임베딩과 쌍을 이루는 방식입니다.
본 튜토리얼은 이 조합을 따르며 dotProduct를 사용합니다.
여기서 similarity는 벡터가 어떻게 비교되는지를 결정하고, quantization은 그것들이 얼마나 정밀하게 저장되는지를 결정합니다. quantization은 세 가지 값 중 하나를 허용합니다:
none: 전체 정밀도(full precision)이며 기본값입니다.
scalar: 메모리 사용량 감소와 더 빠른 검색을 위해 일부 정밀도를 포기하는 방식입니다.
binary: 정밀도에 더 큰 비용이 들지만 훨씬 더 많이 압축합니다.
본 튜토리얼은 scalar를 사용하는데, 이는 binary의 훨씬 더 공격적인 압축 없이 리소스 사용량을 줄여주기 때문입니다. 하지만 scalar가 항상 올바른 기본값인 것은 아닙니다. 최적의 설정은 사용자 자신의 검색 품질(retrieval-quality) 요구 사항에 따라 달라집니다.