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 ein einzelnes Produkt, 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": "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,
    }
    . . .
  ]
}

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.

Komplexe Sammlungsgrenzwerte

Während der Indizierung dürfen maximal 3000 Elemente über alle komplexen Sammlungen hinweg in einem einzelnen Dokument vorhanden sein. Ein Element einer komplexen Sammlung ist ein Mitglied dieser Sammlung. Für Zimmer (die einzige komplexe Sammlung im Hotelbeispiel) ist jedes Zimmer ein Element. Im obigen Beispiel würde das Hotel-Dokument 500 Raumelemente enthalten, wenn das Stay-Kay Hotel 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“).

Aktualisieren komplexer Felder

Alle Neuindizierungsregeln, die allgemein für Felder gelten, gelten auch für komplexe Felder. Das Hinzufügen eines neuen Felds zu einem komplexen Typ erfordert keine Indexneuerstellung, aber die meisten anderen Änderungen erfordern eine Neuerstellung.

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 in Textabfragen

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.

Durchsuchen komplexer Felder in RAG-Abfragen

Ein RAG-Muster übergibt Suchergebnisse an ein Chatmodell für die generative KI und Unterhaltungssuche. Suchergebnisse, die an ein LLM übergeben werden, sind standardmäßig ein vereinfachter Zeilensatz. Wenn Ihr Index jedoch komplexe Typen aufweist, kann Ihre Abfrage diese Felder bereitstellen, wenn Sie die Suchergebnisse zuerst in JSON konvertieren und dann das JSON an das LLM übergeben.

Ein Teilbeispiel veranschaulicht die Technik:

  • Geben Sie die von Ihnen gewünschten Felder im Prompt oder in der Abfrage an
  • Stellen Sie sicher, dass die Felder durchsuchbar und im Index abrufbar sind
  • Wählen Sie die Felder für die Suchergebnisse aus
  • Formatieren Sie die Ergebnisse als JSON
  • Senden Sie die Anforderung für den Chatabschluss an den Modellanbieter
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)

Das End-to-End-Beispiel finden Sie unter Schnellstart: Generative Suche (RAG) mit Basisdaten aus Azure KI-Suche.

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.

Problemumgehung für den komplexen Sammlungsgrenzwert

Denken Sie daran, dass Azure KI-Suche komplexe Objekte in einer Sammlung auf 3000 Objekte pro Dokument beschränkt. Das Überschreiten dieses Grenzwerts führt zur folgenden Meldung:

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

Wenn Sie mehr als 3000 Elemente benötigen, können Sie die Werte mithilfe einer Pipe (|) oder einer beliebigen Form von Trennzeichen trennen, sie verketten und als abgegrenzte Zeichenfolge speichern. Es gibt keine Einschränkung für die Anzahl der in einem Array gespeicherten Zeichenfolgen. Durch das Speichern komplexer Werte als Zeichenfolgen wird die komplexe Sammlungsbeschränkung umgangen.

Gehen Sie zur Veranschaulichung davon aus, dass Sie über ein "searchScope-Array mit mehr als 3000 Elementen verfügen:


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

Die Problemumgehung zum Speichern der Werte als durch Trennzeichen getrennte Zeichenfolge könnte wie folgt aussehen:

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

Das Speichern aller Suchvarianten in der durch Trennzeichen getrennten Zeichenfolge ist in Suchszenarien hilfreich, in denen Sie nach Elementen suchen möchten, die nur „FRA“ oder „1234“ oder eine andere Kombination innerhalb des Arrays aufweisen.

Hier ist ein Codeschnipsel für die Filterformatierung in C#, das Eingaben in durchsuchbare Zeichenfolgen konvertiert:

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

Die folgende Liste enthält Eingaben und Suchzeichenfolgen (Ausgaben) nebeneinander:

  • Für den Ländercode „FRA“ und den Produktcode „1234“ ist die formatierte Ausgabe |FRA|1234|*|.

  • Für den Produktcode „1234“ ist die formatierte Ausgabe |*|1234|*|.

  • Für den Kategoriecode „C100“ ist die formatierte Ausgabe |*|*|C100|.

Geben Sie den Platzhalter (*) nur an, wenn Sie die Problemumgehung für das Zeichenfolgenarray implementieren. Andernfalls könnte ihr Filter wie in diesem Beispiel aussehen, wenn Sie einen komplexen Typ verwenden:

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

Wenn Sie die Problemumgehung implementieren, sollten Sie dies ausführlich testen.

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.