Векторные индексы

Reindexer поддерживает три типа векторных индексов: brute-force, HNSW, IVF, и три типа метрик для вычисления расстояния между векторами: L2, inner_product, cosine. Для всех типов векторных индексов следует явно указывать метрику metric и размерность dimension. Вставлять в индекс и искать в нём можно только векторы указанной размерности. В индекс также можно вставить пустые векторы (пустой массив или null).

Параметры конфигурации для векторных индексов описаны в соответствующих разделах:

Создание индексов и добавление в уже существующий неймспейс описаны в разделах:

KNN — это поиск k наиболее близких соседних векторов. Параметры, необходимые для KNN-запроса, зависят от конкретного типа индекса.

Range search — это поиск ближайших соседних векторов, расстояние до которых не превышает порогового значения, переданного с помощью параметра radius.

Название параметра radius происходит из его геометрического смысла для L2-метрики: он ограничивает векторы выборки сферой заданного радиуса.

Для L2-метрики отсеиваются векторы, ранги которых больше значения параметра radius. Для метрик cosine и inner product — наоборот. Также для метрик cosine и inner product значения radius могут быть отрицательными. Задание неположительного радиуса для L2-метрики не ошибочно, но бессмысленно: результат запроса заведомо будет пустым.

Во избежание путаницы при использовании L2-метрики следует учитывать, что в целях производительности из рангов векторов не извлекаются квадратные корни, так как это не влияет на порядок выдачи. Поэтому ранги представляют собой квадраты расстояний до вектора запроса. И, хотя параметр и называется radius, переданное значение при поиске интерпретируется как квадрат расстояния до вектора запроса.

Общими параметрами KNN являются:

  • k — максимальное количество документов, возвращаемых из индекса для последующей фильтрации,
  • radius — фильтр по рангам векторов.

Для индексов brute-force и IVF обязательно должен быть задан хотя бы один из этих параметров. Для hnsw параметры k и radius можно не указывать и вместо этого использовать потоковый KNN. При задании обоих параметров фильтрация выполняется так, чтобы удовлетворять обоим условиям.

Так как range search работает на той же алгоритмической базе, что и KNN, далее под KNN-поиском подразумевается любой из них, в том числе их комбинация.

Списки используемых при поиске параметров и их назначение приведены на страницах соответствующих индексов:

Особенности реализации KNN-запросов:

  • KNN-фильтр можно использовать в запросах с дополнительной фильтрацией, если условия фильтрации соединены с KNN-фильтром через операторы AND и AND NOT:

    SELECT * FROM test_ns
    WHERE
      KNN(vec_ivf, [2.4, 3.5, ...], k=100, radius=21.12)
      AND value > 10
      AND (
       id < 7 OR
       is_active = true
      )
      INNER JOIN (
        SELECT * FROM second_ns
      ) ON test_ns.id = second_ns.id
      LEFT JOIN (
        SELECT * FROM third_ns
      ) ON test_ns.id = third_ns.id
    
  • Нет возможности использовать несколько KNN-фильтров в запросе.

  • Нет возможности использовать KNN-выборку внутри joined-подзапроса.

  • KNN-условие может быть соединено оператором OR только с полнотекстовым условием (см. гибридный поиск).

Базовые примеры KNN-запросов для конкретных индексов описаны в следующих разделах:

Выдача векторных полей

По умолчанию векторные поля исключаются из результатов всех запросов к неймспейсам, содержащим векторные индексы. Если необходимо получить векторные поля, следует указать это явно в запросе, либо перечислив нужные поля, либо запросив выдачу всех векторных полей, используя vectors(). Следует учитывать, что получение значений векторных полей это весьма “дорогая” операция с точки зрения производительности, поэтому не рекомендуется использовать её без необходимости.

Без явного запроса векторных полей:

SELECT * FROM test_ns;
// Оба запроса эквивалентны и не возвращают значения векторных полей
db.Query("test_ns")

