Семантический поиск с Qdrant: как увеличить конверсию поиска на 50%

Семантический поиск с Qdrant: как увеличить конверсию поиска на 50%


Проблема: классический поиск не понимает смысл

Клиент ищет «кроссовки для бега», а ваш классический поиск (Elasticsearch, PostgreSQL full-text):

  • Ищет точное совпадение слов "кроссовки" И "бега"
  • Не найдет товар с названием «Беговые кеды Nike» (синонимы)
  • Не найдет «Обувь для спорта» (гипонимы)
  • Не учтет опечатку «кросовки» (typo)

Результат: клиент ничего не нашел → уходит к конкурентам.

Статистика по e-commerce

  • 25-35% запросов дают нулевые результаты при точном поиске
  • 60% пользователей уходят, если не нашли товар с первого раза
  • 15-20% падение конверсии из-за плохого поиска

Решение: семантический поиск с векторами

Векторный поиск = поиск по смыслу, а не по ключевым словам:

  1. Каждый товар → вектор (embedding) 384-1024 чисел
  2. Запрос пользователя → тоже вектор
  3. Находим похожие векторы (косинусное расстояние)

Пример:

"кроссовки для бега" → [0.23, -0.45, 0.67, ...]
"беговая обувь"      → [0.21, -0.43, 0.69, ...]  ← похоже!
"зимняя куртка"      → [-0.82, 0.15, -0.34, ...] ← не похоже

Кейс: маркетплейс стройматериалов (150 000 товаров)

Исходная ситуация:

  • Elasticsearch для поиска (keyword matching)
  • 28% запросов дают нулевые результаты
  • Конверсия поиска: 8%
  • Клиенты жалуются: «У вас нет нужного товара» (а он есть!)

Что внедрили

1. Создали embeddings для всех товаров

from sentence_transformers import SentenceTransformer
import pandas as pd

# Модель для создания векторов
model = SentenceTransformer('intfloat/multilingual-e5-large')

# Загружаем товары
products = pd.read_csv('products.csv')

# Создаем текст для векторизации
products['search_text'] = (
    products['name'] + ' ' +
    products['category'] + ' ' +
    products['brand'] + ' ' +
    products['description'].fillna('')
)

# Генерируем embeddings
embeddings = model.encode(
    products['search_text'].tolist(),
    batch_size=32,
    show_progress_bar=True
)

print(f"Shape: {embeddings.shape}")  # (150000, 1024)

2. Загрузили в Qdrant

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

client = QdrantClient(url="http://localhost:6333")

# Создаем коллекцию
client.create_collection(
    collection_name="products",
    vectors_config=VectorParams(
        size=1024,  # размерность вектора
        distance=Distance.COSINE  # метрика похожести
    )
)

# Загружаем данные
points = [
    PointStruct(
        id=row['id'],
        vector=embeddings[idx].tolist(),
        payload={
            'name': row['name'],
            'category': row['category'],
            'brand': row['brand'],
            'price': row['price'],
            'in_stock': row['in_stock']
        }
    )
    for idx, row in products.iterrows()
]

client.upload_points(
    collection_name="products",
    points=points,
    batch_size=100
)

3. Реализовали API для поиска

from fastapi import FastAPI, Query
from typing import List, Optional

app = FastAPI()

@app.get("/search")
def search(
    query: str = Query(..., min_length=2),
    category: Optional[str] = None,
    min_price: Optional[float] = None,
    max_price: Optional[float] = None,
    in_stock_only: bool = False,
    limit: int = 20
):
    # 1. Векторизируем запрос
    query_vector = model.encode([query])[0].tolist()

    # 2. Фильтры
    filters = {}
    if category:
        filters['category'] = category
    if in_stock_only:
        filters['in_stock'] = True
    if min_price or max_price:
        filters['price'] = {
            "gte": min_price if min_price else 0,
            "lte": max_price if max_price else 1e9
        }

    # 3. Поиск в Qdrant
    search_result = client.search(
        collection_name="products",
        query_vector=query_vector,
        query_filter=filters if filters else None,
        limit=limit
    )

    # 4. Форматируем результат
    results = [
        {
            "id": hit.id,
            "name": hit.payload['name'],
            "category": hit.payload['category'],
            "price": hit.payload['price'],
            "score": hit.score  # похожесть 0..1
        }
        for hit in search_result
    ]

    return {"query": query, "results": results}

Результаты

Примеры улучшений

Запрос: “саморез по дереву”

