Modelado de tipos de datos complejos en Azure AI Search

Los conjuntos de datos externos usados para rellenar un índice de Azure AI Search pueden tener muchas formas. A veces, incluyen subestructuras jerárquicas o anidadas. Algunos ejemplos son varias direcciones de un solo cliente, varios tamaños y colores de un único producto, varios autores de un único libro, etc. En términos de modelado, puede que vea que se hace referencia a estas estructuras como tipos de datos complejos, compuestos o agregados. El término que Azure AI Search usa para este concepto es tipo complejo. En Azure AI Search, los tipos complejos se modelan mediante campos complejos. Un campo complejo es un campo que contiene elementos secundarios (subcampos) que pueden ser de cualquier tipo de datos, incluidos otros tipos complejos. Esto funciona de forma similar a los tipos de datos estructurados de un lenguaje de programación.

Los campos complejos representan un único objeto en el documento, o bien una matriz de objetos, en función del tipo de datos. Los campos de tipo Edm.ComplexType representan objetos individuales, mientras que los campos de tipo Collection(Edm.ComplexType) representan matrices de objetos.

Azure AI Search admite de forma nativa colecciones y tipos complejos. Estos tipos le permiten modelar casi cualquier estructura JSON en un índice de Azure AI Search. En versiones anteriores de las API de Azure AI Search, solo se podían importar conjuntos de filas planas. En la versión más reciente, ahora el índice puede corresponderse de forma más exacta con los datos de origen. En otras palabras, si los datos de origen tienen tipos complejos, el índice también puede tener tipos complejos.

Para empezar, recomendamos el conjunto de datos Hotels, que puede cargarse en el asistente Importar datos de Azure Portal. El asistente detecta los tipos complejos en el origen y sugiere un esquema de índice basado en las estructuras detectadas.

Nota:

La compatibilidad con tipos complejos está disponible con carácter general desde api-version=2019-05-06.

Si su solución de búsqueda se basa en soluciones alternativas anteriores de conjuntos de datos planos de una colección, debe cambiar su índice para incluir tipos complejos según se admite en la versión más nueva de la API. Para obtener más información acerca de cómo actualizar las versiones de la API, consulte Actualización a la versión más reciente de la API REST o Actualización a la versión más reciente del SDK de .NET.

Ejemplo de una estructura compleja

El siguiente documento JSON se compone de campos simples y complejos. Los campos complejos, tales como Address y Rooms, tienen subcampos. Address tiene un único conjunto de valores para esos subcampos, puesto que es un objeto único en el documento. En cambio, Rooms tiene varios conjuntos de valores para sus subcampos, uno para cada objeto de la colección.

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

Creación de campos complejos

Al igual que con cualquier definición de índice, puede usar el portal, la API REST o el SDK de .NET para crear un esquema que incluya tipos complejos.

Otros SDK de Azure proporcionan ejemplos en Python, Java y JavaScript.

  1. Inicie sesión en Azure Portal.

  2. En la página de información general del servicio de búsqueda, seleccione la pestaña Índices.

  3. Abra un índice existente o cree uno nuevo.

  4. Seleccione la pestaña Campos y, a continuación, seleccione Agregar campo. Se agrega un campo vacío. Si está trabajando con una colección de campos existente, desplácese hacia abajo para configurar el campo.

  5. Asigne un nombre al campo y establezca el tipo en Edm.ComplexType o Collection(Edm.ComplexType).

  6. Seleccione los puntos suspensivos del extremo derecho y, a continuación, seleccione Agregar campo o Agregar subcampo y, a continuación, asigne atributos.

Límites de recopilación complejos

Durante la indexación, puede tener un máximo de 3000 elementos en todas las colecciones complejas en un solo documento. Un elemento de una colección compleja es un miembro de esa colección. Para Habitaciones (la única colección compleja en el ejemplo del Hotel), cada habitación es un elemento. En el ejemplo anterior, si el "Stay-Kay City Hotel" tuviera 500 habitaciones, el documento del hotel tendría 500 elementos de habitación. En el caso de las colecciones complejas anidadas, cada elemento anidado también se cuenta, además del elemento externo (primario).

Este límite se aplica solo a colecciones complejas y no a tipos complejos (como Dirección) o colecciones de cadenas (como Etiquetas).

Actualización de campos complejos

Todas las reglas de nueva indexación que se aplican a los campos en general se aplican igualmente a los campos complejos. Agregar un nuevo campo a un tipo complejo no requiere una recompilación de índices, pero la mayoría de las demás modificaciones requieren una recompilación.

