Modellieren komplexer Datentypen in Azure AI Search

Externe Datensätze, die zum Auffüllen eines Azure AI Search-Index verwendet werden, können viele Formen haben. Manchmal enthalten sie hierarchische oder geschachtelte Unterstrukturen. Beispiele sind mehrere Adressen für einen einzelnen Kunden, mehrere Farben und Größen für eine einzelne SKU, mehrere Autoren für ein einzelnes Buch usw. In der Modelliersprache werden diese Strukturen bisweilen als komplexe, zusammengesetzte, verbundene oder aggregierte Datentypen bezeichnet. Der Begriff, den Azure AI Search für dieses Konzept verwendet, ist komplexer Typ. In Azure AI Search werden komplexe Typen durch komplexe Felder modelliert. Ein komplexes Feld ist ein Feld, das untergeordnete Elemente (untergeordnete Felder) enthält, die einen beliebigen Datentyp aufweisen können – einschließlich anderer komplexer Typen. Dies funktioniert auf ähnliche Weise wie bei strukturierten Datentypen in einer Programmiersprache.

Komplexe Felder stellen je nach Datentyp entweder ein einzelnes Objekt im Dokument oder ein Array von Objekten dar. Felder vom Typ Edm.ComplexType stellen einzelne Objekte dar, während Felder vom Typ Collection(Edm.ComplexType) für Arrays von Objekten stehen.

Azure AI Search unterstützt von Haus aus komplexe Typen und Sammlungen. Mit diesen Typen können Sie fast jede JSON-Struktur in einem Azure AI Search-Index modellieren. In früheren Versionen von Azure AI Search APIs konnten nur reduzierte Zeilensätze importiert werden. In der neuesten Version kann der Index nun genauer den Quelldaten entsprechen. Mit anderen Worten: Wenn die Quelldaten komplexe Typen enthalten, kann auch der Index komplexe Typen enthalten.

Zum Einstieg empfiehlt sich das Dataset „Hotels“, das Sie im Assistenten Daten importieren im Azure-Portal laden können. Im Assistenten werden komplexe Typen in der Quelle erkannt, und es wird basierend auf den erkannten Strukturen ein Indexschema vorgeschlagen.

Hinweis

Die Unterstützung für komplexe Typen ist seit api-version=2019-05-06 allgemein verfügbar.

Wenn Ihre Suchlösung auf früheren Problemumgehungen von vereinfachten Datasets in einer Sammlung aufbaut, sollten Sie den Index so ändern, dass er komplexe Typen enthält, wie sie in der neuesten API-Version unterstützt werden. Weitere Informationen zum Aktualisieren von API-Versionen finden Sie unter Aktualisieren auf die neueste Version der REST-API oder Aktualisieren auf die neueste Version des .NET SDK.

Beispiel für eine komplexe Struktur

Das folgende JSON-Dokument besteht aus einfachen und komplexen Feldern. Komplexe Felder, z. B. Address und Rooms, enthalten Unterfelder. Address umfasst einen einzelnen Wertesatz für diese Unterfelder, da es sich um ein einzelnes Objekt im Dokument handelt. Im Gegensatz dazu umfasst Rooms mehrere Wertesätze für die zugehörigen Unterfelder, jeweils einen Satz für jedes Objekt in der Sammlung.

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

Indizieren von komplexen Typen

Für eine Indizierung dürfen maximal 3000 Elemente über alle komplexen Sammlungen hinweg in einem einzelnen Dokument vorhanden sein. Ein Element einer komplexen Sammlung ist ein Member dieser Sammlung, sodass im Fall von Räumen (die einzige komplexe Sammlung im Hotel-Beispiel) jeder Raum ein Element ist. Im obigen Beispiel würde das Hotel-Dokument 500 Raumelemente enthalten, wenn das „Secret Point Motel“ 500 Zimmer hätte. Bei verschachtelten komplexen Sammlungen wird jedes untergeordnete Element ebenfalls gezählt, zusätzlich zu dem äußeren (übergeordneten) Element.

Diese Einschränkung gilt nur für komplexe Sammlungen, nicht für komplexe Typen (wie „Address“) oder Zeichenfolgensammlungen (wie „Tags“).

Erstellen komplexer Felder

Wie jede Indexdefinition können Sie ein Schema, das komplexe Typen enthält, im Portal, mit der REST-API oder mit dem .NET SDK erstellen.

