NumPy로 의미 검색하기
NumPy를 사용해 질의 임베딩과 문서 임베딩 모음 사이의 코사인 유사도를 계산하는 순수 Python 의미 검색 시스템을 구축합니다.
NumPy로 의미 검색하기은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Engineering Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
데이터베이스 없이 의미 기반 검색하기
의미 기반 검색은 키워드의 겹침이 아니라 의미를 기준으로 질의와 가장 관련성이 높은 문서를 찾습니다. 가장 간단한 구현에서는 NumPy를 사용하여 메모리에 있는 질의 임베딩과 모든 문서 임베딩 사이의 코사인 유사도를 계산하므로 외부 데이터베이스가 필요하지 않습니다.
이 방식은 수만 개까지의 문서에 적합하며, 벡터 데이터베이스에 투자하기 전에 프로토타입을 만들 때 특히 유용합니다.
문서 코퍼스 구축하기
먼저 문서를 수집하고 OpenAI API를 사용하여 문서마다 하나의 임베딩을 생성하십시오. 임베딩은 각 행이 하나의 문서 벡터를 나타내는 2차원 NumPy 배열로 저장하십시오. 가장 일치하는 항목을 찾은 뒤 원본 내용을 가져올 수 있도록 문서 텍스트의 목록도 별도로 유지하십시오.
import numpy as np
from openai import OpenAI
client = OpenAI()
documents = [
'Python is a high-level programming language.',
'NumPy provides fast numerical computing for Python.',
'Embeddings represent text as dense vectors.',
'Cosine similarity measures angle between vectors.',
'RAG combines retrieval with language generation.'
]
response = client.embeddings.create(
model='text-embedding-3-small',
input=documents
)
corpus_embeddings = np.array([item.embedding for item in response.data])
print(f'Corpus shape: {corpus_embeddings.shape}') # (5, 1536)사용자 질의 임베딩하기
사용자가 검색 질의를 제출하면 문서를 임베딩할 때 사용한 동일한 모델로 질의를 임베딩하십시오. 예를 들어 문서에는 text-embedding-3-small을 사용하고 질의에는 text-embedding-3-large를 사용하는 것처럼 모델을 섞으면 서로 다른 공간의 벡터가 생성되어 의미 없는 유사도 점수가 나옵니다.
from openai import OpenAI
import numpy as np
client = OpenAI()
query = 'How do I compute similarity between text?'
response = client.embeddings.create(
model='text-embedding-3-small', # must match corpus model
input=query
)
query_embedding = np.array(response.data[0].embedding)
print(f'Query vector shape: {query_embedding.shape}') # (1536,)NumPy로 코사인 유사도 계산하기
한 번의 연산으로 질의와 모든 문서 사이의 유사도를 찾으려면 질의 벡터와 문서 벡터 행렬의 내적을 계산하십시오. OpenAI 임베딩은 모두 L2 정규화되어 있으므로, 이 값은 모든 문서에 대한 코사인 유사도와 같습니다. 시간 복잡도는 O(n * d)이며, n은 문서 수이고 d는 차원입니다.
import numpy as np
# corpus_embeddings: (n_docs, 1536)
# query_embedding: (1536,)
def semantic_search(query_vec, corpus_vecs):
# Matrix-vector dot product: shape (n_docs,)
similarities = corpus_vecs @ query_vec
return similarities
# Example call (assuming pre-computed embeddings)
# sims = semantic_search(query_embedding, corpus_embeddings)
# print(sims) # array of similarity scores, one per document상위 K개 결과 순위 지정 및 가져오기
np.argsort를 사용하여 유사도 점수가 높은 순서로 문서 순위를 정한 다음 상위 k개의 인덱스를 잘라 내십시오. 이렇게 하면 가장 관련성이 높은 문서의 인덱스를 얻을 수 있으며, 이 인덱스로 별도로 유지한 목록에서 원본 텍스트를 조회할 수 있습니다.
import numpy as np
def get_top_k(query_vec, corpus_vecs, documents, k=3):
similarities = corpus_vecs @ query_vec
# argsort gives ascending order; [::-1] reverses to descending
ranked_indices = np.argsort(similarities)[::-1]
top_k_indices = ranked_indices[:k]
return [
{'text': documents[i], 'score': float(similarities[i])}
for i in top_k_indices
]
# results = get_top_k(query_embedding, corpus_embeddings, documents, k=3)
# for r in results:
# print(f'{r["score"]:.4f}: {r["text"]}')의미 기반 검색 전체 예제
모든 과정을 하나로 합치면 코퍼스를 임베딩하고, 질의를 임베딩하고, 유사도를 계산한 다음 순위가 매겨진 결과를 반환하게 됩니다. 벡터 데이터베이스가 내부적으로 NumPy를 대체하는 경우에도 이 완전한 패턴은 모든 RAG 검색 단계의 핵심입니다.
import numpy as np
from openai import OpenAI
client = OpenAI()
docs = [
'Embeddings map text to numerical vectors.',
'Python lists store ordered collections.',
'Cosine similarity compares vector directions.',
'RAG retrieves documents to ground LLM answers.',
'Dictionaries store key-value pairs in Python.'
]
corpus_resp = client.embeddings.create(model='text-embedding-3-small', input=docs)
corpus = np.array([d.embedding for d in corpus_resp.data])
query = 'finding similar text using angles'
q_resp = client.embeddings.create(model='text-embedding-3-small', input=query)
q_vec = np.array(q_resp.data[0].embedding)
scores = corpus @ q_vec
for i in np.argsort(scores)[::-1][:3]:
print(f'{scores[i]:.3f}: {docs[i]}')점수 임계값 설정
상위 k개의 결과가 모두 실제로 관련성이 높은 것은 아닙니다. 가장 일치하는 결과조차 의미적으로 적합하지 않은 경우가 있습니다. 점수 임계값을 추가하여 유사도가 낮은 결과를 걸러 내십시오. 코사인 유사도의 일반적인 임계값은 0.70~0.80이지만, 실제 질의를 사용하여 특정 도메인에 맞게 조정해야 합니다.
import numpy as np
def search_with_threshold(query_vec, corpus_vecs, documents, k=5, threshold=0.75):
similarities = corpus_vecs @ query_vec
ranked = np.argsort(similarities)[::-1][:k]
results = []
for i in ranked:
if similarities[i] >= threshold:
results.append({'text': documents[i], 'score': float(similarities[i])})
return results
# Only returns documents above the minimum similarity thresholdNumPy 검색의 성능 특성
NumPy 유사도 검색의 질의당 시간 복잡도는 O(n * d)이며, n은 문서 수이고 d는 임베딩 차원입니다. 1536차원 임베딩을 기준으로 하면 다음과 같습니다.
- 문서 10,000개: 최신 CPU에서 질의당 약 5ms
- 문서 100,000개: 질의당 약 50ms
- 문서 1,000,000개: 약 500ms — 너무 느리므로 벡터 데이터베이스로 전환해야 함
NumPy는 프로토타이핑에 매우 적합하지만, 근사 검색, 필터링 또는 기본 제공 영속성을 지원하지 않습니다.
임베딩을 디스크에 저장하기
실행할 때마다 임베딩을 다시 계산하면 API 호출과 비용이 낭비됩니다. 코퍼스가 변경된 경우에만 다시 임베딩할 수 있도록 코퍼스 임베딩과 문서 텍스트를 디스크에 저장하십시오.
np.save는 임베딩 행렬을 효율적으로 저장하며, 문서 목록은 JSON으로 저장할 수 있습니다. 시작할 때 API를 호출하는 대신 두 파일을 모두 불러오십시오.
import numpy as np
import json
# Save
np.save('/tmp/corpus_embeddings.npy', corpus_embeddings)
with open('/tmp/corpus_docs.json', 'w') as f:
json.dump(documents, f)
# Load
corpus_embeddings = np.load('/tmp/corpus_embeddings.npy')
with open('/tmp/corpus_docs.json') as f:
documents = json.load(f)
print(f'Loaded {len(documents)} docs, shape {corpus_embeddings.shape}')새 문서를 점진적으로 처리하기
새 문서가 추가되더라도 전체 코퍼스를 다시 임베딩할 필요는 없습니다. 새 문서만 임베딩하고 np.vstack를 사용하여 기존 행렬에 해당 벡터를 추가하십시오. 새 텍스트도 같은 순서로 문서 목록에 추가해야 합니다.
import numpy as np
from openai import OpenAI
client = OpenAI()
# Assume these exist from a previous session:
# corpus_embeddings: (n, 1536)
# documents: list of strings
new_docs = ['New document about vector search.']
resp = client.embeddings.create(model='text-embedding-3-small', input=new_docs)
new_vecs = np.array([item.embedding for item in resp.data])
corpus_embeddings = np.vstack([corpus_embeddings, new_vecs])
documents.extend(new_docs)
print(f'Corpus now has {len(documents)} documents')메모리 내 검색의 한계
NumPy 의미 기반 검색에는 전용 벡터 데이터베이스와 비교했을 때 다음과 같은 중요한 한계가 있습니다.
- 영속성 없음 — 모든 데이터가 RAM에만 존재하며 다시 시작하면 사라짐
- 메타데이터 필터링 없음 — 날짜, 카테고리 또는 작성자로 결과를 필터링할 수 없음
- 선형 스캔만 지원 — 근사 최근접 이웃 색인이 없음
- 동시 접근 불가 — 여러 사용자가 이용하는 프로덕션 배포에 적합하지 않음
이러한 한계 때문에 프로덕션 RAG 시스템에서는 전용 벡터 데이터베이스를 사용하게 됩니다.
빠른 확인
이 레슨에서 배운 AI 엔지니어링 개념을 제대로 이해했는지 확인해 보십시오.
레슨 요약
이 레슨에서는 다음을 배웠습니다. L2 정규화된 임베딩에 대한 행렬 내적을 사용하면 한 번의 연산으로 전체 코퍼스의 코사인 유사도를 계산할 수 있습니다. np.argsort를 역순으로 사용하면 가장 유사한 상위 k개 문서를 가져올 수 있습니다. 또한 NumPy 검색은 프로토타입에 적합하지만 영속성과 메타데이터 필터링을 지원하지 않습니다. 다음으로 클러스터링과 UMAP을 사용하여 임베딩 모음에서 주제 구조를 찾아봅니다.
자주 묻는 질문
“NumPy로 의미 검색하기” 강의는 무료인가요?
네 — “NumPy로 의미 검색하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“NumPy로 의미 검색하기”에서 뭘 배우나요?
NumPy를 사용해 질의 임베딩과 문서 임베딩 모음 사이의 코사인 유사도를 계산하는 순수 Python 의미 검색 시스템을 구축합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“NumPy로 의미 검색하기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 벡터 임베딩이란 무엇인가
- OpenAI로 임베딩 생성하기
- NumPy로 의미 검색하기
- 임베딩 군집화 및 시각화