Spec-Zone.ru › Elasticsearch 8
›Elasticsearch Guide [8.17] ›Поиск данных

Поиск k-ближайших соседей (kNN)

Поиск k ближайших соседей (kNN) находит k ближайших векторов к вектору запроса, измеряя их по метрике сходства.

Общие случаи использования kNN включают:

  • Рейтинг релевантности на основе алгоритмов обработки естественного языка (NLP)
  • Рекомендации по продуктам и рекомендательные системы
  • Поиск сходства изображений или видео

Предварительные условия

  • Для выполнения поиска kNN необходимо преобразовать данные в осмысленные векторные значения. Вы можете создать эти векторы с помощью модели обработки естественного языка (NLP) в Elasticsearch, или сгенерировать их вне Elasticsearch. Векторы можно добавить к документам как значения поля dense_vector. Запросы представляются в виде векторов с той же размерностью.

    Проектируйте векторы так, чтобы чем ближе вектор документа к вектору запроса, исходя из метрики сходства, тем лучше совпадение.

  • Для завершения шагов в этом руководстве, у вас должны быть следующие права на индексы:

    • create_index или manage для создания индекса с полем dense_vector
    • create, index или write для добавления данных в созданный вами индекс
    • read для поиска в индексе

Методы kNN

Elasticsearch поддерживает два метода для поиска kNN:

  • Приближенный поиск kNN с использованием опции поиска knn или запроса knn
  • Точный, переборный поиск kNN с использованием запроса script_score с векторной функцией

В большинстве случаев вам следует использовать приближенный поиск kNN. Приближенный поиск kNN предлагает более низкую задержку за счет более медленной индексации и несовершенной точности.

Точный, переборный поиск kNN гарантирует точные результаты, но плохо масштабируется с большими наборами данных. В этом подходе запрос script_score должен просмотреть каждый соответствующий документ для вычисления векторной функции, что может привести к медленной скорости поиска. Однако вы можете улучшить задержку, используя запрос для ограничения количества соответствующих документов, передаваемых функции. Если вы отфильтруете данные до небольшого подмножества документов, вы можете получить хорошую производительность поиска с помощью этого подхода.

Приближенный поиск kNN

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

Для выполнения приближенного поиска kNN используйте knn опцию для поиска одного или нескольких полей dense_vector с включенной индексацией.

  1. Явно отобразите одно или несколько полей dense_vector. Приближенный поиск kNN требует следующих параметров отображения:

    • Значение similarity. Это значение определяет метрику сходства, используемую для оценки документов на основе сходства запроса и векторного документа. Список доступных метрик см. в документации параметра similarity. Значение similarity по умолчанию равно cosine.
    resp = client.indices.create(
        index="image-index",
        mappings={
            "properties": {
                "image-vector": {
                    "type": "dense_vector",
                    "dims": 3,
                    "similarity": "l2_norm"
                },
                "title-vector": {
                    "type": "dense_vector",
                    "dims": 5,
                    "similarity": "l2_norm"
                },
                "title": {
                    "type": "text"
                },
                "file-type": {
                    "type": "keyword"
                }
            }
        },
    )
    print(resp)
    response = client.indices.create(
      index: 'image-index',
      body: {
        mappings: {
          properties: {
            "image-vector": {
              type: 'dense_vector',
              dims: 3,
              similarity: 'l2_norm'
            },
            "title-vector": {
              type: 'dense_vector',
              dims: 5,
              similarity: 'l2_norm'
            },
            title: {
              type: 'text'
            },
            "file-type": {
              type: 'keyword'
            }
          }
        }
      }
    )
    puts response
    const response = await client.indices.create({
      index: "image-index",
      mappings: {
        properties: {
          "image-vector": {
            type: "dense_vector",
            dims: 3,
            similarity: "l2_norm",
          },
          "title-vector": {
            type: "dense_vector",
            dims: 5,
            similarity: "l2_norm",
          },
          title: {
            type: "text",
          },
          "file-type": {
            type: "keyword",
          },
        },
      },
    });
    console.log(response);
    PUT image-index
    {
      "mappings": {
        "properties": {
          "image-vector": {
            "type": "dense_vector",
            "dims": 3,
            "similarity": "l2_norm"
          },
          "title-vector": {
            "type": "dense_vector",
            "dims": 5,
            "similarity": "l2_norm"
          },
          "title": {
            "type": "text"
          },
          "file-type": {
            "type": "keyword"
          }
        }
      }
    }
  2. Индексируйте свои данные.

    POST image-index/_bulk?refresh=true
    { "index": { "_id": "1" } }
    { "image-vector": [1, 5, -20], "title-vector": [12, 50, -10, 0, 1], "title": "moose family", "file-type": "jpg" }
    { "index": { "_id": "2" } }
    { "image-vector": [42, 8, -15], "title-vector": [25, 1, 4, -12, 2], "title": "alpine lake", "file-type": "png" }
    { "index": { "_id": "3" } }
    { "image-vector": [15, 11, 23], "title-vector": [1, 5, 25, 50, 20], "title": "full moon", "file-type": "jpg" }
    ...
  3. Выполните поиск с использованием knn опции или запроса knn (для экспертов).

    resp = client.search(
        index="image-index",
        knn={
            "field": "image-vector",
            "query_vector": [
                -5,
                9,
                -12
            ],
            "k": 10,
            "num_candidates": 100
        },
        fields=[
            "title",
            "file-type"
        ],
    )
    print(resp)
    response = client.search(
      index: 'image-index',
      body: {
        knn: {
          field: 'image-vector',
          query_vector: [
            -5,
            9,
            -12
          ],
          k: 10,
          num_candidates: 100
        },
        fields: [
          'title',
          'file-type'
        ]
      }
    )
    puts response
    const response = await client.search({
      index: "image-index",
      knn: {
        field: "image-vector",
        query_vector: [-5, 9, -12],
        k: 10,
        num_candidates: 100,
      },
      fields: ["title", "file-type"],
    });
    console.log(response);
    POST image-index/_search
    {
      "knn": {
        "field": "image-vector",
        "query_vector": [-5, 9, -12],
        "k": 10,
        "num_candidates": 100
      },
      "fields": [ "title", "file-type" ]
    }

Значение документа _score определяется сходством запроса и векторного документа. См. similarity для получения дополнительной информации о вычислении оценок поиска kNN.

Поддержка приближенного поиска kNN была добавлена в версии 8.0. До этого поля dense_vector не поддерживали включение index в отображении. Если вы создали индекс до версии 8.0, содержащий поля dense_vector, то для поддержки приближенного поиска kNN данные необходимо переиндексировать с использованием нового отображения поля, которое устанавливает index: true, что является значением по умолчанию.

Настройка приближенного поиска kNN для скорости или точности

Для получения результатов API поиска kNN находит приблизительное количество кандидатов ближайших соседей на каждом фрагменте. Поиск вычисляет сходство этих кандидатных векторов с вектором запроса, выбирая k наиболее похожих результатов с каждого фрагмента. Затем поиск объединяет результаты с каждого фрагмента, чтобы вернуть глобальную верхнюю k ближайших соседей.

Вы можете увеличить num_candidates для более точных результатов за счет более медленной скорости поиска. Поиск с высоким значением num_candidates рассматривает больше кандидатов с каждого фрагмента. Это занимает больше времени, но у поиска выше вероятность найти истинные k лучшие ближайшие соседи.