Andere Azure-SDKs enthalten Beispiele in Python, Java und JavaScript.

  1. Melden Sie sich beim Azure-Portal an.

  2. Wählen Sie auf der Seite Übersicht des Suchdiensts die Registerkarte Indizes aus.

  3. Öffnen Sie einen vorhandenen Index, oder erstellen Sie einen neuen Index.

  4. Wählen Sie die Registerkarte Felder und dann Feld hinzufügen aus. Ein leeres Feld wird hinzugefügt. Wenn Sie mit einer vorhandenen Feldauflistung arbeiten, scrollen Sie nach unten, um das Feld einzurichten.

  5. Weisen Sie dem Feld einen Namen zu, und legen Sie den Typ auf Edm.ComplexTypeoder Collection(Edm.ComplexType) fest.

  6. Wählen Sie ganz rechts die Auslassungszeichen und dann entweder Feld hinzufügen oder Untergeordnetes Feld hinzufügen aus. Weisen Sie anschließend Attribute zu.

Aktualisieren komplexer Felder

Alle Neuindizierungsregeln, die allgemein für Felder gelten, gelten auch für komplexe Felder. Eine kurze Wiederholung von einigen Hauptregeln: Das Hinzufügen eines Felds zu einem komplexen Typ erfordert keine Indexneuerstellung, aber die meisten anderen Änderungen erfordern dies.

Strukturelle Aktualisierungen der Definition

Sie können einem komplexen Feld jederzeit neue Unterfelder hinzufügen, ohne dass eine Indexneuerstellung erforderlich ist. Beispielsweise ist es möglich, Address „ZipCode“ oder Rooms „Amenities“ hinzuzufügen, so wie ein Feld auf oberster Ebene einem Index hinzugefügt wird. Vorhandene Dokumente haben einen NULL-Wert für neue Felder, bis Sie diese Felder durch Aktualisieren Ihrer Daten explizit füllen.

Beachten Sie, dass in einem komplexen Typ jedes Unterfeld einen Typ enthält und Attribute enthalten kann, so wie das auch bei übergeordneten Feldern der Fall ist.

Datenupdates

Die Aktualisierung vorhandener Dokumente in einem Index mit der Aktion upload wird für komplexe und einfache Felder auf identische Weise durchgeführt: Alle Felder werden ersetzt. Jedoch wird merge (oder mergeOrUpload beim Anwenden auf ein vorhandenes Dokument) nicht für alle Felder gleich ausgeführt. Insbesondere unterstützt merge nicht das Zusammenführen von Elementen in einer Sammlung. Dies gilt für Sammlungen von primitiven Typen sowie für komplexe Sammlungen. Zum Aktualisieren einer Sammlung müssen Sie den vollständigen Sammlungswert abrufen, Änderungen vornehmen und dann die neue Sammlung in die Anforderung der Index-API einfügen.

Durchsuchen komplexer Felder

Freiform-Suchausdrücke funktionieren bei komplexen Typen wie erwartet. Wenn ein durchsuchbares Feld oder Unterfeld an beliebiger Stelle in einem Dokument übereinstimmt, ist das Dokument selbst eine Übereinstimmung.

Abfragen werden bei mehreren Begriffen und Operatoren differenzierter, und bei einigen Begriffen sind Feldnamen angegeben, wie das mit der Lucene-Syntax möglich ist. Mit der folgenden Abfrage wird beispielsweise versucht, zwei Begriffe, „Portland“ und „OR“, mit zwei Unterfeldern des Felds „Address“ zu vergleichen:

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

Abfragen wie diese sind im Unterschied zu Filtern nicht korreliert für die Volltextsuche. In Filtern werden Abfragen für untergeordnete Felder einer komplexen Sammlung mithilfe der Bereichsvariablen in any oder all korreliert. Die obige Lucene-Abfrage gibt Dokumente zurück, die „Portland, Maine“ und „Portland, Oregon“ enthalten, sowie andere Städte in Oregon. Dies trifft zu, da jede Klausel für alle Werte des jeweiligen Felds im gesamten Dokument gilt – es gibt also kein Konzept für ein „aktuell untergeordnetes Dokument“. Weitere Informationen hierzu finden Sie unter Verstehen der OData-Sammlungsfilter in Azure AI Search.

Auswählen komplexer Felder

Über den Parameter $select wird ausgewählt, welche Felder in den Suchergebnissen zurückgegeben werden. Um diesen Parameter zum Auswählen bestimmter Unterfelder eines komplexen Felds zu verwenden, fügen Sie das übergeordnete Feld und das Unterfeld getrennt durch einen Schrägstrich (/) ein.

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

Felder müssen im Index als „Abrufbar“ markiert sein, wenn sie in den Suchergebnissen enthalten sein sollen. Nur die als „Abrufbar“ markierten Felder können in einer $select-Anweisung verwendet werden.

Filtern, Faceting und Sortieren komplexer Felder

