Modelar tipos de dados complexos na Pesquisa de IA do Azure

Os conjuntos de dados externos usados para preencher um índice do Azure AI Search podem ter várias formas. Às vezes, eles incluem subestruturas hierárquicas ou aninhadas. Os exemplos podem incluir vários endereços para um único cliente, várias cores e tamanhos para um único produto, vários autores de um único livro e assim por diante. Em termos de modelagem, você pode ver essas estruturas referidas como tipos de dados complexos, compostos, compostos ou agregados. O termo Azure AI Search usa para esse conceito é um tipo complexo. No Azure AI Search, tipos complexos são modelados usando campos complexos. Um campo complexo é um campo que contém filhos (subcampos) que podem ser de qualquer tipo de dados, incluindo outros tipos complexos. Isso funciona de maneira semelhante aos tipos de dados estruturados em uma linguagem de programação.

Os campos complexos representam um único objeto no documento ou uma matriz de objetos, dependendo do tipo de dados. Os campos do tipo Edm.ComplexType representam objetos únicos, enquanto os campos do tipo Collection(Edm.ComplexType) representam matrizes de objetos.

O Azure AI Search dá suporte nativo a tipos e coleções complexos. Esses tipos permitem modelar praticamente qualquer estrutura JSON em um índice do Azure AI Search. Em versões anteriores das APIs de Pesquisa do Azure AI, apenas conjuntos de linhas niveladas podiam ser importados. Na versão mais recente, seu índice agora pode corresponder mais de perto aos dados de origem. Em outras palavras, se os dados de origem tiverem tipos complexos, o índice também poderá ter tipos complexos.

Para começar, recomendamos o conjunto de dados Hotéis, que pode ser carregado no assistente Importar dados no portal do Azure. O assistente deteta tipos complexos na origem e sugere um esquema de índice com base nas estruturas detetadas.

Nota

O suporte para tipos complexos tornou-se geralmente disponível a partir de api-version=2019-05-06.

Se sua solução de pesquisa for criada em soluções alternativas anteriores de conjuntos de dados nivelados em uma coleção, você deverá alterar seu índice para incluir tipos complexos, conforme suportado na versão mais recente da API. Para obter mais informações sobre como atualizar versões da API, consulte Atualizar para a versão mais recente da API REST ou Atualizar para a versão mais recente do SDK do .NET.

Exemplo de uma estrutura complexa

O documento JSON a seguir é composto por campos simples e campos complexos. Campos complexos, como Address e Rooms, têm subcampos. Address tem um único conjunto de valores para esses subcampos, uma vez que é um único objeto no documento. Em contraste, Rooms tem vários conjuntos de valores para seus subcampos, um para cada objeto na coleção.

{
  "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,
    }
    . . .
  ]
}

Criar campos complexos

Como em qualquer definição de índice, você pode usar o portal, a API REST ou o SDK do .NET para criar um esquema que inclua tipos complexos.

Outros SDKs do Azure fornecem exemplos em Python, Java e JavaScript.

  1. Inicie sessão no portal do Azure.

  2. Na página Visão geral do serviço de pesquisa, selecione a guia Índices.

  3. Abra um índice existente ou crie um novo índice.

  4. Selecione o separador Campos e, em seguida, selecione Adicionar campo. Um campo vazio é adicionado. Se estiver a trabalhar com uma coleção de campos existente, desloque-se para baixo para configurar o campo.

  5. Dê um nome ao campo e defina o tipo como um Edm.ComplexType ou Collection(Edm.ComplexType).

  6. Selecione as reticências na extremidade direita e, em seguida, selecione Adicionar campo ou Adicionar subcampo e, em seguida, atribuir atributos.

Limites de recolha complexos

Durante a indexação, você pode ter um máximo de 3.000 elementos em todas as coleções complexas em um único documento. Um elemento de uma coleção complexa é um membro dessa coleção. Para Quartos (a única coleção complexa no exemplo do Hotel), cada quarto é um elemento. No exemplo acima, se o "Stay-Kay City Hotel" tivesse 500 quartos, o documento do hotel teria 500 elementos de quarto. Para coleções complexas aninhadas, cada elemento aninhado também é contado, além do elemento externo (pai).

Esse limite se aplica apenas a coleções complexas, e não a tipos complexos (como Endereço) ou coleções de cadeia de caracteres (como Tags).

Atualizar campos complexos