До (Elasticsearch):

  • Нашло: 23 товара (только с точным совпадением “саморез” + “дерево”)
  • Пропустило: “Шуруп для древесины”, “Крепеж деревянных конструкций”

После (Qdrant):

  • Нашло: 87 товаров (с учетом синонимов и смысла)
  • Включает: саморезы, шурупы, крепеж для дерева

Запрос: “гипсокартон влагостойкий”

До: 5 результатов (только с точным “влагостойкий”)

После: 18 результатов (+ “влагоустойчивый”, “для влажных помещений”, “ГКЛВ”)

Метрики

МетрикаДоПослеИзменение
Нулевые результаты28%3%-89%
Конверсия поиска8%23%+187%
Средний чек3 200 ₽3 680 ₽+15%
Выручка из поиска1.2 млн ₽/мес2.1 млн ₽/мес+75%

ROI проекта: 450% за первые 6 месяцев

Гибридный поиск: векторы + BM25

Для максимальной релевантности комбинируем:

  • Vector search (семантика, синонимы)
  • BM25 (точное совпадение ключевых слов, артикулов)
  • Бизнес-правила (бустим товары в наличии, популярные)

Реализация

def hybrid_search(query: str, limit: int = 20):
    # 1. Векторный поиск
    vector_results = qdrant_search(query, limit=50)

    # 2. BM25 (full-text search)
    bm25_results = elasticsearch_search(query, limit=50)

    # 3. Объединяем и ранжируем
    combined = {}

    for result in vector_results:
        combined[result['id']] = {
            'item': result,
            'vector_score': result['score'] * 0.6,  # вес 60%
            'bm25_score': 0
        }

    for result in bm25_results:
        if result['id'] in combined:
            combined[result['id']]['bm25_score'] = result['score'] * 0.3  # вес 30%
        else:
            combined[result['id']] = {
                'item': result,
                'vector_score': 0,
                'bm25_score': result['score'] * 0.3
            }

    # 4. Бизнес-правила (+10% если в наличии)
    for item_id, data in combined.items():
        if data['item']['in_stock']:
            data['business_boost'] = 0.1
        else:
            data['business_boost'] = 0

    # 5. Финальный скор
    for item_id, data in combined.items():
        data['final_score'] = (
            data['vector_score'] +
            data['bm25_score'] +
            data['business_boost']
        )

    # 6. Сортируем и возвращаем топ
    sorted_results = sorted(
        combined.values(),
        key=lambda x: x['final_score'],
        reverse=True
    )[:limit]

    return [item['item'] for item in sorted_results]

Продвинутые фишки

1. Поиск похожих товаров

@app.get("/similar/{product_id}")
def find_similar(product_id: int, limit: int = 10):
    # Получаем вектор товара
    product_vector = client.retrieve(
        collection_name="products",
        ids=[product_id]
    )[0].vector

    # Ищем похожие
    similar = client.search(
        collection_name="products",
        query_vector=product_vector,
        limit=limit + 1  # +1 т.к. первый результат = сам товар
    )[1:]  # пропускаем первый

    return {"similar_products": [hit.payload for hit in similar]}

2. Поиск дубликатов в каталоге

def find_duplicates(threshold: float = 0.95):
    """Находит товары-дубликаты по семантической похожести"""

    products = client.scroll(collection_name="products", limit=10000)[0]

    duplicates = []

    for i, product in enumerate(products):
        similar = client.search(
            collection_name="products",
            query_vector=product.vector,
            limit=5
        )[1:]  # Skip self

        for match in similar:
            if match.score >= threshold:
                duplicates.append({
                    'product_1': product.payload['name'],
                    'product_2': match.payload['name'],
                    'similarity': match.score
                })

    return duplicates

3. Кластеризация товаров

from sklearn.cluster import KMeans
import numpy as np

# Выгружаем все векторы
products = client.scroll(collection_name="products", limit=100000)[0]
vectors = np.array([p.vector for p in products])

# Кластеризуем
kmeans = KMeans(n_clusters=50, random_state=42)
clusters = kmeans.fit_predict(vectors)

# Добавляем cluster_id в payload
for product, cluster_id in zip(products, clusters):
    client.set_payload(
        collection_name="products",
        payload={"cluster_id": int(cluster_id)},
        points=[product.id]
    )

Оптимизация производительности

1. Квантизация векторов

from qdrant_client.models import QuantizationConfig, ScalarQuantization

# Уменьшаем размер векторов float32 → uint8
client.update_collection(
    collection_name="products",
    quantization_config=ScalarQuantization(
        scalar=ScalarQuantizationConfig(
            type="int8",
            always_ram=True
        )
    )
)

