Семантический поиск с Qdrant: как увеличить конверсию поиска на 50%
Проблема: классический поиск не понимает смысл
Клиент ищет «кроссовки для бега», а ваш классический поиск (Elasticsearch, PostgreSQL full-text):
- Ищет точное совпадение слов
"кроссовки"И"бега" - Не найдет товар с названием «Беговые кеды Nike» (синонимы)
- Не найдет «Обувь для спорта» (гипонимы)
- Не учтет опечатку «кросовки» (typo)
Результат: клиент ничего не нашел → уходит к конкурентам.
Статистика по e-commerce
- 25-35% запросов дают нулевые результаты при точном поиске
- 60% пользователей уходят, если не нашли товар с первого раза
- 15-20% падение конверсии из-за плохого поиска
Решение: семантический поиск с векторами
Векторный поиск = поиск по смыслу, а не по ключевым словам:
- Каждый товар → вектор (embedding) 384-1024 чисел
- Запрос пользователя → тоже вектор
- Находим похожие векторы (косинусное расстояние)
Пример:
"кроссовки для бега" → [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 млн товаров
| Компонент | Ресурсы | Стоимость |
|---|---|---|
| Qdrant | 16 GB RAM, 8 CPU, 100 GB SSD | 8 000 ₽/мес |
| API сервер (FastAPI) | 8 GB RAM, 4 CPU | 4 000 ₽/мес |
| Elasticsearch (опционально) | 16 GB RAM, 8 CPU | 8 000 ₽/мес |
| Итого | 12 000 - 20 000 ₽/мес |
ROI: Окупается за 2-4 месяца при обороте от 3 млн ₽/мес.
Внедрение: пошаговый план
Этап 1: PoC (1 неделя)
- Выбираем 1 000 топовых товаров
- Генерируем embeddings
- Разворачиваем Qdrant локально
- Тестируем на реальных запросах
Стоимость: от 50 000 ₽
Этап 2: Production (2-3 недели)
- Масштабируем на весь каталог
- Настраиваем API
- Интегрируем с сайтом (замена поисковой формы)
- A/B тестирование
Стоимость: от 150 000 ₽
Этап 3: Оптимизация (1-2 недели)
- Гибридный поиск (vector + BM25)
- Добавляем фильтры, фасеты
- Настраиваем мониторинг
- Обучаем команду
Стоимость: от 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