Семантический Поиск в Managed OpenSearch

Семантический Поиск в Managed OpenSearch

Mikhail Khludnev

Семантический Поиск в Managed OpenSearch

В предыдущем посте мы говорили о текстовом поиске, сегодня пост о векторном (семантическом) поиске.

Если мы используем OpenSearch в Yandex Cloud представляется логичным использовать модели вложений этого же облака.

См. пример.

Этот код можно запустить как Python Cloud Function, написан он исходя из того что в каталоге сервисного аккаунта под которым запускается функция доступна модель вложений (embedding). Детали подключения к кластеру описаны в документации.

Рассмотрим один крайний случай: если мы подключаемся указывая FQDN DATA узлов у которых не включен публичный доступ, то функция должна запускаться в сети кластера OpenSearch, иначе они будут не доступны. Альтернативные варианты: подключаться через «Особый FQDN» или узел DASHBOARD с публичным доступом.  

Код создаёт тестовый индекс с текстовым и векторным полем, явно вызывает embedding model через REST API создавая вектора вложений для документов и запроса. И выполняет векторный поиск демонстрируя способ интеграции. Обратите внимание на способ выбора разных моделей для документов и запросов.  

Получение вложений через OpenSearch Ingest Pipelines

Другой вариант интеграции с моделью вложений – OS ingest pipelines в этом случае клиент отправляет только текст документах, а вызов модели векторизации выполняет OpenSearch.

В общем виде процесс подключения внешней модели описан тут. Последовательность шагов для подключения модели вложений из YC AI Studio описана в заявке на добавление в документацию OS Connector Blueprints. Пришлось столкнуться со следующими трудностями:

Подключение Manages OpenSearch к моделям AI Studio

DATA хосты подключаются к моделям YC через интернет (до тех пор пока не реализовано что-то подобное). Соответственно, DATA узел без публичного доступа не может вызвать модель AI Studio. Есть два варианта решения:

  1. включаем публичный доступ для DATA хостов. Это немного контр интуитивно.   Кажется что «публичность» определяет входящий трафик, но немного поразмыслив, становится очевидно, что она же требуется и в другую сторону – для исходящего подключения к модели.
  2. создание NAT шлюза с таблицей маршрутизации (Next hop) на этот шлюз и привязка этой таблицы к сети кластера OpenSearch. Тарифицируется NAT отдельно (дополнительно), но выглядит более безопасно чем позволять всему интернету подбирать пароль к DATA хостам.        

Решение этой проблемы физического подключения не отменяет необходимости разрешить OpenSearch отправлять запросы в AI Stutio как указано в последовательности шагов.

Функции процессинга в коннекторе

Другая трудность - функции пре/пост-процессинга. К счастью подошли функции от Amazon Bedrock. Эти функции нужны для адаптации интерфейса вызова коннектора в ml_commons и Embedding REST API. Грубо говоря, из списка строк выбирается первый элемент, который принимает в виде одной строки text REST API. У меня возникли некоторые сомнения насчёт предсказуемости всего конвейера ml_commons: OpenSearch унаследовал от Elasticsearch свободное смешивание списков и отдельных значений, поэтому мне показалось что не исключена ситуация, когда вместо списка из одного элемента на вход придут несколько строк и будут отброшены. Попробую это уточнить в дальнейшем. Базовые примеры работают ожидаемо.

В случае необходимости функции пре/пост-процессинга можно написать самому, но советую быть в стороне от этого «увлекательного» занятия: пишутся они на Painless (диалекте JavaScript), скрипт этот формирует JSON (Java Script Object Notation, you know), потом этот скрипт эскейпится и передаётся строкой в JSON. В результате имеем интуитивно понятные нет серии обратных слэшей и кавычек \"\\\", разбираться c которыми - то ещё удовольствие. Диагностика проблем затруднительна – немногословные ответы с ошибками без стэк-трейсов и логов. Мне пришлось запускать OpenSearch локально под отладчиком, но даже так, некоторые исходники не скачались из-за каких-то конфликтов версий.  

Другой возможный подход – использование Open AI совместимого Embedding API, возможно это упростит конфигурацию коннектора, но исходя из того, что несколько строк в один запрос этого API не отправить, преимущества такого подхода не очевидны.

Токены доступа    

В конфигурации коннектора используется API Key. Другой возможный способ – Bearer ${IAM_TOKEN}. Но «IAM-токен действует не больше 12 часов», в принципе Cloud Function может периодически обновлять Bearer получая его из контекста как в примерах рассмотренных выше. Если быть честным, мне не удалось обновить токен обновляя конфигурацию коннектора. Требует дальнейшего исследования.

Конвейер индексации

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

