Гибридный поиск (HybridRank)

Гибридный поиск объединяет полнотекстовое и векторное ранжирование в едином результате. Гибридный запрос — это SELECT по основной таблице, у которого ключ ORDER BY представляет собой единственный вызов HybridRank:

PRAGMA ydb.KMeansTreeSearchTopSize = "10";

$queryText = "машинное обучение";
$queryVector = Knn::ToBinaryStringFloat([0.1, 0.2, 0.3, 0.4]);

SELECT id, title
FROM documents
ORDER BY HybridRank(
    FullTextScore(text, $queryText),
    Knn::CosineDistance(embedding, $queryVector))
LIMIT 10;

Примечание

Гибридный запрос читает данные из основной таблицы и не использует VIEW IndexName: индекс каждой ветви определяется по соответствующему аргументу HybridRank.

Каждой ветви FullTextScore требуется готовый индекс fulltext_relevance. Каждой ветви Knn требуется готовый индекс vector_kmeans_tree с совместимой метрикой. Оба типа индексов могут иметь префиксы; см. выбор индексов и фильтрация. Полнота векторной ветви управляется параметром KMeansTreeSearchTopSize.

HybridRank

Общая форма функции:

HybridRank(
    score1, score2 [, ... scoreN]        -- 2+ оценивающих выражения: FullTextScore(...) или Knn::<Distance|Similarity>(...)
    [, "rrf" | "linear"  AS Mode]        -- способ объединения (по умолчанию "rrf")
    [, (w1, w2, ...)     AS Weights]     -- веса ветвей (по умолчанию 1.0)
    [, k                 AS K]           -- константа RRF (по умолчанию 60.0)
    [, true | false      AS Normalize]   -- только для режима "linear": min-max нормализация (по умолчанию true)
    [, (idx1, idx2, ...) AS Indexes]     -- имена индексов по ветвям (по умолчанию: автоопределение)
    [, (lim1, lim2, ...) AS Limits]      -- размеры пулов кандидатов по ветвям (по умолчанию: LIMIT * 10)
    [, ($x) -> {...}     AS RankLambda]  -- пользовательское объединение по рангам ветвей (заменяет встроенный Mode)
    [, ($x) -> {...}     AS ScoreLambda] -- пользовательское объединение по исходным оценкам ветвей (заменяет встроенный Mode)
)

HybridRank может использоваться только как единственный ключ сортировки в секции ORDER BY. Он принимает два или более оценивающих выражения (позиционно), за которыми следуют необязательные именованные аргументы.

Каждое оценивающее выражение — это одна ветвь объединения, классифицируемая по своей форме:

  • выражение FullTextScore(text, query) — полнотекстовая ветвь, разрешается через индекс fulltext_relevance по колонке text;
  • Knn-расстояние или сходство по колонке (например, Knn::CosineDistance(embedding, $queryVector) или Knn::CosineSimilarity(embedding, $queryVector)) — векторная ветвь, разрешается через индекс vector_kmeans_tree по этой колонке.

Порядок аргументов не имеет значения, а ветвей может быть больше двух (например, полнотекстовая ветвь и две векторные). Каждая ветвь извлекает собственный пул кандидатов. Объединение рассматривает все эти пулы и возвращает каждый документ один раз, в том числе документы, найденные только одной ветвью. Значение NULL в оцениваемой колонке исключает документ из соответствующей ветви, но другая ветвь всё ещё может его найти.

Используйте непосредственный вызов FullTextScore как оценивающий аргумент. Выражения вида 2 * FullTextScore(...) отклоняются; для изменения вклада используйте Weights или ScoreLambda. Вызов поддерживает именованные параметры DefaultOperator, MinimumShouldMatch, K1 и B, описанные в FullTextScore. K1 и B принимают литералы и параметры типа Float или Double; DefaultOperator и MinimumShouldMatch принимают строковые литералы и параметры.

Поддерживаются векторные функции Knn::CosineDistance, Knn::CosineSimilarity, Knn::ManhattanDistance, Knn::EuclideanDistance и Knn::InnerProductSimilarity.

Режимы объединения

