AACsearch
Поисковый API

Релевантность поиска

Обработка запроса, веса queryBy, typo tolerance, synonyms, curations, ранжирование — developer-справочник, как движок решает, что вернуть.

Страница Relevance tuning объясняет как пользоваться ручками relevance из UI. Эта страница объясняет, как движок принимает решение, что отдавать — developer-side справочник за спиной дашборда.

Прочитать один раз при интеграции и пересмотреть каждый раз, когда аналитика покажет регрессию.

Pipeline обработки запроса

POST /api/search идёт в таком порядке:

1. валидация ввода (Zod)
2. tenant + scoped filter resolve    ← Инварианты 4 + 5
3. expand synonyms                   ← per-index synonym sets
4. tokenize + drop stopwords         ← per-locale stopword list
5. typo tolerance                    ← numTypos default
6. retrieve по queryBy полям         ← weighted match
7. curation rules                    ← pin / hide overrides
8. score через _text_match           ← BM25-flavoured ranking
9. sort через sortBy (или default)
10. paginate, highlight, return

Шаги 3–5 на стороне движка AACSearch; шаг 7 поверх сырого результата до ранжирования. Tenant isolation (шаг 2) применяется всегда — обойти нельзя.

queryBy и веса полей

queryBy объявляет, какие поля движок ищет. Порядок важен — раньше = выше вес по умолчанию. Per-field веса переопределяются через queryByWeights:

{
  "queryBy": "title,brand,description,tags",
  "queryByWeights": "10,5,2,1"
}
  • Это относительные веса, не абсолютные. 10,5,2,1 = 100,50,20,10.
  • Без queryByWeights веса геометрические по умолчанию: примерно 8, 4, 2, 1, ….
  • Поля, не указанные в queryBy, не ищутся — их можно фильтровать и возвращать, но не искать.

Дефолтный queryBy для product-схемы: "title,brand,description,tags". Переопределяйте per request на storefront только при измеренной причине — платформенная консистентность ценнее микротюнинга.

Когда менять веса

По сигналам:

Сигнал в аналитикеЧто попробовать
Brand-запросы дают low-CTRПоднять brand (например, title:10, brand:8).
SKU-запросы падаютДобавить sku в queryBy с максимальным весом.
Long-tail descriptive — нерелевантные заголовкиДобавить description; рассмотреть stem: true на нём.
Tag-driven browse падаетПоднять вес tags или добавить, если нет.
Только title-матчи доминируют, теряются описанияСократить разрыв title/description.

Никогда не меняйте веса без eval-сета — Relevance quality описывает workflow.

Typo tolerance

Typo tolerance включён по умолчанию. Кол-во опечаток зависит от длины токена:

Длина токенаDefault numTypos
1–4 символа0 (только exact)
5–7 символов1
8+ символов2

Hard cap: numTypos ≤ 3. Переопределение per request:

{ "q": "iphn", "numTypos": 2 }

Строже:

  • prioritizeExactMatch: true — буст точных матчей над typo-матчами.
  • dropTokensThreshold: 1 — если ничего не совпало со всеми токенами, отбросить последний и повторить.
  • typoTokensThreshold: 0 — применять typo-tolerance, только когда 0 точных матчей.

Где typo-tolerance вредит: SKU и brand-запросы, где опечатка скорее «другой товар», чем «опечатка в запросе». Частый паттерн — ниже numTypos для SKU-sub-search в multi-search и дефолт для основного.

Synonyms

Synonyms мапят эквивалентные термины на этапе запроса:

"pants" ↔ ["jeans", "trousers"]
"sneakers" ↔ ["trainers"]
"mobile" ↔ ["phone"]    # locale-specific

При "trousers" движок раскроет запрос и матчит и "pants", и "jeans". Synonyms per index; управляются в Relevance tuning или через search.synonyms oRPC.

Три правила:

  1. Привязка к локали. Не делайте "pants" ↔ "trousers" синонимами в обоих en и ru — смысл разный.
  2. Избегайте одностороннего. "laptop" → "macbook" — это curation, не synonym. Если нужно одно направление — curation.
  3. Периодический аудит. Synonym применяется ко всем запросам с корневым словом; забытый synonym полугодовой давности — постоянный low-grade шум.

Synonyms идут до retrieval (шаг 3). Они не вытащат документы, которых нет в индексе — content gap чинится контентом.

Curation rules

Curation pin-ит или hide-ит конкретные ID для точной строки запроса:

Query "summer sale":
  pin    = ["tshirt-123", "shorts-456"]
  hide   = ["winter-coat-789"]

Curations — после retrieval (шаг 7): pinned ID вставляются в топ по порядку; hidden — убираются.