POST /_ingest/pipeline/_simulate
{
 "pipeline" :
 {
   "processors": [
     {
     "text_embedding": {
       "model_id": "6cbj05oB7Vkgz3is7UJH",
       "field_map": {
         "text": "text_embedding"
       }
     }
   }
   ]
 },
 "docs": [
   {
     "_index": "my_hybrid_index",
     "_id": "1",
     "_source": {
       "text": "Poet birthday"
     }
   },
   {
     "_index": "my_hybrid_index",
     "_id": "2",
     "_source": {
       "text": "Blossing plant"
     }
   }
 ]
}
// ответ 
{
 "docs": [
   {
     "doc": {
       "_index": "my_hybrid_index",
       "_id": "1",
       "_source": {
         "text_embedding": [
           0.068237305,
           -0.03338623,
           0.023925781,
           ...
           -0.076538086,
           0.021759033,
           0.065979004
         ],
         "text": "Poet birthday"
       },
       "_ingest": {
         "timestamp": "2025-11-30T08:41:11.016017656Z"
       }
     }
   },
   {
     "doc": {
       "_index": "my_hybrid_index",
       "_id": "2",
       "_source": {
         "text_embedding": [
           -0.038879395,
           -0.037139893,
           ...
           -0.023345947,
           -0.001789093
         ],
         "text": "Blossing plant"
       },
       "_ingest": {
         "timestamp": "2025-11-30T08:41:11.016022038Z"
       }
     }
   }
 ]
} 

Видно как тесты были дополнены векторами. Ранее писал о том что ml_commons, как мне кажется не полностью обрабатывает все возможные виды документов. Вот пример:

POST /_ingest/pipeline/_simulate
{
 "pipeline" :
 {
   "processors": [
     {
     "text_embedding": {
       "model_id": "6cbj05oB7Vkgz3is7UJH",
       "field_map": {
         "text_field": "vector_field",
         "obj.text_field": "vector_field"
       }
     }
   }
   ]
 },
 "docs": [
   {
     "_index": "second-index",
     "_id": "1",
     "_source": {
       "text_field": ["array","isn't handled properly"],
       "obj.text_field": "another way ",
       "obj":{
         "text_field": "to pass an array "
       }
     }
   }
 ]
}

В общем, если вам действительно нужно что-то такое обрабатывать, то придётся приложить некоторые усилия.

Создаём пайплайн для индексации.

 PUT /_ingest/pipeline/_yc_embeddings_pipeline
{
 "description": "Pipeleine with YC embeddings",
 "processors": [
   {
     "text_embedding": {
       "model_id": "6cbj05oB7Vkgz3is7UJH",
       "field_map": {
         "text": "text_embedding"
       }
     }
   }
 ]
} 

Код клиента индексации и поиска.

Модифицируем код демо-функции: при индексировании указываем пайплайн для конверсии текстов в вектора. При поиске используем немного другой запрос передавая в него идентификатор созданной модели, которая конвертирует текст запроса в вектор. Вы же помните, что для запроса нужно указывать другую модель вложений?  При запуске кода видим что вектора были созданы при индексации и запросе. Результат поиска:

[    {
           "_index": "my_hybrid_index",
           "_id": "8saY1JoB7Vkgz3ish0Ky",
           "_score": 0.5327221,
           "_source": {
               "text_embedding": [
                   -0.027313232,
                   -0.077697754,
                   0.043762207,
                   ...
                   -0.0029201508,
                   -0.065979004
               ],
               "text": "Alexander Sergeyevich Pushkin ....."
           }
       },
       {
           "_index": "my_hybrid_index",
           "_id": "88aY1JoB7Vkgz3ish0LY",
           "_score": 0.47184184,
           "_source": {
               "text_embedding": [
                   0.03225708,
                  ...,
                   0.0029239655,
                   0.1027832
               ],
               "text": "Matricaria is a genus of annual flowering plants ...."

           }
       }
   ]
}

В отличии от предыдущей версии кода, при использовании пайплайнов и ссылки на модель функции не нужен сервисный аккаунт, а только пароль OpenSearch, т.к. сама функция не вызывает модель из AI Studio. 

Использование конвейера запроса

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

POST my_hybrid_index/_search?verbose_pipeline=true
{
   "query": {
       "match": {
           "text": {"query":"blossing plant"}
       }
   },
   "search_pipeline": {
   "request_processors": [
     {
     "ml_inference": {
       "model_id": "6cbj05oB7Vkgz3is7UJH",
       "input_map": [
         {
           "inputText": "query.match.text.query"
         }
       ],
       "output_map": [
         {
           "vector_out": "$.inference_results.*.output.*.data"
         }
       ],
       "query_template": """{
       "size": 2,
       "query": {
         "knn": {
           "text_embedding": {
             "vector": ${vector_out},
             "k": 5
             }
           }
          }
         }"""
     }
   }
 ]}
}

Здесь кстати используется трюк с тремя кавычками немного облегчающий ад эскейпинга, на который я жаловался выше. Но работает он только в DevTools. Вот так это значение выглядит при Copy as cURL.

"query_template": "{\n       \"size\": 2,\n       \"query\": {\n         \"knn\": {\n           \"text_embedding\": {\n             \"vector\": ${vector_out},\n             \"k\": 5\n             }\n           }\n          }\n         }"

Получаем аналогичный результат поиска по вектору, кроме того verbose_pipeline=true показывает результат работы процессора – вектор knn запроса 

{
 "took": 29,
..
 "hits": { ... },
 "processor_results": [
   {
     "processor_name": "ml_inference",
     "duration_millis": 27756171,
     "status": "success",
     "input_data": { ....},
    "output_data": {
       "size": 2,
       "query": {
         "knn": {
           "text_embedding": {
             "vector": [
               0.060516357,
               -0.04724121,
               ...
                0.06149292,
               -0.009025574
             ],
             "boost": 1,
             "k": 5
           }
         }
} 

В следующих постах хотелось бы обсудить использование генеративной и ранжирующей моделей.

 

 

 

 

    

 

Report Page