Azere Cognitive SearchとAzure Form Recognizerの連携を試してみた ~③Cognitive Search編~

本記事は、前回の記事の続きとなります。もし良ければ、前回の記事もご覧ください。

https://qiita.com/tmiyata25/items/e0d6d2fb759acb9796dd

「Azere Cognitive SearchとAzure Form Recognizerの連携を試してみた」の3回目ということでCognitive Searchについて解説していきます(⑧インデックスマッピング)

![Cognitive_arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/52156a7d-7ea8-18a2-398e-7ee1991e09ae.png)


# リソースの作成
まずは、Cognitive Searchのリソースを作成します。Azure Portalの検索からCognitive Searchで検索し、[作成]をクリックしましょう。
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/9c54b0c9-74cd-9bfd-d8ed-f3a75e94593c.png)
[リソースグループ]を選択、[サービス名]を入力、[場所]を選択します。
![画像2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/bc1a2af8-db05-bcbf-8ba9-db62954b9f27.png)
価格レベルに関しては下記をご参照ください。価格レベルは、検証用なので「Free」で十分です。

https://azure.microsoft.com/ja-jp/pricing/details/search/

# 全体像の確認
各種設定を行う前にCognitive Searchの重要用語について解説します。Cognitive Searchでは大きく4つの概念(```データソース```、```インデクサー```、```スキルセット```、```インデックス```)があり、一つずつ設定を行います。
![画像5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/262b4709-a505-0d45-6d91-b8b99942b466.png)
```データソース```は、Azure Blob StorageやAzure Cosmos DB等の検索したいドキュメントが保存されている場所を指定します。指定できるデータソースは、公式ドキュメントをご参照ください。

https://learn.microsoft.com/ja-jp/azure/search/search-data-sources-gallery

```インデクサー```は、データソースをスキャンし、各スキルセットと連携しながら、インデックスを作成する機能です。スキャンの頻度やスキルセットとインデックスの紐づけはこの機能で行います。```インデックス```では、インデックスのスキーマを定義します。インデックスとして使いたい項目の名前やデータ型を定義します。```スキルセット```は、OCRやキーフレーズ抽出等のドキュメントからメタデータを抽出するための機能を定義します。

# データソースの作成
それでは、データソースを作成していきます。[概要]->[データソース]->[データソースの追加]をクリックします。
![画像7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/c56849a9-6e64-02c3-c761-0740f0cbc981.png)


[データソース]は「Azure Data Lake Storage Gen2」を選択し、データソースの名前を入力しましょう。次に、[既存の接続を選択します]をクリックし、スキャンしたいデータが格納されているコンテナを選択すると、接続文字とコンテナ名を自動で入力されます。BLOBフォルダーは、Data Lake Storage Gen2のコンテナ以下のフォルダパスを入力します。今回は、「pdf/train-receipt-row」を入力します。すべて入力したら、左上の保存をクリックします。
![画像8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/7133d2c6-a1b1-c54d-64ca-b93ea25f29b8.png)

# インデックスの作成
次に、インデックスを作成します。[概要]->[インデックス]->[インデックスの追加]をクリックしましょう。
![画像10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/f06c2eef-183c-0451-78f6-24c2bb2b28ff.png)

最初にインデックス名を入力し、[フィールドの追加]から、今回インデックスに使いたいフィールドを作成していきます。
![画像11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/a48187da-733f-1c8b-baba-5757005cb111.png)

インデックスには、フィールド一つ一つに下記の設定があります。また、インデックスには必ず一意のキーを設定する必要があり、最初に```id```フィールドが作成されています。
| 用語 | 説明 |
|:-|:-|
| 取得可能 | 検索結果でこのフィールドを返すかどうかを決定します  |
| フィルター可能 | フィルタークエリで参照されます。フィルターを使うとクエリ実行前にレコードを除外でき、検索を高速化できます。|
| ソート可能 | 評価、日付、場所などドキュメント内のフィールドに基づいて並べ替えを構成できます |
| ファセット可能 | カテゴリごとのヒット カウントを含む検索結果に活用できます  |
| 検索可能 | テキスト検索が可能となります|
| アナライザー | テキスト検索可能なフィールド字句を解析するためのアナライザーを設定できます。テキストが日本語の場合は日本語のアナライザーを選択します |

公式サイトも貼っておきます。

https://learn.microsoft.com/ja-jp/azure/search/search-what-is-an-index#field-attributes