db.Query("test_ns").Select("*")
# Оба запроса эквивалентны и не возвращают значения векторных полей
db.new_query("test_ns")

(
    db.new_query("test_ns")
        .select_fields("*")
)
db.query("test_ns", Item.class);
db.query("test_ns", Item.class)
    .select("*");
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/vectors_db/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_ns",
  "type": "select",
  "select_filter": ["*"]
}'

Результат в JSON-формате:

{"id": 0}

С запросом конкретных полей:

SELECT *, vec_bf FROM test_ns;
db.Query("test_ns").Select("*", "vec_bf")
(
    db.new_query("test_ns")
        .select_fields("*", "vec_bf")
)
db.query("test_ns", Item.class)
    .select("*", "vec_bf");
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/vectors_db/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_ns",
  "type": "select",
  "select_filter": ["*", "vec_bf"]
}'

Результат в JSON-формате:

{"id": 0, "vec_bf": [0.0, 1.1, ...]}

С запросом всех векторных полей:

SELECT *, vectors() FROM test_ns;
db.Query("test_ns").Select("*", "vectors()")
db.Query("test_ns").SelectAllFields()
(
    db.new_query("test_ns")
        .select_fields("*", "vectors()")
)
db.query("test_ns", Item.class)
    .select("*", "vectors()");
db.query("test_ns", Item.class)
    .selectAllFields();
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/vectors_db/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_ns",
  "type": "select",
  "select_filter": ["*", "vectors()"]
}'

Результат в JSON-формате:

{
  "id": 0,
  "vec_bf": [
    0.0,
    1.1,
    ...
  ],
  "vec_hnsw": [
    1.2,
    3.5,
    ...
  ],
  "vec_ivf": [
    5.1,
    4.7,
    ...
  ]
}

Получение ранга/расстояния

По умолчанию результаты запросов с KNN сортируются по rank (для векторного поиска он соответствует расстоянию между векторами). Для индексов с метрикой L2 — от меньшего значения к большему, а с метриками inner_product и cosine — от большего значения к меньшему. Что согласуется с наилучшим соответствием для указанных метрик.

В режиме потокового KNN порядок выдачи следует обходу HNSW: примерно по rank(), но без строгой сортировки. ORDER BY не поддерживается.

Когда есть необходимость узнать rank каждого документа в результате запроса, его нужно запросить явно через функцию RANK() в SQL или WithRank() в GO:

Также rank можно использовать в сортировке по выражению. Подробно описано в разделе сортировка и ограничение выдачи.

Индексация массивов с векторами и KNN поиск по ним

Векторный индекс может индексировать не только одиночные векторы, но и массивы с векторами. Для этого требуется создать индекс is_array: true (либо, в случае Go, просто проиндексировать двумерный слайс или слайс массивов):

// Вариант с добавлением индекса через теги
type Item struct {
	Id int `reindex:"id,,pk"`
	// Для слайсов из слайсов требуется явно задать тег `dimension`
	VecHnsw [][]float32 `reindex:"hnsw_idx_1,hnsw,m=16,dimension=1024,metric=inner_product"`
	// Для слайсов из массивов в качестве размерности вектора будет использована размерность массива
	VecHnswBF [][256]float32 `reindex:"bf_idx,bf,metric=cosine"`
}

// Вариант с добавлением индекса явно через метод AddIndex
vecOpts := reindexer.FloatVectorIndexOpts{
	Metric:             "l2",
	Dimension:          1024,
	M:                  16,
	EfConstruction:     200,
	MultithreadingMode: 1,
}
indexDef := reindexer.IndexDef{
	Name:      "hnsw_idx",
	JSONPaths: []string{"VecHnsw"},
	IndexType: "hnsw",
	FieldType: "float_vector",
	Config:    vecOpts,
	IsArray:   true,
}
err := DB.AddIndex("ns_name", indexDef)
if err != nil {
	panic(err)
}
index_definition = {
    "name": "hnsw_idx",
    "json_paths": ["VecHnsw"],
    "field_type": "float_vector",
    "index_type": "hnsw",
    "is_array": True,
    "config": {
        "metric": "l2",
        "dimension": 1024,
        "m": 16,
        "ef_construction": 200,
        "multithreading": 1
    }
}

