검색 서비스 아키텍처: 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단계: 라이브 쿼리 가져오기로 다시 피드백됩니다.

참고 자료

기술 개념

관련 글

요약

우리의 검색 서비스는 별도의 서버에서 독립 실행형 Flask 애플리케이션으로 운영됩니다:

아키텍처:

  • 전용 서버의 독립 실행형 Flask 앱

  • 캐싱 및 벡터 검색을 위한 Valkey (Redis 포크)

  • 메인 웹 서버와의 통신을 위한 HTTP API

API:

  • /api/extract_filters - 쿼리에서 필터 추출

  • /api/related - 유사한 쿼리 찾기 (벡터 검색)

  • /api/autocomplete - 쿼리 제안 (접두사 매칭)

  • /api/popular - 인기 검색어 가져오기

Valkey 기능:

  • 벡터 검색 (RediSearch 모듈)

  • 캐싱 (구문 매핑 30일 TTL)

  • 자동완성 (정렬된 집합)

  • 지속성 (AOF)

이점:

  • 독립적 확장

  • 자원 격리

  • 고성능 (Valkey 캐싱)

  • 내결함성 (우아한 성능 저하)

이 아키텍처는 메인 웹 서버의 응답성을 유지하면서 빠르고 확장 가능한 검색을 가능하게 합니다.


← 문서 인덱스로 돌아가기