IVF

В основе IVF-индекса (Inverted File Index) лежит разбиение простантсва векторов на отдельные области с использованием алгоритма кластеризации k-means. Каждый кластер представлен своим центроидом, который служит опорной точкой для векторов внутри него.

При выполнении вставок в этот индекс для каждого нового вектора происходит поиск наиболее близкого к нему центроида и добавление вектора в соответсвующий кластер.

При поиске ближайших соседей также происходит поиск ближайших центроидов, а затем сканирование соответсвующих им кластеров (количество просканированных таким образом кластеров определяется параметром nprobe в каждом отдельном запросе).

Индекс реализован на основе IVF_FLAT из библиотеки faiss.

Параметры конфигурации

При создании IVF-индекса требуется задать centroids_count — количество опорных векторов (центроидов), которые выбираются для разбиения всего множества векторов на кластеры. Каждый вектор входит в кластер ближайшего к себе центроида. При увеличении centroids_count каждый кластер будет содержать меньшее число векторов, что ускорит поиск, но замедлит построение индекса. Обязательный, диапазон значений: [1, 65536].

Рекомендуется выбирать значения в диапазоне от \(4 \sqrt{\text{N}}\) до \(16 \sqrt{\text{N}}\), где \({\text{N}}\) — количество векторов в индексе.

Также для векторного индекса можно добавить опциональный параметр radius для фильтрации выдачи по умолчанию. Если в запросе будет передан параметр radius, то для фильтрации выдачи будет использовано переданное начение.

Имя параметра Описание Допустимые значения Значение по умолчанию Обязательность
centroids_count Требуемое количество центроидов [1, 65536] Отсутствует Обязательный
radius Фильтрация векторов по рангам любые float32 Отсутствует Опциональный

Пример создания индекса через GO-теги

В этом примере поверх поля VecIvf создаётся индекс с именем ivf_idx, типом ivf, размерностью вектора 1024, метрикой cosine и количеством опорных векторов 80.

// Для векторных полей тип данных должен быть слайсом (`[]float32`) или массивом(`[N]float32`).
type Item struct {
	Id      int       `reindex:"id,,pk"`
	// Для слайсов требуется явно задать тег `dimension`
	VecIvf  []float32 `reindex:"ivf_idx,ivf,centroids_count=80,metric=cosine,dimension=1024"`
}

Пример добавления индекса в уже существующий неймспейс

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

vecOpts := reindexer.FloatVectorIndexOpts {
  Metric: "cosine",
  Dimension: 1024,
  CentroidsCount: 80,
}
indexDef := reindexer.IndexDef {
  Name: "ivf_idx",
  JSONPaths: []string{"VecIvf"},
  IndexType: "ivf",
  FieldType: "float_vector",
  Config: vecOpts,
}
err := DB.AddIndex("ns_name", indexDef)
if err != nil {
  panic(err)
}
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": "ivf_idx",
  "json_paths": ["VecIvf"],
  "field_type": "float_vector",
  "index_type": "ivf",
  "config": {
    "dimension": 1024,
    "metric": "cosine",
    "centroids_count": 80
  }
}'

Параметры выборки из индекса

Имя параметра Описание Допустимые значения Значение по умолчанию Обязательность
k Максимальное количество документов, возвращаемых из индекса для последующей фильтрации >= 1 Отсутствует Обязательный при отсутствии radius
radius Фильтрация векторов по рангам (rank() < radius для L2-метрики и rank() > radius для consine- и inner product-метрик) (0,+∞) для L2;
(-∞,+∞) для inner product;
(-1,1) для cosine
Отсутствует Обязательный при отсутствии k
nprobe Количество кластеров, которые будут просматриваться при поиске. Увеличение этого параметра позволяет получить более качественный результат (recall rate), но в то же время замедляет поиск >= 1 1 Опциональный

Примеры KNN-запросов к IVF-индексу

Получение 100 ближайших соседей (используется nprobe = 16):

SELECT * FROM test_ns WHERE KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=16)
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100)
if err != nil {
	panic(err)
}
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(16, knnBaseSearchParams)
if err != nil {
	panic(err)
}
db.Query("test_ns").
	WithRank().
	WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams)
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",
  "filters": [
    {
      "op": "and",
      "cond": "knn",
      "field": "ivf_idx",
      "value": [2.4, 3.5, ...],
      "params": {"k": 100, "nprobe": 16}
    }
  ],
}'

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

SELECT * FROM test_ns WHERE KNN(ivf_idx, [2.4, 3.5, ...], nprobe=16, radius=10.11)
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetRadius(10.11)
if err != nil {
	panic(err)
}
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(16, knnBaseSearchParams)
if err != nil {
	panic(err)
}
db.Query("test_ns").
	WithRank().
	WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams)
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",
  "filters": [
    {
      "op": "and",
      "cond": "knn",
      "field": "ivf_idx",
      "value": [2.4, 3.5, ...],
      "params": {"radius": 10.11, "nprobe": 16}
    }
  ],
}'