Аналогично, вы можете уменьшить num_candidates для более быстрого поиска с потенциально менее точными результатами.

Приближенный поиск kNN с использованием векторов байтов

API приближенного поиска kNN поддерживает векторы с byte значением в дополнение к векторам с float значением. Используйте knn опцию для поиска поля dense_vector с element_type установленным на byte и включенной индексацией.

  1. Явно отобразите одно или несколько полей dense_vector с element_type установленным на byte и включенной индексацией.

    resp = client.indices.create(
        index="byte-image-index",
        mappings={
            "properties": {
                "byte-image-vector": {
                    "type": "dense_vector",
                    "element_type": "byte",
                    "dims": 2
                },
                "title": {
                    "type": "text"
                }
            }
        },
    )
    print(resp)
    response = client.indices.create(
      index: 'byte-image-index',
      body: {
        mappings: {
          properties: {
            "byte-image-vector": {
              type: 'dense_vector',
              element_type: 'byte',
              dims: 2
            },
            title: {
              type: 'text'
            }
          }
        }
      }
    )
    puts response
    const response = await client.indices.create({
      index: "byte-image-index",
      mappings: {
        properties: {
          "byte-image-vector": {
            type: "dense_vector",
            element_type: "byte",
            dims: 2,
          },
          title: {
            type: "text",
          },
        },
      },
    });
    console.log(response);
    PUT byte-image-index
    {
      "mappings": {
        "properties": {
          "byte-image-vector": {
            "type": "dense_vector",
            "element_type": "byte",
            "dims": 2
          },
          "title": {
            "type": "text"
          }
        }
      }
    }
  2. Индексируйте свои данные, гарантируя, что все векторные значения являются целыми числами в диапазоне [-128, 127].

    resp = client.bulk(
        index="byte-image-index",
        refresh=True,
        operations=[
            {
                "index": {
                    "_id": "1"
                }
            },
            {
                "byte-image-vector": [
                    5,
                    -20
                ],
                "title": "moose family"
            },
            {
                "index": {
                    "_id": "2"
                }
            },
            {
                "byte-image-vector": [
                    8,
                    -15
                ],
                "title": "alpine lake"
            },
            {
                "index": {
                    "_id": "3"
                }
            },
            {
                "byte-image-vector": [
                    11,
                    23
                ],
                "title": "full moon"
            }
        ],
    )
    print(resp)
    response = client.bulk(
      index: 'byte-image-index',
      refresh: true,
      body: [
        {
          index: {
            _id: '1'
          }
        },
        {
          "byte-image-vector": [
            5,
            -20
          ],
          title: 'moose family'
        },
        {
          index: {
            _id: '2'
          }
        },
        {
          "byte-image-vector": [
            8,
            -15
          ],
          title: 'alpine lake'
        },
        {
          index: {
            _id: '3'
          }
        },
        {
          "byte-image-vector": [
            11,
            23
          ],
          title: 'full moon'
        }
      ]
    )
    puts response
    const response = await client.bulk({
      index: "byte-image-index",
      refresh: "true",
      operations: [
        {
          index: {
            _id: "1",
          },
        },
        {
          "byte-image-vector": [5, -20],
          title: "moose family",
        },
        {
          index: {
            _id: "2",
          },
        },
        {
          "byte-image-vector": [8, -15],
          title: "alpine lake",
        },
        {
          index: {
            _id: "3",
          },
        },
        {
          "byte-image-vector": [11, 23],
          title: "full moon",
        },
      ],
    });
    console.log(response);
    POST byte-image-index/_bulk?refresh=true
    { "index": { "_id": "1" } }
    { "byte-image-vector": [5, -20], "title": "moose family" }
    { "index": { "_id": "2" } }
    { "byte-image-vector": [8, -15], "title": "alpine lake" }
    { "index": { "_id": "3" } }
    { "byte-image-vector": [11, 23], "title": "full moon" }
  3. Выполните поиск с использованием knn опции, гарантируя, что значения query_vector являются целыми числами в диапазоне [-128, 127].

    resp = client.search(
        index="byte-image-index",
        knn={
            "field": "byte-image-vector",
            "query_vector": [
                -5,
                9
            ],
            "k": 10,
            "num_candidates": 100
        },
        fields=[
            "title"
        ],
    )
    print(resp)
    response = client.search(
      index: 'byte-image-index',
      body: {
        knn: {
          field: 'byte-image-vector',
          query_vector: [
            -5,
            9
          ],
          k: 10,
          num_candidates: 100
        },
        fields: [
          'title'
        ]
      }
    )
    puts response
    const response = await client.search({
      index: "byte-image-index",
      knn: {
        field: "byte-image-vector",
        query_vector: [-5, 9],
        k: 10,
        num_candidates: 100,
      },
      fields: ["title"],
    });
    console.log(response);
    POST byte-image-index/_search
    {
      "knn": {
        "field": "byte-image-vector",
        "query_vector": [-5, 9],
        "k": 10,
        "num_candidates": 100
      },
      "fields": [ "title" ]
    }

Примечание: В дополнение к стандартному массиву байтов, вы также можете предоставить шестнадцатеричное значение строки для параметра query_vector. Например, запрос поиска выше также можно выразить следующим образом, что даст те же результаты

resp = client.search(
    index="byte-image-index",
    knn={
        "field": "byte-image-vector",
        "query_vector": "fb09",
        "k": 10,
        "num_candidates": 100
    },
    fields=[
        "title"
    ],
)
print(resp)
response = client.search(
  index: 'byte-image-index',
  body: {
    knn: {
      field: 'byte-image-vector',
      query_vector: 'fb09',
      k: 10,
      num_candidates: 100
    },
    fields: [
      'title'
    ]
  }
)
puts response
const response = await client.search({
  index: "byte-image-index",
  knn: {
    field: "byte-image-vector",
    query_vector: "fb09",
    k: 10,
    num_candidates: 100,
  },
  fields: ["title"],
});
console.log(response);
POST byte-image-index/_search
{
  "knn": {
    "field": "byte-image-vector",
    "query_vector": "fb09",
    "k": 10,
    "num_candidates": 100
  },
  "fields": [ "title" ]
}

Квантованный по байтам поиск kNN

Если вы хотите предоставить векторы float, но хотите экономии памяти векторов byte, вы можете использовать функцию квантования. Квантование позволяет предоставить векторы float, но внутренне они индексируются как векторы byte. Кроме того, исходные векторы float по-прежнему сохраняются в индексе.

Тип индекса по умолчанию для dense_vector — int8_hnsw.

Для использования квантования вы можете использовать тип индекса int8_hnsw или объект int4_hnsw в отображении dense_vector.

