分享方式:


在 Azure 搜尋服務中模型化複雜資料類型

用來填入 Azure AI 搜尋服務索引的外部資料集可以有許多形狀。 有時候,其會包括階層式或巢狀子結構。 範例可能包括單一客戶的多個位址、單一產品的多個色彩和大小、單一書籍的多個作者等等。 就模型化而論,您可能會看到這些稱為「複雜」、「複合」、「合成」或「彙總」資料類型的結構。 Azure AI 搜尋服務用於此概念的字詞是「複雜類型」。 在 Azure AI 搜尋服務中,會使用「複雜欄位」來模型化複雜類型。 複雜欄位是一個欄位,其中所包含的子系 (子欄位) 可以是任何資料類型,包括其他複雜類型。 其運作方式與程式設計語言中的結構化資料類型類似。

根據資料類型,複雜欄位會代表文件中的單一物件或物件陣列。 Edm.ComplexType 類型的欄位代表單一物件,而 Collection(Edm.ComplexType) 類型的欄位代表物件陣列。

Azure AI 搜尋服務原本就支援複雜類型和集合。 這些類型幾乎可讓您模型化 Azure AI 搜尋服務索引中的任何 JSON 結構。 在舊版 Azure AI 搜尋服務 API 中,只能匯入扁平化資料列集。 在最新版本中,您的索引現在可以更緊密地對應至來源資料。 換句話說,如果來源資料具有複雜類型,則索引也可以有複雜類型。

若要開始使用,建議使用 Hotels 資料集,而您可以在 Azure 入口網站的 [匯入資料] 精靈中載入此資料集。 此精靈會偵測來源中的複雜類型,並根據偵測到的結構來建議索引結構描述。

注意

api-version=2019-05-06 開始,已正式推出複雜類型支援。

如果您的搜尋解決方案是根據集合中扁平化資料集的先前因應措施所建置,則您應該變更索引,以包括最新 API 版本中所支援的複雜類型。 如需升級 API 版本的詳細資訊,請參閱升級至最新的 REST API 版本升級至最新的 .NET SDK 版本

複雜結構範例

下列 JSON 文件是由簡單欄位和複雜欄位所組成。 AddressRooms 這類複雜欄位具有子欄位。 Address 具有這些子欄位的單一值集,因為其為文件中的單一物件。 相反地,Rooms 具有其子欄位的多個值集,而集合中的每個物件都有一個。

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

建立複雜欄位

與任何索引定義一樣,您可以使用入口網站、REST API.NET SDK 來建立包含複雜類型的結構描述。

其他 Azure SDK 提供 PythonJavaJavaScript 中的範例。

  1. 登入 Azure 入口網站

  2. 在搜尋服務 [概觀] 頁面上,選取 [索引] 索引標籤。

  3. 開啟現有的索引,或建立新的索引。

  4. 選取 [欄位] 索引標籤,然後選取 [新增欄位]。 新增空白欄位。 如果您要使用現有的欄位集合,則請向下捲動以設定欄位。

  5. 指定欄位的名稱,並將類型設定為 Edm.ComplexTypeCollection(Edm.ComplexType)

  6. 選取最右邊的省略符號,並選取 [新增欄位] 或 [新增子欄位],然後指派屬性。

複雜集合限制

在編製索引期間,您最多可以在單一檔內的所有複雜集合中擁有 3,000 個元素。 複雜集合的專案是該集合的成員。 對於 Room (Hotel 範例中唯一複雜的集合),每個房間都是元素。 在上述範例中,如果“Stay-Kay City Hotel” 有 500 個房間,則旅館檔會有 500 個房間元素。 針對巢狀複雜集合,除了外部 (父代) 元素之外,也會計算每個巢狀元素。

此限制僅適用於複雜集合,並不適用於複雜類型 (例如 Address) 或字串集合 (例如 Tags)。

更新複雜欄位

一般套用至欄位的所有重新編製規則索引都仍然適用於複雜欄位。 將新欄位新增至複雜類型不需要索引重建,但大部分的其他修改都需要重建。

定義的結構化更新

您隨時可以將新的子欄位新增至複雜欄位,而不需要重建索引。 例如,允許將 "ZipCode" 新增至 Address 或將 "Amenities" 新增至 Rooms,就像將最上層欄位新增至索引一樣。 除非您更新資料來明確填入新欄位,否則這些欄位的現有文件為 Null 值。

請注意,在複雜類型內,每個子欄位都有一種類型,而且可有多個屬性,就像最上層欄位一樣

資料更新

使用 upload 動作來更新索引中的現有文件,其運作方式與複雜和簡單欄位相同:會取代所有欄位。 不過,所有欄位的 merge (套用至現有文件時為 mergeOrUpload) 的運作方式都不同。 具體來說,merge 不支援合併集合內的元素。 基本類型和複雜集合的集合具有這項限制。 若要更新集合,您需要擷取完整集合值,並進行變更,然後在索引 API 要求中包括新的集合。

在文字查詢中搜尋複雜的欄位

自由格式搜尋運算式預期會與複雜類型搭配運作。 如果文件中的任何位置有任何可搜尋欄位或子欄位相符,則文件本身是相符項目。

如果您有多個字詞和運算子,而某些字詞已指定欄位名稱,則查詢會更為細微,如同使用 Lucene 語法一樣。 例如,此查詢會針對 Address 欄位的兩個子欄位,嘗試比對兩個字詞:"Portland" 和 "OR":

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

與篩選不同,全文檢索搜尋的這類查詢「無關」。 在篩選中,複雜集合子欄位的查詢會使用 anyall 中的範圍變數而相互關聯。 上面的 Lucene 查詢會傳回文件,其中包含 "Portland, Maine" 和 "Portland, Oregon",以及 Oregon 中的其他城市。 發生此情況的原因是每個子句都會套用至整個文件中其欄位的所有值,因此沒有「目前子文件」的概念。 如需此作業的詳細資訊,請參閱了解 Azure AI 搜尋服務中的 OData 集合篩選

