검색 서비스 아키텍처: Valkey를 활용한 독립 실행형 Flask 애플리케이션
이 글에서는 우리의 검색 서비스가 별도의 서버에서 독립 실행형 Flask 애플리케이션으로 운영되는 방식을 설명합니다. 고성능 벡터 검색, 캐싱 및 자동완성을 위해 Valkey (Redis 포크)를 사용합니다.
문제: 검색 성능과 확장성
검색 연산은 계산 비용이 많이 듭니다:
-
필터 추출: 모든 쿼리에 대해 2,500개 이상의 구문을 매칭
-
관련 검색어: 65K개의 쿼리 간 유사도 계산
-
자동완성: 65K개의 쿼리에 대한 접두사 매칭
-
제품 필터링: 5K개 이상의 제품을 다중 기준으로 필터링
이러한 연산을 메인 웹 서버에서 실행하면 다음과 같은 문제가 발생합니다:
-
페이지 로딩 지연: 검색이 다른 요청을 블로킹
-
메모리 부담: 임베딩이 막대한 메모리 공간을 차지
-
CPU 경합: 유사도 계산은 CPU 집약적
-
확장 어려움: 검색을 독립적으로 확장할 수 없음
독립적으로 확장 가능한 전용 검색 서비스가 필요합니다.
해결책: 독립 실행형 검색 서비스
전용 서버에서 별도의 Flask 애플리케이션을 실행합니다:
메인 웹 서버
↓ HTTP API 호출
검색 서비스
↓ Valkey 쿼리
Valkey 서버
이 아키텍처는 다음을 제공합니다:
-
독립적 확장: 메인 웹 서버에 영향을 주지 않고 검색 서비스 확장 가능
-
자원 격리: 검색 연산이 메인 웹 서버에 영향을 미치지 않음
-
캐싱: Valkey가 결과를 캐싱하여 반복 쿼리 처리 속도 향상
-
고가용성: 검색 서비스 재시작이 메인 웹 서버에 영향을 미치지 않음
검색 서비스 구성 요소
1. 필터 추출 API
-
엔드포인트:
/api/extract_filters -
목적: 자연어 쿼리에서 구조화된 필터 추출
-
예시:
GET /api/extract_filters?q=mini+pc+16gb+ram
응답:
{
"Form Factor": "Mini PC",
"Main Memory": "16"
}
구현:
캐싱: 구문 매핑은 Valkey에 캐싱 (30일 TTL)
2. 관련 검색어 API
-
엔드포인트:
/api/related -
목적: 벡터 검색을 사용하여 의미론적으로 유사한 쿼리 찾기
-
예시:
POST /api/related
{
"query": "mini pc",
"limit": 10
}
응답:
{
"related": [
{"query": "small computer", "similarity": 0.92},
{"query": "compact desktop", "similarity": 0.89},
{"query": "mini pc 8gb", "similarity": 0.87}
]
}
구현:
-
all-mpnet-base-v2를 사용하여 쿼리 임베딩
-
Valkey RediSearch에 최근접 이웃 쿼리
-
유사도 기준 상위 N개 결과 정렬 반환
캐싱: 결과는 Valkey에 캐싱 (7일 TTL)
3. 자동완성 API
-
엔드포인트:
/api/autocomplete -
목적: 사용자 입력에 따라 쿼리 제안
-
예시:
GET /api/autocomplete?q=mini+p&limit=5
응답:
{
"suggestions": [
"mini pc",
"mini pc 16gb",
"mini pc 8gb ram",
"mini pc fanless",
"mini pc windows 11"
]
}
구현:
-
접두사 매칭으로 Valkey RediSearch 쿼리
-
인기도(노출 + 클릭 점수) 기준 순위 매기기
-
상위 N개 제안 반환
캐싱: 자동완성 인덱스는 Valkey에 저장 (일일 업데이트)
4. 인기 검색어 API
-
엔드포인트:
/api/popular -
목적: 가장 인기 있는 검색어 가져오기
-
예시:
GET /api/popular?limit=10
응답:
{
"queries": [
"mini pc",
"thin client",
"industrial pc",
"all in one pc"
]
}
구현:
-
Valkey 또는 JSON에서 쿼리 로드
-
트래픽 점수(노출수 + 클릭수) 기준 정렬
-
상위 N개 쿼리 반환
캐싱: 인기 검색어는 Valkey에 캐싱 (30일 TTL)
Valkey 통합
Valkey는 Redis 포크로 다음을 제공합니다:
-
벡터 검색: 유사도 검색을 위한 RediSearch 모듈
-
캐싱: 고속 메모리 내 키-값 저장소
-
자동완성: 정렬된 집합(Sorted Set)을 이용한 접두사 매칭
-
지속성: 내구성을 위한 AOF (Append-Only File)
RediSearch를 이용한 벡터 검색
벡터 유사도 검색을 위해 Valkey의 RediSearch 모듈을 사용합니다:
인덱스 생성:
client.ft("queries_idx").create_index([
VectorField("embedding", "FLAT", {
"TYPE": "FLOAT32",
"DIM": 768,
"DISTANCE_METRIC": "COSINE"
}),
TextField("query"),
NumericField("score")
])
벡터 검색:
query_embedding = model.encode(query)
results = client.ft("queries_idx").search(
Query("*=>[KNN 10 @embedding $vec AS score]")
.sort_by("score")
.return_fields("query", "score")
.dialect(2),
query_params={"vec": query_embedding.tobytes()}
)
이를 통해 코사인 유사도 기준 10개의 최근접 이웃을 반환합니다.
캐싱 전략
다양한 데이터 유형을 Valkey에 캐싱합니다:
구문 매핑 (30일 TTL):
client.setex(
"seo:phrase_mappings",
30 * 24 * 3600,
json.dumps(phrase_mappings)
)
관련 검색어 (7일 TTL):
cache_key = f"related:{query_hash}"
client.setex(cache_key, 7 * 24 * 3600, json.dumps(results))
인기 검색어 (30일 TTL):
client.setex(
"seo:popular_queries",
30 * 24 * 3600,
json.dumps(popular_queries)
)
자동완성 인덱스 (일일 업데이트):
for query, score in queries:
client.zadd("autocomplete:mini", {query: score})
정렬된 집합(Sorted Set)을 이용한 자동완성
자동완성을 위해 Valkey의 정렬된 집합을 사용합니다:
인덱스 구조:
autocomplete:m → ["mini pc": 5000, "mini computer": 3000]
autocomplete:mi → ["mini pc": 5000, "mini computer": 3000]
autocomplete:min → ["mini pc": 5000, "mini computer": 3000]
autocomplete:mini → ["mini pc": 5000, "mini computer": 3000]
접두사 조회:
prefix = "mini"
results = client.zrevrange(f"autocomplete:{prefix}", 0, 9, withscores=True)
이를 통해 "mini"로 시작하는 상위 10개 쿼리를 점수 기준으로 정렬하여 반환합니다.
API 통신
메인 웹 서버는 HTTP를 통해 검색 서비스를 호출합니다:
필터 추출
from app.shared.filter_service import extract_filters_from_query
filters = extract_filters_from_query("mini pc 16gb ram")
# 내부적으로 호출: GET SEARCH_SERVICE_URL/api/extract_filters?q=...
관련 검색어
import requests
response = requests.post(
"SEARCH_SERVICE_URL/api/related",
json={"query": "mini pc", "limit": 10},
timeout=2
)
related = response.json()["related"]
자동완성
response = requests.get(
"SEARCH_SERVICE_URL/api/autocomplete",
params={"q": "mini p", "limit": 5},
timeout=1
)
suggestions = response.json()["suggestions"]
오류 처리 및 대체 수단
메인 웹 서버는 검색 서비스 장애를 우아하게 처리합니다:
try:
filters = extract_filters_from_query(query)
except Exception as e:
logger.error(f"Search service failed: {e}")
filters = {} # 빈 필터로 대체
이를 통해 검색 서비스가 다운되어도 메인 웹 서버가 계속 작동할 수 있습니다.
### 네트워크 구성
모든 서버는 사설 네트워크에 있습니다:
- **메인 웹 서버**: 검색 서비스 및 Valkey에 접근 가능
- **검색 서비스**: Valkey에 접근 가능
- **Valkey**: 메인 웹 서버 및 검색 서비스에서만 접근 가능
검색 서비스나 Valkey에는 외부 접근이 불가능합니다.
## SEO 파이프라인과의 통합
검색 서비스는 SEO 파이프라인과 통합됩니다:
### 11단계: Valkey로 마이그레이션
SEO 파이프라인은 데이터를 Valkey에 로드합니다:
```python
# 쿼리 임베딩 로드
for query, embedding in zip(queries, embeddings):
client.hset(f"query:{query_hash}", mapping={
"query": query,
"embedding": embedding.tobytes(),
"score": score
})
# RediSearch 인덱스 생성
client.ft("queries_idx").create_index([...])
자세한 내용은 Valkey 마이그레이션을 참조하세요.
쿼리 로깅
검색 서비스는 SEO 파이프라인을 위해 쿼리를 로깅합니다:
log_entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"query": query,
"filters_extracted": filters,
"results_count": len(results)
}
with open(SEO_LIVE_QUERIES_LOG, "a") as f:
f.write(json.dumps(log_entry) + "\n")
이 로그는 1d단계: 라이브 쿼리 가져오기로 다시 피드백됩니다.
참고 자료
기술 개념
-
Valkey - 공식 웹사이트
-
Redis - 공식 웹사이트 (Valkey 포크)
-
RediSearch - 벡터 검색 모듈
-
코사인 유사도 - 위키백과
관련 글
-
필터 추출 - 필터가 추출되는 방식
-
관련 검색어 생성 - 관련 검색어가 생성되는 방식
-
임베딩 전략 - 임베딩이 생성되는 방식
-
SEO 파이프라인 개요 - 전체 파이프라인 아키텍처
-
Valkey 마이그레이션 - Valkey에 데이터 로드하기
요약
우리의 검색 서비스는 별도의 서버에서 독립 실행형 Flask 애플리케이션으로 운영됩니다:
아키텍처:
-
전용 서버의 독립 실행형 Flask 앱
-
캐싱 및 벡터 검색을 위한 Valkey (Redis 포크)
-
메인 웹 서버와의 통신을 위한 HTTP API
API:
-
/api/extract_filters- 쿼리에서 필터 추출 -
/api/related- 유사한 쿼리 찾기 (벡터 검색) -
/api/autocomplete- 쿼리 제안 (접두사 매칭) -
/api/popular- 인기 검색어 가져오기
Valkey 기능:
-
벡터 검색 (RediSearch 모듈)
-
캐싱 (구문 매핑 30일 TTL)
-
자동완성 (정렬된 집합)
-
지속성 (AOF)
이점:
-
독립적 확장
-
자원 격리
-
고성능 (Valkey 캐싱)
-
내결함성 (우아한 성능 저하)
이 아키텍처는 메인 웹 서버의 응답성을 유지하면서 빠르고 확장 가능한 검색을 가능하게 합니다.