Modelar os tipos de dados complexos na Pesquisa de IA do Azure

Os conjuntos de valores externos usados para popular um índice da Pesquisa de IA do Azure podem vir em 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ários tamanhos e cores para um único SKU, vários autores de um único livro e assim por diante. Em termos de modelagem, você pode ver essas estruturas referenciadas como tipos de dados complexos, compostos, compostosou agregados. O termo que a Pesquisa de IA do Azure usa para esse conceito é tipo complexo. Na Pesquisa de IA do Azure, os tipos complexos são modelados usando os 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 forma semelhante à de tipos de dados estruturados em uma linguagem de programação.

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

A Pesquisa de IA do Azure dá suporte nativo aos tipos e coleções complexos. Esses tipos permitem que você modele quase qualquer estrutura de JSON em um índice da Pesquisa de IA do Azure. Nas versões anteriores das APIs da Pesquisa de IA do Azure, somente os conjuntos de linhas achatados podiam ser importados. Na versão mais recente, o índice agora pode corresponder mais de acordo com os 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 de hotéis, que pode ser carregado no assistente de importação de dados no portal do Azure. O assistente detecta tipos complexos na origem e sugere um esquema de índice baseado nas estruturas detectadas.

Observação

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

Se sua solução de pesquisa tiver sido criada em soluções alternativas anteriores de conjuntos de valores em uma coleção, você deverá alterar o índice para incluir tipos complexos com suporte na versão mais recente da API. Para obter mais informações sobre como atualizar versões de 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 de campos simples e campos complexos. Campos complexos, como Address e Rooms, possuem subcampos. Address possui um único conjunto de valores para esses subcampos, pois é um único objeto no documento. Por outro lado, Rooms possui 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,
    }
    . . .
  ]
}

Indexando 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 salas (a única coleção complexa no exemplo de Hotel), cada sala é um elemento. No exemplo acima, se o "segredo do ponto Motel" tivesse 500 salas, o documento do Hotel teria 500 elementos Room. Para coleções complexas aninhadas, cada elemento aninhado também é contado, além do elemento externo (pai).

Esse limite se aplica somente a coleções complexas e não a tipos complexos (como endereço) ou coleções de cadeias de caracteres (como marcas).

Criar campos complexos

Assim como ocorre com qualquer definição de índice, você pode usar o portal, a API RESTou o SDK do net para criar um esquema que inclua tipos complexos.

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

  1. Entre no portal do Azure.

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

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

  4. Selecione a guia Campos e, em seguida, selecione Adicionar campo. Um campo vazio é adicionado. Se você estiver trabalhando com uma coleção de campos existente, role para baixo para configurar o campo.

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

  6. Selecione as elipses na extrema direita e, em seguida, selecione Adicionar campo ou Adicionar subcampo e, depois, designe os 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 principais regras aqui, a adição de um campo a um tipo complexo não exige uma recompilação de índice, mas a maioria das modificações faz.

Atualizações estruturais para a definição

Você pode adicionar novos subcampos a um campo complexo a qualquer momento, sem a necessidade de reconstruir o índice. Por exemplo, a adição de "ZipCode" Address ou "comodidades" Rooms é permitida, assim como a adição de 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 possui 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 ação upload funciona da mesma maneira 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 em todos os campos. Especificamente, o merge não dá suporte a elementos de mesclagem dentro de uma coleção. Essa 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, em seguida, incluir a nova coleção na solicitação da API Index.

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 próprio documento será uma correspondência.

As consultas são mais nuances 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, essa consulta tenta corresponder dois termos, "Portland" e "OR", a dois subcampos do campo Endereço:

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

Consultas como essa não são correlacionadas para pesquisa de texto completo, ao contrário de filtros. Nos filtros, as 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 no Oregon. Isso acontece porque cada cláusula se aplica a todos os valores de seu campo em todo o documento, portanto não existe o conceito de "subdocumento atual". Para obter mais informações, consulte Noções básicas sobre filtros de coleção OData na 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 este 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 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 filtragem e pesquisas 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 facetáveis. Para obter mais informações sobre essas regras, consulte a referência de API CREATE INDEX.

