Azure AI Search で複合データ型をモデル化する
Azure AI Search インデックスの作成に使われる外部データセットは、さまざまな形状をしている可能性があります。 場合によっては、階層や入れ子の下部構造が含まれます。 例としては、1 人の顧客の複数の住所、1 つの製品の複数の色やサイズ、1 冊の書籍の複数の著者などがあります。 モデリングの用語では、このような構造は complex (複合)、compound (複合)、composite (複合)、または aggregate (集約) データ型と呼ばれることがあります。 Azure AI Search でこの概念に対して使われる用語は複合型です。 Azure AI Search では、複合型は複合フィールドを使ってモデル化されます。 複合フィールドは、他の複合型も含めて任意のデータ型を使用できる子 (サブフィールド) が含まれるフィールドです。 これは、プログラミング言語の構造化されたデータ型と同様の方法で機能します。
複合フィールドでは、データ型に応じて、ドキュメント内の 1 つのオブジェクトまたはオブジェクトの配列が表されます。 Edm.ComplexType
型のフィールドでは単一のオブジェクトが表され、Collection(Edm.ComplexType)
型のフィールドではオブジェクトの配列が表されます。
Azure AI Search は、複合型とコレクションをネイティブでサポートしています。 これらの型を使うと、Azure AI Search インデックス内のほとんどすべての JSON 構造をモデル化できます。 Azure AI Search API の以前のバージョンでは、フラット化された行セットのみをインポートできました。 最新のバージョンでは、インデックスはソース データにより密接に調和できるようになりました。 つまり、ソース データに複合型がある場合、インデックスも複合型を持つことができます。
Azure portal のデータのインポート ウィザードで読み込むことができる Hotels データ セットから始めることをお勧めします。 ウィザードでは、ソース内の複合型が検出され、検出された構造に基づいてインデックス スキーマが提案されます。
Note
複合型のサポートは、api-version=2019-05-06
から一般提供されています。
検索ソリューションがコレクション内でフラット化されたデータセットの以前の回避策に基づいている場合は、インデックスを変更して、最新の API バージョンでサポートされている複合型を含めることをお勧めします。 API バージョンのアップグレードの詳細については、最新の REST API バージョンへのアップグレードまたは最新の .NET SDK バージョンへのアップグレードに関する記事を参照してください。
複合構造の例
次の JSON ドキュメントは、単純フィールドと複合フィールドで構成されています。 Address
や Rooms
などの複合フィールドには、サブフィールドがあります。 Address
はドキュメント内の単一オブジェクトなので、サブフィールドには単一の値のセットがあります。 対照的に、Rooms
のサブフィールドには、コレクション内の各オブジェクトに 1 つずつ、複数の値のセットがあります。
{
"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,
}
. . .
]
}
複合フィールドを作成する
他のインデックス定義と同様に、ポータル、REST API、または .NET SDK を使用して、複合型を含むスキーマを作成できます。
他の Azure SDK には、Python、Java、JavaScript のサンプルが用意されています。
Azure portal にサインインします。
検索サービスの [概要] ページで、[インデックス] タブを選択します。
既存のインデックスを開くか、新しいインデックスを作成します。
[フィールド] タブを選び、[フィールドの追加] を選びます。 空のフィールドが追加されます。 既存のフィールド コレクションを使っている場合は、下にスクロールしてフィールドを設定します。
フィールドに名前を付け、種類を
Edm.ComplexType
またはCollection(Edm.ComplexType)
に設定します。右端の省略記号を選び、[フィールドの追加] または [サブフィールドの追加] のいずれかを選び、属性を割り当てます。
複合コレクションの制限
インデックス作成中、1 つのドキュメント内のすべての複合コレクションにまたがって許可される要素は最大 3,000 個です。 複合コレクションの要素は、そのコレクションのメンバーです。 Rooms (Hotel の例での唯一の複合コレクション) の場合は、各部屋が要素です。 上の例では、"Secret Point Motel" に 500 室の部屋がある場合、ホテルのドキュメントには 500 の部屋要素が含まれることになります。 入れ子になった複合コレクションでは、外側 (親) の要素に加えて、入れ子になった各要素もカウントされます。
この制限は、複合型 (アドレスなど) や文字列コレクション (タグなど) ではなく、複合コレクションにのみ適用されます。
複合フィールドを更新する
一般にフィールドに適用されるすべての再インデックス作成規則は、複合フィールドにも適用されます。 複合型への新しいフィールドの追加にインデックス再構築は必要ありませんが、その他のほとんどの変更には再構築が必要です。
定義に対する構造的な更新
新しいサブフィールドは、インデックスを再構築することなく、いつでも複合フィールドに追加できます。 たとえば、インデックスに最上位レベルのフィールドを追加する場合と同様に、"ZipCode" を Address
に、"Amenities" を Rooms
に追加することができます。 既存のドキュメントでは、データを更新してそれらのフィールドに明示的にデータを入力するまで、新しいフィールドの値は null です。
最上位レベルのフィールドと同様に、複合型内の各サブフィールドには型があり、属性を持つことができる点に注意してください
データ更新
upload
アクションを使用してインデックス内の既存のドキュメントを更新する処理は、複合フィールドと単純フィールドで同じように機能します。つまり、すべてのフィールドが置き換えられます。 ただし、merge
(または既存のドキュメントに適用された場合は mergeOrUpload
) は、すべてのフィールドで同じようには機能しません。 具体的には、merge
ではコレクション内の要素のマージがサポートされていません。 この制限は、プリミティブ型のコレクションと複合コレクションに対して存在します。 コレクションを更新するには、完全なコレクション値を取得し、変更して、新しいコレクションを インデックス API 要求に含める必要があります。
テキスト クエリで複合フィールドを検索する
自由形式の検索式は、複合型では期待どおりに機能します。 ドキュメント内の任意の場所にある検索可能なフィールドまたはサブフィールドが一致すると、そのドキュメント自体が一致します。
複数の用語と演算子があり、Lucene 構文で可能なように、一部の用語にフィールド名を指定すると、より細かい表現のクエリにすることができます。 たとえば、このクエリは、"Portland" と "OR" という 2 つの用語を Address フィールドの 2 つのサブフィールドに対して照合を試みます。
search=Address/City:Portland AND Address/State:OR
このようなクエリは、フィルターとは異なり、フルテキスト検索では "非相関" です。 フィルターでは、複合コレクションのサブフィールドに対するクエリを、any
または all
の範囲変数を使って相関させます。 上の Lucene クエリでは、"Portland, Maine" および "Portland, Oregon" の両方と共に Oregon の他の都市を含むドキュメントが返されます。 このようになるのは、各句がドキュメント全体のそのフィールドのすべての値に適用され、"現在のサブ ドキュメント" という概念がないためです。 この詳細については、「Azure AI Search の 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 検索からのグラウンディング データを使用した生成検索 (RAG)」を参照してください。
複合フィールドを選ぶ
$select
パラメーターは、検索結果に返すフィールドを選択するために使用されます。 このパラメーターを使用して複合フィールドの特定のサブフィールドを選択するには、スラッシュ (/
) で区切った親フィールドとサブフィールドを含めます。
$select=HotelName, Address/City, Rooms/BaseRate
検索結果に必要な場合は、インデックス内のフィールドを Retrievable とマークする必要があります。 $select
ステートメントでは Retrievable とマークされたフィールドのみを使用できます。
複合フィールドのフィルター処理、ファセット、並べ替え
検索のフィルター処理とフィールド検索に使用されるものと同じ OData パス構文を、検索要求内のフィールドのファセット、並べ替え、および選択にも使用できます。 複合型の場合、sortable または facetable とマークできるサブフィールド管理する規則が適用されます。 これらの規則について詳しくは、Create Index API reference (Create Index API リファレンス) に関する記事をご覧ください。
サブフィールドのファセット化
型が Edm.GeographyPoint
または Collection(Edm.GeographyPoint)
でない限り、すべてのサブフィールドは facetable とマークすることができます。
ファセットの結果で返されるドキュメント数は、複合コレクション (部屋) 内のサブドキュメントではなく、親ドキュメント (ホテル) に対して計算されます。 たとえば、ホテルに 20 室の型 "suite" があるとします。 facet パラメーターを facet=Rooms/Type
とすると、ファセット数は、部屋に対する 20 ではなく、ホテルに対する 1 になります。
複合フィールドの並べ替え
並べ替え操作は、サブドキュメント (Rooms) ではなく、ドキュメント (Hotels) に適用されます。 Rooms のような複合型のコレクションがある場合は、Rooms では並べ替えることがまったくできないことを認識することが重要です。 実際のところ、どのコレクションでも並べ替えることはできません。
並べ替え操作は、フィールドが単純フィールドでも、または複合型のサブフィールドでも、フィールドがドキュメントごとに単一値の場合に機能します。 たとえば、ホテルごとに住所は 1 つだけなので Address/City
は並べ替え可能であり、$orderby=Address/City
では都市別にホテルが並べ替えられます。
複合フィールドでのフィルター処理
フィルター式で複合フィールドのサブフィールドを参照することができます。 フィールドのファセット、並べ替え、選択に使われるのと同じ OData パス構文を使うだけです。 たとえば、次のフィルターではカナダのすべてのホテルが返されます。
$filter=Address/Country eq 'Canada'
複合コレクションのフィールドでフィルター処理するには、ラムダ式と any
および all
演算子を使うことができます。 その場合、ラムダ式の範囲変数はサブフィールドを持つオブジェクトです。 それらのサブフィールドを標準の OData パス構文で参照することができます。 たとえば、次のフィルターでは、デラックス ルームが少なくとも 1 つはあり、すべてが禁煙室であるすべてのホテルが返されます。
$filter=Rooms/any(room: room/Type eq 'Deluxe Room') and Rooms/all(room: not room/SmokingAllowed)
最上位レベルの単純フィールドと同様に、複合フィールド内の単純サブフィールドは、インデックス定義で filterable 属性が true
に設定されている場合にのみ、フィルターに含めることができます。 詳しくは、Create Index 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 + ")");
}
次の一覧には、入力と検索文字列 (出力) が横に並べて示されています。
"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 データ セットを試してください。 データにアクセスするには、readme に記載されている Azure Cosmos DB 接続情報が必要です。
その情報を入手したら、ウィザードの最初の手順は新しい Azure Cosmos DB データ ソースを作成することです。 ウィザードの手順を進め、ターゲットのインデックス ページに移動すると、複合型のインデックスが表示されます。 このインデックスを作成して読み込み、クエリを実行して新しい構造を理解してください。