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


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

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

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

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

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

Примечание.

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

Если ваше решение для поиска основано на работе с плоскими наборами данных в коллекции, измените индекс, включив в него сложные типы данных, поддерживаемые в последней версии API. Дополнительные сведения об обновлении версий API см. в статье Upgrade до последней версии REST API или Upgrade до новой версии пакета 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,
    }
    . . .
  ]
}

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

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

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

  1. Войдите в Azure portal.

  2. На странице search service Overview перейдите на вкладку Indexes.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Поиск сложных полей в запросах 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 AI Search.

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

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

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

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

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

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

Фасетирование подполей

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

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

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

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

Операции сортировки работают, если поля имеют одно значение для каждого документа, будь то простое поле или подполе в сложном типе. Например, можно отсортировать 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 Search ограничивает сложные объекты в коллекции до 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массив с более чем 3 000 элементами.


"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 + ")";

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

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

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