# Результат: в 4× меньше памяти, скорость почти не падает

2. HNSW параметры

from qdrant_client.models import HnswConfigDiff

# Настройка индекса для баланса скорость/точность
client.update_collection(
    collection_name="products",
    hnsw_config=HnswConfigDiff(
        m=16,  # Connections per node (больше = точнее, но медленнее)
        ef_construct=100  # Search depth during indexing
    )
)

# Для поиска
search_params = {"hnsw_ef": 128}  # Search depth (больше = точнее)

3. Sharding для больших каталогов

# docker-compose.yml для кластера Qdrant
services:
  qdrant-node-1:
    image: qdrant/qdrant:latest
    ports:
      - "6333:6333"
    environment:
      QDRANT__CLUSTER__ENABLED: true
      QDRANT__CLUSTER__NODE_ID: 1

  qdrant-node-2:
    image: qdrant/qdrant:latest
    ports:
      - "6334:6333"
    environment:
      QDRANT__CLUSTER__ENABLED: true
      QDRANT__CLUSTER__NODE_ID: 2

Технический стек

  • Vector DB: Qdrant
  • Embeddings: sentence-transformers (multilingual-e5-large)
  • Backend: FastAPI, Python
  • Full-text search: Elasticsearch (для гибридного поиска)
  • Monitoring: Grafana + Prometheus
  • Deploy: Docker + Kubernetes

Стоимость

Сервер для 1 млн товаров

КомпонентРесурсыСтоимость
Qdrant16 GB RAM, 8 CPU, 100 GB SSD8 000 ₽/мес
API сервер (FastAPI)8 GB RAM, 4 CPU4 000 ₽/мес
Elasticsearch (опционально)16 GB RAM, 8 CPU8 000 ₽/мес
Итого12 000 - 20 000 ₽/мес

ROI: Окупается за 2-4 месяца при обороте от 3 млн ₽/мес.

Внедрение: пошаговый план

Этап 1: PoC (1 неделя)

  1. Выбираем 1 000 топовых товаров
  2. Генерируем embeddings
  3. Разворачиваем Qdrant локально
  4. Тестируем на реальных запросах

Стоимость: от 50 000 ₽

Этап 2: Production (2-3 недели)

  1. Масштабируем на весь каталог
  2. Настраиваем API
  3. Интегрируем с сайтом (замена поисковой формы)
  4. A/B тестирование

Стоимость: от 150 000 ₽

Этап 3: Оптимизация (1-2 недели)

  1. Гибридный поиск (vector + BM25)
  2. Добавляем фильтры, фасеты
  3. Настраиваем мониторинг
  4. Обучаем команду

Стоимость: от 80 000 ₽

Итого под ключ: от 280 000 ₽

Частые вопросы

Q: Можно просто использовать ChatGPT для поиска?

A: ChatGPT медленный (2-5 сек) и дорогой ($0.03 за запрос). Qdrant — < 50 мс и $0.001 за запрос.


Q: Какая модель лучше для русского языка?

A:

  • multilingual-e5-large (1024d) — универсальная, хорошее качество
  • LaBSE (768d) — специально для мультиязычности
  • rubert-tiny (312d) — быстрая, для простых задач

Q: Сколько памяти нужно для 1 млн товаров?

A: Зависит от размерности:

  • 384d × 1M × 4 bytes = 1.5 ГБ
  • 1024d × 1M × 4 bytes = 4 ГБ

С квантизацией (int8) — в 4× меньше.


Q: Можно обновлять индекс в реальном времени?

A: Да! Qdrant поддерживает real-time CRUD:

# Добавить новый товар
client.upsert(collection_name="products", points=[new_product])

# Обновить вектор
client.update_vectors(collection_name="products", points=[updated_product])

# Удалить
client.delete(collection_name="products", points_selector=[product_id])

Заключение

Семантический поиск с Qdrant:

  • Конверсия +30-50% за счет понимания смысла запросов
  • Нулевые результаты -80-90% (синонимы, опечатки, разные формулировки)
  • Средний чек +10-20% благодаря блоку “Похожие товары”
  • < 50 мс на запрос (подходит для production)

Подходит для каталогов от 1 000 товаров.


Хотите семантический поиск для вашего каталога?

Мы внедряем vector search на Qdrant, обучаем embedding-модели под ваш домен, интегрируем с сайтом. Под ключ за 3-6 недель.

📞 +7 (924) 547-36-78 📧 info@bi-ai.ru 💬 Telegram: @bi_ai_team