Facetando subcampos

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), e não para os subdocumentos em uma coleção complexa (quartos). Por exemplo, suponha que um hotel tenha 20 salas do tipo "Suite". Dado este parâmetro de faceta facet=Rooms/Type, a contagem de facetas é uma para o hotel e não 20 para os quartos.

Ordenando 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 complexos, como salas, é importante perceber que não é possível classificar em salas. Na verdade, você não pode classificar em nenhuma coleção.

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

Filtrando 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 facetar, classificar e selecionar campos. Por exemplo, o filtro a seguir retorna todos os hotéis no Canadá:

$filter=Address/Country eq 'Canada'

Para filtrar em um campo de coleção complexo, você pode usar uma expressão lambda com os any e all operadores . Nesse caso, a variável de intervalo da expressão lambda é um objeto com subcampos. Você pode consultar esses subcampos com a sintaxe de caminho OData padrão. 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)

Assim como acontece com campos simples de nível superior, subcampos simples de campos complexos só poderão ser incluídos em filtros se tiverem o atributo filtrável definido como true na definição do índice. Para obter mais informações sobre essas regras, consulte a referência de API CREATE INDEX.

O Azure Search tem a limitação de que os objetos complexos nas coleções de 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 no seu documento excede o máximo de elementos em todos os limites de coleções complexas. O documento com chave “1052” possui objetos “4303” em coleções (matrizes JSON). No máximo “3000” objetos podem estar em coleções em todo o documento. Remova objetos das coleções e tente indexar o documento novamente."

Em alguns casos de uso, poderemos precisar 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 de caracteres delimitada. Não há limitação no número de cadeias de caracteres armazenadas em uma matriz no Azure Search. Armazenar esses valores complexos como cadeia 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 o array "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 cadeia 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 conforme abaixo

|FRA|1234|C100|

é atender a cenários de pesquisa em que o cliente possa querer pesquisar itens que tenham como país a França, independentemente dos produtos e categorias. Da mesma forma, o cliente pode precisar pesquisar para ver se o item possui 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 poderemos converter a entrada do usuário para corresponder ao array "searchScope" porque não sabemos qual combinação de França está presente em nosso array "searchScope"

Se o usuário quiser filtrar apenas por país, digamos França. Pegaremos a entrada do usuário e a construiremos como uma cadeia de caracteres conforme abaixo:

|FRA|*|*|

que podemos usar para filtrar na pesquisa do Azure enquanto pesquisamos em uma matriz de valores de itens

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 França e o código do produto 1234, pegaremos a entrada do usuário, construí-la como uma cadeia de caracteres delimitada conforme abaixo e combiná-la com nossa matriz de pesquisa.

|FRA|1234|*|

Se o usuário pesquisar o código do produto 1234, pegaremos a entrada do usuário, construí-la como uma cadeia de caracteres delimitada conforme abaixo e combiná-la com nossa matriz de pesquisa.

|*|1234|*|

Se o usuário pesquisar o código da categoria C100, pegaremos a entrada do usuário, construí-la como uma cadeia de caracteres delimitada conforme abaixo e combiná-la com nossa matriz de pesquisa.

|*|*|C100|

Se o usuário pesquisar França e o código do produto 1234 e o código da categoria C100, pegaremos a entrada do usuário, construí-la como uma cadeia de caracteres delimitada conforme abaixo e combiná-la com nossa matriz de pesquisa.

|FRA|1234|C100|

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

|CAN|1234|*|

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

Somente a escolha de design acima requer essa entrada curinga; se tivesse sido salvo como um objeto complexo, poderíamos simplesmente ter realizado uma pesquisa explícita conforme 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 de procurar uma combinação de valores, armazenando-a como uma cadeia de caracteres delimitada em vez de uma coleção complexa se as nossas coleções complexas excederem o limite do Azure Search. Essa é uma das soluções alternativas e o cliente precisa validar se isso atenderia aos requisitos do cenário.

Próximas etapas

Experimente o conjunto de dados de hotéis no assistente de importação de dados. Você precisa das informações de conexão do Azure Cosmos DB fornecidas no leia-me para acessar os dados.

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