db.index_add("ns_name", index_definition)
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/vectors_db/namespaces/test_ns/indexes' \
--header 'Content-Type: application/json' \
--data-raw '{
  "name": "hnsw_idx",
  "json_paths": ["VecHnsw"],
  "field_type": "float_vector",
  "index_type": "hnsw",
  "is_array": true,
  "config": {
    "dimension": 1024,
    "metric": "inner_product",
    "start_size": 1000,
    "m": 16,
    "ef_construction": 20,
    "multithreading": 1
  }
}'

Массив с векторами может быть проиндексирован при помощи любого из доступных векторных индексов. Поисковые запросы для векторных индексов-массивов полностью идентичны запросам по обычным векторным индексам.

В получившуюся индексную структуру каждый вектор массива попадает по отдельности, а KNN поиск выполняется по всем векторам, содержащимся в индексе, вне зависимости от того, к каким документам они принадлежат. Конечный результат поиска дедублицируется по внутренним id документов. При этом в качестве значения финального ранга выбирается наилучшее из всех, относящихся к конкретному документу. Как следствие, KNN-запрос с параметром k может вернуть меньше документов, чем было указано в k.

Например, если неймспейс с векторным индексом fv с metric=L2 содержит следующие документы:

{"id": 0, "fv": [[1.0, 1.0], [1.1, 1.0], [1.0, 1.1]]}
{"id": 1, "fv": [[2.0, 0.0]]}
{"id": 2, "fv": [[3.0, 0.0]]}

то результат следующего запроса

SELECT * FROM test_ns WHERE KNN(fv, [1.0, 1.0], k=3)

будет содержать единственный документ, поскольку все 3 наиболее подходящих вектора относятся к этому документу:

{"id": 0, "fv": [[1.0, 1.0], [1.1, 1.0], [1.0, 1.1]]}

Дополнительные action-команды

Эти команды могут быть использованы при помощи их вставки через upsert в #config-неймспейс.

Удаление дискового кеша для ANN-индексов

Формат команды в JSON:

{
  "type": "action",
  "action": {
    "command": "drop_ann_storage_cache",
    "namespace": "*",
    "index": "*"
  }
}

Пример использования через reindexer_tool:

reindexer_tool \
--dsn cproto://127.0.0.1:6534/vectors_db \
-c '\upsert #config {"type":"action","action":{"command":"drop_ann_storage_cache", "namespace":"*", "index":"*"}}'

Команда может быть полезна для случаев, когда требуется форсировать пересоздание дискового кеша для ANN-индексов, либо полностью его отключить (используя совместно с переменной среды RX_DISABLE_ANN_CACHE).

  • namespace — определяет целевой неймспейс (* — применяет команду ко всем неймспейсам);
  • index — определяет целевой индекс (* — применяет команду ко всем подходящим индексам в неймспейсе).

Переменные среды, влияющие на работу векторных индексов

Имя переменной Описание
RX_DISABLE_ANN_CACHE Если эта переменная установлена, то reindexer отключает использование дискового ANN-кеша для построения векторных индексов при загрузке данных с диска во время запуска (в общем случае отключать кеш не рекомендуется, так как это сильно снижает скорость запуска).
RX_TARGET_INSTRUCTIONS Ожидает одно из значений: avx512, avx2, avx или sse. Позволяет выбрать набор векторных инструкций, используемых для функций вычисления расстояния. По умолчанию, если переменная не задана, reindexer использует «лучшие» из доступных для CPU, на котором запущен, но в некоторых случаях avx512 может оказываться менее эффективен, чем avx2/avx в многопоточном окружении.