Поделиться через


Моделировать сложные типы данных в поиске ИИ Azure

Внешние наборы данных, используемые для заполнения индекса поиска ИИ Azure, могут находиться во многих фигурах. Иногда они содержат иерархические или вложенные структуры. Примеры могут включать несколько адресов для одного клиента, несколько цветов и размеров для одного продукта, несколько авторов одной книги и т. д. В моделировании такие структуры называются сложными, составными или агрегатными типами данных. Термин "Поиск ИИ Azure" используется для этой концепции, является сложным типом. В поиске ИИ Azure сложные типы моделиируются с помощью сложных полей. Сложное поле — это поле, содержащее дочерние (подфилды), которое может быть любого типа данных, включая другие сложные типы. Это схоже со структурированными типами данных в языке программирования.

Сложные поля представляют либо один объект в документе, либо массив объектов, в зависимости от типа данных. Поля типа Edm.ComplexType представляют отдельные объекты, а поля типа Collection(Edm.ComplexType) — массивы объектов.

Поиск по искусственному интеллекту Azure изначально поддерживает сложные типы и коллекции. Эти типы позволяют моделировать практически любую структуру JSON в индексе поиска ИИ Azure. В предыдущих версиях API поиска ИИ Azure можно импортировать только плоские наборы строк. В последней версии индекс может более точно соответствовать исходным данным. Иными словами, если у исходных данных сложный тип, индекс также может быть сложным.

Для начала мы рекомендуем использовать набор данных гостиниц, который можно загрузить в мастере импорта данных на портале Azure. Мастер обнаруживает сложные типы в источнике и предлагает схему индекса на основе обнаруженных структур.

Примечание.

Поддержка сложных типов стала общедоступной, начиная с версии api-version=2019-05-06.

Если решение поиска основано работе с плоскими наборами данных в коллекции, измените индекс, включив в него сложные типы, которые поддерживаются в последней версии API. Дополнительные сведения об обновлении версий API см. в статье Обновление до последней версии REST API или Обновление до последней версии пакета SDK для .NET.

Пример сложной структуры

Следующий документ JSON состоит из простых и сложных полей. Сложные поля, такие как Address и Rooms, имеют подфилды. Address имеет один набор значений для этих подфилдов, так как это один объект в документе. В отличие от этого, Rooms имеет несколько наборов значений для его подфилдов, по одному для каждого объекта в коллекции.

{
  "HotelId": "1",
  "HotelName": "Stay-Kay City Hotel",
  "Description": "Ideally located on the main commercial artery of the city in the heart of New York.",
  "Tags": ["Free wifi", "on-site parking", "indoor pool", "continental breakfast"],
  "Address": {
    "StreetAddress": "677 5th Ave",
    "City": "New York",
    "StateProvince": "NY"
  },
  "Rooms": [
    {
      "Description": "Budget Room, 1 Queen Bed (Cityside)",
      "RoomNumber": 1105,
      "BaseRate": 96.99,
    },
    {
      "Description": "Deluxe Room, 2 Double Beds (City View)",
      "Type": "Deluxe Room",
      "BaseRate": 150.99,
    }
    . . .
  ]
}

Создание сложных полей

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

Другие пакеты SDK Azure предоставляют примеры в Python, Java и JavaScript.

  1. Войдите на портал Azure.

  2. На странице обзора службы поиска выберите вкладку "Индексы".

  3. Откройте существующий индекс или создайте новый индекс.

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

  5. Присвойте полю имя и задайте тип либо Edm.ComplexType Collection(Edm.ComplexType).

  6. Выберите многоточие в правом углу, а затем выберите " Добавить поле " или "Добавить подфилд", а затем назначьте атрибуты.

Сложные ограничения коллекции

Во время индексирования можно использовать не более 3000 элементов во всех сложных коллекциях в одном документе. Элемент сложной коллекции является членом этой коллекции. Для комнат (единственная сложная коллекция в примере hotel), каждая комната является элементом. В приведенном выше примере, если "Stay-Kay City Hotel" было 500 номеров, документ отеля будет иметь 500 элементов комнаты. Для вложенных сложных коллекций каждый вложенный элемент также учитывается вместе с внешним (родительским) элементом.

Это ограничение применяется только к сложным коллекциям, а не к сложным типам (например, Address) или коллекциям строк (например, Tags).

Обновление сложных полей

Все правила повторного индексирования, применяемые к полям в целом, применяются и к сложным полям. Добавление нового поля в сложный тип не требует перестроения индекса, но для большинства других изменений требуется перестроение.

Структурные обновления определения