resp = client.indices.create(
    index="quantized-image-index",
    mappings={
        "properties": {
            "image-vector": {
                "type": "dense_vector",
                "element_type": "float",
                "dims": 2,
                "index": True,
                "index_options": {
                    "type": "int8_hnsw"
                }
            },
            "title": {
                "type": "text"
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'quantized-image-index',
  body: {
    mappings: {
      properties: {
        "image-vector": {
          type: 'dense_vector',
          element_type: 'float',
          dims: 2,
          index: true,
          index_options: {
            type: 'int8_hnsw'
          }
        },
        title: {
          type: 'text'
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "quantized-image-index",
  mappings: {
    properties: {
      "image-vector": {
        type: "dense_vector",
        element_type: "float",
        dims: 2,
        index: true,
        index_options: {
          type: "int8_hnsw",
        },
      },
      title: {
        type: "text",
      },
    },
  },
});
console.log(response);
PUT quantized-image-index
{
  "mappings": {
    "properties": {
      "image-vector": {
        "type": "dense_vector",
        "element_type": "float",
        "dims": 2,
        "index": true,
        "index_options": {
          "type": "int8_hnsw"
        }
      },
      "title": {
        "type": "text"
      }
    }
  }
}
  1. Индексируйте ваши float векторы.

    resp = client.bulk(
        index="quantized-image-index",
        refresh=True,
        operations=[
            {
                "index": {
                    "_id": "1"
                }
            },
            {
                "image-vector": [
                    0.1,
                    -2
                ],
                "title": "moose family"
            },
            {
                "index": {
                    "_id": "2"
                }
            },
            {
                "image-vector": [
                    0.75,
                    -1
                ],
                "title": "alpine lake"
            },
            {
                "index": {
                    "_id": "3"
                }
            },
            {
                "image-vector": [
                    1.2,
                    0.1
                ],
                "title": "full moon"
            }
        ],
    )
    print(resp)
    response = client.bulk(
      index: 'quantized-image-index',
      refresh: true,
      body: [
        {
          index: {
            _id: '1'
          }
        },
        {
          "image-vector": [
            0.1,
            -2
          ],
          title: 'moose family'
        },
        {
          index: {
            _id: '2'
          }
        },
        {
          "image-vector": [
            0.75,
            -1
          ],
          title: 'alpine lake'
        },
        {
          index: {
            _id: '3'
          }
        },
        {
          "image-vector": [
            1.2,
            0.1
          ],
          title: 'full moon'
        }
      ]
    )
    puts response
    const response = await client.bulk({
      index: "quantized-image-index",
      refresh: "true",
      operations: [
        {
          index: {
            _id: "1",
          },
        },
        {
          "image-vector": [0.1, -2],
          title: "moose family",
        },
        {
          index: {
            _id: "2",
          },
        },
        {
          "image-vector": [0.75, -1],
          title: "alpine lake",
        },
        {
          index: {
            _id: "3",
          },
        },
        {
          "image-vector": [1.2, 0.1],
          title: "full moon",
        },
      ],
    });
    console.log(response);
    POST quantized-image-index/_bulk?refresh=true
    { "index": { "_id": "1" } }
    { "image-vector": [0.1, -2], "title": "moose family" }
    { "index": { "_id": "2" } }
    { "image-vector": [0.75, -1], "title": "alpine lake" }
    { "index": { "_id": "3" } }
    { "image-vector": [1.2, 0.1], "title": "full moon" }
  2. Запустите поиск, используя knn параметр. При поиске вектор float автоматически квантизуется в вектор byte.

    resp = client.search(
        index="quantized-image-index",
        knn={
            "field": "image-vector",
            "query_vector": [
                0.1,
                -2
            ],
            "k": 10,
            "num_candidates": 100
        },
        fields=[
            "title"
        ],
    )
    print(resp)
    response = client.search(
      index: 'quantized-image-index',
      body: {
        knn: {
          field: 'image-vector',
          query_vector: [
            0.1,
            -2
          ],
          k: 10,
          num_candidates: 100
        },
        fields: [
          'title'
        ]
      }
    )
    puts response
    const response = await client.search({
      index: "quantized-image-index",
      knn: {
        field: "image-vector",
        query_vector: [0.1, -2],
        k: 10,
        num_candidates: 100,
      },
      fields: ["title"],
    });
    console.log(response);
    POST quantized-image-index/_search
    {
      "knn": {
        "field": "image-vector",
        "query_vector": [0.1, -2],
        "k": 10,
        "num_candidates": 100
      },
      "fields": [ "title" ]
    }

Так как исходные float векторы все еще сохранены в индексе, вы можете дополнительно использовать их для повторной оценки. Это значит, что вы можете быстро выполнить поиск по всем векторам, используя индекс int8_hnsw, а затем переоценить только верхние k результаты. Это обеспечивает наилучшее из обоих миров: быстрый поиск и точную оценку.

resp = client.search(
    index="quantized-image-index",
    knn={
        "field": "image-vector",
        "query_vector": [
            0.1,
            -2
        ],
        "k": 15,
        "num_candidates": 100
    },
    fields=[
        "title"
    ],
    rescore={
        "window_size": 10,
        "query": {
            "rescore_query": {
                "script_score": {
                    "query": {
                        "match_all": {}
                    },
                    "script": {
                        "source": "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
                        "params": {
                            "query_vector": [
                                0.1,
                                -2
                            ]
                        }
                    }
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'quantized-image-index',
  body: {
    knn: {
      field: 'image-vector',
      query_vector: [
        0.1,
        -2
      ],
      k: 15,
      num_candidates: 100
    },
    fields: [
      'title'
    ],
    rescore: {
      window_size: 10,
      query: {
        rescore_query: {
          script_score: {
            query: {
              match_all: {}
            },
            script: {
              source: "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
              params: {
                query_vector: [
                  0.1,
                  -2
                ]
              }
            }
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "quantized-image-index",
  knn: {
    field: "image-vector",
    query_vector: [0.1, -2],
    k: 15,
    num_candidates: 100,
  },
  fields: ["title"],
  rescore: {
    window_size: 10,
    query: {
      rescore_query: {
        script_score: {
          query: {
            match_all: {},
          },
          script: {
            source:
              "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
            params: {
              query_vector: [0.1, -2],
            },
          },
        },
      },
    },
  },
});
console.log(response);
POST quantized-image-index/_search
{
  "knn": {
    "field": "image-vector",
    "query_vector": [0.1, -2],
    "k": 15,
    "num_candidates": 100
  },
  "fields": [ "title" ],
  "rescore": {
    "window_size": 10,
    "query": {
      "rescore_query": {
        "script_score": {
          "query": {
            "match_all": {}
          },
          "script": {
            "source": "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
            "params": {
              "query_vector": [0.1, -2]
            }
          }
        }
      }
    }
  }
}

Отфильтрованный поиск kNN

API поиска kNN поддерживает ограничение поиска с помощью фильтра. Поиск вернёт верхние k документов, которые также соответствуют запросу фильтра.

Следующий запрос выполняет приближенный поиск kNN, отфильтрованный по полю file-type:

resp = client.search(
    index="image-index",
    knn={
        "field": "image-vector",
        "query_vector": [
            54,
            10,
            -2
        ],
        "k": 5,
        "num_candidates": 50,
        "filter": {
            "term": {
                "file-type": "png"
            }
        }
    },
    fields=[
        "title"
    ],
    source=False,
)
print(resp)
response = client.search(
  index: 'image-index',
  body: {
    knn: {
      field: 'image-vector',
      query_vector: [
        54,
        10,
        -2
      ],
      k: 5,
      num_candidates: 50,
      filter: {
        term: {
          "file-type": 'png'
        }
      }
    },
    fields: [
      'title'
    ],
    _source: false
  }
)
puts response
const response = await client.search({
  index: "image-index",
  knn: {
    field: "image-vector",
    query_vector: [54, 10, -2],
    k: 5,
    num_candidates: 50,
    filter: {
      term: {
        "file-type": "png",
      },
    },
  },
  fields: ["title"],
  _source: false,
});
console.log(response);
POST image-index/_search
{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  },
  "fields": ["title"],
  "_source": false
}

Фильтр применяется во время приближённого поиска kNN, чтобы гарантировать, что k соответствующих документов будут возвращены. Это отличается от подхода пост-фильтрации, где фильтр применяется после завершения приближённого поиска kNN. Недостаток пост-фильтрации состоит в том, что иногда возвращается меньше k результатов, даже когда есть достаточно соответствующих документов.

Приближенный поиск kNN и фильтрация

В отличие от традиционной фильтрации запросов, где более строгие фильтры обычно приводят к более быстрым запросам, применение фильтров в приближённом поиске kNN с индексом HNSW может снизить производительность. Это происходит потому, что поиск в графе HNSW требует дополнительных исследований, чтобы получить num_candidates документы, удовлетворяющие критериям фильтра.

Чтобы избежать существенных проблем с производительностью, Lucene реализует следующие стратегии по сегментам:

  • Если количество отфильтрованных документов меньше или равно num_candidates, поиск обходит граф HNSW и использует поиск методом полного перебора по отфильтрованным документам.
  • При исследовании графа HNSW, если количество исследованных узлов превышает количество документов, удовлетворяющих фильтру, поиск прекратит исследование графа и переключится на поиск методом полного перебора по отфильтрованным документам.

Комбинирование приближённого поиска kNN с другими функциями

Вы можете выполнить гибридный поиск, указав knn параметр и query:

resp = client.search(
    index="image-index",
    query={
        "match": {
            "title": {
                "query": "mountain lake",
                "boost": 0.9
            }
        }
    },
    knn={
        "field": "image-vector",
        "query_vector": [
            54,
            10,
            -2
        ],
        "k": 5,
        "num_candidates": 50,
        "boost": 0.1
    },
    size=10,
)
print(resp)
response = client.search(
  index: 'image-index',
  body: {
    query: {
      match: {
        title: {
          query: 'mountain lake',
          boost: 0.9
        }
      }
    },
    knn: {
      field: 'image-vector',
      query_vector: [
        54,
        10,
        -2
      ],
      k: 5,
      num_candidates: 50,
      boost: 0.1
    },
    size: 10
  }
)
puts response
const response = await client.search({
  index: "image-index",
  query: {
    match: {
      title: {
        query: "mountain lake",
        boost: 0.9,
      },
    },
  },
  knn: {
    field: "image-vector",
    query_vector: [54, 10, -2],
    k: 5,
    num_candidates: 50,
    boost: 0.1,
  },
  size: 10,
});
console.log(response);
POST image-index/_search
{
  "query": {
    "match": {
      "title": {
        "query": "mountain lake",
        "boost": 0.9
      }
    }
  },
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "boost": 0.1
  },
  "size": 10
}

Этот поиск находит глобально наилучшие совпадения векторов k = 5, объединяет их с совпадениями из запроса match и, наконец, возвращает 10 документов с наибольшим рейтингом. knn и query совпадения объединяются через дизъюнкцию, как если бы вы использовали булеву операцию или между ними. Верхние k векторные результаты представляют собой глобальных ближайших соседей по всем фрагментам индекса.

Оценка каждого результата является суммой knn и query оценок. Вы можете указать значение boost, чтобы придать вес каждой оценке в сумме. В приведённом выше примере оценки будут вычисляться как

score = 0.9 * match_score + 0.1 * knn_score

knn параметр также может быть использован с aggregations. В общем случае Elasticsearch вычисляет агрегации по всем документам, соответствующим запросу. Таким образом, для приближённого поиска kNN агрегации рассчитываются по верхним k ближайшим документам. Если в поиске также используется query, тогда агрегации рассчитываются по объединённому набору knn и query совпадений.

Выполнение семантического поиска

Поиск kNN позволяет выполнять семантический поиск, используя предварительно развернутую модель встраивания текста. Вместо буквального соответствия по поисковым терминам, семантический поиск извлекает результаты на основе намерения и контекстуального смысла поискового запроса.

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

Для выполнения семантического поиска:

  • вам нужен индекс, содержащий плотное векторное представление входных данных для поиска,
  • вы должны использовать ту же модель встраивания текста для поиска, что и для создания плотных векторов из входных данных,
  • развертывание модели NLP встраивания текста должно быть запущено.

Обратитесь к развернутой модели встраивания текста или к развертыванию модели в объекте query_vector_builder и укажите поисковый запрос как model_text:

(...)
{
  "knn": {
    "field": "dense-vector-field",
    "k": 10,
    "num_candidates": 100,
    "query_vector_builder": {
      "text_embedding": { 
        "model_id": "my-text-embedding-model", 
        "model_text": "The opposite of blue" 
      }
    }
  }
}
(...)

Задача обработки естественного языка для выполнения. Она должна быть text_embedding.

Идентификатор модели встраивания текста, который нужно использовать для генерации плотных векторов из строки запроса. Используйте ту же модель, которая сгенерировала встраивания из входного текста в индексе, по которому вы выполняете поиск. Вместо этого можно использовать значение deployment_id в аргументе model_id.

Строка запроса, из которой модель генерирует плотное векторное представление.

Дополнительную информацию о развертывании обученной модели и её использовании для создания векторных представлений текста см. в этом примере.

Поиск по нескольким полям kNN

В дополнение к гибридному поиску, вы можете искать сразу по нескольким полям векторов kNN:

resp = client.search(
    index="image-index",
    query={
        "match": {
            "title": {
                "query": "mountain lake",
                "boost": 0.9
            }
        }
    },
    knn=[
        {
            "field": "image-vector",
            "query_vector": [
                54,
                10,
                -2
            ],
            "k": 5,
            "num_candidates": 50,
            "boost": 0.1
        },
        {
            "field": "title-vector",
            "query_vector": [
                1,
                20,
                -52,
                23,
                10
            ],
            "k": 10,
            "num_candidates": 10,
            "boost": 0.5
        }
    ],
    size=10,
)
print(resp)
response = client.search(
  index: 'image-index',
  body: {
    query: {
      match: {
        title: {
          query: 'mountain lake',
          boost: 0.9
        }
      }
    },
    knn: [
      {
        field: 'image-vector',
        query_vector: [
          54,
          10,
          -2
        ],
        k: 5,
        num_candidates: 50,
        boost: 0.1
      },
      {
        field: 'title-vector',
        query_vector: [
          1,
          20,
          -52,
          23,
          10
        ],
        k: 10,
        num_candidates: 10,
        boost: 0.5
      }
    ],
    size: 10
  }
)
puts response
const response = await client.search({
  index: "image-index",
  query: {
    match: {
      title: {
        query: "mountain lake",
        boost: 0.9,
      },
    },
  },
  knn: [
    {
      field: "image-vector",
      query_vector: [54, 10, -2],
      k: 5,
      num_candidates: 50,
      boost: 0.1,
    },
    {
      field: "title-vector",
      query_vector: [1, 20, -52, 23, 10],
      k: 10,
      num_candidates: 10,
      boost: 0.5,
    },
  ],
  size: 10,
});
console.log(response);
POST image-index/_search
{
  "query": {
    "match": {
      "title": {
        "query": "mountain lake",
        "boost": 0.9
      }
    }
  },
  "knn": [ {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "boost": 0.1
  },
  {
    "field": "title-vector",
    "query_vector": [1, 20, -52, 23, 10],
    "k": 10,
    "num_candidates": 10,
    "boost": 0.5
  }],
  "size": 10
}

Этот поиск находит глобально наилучшие совпадения векторов k = 5 для image-vector и глобальные k = 10 для title-vector. Эти значения затем объединяются с результатами запроса match, и возвращаются 10 лучших документов. Несколько knn записей и query совпадения объединяются посредством дизъюнкции, как если бы вы использовали булеву операцию или между ними. Верхние k векторные результаты представляют собой глобальных ближайших соседей по всем фрагментам индекса.

Оценка документа с указанными выше коэффициентами усиления будет следующей:

score = 0.9 * match_score + 0.1 * knn_score_image-vector + 0.5 * knn_score_title-vector

Поиск kNN с ожидаемой степенью сходства

Хотя kNN является мощным инструментом, он всегда пытается вернуть k ближайших соседей. Следовательно, при использовании knn с filter, вы могли бы отфильтровать все релевантные документы и оставить только нерелевантные для поиска. В этой ситуации knn всё равно сделает всё возможное, чтобы вернуть k ближайших соседей, даже если эти соседи могут находиться далеко в векторном пространстве.

Для устранения этой проблемы доступен параметр similarity в предложении knn. Это минимальное необходимое сходство для того, чтобы вектор считался совпадением. knn процесс поиска с этим параметром выглядит так:

  • Применить любые предоставленные пользователем filter запросы
  • Исследовать векторное пространство, чтобы получить k векторы
  • Не возвращать никакие векторы, которые находятся дальше, чем заданное similarity

similarity — это истинное сходство до того, как оно было преобразовано в _score и применено усиление.

Для каждого настроенного сходства, вот соответствующая инвертированная _score функция. Это необходимо, если вы хотите фильтровать с точки зрения _score, вы можете выполнить эту небольшую трансформацию для правильного отклонения нерелевантных результатов.

  • l2_norm: sqrt((1 / _score) - 1)
  • cosine: (2 * _score) - 1
  • dot_product: (2 * _score) - 1
  • max_inner_product:

    • _score < 1: 1 - (1 / _score)
    • _score >= 1: _score - 1

Вот пример. В этом примере мы ищем заданные query_vector для k ближайших соседей. Однако с применением filter и требованием, чтобы найденные векторы имели как минимум заданное similarity между ними.

resp = client.search(
    index="image-index",
    knn={
        "field": "image-vector",
        "query_vector": [
            1,
            5,
            -20
        ],
        "k": 5,
        "num_candidates": 50,
        "similarity": 36,
        "filter": {
            "term": {
                "file-type": "png"
            }
        }
    },
    fields=[
        "title"
    ],
    source=False,
)
print(resp)
response = client.search(
  index: 'image-index',
  body: {
    knn: {
      field: 'image-vector',
      query_vector: [
        1,
        5,
        -20
      ],
      k: 5,
      num_candidates: 50,
      similarity: 36,
      filter: {
        term: {
          "file-type": 'png'
        }
      }
    },
    fields: [
      'title'
    ],
    _source: false
  }
)
puts response
const response = await client.search({
  index: "image-index",
  knn: {
    field: "image-vector",
    query_vector: [1, 5, -20],
    k: 5,
    num_candidates: 50,
    similarity: 36,
    filter: {
      term: {
        "file-type": "png",
      },
    },
  },
  fields: ["title"],
  _source: false,
});
console.log(response);
POST image-index/_search
{
  "knn": {
    "field": "image-vector",
    "query_vector": [1, 5, -20],
    "k": 5,
    "num_candidates": 50,
    "similarity": 36,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  },
  "fields": ["title"],
  "_source": false
}

В нашем наборе данных, единственный документ с типом файла png имеет вектор [42, 8, -15]. Расстояние l2_norm между [42, 8, -15] и [1, 5, -20] равно 41.412, что больше, чем настроенное сходство 36. Это означает, что этот поиск не вернёт совпадений.

Вложенный поиск kNN

Часто текст превышает предел токенов конкретной модели и требует фрагментации перед построением встраиваний для отдельных фрагментов. При использовании nested с dense_vector, вы можете достичь поиска ближайшего фрагмента без копирования метаданных документа верхнего уровня.

Вот простой индекс векторов фрагментов, который хранит векторы и некоторые метаданные верхнего уровня для фильтрации.

resp = client.indices.create(
    index="passage_vectors",
    mappings={
        "properties": {
            "full_text": {
                "type": "text"
            },
            "creation_time": {
                "type": "date"
            },
            "paragraph": {
                "type": "nested",
                "properties": {
                    "vector": {
                        "type": "dense_vector",
                        "dims": 2,
                        "index_options": {
                            "type": "hnsw"
                        }
                    },
                    "text": {
                        "type": "text",
                        "index": False
                    }
                }
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'passage_vectors',
  body: {
    mappings: {
      properties: {
        full_text: {
          type: 'text'
        },
        creation_time: {
          type: 'date'
        },
        paragraph: {
          type: 'nested',
          properties: {
            vector: {
              type: 'dense_vector',
              dims: 2,
              index_options: {
                type: 'hnsw'
              }
            },
            text: {
              type: 'text',
              index: false
            }
          }
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "passage_vectors",
  mappings: {
    properties: {
      full_text: {
        type: "text",
      },
      creation_time: {
        type: "date",
      },
      paragraph: {
        type: "nested",
        properties: {
          vector: {
            type: "dense_vector",
            dims: 2,
            index_options: {
              type: "hnsw",
            },
          },
          text: {
            type: "text",
            index: false,
          },
        },
      },
    },
  },
});
console.log(response);
PUT passage_vectors
{
    "mappings": {
        "properties": {
            "full_text": {
                "type": "text"
            },
            "creation_time": {
                "type": "date"
            },
            "paragraph": {
                "type": "nested",
                "properties": {
                    "vector": {
                        "type": "dense_vector",
                        "dims": 2,
                        "index_options": {
                            "type": "hnsw"
                        }
                    },
                    "text": {
                        "type": "text",
                        "index": false
                    }
                }
            }
        }
    }
}

С указанным выше отображением, мы можем индексировать несколько векторов фрагментов вместе с хранением текста отдельных фрагментов.

resp = client.bulk(
    index="passage_vectors",
    refresh=True,
    operations=[
        {
            "index": {
                "_id": "1"
            }
        },
        {
            "full_text": "first paragraph another paragraph",
            "creation_time": "2019-05-04",
            "paragraph": [
                {
                    "vector": [
                        0.45,
                        45
                    ],
                    "text": "first paragraph",
                    "paragraph_id": "1"
                },
                {
                    "vector": [
                        0.8,
                        0.6
                    ],
                    "text": "another paragraph",
                    "paragraph_id": "2"
                }
            ]
        },
        {
            "index": {
                "_id": "2"
            }
        },
        {
            "full_text": "number one paragraph number two paragraph",
            "creation_time": "2020-05-04",
            "paragraph": [
                {
                    "vector": [
                        1.2,
                        4.5
                    ],
                    "text": "number one paragraph",
                    "paragraph_id": "1"
                },
                {
                    "vector": [
                        -1,
                        42
                    ],
                    "text": "number two paragraph",
                    "paragraph_id": "2"
                }
            ]
        }
    ],
)
print(resp)
response = client.bulk(
  index: 'passage_vectors',
  refresh: true,
  body: [
    {
      index: {
        _id: '1'
      }
    },
    {
      full_text: 'first paragraph another paragraph',
      creation_time: '2019-05-04',
      paragraph: [
        {
          vector: [
            0.45,
            45
          ],
          text: 'first paragraph',
          paragraph_id: '1'
        },
        {
          vector: [
            0.8,
            0.6
          ],
          text: 'another paragraph',
          paragraph_id: '2'
        }
      ]
    },
    {
      index: {
        _id: '2'
      }
    },
    {
      full_text: 'number one paragraph number two paragraph',
      creation_time: '2020-05-04',
      paragraph: [
        {
          vector: [
            1.2,
            4.5
          ],
          text: 'number one paragraph',
          paragraph_id: '1'
        },
        {
          vector: [
            -1,
            42
          ],
          text: 'number two paragraph',
          paragraph_id: '2'
        }
      ]
    }
  ]
)
puts response
const response = await client.bulk({
  index: "passage_vectors",
  refresh: "true",
  operations: [
    {
      index: {
        _id: "1",
      },
    },
    {
      full_text: "first paragraph another paragraph",
      creation_time: "2019-05-04",
      paragraph: [
        {
          vector: [0.45, 45],
          text: "first paragraph",
          paragraph_id: "1",
        },
        {
          vector: [0.8, 0.6],
          text: "another paragraph",
          paragraph_id: "2",
        },
      ],
    },
    {
      index: {
        _id: "2",
      },
    },
    {
      full_text: "number one paragraph number two paragraph",
      creation_time: "2020-05-04",
      paragraph: [
        {
          vector: [1.2, 4.5],
          text: "number one paragraph",
          paragraph_id: "1",
        },
        {
          vector: [-1, 42],
          text: "number two paragraph",
          paragraph_id: "2",
        },
      ],
    },
  ],
});
console.log(response);
POST passage_vectors/_bulk?refresh=true
{ "index": { "_id": "1" } }
{ "full_text": "first paragraph another paragraph", "creation_time": "2019-05-04", "paragraph": [ { "vector": [ 0.45, 45 ], "text": "first paragraph", "paragraph_id": "1" }, { "vector": [ 0.8, 0.6 ], "text": "another paragraph", "paragraph_id": "2" } ] }
{ "index": { "_id": "2" } }
{ "full_text": "number one paragraph number two paragraph", "creation_time": "2020-05-04", "paragraph": [ { "vector": [ 1.2, 4.5 ], "text": "number one paragraph", "paragraph_id": "1" }, { "vector": [ -1, 42 ], "text": "number two paragraph", "paragraph_id": "2" } ] }

Запрос будет очень похож на типичный поиск kNN:

resp = client.search(
    index="passage_vectors",
    fields=[
        "full_text",
        "creation_time"
    ],
    source=False,
    knn={
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2
    },
)
print(resp)
response = client.search(
  index: 'passage_vectors',
  body: {
    fields: [
      'full_text',
      'creation_time'
    ],
    _source: false,
    knn: {
      query_vector: [
        0.45,
        45
      ],
      field: 'paragraph.vector',
      k: 2,
      num_candidates: 2
    }
  }
)
puts response
const response = await client.search({
  index: "passage_vectors",
  fields: ["full_text", "creation_time"],
  _source: false,
  knn: {
    query_vector: [0.45, 45],
    field: "paragraph.vector",
    k: 2,
    num_candidates: 2,
  },
});
console.log(response);
POST passage_vectors/_search
{
    "fields": ["full_text", "creation_time"],
    "_source": false,
    "knn": {
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2
    }
}

Обратите внимание ниже, что, хотя у нас есть 4 вектора в целом, мы по-прежнему возвращаем два документа. Поиск kNN по вложенным dense_vectors всегда будет разнообразить лучшие результаты по документам верхнего уровня. То есть, будет возвращено несколько документов верхнего уровня, упорядоченных по вектору ближайшего фрагмента (например, "paragraph.vector").

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "passage_vectors",
                "_id": "1",
                "_score": 1.0,
                "fields": {
                    "creation_time": [
                        "2019-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "first paragraph another paragraph"
                    ]
                }
            },
            {
                "_index": "passage_vectors",
                "_id": "2",
                "_score": 0.9997144,
                "fields": {
                    "creation_time": [
                        "2020-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "number one paragraph number two paragraph"
                    ]
                }
            }
        ]
    }
}

Что если вы хотите отфильтровать по некоторым метаданным документа верхнего уровня? Вы можете сделать это, добавив filter к вашему предложению knn.

filter всегда будет по метаданным документа верхнего уровня. Это означает, что вы не можете фильтровать по метаданным поля nested.

resp = client.search(
    index="passage_vectors",
    fields=[
        "creation_time",
        "full_text"
    ],
    source=False,
    knn={
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2,
        "filter": {
            "bool": {
                "filter": [
                    {
                        "range": {
                            "creation_time": {
                                "gte": "2019-05-01",
                                "lte": "2019-05-05"
                            }
                        }
                    }
                ]
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'passage_vectors',
  body: {
    fields: [
      'creation_time',
      'full_text'
    ],
    _source: false,
    knn: {
      query_vector: [
        0.45,
        45
      ],
      field: 'paragraph.vector',
      k: 2,
      num_candidates: 2,
      filter: {
        bool: {
          filter: [
            {
              range: {
                creation_time: {
                  gte: '2019-05-01',
                  lte: '2019-05-05'
                }
              }
            }
          ]
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "passage_vectors",
  fields: ["creation_time", "full_text"],
  _source: false,
  knn: {
    query_vector: [0.45, 45],
    field: "paragraph.vector",
    k: 2,
    num_candidates: 2,
    filter: {
      bool: {
        filter: [
          {
            range: {
              creation_time: {
                gte: "2019-05-01",
                lte: "2019-05-05",
              },
            },
          },
        ],
      },
    },
  },
});
console.log(response);
POST passage_vectors/_search
{
    "fields": [
        "creation_time",
        "full_text"
    ],
    "_source": false,
    "knn": {
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2,
        "filter": {
            "bool": {
                "filter": [
                    {
                        "range": {
                            "creation_time": {
                                "gte": "2019-05-01",
                                "lte": "2019-05-05"
                            }
                        }
                    }
                ]
            }
        }
    }
}

Теперь мы отфильтровали по верхнему уровню "creation_time", и только один документ попадает в этот диапазон.

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "passage_vectors",
                "_id": "1",
                "_score": 1.0,
                "fields": {
                    "creation_time": [
                        "2019-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "first paragraph another paragraph"
                    ]
                }
            }
        ]
    }
}

Вложенный поиск kNN с внутренними совпадениями

Кроме того, если вы хотите извлечь ближайший фрагмент для сопоставленного документа, вы можете указать inner_hits в предложении knn.

При использовании inner_hits и нескольких knn предложений, обязательно укажите поле inner_hits.name. В противном случае может произойти конфликт имён и запрос поиска завершится ошибкой.

resp = client.search(
    index="passage_vectors",
    fields=[
        "creation_time",
        "full_text"
    ],
    source=False,
    knn={
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2,
        "inner_hits": {
            "_source": False,
            "fields": [
                "paragraph.text"
            ],
            "size": 1
        }
    },
)
print(resp)
const response = await client.search({
  index: "passage_vectors",
  fields: ["creation_time", "full_text"],
  _source: false,
  knn: {
    query_vector: [0.45, 45],
    field: "paragraph.vector",
    k: 2,
    num_candidates: 2,
    inner_hits: {
      _source: false,
      fields: ["paragraph.text"],
      size: 1,
    },
  },
});
console.log(response);
POST passage_vectors/_search
{
    "fields": [
        "creation_time",
        "full_text"
    ],
    "_source": false,
    "knn": {
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2,
        "inner_hits": {
            "_source": false,
            "fields": [
                "paragraph.text"
            ],
            "size": 1
        }
    }
}

Теперь результат будет содержать ближайший найденный абзац при поиске.

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "passage_vectors",
                "_id": "1",
                "_score": 1.0,
                "fields": {
                    "creation_time": [
                        "2019-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "first paragraph another paragraph"
                    ]
                },
                "inner_hits": {
                    "paragraph": {
                        "hits": {
                            "total": {
                                "value": 2,
                                "relation": "eq"
                            },
                            "max_score": 1.0,
                            "hits": [
                                {
                                    "_index": "passage_vectors",
                                    "_id": "1",
                                    "_nested": {
                                        "field": "paragraph",
                                        "offset": 0
                                    },
                                    "_score": 1.0,
                                    "fields": {
                                        "paragraph": [
                                            {
                                                "text": [
                                                    "first paragraph"
                                                ]
                                            }
                                        ]
                                    }
                                }
                            ]
                        }
                    }
                }
            },
            {
                "_index": "passage_vectors",
                "_id": "2",
                "_score": 0.9997144,
                "fields": {
                    "creation_time": [
                        "2020-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "number one paragraph number two paragraph"
                    ]
                },
                "inner_hits": {
                    "paragraph": {
                        "hits": {
                            "total": {
                                "value": 2,
                                "relation": "eq"
                            },
                            "max_score": 0.9997144,
                            "hits": [
                                {
                                    "_index": "passage_vectors",
                                    "_id": "2",
                                    "_nested": {
                                        "field": "paragraph",
                                        "offset": 1
                                    },
                                    "_score": 0.9997144,
                                    "fields": {
                                        "paragraph": [
                                            {
                                                "text": [
                                                    "number two paragraph"
                                                ]
                                            }
                                        ]
                                    }
                                }
                            ]
                        }
                    }
                }
            }
        ]
    }
}

Учитывая при индексировании

Для приближенного поиска kNN Elasticsearch хранит значения плотных векторов каждого сегмента в виде графа HNSW. Индексирование векторов для приближенного поиска kNN может занимать существенное время из-за дороговизны построения этих графов. Возможно, вам потребуется увеличить таймаут запросов клиента для индекса и массовых запросов. Руководство по настройке приближенного поиска kNN содержит важную информацию по производительности индекса и о том, как конфигурация индекса может повлиять на производительность поиска.

Помимо параметров настройки на этапе поиска, алгоритм HNSW имеет параметры на этапе индексирования, которые обеспечивают баланс между стоимостью построения графа, скоростью поиска и точностью. При настройке отображения dense_vector вы можете использовать аргумент index_options для настройки этих параметров:

resp = client.indices.create(
    index="image-index",
    mappings={
        "properties": {
            "image-vector": {
                "type": "dense_vector",
                "dims": 3,
                "similarity": "l2_norm",
                "index_options": {
                    "type": "hnsw",
                    "m": 32,
                    "ef_construction": 100
                }
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'image-index',
  body: {
    mappings: {
      properties: {
        "image-vector": {
          type: 'dense_vector',
          dims: 3,
          similarity: 'l2_norm',
          index_options: {
            type: 'hnsw',
            m: 32,
            ef_construction: 100
          }
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "image-index",
  mappings: {
    properties: {
      "image-vector": {
        type: "dense_vector",
        dims: 3,
        similarity: "l2_norm",
        index_options: {
          type: "hnsw",
          m: 32,
          ef_construction: 100,
        },
      },
    },
  },
});
console.log(response);
PUT image-index
{
  "mappings": {
    "properties": {
      "image-vector": {
        "type": "dense_vector",
        "dims": 3,
        "similarity": "l2_norm",
        "index_options": {
          "type": "hnsw",
          "m": 32,
          "ef_construction": 100
        }
      }
    }
  }
}

Ограничения для приближенного поиска kNN

  • При использовании поиска kNN в межкластерном поиске, опция ccs_minimize_roundtrips не поддерживается.
  • Elasticsearch использует алгоритм HNSW для поддержки эффективного поиска kNN. Как и большинство алгоритмов kNN, HNSW — это приближённый метод, который жертвовает точностью результатов ради повышения скорости поиска. Это означает, что возвращаемые результаты не всегда являются истинными k ближайшими соседями.

Приближённый поиск kNN всегда использует тип поиска dfs_query_then_fetch, чтобы собрать глобальные лучшие k совпадения по фрагментам. Вы не можете явно установить search_type при выполнении поиска kNN.

Точный поиск kNN

Чтобы выполнить точный поиск kNN, используйте запрос script_score с функцией вектора.

  1. Явно отобразите одно или несколько полей dense_vector. Если вы не собираетесь использовать поле для приближённого поиска kNN, установите опцию отображения index в значение false. Это может значительно улучшить скорость индексирования.

    resp = client.indices.create(
        index="product-index",
        mappings={
            "properties": {
                "product-vector": {
                    "type": "dense_vector",
                    "dims": 5,
                    "index": False
                },
                "price": {
                    "type": "long"
                }
            }
        },
    )
    print(resp)
    response = client.indices.create(
      index: 'product-index',
      body: {
        mappings: {
          properties: {
            "product-vector": {
              type: 'dense_vector',
              dims: 5,
              index: false
            },
            price: {
              type: 'long'
            }
          }
        }
      }
    )
    puts response
    const response = await client.indices.create({
      index: "product-index",
      mappings: {
        properties: {
          "product-vector": {
            type: "dense_vector",
            dims: 5,
            index: false,
          },
          price: {
            type: "long",
          },
        },
      },
    });
    console.log(response);
    PUT product-index
    {
      "mappings": {
        "properties": {
          "product-vector": {
            "type": "dense_vector",
            "dims": 5,
            "index": false
          },
          "price": {
            "type": "long"
          }
        }
      }
    }
  2. Проиндексируйте ваши данные.

    POST product-index/_bulk?refresh=true
    { "index": { "_id": "1" } }
    { "product-vector": [230.0, 300.33, -34.8988, 15.555, -200.0], "price": 1599 }
    { "index": { "_id": "2" } }
    { "product-vector": [-0.5, 100.0, -13.0, 14.8, -156.0], "price": 799 }
    { "index": { "_id": "3" } }
    { "product-vector": [0.5, 111.3, -13.0, 14.8, -156.0], "price": 1099 }
    ...
  3. Используйте API поиска поиска, чтобы выполнить запрос script_score, содержащий функцию вектора.

    Чтобы ограничить количество сопоставленных документов, переданных функции вектора, рекомендуется указать фильтр запроса в параметре script_score.query. При необходимости, вы можете использовать запрос match_all в этом параметре для сопоставления всех документов. Однако сопоставление всех документов может значительно увеличить задержку поиска.

    resp = client.search(
        index="product-index",
        query={
            "script_score": {
                "query": {
                    "bool": {
                        "filter": {
                            "range": {
                                "price": {
                                    "gte": 1000
                                }
                            }
                        }
                    }
                },
                "script": {
                    "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
                    "params": {
                        "queryVector": [
                            -0.5,
                            90,
                            -10,
                            14.8,
                            -156
                        ]
                    }
                }
            }
        },
    )
    print(resp)
    response = client.search(
      index: 'product-index',
      body: {
        query: {
          script_score: {
            query: {
              bool: {
                filter: {
                  range: {
                    price: {
                      gte: 1000
                    }
                  }
                }
              }
            },
            script: {
              source: "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
              params: {
                "queryVector": [
                  -0.5,
                  90,
                  -10,
                  14.8,
                  -156
                ]
              }
            }
          }
        }
      }
    )
    puts response
    const response = await client.search({
      index: "product-index",
      query: {
        script_score: {
          query: {
            bool: {
              filter: {
                range: {
                  price: {
                    gte: 1000,
                  },
                },
              },
            },
          },
          script: {
            source: "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
            params: {
              queryVector: [-0.5, 90, -10, 14.8, -156],
            },
          },
        },
      },
    });
    console.log(response);
    POST product-index/_search
    {
      "query": {
        "script_score": {
          "query" : {
            "bool" : {
              "filter" : {
                "range" : {
                  "price" : {
                    "gte": 1000
                  }
                }
              }
            }
          },
          "script": {
            "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
            "params": {
              "queryVector": [-0.5, 90.0, -10, 14.8, -156.0]
            }
          }
        }
      }
    }

Перевыборка и повторная оценка для квантованных векторов

Все формы квантования приведут к некоторой потере точности, и по мере увеличения уровня квантования потеря точности также будет увеличиваться. Как правило, мы обнаружили, что: - int8 требует минимальной, если вообще какой-либо, переоценки - int4 требует некоторой переоценки для достижения большей точности и более высоких значений отзыва в сценариях с большими объёмами. Как правило, выборочное увеличение на 1,5-2 раза восстанавливает большую часть потери точности. - bbq требует переоценки, за исключением исключительно больших индексов или моделей, специально разработанных для квантования. Мы обнаружили, что выборочное увеличение на 3-5 раз обычно достаточно. Но для меньшего количества измерений или векторов, которые плохо квантуются, может потребоваться большее выборочное увеличение.

Существует два основных способа выборочного увеличения и переоценки. Первый — использование раздела переоценки в запросе _search.

Вот пример использования поиска на верхнем уровне knn с выборочным увеличением и использованием rescore для повторной сортировки результатов:

resp = client.search(
    index="my-index",
    size=10,
    knn={
        "query_vector": [
            0.04283529,
            0.85670587,
            -0.51402352,
            0
        ],
        "field": "my_int4_vector",
        "k": 20,
        "num_candidates": 50
    },
    rescore={
        "window_size": 20,
        "query": {
            "rescore_query": {
                "script_score": {
                    "query": {
                        "match_all": {}
                    },
                    "script": {
                        "source": "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)",
                        "params": {
                            "queryVector": [
                                0.04283529,
                                0.85670587,
                                -0.51402352,
                                0
                            ]
                        }
                    }
                }
            },
            "query_weight": 0,
            "rescore_query_weight": 1
        }
    },
)
print(resp)
const response = await client.search({
  index: "my-index",
  size: 10,
  knn: {
    query_vector: [0.04283529, 0.85670587, -0.51402352, 0],
    field: "my_int4_vector",
    k: 20,
    num_candidates: 50,
  },
  rescore: {
    window_size: 20,
    query: {
      rescore_query: {
        script_score: {
          query: {
            match_all: {},
          },
          script: {
            source: "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)",
            params: {
              queryVector: [0.04283529, 0.85670587, -0.51402352, 0],
            },
          },
        },
      },
      query_weight: 0,
      rescore_query_weight: 1,
    },
  },
});
console.log(response);
POST /my-index/_search
{
  "size": 10, 
  "knn": {
    "query_vector": [0.04283529, 0.85670587, -0.51402352, 0],
    "field": "my_int4_vector",
    "k": 20, 
    "num_candidates": 50
  },
  "rescore": {
    "window_size": 20, 
    "query": {
      "rescore_query": {
        "script_score": {
          "query": {
            "match_all": {}
          },
          "script": {
            "source": "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)", 
            "params": {
              "queryVector": [0.04283529, 0.85670587, -0.51402352, 0]
            }
          }
        }
      },
      "query_weight": 0, 
      "rescore_query_weight": 1 
    }
  }
}

Количество возвращаемых результатов, обратите внимание, что это всего 10, и мы произведем выборочное увеличение на 2 раза, получив 20 ближайших соседей.

Количество результатов, возвращаемых из поиска KNN. Это выполнит приближенный поиск KNN с 50 кандидатами на каждом графе HNSW и использует квантованные векторы, возвращая 20 наиболее похожих векторов в соответствии с квантованным значением оценки. Кроме того, поскольку это объект верхнего уровня knn, глобальные 20 лучших результатов со всех фрагментов будут собраны до переоценки. В сочетании с rescore, это выборочное увеличение на 2x, что означает сбор 20 ближайших соседей в соответствии с квантованной оценкой и переоценку с использованием векторов с плавающей запятой с большей точностью.

Количество результатов, подлежащих переоценке, если вы хотите переоценить все результаты, установите это значение равным значению k

Сценарий для переоценки результатов. Сценарий оценки будет взаимодействовать непосредственно с первоначально предоставленным вектором float32.

Вес исходного запроса, здесь мы просто отбрасываем исходную оценку

Вес запроса переоценки, здесь мы используем только запрос переоценки

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

resp = client.search(
    index="my-index",
    size=10,
    query={
        "script_score": {
            "query": {
                "knn": {
                    "query_vector": [
                        0.04283529,
                        0.85670587,
                        -0.51402352,
                        0
                    ],
                    "field": "my_int4_vector",
                    "num_candidates": 20
                }
            },
            "script": {
                "source": "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)",
                "params": {
                    "queryVector": [
                        0.04283529,
                        0.85670587,
                        -0.51402352,
                        0
                    ]
                }
            }
        }
    },
)
print(resp)
const response = await client.search({
  index: "my-index",
  size: 10,
  query: {
    script_score: {
      query: {
        knn: {
          query_vector: [0.04283529, 0.85670587, -0.51402352, 0],
          field: "my_int4_vector",
          num_candidates: 20,
        },
      },
      script: {
        source: "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)",
        params: {
          queryVector: [0.04283529, 0.85670587, -0.51402352, 0],
        },
      },
    },
  },
});
console.log(response);
POST /my-index/_search
{
  "size": 10, 
  "query": {
    "script_score": {
      "query": {
        "knn": { 
          "query_vector": [0.04283529, 0.85670587, -0.51402352, 0],
          "field": "my_int4_vector",
          "num_candidates": 20 
        }
      },
      "script": {
        "source": "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)", 
        "params": {
          "queryVector": [0.04283529, 0.85670587, -0.51402352, 0]
        }
      }
    }
  }
}

Количество результатов для возврата

Запрос knn для выполнения начального поиска, который выполняется по каждому фрагменту

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

Сценарий для оценки результатов. Сценарий оценки будет взаимодействовать непосредственно с первоначально предоставленным вектором float32.

© 2023-2025 Elasticsearch
As of September 2024, Elasticsearch is available under a choice of three licenses: the Server Side Public License (SSPL), the Elastic License, or the AGPLv3 (OSI approved).
Elasticsearch and the Elasticsearch logo are trademarks of Elasticsearch B.V., registered in the U.S. and in other countries.
https://www.elastic.co/guide/en/elasticsearch/reference/8.17/knn-search.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API