Die für die Filterung und für feldbezogene Suchen verwendete OData-Pfadsyntax kann auch für das Faceting, die Sortierung und die Auswahl von Feldern in einer Suchanforderung verwendet werden. Für komplexe Typen gelten Regeln, mit denen gesteuert wird, welche Unterfelder als sortierbar oder facettierbar markiert werden können. Weitere Informationen zu diesen Regeln finden Sie in der Referenz zur API zur Indexerstellung.

Facettieren von Unterfeldern

Jedes Unterfeld kann als facettierbar markiert werden, mit Ausnahme von Feldern der Typen Edm.GeographyPoint und Collection(Edm.GeographyPoint).

Die in den Facettenergebnissen zurückgegebene Dokumentanzahl wird für das übergeordnete Dokument (ein Hotel) berechnet, nicht für die untergeordneten Dokumente in einer komplexen Sammlung (Zimmer). Beispiel: Ein Hotel hat 20 Zimmer vom Typ „suite“. Für den facettierten Parameter facet=Rooms/Type lautet die Anzahl der Facetten für das Hotel 1, nicht 20 für die Zimmer.

Sortieren komplexer Felder

Sortiervorgänge gelten für Dokumente (Hotels) und nicht für Unterdokumente (Zimmer). Bei einer Sammlung von komplexen Typen, z. B. „Rooms“ (Zimmer), ist es wichtig zu wissen, dass für „Rooms“ keinerlei Sortiervorgänge durchgeführt werden können. Sortiervorgänge können für keine Sammlung durchgeführt werden.

Sortiervorgänge sind möglich, wenn Felder in einem Dokument einwertig sind. Dabei kann es sich um einfache Felder oder um Unterfelder in einem komplexen Typ handeln. Address/City darf z. B. sortierbar sein, da es nur eine Adresse pro Hotel gibt, $orderby=Address/City sortiert die Hotels also nach der Stadt.

Filtern komplexer Felder

Sie können auf die untergeordneten Felder eines komplexen Felds in einem Filterausdruck verweisen. Verwenden Sie einfach die gleiche OData-Pfadsyntax wie für die Facettierung, Sortierung und Auswahl von Feldern. Der folgende Filter gibt z. B. alle Hotels in Kanada zurück:

$filter=Address/Country eq 'Canada'

Um nach einem Feld in einer komplexen Sammlung zu filtern, können Sie einen Lambdaausdruck mit den Operatoren any und all verwenden. In diesem Fall ist die Bereichsvariable des Lambdaausdrucks ein Objekt mit untergeordneten Feldern. Sie können auf diese untergeordneten Felder mit der OData-Standardpfadsyntax verweisen. Der folgende Filter gibt beispielsweise alle Hotels zurück, die mindestens ein Luxuszimmer und ausschließlich Nichtraucherzimmer haben:

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

Wie schon bei einfachen Feldern der obersten Ebene können auch einfache untergeordnete Felder von komplexen Feldern nur in Filtern verwendet werden, wenn ihr filterable-Attribut in der Indexdefinition auf true festgelegt wurde. Weitere Informationen finden Sie in der Referenz zur API zur Indexerstellung.

Azure Search hat die Einschränkung, dass die komplexen Objekte in den Sammlungen in einem einzigen Dokument den Wert von 3000 nicht überschreiten dürfen.

Benutzern wird bei der Indizierung der folgende Fehler angezeigt, wenn komplexe Sammlungen die Grenze von 3000 überschreiten.

„Bei einer Sammlung in Ihrem Dokument wurde die maximale Anzahl der Elemente in allen komplexen Sammlungen überschritten. Das Dokument mit dem Schlüssel ‚1052‘ enthält ‚4303‘ Objekte in Sammlungen (JSON-Arrays). Höchstens 3000 Objekte sind in Sammlungen im gesamten Dokument zulässig. Entfernen Sie Objekte aus Sammlungen, und indizieren Sie das Dokument erneut.“

In einigen Anwendungsfällen müssen wir möglicherweise mehr als 3000 Elemente zu einer Sammlung hinzufügen. In diesen Anwendungsfällen können wir Pipe (|) oder ein beliebiges Trennzeichen verwenden, um die Werte zu trennen, sie zu verketten und als mit Trennzeichen getrennte Zeichenfolge zu speichern. Es gibt keine Einschränkung für die Anzahl von Zeichenfolgen, die in einem Array in Azure Search gespeichert werden. Wenn Sie die komplexen Werte als Zeichenfolgen speichern, können Sie die Einschränkung umgehen. Der Kunde muss überprüfen, ob diese Problemumgehung die Anforderungen seines Szenarios erfüllt.

Es wäre z. B. nicht möglich, komplexe Typen zu verwenden, wenn das nachfolgende „searchScope“-Array mehr als 3000 Elemente aufweist.


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

