Azure AI Search で複合データ型をモデル化する

Azure AI Search インデックスの作成に使われる外部データセットは、さまざまな形状をしている可能性があります。 場合によっては、階層や入れ子の下部構造が含まれます。 たとえば、単一の顧客に複数の住所が含まれるケース、単一の SKU に複数の色とサイズが含まれるケース、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 ドキュメントは、単純フィールドと複合フィールドで構成されています。 AddressRooms などの複合フィールドには、サブフィールドがあります。 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,
    }
    . . .
  ]
}

複合型のインデックス作成

インデックス作成時には、1 つのドキュメント内のすべての複合コレクションに対して最大 3,000 個の要素を持つことができます。 複合コレクションの 1 つの要素は、そのコレクションのメンバーです。そのため、部屋 (ホテルの例では唯一の複合コレクション) の場合は、各部屋が 1 つの要素となります。 上の例では、"Secret Point Motel" に 500 室の部屋がある場合、ホテルのドキュメントには 500 の部屋要素が含まれることになります。 入れ子になった複合コレクションでは、外側 (親) の要素に加えて、入れ子になった各要素もカウントされます。

この制限は、複合型 (アドレスなど) や文字列コレクション (タグなど) ではなく、複合コレクションにのみ適用されます。

複合フィールドを作成する

他のインデックス定義と同様に、ポータル、REST API、または .NET SDK を使用して、複合型を含むスキーマを作成できます。

他の Azure SDK には、PythonJavaJavaScript のサンプルが用意されています。

  1. Azure portal にサインインします。

  2. 検索サービスの [概要] ページで、[インデックス] タブを選択します。

  3. 既存のインデックスを開くか、新しいインデックスを作成します。

  4. [フィールド] タブを選び、[フィールドの追加] を選びます。 空のフィールドが追加されます。 既存のフィールド コレクションを使っている場合は、下にスクロールしてフィールドを設定します。

  5. フィールドに名前を付け、種類を Edm.ComplexType または Collection(Edm.ComplexType) に設定します。

  6. 右端の省略記号を選び、[フィールドの追加] または [サブフィールドの追加] のいずれかを選び、属性を割り当てます。

複合フィールドを更新する

一般にフィールドに適用されるすべての再インデックス作成規則は、複合フィールドにも適用されます。 ここで、主な規則をいくつか言い換えてみます。フィールドの複合型への追加にインデックスの再構築は必要ありませんが、修正する場合には、ほとんどの場合で必要となります。

定義に対する構造的な更新

新しいサブフィールドは、インデックスを再構築することなく、いつでも複合フィールドに追加できます。 たとえば、インデックスに最上位レベルのフィールドを追加する場合と同様に、"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 コレクション フィルターの概要」をご覧ください。

複合フィールドを選ぶ

$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 Search には、1 つのドキュメント全体のコレクション内の複合オブジェクトの数が 3,000 を超えてはならないという制限があります。

複合コレクションがこの 3000 の制限を超えると、インデックス作成中に次のエラーが発生します。

"ドキュメント内のコレクションが、すべての複合コレクション制限にわたる最大要素数を超えています。 キー '1052' のドキュメントには、コレクション (JSON 配列) 内に '4303' 個のオブジェクトがあります。 ドキュメント全体のコレクションに最大で '3000' 個のオブジェクトを含めることができます。 コレクションからオブジェクトを削除して、ドキュメントのインデックス作成を再試行してください。"

ユース ケースによっては、3,000 個を超える項目をコレクションに追加することが必要になる場合があります。 このようなユース ケースでは、パイプ (|) や任意の形式の区切り記号を使用して値を区切り、それらを連結して、区切られた文字列として保存することができます。 Azure Search の配列に保存される文字列の数には制限がありません。 これらの複合値を文字列として保存すると、この制限を回避できます。 お客様は、この回避策がご自分のシナリオの要件を満たしているかどうかを検証する必要があります。

たとえば、以下の "searchScope" 配列に 3,000 個を超える要素がある場合、複合型を使用することはできません。


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

お客様が、製品やカテゴリに関係なく、国がフランスである項目を検索したいという検索シナリオに対応するためです。 同様に、お客様は、国やカテゴリに関係なく、項目に製品 1234 が含まれているかどうかを確認するために検索が必要になる場合があります。

次の 1 つのエントリのみを

|FRA|1234|C100|

ワイルドカードを使わずに保存したとすると、ユーザーがフランスのみでフィルター処理する場合、"searchScope" 配列に存在するフランスの組み合わせがわからないため、ユーザー入力を "searchScope" 配列に一致するように変換できません

ユーザーが国でのみフィルター処理したい場合があります。ここでは、フランスとしましょう。 ユーザー入力を受け取り、次のように文字列として構築します。

|FRA|*|*|

これは、項目値の配列を検索する際に Azure Search でフィルター処理するために使用できます

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

同様に、ユーザーがフランスと 1234 製品コードを検索する場合は、ユーザー入力を受け取り、それを次のように区切り文字列として構築し、検索配列と照合します。

|FRA|1234|*|

ユーザーが 1234 製品コードを検索する場合は、ユーザー入力を受け取り、それを次のように区切り文字列として構築し、検索配列と照合します。

|*|1234|*|

ユーザーが C100 カテゴリ コードを検索する場合は、ユーザー入力を受け取り、それを次のように区切り文字列として構築し、検索配列と照合します。

|*|*|C100|

ユーザーがフランス、1234 製品コード、および C100 カテゴリ コードを検索する場合は、ユーザー入力を受け取り、それを次のように区切り文字列として構築し、検索配列と照合します。

|FRA|1234|C100|

ユーザーがリストに存在しない国を検索しようとすると、検索インデックスに保存されている区切り配列 "searchScope" と一致せず、結果は返されません。 たとえば、ユーザーがカナダと製品コード 1234 を検索するとします。 ユーザーの検索は次のように変換されます。

|CAN|1234|*|

これは、検索インデックス内の区切り配列のどのエントリにも一致しません。

上記の設計の選択でのみ、このワイルドカード エントリが必要となります。複合オブジェクトとして保存されていた場合は、次に示すように単に明示的な検索を実行することになります。

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

このように、複合コレクションが Azure Search の制限を超える場合、複合コレクションではなく区切り文字列として保存することで、値の組み合わせを検索する必要があるという要件を満たすことができます。 これは回避策の 1 つであり、お客様はこれがご自分のシナリオの要件を満たすかどうかを検証する必要があります。

次のステップ

データのインポート ウィザードで Hotels データ セットを試してください。 データにアクセスするには、readme に記載されている Azure Cosmos DB 接続情報が必要です。

その情報を入手したら、ウィザードの最初の手順は新しい Azure Cosmos DB データ ソースを作成することです。 ウィザードの手順を進め、ターゲットのインデックス ページに移動すると、複合型のインデックスが表示されます。 このインデックスを作成して読み込み、クエリを実行して新しい構造を理解してください。