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

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.

Limites de coleção complexa

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 hotel), cada quarto é 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).

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 recompilação de índice, mas a maioria das outras modificações exige uma recompilação.

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

Pesquisar campos complexos em consultas RAG

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

Um exemplo parcial ilustra a técnica:

  • Indique os campos desejados no prompt ou na consulta
  • Verifique se 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 de chat 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 Início Rápido: RAG (pesquisa generativa) com dados de fundamentação 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 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.

Solução alternativa para o limite de coleção complexa

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 você precisar de mais de 3.000 itens, poderá 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. Armazenar valores complexos como cadeias de caracteres ignora a limitação de coleção complexa.

Para ilustrar, suponha que você tenha uma matriz "searchScope" 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 do condado "FRA" e o código do 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 apenas o curinga (*) se você 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á ser semelhante a 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 de forma extensiva.

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.