이미지 로딩 중...
AI Generated
2025. 11. 17. · 7 Views
OpenAI Embeddings API 완벽 가이드
OpenAI Embeddings API를 활용하여 텍스트를 벡터로 변환하고, 유사도 검색과 추천 시스템을 구축하는 방법을 초급자도 쉽게 이해할 수 있도록 설명합니다. 실무에서 바로 활용 가능한 코드 예제와 팁을 제공합니다.
목차
- OpenAI Embeddings API 기본 사용법
- 텍스트 유사도 계산하기
- 배치 처리로 비용 절감하기
- 벡터 데이터베이스에 저장하기
- 의미 기반 검색 시스템 구축
- RAG 시스템 구축하기
- 중복 탐지 시스템 만들기
- 다국어 검색 지원하기
- 임베딩 캐싱으로 비용 절감하기
- 개인화 추천 시스템 구축하기
1. OpenAI Embeddings API 기본 사용법
시작하며
여러분이 사용자의 검색어로 관련된 문서를 찾고 싶을 때, 단순히 키워드만 매칭하면 제대로 된 결과를 찾기 어려운 경험을 해보셨나요? 예를 들어 사용자가 "강아지 키우는 방법"을 검색했는데, 실제로는 "반려견 사육 가이드"라는 제목의 문서가 가장 적합한 결과인 경우가 많습니다.
이런 문제는 실제 서비스 개발에서 자주 발생합니다. 전통적인 키워드 검색은 단어가 정확히 일치해야만 결과를 찾을 수 있기 때문에, 같은 의미라도 다른 표현으로 작성된 내용은 찾지 못합니다.
이로 인해 사용자 경험이 크게 떨어지고, 좋은 콘텐츠가 있어도 발견되지 않는 문제가 생깁니다. 바로 이럴 때 필요한 것이 OpenAI Embeddings API입니다.
이 API는 텍스트의 '의미'를 숫자 벡터로 변환해서, 표현은 다르지만 의미가 비슷한 텍스트들을 찾아낼 수 있게 해줍니다.
개요
간단히 말해서, Embeddings API는 텍스트를 컴퓨터가 이해할 수 있는 숫자 배열(벡터)로 바꿔주는 도구입니다. 마치 단어를 좌표로 표현하는 것처럼, 비슷한 의미의 단어들은 비슷한 위치에 놓이게 됩니다.
왜 이 API가 필요할까요? 검색 엔진, 추천 시스템, 챗봇 등을 만들 때 텍스트의 의미를 정확히 파악해야 합니다.
예를 들어, 고객 지원 챗봇을 만든다면 "비밀번호를 까먹었어요"와 "로그인이 안 돼요"가 비슷한 문제라는 것을 알아야 적절한 답변을 줄 수 있습니다. 전통적인 방법에서는 모든 유사한 표현을 일일이 등록해야 했다면, 이제는 Embeddings API를 사용하면 의미적으로 유사한 텍스트를 자동으로 찾을 수 있습니다.
이 API의 핵심 특징은 첫째, 다양한 언어를 지원하고, 둘째, 최대 8,191개의 토큰까지 처리 가능하며, 셋째, 1536차원의 벡터로 매우 정밀하게 의미를 표현한다는 점입니다. 이러한 특징들이 실무에서 높은 정확도의 검색과 추천을 가능하게 만듭니다.
코드 예제
from openai import OpenAI
# OpenAI 클라이언트 초기화
client = OpenAI(api_key="your-api-key-here")
# 텍스트를 벡터로 변환
def get_embedding(text):
# text-embedding-3-small 모델 사용 (비용 효율적)
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
# 벡터 데이터 반환 (1536차원 배열)
return response.data[0].embedding
# 실제 사용 예시
text = "강아지를 처음 키우는 방법"
embedding = get_embedding(text)
print(f"벡터 차원: {len(embedding)}") # 출력: 1536
print(f"처음 5개 값: {embedding[:5]}") # 벡터의 일부 확인
설명
이것이 하는 일: 이 코드는 OpenAI의 Embeddings API를 사용하여 한국어 텍스트를 벡터로 변환하는 가장 기본적인 방법을 보여줍니다. 텍스트를 입력하면 그 의미를 담은 1536개의 숫자 배열이 반환됩니다.
첫 번째로, OpenAI 클라이언트를 초기화하는 부분에서는 여러분의 API 키를 설정합니다. 이 키는 OpenAI 대시보드에서 발급받을 수 있으며, 보안상 환경변수나 비밀 관리 도구에 저장하는 것이 좋습니다.
절대 코드에 직접 하드코딩하지 마세요. 그 다음으로, get_embedding 함수가 실행되면서 실제 API 호출이 일어납니다.
여기서 text-embedding-3-small 모델을 사용하는데, 이는 가장 최신 모델로 기존 모델보다 성능이 좋으면서도 비용이 저렴합니다. API는 입력된 텍스트를 분석하여 의미를 파악하고, 이를 1536개의 실수 값으로 구성된 벡터로 변환합니다.
마지막으로, 반환된 response 객체에서 data[0].embedding을 추출하여 실제 벡터 배열을 얻습니다. 이 벡터는 리스트 형태이며, 각 값은 -1에서 1 사이의 실수입니다.
예를 들어 "강아지"와 "개"라는 단어는 의미가 비슷하므로 벡터 공간에서 가까운 위치에 배치됩니다. 여러분이 이 코드를 사용하면 어떤 텍스트든 숫자로 표현할 수 있어서, 나중에 수학적 계산(코사인 유사도 등)으로 텍스트 간 유사도를 측정할 수 있습니다.
실무에서는 이를 활용하여 검색 시스템, 문서 분류, 중복 탐지, 추천 엔진 등을 구축할 수 있습니다.
실전 팁
💡 API 키는 절대 코드에 직접 작성하지 말고, os.getenv("OPENAI_API_KEY")처럼 환경변수로 관리하세요. GitHub에 실수로 올리면 요금 폭탄을 맞을 수 있습니다.
💡 text-embedding-3-small 모델이 대부분의 경우 충분하지만, 더 높은 정확도가 필요하면 text-embedding-3-large 모델을 사용하세요. 다만 비용이 약 5배 더 비쌉니다.
💡 한 번에 여러 텍스트를 처리할 때는 input에 리스트를 전달하면 배치 처리가 가능해서 API 호출 횟수를 줄이고 비용을 절약할 수 있습니다.
💡 API 호출 실패에 대비해 try-except 블록으로 에러 처리를 추가하고, 네트워크 오류 시 재시도 로직을 구현하세요. 프로덕션 환경에서는 필수입니다.
💡 생성된 벡터는 재사용이 가능하므로 데이터베이스나 캐시에 저장해두고 사용하세요. 같은 텍스트를 반복해서 API로 변환하면 불필요한 비용이 발생합니다.
2. 텍스트 유사도 계산하기
시작하며
여러분이 수천 개의 고객 문의 중에서 현재 문의와 가장 비슷한 이전 사례를 찾고 싶을 때, 어떻게 하시겠습니까? 일일이 사람이 읽어보는 것은 불가능하고, 단순히 키워드만 비교하면 정확도가 떨어집니다.
이런 문제는 고객 지원, 문서 검색, 표절 탐지 등 다양한 분야에서 발생합니다. 두 텍스트가 얼마나 비슷한지 정확하게 측정하지 못하면, 사용자에게 엉뚱한 결과를 제공하게 되고 서비스 품질이 크게 떨어집니다.
바로 이럴 때 필요한 것이 코사인 유사도 계산입니다. Embeddings 벡터를 활용하면 두 텍스트가 의미적으로 얼마나 가까운지 0에서 1 사이의 숫자로 명확하게 알 수 있습니다.
개요
간단히 말해서, 코사인 유사도는 두 벡터 사이의 각도를 측정하여 텍스트가 얼마나 비슷한지 계산하는 방법입니다. 값이 1에 가까울수록 매우 비슷하고, 0에 가까울수록 관련이 없다는 의미입니다.
왜 이 방법이 필요할까요? 단순히 단어가 몇 개 겹치는지 세는 것보다 훨씬 정확하기 때문입니다.
예를 들어, "자동차 수리 비용"과 "차량 정비 가격"은 겹치는 단어가 하나도 없지만 의미는 거의 같습니다. 코사인 유사도를 사용하면 이런 경우도 정확하게 비슷하다고 판단할 수 있습니다.
기존에는 편집 거리(Levenshtein Distance)나 Jaccard 유사도 같은 방법을 사용했다면, 이제는 의미 기반 유사도로 훨씬 더 정확한 결과를 얻을 수 있습니다. 이 방법의 핵심 특징은 첫째, 텍스트 길이에 영향을 받지 않고, 둘째, 계산이 빠르며, 셋째, 해석이 직관적(0~1 사이 값)이라는 점입니다.
이러한 특징들이 실시간 검색 시스템에서도 빠르게 유사도를 계산할 수 있게 해줍니다.
코드 예제
import numpy as np
from openai import OpenAI
client = OpenAI(api_key="your-api-key-here")
def get_embedding(text):
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
# 코사인 유사도 계산 함수
def cosine_similarity(vec1, vec2):
# numpy 배열로 변환
vec1 = np.array(vec1)
vec2 = np.array(vec2)
# 내적 / (크기1 * 크기2)
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
# 실제 사용 예시
text1 = "강아지 훈련 방법"
text2 = "반려견 교육 가이드"
text3 = "자동차 수리"
emb1 = get_embedding(text1)
emb2 = get_embedding(text2)
emb3 = get_embedding(text3)
print(f"유사도 (강아지 vs 반려견): {cosine_similarity(emb1, emb2):.4f}") # 높은 값
print(f"유사도 (강아지 vs 자동차): {cosine_similarity(emb1, emb3):.4f}") # 낮은 값
설명
이것이 하는 일: 이 코드는 두 텍스트의 의미적 유사도를 0에서 1 사이의 숫자로 계산합니다. 예를 들어 "강아지 훈련"과 "반려견 교육"은 0.9 이상의 높은 유사도를 보이지만, "강아지 훈련"과 "자동차 수리"는 0.3 이하의 낮은 값을 보입니다.
첫 번째로, 각 텍스트를 OpenAI API로 벡터로 변환합니다. 여기서 중요한 점은 같은 모델(text-embedding-3-small)을 사용해야 한다는 것입니다.
다른 모델로 만든 벡터끼리는 비교가 불가능합니다. 그 다음으로, cosine_similarity 함수가 실행되면서 실제 유사도를 계산합니다.
내부적으로는 두 벡터의 내적(dot product)을 계산하고, 각 벡터의 크기(norm)로 나눕니다. 이 수학적 계산이 두 벡터 사이의 각도를 코사인 값으로 변환해줍니다.
세 번째 단계에서는 NumPy 라이브러리를 사용하여 계산 속도를 높입니다. np.dot은 벡터 내적을, np.linalg.norm은 벡터 크기를 매우 빠르게 계산해줍니다.
순수 Python으로 구현하면 1536개의 값을 하나씩 처리해야 하지만, NumPy는 한 번에 처리합니다. 마지막으로, 계산된 유사도 값을 확인해보면 "강아지 훈련"과 "반려견 교육"은 0.850.95 정도의 높은 값이 나오지만, "강아지 훈련"과 "자동차 수리"는 0.10.3 정도의 낮은 값이 나옵니다.
일반적으로 0.8 이상이면 매우 유사, 0.5~0.8이면 관련 있음, 0.5 이하면 관련 없음으로 판단합니다. 여러분이 이 코드를 사용하면 검색 엔진에서 사용자 쿼리와 가장 관련 있는 문서를 찾거나, 중복 질문을 자동으로 탐지하거나, 비슷한 상품을 추천하는 시스템을 쉽게 만들 수 있습니다.
실무에서는 수만 개의 문서 중에서 가장 유사한 상위 10개를 찾는 등의 작업에 활용됩니다.
실전 팁
💡 대량의 벡터를 비교할 때는 NumPy의 행렬 연산을 활용하면 한 번에 수천 개의 유사도를 계산할 수 있어 속도가 100배 이상 빨라집니다.
💡 실무에서는 보통 0.8 이상을 "매우 유사", 0.5~0.8을 "관련 있음"으로 설정하지만, 여러분의 데이터로 실험해서 최적의 임계값을 찾으세요.
💡 유사도 계산 전에 벡터를 정규화(normalize)하면 계산이 더 빠르고 정확합니다. sklearn의 normalize 함수를 사용하세요.
💡 수만 개 이상의 벡터를 검색할 때는 FAISS나 Pinecone 같은 벡터 데이터베이스를 사용하면 검색 속도가 획기적으로 빨라집니다.
💡 유사도가 너무 낮게 나온다면 텍스트 전처리(불용어 제거, 정규화)를 해보세요. 특히 특수문자나 이모지가 많으면 유사도가 왜곡될 수 있습니다.
3. 배치 처리로 비용 절감하기
시작하며
여러분이 블로그 포스트 1000개를 벡터로 변환해야 한다고 가정해봅시다. 하나씩 API를 호출하면 1000번의 요청이 발생하고, 시간도 오래 걸리며, API 제한에도 걸릴 수 있습니다.
이런 문제는 대량의 데이터를 처리할 때 항상 발생합니다. API 호출 횟수가 많아지면 네트워크 오버헤드가 커지고, 비용도 증가하며, 처리 시간도 길어집니다.
또한 OpenAI API는 분당 요청 수 제한(Rate Limit)이 있어서 너무 빠르게 호출하면 에러가 발생합니다. 바로 이럴 때 필요한 것이 배치 처리입니다.
여러 텍스트를 한 번에 묶어서 API를 호출하면 속도는 10배 빠르고, 비용은 동일하며, Rate Limit 걱정도 줄어듭니다.
개요
간단히 말해서, 배치 처리는 여러 개의 텍스트를 리스트로 만들어 한 번의 API 호출로 모두 처리하는 방법입니다. 마치 택배를 하나씩 보내는 것보다 한 박스에 담아서 보내는 것이 효율적인 것과 같은 원리입니다.
왜 배치 처리가 필요할까요? OpenAI API는 한 번의 호출로 최대 2048개의 텍스트를 동시에 처리할 수 있습니다.
예를 들어, 1000개의 상품 설명을 벡터화한다면 개별 호출 시 약 10분이 걸리지만, 배치로 처리하면 1분 안에 끝낼 수 있습니다. 전통적인 방법에서는 반복문으로 하나씩 API를 호출했다면, 이제는 리스트로 한 번에 전달하여 훨씬 효율적으로 처리할 수 있습니다.
배치 처리의 핵심 특징은 첫째, API 호출 횟수를 최소화하고, 둘째, 네트워크 오버헤드를 줄이며, 셋째, Rate Limit 제한을 피할 수 있다는 점입니다. 이러한 특징들이 대규모 데이터 처리를 가능하게 만들고, 실무에서 비용을 크게 절감시켜줍니다.
코드 예제
from openai import OpenAI
import time
client = OpenAI(api_key="your-api-key-here")
# 배치로 여러 텍스트 임베딩 생성
def get_embeddings_batch(texts):
# 최대 2048개까지 한 번에 처리 가능
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts # 리스트로 전달
)
# 각 텍스트의 벡터를 순서대로 반환
return [item.embedding for item in response.data]
# 대량 데이터 처리 예시
documents = [
"강아지 훈련 가이드",
"고양이 사료 추천",
"반려동물 건강 관리",
"애완견 미용 팁",
# ... 수백 개의 문서
]
# 한 번에 100개씩 처리 (안정적인 배치 크기)
batch_size = 100
all_embeddings = []
for i in range(0, len(documents), batch_size):
batch = documents[i:i+batch_size]
embeddings = get_embeddings_batch(batch)
all_embeddings.extend(embeddings)
print(f"처리 완료: {i+len(batch)}/{len(documents)}")
time.sleep(1) # Rate Limit 방지용 대기
print(f"총 {len(all_embeddings)}개 벡터 생성 완료")
설명
이것이 하는 일: 이 코드는 수백, 수천 개의 텍스트를 효율적으로 벡터로 변환하는 방법을 보여줍니다. 개별 호출 대신 배치로 처리하여 시간과 비용을 크게 절감합니다.
첫 번째로, get_embeddings_batch 함수는 텍스트 리스트를 받아서 한 번의 API 호출로 모든 벡터를 생성합니다. 여기서 중요한 점은 input 파라미터에 문자열이 아닌 리스트를 전달한다는 것입니다.
API는 자동으로 각 텍스트를 처리하고 결과를 순서대로 반환해줍니다. 그 다음으로, 실제 대량 데이터를 처리할 때는 적절한 배치 크기로 나누어 처리합니다.
이론적으로는 2048개까지 가능하지만, 실무에서는 100~200개 정도가 안정적입니다. 너무 큰 배치는 timeout 에러를 발생시킬 수 있고, 너무 작은 배치는 효율이 떨어집니다.
세 번째 단계에서는 반복문으로 전체 문서를 배치 단위로 나누어 처리합니다. range(0, len(documents), batch_size)는 0부터 시작해서 배치 크기만큼 건너뛰면서 인덱스를 생성합니다.
예를 들어 배치 크기가 100이면 0, 100, 200, 300... 순서로 처리합니다.
마지막으로, 각 배치 처리 후 1초씩 대기하는 것이 중요합니다. OpenAI API는 분당 요청 수 제한이 있어서 너무 빠르게 호출하면 429 에러(Too Many Requests)가 발생합니다.
time.sleep(1)로 적절한 간격을 두면 이런 문제를 방지할 수 있습니다. 여러분이 이 코드를 사용하면 1000개의 문서를 10분이 아닌 1~2분 안에 처리할 수 있습니다.
실무에서는 전체 블로그 포스트, 상품 카탈로그, 고객 리뷰 등 대량의 텍스트를 벡터화할 때 필수적으로 사용하는 패턴입니다. 또한 API 비용도 호출 횟수가 아닌 토큰 수로 책정되므로, 배치로 처리해도 비용은 동일합니다.
실전 팁
💡 배치 크기는 100~200개가 가장 안정적입니다. 2048개까지 가능하지만 네트워크 timeout이나 메모리 문제가 발생할 수 있습니다.
💡 Rate Limit 에러(429)가 발생하면 exponential backoff 방식으로 재시도하세요. 첫 실패 시 1초, 두 번째는 2초, 세 번째는 4초씩 대기 시간을 늘립니다.
💡 대량 처리 시 중간에 실패하면 처음부터 다시 해야 하므로, 진행 상황을 파일에 저장해두고 중단된 지점부터 재개할 수 있게 만드세요.
💡 OpenAI API는 분당 처리 토큰 수 제한도 있으므로, 매우 긴 텍스트가 많다면 배치 크기를 줄여야 합니다. 평균 텍스트 길이를 고려하세요.
💡 프로덕션 환경에서는 Celery나 RabbitMQ 같은 작업 큐를 사용하여 백그라운드에서 배치 처리하고, 프론트엔드에는 진행률을 보여주세요.
4. 벡터 데이터베이스에 저장하기
시작하며
여러분이 수만 개의 문서를 벡터로 변환했다면, 이제 이 벡터들을 어디에 저장하고 어떻게 빠르게 검색할지 고민하게 됩니다. 일반 데이터베이스에 저장하면 검색이 매우 느리고, 메모리에만 두면 서버가 재시작될 때 사라집니다.
이런 문제는 실제 서비스를 운영할 때 반드시 해결해야 합니다. 벡터는 일반 텍스트나 숫자와 달리 1536개의 실수 배열이므로 저장 공간도 많이 차지하고, 유사도 검색도 복잡한 계산이 필요합니다.
일반 PostgreSQL이나 MongoDB에 저장하면 검색 속도가 너무 느려서 실시간 서비스가 불가능합니다. 바로 이럴 때 필요한 것이 벡터 데이터베이스입니다.
Pinecone, Weaviate, Qdrant, ChromaDB 같은 전문 데이터베이스를 사용하면 수백만 개의 벡터 중에서도 밀리초 단위로 가장 유사한 결과를 찾을 수 있습니다.
개요
간단히 말해서, 벡터 데이터베이스는 벡터를 저장하고 빠르게 유사도 검색을 수행하도록 최적화된 특수한 데이터베이스입니다. 일반 데이터베이스가 정확한 값을 찾는 데 특화되었다면, 벡터 DB는 "비슷한" 값을 찾는 데 특화되어 있습니다.
왜 벡터 전용 데이터베이스가 필요할까요? 1만 개의 문서 중에서 쿼리와 가장 유사한 5개를 찾으려면 1만 번의 유사도 계산이 필요합니다.
일반 방법으로는 수 초가 걸리지만, 벡터 DB는 ANN(Approximate Nearest Neighbor) 알고리즘으로 0.01초 만에 찾아냅니다. 전통적인 방법에서는 모든 벡터와 일일이 비교했다면, 이제는 인덱싱 기술(HNSW, IVF 등)을 사용하여 비슷한 벡터가 있을 만한 영역만 검색합니다.
마치 도서관에서 모든 책을 뒤지는 대신 색인을 보는 것과 같습니다. 벡터 데이터베이스의 핵심 특징은 첫째, 수백만 개의 벡터도 빠르게 검색하고, 둘째, 자동으로 인덱스를 관리하며, 셋째, 필터링과 메타데이터 검색도 지원한다는 점입니다.
이러한 특징들이 실시간 검색 서비스, 추천 엔진, RAG 시스템 등을 가능하게 만듭니다.
코드 예제
from openai import OpenAI
import chromadb
from chromadb.config import Settings
client = OpenAI(api_key="your-api-key-here")
# ChromaDB 클라이언트 초기화 (로컬 저장)
chroma_client = chromadb.PersistentClient(path="./vector_db")
# 컬렉션 생성 (테이블과 유사)
collection = chroma_client.get_or_create_collection(
name="documents",
metadata={"description": "문서 임베딩 저장소"}
)
# 문서와 벡터 저장
def store_documents(documents):
embeddings = []
for doc in documents:
# OpenAI로 벡터 생성
emb = client.embeddings.create(
model="text-embedding-3-small",
input=doc
).data[0].embedding
embeddings.append(emb)
# ChromaDB에 저장
collection.add(
documents=documents, # 원본 텍스트
embeddings=embeddings, # 벡터
ids=[f"doc_{i}" for i in range(len(documents))], # 고유 ID
metadatas=[{"source": "blog"} for _ in documents] # 메타데이터
)
# 유사 문서 검색
def search_similar(query, top_k=5):
# 쿼리를 벡터로 변환
query_emb = client.embeddings.create(
model="text-embedding-3-small",
input=query
).data[0].embedding
# 유사도 검색
results = collection.query(
query_embeddings=[query_emb],
n_results=top_k
)
return results['documents'][0]
# 사용 예시
docs = ["강아지 훈련법", "고양이 사료", "반려동물 건강"]
store_documents(docs)
similar = search_similar("개 교육 방법")
print(f"유사 문서: {similar}")
설명
이것이 하는 일: 이 코드는 ChromaDB를 사용하여 문서와 벡터를 저장하고, 나중에 유사한 문서를 빠르게 검색하는 완전한 시스템을 구현합니다. 개발 환경에서 바로 실행 가능하며, 프로덕션으로 확장하기도 쉽습니다.
첫 번째로, ChromaDB 클라이언트를 초기화할 때 PersistentClient를 사용하면 데이터가 로컬 디스크에 저장됩니다. path="./vector_db"는 현재 디렉토리의 vector_db 폴더에 데이터를 저장한다는 의미입니다.
서버가 재시작되어도 데이터가 유지되므로 안심하고 사용할 수 있습니다. 그 다음으로, 컬렉션(collection)을 생성하는데 이는 일반 데이터베이스의 테이블과 비슷한 개념입니다.
get_or_create_collection을 사용하면 이미 존재하면 가져오고, 없으면 새로 만들어서 에러를 방지합니다. 여러 컬렉션을 만들어 문서, 상품, 이미지 등을 분리해서 관리할 수 있습니다.
세 번째 단계에서 store_documents 함수는 각 문서를 OpenAI로 벡터화한 후 ChromaDB에 저장합니다. 여기서 중요한 점은 원본 텍스트(documents), 벡터(embeddings), 고유 ID(ids), 메타데이터(metadatas)를 모두 함께 저장한다는 것입니다.
메타데이터를 활용하면 나중에 "source가 blog인 것만 검색" 같은 필터링이 가능합니다. 마지막으로, search_similar 함수는 사용자 쿼리를 벡터로 변환한 후 ChromaDB에서 가장 유사한 문서를 찾습니다.
collection.query는 내부적으로 HNSW 알고리즘을 사용하여 전체를 비교하지 않고도 빠르게 가장 가까운 벡터들을 찾아냅니다. n_results=5는 상위 5개만 반환하라는 의미입니다.
여러분이 이 코드를 사용하면 검색 엔진, 챗봇의 지식 베이스, 추천 시스템 등을 쉽게 만들 수 있습니다. 실무에서는 수십만 개의 문서를 저장하고, 사용자가 질문하면 관련 문서를 0.01초 안에 찾아서 GPT에게 컨텍스트로 제공하는 RAG(Retrieval Augmented Generation) 시스템에 활용됩니다.
실전 팁
💡 개발 단계에서는 ChromaDB가 간편하지만, 프로덕션에서는 Pinecone(클라우드), Qdrant(오픈소스), Weaviate 등도 고려하세요. 각각 장단점이 다릅니다.
💡 메타데이터를 적극 활용하세요. 날짜, 카테고리, 작성자 등을 저장하면 "2024년 1월 이후의 Python 관련 문서만 검색" 같은 복잡한 쿼리가 가능합니다.
💡 벡터 차원을 줄여서 저장 공간과 검색 속도를 개선할 수 있습니다. OpenAI API의 dimensions 파라미터로 512차원으로 줄이면 성능 손실은 미미하지만 속도는 3배 빨라집니다.
💡 대용량 데이터는 한 번에 저장하지 말고 배치로 나누어 collection.add를 여러 번 호출하세요. 한 번에 10만 개를 저장하면 메모리 부족 에러가 발생할 수 있습니다.
💡 검색 성능을 모니터링하세요. 벡터가 100만 개를 넘으면 인덱스 설정을 조정해야 할 수 있습니다. ChromaDB의 경우 자동으로 최적화하지만, Pinecone 등은 수동 설정이 필요합니다.
5. 의미 기반 검색 시스템 구축
시작하며
여러분이 회사 문서 검색 시스템을 만든다고 생각해봅시다. 직원이 "연차 신청 방법"을 검색했는데, 실제 문서 제목은 "휴가 사용 가이드"라서 찾지 못하는 상황이 자주 발생합니다.
전통적인 키워드 검색의 한계입니다. 이런 문제는 사내 위키, 고객 지원 센터, 법률 문서 검색 등 모든 정보 검색 시스템에서 발생합니다.
같은 의미를 다르게 표현한 경우, 동의어를 사용한 경우, 오타가 있는 경우 등 수많은 상황에서 원하는 정보를 찾지 못합니다. 바로 이럴 때 필요한 것이 의미 기반 검색(Semantic Search)입니다.
OpenAI Embeddings를 활용하면 단어가 정확히 일치하지 않아도 의미가 비슷하면 찾아주는 똑똑한 검색 시스템을 만들 수 있습니다.
개요
간단히 말해서, 의미 기반 검색은 사용자의 질문과 문서의 의미를 벡터로 변환한 후, 가장 의미가 가까운 문서를 찾아주는 시스템입니다. 표면적인 단어 매칭이 아닌 진짜 의미를 이해하는 검색입니다.
왜 의미 기반 검색이 필요할까요? 사용자는 같은 의미를 다양한 방식으로 표현합니다.
"급여 명세서"를 찾는 사람이 "월급 내역서", "페이슬립", "임금 상세" 등 다양하게 검색하는데, 키워드 검색으로는 모두 커버할 수 없습니다. 의미 기반 검색은 이 모든 표현을 이해합니다.
전통적인 방법에서는 Elasticsearch의 BM25 알고리즘이나 TF-IDF를 사용했다면, 이제는 AI 임베딩으로 훨씬 더 정확하고 직관적인 검색이 가능합니다. 의미 기반 검색의 핵심 특징은 첫째, 동의어와 유사 표현을 자동으로 인식하고, 둘째, 다국어 검색도 지원하며, 셋째, 사용자 의도를 파악한다는 점입니다.
이러한 특징들이 사용자 만족도를 크게 높이고, 원하는 정보를 찾는 시간을 단축시켜줍니다.
코드 예제
from openai import OpenAI
import chromadb
from typing import List, Dict
client = OpenAI(api_key="your-api-key-here")
chroma_client = chromadb.PersistentClient(path="./search_db")
collection = chroma_client.get_or_create_collection(name="knowledge_base")
class SemanticSearch:
def __init__(self):
self.collection = collection
# 문서 색인 (인덱싱)
def index_documents(self, documents: List[Dict]):
texts = [doc['content'] for doc in documents]
# 배치로 임베딩 생성
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
embeddings = [item.embedding for item in response.data]
# 벡터 DB에 저장
self.collection.add(
documents=texts,
embeddings=embeddings,
ids=[doc['id'] for doc in documents],
metadatas=[{
'title': doc['title'],
'category': doc.get('category', 'general')
} for doc in documents]
)
# 검색
def search(self, query: str, top_k: int = 5, category: str = None):
# 쿼리 임베딩 생성
query_emb = client.embeddings.create(
model="text-embedding-3-small",
input=query
).data[0].embedding
# 필터 적용 (선택적)
where_filter = {"category": category} if category else None
# 유사도 검색
results = self.collection.query(
query_embeddings=[query_emb],
n_results=top_k,
where=where_filter
)
return [{
'content': results['documents'][0][i],
'metadata': results['metadatas'][0][i],
'distance': results['distances'][0][i]
} for i in range(len(results['documents'][0]))]
# 사용 예시
search_engine = SemanticSearch()
# 문서 색인
docs = [
{'id': '1', 'title': '휴가 사용 가이드', 'content': '연차 신청은 1개월 전에...', 'category': 'hr'},
{'id': '2', 'title': '급여 안내', 'content': '월급 명세서 확인 방법...', 'category': 'hr'},
]
search_engine.index_documents(docs)
# 검색
results = search_engine.search("연차 신청 방법", top_k=3)
for r in results:
print(f"제목: {r['metadata']['title']}, 거리: {r['distance']:.4f}")
설명
이것이 하는 일: 이 코드는 실무에서 바로 사용 가능한 완전한 의미 기반 검색 시스템입니다. 문서를 색인하고, 사용자 질문에 가장 관련 있는 문서를 찾아주며, 카테고리 필터링도 지원합니다.
첫 번째로, SemanticSearch 클래스를 만들어 검색 엔진을 객체지향적으로 구현했습니다. 이렇게 하면 여러 컬렉션을 관리하거나, 검색 로직을 확장하기가 훨씬 쉬워집니다.
클래스 내부에 collection을 저장해두어 매번 초기화할 필요가 없습니다. 그 다음으로, index_documents 메서드는 여러 문서를 한 번에 색인합니다.
딕셔너리 형태로 문서를 받아서 content(본문), title(제목), category(카테고리) 등을 분리해서 저장합니다. 메타데이터에 제목과 카테고리를 저장하면 나중에 검색 결과를 보여줄 때 유용하고, 필터링도 가능합니다.
세 번째 단계에서 search 메서드는 사용자 쿼리를 받아 가장 관련 있는 문서를 찾습니다. where_filter를 사용하면 특정 카테고리만 검색할 수 있습니다.
예를 들어 "연차 신청 방법"을 검색하되 'hr' 카테고리만 검색하면 더 정확한 결과를 얻을 수 있습니다. 마지막으로, 검색 결과는 거리(distance) 값과 함께 반환됩니다.
거리가 작을수록 더 유사하다는 의미입니다. 일반적으로 0.3 이하면 매우 관련성이 높고, 0.3~0.5면 관련 있음, 0.5 이상이면 관련성이 낮다고 판단합니다.
이 값을 기준으로 검색 결과 품질을 평가할 수 있습니다. 여러분이 이 코드를 사용하면 사내 위키, FAQ 시스템, 고객 지원 챗봇, 법률 문서 검색 등 다양한 검색 시스템을 쉽게 구축할 수 있습니다.
실무에서는 수만 개의 문서를 색인하고, 사용자가 질문하면 0.1초 안에 가장 관련 있는 상위 5개 문서를 보여주는 시스템으로 활용됩니다. 또한 검색 결과를 GPT에게 제공하여 정확한 답변을 생성하는 RAG 시스템의 핵심 컴포넌트로도 사용됩니다.
실전 팁
💡 문서가 너무 길면 청크(chunk)로 나누어 색인하세요. 한 문서를 500자씩 나누면 검색 정확도가 크게 향상됩니다. 단, 청크 간 문맥이 끊기지 않도록 100자 정도는 겹치게 만드세요.
💡 하이브리드 검색을 고려하세요. 의미 기반 검색(70%)과 키워드 검색(30%)을 조합하면 둘의 장점을 모두 활용할 수 있습니다. Elasticsearch + Embeddings 조합이 인기있습니다.
💡 검색 품질을 지속적으로 모니터링하세요. 사용자가 클릭한 결과, 검색 후 이탈률, 재검색 비율 등을 추적하여 임계값을 조정하세요.
💡 동일한 쿼리에 대한 검색 결과는 캐싱하세요. Redis에 쿼리 해시를 키로 결과를 저장하면 반복 검색 시 API 비용을 절감하고 속도도 100배 빨라집니다.
💡 다국어 서비스라면 OpenAI의 다국어 모델을 활용하세요. 영어 쿼리로 한국어 문서를 검색하거나 그 반대도 가능합니다. 별도 번역 없이 크로스 언어 검색이 됩니다.
6. RAG 시스템 구축하기
시작하며
여러분이 회사의 모든 문서를 학습한 AI 챗봇을 만들고 싶다면 어떻게 해야 할까요? GPT를 직접 파인튜닝하는 것은 비용도 많이 들고 시간도 오래 걸립니다.
더 큰 문제는 문서가 업데이트될 때마다 다시 학습해야 한다는 점입니다. 이런 문제는 지식 기반 챗봇, 고객 지원 AI, 법률 자문 봇 등을 만들 때 항상 발생합니다.
최신 정보를 빠르게 반영해야 하고, 특정 도메인 지식이 필요하며, 출처를 명확히 밝혀야 하는 경우 파인튜닝만으로는 부족합니다. 바로 이럴 때 필요한 것이 RAG(Retrieval Augmented Generation)입니다.
사용자 질문에 관련된 문서를 먼저 검색한 후, 그 문서를 참고하여 GPT가 답변을 생성하게 만드는 방식입니다.
개요
간단히 말해서, RAG는 검색과 생성을 결합한 시스템입니다. 마치 시험 볼 때 교과서를 펼쳐놓고 답안을 작성하는 것처럼, AI가 관련 문서를 먼저 찾아보고 그것을 참고해서 답변을 만듭니다.
왜 RAG가 필요할까요? GPT는 학습 데이터에 없는 최신 정보나 회사 내부 문서는 모릅니다.
예를 들어 "우리 회사의 2024년 복지 정책"을 물어보면 GPT는 답할 수 없지만, RAG를 사용하면 관련 문서를 찾아서 정확한 답변을 줄 수 있습니다. 전통적인 방법에서는 규칙 기반 챗봇을 만들거나 비싼 파인튜닝을 했다면, 이제는 RAG로 더 유연하고 정확하며 업데이트가 쉬운 시스템을 만들 수 있습니다.
RAG의 핵심 특징은 첫째, 최신 정보를 즉시 반영할 수 있고, 둘째, 출처를 명확히 밝힐 수 있으며, 셋째, 환각(hallucination)을 크게 줄일 수 있다는 점입니다. 이러한 특징들이 신뢰할 수 있는 기업용 AI 챗봇을 가능하게 만듭니다.
코드 예제
from openai import OpenAI
import chromadb
client = OpenAI(api_key="your-api-key-here")
chroma_client = chromadb.PersistentClient(path="./rag_db")
collection = chroma_client.get_or_create_collection(name="company_docs")
class RAGSystem:
def __init__(self):
self.collection = collection
# 문서 색인
def add_documents(self, documents):
texts = [doc['content'] for doc in documents]
embeddings = []
for text in texts:
emb = client.embeddings.create(
model="text-embedding-3-small",
input=text
).data[0].embedding
embeddings.append(emb)
self.collection.add(
documents=texts,
embeddings=embeddings,
ids=[doc['id'] for doc in documents],
metadatas=[{'title': doc['title']} for doc in documents]
)
# 관련 문서 검색
def retrieve_context(self, query, top_k=3):
query_emb = client.embeddings.create(
model="text-embedding-3-small",
input=query
).data[0].embedding
results = self.collection.query(
query_embeddings=[query_emb],
n_results=top_k
)
# 검색된 문서들을 하나의 컨텍스트로 합치기
contexts = results['documents'][0]
sources = [meta['title'] for meta in results['metadatas'][0]]
return contexts, sources
# RAG 질의응답
def ask(self, question):
# 1단계: 관련 문서 검색
contexts, sources = self.retrieve_context(question)
# 2단계: 컨텍스트와 함께 GPT에 질문
context_text = "\n\n".join(contexts)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "당신은 회사 문서를 기반으로 정확하게 답변하는 AI입니다. 제공된 문서 내용만 사용하세요."},
{"role": "user", "content": f"다음 문서를 참고하여 질문에 답하세요.\n\n문서:\n{context_text}\n\n질문: {question}"}
]
)
answer = response.choices[0].message.content
return {
'answer': answer,
'sources': sources
}
# 사용 예시
rag = RAGSystem()
# 문서 추가
docs = [
{'id': '1', 'title': '연차 정책', 'content': '직원은 연 15일의 유급휴가를 사용할 수 있습니다. 1개월 전 신청이 원칙입니다.'},
{'id': '2', 'title': '재택근무 가이드', 'content': '주 2회까지 재택근무가 가능합니다. 사전 승인이 필요합니다.'},
]
rag.add_documents(docs)
# 질문
result = rag.ask("연차는 며칠 사용할 수 있나요?")
print(f"답변: {result['answer']}")
print(f"출처: {result['sources']}")
설명
이것이 하는 일: 이 코드는 완전한 RAG 시스템을 구현합니다. 문서를 색인하고, 사용자 질문에 관련된 문서를 찾아서, GPT에게 그 문서를 참고하도록 하여 정확한 답변을 생성합니다.
첫 번째로, add_documents 메서드는 회사 문서들을 벡터 데이터베이스에 저장합니다. 인사 규정, 업무 가이드, FAQ 등 모든 문서를 색인해두면 나중에 빠르게 검색할 수 있습니다.
실무에서는 수천 개의 문서를 한 번에 색인하며, 주기적으로 업데이트합니다. 그 다음으로, retrieve_context 메서드가 핵심입니다.
사용자 질문을 벡터로 변환한 후, 가장 관련 있는 상위 3개 문서를 찾습니다. 여기서 top_k=3은 조정 가능한데, 너무 많으면 GPT가 혼란스러워하고, 너무 적으면 정보가 부족할 수 있습니다.
일반적으로 3~5개가 적절합니다. 세 번째 단계에서 ask 메서드는 검색된 문서들을 GPT의 프롬프트에 포함시킵니다.
"다음 문서를 참고하여"라는 지시와 함께 검색된 내용을 제공하면, GPT는 그 내용을 기반으로 답변을 생성합니다. 시스템 메시지에서 "제공된 문서 내용만 사용하세요"라고 명시하여 환각을 방지합니다.
마지막으로, 답변과 함께 출처(sources)도 반환합니다. 이것이 RAG의 큰 장점인데, "이 정보는 '연차 정책' 문서에서 가져왔습니다"라고 명확히 밝힐 수 있어 신뢰도가 높아집니다.
법률, 의료, 금융 등 정확성이 중요한 분야에서는 필수적입니다. 여러분이 이 코드를 사용하면 사내 지식 베이스 챗봇, 고객 지원 AI, 제품 매뉴얼 검색, 법률 문서 분석 등 다양한 시스템을 구축할 수 있습니다.
실무에서는 Slack 봇으로 배포하여 직원들이 회사 정책을 물어보거나, 웹사이트에 고객 지원 챗봇으로 사용합니다. 문서가 업데이트되면 색인만 다시 하면 되므로 유지보수도 매우 쉽습니다.
실전 팁
💡 시스템 프롬프트를 잘 작성하세요. "문서에 없는 내용은 '문서에서 찾을 수 없습니다'라고 답하세요"라고 명시하면 환각을 더욱 줄일 수 있습니다.
💡 검색 품질이 RAG의 성능을 좌우합니다. 검색된 문서가 관련 없으면 GPT도 좋은 답변을 못 만듭니다. 주기적으로 검색 정확도를 평가하고 개선하세요.
💡 긴 문서는 청크로 나누세요. 한 문서가 3000자가 넘으면 500자 단위로 나누어 색인하는 것이 훨씬 정확합니다. 각 청크에 원본 문서 정보를 메타데이터로 저장하세요.
💡 비용 절감을 위해 검색된 컨텍스트를 캐싱하세요. 같은 질문이 반복되면 검색 단계를 건너뛰고 바로 GPT 호출만 하면 됩니다.
💡 답변 품질을 모니터링하세요. 사용자 피드백(좋아요/싫어요)을 받아서 어떤 질문에서 실패했는지 추적하고, 해당 문서를 개선하거나 청크 크기를 조정하세요.
7. 중복 탐지 시스템 만들기
시작하며
여러분이 Q&A 커뮤니티를 운영하는데, 매일 같은 질문이 반복해서 올라온다면 어떻게 하시겠습니까? "이 질문은 이미 답변되었습니다"라고 자동으로 알려주고 싶지만, 표현이 조금씩 다르면 찾기가 어렵습니다.
이런 문제는 Q&A 사이트, 고객 지원 티켓, 뉴스 기사, 학술 논문 등 다양한 분야에서 발생합니다. 완전히 똑같은 텍스트는 쉽게 찾을 수 있지만, 의미는 같은데 표현만 다른 경우를 찾는 것은 매우 어렵습니다.
이로 인해 중복 콘텐츠가 계속 쌓이고 사용자 경험이 나빠집니다. 바로 이럴 때 필요한 것이 임베딩 기반 중복 탐지입니다.
텍스트를 벡터로 변환하여 유사도를 측정하면, "로그인이 안 돼요"와 "로그인 오류 발생"을 같은 문제로 인식할 수 있습니다.
개요
간단히 말해서, 중복 탐지는 새로운 텍스트가 들어왔을 때 기존 데이터 중에서 의미적으로 매우 유사한 것이 있는지 찾아내는 시스템입니다. 완전히 똑같지 않아도 의미가 거의 같으면 중복으로 판단합니다.
왜 이 시스템이 필요할까요? 중복 콘텐츠는 검색 품질을 떨어뜨리고, 저장 공간을 낭비하며, 사용자를 혼란스럽게 만듭니다.
예를 들어 Stack Overflow 같은 Q&A 사이트에서는 같은 질문이 수십 번 올라오는 것을 방지하기 위해 중복 탐지가 필수입니다. 전통적인 방법에서는 해시값 비교나 편집 거리 측정을 사용했다면, 이제는 의미 기반 유사도로 훨씬 더 정확하게 중복을 찾을 수 있습니다.
단어 순서가 바뀌거나 동의어를 사용해도 정확히 탐지합니다. 중복 탐지의 핵심 특징은 첫째, 표현이 달라도 의미가 같으면 찾아내고, 둘째, 유사도 임계값을 조정하여 민감도를 조절할 수 있으며, 셋째, 실시간으로 빠르게 확인 가능하다는 점입니다.
이러한 특징들이 콘텐츠 품질을 높이고 사용자 경험을 개선시켜줍니다.
코드 예제
from openai import OpenAI
import chromadb
from typing import Optional, Dict
client = OpenAI(api_key="your-api-key-here")
chroma_client = chromadb.PersistentClient(path="./dedup_db")
collection = chroma_client.get_or_create_collection(name="content_db")
class DuplicateDetector:
def __init__(self, similarity_threshold=0.85):
self.collection = collection
self.threshold = similarity_threshold # 유사도 임계값
# 중복 확인
def check_duplicate(self, text: str) -> Optional[Dict]:
# 새 텍스트를 벡터로 변환
embedding = client.embeddings.create(
model="text-embedding-3-small",
input=text
).data[0].embedding
# 기존 데이터에서 유사한 것 검색
results = self.collection.query(
query_embeddings=[embedding],
n_results=1 # 가장 유사한 1개만
)
# 결과가 있고 유사도가 임계값 이상이면 중복
if results['documents'][0]:
# ChromaDB는 거리를 반환 (낮을수록 유사)
# 코사인 유사도로 변환: similarity = 1 - distance
distance = results['distances'][0][0]
similarity = 1 - distance
if similarity >= self.threshold:
return {
'is_duplicate': True,
'similarity': similarity,
'matched_text': results['documents'][0][0],
'matched_id': results['ids'][0][0]
}
return {'is_duplicate': False}
# 새 콘텐츠 추가
def add_content(self, content_id: str, text: str, metadata: dict = None):
# 먼저 중복 확인
dup_check = self.check_duplicate(text)
if dup_check['is_duplicate']:
print(f"⚠️ 중복 발견! 유사도: {dup_check['similarity']:.2%}")
print(f"기존 내용: {dup_check['matched_text'][:50]}...")
return False
# 중복 아니면 추가
embedding = client.embeddings.create(
model="text-embedding-3-small",
input=text
).data[0].embedding
self.collection.add(
documents=[text],
embeddings=[embedding],
ids=[content_id],
metadatas=[metadata or {}]
)
print(f"✅ 새 콘텐츠 추가됨: {content_id}")
return True
# 사용 예시
detector = DuplicateDetector(similarity_threshold=0.85)
# 첫 번째 질문 추가
detector.add_content("q1", "React에서 useState를 어떻게 사용하나요?")
# 유사한 질문 추가 시도 (중복 탐지됨)
detector.add_content("q2", "useState 훅 사용법을 알려주세요")
# 다른 질문 추가 (중복 아님)
detector.add_content("q3", "Python에서 리스트를 정렬하는 방법은?")
설명
이것이 하는 일: 이 코드는 Q&A 사이트, 고객 지원 티켓, 뉴스 기사 등에서 의미적으로 중복된 콘텐츠를 자동으로 탐지하는 시스템입니다. 새 콘텐츠가 추가되기 전에 기존 데이터와 비교하여 중복 여부를 판단합니다.
첫 번째로, DuplicateDetector 클래스는 유사도 임계값(threshold)을 설정할 수 있습니다. 기본값 0.85는 85% 이상 유사하면 중복으로 판단한다는 의미인데, 여러분의 서비스 특성에 맞게 조정할 수 있습니다.
엄격하게 하려면 0.9 이상, 느슨하게 하려면 0.7 정도로 설정하세요. 그 다음으로, check_duplicate 메서드는 새 텍스트를 벡터로 변환한 후 기존 데이터에서 가장 유사한 것을 찾습니다.
ChromaDB는 거리(distance) 값을 반환하는데, 이를 유사도로 변환하려면 1 - distance 계산이 필요합니다. 거리가 0.1이면 유사도는 0.9(90%)라는 의미입니다.
세 번째 단계에서 add_content 메서드는 실제로 중복을 확인하고 추가하는 전체 프로세스를 처리합니다. 먼저 중복 여부를 확인하고, 중복이면 경고 메시지와 함께 추가를 거부합니다.
중복이 아니면 벡터 DB에 저장하여 다음 중복 검사에 사용됩니다. 마지막으로, 실제 사용 예시를 보면 "useState를 어떻게 사용하나요"와 "useState 훅 사용법을 알려주세요"는 표현은 다르지만 의미가 거의 같아서 중복으로 탐지됩니다.
반면 "Python 리스트 정렬"은 완전히 다른 주제이므로 중복이 아닙니다. 여러분이 이 코드를 사용하면 Q&A 플랫폼에서 중복 질문을 자동으로 찾아 기존 답변으로 안내하거나, 뉴스 사이트에서 같은 사건에 대한 중복 기사를 필터링하거나, 고객 지원에서 유사한 티켓을 그룹화할 수 있습니다.
실무에서는 사용자가 질문을 작성하는 중에 실시간으로 "유사한 질문이 이미 있습니다"라고 알려주어 중복 생성을 사전에 방지합니다.
실전 팁
💡 임계값은 데이터로 실험해서 정하세요. 100개 정도의 샘플로 테스트해보고, 0.8, 0.85, 0.9에서 각각 정확도를 측정하여 최적값을 찾으세요.
💡 중복으로 판단되면 사용자에게 "이 질문과 유사합니다"라고 보여주고 선택권을 주세요. 100% 중복이 아닐 수 있으므로 강제로 막지 말고 제안만 하는 것이 좋습니다.
💡 매우 짧은 텍스트(10단어 이하)는 중복 탐지가 부정확할 수 있습니다. 최소 길이를 설정하거나, 짧은 텍스트는 키워드 매칭을 병행하세요.
💡 카테고리별로 분리해서 중복 검사하면 정확도가 높아집니다. "Python" 카테고리 내에서만 중복 확인하는 식으로 범위를 좁히세요.
💡 주기적으로 중복으로 잡힌 케이스를 리뷰하세요. False Positive(실제로는 중복 아닌데 중복으로 판단)가 많다면 임계값을 높이고, False Negative(중복인데 놓친 것)가 많다면 낮추세요.
8. 다국어 검색 지원하기
시작하며
여러분이 글로벌 서비스를 운영하는데, 한국 사용자는 한국어로 검색하고 미국 사용자는 영어로 검색합니다. 하지만 좋은 콘텐츠는 여러 언어로 섞여 있어서, 한국어로 검색하면 영어 콘텐츠를 못 찾는 문제가 발생합니다.
이런 문제는 국제적인 이커머스, 다국어 지식 베이스, 글로벌 고객 지원 등에서 항상 발생합니다. 사용자가 자신의 모국어로 검색했을 때 다른 언어로 작성된 관련 콘텐츠도 함께 보여주고 싶지만, 전통적인 검색으로는 불가능합니다.
바로 이럴 때 필요한 것이 크로스 언어 검색(Cross-lingual Search)입니다. OpenAI의 임베딩 모델은 100개 이상의 언어를 지원하며, 한국어 쿼리로 영어 문서를 검색하거나 그 반대도 가능합니다.
개요
간단히 말해서, 크로스 언어 검색은 검색어와 문서의 언어가 달라도 의미만 비슷하면 찾아주는 시스템입니다. 내부적으로는 모든 언어를 같은 벡터 공간에 매핑하여 언어 장벽을 넘어 검색합니다.
왜 다국어 검색이 필요할까요? 글로벌 기업에서는 영어, 한국어, 일본어, 스페인어 등 다양한 언어로 문서가 작성됩니다.
한 사용자가 한국어로 "사용자 인증 방법"을 검색했을 때, 영어로 작성된 "User Authentication Guide"도 함께 보여줘야 완전한 정보를 제공할 수 있습니다. 전통적인 방법에서는 모든 문서를 번역하거나 언어별로 분리된 검색을 했다면, 이제는 번역 없이 다국어 검색이 가능합니다.
비용도 절감되고 최신 정보도 바로 반영됩니다. 다국어 검색의 핵심 특징은 첫째, 100개 이상의 언어를 지원하고, 둘째, 번역 없이 직접 검색 가능하며, 셋째, 언어 간 의미가 정확히 매칭된다는 점입니다.
이러한 특징들이 진정한 글로벌 서비스를 가능하게 만듭니다.
코드 예제
from openai import OpenAI
import chromadb
client = OpenAI(api_key="your-api-key-here")
chroma_client = chromadb.PersistentClient(path="./multilingual_db")
collection = chroma_client.get_or_create_collection(name="global_docs")
class MultilingualSearch:
def __init__(self):
self.collection = collection
# 다국어 문서 색인
def index_multilingual_docs(self, documents):
"""
documents: [
{'id': '1', 'content': 'User guide...', 'language': 'en'},
{'id': '2', 'content': '사용자 가이드...', 'language': 'ko'},
]
"""
texts = [doc['content'] for doc in documents]
# 모든 언어를 같은 모델로 벡터화
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
embeddings = [item.embedding for item in response.data]
# 언어 정보를 메타데이터로 저장
self.collection.add(
documents=texts,
embeddings=embeddings,
ids=[doc['id'] for doc in documents],
metadatas=[{
'language': doc['language'],
'title': doc.get('title', '')
} for doc in documents]
)
# 다국어 검색
def search(self, query, query_language=None, target_languages=None, top_k=5):
# 쿼리 벡터화 (어떤 언어든 가능)
query_emb = client.embeddings.create(
model="text-embedding-3-small",
input=query
).data[0].embedding
# 특정 언어만 검색하려면 필터 적용
where_filter = None
if target_languages:
where_filter = {"language": {"$in": target_languages}}
# 검색
results = self.collection.query(
query_embeddings=[query_emb],
n_results=top_k,
where=where_filter
)
# 결과 포맷팅
return [{
'content': results['documents'][0][i],
'language': results['metadatas'][0][i]['language'],
'title': results['metadatas'][0][i]['title'],
'similarity': 1 - results['distances'][0][i]
} for i in range(len(results['documents'][0]))]
# 사용 예시
search_system = MultilingualSearch()
# 다국어 문서 색인
docs = [
{'id': '1', 'content': 'How to use React Hooks for state management', 'language': 'en', 'title': 'React Hooks Guide'},
{'id': '2', 'content': 'React 훅을 사용한 상태 관리 방법', 'language': 'ko', 'title': 'React 훅 가이드'},
{'id': '3', 'content': 'Cómo usar React Hooks', 'language': 'es', 'title': 'Guía de React Hooks'},
{'id': '4', 'content': 'Python 데이터 분석 기초', 'language': 'ko', 'title': 'Python 분석'},
]
search_system.index_multilingual_docs(docs)
# 한국어로 검색 (모든 언어 결과)
print("=== 한국어 검색: 'React 상태 관리' ===")
results = search_system.search("React 상태 관리")
for r in results[:3]:
print(f"[{r['language']}] {r['title']} (유사도: {r['similarity']:.2%})")
# 영어로 검색 (한국어 문서도 찾음)
print("\n=== 영어 검색: 'React state' ===")
results = search_system.search("React state")
for r in results[:3]:
print(f"[{r['language']}] {r['title']} (유사도: {r['similarity']:.2%})")
설명
이것이 하는 일: 이 코드는 여러 언어로 작성된 문서들을 하나의 데이터베이스에 저장하고, 사용자가 어떤 언어로 검색하든 의미적으로 관련 있는 모든 문서를 찾아주는 시스템입니다. 첫 번째로, index_multilingual_docs 메서드는 영어, 한국어, 스페인어 등 다양한 언어의 문서를 같은 임베딩 모델로 벡터화합니다.
핵심은 모든 언어를 text-embedding-3-small 모델 하나로 처리한다는 점입니다. 이 모델은 내부적으로 다국어를 같은 의미 공간에 매핑하도록 학습되었습니다.
그 다음으로, 각 문서의 언어 정보를 메타데이터로 저장합니다. 이렇게 하면 나중에 "영어와 한국어 문서만 검색" 같은 필터링이 가능합니다.
실무에서는 사용자 언어 설정에 따라 결과를 조정할 때 유용합니다. 세 번째 단계에서 search 메서드는 쿼리 언어와 관계없이 검색을 수행합니다.
"React 상태 관리"라는 한국어 쿼리를 벡터로 변환하면, 영어 문서 "React Hooks for state management"와 의미적으로 가까운 위치에 배치되어 높은 유사도로 검색됩니다. 마지막으로, target_languages 파라미터를 사용하면 검색 대상 언어를 제한할 수 있습니다.
예를 들어 한국 사용자에게는 한국어와 영어 결과만 보여주고, 중국어나 스페인어는 제외할 수 있습니다. 이렇게 하면 사용자 경험을 개선하면서도 다국어의 이점을 살릴 수 있습니다.
여러분이 이 코드를 사용하면 글로벌 이커머스에서 한국 사용자가 한국어로 검색해도 영어 상품 설명을 찾거나, 다국어 고객 지원에서 어느 언어로 작성된 FAQ든 찾을 수 있습니다. 실무에서는 Netflix, Airbnb 같은 글로벌 서비스에서 사용자 언어와 관계없이 최적의 콘텐츠를 추천하는 시스템에 활용됩니다.
번역 비용을 절감하면서도 더 나은 검색 경험을 제공할 수 있습니다.
실전 팁
💡 언어 감지를 자동화하세요. langdetect 라이브러리로 문서와 쿼리의 언어를 자동 탐지하여 메타데이터에 저장하면 관리가 쉬워집니다.
💡 같은 의미라도 언어마다 문화적 뉘앙스가 다를 수 있습니다. 중요한 비즈니스 문서는 언어별로 인간 검수를 거치세요.
💡 결과를 언어별로 그룹화해서 보여주면 사용자 경험이 좋아집니다. "한국어 결과 3개, 영어 결과 2개" 형식으로 구분하세요.
💡 모든 언어가 동등하게 잘 지원되는 것은 아닙니다. 영어, 한국어, 중국어, 스페인어 등 주요 언어는 매우 정확하지만, 소수 언어는 품질이 떨어질 수 있습니다.
💡 사용자 언어를 추적하여 개인화하세요. 한국 사용자는 같은 유사도라면 한국어 문서를 우선 보여주는 식으로 랭킹을 조정하면 만족도가 높아집니다.
9. 임베딩 캐싱으로 비용 절감하기
시작하며
여러분이 매일 같은 문서들을 반복해서 벡터로 변환하고 있다면, 불필요하게 API 비용을 지불하고 있는 것입니다. 예를 들어 상품 설명이 바뀌지 않았는데 매번 새로 임베딩을 생성한다면 낭비입니다.
이런 문제는 프로덕션 환경에서 매우 흔합니다. 같은 FAQ를 하루에 100번 벡터화하거나, 변경되지 않은 블로그 포스트를 계속 재처리하는 경우가 많습니다.
이로 인해 API 비용이 불필요하게 증가하고, 처리 시간도 길어집니다. 바로 이럴 때 필요한 것이 임베딩 캐싱입니다.
한 번 생성한 벡터를 저장해두고 재사용하면 비용을 90% 이상 절감하고 속도도 100배 빠르게 만들 수 있습니다.
개요
간단히 말해서, 임베딩 캐싱은 텍스트를 키로 하고 벡터를 값으로 하는 저장소를 만들어 중복 API 호출을 방지하는 기법입니다. 마치 웹 브라우저가 이미지를 캐싱하는 것처럼, 한 번 만든 벡터를 저장해둡니다.
왜 캐싱이 필요할까요? OpenAI API는 사용량에 따라 비용이 청구되는데, 같은 텍스트를 반복해서 처리하면 불필요한 비용이 발생합니다.
예를 들어 1000개의 FAQ가 있다면 첫 번째는 API를 호출하지만, 두 번째부터는 캐시에서 가져오면 비용이 0입니다. 전통적인 방법에서는 매번 새로 API를 호출했다면, 이제는 Redis나 파일 시스템에 캐싱하여 효율을 극대화할 수 있습니다.
임베딩 캐싱의 핵심 특징은 첫째, API 비용을 90% 이상 절감하고, 둘째, 응답 속도를 100배 이상 향상시키며, 셋째, API Rate Limit 걱정을 줄인다는 점입니다. 이러한 특징들이 대규모 프로덕션 서비스를 경제적으로 운영 가능하게 만듭니다.
코드 예제
from openai import OpenAI
import hashlib
import json
import os
from typing import List
client = OpenAI(api_key="your-api-key-here")
class EmbeddingCache:
def __init__(self, cache_dir="./embedding_cache"):
self.cache_dir = cache_dir
os.makedirs(cache_dir, exist_ok=True)
# 텍스트를 해시로 변환 (캐시 키)
def _get_cache_key(self, text: str) -> str:
# SHA256 해시로 텍스트를 고유 키로 변환
return hashlib.sha256(text.encode()).hexdigest()
# 캐시 파일 경로
def _get_cache_path(self, cache_key: str) -> str:
return os.path.join(self.cache_dir, f"{cache_key}.json")
# 캐시에서 읽기
def get(self, text: str) -> List[float]:
cache_key = self._get_cache_key(text)
cache_path = self._get_cache_path(cache_key)
# 캐시 파일이 있으면 읽어서 반환
if os.path.exists(cache_path):
with open(cache_path, 'r') as f:
data = json.load(f)
return data['embedding']
return None
# 캐시에 저장
def set(self, text: str, embedding: List[float]):
cache_key = self._get_cache_key(text)
cache_path = self._get_cache_path(cache_key)
# 캐시 파일에 저장
with open(cache_path, 'w') as f:
json.dump({
'text': text[:100], # 확인용으로 일부만 저장
'embedding': embedding
}, f)
# 캐시를 활용한 임베딩 생성
def get_embedding(self, text: str) -> List[float]:
# 1. 캐시 확인
cached = self.get(text)
if cached:
print(f"✅ 캐시 히트: {text[:50]}...")
return cached
# 2. 캐시 미스 - API 호출
print(f"🔄 API 호출: {text[:50]}...")
embedding = client.embeddings.create(
model="text-embedding-3-small",
input=text
).data[0].embedding
# 3. 캐시에 저장
self.set(text, embedding)
return embedding
# 배치 처리 (캐시 활용)
def get_embeddings_batch(self, texts: List[str]) -> List[List[float]]:
embeddings = []
uncached_texts = []
uncached_indices = []
# 캐시에 있는 것과 없는 것 분리
for i, text in enumerate(texts):
cached = self.get(text)
if cached:
embeddings.append(cached)
else:
embeddings.append(None) # 나중에 채울 자리
uncached_texts.append(text)
uncached_indices.append(i)
# 캐시에 없는 것만 API 호출
if uncached_texts:
print(f"🔄 API 호출: {len(uncached_texts)}개 (전체 {len(texts)}개 중)")
response = client.embeddings.create(
model="text-embedding-3-small",
input=uncached_texts
)
# 결과를 캐시에 저장하고 리스트에 추가
for i, item in enumerate(response.data):
embedding = item.embedding
original_idx = uncached_indices[i]
embeddings[original_idx] = embedding
self.set(uncached_texts[i], embedding)
print(f"✅ 완료: 캐시 {len(texts) - len(uncached_texts)}개, 새로 생성 {len(uncached_texts)}개")
return embeddings
# 사용 예시
cache = EmbeddingCache()
# 첫 번째 호출 - API 사용
emb1 = cache.get_embedding("React Hooks 사용법")
# 두 번째 호출 - 캐시 사용 (비용 0, 속도 100배)
emb2 = cache.get_embedding("React Hooks 사용법")
# 배치 처리 (일부는 캐시, 일부는 새로 생성)
texts = [
"React Hooks 사용법", # 캐시에 있음
"Python 리스트 정렬", # 새로 생성
"React Hooks 사용법", # 캐시에 있음
]
embeddings = cache.get_embeddings_batch(texts)
설명
이것이 하는 일: 이 코드는 한 번 생성한 임베딩을 로컬 파일 시스템에 저장하고, 같은 텍스트가 다시 요청되면 API를 호출하지 않고 캐시에서 바로 가져옵니다. 첫 번째로, _get_cache_key 메서드는 텍스트를 SHA256 해시로 변환합니다.
"React Hooks 사용법"이라는 텍스트는 항상 같은 해시값(예: a3f8e2...)을 생성하므로, 이를 파일명으로 사용하여 빠르게 찾을 수 있습니다. 해시를 사용하면 긴 텍스트나 특수문자도 안전하게 처리됩니다.
그 다음으로, get_embedding 메서드는 캐시 우선 전략을 사용합니다. 먼저 캐시를 확인하고, 있으면 바로 반환하여 0.001초 안에 완료됩니다.
없으면 OpenAI API를 호출하여 새로 생성하고, 결과를 캐시에 저장합니다. 이렇게 하면 두 번째 요청부터는 즉시 응답할 수 있습니다.
세 번째 단계에서 get_embeddings_batch 메서드는 더욱 똑똑합니다. 배치로 들어온 텍스트 중에서 캐시에 있는 것과 없는 것을 분리하여, 없는 것만 API로 호출합니다.
예를 들어 100개 중 80개가 캐시에 있다면 20개만 API를 호출하여 비용을 80% 절감합니다. 마지막으로, 캐시 파일은 JSON 형식으로 저장되므로 사람이 읽을 수도 있고, 다른 프로그램에서도 사용할 수 있습니다.
실무에서는 파일 대신 Redis를 사용하면 더 빠르고, 여러 서버가 캐시를 공유할 수 있어 효율이 더욱 높아집니다. 여러분이 이 코드를 사용하면 프로덕션 환경에서 API 비용을 극적으로 절감할 수 있습니다.
실무에서는 FAQ 1000개를 매일 처리한다면 첫날만 API 비용이 발생하고, 이후로는 캐시만 사용하여 비용이 0입니다. 또한 응답 속도도 0.3초에서 0.003초로 100배 빨라져 사용자 경험이 크게 향상됩니다.
대규모 서비스에서는 월 수백만 원의 비용 절감 효과가 있습니다.
실전 팁
💡 프로덕션에서는 파일 대신 Redis를 사용하세요. redis.set(cache_key, json.dumps(embedding))으로 저장하면 속도가 10배 더 빠르고 메모리 효율도 좋습니다.
💡 캐시에 TTL(Time To Live)을 설정하세요. 7일 또는 30일 후 자동 삭제되게 하면 오래된 데이터가 쌓이는 것을 방지할 수 있습니다.
💡 캐시 히트율을 모니터링하세요. "전체 요청 중 캐시에서 가져온 비율"을 추적하여 80% 이상 유지되도록 하세요. 낮다면 캐싱 전략을 재검토해야 합니다.
💡 텍스트 전처리를 일관되게 하세요. 공백이나 대소문자 차이로 같은 텍스트가 다르게 인식되면 캐시 미스가 발생합니다. 소문자 변환, 공백 정규화 등을 먼저 적용하세요.
💡 배포 시 캐시를 미리 워밍업(warming up)하세요. 주요 FAQ나 자주 검색되는 텍스트를 미리 임베딩해두면 사용자가 처음부터 빠른 응답을 경험합니다.
10. 개인화 추천 시스템 구축하기
시작하며
여러분이 블로그 플랫폼을 운영하는데, 사용자가 읽은 글을 바탕으로 관심 있을 만한 다른 글을 추천하고 싶다면 어떻게 해야 할까요? 단순히 같은 카테고리 글을 보여주는 것은 정확도가 떨어집니다.
이런 문제는 콘텐츠 플랫폼, 이커머스, 뉴스 사이트 등 모든 추천 시스템에서 발생합니다. 사용자의 관심사를 정확히 파악하고, 그에 맞는 콘텐츠를 추천해야 사용자 참여도와 체류 시간이 증가합니다.
부정확한 추천은 오히려 사용자 경험을 해칩니다. 바로 이럴 때 필요한 것이 임베딩 기반 추천 시스템입니다.
사용자가 읽은 글들의 벡터를 평균내어 사용자 프로필을 만들고, 그와 유사한 콘텐츠를 찾아 추천할 수 있습니다.
개요
간단히 말해서, 임베딩 기반 추천은 사용자가 좋아한 콘텐츠의 의미를 벡터로 표현하여, 비슷한 의미의 새로운 콘텐츠를 찾아주는 시스템입니다. 협업 필터링보다 콜드 스타트 문제에 강하고, 더 정확합니다.
왜 임베딩 기반 추천이 필요할까요? 전통적인 협업 필터링은 "이 글을 읽은 사람은 저 글도 읽었다"는 패턴을 찾지만, 신규 콘텐츠나 신규 사용자에게는 작동하지 않습니다.
반면 임베딩은 콘텐츠 자체의 의미를 분석하므로 새 글도 즉시 추천할 수 있습니다. 전통적인 방법에서는 사용자-아이템 행렬을 만들어 협업 필터링을 했다면, 이제는 의미 벡터로 더 정확하고 실시간성이 높은 추천이 가능합니다.
임베딩 추천의 핵심 특징은 첫째, 콜드 스타트 문제를 해결하고, 둘째, 실시간으로 사용자 프로필을 업데이트할 수 있으며, 셋째, 설명 가능한 추천이 가능하다는 점입니다. 이러한 특징들이 사용자 만족도를 높이고 체류 시간을 증가시켜줍니다.
코드 예제
from openai import OpenAI
import chromadb
import numpy as np
from typing import List, Dict
client = OpenAI(api_key="your-api-key-here")
chroma_client = chromadb.PersistentClient(path="./recommend_db")
collection = chroma_client.get_or_create_collection(name="articles")
class RecommendationSystem:
def __init__(self):
self.collection = collection
self.user_profiles = {} # 사용자 프로필 저장
# 콘텐츠 색인
def index_content(self, articles: List[Dict]):
texts = [article['content'] for article in articles]
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
embeddings = [item.embedding for item in response.data]
self.collection.add(
documents=texts,
embeddings=embeddings,
ids=[article['id'] for article in articles],
metadatas=[{
'title': article['title'],
'category': article.get('category', '')
} for article in articles]
)
# 사용자 프로필 업데이트 (읽은 글 기반)
def update_user_profile(self, user_id: str, article_ids: List[str]):
# 사용자가 읽은 글들의 벡터 가져오기
results = self.collection.get(ids=article_ids, include=['embeddings'])
if results['embeddings']:
# 읽은 글들의 벡터를 평균내어 사용자 프로필 생성
embeddings_array = np.array(results['embeddings'])
user_vector = np.mean(embeddings_array, axis=0)
# 사용자 프로필 저장
self.user_profiles[user_id] = user_vector.tolist()
print(f"✅ 사용자 {user_id} 프로필 업데이트 완료")
# 개인화 추천
def recommend(self, user_id: str, exclude_ids: List[str] = None, top_k: int = 5):
# 사용자 프로필 확인
if user_id not in self.user_profiles:
print(f"⚠️ 사용자 {user_id}의 프로필이 없습니다")
return []
user_vector = self.user_profiles[user_id]
# 사용자 프로필과 유사한 콘텐츠 검색
results = self.collection.query(
query_embeddings=[user_vector],
n_results=top_k + (len(exclude_ids) if exclude_ids else 0)
)
# 이미 읽은 글 제외
recommendations = []
for i in range(len(results['documents'][0])):
article_id = results['ids'][0][i]
# 제외 목록에 없으면 추가
if not exclude_ids or article_id not in exclude_ids:
recommendations.append({
'id': article_id,
'title': results['metadatas'][0][i]['title'],
'category': results['metadatas'][0][i]['category'],
'similarity': 1 - results['distances'][0][i],
'content': results['documents'][0][i][:100] + '...'
})
if len(recommendations) >= top_k:
break
return recommendations
# 비슷한 콘텐츠 추천 (콘텐츠 기반)
def similar_content(self, article_id: str, top_k: int = 5):
# 특정 글과 비슷한 글 찾기
article = self.collection.get(ids=[article_id], include=['embeddings'])
if not article['embeddings']:
return []
results = self.collection.query(
query_embeddings=[article['embeddings'][0]],
n_results=top_k + 1 # 자기 자신 제외
)
# 자기 자신 제외하고 반환
recommendations = []
for i in range(len(results['documents'][0])):
if results['ids'][0][i] != article_id:
recommendations.append({
'id': results['ids'][0][i],
'title': results['metadatas'][0][i]['title'],
'similarity': 1 - results['distances'][0][i]
})
return recommendations[:top_k]
# 사용 예시
recommender = RecommendationSystem()
# 콘텐츠 색인
articles = [
{'id': '1', 'title': 'React Hooks 완벽 가이드', 'content': 'React Hooks를 사용하면 함수형 컴포넌트에서도 상태를 관리할 수 있습니다...', 'category': 'React'},
{'id': '2', 'title': 'Vue 3 Composition API', 'content': 'Vue 3의 Composition API는 React Hooks와 유사한 방식으로...', 'category': 'Vue'},
{'id': '3', 'title': 'Python 데이터 분석', 'content': 'Pandas와 NumPy를 사용한 데이터 분석 기초...', 'category': 'Python'},
{'id': '4', 'title': 'React 성능 최적화', 'content': 'useMemo와 useCallback을 활용한 React 성능 개선...', 'category': 'React'},
]
recommender.index_content(articles)
# 사용자가 React 관련 글 읽음
recommender.update_user_profile('user123', ['1', '4'])
# 개인화 추천 (이미 읽은 글 제외)
recommendations = recommender.recommend('user123', exclude_ids=['1', '4'], top_k=3)
print("\n=== user123에게 추천 ===")
for rec in recommendations:
print(f"- {rec['title']} (유사도: {rec['similarity']:.2%})")
# 특정 글과 비슷한 글 추천
similar = recommender.similar_content('1', top_k=2)
print("\n=== 'React Hooks 완벽 가이드'와 비슷한 글 ===")
for s in similar:
print(f"- {s['title']} (유사도: {s['similarity']:.2%})")
설명
이것이 하는 일: 이 코드는 Netflix나 YouTube처럼 사용자의 과거 행동을 분석하여 관심 있을 만한 콘텐츠를 자동으로 추천하는 완전한 시스템입니다. 첫 번째로, index_content 메서드는 모든 블로그 글, 상품, 뉴스 기사 등을 벡터 데이터베이스에 저장합니다.
각 콘텐츠의 의미가 벡터로 표현되어, 나중에 빠르게 유사도를 계산할 수 있습니다. 그 다음으로, update_user_profile 메서드가 핵심입니다.
사용자가 읽은 글들의 벡터를 평균내어 하나의 "사용자 취향 벡터"를 만듭니다. 예를 들어 React 글 3개를 읽었다면, 사용자 벡터는 React 쪽으로 치우쳐져서 나중에 React 관련 글이 높은 유사도로 추천됩니다.
세 번째 단계에서 recommend 메서드는 사용자 벡터와 가장 유사한 콘텐츠를 찾습니다. 중요한 점은 이미 읽은 글(exclude_ids)은 제외한다는 것입니다.
아무리 유사도가 높아도 이미 본 콘텐츠를 다시 추천하면 의미가 없습니다. 마지막으로, similar_content 메서드는 "이 글을 읽는 사람은 이것도 좋아합니다" 스타일의 추천을 제공합니다.
특정 글의 벡터와 유사한 다른 글들을 찾아서, 글 하단에 "관련 글" 섹션을 만들 수 있습니다. 여러분이 이 코드를 사용하면 블로그, 뉴스 사이트, 이커머스 등에서 개인화 추천을 쉽게 구현할 수 있습니다.
실무에서는 사용자가 글을 읽을 때마다 실시간으로 프로필을 업데이트하고, 다음 페이지에서 바로 개인화된 추천을 보여줍니다. 클릭률이 일반 추천보다 2~3배 높아지고, 사용자 체류 시간도 크게 증가합니다.
Amazon, Netflix, YouTube 등 모든 주요 플랫폼이 유사한 방식을 사용합니다.
실전 팁
💡 사용자 프로필은 시간 가중치를 적용하세요. 최근 읽은 글에 더 높은 가중치를 주면 변화하는 관심사를 더 잘 반영합니다. 예: 최근 7일 글은 1.0, 30일 전 글은 0.5 가중치.
💡 다양성(diversity)을 고려하세요. 상위 10개가 모두 React 글이면 지루하므로, 상위 후보 중에서 카테고리를 분산시켜