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 uma única SKU, 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": "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,
    }
    . . .
  ]
}

Indexação de tipos complexos

Durante a indexação, você pode ter um máximo de 3000 elementos em todas as coleções complexas em um único documento. Um elemento de uma coleção complexa é um membro dessa coleção, portanto, no caso de Quartos (a única coleção complexa no exemplo do Hotel), cada quarto é um elemento. No exemplo acima, se o "Secret Point Motel" 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).

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.

Atualizar campos complexos

Todas as regras de reindexação que se aplicam a campos em geral ainda se aplicam a campos complexos. Reafirmando algumas das regras principais aqui, adicionar um campo a um tipo complexo não requer uma reconstrução de índice, mas a maioria das modificações sim.

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

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.

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.

A Pesquisa do Azure tem a limitação de que os objetos complexos nas coleções em um único documento não podem exceder 3000.

Os usuários encontrarão o erro abaixo durante a indexação quando coleções complexas excederem o limite de 3000.

"Uma coleção em seu documento excede os elementos máximos em todos os limites de coleções complexas. O documento com a chave '1052' tem objetos '4303' em coleções (matrizes JSON). No máximo '3000' objetos podem estar em coleções em todo o documento. Remova objetos de coleções e tente indexar o documento novamente."

Em alguns casos de uso, talvez seja necessário adicionar mais de 3000 itens a uma coleção. Nesses casos de uso, podemos canalizar (|) ou usar qualquer forma de delimitador para delimitar os valores, concatená-los e armazená-los como uma cadeia delimitada. Não há limitação no número de cadeias de caracteres armazenadas em uma matriz na Pesquisa do Azure. Armazenar esses valores complexos como cadeias de caracteres evita a limitação. O cliente precisa validar se essa solução alternativa atende aos requisitos do cenário.

Por exemplo, não seria possível usar tipos complexos se a matriz "searchScope" abaixo tivesse mais de 3000 elementos.


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

Armazenar esses valores complexos como cadeias de caracteres com um delimitador evita a limitação

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

Em vez de armazená-los com curingas, também podemos usar um analisador personalizado que divide a palavra em | para reduzir o tamanho do armazenamento.

A razão pela qual armazenamos os valores com curingas em vez de apenas armazená-los como abaixo

|FRA|1234|C100|

é atender a cenários de pesquisa onde o cliente pode querer procurar itens que têm país França, independentemente de produtos e categorias. Da mesma forma, o cliente pode precisar pesquisar para ver se o item tem o produto 1234, independentemente do país ou da categoria.

Se tivéssemos armazenado apenas uma entrada

|FRA|1234|C100|

sem curingas, se o usuário quiser filtrar apenas na França, não podemos converter a entrada do usuário para corresponder à matriz "searchScope" porque não sabemos qual combinação de França está presente em nossa matriz "searchScope"

Se o usuário quiser filtrar apenas por país, digamos França. Vamos pegar a entrada do usuário e construí-la como uma cadeia de caracteres como abaixo:

|FRA|*|*|

que podemos usar para filtrar na Pesquisa do Azure à medida que pesquisamos em uma matriz de valores de item

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

Da mesma forma, se o usuário pesquisar por França e o código do produto 1234, pegaremos a entrada do usuário, construiremos como uma cadeia de caracteres delimitada como abaixo e a combinaremos com nossa matriz de pesquisa.

|FRA|1234|*|

Se o usuário pesquisar o código do produto 1234, pegaremos a entrada do usuário, a construiremos como uma cadeia de caracteres delimitada como abaixo e a combinaremos com nossa matriz de pesquisa.

|*|1234|*|

Se o usuário pesquisar o código de categoria C100, pegaremos a entrada do usuário, a construiremos como uma cadeia de caracteres delimitada como abaixo e a compararemos com nossa matriz de pesquisa.

|*|*|C100|

Se o usuário pesquisar por França e o código do produto 1234 e o código de categoria C100, pegaremos a entrada do usuário, a construiremos como uma cadeia de caracteres delimitada como abaixo e a combinaremos com nossa matriz de pesquisa.

|FRA|1234|C100|

Se um usuário tentar pesquisar por países não presentes em nossa lista, ele não corresponderá à matriz delimitada "searchScope" armazenada no índice de pesquisa e nenhum resultado será retornado. Por exemplo, um usuário pesquisa Canadá e código de produto 1234. A pesquisa do usuário seria convertida em

|CAN|1234|*|

Isso não corresponderá a nenhuma das entradas na matriz delimitada em nosso índice de pesquisa.

Apenas a escolha de design acima requer esta entrada curinga; Se ele tivesse sido salvo como um objeto complexo, poderíamos simplesmente ter realizado uma pesquisa explícita como mostrado abaixo.

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

Assim, podemos satisfazer os requisitos em que precisamos pesquisar uma combinação de valores armazenando-a como uma cadeia de caracteres delimitada em vez de uma coleção complexa se nossas coleções complexas excederem o limite da Pesquisa do Azure. Essa é uma das soluções alternativas, e o cliente precisa validar se isso atenderia aos requisitos do cenário.

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.