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

Внешние наборы данных, используемые для заполнения индекса поиска ИИ Azure, могут находиться во многих фигурах. Иногда они содержат иерархические или вложенные структуры. Например, несколько адресов для одного клиента, несколько цветов и размеров для одного SKU, несколько авторов одной книги и т. д. В моделировании такие структуры называются сложными, составными или агрегатными типами данных. Термин "Поиск ИИ 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": "Secret Point Motel",
  "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,
    }
    . . .
  ]
}

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

Во время индексирования можно обрабатывать не более 3000 элементов во всех сложных коллекциях внутри одного документа. Элемент сложной коллекции является членом этой коллекции, поэтому в случае поля Rooms (единственная сложная коллекция в примере для гостиницы) каждое вложенное поле (номер гостиницы) является элементом. В приведенном выше примере, если в гостинице Secret Point Motel 500 номеров, в документе для гостиницы будет 500 элементов. Для вложенных сложных коллекций каждый вложенный элемент также учитывается вместе с внешним (родительским) элементом.

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

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

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

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

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

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

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

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

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

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

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

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

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

Вы можете добавлять новые подфилды в сложное поле в любое время без необходимости перестроить индекс. Например, добавление 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".

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

С помощью параметра $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 имеет ограничение, что сложные объекты в коллекциях в одном документе не могут превышать 3000.

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

"Коллекция в документе превышает максимальное количество элементов во всех сложных коллекциях. Документ с ключом "1052" содержит объекты "4303" в коллекциях (массивы JSON). Не более 3000 объектов могут находиться в коллекциях по всему документу. Удалите объекты из коллекций и повторите попытку индексирования документа".

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

Например, нельзя было бы использовать сложные типы, если в массиве 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|C100|

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

Если бы мы сохранили только одну запись

|FRA|1234|C100|

без диких карта, если пользователь хочет отфильтровать только во Франции, мы не можем преобразовать входные данные пользователя в соответствие с массивом searchScope, так как мы не знаем, какое сочетание Франции присутствует в нашем массиве searchScope

Если пользователь хочет отфильтровать только по странам, предположим, Франция. Мы возьмем входные данные пользователя и создадим его в виде строки, как показано ниже:

|FRA|*|*|

то, что мы можем использовать для фильтрации в поиске Azure при поиске в массиве значений элементов

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

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

|FRA|1234|*|

Если пользователь ищет код продукта 1234, мы будем принимать входные данные пользователя, создавать его как строку с разделителями, как показано ниже, и сопоставлять ее с нашим массивом поиска.

|*|1234|*|

Если пользователь ищет код категории C100, мы будем принимать входные данные пользователя, создавать его как строку с разделителями, как показано ниже, и сопоставлять ее с нашим массивом поиска.

|*|*|C100|

Если пользователь ищет код продукта 1234 и код категории C100, мы будем принимать входные данные пользователя, создавать его как строку с разделителями, как показано ниже, и сопоставлять ее с нашим массивом поиска.

|FRA|1234|C100|

Если пользователь пытается искать страны, не присутствующих в нашем списке, он не будет соответствовать массиву с разделителями "searchScope", хранящимся в индексе поиска, и результаты не будут возвращены. Например, пользователь выполняет поиск в Канаде и коде продукта 1234. Поиск пользователя будет преобразован в

|CAN|1234|*|

Это не будет соответствовать ни одному из записей в разделенном массиве в нашем индексе поиска.

Только приведенный выше вариант конструктора требует этого дикого карта записи; если он был сохранен как сложный объект, мы могли бы просто выполнить явный поиск, как показано ниже.

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

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

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

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

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