Actualizaciones estructurales en la definición

Puede agregar nuevos subcampos a un campo complejo en cualquier momento sin necesidad de recompilar un índice. Por ejemplo, la adición de "ZipCode" a Address o "Amenities" a Rooms se permite, al igual que la adición de un campo de nivel superior a un índice. Los documentos existentes tienen un valor null para los nuevos campos hasta que rellena explícitamente dichos campos al actualizar sus datos.

Tenga en cuenta que, en un tipo complejo, cada subcampo tiene un tipo y puede tener atributos, al igual que sucede con los campos de nivel superior.

Actualizaciones de datos

La actualización de documentos existentes en un índice con la acción upload funciona del mismo modo tanto para los campos complejos como simples: se reemplazan todos los campos. Sin embargo, merge (o mergeOrUpload cuando se aplica a un documento existente) no funciona igual en todos los campos. En concreto, merge no admite la combinación de elementos dentro de una colección. Esta limitación existe para las colecciones de tipos primitivos y complejas. Para actualizar una colección, debe recuperar el valor de la colección completa, realizar cambios y, a continuación, incluir la nueva colección en la solicitud de API de índice.

Búsqueda de campos complejos en consultas de texto

Las expresiones de búsqueda de forma libre funcionan según lo esperado con tipos complejos. Si cualquier campo o subcampo de búsqueda de cualquier parte de un documento coincide, entonces el documento en sí es una coincidencia.

Las consultas adquieren más matices cuando tiene varios términos y operadores, y algunos términos tienen nombres de campos especificados, tal y como es posible con la sintaxis de Lucene. Por ejemplo, esta consulta intenta hacer coincidir dos términos, "Portland" y "OR", con dos subcampos del campo Dirección:

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

Las consultas de este tipo no están correlacionadas para la búsqueda de texto completo, a diferencia de los filtros. En los filtros, las consultas a través de subcampos de una colección compleja se correlacionan mediante variables de rango en any o all. La consulta de Lucene anterior devuelve documentos que contienen "Portland, Maine" y "Portland, Oregon", junto con otras ciudades de Oregón. Esto sucede porque cada cláusula se aplica a todos los valores de su campo en todo el documento, por lo que no hay ningún concepto de un "subdocumento actual". Para obtener más información, consulte Descripción de los filtros de colección de OData en Azure AI Search.

Búsqueda de campos complejos en consultas RAG

Un patrón RAG pasa los resultados de búsqueda a un modelo de chat para la inteligencia artificial generativa y la búsqueda conversacional. De manera predeterminada, los resultados de búsqueda pasados a un LLM son un conjunto de filas sin formato. Sin embargo, si el índice tiene tipos complejos, la consulta puede proporcionar esos campos si primero convierte los resultados de la búsqueda a JSON y, a continuación, pasa el JSON al LLM.

Un ejemplo parcial ilustra la técnica:

  • Indique los campos que desea en el símbolo del sistema o en la consulta
  • Asegúrese de que los campos se pueden buscar y recuperar en el índice
  • Seleccione los campos de los resultados de la búsqueda
  • Formato de los resultados como JSON
  • Envío de la solicitud de finalización del chat al proveedor de modelos
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 ver el ejemplo de un extremo a otro, consulte Inicio rápido: Búsqueda generativa (RAG) con datos de contextualización de Búsqueda de Azure AI.

Selección de campos complejos

El parámetro $select se utiliza para elegir qué campos se devuelven en los resultados de la búsqueda. Para utilizar este parámetro para seleccionar subcampos específicos de un campo complejo, incluya el campo primario y el subcampo separados por una barra diagonal (/).

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

Los campos deben marcarse como Recuperables en el índice, si quiere que aparezcan en los resultados de la búsqueda. Solo los campos marcados como Recuperables se pueden usar en una instrucción $select.

Filtros, facetas y orden de los campos complejos

La misma sintaxis de ruta de acceso de OData utilizada para el filtrado y las búsquedas por campos también se puede usar para ordenar y seleccionar campos en una solicitud de búsqueda, así como para definirles facetas. En el caso de los tipos complejos, se aplican reglas que rigen los subcampos que se pueden marcar como definibles por facetas u ordenables. Para obtener más información sobre estas reglas, vea la referencia de la API de creación de índices.

Definición de facetas de los subcampos