Вы можете добавлять новые подфилды в сложное поле в любое время без необходимости перестроить индекс. Например, добавление ZipCode в Address или Amenities в Rooms разрешено, так же как и добавление поля верхнего уровня в индекс. В существующих документах для новых полей будет указано значение NULL, пока вы не заполните эти поля явным образом, обновив данные.

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

Обновление данных

Обновление существующих документов в индексе с upload помощью действия работает одинаково для сложных и простых полей: все поля заменяются. Однако действие merge (или mergeOrUpload в отношении существующего документа) выполняется по-разному. В частности, действие merge не поддерживает объединение элементов в коллекции. Это ограничение относится к коллекциям примитивных типов и сложным коллекциям. Чтобы обновить коллекцию, необходимо получить полное значение коллекции, внести изменения, а затем включить новую коллекцию в запрос API индекса.

Поиск сложных полей в текстовых запросах

Выражения поиска в свободной форме можно применять и к сложными типам. Если любое поле, доступное для поиска, или подполе в любом месте документа совпадает, сам документ является совпадением.

Запросы становятся более сложными, если есть несколько терминов и операторов, а в некоторых терминах указаны имена полей, как в синтаксисе Lucene. Например, этот запрос пытается соответствовать двум терминам "Портленд" и "OR", в двух подполях поля "Адрес":

search=Address/City:Portland AND Address/State:OR

Такие запросы не коррелируются в рамках полнотекстового поиска, в отличие от фильтров. В фильтрах запросы по подфилдам сложной коллекции коррелируются с помощью переменных диапазона в any или all. Указанный выше запрос Lucene возвращает документы, содержащие значения "Portland, Maine" и "Portland, Oregon", а также другие города в штате Орегон (Oregon). Это происходит из-за того, что каждое предложение применяется ко всем значениям его поля во всем документе, поэтому нет понятия "текущего поддокумента". Дополнительные сведения об этом см. в статье "Общие сведения о фильтрах коллекций OData" в службе "Поиск ИИ Azure".

Поиск сложных полей в запросах RAG

Шаблон RAG передает результаты поиска в модель чата для создания искусственного интеллекта и общения. По умолчанию результаты поиска, передаваемые в LLM, представляют собой плоский набор строк. Однако если индекс имеет сложные типы, запрос может предоставить эти поля, если сначала преобразовать результаты поиска в JSON, а затем передать JSON в LLM.

Частичный пример иллюстрирует метод:

  • Укажите нужные поля в запросе или запросе.
  • Убедитесь, что поля доступны для поиска и извлекаются в индексе.
  • Выберите поля для результатов поиска
  • Форматирование результатов в формате JSON
  • Отправка запроса на завершение чата поставщику модели
import json

# Query is the question being asked. It's sent to the search engine and the LLM.
query="Can you recommend a few hotels that offer complimentary breakfast? Tell me their description, address, tags, and the rate for one room they have which sleep 4 people."

# Set up the search results and the chat thread.
# Retrieve the selected fields from the search index related to the question.
selected_fields = ["HotelName","Description","Address","Rooms","Tags"]
search_results = search_client.search(
    search_text=query,
    top=5,
    select=selected_fields,
    query_type="semantic"
)
sources_filtered = [{field: result[field] for field in selected_fields} for result in search_results]
sources_formatted = "\n".join([json.dumps(source) for source in sources_filtered])

response = openai_client.chat.completions.create(
    messages=[
        {
            "role": "user",
            "content": GROUNDED_PROMPT.format(query=query, sources=sources_formatted)
        }
    ],
    model=AZURE_DEPLOYMENT_MODEL
)

print(response.choices[0].message.content)

Полный пример см . в кратком руководстве по созданию поиска (RAG) с данными об основе из поиска ИИ Azure.

Выбор сложных полей

С помощью параметра $select можно указать поля, которые будут возвращаться в результатах поиска. Чтобы использовать этот параметр для выбора определенных подфилдов сложного поля, включите родительское поле и подполе, разделенные косой чертой (/).

$select=HotelName, Address/City, Rooms/BaseRate

Если вы хотите, чтобы поле отображалось в результатах поиска, пометьте его в индексе как доступное для получения. В инструкции $select можно использовать только поля, помеченные как доступные для получения.

Фильтрация, фасетизация и сортировка сложных полей

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

Аспектирование подфилдов

Любое подполе можно пометить как аспект, если он не имеет типа Edm.GeographyPoint или Collection(Edm.GeographyPoint).