Todas as regras de reindexação que se aplicam a campos em geral ainda se aplicam a campos complexos. Adicionar um novo campo a um tipo complexo não requer uma reconstrução de índice, mas a maioria das outras modificações requer uma reconstrução.

Atualizações estruturais da definição

Você pode adicionar novos subcampos a um campo complexo a qualquer momento sem a necessidade de uma reconstrução de índice. Por exemplo, adicionar "CEP" ou Address "Serviços" é Rooms permitido, assim como adicionar um campo de nível superior a um índice. Os documentos existentes têm um valor nulo para novos campos até que você preencha explicitamente esses campos atualizando seus dados.

Observe que, dentro de um tipo complexo, cada subcampo tem um tipo e pode ter atributos, assim como os campos de nível superior

Atualizações de dados

A atualização de documentos existentes em um índice com a upload ação funciona da mesma forma para campos complexos e simples: todos os campos são substituídos. No entanto, merge (ou mergeOrUpload quando aplicado a um documento existente) não funciona da mesma forma em todos os campos. Especificamente, merge não suporta a mesclagem de elementos dentro de uma coleção. Esta limitação existe para coleções de tipos primitivos e coleções complexas. Para atualizar uma coleção, você precisa recuperar o valor completo da coleção, fazer alterações e incluir a nova coleção na solicitação da API de índice.

Pesquisar campos complexos em consultas de texto

As expressões de pesquisa de forma livre funcionam como esperado com tipos complexos. Se qualquer campo ou subcampo pesquisável em qualquer lugar de um documento corresponder, então o documento em si é uma correspondência.

As consultas ficam mais matizadas quando você tem vários termos e operadores, e alguns termos têm nomes de campo especificados, como é possível com a sintaxe Lucene. Por exemplo, esta consulta tenta fazer a correspondência de dois termos, "Portland" e "OR", contra dois subcampos do campo Endereço:

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

Consultas como esta não estão correlacionadas para a pesquisa de texto completo, ao contrário dos filtros. Em filtros, consultas sobre subcampos de uma coleção complexa são correlacionadas usando variáveis de intervalo em any ou all. A consulta Lucene acima retorna documentos contendo "Portland, Maine" e "Portland, Oregon", juntamente com outras cidades em Oregon. Isso acontece porque cada cláusula se aplica a todos os valores de seu campo em todo o documento, portanto, não há o conceito de um "subdocumento atual". Para obter mais informações sobre isso, consulte Noções básicas sobre filtros de coleção OData no Azure AI Search.

Pesquisar campos complexos em consultas RAG

Um padrão RAG passa os resultados da pesquisa para um modelo de chat para IA generativa e pesquisa conversacional. Por padrão, os resultados da pesquisa passados para um LLM são um conjunto de linhas nivelado. No entanto, se o índice tiver tipos complexos, sua consulta poderá fornecer esses campos se você primeiro converter os resultados da pesquisa em JSON e, em seguida, passar o JSON para o LLM.

Um exemplo parcial ilustra a técnica:

  • Indique os campos desejados no prompt ou na consulta
  • Certifique-se de que os campos são pesquisáveis e recuperáveis no índice
  • Selecione os campos para os resultados da pesquisa
  • Formatar os resultados como JSON
  • Enviar a solicitação de conclusão do bate-papo para o provedor de modelo
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)

Para obter o exemplo de ponta a ponta, consulte Guia de início rápido: pesquisa generativa (RAG) com dados de aterramento da Pesquisa de IA do Azure.

Selecionar campos complexos

O $select parâmetro é usado para escolher quais campos são retornados nos resultados da pesquisa. Para usar esse parâmetro para selecionar subcampos específicos de um campo complexo, inclua o campo pai e o subcampo separados por uma barra (/).

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

Os campos devem ser marcados como Recuperáveis no índice se você quiser que eles sejam exibidos nos resultados da pesquisa. Somente os campos marcados como recuperáveis podem ser usados em uma $select instrução.

Filtrar, facetar e classificar campos complexos

A mesma sintaxe de caminho OData usada para filtrar e pesquisar em campo também pode ser usada para facetar, classificar e selecionar campos em uma solicitação de pesquisa. Para tipos complexos, aplicam-se regras que regem quais subcampos podem ser marcados como classificáveis ou facial. Para obter mais informações sobre essas regras, consulte a referência Criar API de índice.

Subcampos de facetagem

