AACsearch
AI Поиск

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

Векторный поиск по смыслу вместо точного совпадения токенов — и как комбинировать его с keyword search в гибридном режиме.

Semantic search возвращает документы, чьи векторные эмбеддинги ближе всего к эмбеддингу запроса. Это дополнение к keyword search: там, где keyword промахивается на синонимах и перефразировках («running shoes» vs «sport sneakers»), semantic подбирает совпадение. Их можно объединить в hybrid search — это почти всегда лучше, чем любой из режимов в одиночку.

Как это работает

  1. Каждое поисковое текстовое поле эмбеддится при ingest. Эмбеддинг сохраняется как float[] (см. Index schema).
  2. При запросе тот же текст пользователя эмбеддится той же моделью.
  3. Движок считает cosine similarity (или inner product) между вектором запроса и каждым документом, возвращает top по расстоянию.
  4. 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 бюджет.

Связанные страницы

On this page