Cualquier subcampo puede marcarse como definible por facetas a menos que sea de tipo Edm.GeographyPoint o Collection(Edm.GeographyPoint).

Los recuentos de documentos devueltos en los resultados de facetas se calculan para el documento principal (un hotel) y no para los subdocumentos de una colección compleja (habitaciones). Por ejemplo, supongamos que un hotel tiene 20 habitaciones de tipo "suite". Dado este parámetro de faceta facet=Rooms/Type, el recuento de facetas será uno para el hotel y no 20 para las habitaciones.

Ordenación de los campos complejos

Las operaciones de ordenación se aplican a documentos (hoteles) y no a subdocumentos (habitaciones). Cuando haya una colección de tipo complejo, como las salas, es importante tener en cuenta que se puede ordenar en salas de ningún modo. De hecho, no puede ordenar ninguna colección.

Las operaciones de ordenación funcionan cuando los campos tienen un único valor por documento, tanto si se trata de un campo sencillo como de un subcampo en un tipo complejo. Por ejemplo, Address/City puede ordenarse porque solo hay una dirección por hotel, por lo que $orderby=Address/City ordena los hoteles por ciudad.

Filtrado en campos complejos

Se puede hacer referencia a subcampos de un campo complejo en una expresión de filtro. Simplemente use la misma sintaxis de ruta de acceso de OData que se usa para ordenar y seleccionar campos, así como para definirles facetas. Por ejemplo, el filtro siguiente devuelve todos los hoteles de Canadá:

$filter=Address/Country eq 'Canada'

Para filtrar según un campo de colección complejo, puede usar una expresión lambda con los operadores any y all. En ese caso, la variable de rango de la expresión lambda es un objeto con subcampos. Puede hacer referencia a esos subcampos con la sintaxis de ruta de acceso de OData estándar. Por ejemplo, el filtro siguiente devuelve los hoteles que tengan al menos una habitación deluxe y todas las habitaciones en las que no se permita fumar:

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

Al igual que con los campos simples de nivel superior, los subcampos simples de campos complejos solo pueden incluirse en los filtros si tienen el atributo filterable establecido en true en la definición del índice. Para obtener más información, vea la referencia de la API de creación de índices.

Solución alternativa para el límite de recopilación compleja

Recuerde que Búsqueda de Azure AI limita los objetos complejos de una colección a 3000 objetos por documento. Si se supera este límite, se muestra el siguiente mensaje:

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

Si necesita más de 3000 elementos, puede usar una barra vertical (|) o usar cualquier forma de delimitador para los valores, concatenarlos y almacenarlos como una cadena delimitada. No existe limitación alguna respecto al número de cadenas almacenadas en una matriz. El almacenamiento de valores complejos como cadenas omite la limitación de las colecciones complejas.

Para ilustrarlo, supongamos que tiene una matriz "searchScope" con más de 3000 elementos:


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

La solución alternativa para almacenar los valores como una cadena delimitada podría tener este aspecto:

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

Almacenar todas las variantes de búsqueda en la cadena delimitada es útil en escenarios de búsqueda en los que desea buscar elementos que solo tengan "FRA" o "1234" u otra combinación dentro de la matriz.

Este es un fragmento de código de formato de filtro en C# que convierte las entradas en cadenas que se pueden buscar:

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

La siguiente lista muestra las entradas y las cadenas de búsqueda (salidas) una al lado de la otra:

  • Para el código del condado "FRA" y el código de producto "1234", el formato de salida es |FRA|1234|*|.

  • Para el código de producto "1234", el formato de salida es |*|1234|*|.

  • Para el código de categoría "C100", el formato de salida es |*|*|C100|.

Proporcione solo el carácter comodín (*) si va a implementar la solución alternativa de la matriz de cadenas. De lo contrario, si usa un tipo complejo, el filtro podría tener este aspecto:

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

Si implementa la solución alternativa, asegúrese de realizar pruebas exhaustivas.

Pasos siguientes

Pruebe el conjunto de datos de hoteles en el asistente Importar datos. Necesita la información de conexión de Azure Cosmos DB proporcionada en el archivo Léame para acceder a los datos.

Con esa información a mano, el primer paso del asistente es crear un nuevo origen de datos de Azure Cosmos DB. Más adelante en el asistente, cuando llegue a la página de índice de destino, se ve un índice con tipos complejos. Cree y cargue este índice y, luego, ejecute las consultas para comprender la nueva estructura.