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

Indexación de tipos 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 miembro de esa colección, por lo que en el caso de las habitaciones (la única colección compleja en el ejemplo del hotel), cada habitación es un elemento. En el ejemplo anterior, si el "motel Secret Point" tuviera 500 habitaciones, el documento del hotel tendría 500 elementos de habitaciones. 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).

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.

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. Si replanteamos algunas de las reglas principales aquí, la adición de un campo a un tipo complejo no requiere la recompilación de un índice, pero la mayoría de las modificaciones sí.

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

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.

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.

Azure Search tiene la limitación de que los objetos complejos de las colecciones de un solo documento no pueden superar los 3000.

Los usuarios encontrarán un error similar al siguiente durante la indexación cuando las colecciones complejas superen el límite de 3000.

“Una colección del documento supera el número máximo de elementos en el límite de todas las colecciones complejas. El documento con la clave '1052' tiene '4303' objetos en las colecciones (matrices JSON). Se permiten como máximo 3000 objetos en las colecciones de todo el documento. Quite los objetos de las colecciones e intente indexar el documento de nuevo".

En algunos casos de uso, puede que sea necesario agregar más de 3000 elementos a una colección. Si es así, podemos usar una barra vertical (|) o usar cualquier forma de delimitador para los valores, concatenarlos y almacenarlos como cadena delimitada. No existe limitación alguna respecto al número de cadenas almacenadas en una matriz en Azure Search. Al almacenar estos valores complejos como cadenas se evita la limitación. El cliente debe validar si esta solución alternativa cumple los requisitos de su escenario.

Por ejemplo, no sería posible usar tipos complejos si la matriz "searchScope" siguiente tuviera más de 3000 elementos.


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

Al almacenar estos valores complejos como cadenas con un delimitador se evita la limitación.

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

En lugar de almacenarlos con caracteres comodín, también podemos usar un analizador personalizado que divide la palabra en | para reducir el tamaño de almacenamiento.

La razón por la que hemos almacenado los valores con caracteres comodín en lugar de almacenarlos simplemente como se hace a continuación

|FRA|1234|C100|

es abordar escenarios de búsqueda en los que puede que el cliente quiera buscar elementos que tengan Francia como país, independientemente de los productos y las categorías. Del mismo modo, puede que el cliente necesite buscar para ver si el elemento tiene el producto 1234, independientemente del país o de la categoría.

Si solo hubiéramos almacenado una entrada

|FRA|1234|C100|

sin caracteres comodín y el usuario quisiera filtrar solo por Francia, no podemos convertir la entrada de usuario para que coincida con la matriz "searchScope" porque no sabemos qué combinación de Francia está presente en nuestra matriz "searchScope".

Si el usuario quiere filtrar solo por país, por ejemplo, Francia, tomaremos la entrada de usuario y la construiremos como cadena, como se muestra a continuación:

|FRA|*|*|

la cual podemos usar después para filtrar en Azure Search al buscar en una matriz de valores de elemento

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

Del mismo modo, si el usuario busca Francia y el código de producto 1234, tomaremos la entrada de usuario, la construiremos como cadena delimitada como se muestra a continuación y haremos que concuerde con nuestra matriz de búsqueda.

|FRA|1234|*|

Si el usuario busca el código de producto 1234, tomaremos la entrada de usuario, la construiremos como cadena delimitada como se muestra a continuación y haremos que concuerde con nuestra matriz de búsqueda.

|*|1234|*|

Si el usuario busca el código de categoría C100, tomaremos la entrada de usuario, la construiremos como cadena delimitada como se muestra a continuación y haremos que concuerde con nuestra matriz de búsqueda.

|*|*|C100|

Si el usuario busca Francia, el código de producto 1234 y el código de categoría C100, tomaremos la entrada de usuario, la construiremos como cadena delimitada como se muestra a continuación y haremos que concuerde con nuestra matriz de búsqueda.

|FRA|1234|C100|

Si un usuario intenta buscar países que no estén en nuestra lista, no tendrá coincidencias con la matriz delimitada "searchScope" almacenada en el índice de búsqueda y no se devolverá ningún resultado. Por ejemplo, un usuario busca Canadá y el código de producto 1234. La búsqueda del usuario se convertirá a

|CAN|1234|*|

Esto no coincidirá con ninguna de las entradas de la matriz delimitada en nuestro índice de búsqueda.

Solo la opción de diseño anterior requiere esta entrada de carácter comodín; si se hubiera guardado como objeto complejo, podríamos haber realizado simplemente una búsqueda explícita, como se muestra a continuación.

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

De esta forma, podemos satisfacer los requisitos donde necesitemos buscar una combinación de valores al almacenarla como cadena delimitada en lugar de colección compleja en caso de que nuestras colecciones complejas superen el límite de Azure Search. Esta es una de las soluciones alternativas y el cliente debe validar si cumpliría los requisitos de su escenario.

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.