저는 3년 넘게 AI API 게이트웨이 개발을 진행하면서 수백 개의 프로젝트에서 동일한 문제점을 목격했습니다. 바로 불필요한 API 호출로 인한 비용 폭탄과 반복 질문으로 인한 응답 지연입니다. 이번 튜토리얼에서는 제가 실제 프로젝트에서 검증한 AI API 캐싱 전략을 상세히 다룹니다.
왜 AI API 캐싱이 필수인가?
HolySheep AI의 가격표를 보면 Claude Sonnet 4.5는 $15/MTok, DeepSeek V3.2는 $0.42/MTok입니다. 같은 질문을 1000번 반복하면?
- Claude 사용 시: 약 $0.015 (100회) × 10 = $0.15
- DeepSeek 사용 시: 약 $0.00042 (100회) × 10 = $0.0042
- 캐싱 적용 시: 1회 비용 + Redis 운영비 = 약 90~95% 절감
실전 사용 사례 1: 이커머스 AI 고객 서비스 급증 대응
제가 컨설팅한 쇼핑몰 프로젝트에서 프로모션 기간 동안 트래픽이 50배 급증한 사례를 공유합니다.
문제 상황
# 캐싱 전 - 매번 API 호출
def get_product_recommendation(product_id: str, user_query: str) -> str:
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "system", "content": "당신은 제품 추천 전문가입니다."},
{"role": "user", "content": f"사용자 질문: {user_query}\n제품 ID: {product_id}"}
]
)
return response["choices"][0]["message"]["content"]
문제: 동일한 제품에 대한 질문이 반복 호출
10,000회 호출/시 × $0.03/1K토큰 = $300/시
프로모션 8시간 = $2,400 소요
해결책: Redis 기반 의미론적 캐싱
import hashlib
import json
import redis
from openai import OpenAI
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
class SemanticCache:
def __init__(self, redis_url: str = "redis://localhost:6379"):
self.redis_client = redis.from_url(redis_url)
self.vectorizer = TfidfVectorizer(max_features=384)
self.vector_cache = {} # question_id -> vector
self.similarity_threshold = 0.85
self.ttl = 3600 # 1시간
def _normalize_text(self, text: str) -> str:
"""텍스트 정규화 - 캐시 히트율 극대화"""
return " ".join(text.lower().strip().split())
def _get_cache_key(self, text: str) -> str:
"""텍스트 기반 해시 키 생성"""
normalized = self._normalize_text(text)
return f"semantic_cache:{hashlib.sha256(normalized.encode()).hexdigest()[:32]}"
def _compute_similarity(self, text1: str, text2: str) -> float:
"""TF-IDF 기반 유사도 계산"""
try:
vectors = self.vectorizer.fit_transform([text1, text2])
similarity = cosine_similarity(vectors[0], vectors[1])[0][0]
return float(similarity)
except:
return 0.0
async def get_or_compute(self, prompt: str, compute_fn) -> str:
"""
의미론적 캐시 조회 또는 API 호출
HolySheep AI API 사용
"""
normalized_prompt = self._normalize_text(prompt)
cache_key = self._get_cache_key(normalized_prompt)
# 1단계: 정확한 키 조회
cached = self.redis_client.get(cache_key)
if cached:
self.redis_client.incr(f"{cache_key}:hits")
return cached.decode()
# 2단계: 유사한 캐시 탐색
all_keys = self.redis_client.keys("semantic_cache:*")
for key in all_keys:
cached_text = self.redis_client.get(key)
if cached_text:
similarity = self._compute_similarity(
normalized_prompt,
cached_text.decode()
)
if similarity >= self.similarity_threshold:
self.redis_client.incr(f"{key}:hits")
# 유사도 가중치로 반환
return f"[ cached ({similarity:.0%}) ] " + cached_text.decode()
# 3단계: API 호출 (HolySheep AI)
client = OpenAI(
api_key="YOUR_HOLYSHEEP_API_KEY",
base_url="https://api.holysheep.ai/v1"
)
result = await compute_fn(client, prompt)
# 캐시에 저장
self.redis_client.setex(cache_key, self.ttl, result)
return result
def get_stats(self) -> dict:
"""캐시 성능 통계"""
keys = self.redis_client.keys("semantic_cache:*")
total_hits = sum(
int(self.redis_client.get(f"{k}:hits") or 0)
for k in keys
)
return {
"cached_items": len(keys),
"total_hits": total_hits,
"hit_rate": total_hits / (total_hits + 1) * 100
}
사용 예시
cache = SemanticCache("redis://localhost:6379")
async def product_recommendation(product_id: str, user_query: str) -> str:
prompt = f"사용자 질문: {user_query}\n제품 ID: {product_id}"
async def call_api(client, p):
response = client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": p}],
max_tokens=500
)
return response.choices[0].message.content
return await cache.get_or_compute(prompt, call_api)
실전 사용 사례 2: 기업 RAG 시스템 - 벡터 캐싱
저는 최근 금융 기업 RAG 시스템 구축 프로젝트를 진행했습니다. 문서 임베딩 비용이 전체 운영비의 65%를 차지하는 문제가 있었습니다.
import chromadb
from chromadb.config import Settings
import hashlib
import json
from datetime import datetime, timedelta
class VectorCache:
"""벡터 데이터베이스 기반 문서 캐싱 시스템"""
def __init__(self, persist_directory: str = "./chroma_db"):
self.client = chromadb.PersistentClient(path=persist_directory)
self.collection = self.client.get_or_create_collection(
name="embedding_cache",
metadata={"hnsw:space": "cosine"}
)
self.text_to_id = {} # 빠른 조회를 위한 매핑
def _generate_doc_id(self, text: str, metadata: dict) -> str:
"""문서 고유 ID 생성"""
content = json.dumps({"text": text[:500], "meta": metadata}, sort_keys=True)
return hashlib.sha256(content.encode()).hexdigest()[:16]
def _should_embed(self, text: str) -> bool:
"""임베딩 필요 여부 판단"""
# 너무 짧은 텍스트는 무시
if len(text) < 50:
return False
# 패턴 기반 필터링
noise_patterns = ["test", "dummy", "lorem ipsum", "undefined"]
return not any(p in text.lower() for p in noise_patterns)
async def get_embedding(self, text: str, metadata: dict, client) -> list:
"""
벡터 캐시 조회 또는 생성
HolySheep AI Embeddings API 사용
"""
if not self._should_embed(text):
return None
doc_id = self._generate_doc_id(text, metadata)
# 캐시 히트 확인
existing = self.collection.get(ids=[doc_id])
if existing["ids"]:
return existing["embeddings"][0]
# HolySheep AI로 임베딩 생성
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
embedding = response.data[0].embedding
# 캐시에 저장
self.collection.add(
ids=[doc_id],
embeddings=[embedding],
documents=[text],
metadatas=[{
**metadata,
"created_at": datetime.now().isoformat(),
"token_count": response.usage.total_tokens
}]
)
return embedding
def batch_get_embeddings(self, texts: list, metadata: dict, client) -> dict:
"""배치 처리로 API 호출 최소화"""
results = {}
missing_texts = []
missing_indices = []
# 캐시 조회
for i, text in enumerate(texts):
if not self._should_embed(text):
results[i] = None
continue
doc_id = self._generate_doc_id(text, metadata)
existing = self.collection.get(ids=[doc_id])
if existing["ids"]:
results[i] = existing["embeddings"][0]
else:
missing_texts.append(text)
missing_indices.append(i)
# 미스된 텍스트만 API 호출
if missing_texts:
# HolySheep AI 배치 임베딩
response = client.embeddings.create(
model="text-embedding-3-small",
input=missing_texts
)
for idx, embedding_data in zip(missing_indices, response.data):
results[idx] = embedding_data.embedding
# 캐시 저장
doc_id = self._generate_doc_id(texts[idx], metadata)
self.collection.add(
ids=[doc_id],
embeddings=[embedding_data.embedding],
documents=[texts[idx]],
metadatas=[{
**metadata,
"created_at": datetime.now().isoformat()
}]
)
return results
def cleanup_old_entries(self, days: int = 30):
"""오래된 캐시 엔트리 정리"""
cutoff = datetime.now() - timedelta(days=days)
all_data = self.collection.get()
for i, metadata in enumerate(all_data["metadatas"]):
created = datetime.fromisoformat(metadata["created_at"])
if created < cutoff:
self.collection.delete(ids=[all_data["ids"][i]])
실제 사용 예시
async def rag_pipeline(document_id: str, query: str):
from openai import OpenAI
client = OpenAI(
api_key="YOUR_HOLYSHEEP_API_KEY",
base_url="https://api.holysheep.ai/v1"
)
cache = VectorCache("./production_vector_db")
# 1단계: 쿼리 임베딩 (캐시됨)
query_embedding = await cache.get_embedding(
text=query,
metadata={"type": "query", "doc_id": document_id},
client=client
)
# 2단계: 문서 검색
# ... (RAG 검색 로직)
# 3단계: 답변 생성
response = client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": f"Context: ...\nQuery: {query}"}]
)
return response.choices[0].message.content
실전 사용 사례 3: 개인 개발자 -TTL 기반 단순 캐싱
사이드 프로젝트나 MVP 단계에서는 복잡한 시스템보다 TTL(Time-To-Live) 기반 단순 캐싱이 효과적입니다. 제가 개인 블로그에 적용한 방법입니다.
import time
import json
from functools import wraps
from typing import Callable, Any, Optional
import hashlib
class SimpleTTLCache:
"""단순 TTL 기반 메모리 캐시 - 개인 프로젝트용"""
def __init__(self, default_ttl: int = 300): # 5분 기본 TTL
self._cache = {}
self._expiry = {}
self.default_ttl = default_ttl
self._hits = 0
self._misses = 0
def _make_key(self, *args, **kwargs) -> str:
"""캐시 키 생성"""
key_data = json.dumps({"args": args, "kwargs": kwargs}, sort_keys=True)
return hashlib.md5(key_data.encode()).hexdigest()
def get(self, key: str) -> Optional[Any]:
"""캐시 조회"""
if key in self._cache:
if time.time() < self._expiry.get(key, 0):
self._hits += 1
return self._cache[key]
else:
# 만료된 항목 삭제
del self._cache[key]
del self._expiry[key]
self._misses += 1
return None
def set(self, key: str, value: Any, ttl: Optional[int] = None):
"""캐시 저장"""
self._cache[key] = value
self._expiry[key] = time.time() + (ttl or self.default_ttl)
@property
def hit_rate(self) -> float:
"""히트율 계산"""
total = self._hits + self._misses
return (self._hits / total * 100) if total > 0 else 0
def clear_expired(self):
"""만료된 항목 일괄 삭제"""
now = time.time()
expired_keys = [k for k, exp in self._expiry.items() if now >= exp]
for key in expired_keys:
del self._cache[key]
del self._expiry[key]
return len(expired_keys)
def cached(ttl: int = 300, key_prefix: str = ""):
"""함수 데코레이터 기반 캐싱"""
cache = SimpleTTLCache(default_ttl=ttl)
def decorator(func: Callable) -> Callable:
@wraps(func)
async def async_wrapper(*args, **kwargs):
key = f"{key_prefix}:{func.__name__}:{cache._make_key(*args, **kwargs)}"
result = cache.get(key)
if result is not None:
print(f"✅ Cache HIT: {func.__name__}")
return result
print(f"⏳ Cache MISS: {func.__name__} - calling API")
result = await func(*args, **kwargs)
cache.set(key, result, ttl)
return result
@wraps(func)
def sync_wrapper(*args, **kwargs):
key = f"{key_prefix}:{func.__name__}:{cache._make_key(*args, **kwargs)}"
result = cache.get(key)
if result is not None:
print(f"✅ Cache HIT: {func.__name__}")
return result
print(f"⏳ Cache MISS: {func.__name__} - calling API")
result = func(*args, **kwargs)
cache.set(key, result, ttl)
return result
# 동기/비동기 함수를 자동으로 감지
import asyncio
if asyncio.iscoroutinefunction(func):
return async_wrapper
return sync_wrapper
return decorator
사용 예시
from openai import OpenAI
client = OpenAI(
api_key="YOUR_HOLYSHEEP_API_KEY",
base_url="https://api.holysheep.ai/v1"
)
블로그 포스트 요약 캐싱 (5분 TTL)
@cached(ttl=300, key_prefix="blog")
def summarize_post(post_content: str, style: str = "concise") -> str:
response = client.chat.completions.create(
model="gpt-4.1",
messages=[
{
"role": "system",
"content": f"당신은 블로그 포스트를 {style}하게 요약하는 전문가입니다."
},
{"role": "user", "content": f"다음 포스트를 요약해주세요:\n\n{post_content}"}
],
max_tokens=200
)
return response.choices[0].message.content
첫 호출: API 호출 발생
result1 = summarize_post("긴 블로그 포스트 내용...", "concise")
⏳ Cache MISS: summarize_post - calling API
5분 내 재호출: 캐시 히트
result2 = summarize_post("긴 블로그 포스트 내용...", "concise")
✅ Cache HIT: summarize_post
다른 파라미터: API 호출 발생
result3 = summarize_post("긴 블로그 포스트 내용...", "detailed")
⏳ Cache MISS: summarize_post - calling API
HolySheep AI 캐싱 최적화 아키텍처
제가 추천하는 프로덕션 환경의 캐싱 아키텍처는 다음과 같습니다:
# docker-compose.yml - 프로덕션 캐싱 인프라
version: '3.8'
services:
redis-primary:
image: redis:7-alpine
command: redis-server --appendonly yes --maxmemory 2gb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
redis-sentinel:
image: redis:7-alpine
command: redis-server --sentinel --sentinel-announce-ip redis-sentinel --sentinel-announce-port 26379
depends_on:
- redis-primary
ports:
- "26379:26379"
chromadb:
image: chromadb/chroma:latest
volumes:
- chroma-data:/chroma/chroma
ports:
- "8000:8000"
environment:
- IS_PERSISTENT=TRUE
- ANONYMIZED_TELEMETRY=FALSE
cache-manager:
build: .
environment:
- REDIS_URL=redis://redis-primary:6379
- CHROMA_URL=http://chromadb:8000
- HOLYSHEEP_API_KEY=${HOLYSHEEP_API_KEY}
depends_on:
- redis-primary
- chromadb
volumes:
- ./cache_manager:/app
volumes:
redis-data:
chroma-data:
# cache_manager/app.py - 중앙 캐시 관리자
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import redis.asyncio as redis
import httpx
import hashlib
import json
from datetime import datetime, timedelta
app = FastAPI(title="AI Cache Manager")
class CacheConfig(BaseModel):
ttl: int = 3600
cache_type: str = "exact" # exact, semantic, vector
model: str = "gpt-4.1"
class CacheStats(BaseModel):
total_requests: int
cache_hits: int
cache_misses: int
hit_rate: float
estimated_savings_usd: float
avg_latency_ms: float
class CacheManager:
def __init__(self, redis_url: str, holysheep_api_key: str):
self.redis = redis.from_url(redis_url)
self.api_key = holysheep_api_key
self.base_url = "https://api.holysheep.ai/v1"
# 가격 정보 (HolySheep AI)
self.pricing = {
"gpt-4.1": 8.0, # $8/MTok
"claude-sonnet-4-20250514": 15.0, # $15/MTok
"gemini-2.5-flash": 2.5, # $2.50/MTok
"deepseek-v3-0324": 0.42 # $0.42/MTok
}
async def get_completion(
self,
model: str,
messages: list,
config: CacheConfig
) -> dict:
"""캐시 적용된 API 호출"""
request_id = hashlib.sha256(
json.dumps({"model": model, "messages": messages}, sort_keys=True).encode()
).hexdigest()
cache_key = f"completion:{request_id}"
# 캐시 조회
cached = await self.redis.get(cache_key)
if cached:
await self.redis.hincrby("stats:hits", 1)
return {"response": json.loads(cached), "cached": True}
# API 호출
async with httpx.AsyncClient(timeout=30.0) as client:
start = datetime.now()
response = await client.post(
f"{self.base_url}/chat/completions",
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
},
json={
"model": model,
"messages": messages,
"max_tokens": 1000
}
)
latency = (datetime.now() - start).total_seconds() * 1000
if response.status_code == 200:
result = response.json()
# 캐시 저장
await self.redis.setex(
cache_key,
config.ttl,
json.dumps(result)
)
# 통계 업데이트
await self._update_stats(model, result, latency)
return {"response": result, "cached": False}
raise HTTPException(status_code=response.status_code, detail=response.text)
async def _update_stats(self, model: str, response: dict, latency_ms: float):
"""통계 업데이트 및 비용 절감 계산"""
usage = response.get("usage", {})
total_tokens = usage.get("total_tokens", 0)
price_per_million = self.pricing.get(model, 8.0)
cost_usd = (total_tokens / 1_000_000) * price_per_million
pipe = self.redis.pipeline()
pipe.hincrby("stats:total_requests", 1)
pipe.hincrbyfloat("stats:total_cost_usd", cost_usd)
pipe.hincrby("stats:total_latency_ms", int(latency_ms))
await pipe.execute()
async def get_stats(self) -> CacheStats:
"""캐시 성능 통계 조회"""
pipe = self.redis.pipeline()
pipe.hgetall("stats:hits")
pipe.hgetall("stats:total_requests")
pipe.get("stats:total_cost_usd")
results = await pipe.execute()
hits = sum(int(v) for v in results[0].values()) if results[0] else 0
total = sum(int(v) for v in results[1].values()) if results[1] else 0
cost = float(results[2] or 0)
# 캐싱 없을 때 예상 비용 (평균 3배 토큰 소모로 가정)
estimated_without_cache = cost * 3
return CacheStats(
total_requests=total,
cache_hits=hits,
cache_misses=total - hits,
hit_rate=(hits / total * 100) if total > 0 else 0,
estimated_savings_usd=estimated_without_cache - cost,
avg_latency_ms=0 # 실제 구현 시 계산
)
HolySheep AI 가입 후 API 키 설정
https://www.holysheep.ai/register
cache_manager = CacheManager(
redis_url="redis://redis-primary:6379",
holysheep_api_key="YOUR_HOLYSHEEP_API_KEY"
)
@app.post("/chat/completions")
async def chat_completions(model: str, messages: list, config: CacheConfig):
return await cache_manager.get_completion(model, messages, config)
@app.get("/stats")
async def get_cache_stats():
return await cache_manager.get_stats()
@app.delete("/cache/clear")
async def clear_cache():
await cache_manager.redis.flushdb()
return {"message": "Cache cleared"}
자주 발생하는 오류와 해결책
오류 1: 캐시 키 충돌로 인한 잘못된 응답 반환
# ❌ 잘못된 예: 불완전한 키 생성
def bad_cache_key(text: str) -> str:
return hashlib.md5(text.encode()).hexdigest() # 공백, 대소문자 무시
✅ 올바른 예: 정규화된 키 생성
def good_cache_key(text: str, metadata: dict = None) -> str:
normalized = " ".join(text.lower().split())
key_data = {"text": normalized}
if metadata:
key_data["meta"] = {k: str(v) for k, v in sorted(metadata.items())}
return hashlib.sha256(json.dumps(key_data, sort_keys=True).encode()).hexdigest()
해결 코드
class RobustCache:
def __init__(self):
self.redis = redis.from_url("redis://localhost:6379")
def make_key(self, prompt: str, model: str, temperature: float = 0.7, **kwargs) -> str:
"""모든 파라미터를 포함한 강력한 키 생성"""
key_parts = {
"prompt": prompt.strip().lower(),
"model": model,
"temp": round(temperature, 2), # 소수점 통일
**kwargs
}
key_string = json.dumps(key_parts, sort_keys=True, separators=(',', ':'))
return f"rcache:{hashlib.sha256(key_string.encode()).hexdigest()[:24]}"
async def get_or_fetch(self, prompt: str, model: str, **params):
key = self.make_key(prompt, model, **params)
cached = self.redis.get(key)
if cached:
return json.loads(cached), True
# API 호출
result = await self._call_api(prompt, model, **params)
self.redis.setex(key, 3600, json.dumps(result))
return result, False
오류 2: Redis 메모리 부족으로 인한 캐시 데이터 손실
# ❌ 잘못된 예: 메모리 관리 없음
cache = redis.Redis()
캐시가 계속 쌓여 OOM 발생
✅ 올바른 예: LRU 정책과 메모리 모니터링
class MemoryAwareCache:
def __init__(self, max_memory_mb: int = 512):
self.redis = redis.Redis()
self.max_memory = max_memory_mb * 1024 * 1024
self._configure()
def _configure(self):
"""Redis 메모리 최적화 설정"""
self.redis.config_set("maxmemory", self.max_memory)
self.redis.config_set("maxmemory-policy", "allkeys-lru")
self.redis.config_set("maxmemory-samples", "5") # 샘플링 최적화
async def safe_set(self, key: str, value: str, ttl: int):
"""메모리 체크 후 저장"""
# 메모리 사용량 체크
info = self.redis.info("memory")
used_memory = info.get("used_memory", 0)
if used_memory > self.max_memory * 0.9: # 90% 이상 시
# 오래된 데이터 먼저 삭제
await self._aggressive_cleanup()
self.redis.setex(key, ttl, value)
async def _aggressive_cleanup(self):
"""메모리 확보를 위한 агрес시브 클린업"""
keys = self.redis.keys("*")
# 접근 빈도 기반 정렬 (최소 사용 삭제)
for key in keys[:len(keys)//4]: # 25% 삭제
self.redis.delete(key)
print("⚠️ Memory cleanup performed")
오류 3: 비동기 캐시 접근 시 데이터 레이스 컨디션
# ❌ 잘못된 예: 동시 접근 시 중복 API 호출
async def bad_dual_access(prompt: str) -> str:
cached = await redis.get(prompt)
if cached:
return cached
# 두 요청이 동시에 여기로 진입 가능!
result = await api_call(prompt)
await redis.set(prompt, result)
return result
✅ 올바른 예: 분산 락으로 동시성 제어
import asyncio
class LockedCache:
def __init__(self):
self.redis = redis.Redis()
self.lock_timeout = 10
async def get_or_compute(self, key: str, compute_fn):
"""분산 락 기반 동시성 안전 캐시 조회"""
lock_key = f"lock:{key}"
# 1단계: 캐시 확인
cached = await self.redis.get(key)
if cached:
return cached.decode(), True
# 2단계: 락 획득 시도
lock_acquired = await self.redis.set(
lock_key, "1", nx=True, ex=self.lock_timeout
)
if not lock_acquired:
# 다른 요청이 이미 처리 중 - 대기 후 재확인
for _ in range(10):
await asyncio.sleep(0.5)
cached = await self.redis.get(key)
if cached:
return cached.decode(), True
raise TimeoutError(f"Lock timeout for key: {key}")
try:
# 3단계: API 호출
result = await compute_fn()
await self.redis.setex(key, 3600, result)
return result, False
finally:
# 4단계: 락 해제
await self.redis.delete(lock_key)
사용 예시
cache = LockedCache()
async def api_call(prompt: str) -> str:
client = OpenAI(
api_key="YOUR_HOLYSHEEP_API_KEY",
base_url="https://api.holysheep.ai/v1"
)
response = client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
동시 100개 요청이 와도 API 호출은 1회만 발생
results = await asyncio.gather(*[
cache.get_or_compute("same_prompt", lambda: api_call("same_prompt"))
for _ in range(100)
])
오류 4: 벡터 캐시 유사도 검색 성능 저하
# ❌ 잘못된 예: 전체 벡터 순차 비교 (느림)
def bad_similarity_search(query_vector: list, all_vectors: list) -> list:
results = []
for vec in all_vectors:
similarity = cosine_similarity([query_vector], [vec])[0][0]
results.append((similarity, vec))
return sorted(results, reverse=True)[:5] # O(n) 복잡도
✅ 올바른 예: HNSW 인덱싱 활용
class OptimizedVectorCache:
def __init__(self, collection_name: str = "embeddings"):
self.client = chromadb.PersistentClient(path="./chroma")
self.collection = self.client.get_or_create_collection(
name=collection_name,
metadata={
"hnsw:space": "cosine",
"hnsw:construction_ef": 100, # 인덱스 품질
"hnsw:search_ef": 100, # 검색 품질
"hnsw:M": 16 # 메모리-속도 트레이드오프
}
)
async def semantic_search(
self,
query_vector: list,
top_k: int = 5,
threshold: float = 0.85
) -> list:
"""HNSW 인덱스를 활용한 고속 유사도 검색"""
results = self.collection.query(
query_embeddings=[query_vector],
n_results=top_k,
include=["distances", "metadatas"]
)
# 코사인 유사도로 변환 (ChromaDB는 거리를 반환)
filtered = []
for distance, metadata in zip(
results["distances"][0],
results["metadatas"][0]
):
similarity = 1 - distance # 거리 -> 유사도로 변환
if similarity >= threshold:
filtered.append({
"similarity": similarity,
"metadata": metadata
})
return filtered
배치 최적화
class BatchVectorCache:
async def batch_embed_with_cache(self, texts: list, client) -> list:
"""배치 처리로 API 호출 최소화"""
# 1단계: 병렬 캐시 조회
tasks = [self.get_cached(text) for text in texts]
cached_results = await asyncio.gather(*tasks)
# 2단계: 미스된 텍스트만 추출
missing_indices = [
i for i, result in enumerate(cached_results)
if result is None
]
missing_texts = [texts[i] for i in missing_indices]
# 3단계: 배치 API 호출 (1회)
if missing_texts:
embeddings = await self.batch_create_embeddings(missing_texts, client)
# 캐시 저장
for idx, emb in zip(missing_indices, embeddings):
await self.cache(texts[idx], emb)
# 4단계: 결과 조합
return [
cached_results[i] or embeddings[missing_indices.index(i)]
if i in missing_indices else cached_results[i]
for i in range(len(texts))
]
캐싱 전략 선택 가이드
| 프로젝트 규모 | 추천 전략 | 예상 비용 절감 | 구현 난이도 |
|---|---|---|---|
| 개인/MVP | 메모리 TTL 캐시 | 40~60% | ⭐ |
| 스타트업 | Redis TTL 캐시 | 60~80% | ⭐⭐ |
| 중기업 | Redis + 의미론적 캐시 | 70~85% | ⭐⭐⭐ |
| 대기업 | 다층 캐시 + 벡터 DB
관련 리소스관련 문서 |