Qualquer subcampo pode ser marcado como facetable, a menos que seja do tipo Edm.GeographyPoint ou Collection(Edm.GeographyPoint).

As contagens de documentos retornadas nos resultados da faceta são calculadas para o documento pai (um hotel), não para os subdocumentos de uma coleção complexa (quartos). Por exemplo, suponha que um hotel tenha 20 quartos do tipo "suíte". Dado este parâmetro facet=Rooms/Typede faceta, a contagem de facetas é uma para o hotel, não 20 para os quartos.

Classificação de campos complexos

As operações de classificação aplicam-se a documentos (Hotéis) e não a subdocumentos (Quartos). Quando você tem uma coleção de tipos complexa, como Quartos, é importante perceber que não é possível classificar em Salas. Na verdade, você não pode classificar em qualquer coleção.

As operações de classificação funcionam quando os campos têm um único valor por documento, seja um campo simples ou um subcampo em um tipo complexo. Por exemplo, Address/City é permitido ser classificável porque há apenas um endereço por hotel, portanto $orderby=Address/City , classifica os hotéis por cidade.

Filtragem em campos complexos

Você pode fazer referência a subcampos de um campo complexo em uma expressão de filtro. Basta usar a mesma sintaxe de caminho OData usada para enfrentar, classificar e selecionar campos. Por exemplo, o filtro a seguir retorna todos os hotéis no Canadá:

$filter=Address/Country eq 'Canada'

Para filtrar um campo de coleção complexo, você pode usar uma expressão lambda com os any operadores andall. Nesse caso, a variável range da expressão lambda é um objeto com subcampos. Você pode fazer referência a esses subcampos com a sintaxe padrão do caminho OData. Por exemplo, o filtro a seguir retorna todos os hotéis com pelo menos um quarto deluxe e todos os quartos para não fumantes:

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

Tal como acontece com os campos simples de nível superior, os subcampos simples de campos complexos só podem ser incluídos nos filtros se tiverem o atributo filtrável definido como true na definição do índice. Para obter mais informações, consulte a referência Criar API de índice.

Solução alternativa para o limite de coleta complexo

Lembre-se de que a Pesquisa de IA do Azure limita objetos complexos em uma coleção a 3.000 objetos por documento. Exceder esse limite resulta na seguinte mensagem:

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."

Se precisar de mais de 3.000 itens, você pode canalizar (|) ou usar qualquer forma de delimitador para delimitar os valores, concatená-los e armazená-los como uma cadeia de caracteres delimitada. Não há limitação no número de cadeias de caracteres armazenadas em uma matriz. O armazenamento de valores complexos como cadeias de caracteres ignora a limitação de coleta complexa.

Para ilustrar, suponha que você tenha uma "searchScope" matriz com mais de 3.000 elementos:


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

A solução alternativa para armazenar os valores como uma cadeia de caracteres delimitada pode ter esta aparência:

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

Armazenar todas as variantes de pesquisa na cadeia de caracteres delimitada é útil em cenários de pesquisa em que você deseja pesquisar itens que tenham apenas "FRA" ou "1234" ou outra combinação dentro da matriz.

Aqui está um trecho de formatação de filtro em C# que converte entradas em cadeias de caracteres pesquisáveis:

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

A lista a seguir fornece entradas e cadeias de caracteres de pesquisa (saídas) lado a lado:

  • Para o código de condado "FRA" e o código de produto "1234", a saída formatada é |FRA|1234|*|.

  • Para o código do produto "1234", a saída formatada é |*|1234|*|.

  • Para o código de categoria "C100", a saída formatada é |*|*|C100|.

Forneça o curinga (*) somente se estiver implementando a solução alternativa da matriz de cadeia de caracteres. Caso contrário, se você estiver usando um tipo complexo, seu filtro poderá se parecer com este exemplo:

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

Se você implementar a solução alternativa, certifique-se de testar extensivamente.

Próximos passos

Experimente o conjunto de dados Hotéis no assistente Importar dados . Você precisa das informações de conexão do Azure Cosmos DB fornecidas no Leiame para acessar os dados.

Com essas informações em mãos, sua primeira etapa no assistente é criar uma nova fonte de dados do Azure Cosmos DB. Mais adiante no assistente, quando você chega à página de índice de destino, você vê um índice com tipos complexos. Crie e carregue esse índice e, em seguida, execute consultas para entender a nova estrutura.