在RAG查詢中搜尋複雜欄位

RAG 模式會將搜尋結果傳遞至聊天模型,以進行產生 AI 和交談式搜尋。 根據預設,傳遞給 LLM 的搜尋結果是扁平化的數據列集。 不過,如果您的索引具有複雜類型,則查詢可以在您先將搜尋結果轉換成 JSON 時提供這些欄位,然後將 JSON 傳遞至 LLM。

部分範例說明這項技術:

  • 在提示中或查詢中指出您想要的欄位
  • 請確定欄位可在索引中搜尋和擷取
  • 選取搜尋結果的欄位
  • 將結果格式化為 JSON
  • 將聊天完成的要求傳送給模型提供者
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)

如需端對端範例,請參閱 快速入門:具有來自 Azure AI 搜尋之基礎數據的 Generative search (RAG)。

選取複雜欄位

$select 參數用來選擇搜尋結果中所傳回的欄位。 若要使用此參數來選取複雜欄位的特定子欄位,請包括以斜線 (/) 區隔的父欄位和子欄位。

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

如果您想要搜尋結果中有欄位,則欄位在索引中必須標示為 [可擷取]。 只有標示為 [可擷取] 的欄位才能用於 $select 陳述式中。

篩選、Facet 化和排序複雜欄位

用於篩選和欄位式搜尋的相同 OData 路徑語法也可以用於 Facet 化、排序和選取搜尋要求中的欄位。 針對複雜類型,會套用規則,以治理哪些子欄位可以標示為可排序或可 Facet 化。 如需這些規則的詳細資訊,請參閱建立索引 API 參考

Facet 化子欄位

除非任何子欄位的類型為 Edm.GeographyPointCollection(Edm.GeographyPoint),否則可以將其標示為可 Facet 化。

Facet 結果中所傳回的文件計數是針對父文件 (飯店) 計算而來,而不是複雜集合中的子文件 (房間)。 例如,假設飯店有 20 間類型為「套房」的房間。 如果有此 Facet 參數 facet=Rooms/Type,則 Facet 計數會是一間飯店,而不是 20 間房間。

排序複雜欄位

排序作業會套用至文件 (Hotels),而不是子文件 (Rooms)。 當您有複雜類型集合 (例如 Rooms) 時,請務必瞭解您根本無法根據 Rooms 進行排序。 事實上,您無法根據任何集合進行排序。

欄位的每份文件都有單一值時,排序作業會運作,無論欄位是簡單欄位,還是複雜類型中的子欄位。 例如,Address/City 允許可進行排序,因為每間飯店只會有一個地址,因此 $orderby=Address/City 會依縣/市來排序飯店。

根據複雜欄位進行篩選

您可以在篩選運算式中參照複雜欄位的子欄位。 僅使用用於 Facet 化、排序和選取欄位的相同 OData 路徑語法。 例如,下列篩選條件會傳回加拿大的所有飯店:

$filter=Address/Country eq 'Canada'

若要根據複雜集合欄位進行篩選,您可以搭配使用 Lambda 運算式anyall 運算子。 在此情況下,Lambda 運算式的 [範圍變數] 是具有子欄位的物件。 您可以使用標準 OData 路徑語法來參照這些子欄位。 例如,下列篩選條件會傳回所有至少有一間豪華房和所有非吸煙房的飯店:

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

與最上層簡單欄位一樣,如果索引定義中複雜欄位的簡單子欄位將 filterable 屬性設定為 true 時,則只能包括在篩選條件中。 如需詳細資訊,請參閱建立索引 API 參考

複雜集合限制的因應措施

回想一下,Azure AI 搜尋會將集合中的複雜物件限制為每個檔 3,000 個物件。 超過此限制會導致下列訊息:

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

如果您需要超過 3,000 個專案,您可以使用管線 (|) 或使用任何形式的分隔符來分隔值、串連這些值,並將其儲存為分隔字串。 陣列中儲存的字串數目沒有任何限制。 將複雜值儲存為字串會略過複雜的集合限制。

為了說明,假設您有一個具有超過 3,000 個 "searchScope元素的 「陣列:


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

將值儲存為分隔字串的因應措施可能如下所示:

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

在搜尋案例中儲存分隔字串中的所有搜尋變體,對於您想要搜尋只有 「FRA」 或 「1234」 或數位另一個組合的專案很有説明。

以下是 C# 中的篩選格式化代碼段,可將輸入轉換成可搜尋的字串:

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

下列清單會並排提供輸入和搜尋字串 (outputs) :

  • 對於 「FRA」 縣碼和 「1234」 產品名稱,格式化的輸出為 |FRA|1234|*|

  • 針對「1234」 產品代碼,格式化的輸出為 |*|1234|*|

  • 對於 「C100」 類別代碼,格式化的輸出為 |*|*|C100|

只有在您實作字串數位因應措施時,才提供通配符 (*)。 否則,如果您使用複雜類型,您的篩選可能會像下列範例所示:

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

如果您實作因應措施,請務必以範圍測試。

下一步

在 [匯入資料] 精靈中嘗試 Hotels 資料集。 您需要讀我檔案中所提供的 Azure Cosmos DB 連線資訊,才能存取資料。

有了該資訊之後,精靈中的第一個步驟就是建立新的 Azure Cosmos DB 資料來源。 進一步來說,在精靈中,當您進入目標索引頁面時,會看到具有複雜類型的索引。 建立並載入此索引,然後執行查詢以了解新的結構。