Через аргумент Mode доступны два способа объединения:

  • rrf (по умолчанию) — Reciprocal Rank Fusion. Каждая ветвь вносит вклад weight / (K + rank), где rank — позиция документа внутри этой ветви. Так как RRF использует ранги, а не исходные оценки, несопоставимые величины оценок ветвей не имеют значения.
  • linear вычисляет взвешенную сумму оценок ветвей, в которой большее значение всегда означает лучшее совпадение. При Normalize = true (по умолчанию) нормализация использует минимум и максимум внутри пула кандидатов каждой ветви. Для релевантности и сходства используется (score - min) / (max - min), для расстояний используется (max - score) / (max - min). Ветвь с одинаковыми оценками всех кандидатов вносит 0. При Normalize = false релевантность и сходство сохраняют исходные значения, а расстояния меняют знак перед умножением на вес.

В обоих режимах документ, отсутствующий в пуле кандидатов ветви, просто не получает вклада этой ветви.

Итоговый результат отсортирован по убыванию объединённой оценки. Взаимный порядок документов с одинаковой итоговой оценкой не гарантируется. Вес 0 обнуляет вклад ветви в оценку, но её кандидаты могут присутствовать в результате.

Пользовательская лямбда объединения

Вместо встроенного Mode (rrf/linear) можно передать собственную лямбду объединения, которая вычисляет итоговую оценку из значений ветвей. Доступны два взаимоисключающих вида лямбд:

  • RankLambda — лямбда получает ранги документа по ветвям в виде Dict<Int64, Int64> (индекс ветви → 1-based ранг внутри этой ветви). Если документ отсутствует в ветви, соответствующей записи нет, поэтому $ranks[i] равно NULL.
  • ScoreLambda — лямбда получает исходные оценки документа по ветвям в виде Dict<Int64, Double> (индекс ветви → значение оценки ветви: релевантность полнотекстового поиска или векторное расстояние/сходство). Если документ отсутствует в ветви, соответствующей записи нет, поэтому $scores[i] равно NULL.

Лямбда принимает один аргумент (словарь) и должна возвращать числовое значение (рекомендуется Double); чем больше значение, тем выше позиция в ранжировании. Лямбда заменяет встроенное объединение, поэтому её нельзя комбинировать с Mode, Weights, K или Normalize — перенесите все веса и константы в тело лямбды.

SELECT id, title
FROM documents
ORDER BY HybridRank(
    FullTextScore(text, $queryText),
    Knn::CosineDistance(embedding, $queryVector),
    ($ranks) -> {
        RETURN 1.0 / (60 + COALESCE($ranks[0], 100000))
             + 1.0 / (60 + COALESCE($ranks[1], 100000));
    } AS RankLambda)
LIMIT 10;

Именованные аргументы

Все необязательные параметры передаются как именованные аргументы. За исключением лямбд объединения, их значения должны быть литералами. Аргументы Weights, Limits и Indexes — это кортежи, позиционно соответствующие оценивающим выражениям: в них должно быть ровно по одному элементу на ветвь. Веса и константа RRF должны быть конечными; для RRF также требуется ненулевое значение K + rank.

Аргумент Тип Описание
Mode String Способ объединения: "rrf" (по умолчанию) или "linear". Регистр не учитывается ("RRF" / "Linear" также допустимы).
Weights Кортеж чисел Вес каждой ветви, по одному на оценивающий аргумент. По умолчанию 1.0 для каждой ветви. Вес 0 обнуляет вклад в оценку, сохраняя кандидатов ветви.
K Double Константа RRF (только режим rrf). По умолчанию 60.0. Большие значения сглаживают влияние верхних рангов.
Normalize Bool Только режим linear. При true (по умолчанию) оценки ветвей нормализуются методом min-max, а расстояния инвертируются. При false используются исходные оценки релевантности и сходства, а исходные расстояния меняют знак.
Indexes Кортеж строк Явное имя индекса для каждой ветви вместо автоматического выбора. Требуется при неоднозначности автоматического выбора.
Limits Кортеж положительных целых Явный размер пула кандидатов для каждой ветви. По умолчанию каждая ветвь извлекает до LIMIT * HybridSearchFactor кандидатов (фактор по умолчанию 10).
RankLambda Лямбда Пользовательское объединение по рангам ветвей. Лямбда принимает Dict<Int64, Int64> (индекс ветви → 1-based ранг; для отсутствующих ветвей записи нет) и возвращает числовую оценку. Заменяет встроенный Mode; несовместим с Mode, Weights, K и Normalize.
ScoreLambda Лямбда Пользовательское объединение по исходным оценкам ветвей. Лямбда принимает Dict<Int64, Double> (индекс ветви → исходная оценка; для отсутствующих ветвей записи нет) и возвращает числовую оценку. Заменяет встроенный Mode; несовместим с Mode, Weights, K и Normalize.