今回は簡単のため```name```のみを「検索可能」にし、そのほかは「取得可能」という設定を行いました。
![画像12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/139b8930-ba3a-648c-466b-394b5aa33352.png)
JSONの定義は下記の通りです。
```json:index.json
{
  "name": "{{index-name}}",
  "fields": [
    {
      "name": "id",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "sortable": false,
      "facetable": false,
      "key": true
    },
    {
      "name": "name",
      "type": "Edm.String",
      "searchable": true,
      "filterable": false,
      "retrievable": true,
      "sortable": false,
      "facetable": false,
      "key": false,
      "analyzer": "ja.microsoft"
    },
    {
      "name": "reservation_number",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "sortable": false,
      "facetable": false,
      "key": false,
    },
    {
      "name": "total_amount",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "sortable": false,
      "facetable": false,
      "key": false,
    },
    {
      "name": "date_of_purchase",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "sortable": false,
      "facetable": false,
      "key": false,
    },
    {
      "name": "date_of_departure",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "sortable": false,
      "facetable": false,
      "key": false,
    },
    {
      "name": "card_company",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "sortable": false,
      "facetable": false,
      "key": false,
    },
    {
      "name": "from",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "sortable": false,
      "facetable": false,
      "key": false,
    },
    {
      "name": "to",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "sortable": false,
      "facetable": false,
      "key": false,
    }
  ]
}
```

# スキルセットの概念
スキルセットを作成する前に、スキルセットの概念を説明します。インデックスのスキャンが走ると、データソースをスキャンし、ドキュメントのメタ情報を取得します(```Document cracking```)。取得されたメタ情報はそのままインデックスとして使うこともできますし、何らかの処理を加えて別のインデックスとして使うこともできます(```Field mappings```)。さらに、ドキュメントに対してOCRによる文字認識やドキュメント抽出等の```Skillset```を活用することで、```Enriched document```を作成できます。```Enriched document```では、```/document```をルートディレクトリに、グラフ構造としてデータを保持します。データパスとインデックスの紐づけは```Output field mappings```で行います。



![画像18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/215bb556-8afd-e7fb-b0b2-91a4611bd3b6.png)


# スキルセットの作成
それではスキルセットを作成します。今回は、第2回の時に作成したFunctionsを使いたいので、カスタムWEB APIというスキルを使います。[概要]->[スキルセット]->[スキルセットを追加]をクリックしましょう。
![画像13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/850f8729-91a5-364d-8808-f3727091fcf7.png)
スキルセットでは、右側のスキル定義テンプレートを活用してJSON形式でスキルの設定を書いていきます。
![画像14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/d658f78c-9f60-32ef-9996-d67e70e02972.png)
今回は下記の通り設定しました。一つ一つ設定項目を説明していきます。
```json:skillset.json
{
  "name": "train-receipt-skills",
  "skills": [
    {
      "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill",
      "name": "formrecognizer",
      "context": "/document",
      "uri": "https://form-recog-api-2.azurewebsites.net/api/AnalyzeDocument?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "httpMethod": "POST",
      "timeout": "PT30S",
      "batchSize": 2,
      "inputs": [
        {
          "name": "formUrl",
          "source": "/document/metadata_storage_path_plain"
        },
        {
          "name": "formSasToken",
          "source": "/document/metadata_storage_sas_token"
        },
        {
          "name": "model",
          "source": "=\"train-receipt-model-v3\""
        }
      ],
      "outputs": [
        {
          "name": "documents",
          "targetName": "analyzed_documents"
        }
      ]
    }
  ],
  "cognitiveServices": {
    "@odata.type": "#Microsoft.Azure.Search.DefaultCognitiveServices"
  }
}
```
| JSONの項目 | 説明 |
|:-|:-|
| name | スキルセットの名前  |
| skills -> @odata.type | スキルの種類。今回はCustom.WebApiSkillを選択  |
| skills -> name | 個々のスキルの名前  |
| skills -> context | Cognitive Searchが読み取ったドキュメントのデータパス  |
| skills -> uri | FunctionsのURL。前回記事で作成したFunctionsのURLを指定 |
| skills -> timeout | API 呼び出しを行う http クライアントのタイムアウト値。PT30Sは30秒でタイムアウトすることを意味する |
| skills -> batchSize | 1 回の API 呼び出しにつき、どれだけのデータレコードが送信されるか |
| skills -> inputs | FunctionsのAPIを叩く時の入力データ。metadata_storage_path_plainはドキュメントのファイルパス。metadata_storage_sas_tokenはドキュメントのSAS。modelはForm Recognizerのモデル名|
| skills -> outputs | nameにはFunctionsの出力のdata以下のデータパス。targetNameはCognitive Search内のデータパス |

今回作成したForm Recognizerのカスタムモデルは、一回の呼び出しに約10秒弱かかります。従って、10秒×(batchSize:2)=20秒 < timeout:30秒となるように、batchSizeとtimeoutを設定しています。

