이런 순간에는 일반적인 텍스트 검색이 한계를 드러냅니다. 실제로는 다음과 같은 상황이 발생합니다.
- 고객지원 포털에서 사용자가 도움말 문서에 쓰인 정확한 문구 대신 "전원이 켜지지 않음"이라고 입력하면 결과가 반환되지 않습니다.
- 이커머스 플랫폼에서 쇼핑객이 "겨울에 따뜻한 것"을 검색하면 제품 설명에 그 정확한 단어가 없어 아무것도 찾지 못합니다.
- 지식 베이스는 사용자가 문서와 다른 표현으로 질문하면 올바른 문서를 놓칩니다.
시맨틱 검색은 이를 해결합니다. 정확한 단어 일치가 아니라, 쿼리의 의미와 의도를 이해합니다.
이 튜토리얼에서는 Python과 무료 nomic-embed-text-v1 임베딩 모델을 사용해 MongoDB에서 시맨틱 검색을 구현하는 방법을 보여드립니다.
참고: nomic-embed-text-v1은 메모리에 로드하면 약 0.27GB이므로, 이 튜토리얼의 스크립트를 실행하기 전에 최소 1GB 이상의 사용 가능한 RAM이 있는지 확인하세요. 처음 실행 시 모델이 Hugging Face에서 다운로드되므로 인터넷 연결 속도에 따라 시간이 더 필요할 수 있습니다. 이후 실행에서는 로컬 캐시에서 모델이 로드됩니다. CPU 추론은 GPU 추론보다 훨씬 느립니다. 전용 GPU가 없는 머신에서는 모델이 CPU에서 실행되어 VRAM 대신 RAM을 사용하므로 임베딩 생성에 시간이 더 걸릴 수 있습니다.
이 튜토리얼에서 다음을 학습합니다.
- 벡터 임베딩이 무엇이며 의미를 어떻게 표현하는지
- MongoDB에서 임베딩을 생성하고 저장하는 방법
- MongoDB Vector Search 인덱스를 만드는 방법
- 사용자 쿼리를 벡터로 변환하는 방법
$vectorSearch집계 파이프라인을 실행하고 결과를 해석하는 방법
이 튜토리얼의 모든 코드 샘플은 GitHub 저장소에서 확인할 수 있습니다.
텍스트 검색 vs 시맨틱 검색
텍스트 검색은 쿼리에 포함된 정확한 단어를 포함하는 문서를 매칭합니다. 시맨틱 검색은 전혀 다른 단어를 사용하더라도 쿼리와 같은 의미를 담은 문서를 매칭합니다.
다음 표는 "heart problems"라는 검색 쿼리에 대해 각 접근 방식이 반환하는 결과를 보여줍니다.
| 문서 텍스트 | 텍스트 검색 | 시맨틱 검색 | 텍스트 검색이 반환하는 이유 |
|---|---|---|---|
| "...heart problems in adults..." | 예 | 예 | 정확한 단어 "heart problems"를 포함함 |
| "...cardiac conditions and symptoms..." | 아니요 | 예 | 정확한 단어 "heart problems"를 포함하지 않음 |
| "...risk factors for heart disease..." | 아니요 | 예 | 정확한 단어 "heart problems"를 포함하지 않음 |
| "...chest pain and shortness of breath..." | 아니요 | 예 | 정확한 단어 "heart problems"를 포함하지 않음 |
시맨틱 검색 핵심 개념
MongoDB의 시맨틱 검색에는 세 가지 개념이 핵심입니다. 이를 이해하면 이 튜토리얼의 각 단계가 왜 그렇게 동작하는지 파악하는 데 도움이 됩니다.
벡터 임베딩
임베딩 모델은 텍스트를 벡터라 불리는 고정 길이의 숫자 목록으로 변환합니다. 벡터의 각 숫자는 의미의 한 차원을 나타냅니다.
유사한 의미의 두 텍스트는 수치적으로 서로 가까운 벡터를 생성합니다. "cardiac conditions"와 "heart problems"는 단어를 공유하지 않더라도 벡터 공간에서는 서로 가깝게 위치합니다. 이러한 근접성이 시맨틱 검색을 가능하게 합니다.
이 튜토리얼은 로컬 머신에서 완전히 실행되는 무료 오픈 소스 임베딩 모델 nomic-embed-text-v1을 사용합니다. 스크립트를 처음 실행하면 모델이 Hugging Face에서 자동으로 다운로드되어 이후 실행을 위해 로컬에 저장됩니다. 이 모델은 입력당 768개의 숫자를 생성합니다.
벡터 검색 인덱스
벡터 검색 인덱스는 어떤 필드에 임베딩이 저장되어 있는지, 몇 차원인지, 비교에 어떤 유사도 함수를 사용할지 MongoDB에 알려줍니다. 어떤 $vectorSearch 쿼리를 실행하기 전에 반드시 이 인덱스를 만들어야 합니다.
$vectorSearch 집계 단계
$vectorSearch는 검색을 수행하는 집계 단계입니다. 쿼리 벡터를 받아 인덱싱된 필드를 검색하고, 시맨틱 유사도 순으로 문서를 반환합니다. 다른 집계 파이프라인과 마찬가지로 $project 등의 단계와 함께 체이닝합니다.
그럼 시작해 보겠습니다.
사전 준비 사항
시작하기 전에 다음을 준비하세요.
- Python 3.8 이상 설치
- M0 (무료 티어) 클러스터가 설정된 MongoDB Atlas 계정
pymongo4.7 이상,sentence-transformers,einopsPython 패키지 설치- Python과 MongoDB 컬렉션에 대한 기본 지식
- Atlas 연결 문자열(Atlas UI의 **Database > Connect > Drivers**에서 확인 가능)
- 스크립트를 실행하기 전에 Atlas에서 IP 주소가 허용되어 있어야 합니다. Atlas UI의 **Security > Network Access**로 이동하여 현재 IP 주소를 추가하세요.
프로젝트 설정
프로젝트용 폴더를 만들고 터미널에서 해당 폴더로 이동하세요.
mkdir mongodb-semantic-search
cd mongodb-semantic-search
프로젝트 종속성을 격리하기 위해 가상 환경을 생성하고 활성화하세요.
python -m venv venv
source venv/bin/activate
Windows에서는 다음으로 가상 환경을 활성화합니다.
venv\Scripts\activate
이제 필요한 패키지를 설치하세요.
pip install pymongo sentence-transformers einops
이 튜토리얼의 각 단계마다 하나의 Python 파일을 만듭니다. 모든 파일은 mongodb-semantic-search 폴더에 저장합니다.
임베딩 유틸리티 파일 만들기
embedding_utils.py라는 파일을 만드세요. 이 파일에는 튜토리얼에서 사용하는 두 가지 임베딩 함수가 포함됩니다.
from sentence_transformers import SentenceTransformer
# Load the free, open-source embedding model.
# The model downloads from Hugging Face on first run and saves locally.
# trust_remote_code=True is required by this model:
model = SentenceTransformer("nomic-ai/nomic-embed-text-v1", trust_remote_code=True)
def get_embedding(text, precision="float32"):
# Use this function when embedding text you plan to store in MongoDB:
return model.encode(text, precision=precision).tolist()
def get_query_embedding(text, precision="float32"):
# Use this function when embedding a user's search query:
return model.encode(text, precision=precision).tolist()
경고: trust_remote_code=True는 Hugging Face에서 다운로드된 모델별 Python 코드를 로컬 머신에서 실행할 수 있도록 허용합니다. Hugging Face의 모델 소스가 손상되거나 악의적인 변경으로 업데이트되면 해당 코드가 환경에서 자동으로 실행됩니다. 프로덕션 환경에서 이 매개변수를 사용할 때는 주의가 필요합니다.
벡터 임베딩 생성 및 저장
generate_embeddings.py라는 파일을 만드세요. 이 파일은 embedding_utils.py에서 임베딩 함수를 가져와 각 문서의 text 필드에 대한 벡터를 생성하고, 임베딩을 포함한 전체 문서를 MongoDB에 저장합니다.
from embedding_utils import get_embedding
from pymongo import MongoClient
# Replace the placeholder with your Atlas connection string:
mongodb_client = MongoClient(
"mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/",
appname="devrel-tutorial-python-semantic-search"
)
collection = mongodb_client["sample_db"]["documents"]
# Sample data:
sample_documents = [
{
"title": "MongoDB Atlas",
"text": "MongoDB Atlas is a fully managed cloud database."
},
{
"title": "Vector Search",
"text": "Vector search finds results based on semantic meaning."
},
{
"title": "Nomic AI",
"text": "nomic-embed-text-v1 is a free, open-source embedding model."
},
]
docs_to_insert = []
for doc in sample_documents:
embedding = get_embedding(doc["text"])
docs_to_insert.append({
"title": doc["title"],
"text": doc["text"],
# The vector lives alongside your original data in the same document:
"embedding": embedding
})
# Drop the collection before each run to avoid inserting duplicate documents:
collection.drop()
result = collection.insert_many(docs_to_insert)
print(f"Inserted {len(result.inserted_ids)} documents with embeddings.")
mongodb_client.close()
위 코드에서 collection.drop()은 매 실행 전에 컬렉션의 모든 문서와 인덱스를 삭제합니다. generate_embeddings.py에서 이 줄 없이 다시 실행하면 문서가 중복 삽입되어 $vectorSearch가 동일한 문서를 여러 번 반환하게 됩니다. 컬렉션을 드롭하면 벡터 검색 인덱스도 삭제되므로, generate_embeddings.py를 다시 실행할 때마다 create_vector_index.py를 재실행해야 합니다.
스크립트를 실행하세요.
python generate_embeddings.py
터미널에 다음과 같은 출력이 표시됩니다.
<All keys matched successfully>
Inserted 3 documents with embeddings.
이제 MongoDB의 각 문서에는 원본 텍스트와 768차원 벡터가 embedding 필드에 함께 저장되어 있습니다. 벡터는 동일한 문서 내 데이터와 나란히 존재하므로, 쿼리 시 별도의 저장소나 조회가 필요하지 않습니다.
MongoDB Vector Search 인덱스 만들기
create_vector_index.py라는 파일을 만드세요. 이 파일은 embedding 필드에 벡터 검색 인덱스를 정의하고 생성하여 MongoDB가 컬렉션에 대해 $vectorSearch 쿼리를 실행할 수 있도록 합니다.
인덱스 정의에는 세 가지 필드가 필요합니다.
path: 임베딩이 저장된 필드(이 튜토리얼에서는embedding)numDimensions: 모델의 출력 크기와 일치해야 함(nomic-embed-text-v1은768)similarity: 비교 함수(이 모델에는cosine이 적합)
numDimensions 값은 임베딩 모델과 정확히 일치해야 합니다. 불일치 시 인덱스 생성이 실패합니다.
from pymongo.mongo_client import MongoClient
from pymongo.operations import SearchIndexModel
import time
# Replace the placeholder with your Atlas connection string:
mongodb_client = MongoClient(
"mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/",
appname="devrel-tutorial-python-semantic-search"
)
# Point to the same database and collection you used in the previous step:
database = mongodb_client["sample_db"]
collection = database["documents"]
# Define the vector search index.
# The three required fields tell MongoDB what to index and how to compare vectors:
search_index_model = SearchIndexModel(
definition={
"fields": [
{
"type": "vector",
"path": "embedding", # The field name from generate_embeddings.py
"numDimensions": 768, # nomic-embed-text-v1 always outputs 768 dimensions
"similarity": "cosine" # Recommended similarity function for this model
}
]
},
name="vector_index",
type="vectorSearch"
)
result = collection.create_search_index(model=search_index_model)
print("New search index named " + result + " is building.")
# Poll every five seconds until the index is ready to accept queries:
print("Polling to check if the index is ready. This may take up to a minute.")
predicate = lambda index: index.get("queryable") is True
while True:
indices = list(collection.list_search_indexes(result))
if len(indices) and predicate(indices[0]):
break
time.sleep(5)
print(result + " is ready for querying.")
mongodb_client.close()
스크립트를 실행하세요.
python create_vector_index.py
터미널에 다음과 같은 출력이 표시됩니다.
New search index named vector_index is building.
Polling to check if the index is ready. This may take up to a minute.
vector_index is ready for querying.
인덱스 빌드는 최대 1분 정도 걸립니다. 폴링 루프는 5초마다 확인하며, MongoDB가 인덱스가 쿼리 가능한 상태임을 확인할 때에만 종료합니다. "vector_index is ready for querying." 메시지를 보기 전에는 다음 단계로 넘어가지 마세요.
검색 쿼리를 벡터로 변환
generate_query_vector.py라는 파일을 만드세요. 이 파일은 embedding_utils.py의 get_query_embedding() 함수를 가져옵니다. generate_query_vector.py를 실행하여 임베딩 모델이 올바르게 로드되고 768차원 벡터를 생성하는지 확인하세요.
get_query_embedding()함수는 다음 단계에서 임포트합니다.- 모델이 제대로 동작하는지 단독으로 실행해 확인할 수 있습니다.
여기에서도 첫 단계와 동일한 모델을 사용해야 합니다. 다른 모델은 다른 수치 공간의 벡터를 생성하여 비교가 무의미해집니다.
from embedding_utils import get_query_embedding
user_query = "I need an automated, scalable system for serious information storage"
query_vector = get_query_embedding(user_query)
print(f"Query: '{user_query}'")
print(f"Vector dimensions: {len(query_vector)}")
print(f"First 5 values: {query_vector[:5]}")
스크립트를 실행하세요.
python generate_query_vector.py
다음과 유사한 출력이 표시됩니다.
<All keys matched successfully>
Query: 'I need an automated, scalable system for serious information storage'
Vector dimensions: 768
First 5 values: [0.0025914530269801617, 0.09862980246543884, -0.023092379793524742, -0.0171672236174345, -0.05548065900802612]
위 출력에서 Vector dimensions: 768은 모델 출력이 저장된 임베딩과 일치함을 확인해 줍니다.
$vectorSearch 쿼리 실행
run_vector_search.py라는 파일을 만드세요. 이 파일은 embedding_utils.py의 get_query_embedding() 함수를 임포트하여 사용자의 검색어를 벡터로 변환합니다. run_vector_search.py는 $vectorSearch 집계 파이프라인을 실행하고, 시맨틱 유사도 순으로 정렬된 결과를 출력합니다.
from embedding_utils import get_query_embedding
from pymongo import MongoClient
# Replace the placeholder with your Atlas connection string:
mongodb_client = MongoClient(
"mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/",
appname="devrel-tutorial-python-semantic-search"
)
collection = mongodb_client["sample_db"]["documents"]
# Generate a query vector from the user's search input:
user_query = "I need an automated, scalable system for serious information storage"
query_vector = get_query_embedding(user_query)
# Define the $vectorSearch aggregation pipeline:
pipeline = [
{
"$vectorSearch": {
"index": "vector_index", # The index created in create_vector_index.py
"path": "embedding", # The field that holds your stored vectors
"queryVector": query_vector, # The vector generated from the user's query
"numCandidates": 150, # How many neighbors MongoDB considers
"limit": 3 # How many results to return
}
},
{
"$project": {
"_id": 0,
"title": 1,
"text": 1,
"score": {
"$meta": "vectorSearchScore" # Relevance score for each result
}
}
}
]
results = collection.aggregate(pipeline)
print(f"\nTop results for query: '{user_query}'\n")
for doc in results:
print(f"Title: {doc['title']}")
print(f"Text: {doc['text']}")
print(f"Score: {doc['score']:.4f}")
print()
mongodb_client.close()
위 코드에서 두 매개변수가 검색 동작을 제어합니다.
numCandidates는 최종 결과로 좁히기 전에 MongoDB가 초기 검색 풀에서 검사하는 개수입니다. 값을 크게 하면 재현율이 개선되지만 약간 더 오래 걸립니다.limit은 반환할 결과 수를 설정합니다. 이 튜토리얼에서는limit을 3으로, numCandidates를 150으로 설정합니다. 일반적인 시작점은numCandidates를 limit의 10~15배로 두는 것입니다.
이제 스크립트를 실행하세요.
python run_vector_search.py
다음과 유사한 출력이 표시됩니다.
<All keys matched successfully>
Top results for query: 'I need an automated, scalable system for serious information storage.'
Title: MongoDB Atlas
Text: MongoDB Atlas is a fully managed cloud database.
Score: 0.7211
Title: Nomic AI
Text: nomic-embed-text-v1 is a free, open-source embedding model.
Score: 0.6818
Title: Vector Search
Text: Vector search finds results based on semantic meaning.
Score: 0.6642
쿼리 "I need an automated, scalable system for serious information storage"와 "MongoDB Atlas is a fully managed cloud database."는 단어를 공유하지 않지만, "MongoDB Atlas"가 가장 높은 점수를 받습니다. 두 문장의 벡터가 의미상 가깝기 때문입니다. "...정보 저장을 위한 자동화되고 확장 가능한 시스템"은 의미적으로 "...완전 관리형 클라우드 데이터베이스"에 매핑됩니다. 동일한 쿼리로 텍스트 검색을 하면 어떤 문서에도 정확한 쿼리 단어가 없으므로 결과가 0건이 됩니다. 이것이 의도한 대로 동작하는 시맨틱 검색입니다.
핵심 정리
- MongoDB Vector Search는 M0 무료 티어 클러스터에서 실행됩니다. 유료 플랜이 필요하지 않습니다.
- 저장용 임베딩 생성과 쿼리 벡터 생성 모두에 동일한 임베딩 모델을 사용해야 합니다. 모델을 섞어 사용하면 결과가 무의미해집니다.
numDimensions는 인덱스 정의에서 임베딩 모델의 출력 크기와 정확히 일치해야 합니다.nomic-embed-text-v1은 항상 768차원을 생성합니다.input_type="document"는 저장에 최적화된 임베딩을,input_type="query"는 검색에 최적화된 임베딩을 생성합니다. 각 단계에서 올바른 타입을 사용하세요.numCandidates는 MongoDB가 최종limit결과로 좁히기 전에 탐색하는 범위를 제어합니다. 값을 크게 하면 재현율이 향상되지만 쿼리 시간이 늘어납니다.vectorSearchScore는 시맨틱 유사도로 결과를 랭킹합니다. 결과에는 쿼리의 정확한 단어가 포함될 필요가 없습니다. 점수 범위는 모델과 데이터셋에 따라 다르지만,nomic-embed-text-v1의 코사인 유사도 기준으로 다음 경계값이 유용한 시작점입니다.- 0.9 이상: 의미가 거의 동일합니다. 문서와 쿼리가 시맨틱하게 거의 같습니다.
- 0.7~0.9: 높은 관련성. 문서가 쿼리 의도와 명확히 관련됩니다.
- 0.5~0.7: 보통 관련성. 주제는 같지만 프레이밍이나 문맥이 다릅니다.
- 0.5 미만: 낮은 관련성. 연관성이 약해 유용하지 않을 수 있습니다.
이 튜토리얼의 모든 코드 샘플은 GitHub 저장소에서 확인할 수 있습니다.
추가로 읽기
- MongoDB Vector Search 개요는 필터링과 양자화 등 MongoDB Vector Search의 전체 기능을 다룹니다.
- 하이브리드 검색 수행 방법은 하나의 쿼리에서 벡터 검색과 전체 텍스트 검색을 결합하는 방법을 보여줍니다.
- 벡터 임베딩 생성 방법은 Voyage AI, OpenAI 및 기타 오픈 소스 모델 제공자의 임베딩 모델을 사용하여 컬렉션의 텍스트 데이터에 대한 벡터 임베딩을 생성하는 방법을 설명합니다.
- MongoDB로 RAG(Retrieval-Augmented Generation)는 검색 증강 생성 애플리케이션에서 검색 계층으로 시맨틱 검색을 사용하는 방법을 보여줍니다.
FAQs
시맨틱 검색을 구현하려면 유료 Atlas 플랜이 필요한가요?
아니요. 이 튜토리얼의 네 단계 모두 무료인 M0 무료 티어 클러스터에서 실행됩니다.
저장된 문서와 다른 임베딩 모델로 쿼리를 생성하면 어떻게 되나요?
결과가 무의미해집니다. 서로 다른 모델은 텍스트를 다른 수치 공간에 매핑하므로 벡터를 비교할 수 없습니다. 인덱싱과 쿼리 모두에 항상 동일한 모델을 사용하세요.
`numCandidates`와 `limit`의 차이는 무엇인가요?
numCandidates는 검색 중에 MongoDB가 검사하는 벡터 수이고, limit는 상위 결과 중 반환하는 개수입니다. numCandidates 값을 높이면 쿼리가 약간 느려지는 대가로 결과 품질이 향상됩니다. 일반적인 시작점은 numCandidates를 limit의 10~15배로 설정하는 것입니다.
시맨틱 검색과 텍스트 검색을 함께 사용할 수 있나요?
예. MongoDB는 하나의 파이프라인에서 $vectorSearch와 $search를 결합하는 하이브리드 검색을 지원합니다.
영어 이외의 언어에서도 시맨틱 검색이 작동하나요?
임베딩 모델에 따라 다릅니다. nomic-embed-text-v1은 주로 영어 텍스트로 학습되었습니다. 다국어 사용 사례의 경우 데이터에 포함된 언어로 학습된 다국어 임베딩 모델을 선택하세요.