Получение 100 ближайших соседей в некотором радиусе (используется nprobe = 16):

SELECT * FROM test_ns WHERE KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=16, radius=10.11)
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100).SetRadius(10.11)
if err != nil {
	panic(err)
}
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(16, knnBaseSearchParams)
if err != nil {
	panic(err)
}
db.Query("test_ns").
	WithRank().
	WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams)
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",
  "filters": [
    {
      "op": "and",
      "cond": "knn",
      "field": "ivf_idx",
      "value": [2.4, 3.5, ...],
      "params": {"k": 100, "radius": 10.11, "nprobe": 16}
    }
  ],
}'

Получение 100 ближайших соседей и их последующая фильтрация по условию id > 5 (также явно запрошен возврат значения поля VecIvf в выдаче и используется nprobe = 10):

SELECT *, VecIvf FROM test_ns WHERE id > 5 AND KNN(ivf_idx, [2.4, 3.5, ...], k=100, nprobe=10)
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100)
if err != nil {
	panic(err)
}
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(10, knnBaseSearchParams)
if err != nil {
	panic(err)
}
db.Query("test_ns").
	Select("*", "VecIvf").
	Where("id", reindexer.GT, 5).
	WhereKnn("ivf_idx", []float32{2.4, 3.5, ...}, ivfSearchParams)
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": ["*", "VecIvf"],
  "filters": [
    {
      "op": "and",
      "cond": "gt",
      "field": "id",
      "value": 5
    },
    {
      "op": "and",
      "cond": "knn",
      "field": "ivf_idx",
      "value": [2.4, 3.5, ...],
      "params": {"k": 100, "nprobe": 16}
    }
  ]
}'

Получение ранга в выдаче

Пример KNN-запроса по IVF-индексу с получением 100 ближайших соседей и соответствующих им рангов/расстояний:

SELECT *, RANK() FROM test_ns WHERE KNN(vec_ivf, [2.4, 3.5, ...], k=100, nprobe=12)
knnBaseSearchParams := reindexer.BaseKnnSearchParam{}.SetK(100)
if err != nil {
	panic(err)
}
ivfSearchParams, err := reindexer.NewIndexIvfSearchParam(12, knnBaseSearchParams)
if err != nil {
	panic(err)
}
db.Query("test_ns").WithRank().WhereKnn("vec_ivf", []float32{2.4, 3.5, ...}, ivfSearchParams)
curl --location --request POST 'http://172.0.0.1:9088/api/v1/db/frontapi/query' \
--header 'Content-Type: application/json' \
--data-raw '{
  "namespace": "test_ns",
  "type": "select",
  "select_with_rank": true,
  "filters": [
    {
      "cond": "knn",
      "field": "vec_ivf",
      "value": [2.4, 3.5, ...],
      "params": {"k": 100, "nprobe": 12}
    }
  ],
}'

Пример результирующей выдачи:

{"id": 0, "rank()": 1.0}
{"id": 7, "rank()": 0.97}
{"id": 2, "rank()": 0.8432}
{"id": 10, "rank()": 0.54}

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

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

Перестроение кластеров для IVF-индекса

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

{
    "type":"action",
    "action":{
        "command":"rebuild_ivf_index",
        "namespace":"*",
        "index":"*",
        "data_part": 0.5
    }
}

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

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

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

  • namespace - определяет целевой неймспейс (* - применяет команду ко всем неймспейсам);
  • index - определяет целевой индекс (* - применяет команду ко всем подходящим индексам в неймспейсе);
  • data_part - определяет долю от общего количества данных в индексе, которая будет использована для построения новых кластеров (диапазон [0.0, 1.0]). Чем больше используется данных, тем более точными получатся результирующие кластеры, однако время вычисления центроидов тоже будет больше.

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

Часть дополнительных глобальных настроек IVF-индексов может быть задана через переменные среды.

Имя переменной Описание
RX_IVF_MT Ожидает числовое значение 0 или 1. Если задан 1, то для IVF-индексов используется многопоточный режим поиска. По умолчанию 0.
RX_IVF_OMP_THREADS Количество потоков OpenMP, используемое при построении кластеров для IVF-индексов. По умолчанию 8.
RX_CUSTOM_BLAS_LIB_NAME Позволяет задать пользовательское имя для библиотеки BLAS, используемой в IVF-индексах (для случаев, когда reindexer не может найти её самостоятельно).
RX_CUSTOM_LAPACK_LIB_NAME Позволяет задать пользовательское имя для библиотеки LAPACK, используемой в IVF-индексах (для случаев, когда reindexer не может найти её самостоятельно).