Гибридный поиск (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. - Префиксные индексы требуют равенств для всех колонок префикса; текущая реализация гибридного запроса не поддерживает частичные префиксы.