Семантический поиск
Векторный поиск по смыслу вместо точного совпадения токенов — и как комбинировать его с keyword search в гибридном режиме.
Semantic search возвращает документы, чьи векторные эмбеддинги ближе всего к эмбеддингу запроса. Это дополнение к keyword search: там, где keyword промахивается на синонимах и перефразировках («running shoes» vs «sport sneakers»), semantic подбирает совпадение. Их можно объединить в hybrid search — это почти всегда лучше, чем любой из режимов в одиночку.
Как это работает
- Каждое поисковое текстовое поле эмбеддится при ingest. Эмбеддинг сохраняется как
float[](см. Index schema). - При запросе тот же текст пользователя эмбеддится той же моделью.
- Движок считает cosine similarity (или inner product) между вектором запроса и каждым документом, возвращает top по расстоянию.
- Hybrid mode прогоняет оба режима и склеивает результаты с обучаемым весом.
Под капотом — оператор vector_query Typesense; @repo/search оборачивает его через formatVectorQuery() и generateEmbedding() (см. packages/search/lib/embeddings.ts).
Статус
Semantic search — Beta:
| Возможность | Статус |
|---|---|
| Векторное поле при ingest | ✅ Available (объявите float[] с num_dim) |
vector_query при запросе | ✅ Available |
Auto-embed на upsertDocument / bulkUpsert | 🟡 Beta — org-флаг |
| Hybrid (keyword + vector) ranking | 🟡 Beta |
| Кастомная модель эмбеддингов per Knowledge space | 🟡 Beta (KnowledgeSpace.ragConfig.embeddingModel) |
| Per-org fine-tuned модель | ⏳ Roadmap (Enterprise) |
Считайте схему и shape запроса стабильными; per-org tuning knobs — могут поменяться.
Требования к схеме
Добавьте вектор в схему индекса:
await orpc.search.createIndex.call({
organizationId: "org_…",
slug: "products",
fields: [
{ name: "id", type: "string" },
{ name: "title", type: "string", sort: true },
{ name: "description", type: "string" },
{ name: "embedding", type: "float[]", num_dim: 1536, vec_dist: "cosine" },
],
});Выбор значений:
num_dimдолжно совпадать с моделью эмбеддингов (1536дляtext-embedding-3-small,3072дляtext-embedding-3-large). Несовпадение → ingest падает сexpected vector of length X, got Y.vec_distпо умолчанию"cosine"; переключайтесь на"ip", только если модель этого требует.hnsw_paramsтюньте только после бенчмарка. Дефолты адекватные.
Имя embedding — конвенция; движку всё равно, главное — ссылка в vector_query.
Ingest документов с эмбеддингами
Два варианта:
Вариант 1 — server-side auto-embed (Beta)
Включите per-org auto-embed флаг в AI feature config; воркер вызовет generateEmbedding() на сконфигурированных текстовых полях и положит вектор до отправки в Typesense. Beta — модель эмбеддингов настраивается пока только платформой.
Вариант 2 — client-side эмбеддинг
Считаете эмбеддинг сами и передаёте обычным полем через upsertDocument / bulkUpsert:
await orpc.search.upsertDocument.call({
organizationId: "org_…",
indexSlug: "products",
document: {
id: "product-123",
title: "Wireless Headphones",
description: "Noise-cancelling over-ear headphones…",
embedding: [0.0123, -0.0456, …, 0.0789], // 1536 float-ов
},
});Тот же DB-first путь (Инвариант 2). Вектор для буфера непрозрачен; воркер пишет то, что вы передали.
Запрос
Только вектор
const res = await fetch("/api/search", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${searchKey}` },
body: JSON.stringify({
indexSlug: "products",
q: "*",
vectorQuery: "embedding:([0.01, -0.05, …], k:20)",
}),
});vector_query принимает литеральный вектор и k (сколько ближайших соседей рассматривать). q: "*" — keyword-сторона как no-op.
Hybrid (keyword + vector)
{
"indexSlug": "products",
"q": "running shoes",
"queryBy": "title,description",
"vectorQuery": "embedding:([…], k:50, distance_threshold:0.7, alpha:0.7)"
}alphaсмешивает скоры:0— чистый keyword,1— чистый вектор. Стартовая точка:0.4–0.6.distance_threshold— максимально допустимая cosine-дистанция; отсекает заведомо несвязанные документы.
Hybrid почти всегда строго лучше keyword или vector в одиночку на шумных/неоднозначных запросах. На точных запросах (SKU, бренды) keyword быстрее и так же точен.
Фильтры
filterBy, facetBy, sortBy работают так же. Частый паттерн — расширить recall вектором и закрутить бизнес-правила фильтром:
{
"q": "*",
"vectorQuery": "embedding:([…], k:100)",
"filterBy": "availability:=in_stock && price:<100"
}Когда semantic помогает
- Перефразировки.
«running shoes» ↔ «sport sneakers»,«wireless earbuds» ↔ «bluetooth headphones». - Многоязычные каталоги. Эмбеддинги мультилингвальных моделей мостят локали без per-language synonyms.
- Длинные запросы. Когда пользователь пишет предложение целиком, его слова часто не совпадают с лексикой каталога.
- Восстановление на no-results. Если keyword отдал 0 — вторая попытка через вектор (см. No-results loop).
Когда semantic вредит
- Точные запросы. SKU, артикулы, бренды. Keyword быстрее, детерминированный и не зависит от drift-а эмбеддингов.
- Холодные каталоги. При менее ~50 документах вектора не дают сигнала сверх keyword.
- Чувствительность к латентности. Vector-запросы быстрые, но query-time
text-embedding-3-largeдобавляет 100–300 мс; учитывайте. - Compliance-запросы. Когда пользователь должен видеть точный источник («что говорит пункт 4.2»), keyword + curation аудируем; вектора — нет.
Форма стоимости
Query-time эмбеддинг метерится через AI Wallet (CREDIT_RATES.embedding_query). Bulk ingest эмбеддинг — свой тариф (CREDIT_RATES.embedding_ingest). Нехватка → 402 Payment Required (Инвариант 6 в силе — upstream-ошибка маппится в типизованный JSON).
При устойчивой semantic-нагрузке следите в Activity за embedding_cost_exceeded и тюньте per-org бюджет.
Связанные страницы
- Обзор AI Search
- AI answers — панель ответа над хитами
- Index schema — декларация векторного поля
- Public search endpoint — keyword-сторона запроса
- Multi-search and querying — батчинг semantic + keyword в одном round-trip-е