Архитектура поискового сервиса: Автономное Flask-приложение с Valkey

В этой статье объясняется, как наш поисковый сервис работает как автономное Flask-приложение на отдельном сервере, используя Valkey (форк Redis) для высокопроизводительного векторного поиска, кэширования и автодополнения.

Проблема: Производительность и масштабируемость поиска

Поисковые операции требуют больших вычислительных ресурсов:

  • Извлечение фильтров: Сопоставление 2500+ фраз с каждым запросом

  • Похожие запросы: Вычисление сходства по 65К запросам

  • Автодополнение: Префиксный поиск по 65К запросам

  • Фильтрация товаров: Фильтрация 5К+ товаров по множеству критериев

Выполнение этих операций на основном веб-сервере приводит к:

  • Медленной загрузке страниц: Поиск блокирует другие запросы

  • Нагрузке на память: Эмбеддинги занимают огромный объем памяти

  • Конкуренции за 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 (TTL 30 дней)

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 (TTL 7 дней)

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 (TTL 30 дней)

Интеграция с Valkey

Valkey — это форк Redis, который предоставляет:

  • Векторный поиск: Модуль RediSearch для поиска по сходству

  • Кэширование: Быстрое хранилище ключ-значение в памяти

  • Автодополнение: Префиксный поиск с отсортированными множествами

  • Сохранность данных: AOF (Append-Only File) для устойчивости

Векторный поиск с RediSearch

Мы используем модуль RediSearch Valkey для векторного поиска по сходству:

Создание индекса:

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:

Соответствия фраз (TTL 30 дней):

client.setex(
    "seo:phrase_mappings",
    30 * 24 * 3600,
    json.dumps(phrase_mappings)
)

Похожие запросы (TTL 7 дней):

cache_key = f"related:{query_hash}"
client.setex(cache_key, 7 * 24 * 3600, json.dumps(results))

Популярные запросы (TTL 30 дней):

client.setex(
    "seo:popular_queries",
    30 * 24 * 3600,
    json.dumps(popular_queries)
)

Индекс автодополнения (обновляется ежедневно):

for query, score in queries:
    client.zadd("autocomplete:mini", {query: score})

Автодополнение с отсортированными множествами

Мы используем отсортированные множества 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)

Это возвращает топ-10 запросов, начинающихся с "mini", отсортированных по оценке.

Взаимодействие через 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)

  • Кэширование (TTL 30 дней для соответствий фраз)

  • Автодополнение (отсортированные множества)

  • Сохранность данных (AOF)

Преимущества:

  • Независимое масштабирование

  • Изоляция ресурсов

  • Высокая производительность (кэширование Valkey)

  • Отказоустойчивость (плавная деградация)

Эта архитектура обеспечивает быстрый, масштабируемый поиск, сохраняя при этом отзывчивость основного веб-сервера.


← Назад к указателю документации