Выбор индексов и фильтрация

Автоматический выбор рассматривает готовые индексы, последняя индексируемая колонка которых совпадает с колонкой оценивающего выражения. Префиксный индекс подходит, только если WHERE фиксирует значение каждой колонки префикса предикатами равенства, соединёнными через AND.

  • Для полнотекстовой ветви должен остаться ровно один подходящий индекс релевантности. Поддерживаются исходный и компактный форматы хранения.
  • Для векторной ветви метрика также должна соответствовать функции Knn. Из совместимых индексов выбирается индекс с самым длинным полностью заданным префиксом. Индексы с незаданным или частично заданным префиксом пропускаются. Если у нескольких индексов одинаковая максимальная длина префикса, выбор неоднозначен.

Для устранения неоднозначности используйте AS Indexes. При явном указании имени индекса по-прежнему проверяются колонка, метрика, готовность и префикс.

Например, для индексов по (category, text) и (category, embedding) выберите одну категорию перед ранжированием:

DECLARE $category AS Utf8;
DECLARE $queryText AS Utf8;
DECLARE $queryVector AS String;

SELECT id, title
FROM documents
WHERE category = $category
ORDER BY HybridRank(
    FullTextScore(text, $queryText),
    Knn::CosineDistance(embedding, $queryVector),
    ("ft_by_category", "vec_by_category") AS Indexes,
    (100, 100) AS Limits)
LIMIT 10;

Значениями префикса могут быть литералы, параметры или скалярные поля неопциональных параметров-структур. Колонки таблицы, допускающие NULL, могут входить в префикс при сравнении с неопциональными значениями. Требуется задать каждую колонку префикса, в том числе входящую в первичный ключ. В текущей реализации начальная часть префикса, равенство внутри OR и поле опционального параметра-структуры не удовлетворяют этому требованию.

Предикаты префикса ограничивают соответствующую индексную ветвь. Остальные предикаты WHERE применяются после объединения кандидатов и чтения строк основной таблицы, перед итоговым LIMIT. Они могут сократить результат ниже запрошенного размера; при необходимости увеличьте пулы кандидатов. Саму колонку, оцениваемую FullTextScore, нельзя фиксировать предикатом равенства в WHERE, поскольку все оставшиеся документы имели бы одинаковое текстовое значение для этой ветви.

Пулы кандидатов и пагинация

Задайте PRAGMA ydb.HybridSearchFactor = "20";, чтобы извлекать из каждой ветви до двадцати итоговых лимитов кандидатов, или используйте AS Limits для явного задания каждого пула. Эти настройки определяют количество объединяемых кандидатов; KMeansTreeSearchTopSize отдельно управляет обходом векторного индекса.

Изменение итогового LIMIT меняет и пулы кандидатов по умолчанию, поэтому может изменить ранжирование. Для пагинации используйте фиксированные явные пулы, достаточные для запрошенной страницы. OFFSET применяется к объединённому ранжированию. Векторный поиск приближённый, а порядок при равных оценках не определён, поэтому стабильный полный порядок между отдельными запросами не гарантируется.

Примеры

Взвешенный RRF — смещение ранжирования в сторону векторной ветви:

$queryText = "машинное обучение";
$queryVector = Knn::ToBinaryStringFloat([0.1, 0.2, 0.3, 0.4]);