Wenn Sie die komplexen Werte als Zeichenfolgen mit einem Trennzeichen speichern, können Sie die Einschränkung umgehen.

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

Anstatt diese mit Platzhaltern zu speichern, können wir auch ein benutzerdefiniertes Analysetool verwenden, das das Wort in „|“ aufteilt, um die Speichergröße zu reduzieren.

Wir haben die Werte mit Platzhaltern gespeichert, anstatt sie einfach wie folgt zu speichern:

|FRA|1234|C100|

So können wir Suchszenarien gerecht werden, in denen der Kunde möglicherweise nach Elementen mit dem Land Frankreich suchen möchte, unabhängig von Produkten und Kategorien. Ebenso könnte es sein, dass der Kunde mit der Suche sehen möchte, ob das Element das Produkt „1234“ enthält, unabhängig vom Land oder von der Kategorie.

Wenn wir nur den einen Eintrag

|FRA|1234|C100|

ohne Wildcards gespeichert haben und der Benutzer nur nach Frankreich filtern möchte, können wir die Benutzereingabe nicht konvertieren, sodass sie mit dem „searchScope“-Array übereinstimmt, da wir nicht wissen, welche Kombination von Frankreich in unserem „searchScope“-Array vorhanden ist.

Angenommen, der Benutzer möchte nur nach dem Land Frankreich filtern. Wir nehmen die Benutzereingabe und konstruieren daraus die folgende Zeichenfolge:

|FRA|*|*|

Diese können wir dann zum Filtern in der Azure-Suche verwenden, wenn wir in einem Array von Elementwerten suchen

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

Ähnlich verhält es sich, wenn der Benutzer nach „Frankreich“ und dem Produktcode „1234“ sucht: Wir nehmen die Benutzereingabe, konstruieren daraus die folgende mit Trennzeichen getrennte Zeichenfolge und gleichen sie mit unserem Sucharray ab.

|FRA|1234|*|

Wenn der Benutzer nach dem Produktcode „1234“ sucht, nehmen wir die Benutzereingabe, konstruieren daraus die folgende mit Trennzeichen getrennte Zeichenfolge und gleichen sie mit unserem Sucharray ab.

|*|1234|*|

Wenn der Benutzer nach dem Kategoriecode „C100“ sucht, nehmen wir die Benutzereingabe, konstruieren daraus die folgende mit Trennzeichen getrennte Zeichenfolge und gleichen sie mit unserem Sucharray ab.

|*|*|C100|

Wenn der Benutzer nach „Frankreich“, dem Produktcode „1234“ und dem Kategoriecode „C100“ sucht, nehmen wir die Benutzereingabe, konstruieren daraus die folgende mit Trennzeichen getrennte Zeichenfolge und gleichen sie mit unserem Sucharray ab.

|FRA|1234|C100|

Wenn ein Benutzer versucht, nach Ländern zu suchen, die nicht in unserer Liste vorhanden sind, wird keine Übereinstimmung mit dem mit Trennzeichen getrennten Array „searchScope“ erzielt, der im Suchindex gespeichert ist, und es werden keine Ergebnisse zurückgegeben. Angenommen, ein Benutzer sucht nach Kanada und dem Produktcode „1234“. Die Benutzersuche wird konvertiert in

|CAN|1234|*|

Diese Zeichenfolge ergibt keine Übereinstimmung mit einem der Einträge im mit Trennzeichen getrennten Array in unserem Suchindex.

Nur die obige Designauswahl erfordert diesen Platzhaltereintrag. Wenn es als komplexes Objekt gespeichert worden wäre, hätten wir einfach eine explizite Suche wie folgt ausführen können.

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

So können wir Anforderungen erfüllen, bei denen wir nach einer Kombination von Werten suchen müssen, indem wir sie als mit Trennzeichen getrennte Zeichenfolge anstelle einer komplexen Sammlung speichern, wenn unsere komplexen Sammlungen die Grenze von Azure Search überschreiten. Das ist eine der Problemumgehungen, und der Kunde muss überprüfen, ob sie die Anforderungen seines Szenarios erfüllt.

Nächste Schritte

Testen Sie das Dataset „Hotels“ im Assistenten Daten importieren. Für den Zugriff auf die Daten benötigen Sie die in der Infodatei angegebenen Azure Cosmos DB-Verbindungsinformationen.

Mit diesen Informationen erstellen Sie im ersten Schritt im Assistenten eine neue Azure Cosmos DB-Datenquelle. Später im Assistenten wird auf der Seite für den Zielindex ein Index mit komplexen Typen angezeigt. Erstellen und laden Sie diesen Index, und führen Sie dann Abfragen aus, um sich mit der neuen Struktur vertraut zu machen.