プロパティの詳しい説明は公式ドキュメントをご参照ください。


https://learn.microsoft.com/ja-jp/azure/search/cognitive-search-custom-skill-web-api#skill-parameters

# インデクサーの設定
最後にインデクサーの設定を行います。[概要]->[インデクサー]->[インデクサーの追加]をクリックします。
![画像15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/fed2a580-898e-4dda-6703-0e44f6355797.png)

インデクサーの名前を入力し、作成したインデックス、データソース、スキルセットを選択します。
![画像16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/b685afc5-94fb-2092-fc98-fc734cffe6fc.png)
その他の設定はGUIからできない項目も含まれているので、タブから[インデクサー定義(JSON)]をクリックし、JSON形式で設定を書いていきます。
![画像17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/a2a7ab8e-3e89-4b3e-ac43-8dc3d0172f13.png)
JSONの設定は下記の通りです。
```json:indexer.json
{
  "name": "train-receipt-indexer",
  "dataSourceName": "train-receipt-datasource",
  "skillsetName": "train-receipt-skills",
  "targetIndexName": "train-receipt-index",
  "fieldMappings": [
    {
      "sourceFieldName": "metadata_storage_path",
      "targetFieldName": "id",
      "mappingFunction": {
        "name": "base64Encode"
      }
    },
    {
      "sourceFieldName": "metadata_storage_path",
      "targetFieldName": "metadata_storage_path_plain"
    }
  ],
  "outputFieldMappings": [
    {
      "sourceFieldName": "/document/analyzed_documents/0/name",
      "targetFieldName": "name"
    },
    {
      "sourceFieldName": "/document/analyzed_documents/0/reservation_number",
      "targetFieldName": "reservation_number"
    },
    {
      "sourceFieldName": "/document/analyzed_documents/0/total_amount",
      "targetFieldName": "total_amount"
    },
    {
      "sourceFieldName": "/document/analyzed_documents/0/date_of_purchase",
      "targetFieldName": "date_of_purchase"
    },
    {
      "sourceFieldName": "/document/analyzed_documents/0/date_of_departure",
      "targetFieldName": "date_of_departure"
    },
    {
      "sourceFieldName": "/document/analyzed_documents/0/card_company",
      "targetFieldName": "card_company"
    },
    {
      "sourceFieldName": "/document/analyzed_documents/0/from",
      "targetFieldName": "from"
    },
    {
      "sourceFieldName": "/document/analyzed_documents/0/to",
      "targetFieldName": "to"
    }
  ]
}
```

```fieldMappings```ではインデックスの処理・追加の設定を行います。```metadata_storage_path```にはBlobのストレージパスが格納されているので、それを```base64Encode```した値を一意の値として```id```に格納します。ちなみにこのkeyの決め方はCognitive Searchのサンプルで良く使われているみたいです。エンコード前のBlobのストレージパスも必要なので、```metadata_storage_path_plain```として退避しておきます。

```json
  "fieldMappings": [
    {
      "sourceFieldName": "metadata_storage_path",
      "targetFieldName": "id",
      "mappingFunction": {
        "name": "base64Encode"
      }
    },
    {
      "sourceFieldName": "metadata_storage_path",
      "targetFieldName": "metadata_storage_path_plain"
    }
  ]
```

https://learn.microsoft.com/ja-jp/azure/search/search-indexer-field-mappings?tabs=rest#define-a-field-mapping

```outputFieldMappings```では```Enriched document```のデータとインデックスの紐づけを行います。

```json
"outputFieldMappings": [
    {
      "sourceFieldName": "/document/analyzed_documents/0/name",
      "targetFieldName": "name"
    }
```
インデクサー定義をすべて書いたら、[保存]をクリックします。
![画像19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/12ca1bbe-de35-e5fe-e83f-5354a3a2d642.png)
すると、初回は自動的にインデックスの実行が走ります。上手くいくと「成功」のステータスが確認できます。
![画像20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/dbf9b816-aa8e-a6bb-77fe-b6cbb3ab779b.png)
最後にインデックスがきちっと取れているか確認します。[概要]->[インデックス]から先ほど作成したインデックスをクリックします。
![画像21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/6b4ddf00-ee9a-b37e-34e8-b703221d6411.png)
クエリ文字列を空白にして、[検索]を押すと全数検索できます。下記の通り正しくインデックス化できていることが確認できました。
![画像22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/494f3c18-55b4-4df2-6cf0-5c553c7e4c9b.png)

# まとめ
3回に分けてCognitive SearchとForm Recognizerの連携を解説しました。サービスの理解とプログラミングが必要なので、システム構築に少し時間がかかりますが、自由度が高く、柔軟にカスタマイズすることができます。是非、ご活用いただければと思います。