Счетчики документов, возвращаемые в результатах аспектов, вычисляются для родительского документа (отеля), а не поддокументов в сложной коллекции (номера). Например, предположим, что в гостинице 20 номеров типа "люкс". Учитывая этот параметр facet=Rooms/Typeаспекта, количество аспектов является одним для отеля, а не 20 для номеров.

Сортировка сложных полей

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

Операции сортировки работают, если поля имеют одно значение для каждого документа, будь то простое поле или подполе в сложном типе. Например, допускается сортировка, Address/City так как в каждом отеле есть только один адрес, поэтому $orderby=Address/City сортировка отелей по городу.

Фильтрация по сложным полям

Можно ссылаться на подфилды сложного поля в выражении фильтра. Для этого можно использовать синтаксис пути OData, применяемый для фасетизации, сортировки и выбора полей. Например, следующий фильтр возвращает все отели в Канаде:

$filter=Address/Country eq 'Canada'

Для фильтрации по сложному полю коллекции можно использовать лямбда-выражение с операторами any и all. В этом случае переменная диапазона лямбда-выражения является объектом с подфилдами. Эти подполя можно ссылаться на стандартный синтаксис пути OData. Например, следующий фильтр возвращает все отели с по крайней мере одним номером делюкс и всеми номерами, не являющихся номерами:

$filter=Rooms/any(room: room/Type eq 'Deluxe Room') and Rooms/all(room: not room/SmokingAllowed)

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

Обходное решение для ограничения сложной коллекции

Помните, что поиск azure AI ограничивает сложные объекты в коллекции до 3000 объектов на документ. Превышение этого ограничения приводит к следующему сообщению:

A collection in your document exceeds the maximum elements across all complex collections limit. 
The document with key '1052' has '4303' objects in collections (JSON arrays). 
At most '3000' objects are allowed to be in collections across the entire document. 
Remove objects from collections and try indexing the document again."

Если требуется более 3000 элементов, можно передать (|) или использовать любую форму разделителя для разделителя значений, сцепить их и сохранить в виде строки с разделителями. Нет ограничений на количество строк, хранящихся в массиве. Хранение сложных значений в виде строк проходит ограничение сложной коллекции.

Чтобы проиллюстрировать, предположим, что у вас есть "searchScopeмассив с более чем 3000 элементами:


"searchScope": [
  {
     "countryCode": "FRA",
     "productCode": 1234,
     "categoryCode": "C100" 
  },
  {
     "countryCode": "USA",
     "productCode": 1235,
     "categoryCode": "C200" 
  }
  . . .
]

Обходное решение для хранения значений в виде строки с разделителями может выглядеть следующим образом:

"searchScope": [
        "|FRA|1234|C100|",
        "|FRA|*|*|",
        "|*|1234|*|",
        "|*|*|C100|",
        "|FRA|*|C100|",
        "|*|1234|C100|"
]

Хранение всех вариантов поиска в строке с разделителями полезно в сценариях поиска, где требуется искать элементы, имеющие только "FRA" или "1234" или другое сочетание в массиве.

Ниже приведен фрагмент кода форматирования фильтра в C#, который преобразует входные данные в строки, доступные для поиска:

foreach (var filterItem in filterCombinations)
        {
            var formattedCondition = $"searchScope/any(s: s eq '{filterItem}')";
            combFilter.Append(combFilter.Length > 0 ? " or (" + formattedCondition + ")" : "(" + formattedCondition + ")");
        }

В следующем списке представлены входные данные и строки поиска (выходные данные) параллельно:

  • Для кода округа "FRA" и кода продукта "1234" форматированный результат равен |FRA|1234|*|.

  • Для кода продукта "1234" форматированный результат равен |*|1234|*|.

  • Для кода категории "C100" форматированный результат равен |*|*|C100|.

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

var countryFilter = $"searchScope/any(ss: search.in(countryCode ,'FRA'))";
var catgFilter = $"searchScope/any(ss: search.in(categoryCode ,'C100'))";
var combinedCountryCategoryFilter = "(" + countryFilter + " and " + catgFilter + ")";

Если вы реализуете обходное решение, обязательно протестируйте экстентивно.

Следующие шаги

Используйте набор данных гостиниц в мастере импорта данных. Чтобы получить доступ к данным, вам потребуются сведения о подключении Azure Cosmos DB, предоставленные в средстве чтения.

Изучив ее, с помощью мастера создайте источник данных Azure Cosmos DB. Далее в мастере, когда вы получите на целевую страницу индекса, вы увидите индекс с сложными типами. Создайте и загрузите этот индекс, а затем выполните запросы, чтобы изучить новую структуру.