Гибридный поиск
Reindexer поддерживает гибридный поиск по полнотекстовому и векторному индексам (KNN-поиск) в одном запросе. Условия разных поисков могут быть объединены логическими операторами AND или OR.
Примеры запросов:
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100)
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(10, knnBaseSearchParams)
if err != nil {
panic(err)
}
db.Query("test_namespace").
Where("ft_idx", reindexer.EQ, "my search~ phrase~*").
WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams)
param = IndexSearchParamIvf(100, 10)
query = (
db.new_query("test_namespace")
.where("ft_idx", CondType.CondEq, "my search~ phrase~*")
.where_knn("ivf_idx", [2.4, 3.5, ...], param)
)
SELECT * FROM test_namespace WHERE ft_idx = 'my search~ phrase~*' AND KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=10)
curl \
--location \
--request POST \
http://127.0.0.1:9088/api/v1/db/testdb/query \
--header 'Content-Type: application/json' \
--data-raw \
'
{
"namespace": "test_namespace",
"type": "select",
"filters": [
{
"op": "and",
"cond": "eq",
"field": "ft_idx",
"value": "my search~ phrase~*"
},
{
"op": "and",
"cond": "knn",
"field": "ivf_idx",
"value": [2.4, 3.5, ...],
"params": {
"k": 100,
"nprobe": 10
}
}
]
}
'
Запрос должен содержать ровно по одному условию для каждой разновидности поиска. При этом они должны быть внутри одной скобки либо вне скобок:
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100)
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(10, knnBaseSearchParams)
if err != nil {
panic(err)
}
db.Query("test_namespace").
OpenBracket().
Where("ft_idx", reindexer.EQ, "my search~ phrase~*").
Where("id", reindexer.GT, 50).
WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams).
CloseBracket().
Where("id", reindexer.LT, 10000)
param = IndexSearchParamIvf(100, 10)
query = (
db.new_query("test_namespace")
.open_bracket().
.where("ft_idx", CondType.CondEq, "my search~ phrase~*")
.where("id", CondType.CondGt, 50)
.where_knn("ivf_idx", [2.4, 3.5, ...], param)
.close_bracket().
.where("id", CondType.CondLt, 10000)
)
SELECT * FROM test_namespace
WHERE
(
ft_idx = 'my search~ phrase~*'
AND id > 50
AND KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=10)
)
AND id < 10000
curl \
--location \
--request POST \
http://127.0.0.1:9088/api/v1/db/testdb/query \
--header 'Content-Type: application/json' \
--data-raw \
'
{
"namespace": "test_namespace",
"type": "select",
"filters": [
{
"op": "and",
"filters": [
{
"op": "and",
"cond": "eq",
"field": "ft_idx",
"value": "my search~ phrase~*"
},
{
"op": "and",
"cond": "knn",
"field": "ivf_idx",
"value": [2.4, 3.5, ...],
"params": {
"k": 100,
"nprobe": 10
}
},
{
"op": "and",
"field": "id",
"cond": "GT",
"value": 50
}
]
},
{
"op": "and",
"field": "id",
"cond": "LT",
"value": 10000
}
]
}
'
Важно
Нельзя помещать условия, относящиеся к разным методам поиска, в разные скобки.
При гибридном поиске можно использовать автовекторизацию (требует настройки):
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100)
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(10, knnBaseSearchParams)
if err != nil {
panic(err)
}
it := db.
Query("test_namespace").
Where("ft_idx", reindexer.EQ, "my search~ phrase~*").
WhereKnnString("ivf_idx", "my search phrase", ivfSearchParams).
Exec()
defer it.Close()
param = IndexSearchParamIvf(100, 10)
query = (
db.new_query("test_namespace")
.where("ft_idx", CondType.CondEq, "my search~ phrase~*")
.where_knn_string("ivf_idx", "my search phrase", param)
.execute()
)
SELECT * FROM test_namespace
WHERE ft_idx = 'my search~ phrase~*' AND KNN(ivf_idx, 'my search phrase', k=100, nprobe=10)
curl \
--location \
--request POST \
http://127.0.0.1:9088/api/v1/db/testdb/query \
--header 'Content-Type: application/json' \
--data-raw \
'
{
"namespace": "test_namespace",
"type": "select",
"filters": [
{
"op": "and",
"cond": "eq",
"field": "ft_idx",
"value": "my search~ phrase~*"
},
{
"op": "and",
"cond": "knn",
"field": "ivf_idx",
"value": "my search phrase",
"params": {
"k": 100,
"nprobe": 10
}
}
]
}
'
Если сервис векторизации не отвечает, запрос KNN будет проигнорирован.
Повторное ранжирование
При гибридном поиске происходит повторное ранжирование, которое учитывает ранги результатов полнотекстового поиска и KNN-поиска. Для этого используется оператор ORDER BY с указанием функции ранжирования. Если выражение не указано, по умолчанию будет использована функция RRF().
Reciprocal rank fusion (RRF)
Reciprocal rank fusion (RRF) — это алгоритм объединения результатов ранжирования нескольких методов поиска. В качестве исходных данных алгоритм использует обратные ранги результатов поиска, т. е. результаты с первых позиций в нескольких поисковых выдачах имеют наивысший ранг. RRF вычисляется следующим образом:
R = 1 / (rankConst + posFt) + 1 / (rankConst + posKnn)
rankConst(rank_const) — сглаживающий коэффициент, который позволяет избежать слишком сильного влияния метода одного поиска на финальный ранг. По умолчанию равен 60;posFt,posKnn— позиции документов в поисковых выдачах (наименьшее значение соответствует наилучшему совпадению).
Если условия объединены оператором OR и только одно из них дает совпадения, сортировка учитывает выдачу только по этому условию. Например, если документ попал только в полнотекстовую выдачу, то слагаемое 1 / (rankConst + posKnn) отбрасывается.
Примеры запросов с произвольным rank_const:
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100)
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(10, knnBaseSearchParams)
if err != nil {
panic(err)
}
db.Query("test_namespace").
Where("ft_idx", reindexer.EQ, "my search~ phrase~*").
WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams).
Sort("RRF(rank_const=120)", false)
param = IndexSearchParamIvf(100, 10)
query = (
db.new_query("test_namespace")
.where("ft_idx", CondType.CondEq, "my search~ phrase~*")
.where_knn("ivf_idx", [2.4, 3.5, ...], param)
.sort("RRF(rank_const=120)", False)
)
SELECT * FROM test_namespace
WHERE
ft_idx = 'my search~ phrase~*'
AND KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=10)
ORDER BY
'RRF(rank_const=120)'
curl \
--location \
--request POST \
http://127.0.0.1:9088/api/v1/db/testdb/query \
--header 'Content-Type: application/json' \
--data-raw \
'
{
"namespace": "test_namespace",
"type": "select",
"filters": [
{
"op": "and",
"cond": "eq",
"field": "ft_idx",
"value": "my search~ phrase~*"
},
{
"op": "and",
"cond": "knn",
"field": "ivf_idx",
"value": [2.4, 3.5, ...],
"params": {
"k": 100,
"nprobe": 10
}
}
],
"sort": {
"field": "RRF(rank_const=120)"
}
}
'
Линейное ранжирование
Для результатов гибридного поиска возможно повторное ранжирование по линейной комбинации рангов в разных поисковых выдачах. Примеры запросов:
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100)
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(10, knnBaseSearchParams)
if err != nil {
panic(err)
}
db.Query("test_namespace").
Where("ft_idx", reindexer.EQ, "my search~ phrase~*").
WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams).
Sort("30 * rank(ft_idx) + 50 * rank(ivf_idx, 100.0) + 100", false)
param = IndexSearchParamIvf(100, 10)
query = (
db.new_query("test_namespace")
.where("ft_idx", CondType.CondEq, "my search~ phrase~*")
.where_knn("ivf_idx", [2.4, 3.5, ...], param)
.sort("30 * rank(ft_idx) + 50 * rank(ivf_idx, 100.0) + 100", False)
)
SELECT * FROM test_namespace WHERE ft_idx = 'my search~ phrase~*' AND KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=10)
ORDER BY '30 * rank(ft_idx) + 50 * rank(ivf_idx, 100.0) + 100'
curl \
--location \
--request POST \
http://127.0.0.1:9088/api/v1/db/testdb/query \
--header 'Content-Type: application/json' \
--data-raw \
'
{
"namespace": "test_namespace",
"type": "select",
"filters": [
{
"op": "and",
"cond": "eq",
"field": "ft_idx",
"value": "my search~ phrase~*"
},
{
"op": "and",
"cond": "knn",
"field": "ivf_idx",
"value": [2.4, 3.5, ...],
"params": {
"k": 100,
"nprobe": 10
}
}
],
"sort": {
"field": "30 * rank(ft_idx) + 50 * rank(ivf_idx, 100.0) + 100"
}
}
'
Здесь rank(index, [defaultRank]) — это ранг в полнотекстовом поиске или в поиске по векторному индексу. Опциональный параметр defaultRank определяет ранг, возвращаемый в случае отсутствия результатов в выдаче (по умолчанию 0).
Общая форма выражения для линейного пересчета ранга выглядит следующим образом:
R = a * rank(ft_idx, [ftDefault]) + b * rank(ivf_idx, [knnDefault]) + c
rank(ft_idx, [ftDefault])— ранг в выдаче полнотекстового поиска в интервале от 1 до 255;rank(ivf_idx, [knnDefault])— ранг в выдаче KNN-поиска, зависящий от выбранной метрики. Например, значения метрикиcosineнаходятся в пределах от -1.0 до 1.0, где -1.0 соответствует наименее релевантному вектору и 1.0 — наиболее релевантному;a,b,c— произвольные числа с плавающей точкой.
Выборка из нескольких неймспейсов
Допускается объединение результатов выборок из нескольких неймспейсов и повторное ранжирование финального результата. Примеры запросов:
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100)
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(10, knnBaseSearchParams)
if err != nil {
panic(err)
}
query := db.Query("test_namespace").
Where("ft_idx", reindexer.EQ, "my search~ phrase~*").
WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams).
Sort("30 * rank(ft_idx) + 50 * rank(ivf_idx, 100.0) + 100", false)
query_1 := db.Query("test_namespace_1").
Where("ft_idx", reindexer.EQ, "my search~ phrase~*").
WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams).
Sort("RRF(rank_const=120)", false)
query_2 := db.Query("test_namespace_2").
Where("ft_idx", reindexer.EQ, "my search~ phrase~*").
WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams)
query.
Merge(query_1).
Merge(query_2)
param = IndexSearchParamIvf(100, 10)
query = (
db.new_query("test_namespace")
.where("ft_idx", CondType.CondEq, "my search~ phrase~*")
.where_knn("ivf_idx", [2.4, 3.5, ...], param)
.sort("30 * rank(ft_idx) + 50 * rank(ivf_idx, 100.0) + 100", False)
)
query_1 = (
db.new_query("test_namespace_1")
.where("ft_idx", CondType.CondEq, "my search~ phrase~*")
.where_knn("ivf_idx", [2.4, 3.5, ...], param)
.sort("RRF(rank_const=120)", False)
)
query_2 = (
db.new_query("test_namespace_2")
.where("ft_idx", CondType.CondEq, "my search~ phrase~*")
.where_knn("ivf_idx", [2.4, 3.5, ...], param)
)
(
query
.merge(query_1)
.merge(query_2)
)
SELECT * FROM test_namespace
WHERE ft_idx = 'my search~ phrase~*' OR KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=10)
ORDER BY '30 * rank(ft_idx) + 50 * rank(ivf_idx, 100.0) + 100'
MERGE(
SELECT * FROM test_namespace_1
WHERE ft_idx = 'my search~ phrase~*' AND KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=10)
ORDER BY 'RRF(rank_const=120)'
)
MERGE(
SELECT * FROM test_namespace_2
WHERE ft_idx = 'my search~ phrase~*' OR KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=10)
)
curl \
--location \
--request POST \
http://127.0.0.1:9088/api/v1/db/testdb/query \
--header 'Content-Type: application/json' \
--data-raw \
'
{
"namespace": "test_namespace",
"type": "select",
"filters": [
{
"op": "and",
"cond": "eq",
"field": "ft_idx",
"value": "my search~ phrase~*"
},
{
"op": "and",
"cond": "knn",
"field": "ivf_idx",
"value": [2.4, 3.5, ...],
"params": {
"k": 100,
"nprobe": 10
}
}
],
"sort": {
"field": "30 * rank(ft_idx) + 50 * rank(ivf_idx, 100.0) + 100"
},
"merge_queries": [
{
"namespace": "test_namespace_1",
"type": "select",
"filters": [
{
"op": "and",
"cond": "eq",
"field": "ft_idx",
"value": "my search~ phrase~*"
},
{
"op": "and",
"cond": "knn",
"field": "ivf_idx",
"value": [2.4, 3.5, ...],
"params": {
"k": 100,
"nprobe": 10
}
}
],
"sort": {
"field": "RRF(rank_const=120)"
}
},
{
"namespace": "test_namespace_2",
"type": "select",
"filters": [
{
"op": "and",
"cond": "eq",
"field": "ft_idx",
"value": "my search~ phrase~*"
},
{
"op": "and",
"cond": "knn",
"field": "ivf_idx",
"value": [2.4, 3.5, ...],
"params": {
"k": 100,
"nprobe": 10
}
}
]
}
]
}
'