SELECT id, title
FROM documents
ORDER BY HybridRank(
    FullTextScore(text, $queryText),
    Knn::CosineDistance(embedding, $queryVector),
    (1, 2) AS Weights)
LIMIT 10;

RRF по трём ветвям — комбинация текстовой релевантности с двумя векторными ветвями по разным колонкам эмбеддингов:

$queryText = "машинное обучение";
$titleVector = Knn::ToBinaryStringFloat([0.1, 0.2, 0.3, 0.4]);
$bodyVector = Knn::ToBinaryStringFloat([0.7, 0.8, 0.9, 1.0]);

SELECT id, title
FROM documents
ORDER BY HybridRank(
    FullTextScore(text, $queryText),
    Knn::CosineDistance(title_embedding, $titleVector),
    Knn::CosineDistance(body_embedding, $bodyVector),
    (1, 2, 3) AS Weights)
LIMIT 10;

Линейное объединение нормализованных оценок:

$queryText = "машинное обучение";
$queryVector = Knn::ToBinaryStringFloat([0.1, 0.2, 0.3, 0.4]);

SELECT id, title
FROM documents
ORDER BY HybridRank(
    FullTextScore(text, $queryText),
    Knn::CosineDistance(embedding, $queryVector),
    "linear" AS Mode)
LIMIT 10;

Явные имена индексов и размеры пулов кандидатов. Так как размеры пулов по ветвям заданы через Limits, собственный LIMIT запроса больше не обязан быть литералом и может быть параметром:

DECLARE $limit AS Uint64;

$queryText = "машинное обучение";
$queryVector = Knn::ToBinaryStringFloat([0.1, 0.2, 0.3, 0.4]);

SELECT id, title
FROM documents
ORDER BY HybridRank(
    FullTextScore(text, $queryText),
    Knn::CosineDistance(embedding, $queryVector),
    ("ft_idx", "vec_idx") AS Indexes,
    (100, 200) AS Limits)
LIMIT $limit;

Пользовательский RankLambda — RRF вручную, с весом векторной ветви в 100 раз больше текстовой:

$queryText = "машинное обучение";
$queryVector = Knn::ToBinaryStringFloat([0.1, 0.2, 0.3, 0.4]);

SELECT id, title
FROM documents
ORDER BY HybridRank(
    FullTextScore(text, $queryText),
    Knn::CosineDistance(embedding, $queryVector),
    ($ranks) -> {
        RETURN   1.0 / (60 + COALESCE($ranks[0], 100000))
             + 100.0 / (60 + COALESCE($ranks[1], 100000));
    } AS RankLambda)
LIMIT 10;

Пользовательский ScoreLambda — комбинация исходной релевантности полнотекстового поиска и векторного расстояния со знаком минус (ближе = больше оценка):

$queryText = "машинное обучение";
$queryVector = Knn::ToBinaryStringFloat([0.1, 0.2, 0.3, 0.4]);

SELECT id, title
FROM documents
ORDER BY HybridRank(
    FullTextScore(text, $queryText),
    Knn::CosineDistance(embedding, $queryVector),
    ($scores) -> {
        RETURN COALESCE($scores[0], 0.0) - COALESCE($scores[1], 1000000.0);
    } AS ScoreLambda)
LIMIT 10;

Ограничения

  • HybridRank(...) должен быть единственным ключом ORDER BY; его нельзя отрицать, вкладывать в большее выражение или комбинировать с другими ключами сортировки.
  • Текущая реализация поддерживает только таблицы с одноколоночным первичным ключом.
  • Требуется не менее двух оценивающих аргументов — одна ветвь не является гибридным запросом.
  • LIMIT должен быть литералом (он задаёт размеры пулов кандидатов по ветвям). Чтобы использовать параметризованный LIMIT, передайте явный AS Limits.
  • Пользовательская лямбда объединения (RankLambda или ScoreLambda) заменяет встроенное объединение и не может комбинироваться с Mode, Weights, K или Normalize. Можно указать не более одного из RankLambda или ScoreLambda.
  • Префиксные индексы требуют равенств для всех колонок префикса; текущая реализация гибридного запроса не поддерживает частичные префиксы.
Предыдущая
Следующая