Когда брать:

  • Брендовый запрос, branded landing. «summer sale» — всегда сезонный landing-пин.
  • Compliance / regulatory. Скрыть товары, которые не должны появляться по определённым запросам.
  • Merchandising. Пин in-stock вариантов популярного SKU над out-of-stock.

Когда не надо:

  • «Все running-shoe запросы — сначала Nike» — это вес или synonym, не curation. Curations — exact-query.
  • «Поднять новые товары» — это sortBy: created_at:desc tiebreak.

Ранжирование и взаимодействие с sort

Дефолтный порядок — relevance-first:

sortBy: "_text_match:desc"

_text_match — BM25-style score, посчитанный по queryBy + весам + typo + позициям матча. Multi-field sort — для tiebreak-ов:

sortBy: "_text_match:desc,popularity_score:desc,price:asc"

Три поведения:

  • Чистый field-sort перебивает relevance. sortBy: "price:asc" отдаст самые дешёвые матчи — relevance игнорируется. Только для facet-driven browse.
  • Pin-curations игнорируют sort. Pinned документы остаются в топе. Hide-правила тоже работают при любом sort.
  • Wildcard q: "*" не имеет relevance-сигнала. При q: "*" _text_match одинаков для всех. Комбинируйте с осмысленным sortBy и агрессивным filterBy.

Для e-commerce типичный дефолт storefront-а:

sortBy: "_text_match:desc,popularity_score:desc"

popularity_score — поле, которое вы поддерживаете при ingest (обычно клики за 30 дней; см. popularity-ranking job в packages/search/lib/popularity-ranking.ts).

Обработка no-results

Поиск возвращает found: 0 либо потому что ничего не сматчилось, либо потому что фильтры сузили выдачу до нуля. Движок возвращает пустой результат с эхо-параметрами, чтобы клиент мог различить.

Три паттерна:

1. Retry с relaxed filters

Если filterBy был — попробовать без него. Виджет делает это автоматом при found === 0 и непустом filterBy.

2. Retry с dropTokensThreshold

Для длинных запросов — отбросить последний токен:

{ "q": "wireless noise cancelling over ear headphones", "dropTokensThreshold": 2 }

До 2 токенов дроп. Полезно при over-specific запросе.

3. AI / semantic fallback

Для storefront-ов с AI Search — fallback на semantic search (hybrid). Пользователь получает что-то вместо ничего.

Комбинируйте: filter-relaxation сначала (бесплатно), token-drop потом (бесплатно), semantic в конце (платно). Всегда инструментируйте fallback-путь, чтобы измерять окупаемость — No-results loop.

Workflow тестирования relevance

Релевант-правки против eval-сета — 30–100 запросов с известно-хорошими топ-результатами. Workflow:

  1. Snapshot. Прогоните все eval-запросы, сохраните текущий top-5 ID.
  2. Тюнинг. Меняете synonym / weight / curation в staging-индексе.
  3. Re-run. Прогоняете eval против staging.
  4. Diff. Для каждого запроса — staging top-5 vs production top-5. Скор:
    • +1 если ранее отсутствовавший target теперь в top-5.
    • −1 если ранее присутствовавший target выпал.
  5. Roll out, если net-score положительный и ни один query не упал до 0.

Eval-сет — самый ценный артефакт релевант-проекта. Переиспользуется через деплои, A/B и upgrades движка.

Аналогичный workflow для RAG — Knowledge evaluation; структура та же.

Пример e-commerce

Product-индекс с title, brand, description, sku, categories, tags, popularity_score. Разумный дефолт storefront search:

{
  "indexSlug": "products",
  "q": "running shoes",
  "queryBy": "title,brand,description,sku,tags",
  "queryByWeights": "10,5,3,8,2",
  "sortBy": "_text_match:desc,popularity_score:desc",
  "facetBy": "brand,categories,price",
  "filterBy": "availability:=in_stock"
}
  • SKU взвешен жирно — SKU-shaped запрос ("WH-2024") даёт точный мач первым.
  • Description ниже title/brand — descriptive-запросы матчат, но title всегда обгоняет description на одном товаре.
  • popularity_score — tiebreaker.

Пример help-center / контент

Article-индекс с title, excerpt, body, tags, published_at. Разумный дефолт help-center search:

{
  "indexSlug": "help-center",
  "q": "reset my password",
  "queryBy": "title,excerpt,body,tags",
  "queryByWeights": "8,4,2,3",
  "sortBy": "_text_match:desc,published_at:desc",
  "filterBy": "locale:=`en`"
}
  • title и tags выше — help-center заголовки короткие и осмысленные.
  • body ищется с stem: true (объявлено в схеме) — "reset" найдёт "resetting".
  • published_at tiebreaker — свежие статьи в случае ничьей.

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

On this page