Azure Cognitive Search で Python のカスタムスキルを Azure Functions で簡単に実装する (Read API v3.2)

# はじめに
組織内に貯まっている大量な構造化・非構造化データから、新たな価値を見出すためのフルマネージド全文検索サービスである [Azure Cognitive Search](https://www.youtube.com/watch?v=9GcjFofXdd0) では、カスタムスキルを使って自分で開発したプログラムをインデクサーの処理に組み込むことができます。今回は [Azure Functions](https://azure.microsoft.com/services/functions/) に Python で作ったカスタムスキルをデプロイして Azure Cognitive Search のインデクサーからコールできるように実装します。

# 目次
1. [カスタムスキルについて](#カスタムスキルについて)
1. [アーキテクチャ](#アーキテクチャ)
1. [カスタムスキルの解説](#カスタムスキルの解説)
1. [インデクサーから送信されるデータ構造](#インデクサーから送信されるデータ構造)
1. [カスタムスキルのデプロイ](#カスタムスキルのデプロイ)
1. [環境変数のセット](#環境変数のセット)
1. [インデクサーの実行](#インデクサーの実行)
1. [参考記事](#参考記事)


#カスタムスキルについて
[Azure Cognitive Search](https://azure.microsoft.com/services/search/) では、カスタムスキルという機能を使って、インデクサーが検索したドキュメントの中身のデータを外部 Web API に飛ばして、さまざまなエンリッチメント処理をすることができます。詳しくは、「[Azure Cognitive Search のカスタムスキルを作成しよう](https://qiita.com/nohanaga/items/986887b82cf76dc7b0a9)」を参照してください。

#アーキテクチャ
今回は以下の図のようなアーキテクチャを考えます。

![001b.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/540f0bd1-04e7-0356-6790-6b0bf21fa1bf.jpeg)

今回のサンプルでは、シンプルに画像データを外部 Web API に送信し、[Read API v3.2](https://qiita.com/nohanaga/items/05281ed97f5fe01f0151) を使って OCR 読み取り結果を返すという処理にしています。このサンプルコードをベースにご自分が行いたい処理に書き換えることができます。ちなみに、Azure Cognitive Search の組み込みスキルを使うこともできますが、Azure Cognitive Services 側の更新がすぐに適用されるという訳ではないため、最新のエンドポイント/モデルを使って、パラメータも細かく指定して API をコールしたい場合に対応できます。

# カスタムスキルの解説

スキルセット定義に従ってインデクサーから画像データを受け取り、Cognitive Services の Read API v3.2 をコールして結果を取得し、インデクサーに返却するサンプルコードを [Github](https://github.com/nohanaga/azure-cognitive-search-readapi-skill) にアップしました。

Read API v3.2 では、読み取り結果取得までに 2 段階の API コール（非同期的）が必要となったため、[コード](https://github.com/nohanaga/azure-cognitive-search-readapi-skill/blob/main/ReadAPI/readapi.py)に結果取得のループを入れています。


# インデクサーから送信されるデータ構造
このデータ構造を正しく理解してパースしないと入力でハマります。

```python
{
    "values": [
        {
            "recordId": "record1",
            "data": { 
                "image": {
                    "$type": "file",
                    "url": "https://xxx.jpg",
                    "data": "/9j/4AAQS..."
                }
            }
        }
    ]
}
```

スキルセット定義の入力ソースフィールドに `/document/normalized_images/*` と指定した場合、上記のようなデータがインデクサーから送信されます。Azure Cognitive Search が優れているのは、画像ファイルだけでなく、ドキュメント内の埋め込み画像についても抽出することができる点です。`data` 項目には画像データのバイナリが Base64 エンコードされて格納されているので、これをデコードして Python の バイナリストリームにロードしています。

# カスタムスキルのデプロイ
ローカルに clone したコードを、Azure Functions にデプロイし、Web API として使えるようにするには、Visual Studio Code から [Azure Functions for Visual Studio Code 拡張](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-develop-vs-code?tabs=python)を使ってデプロイするのが便利です。

![003.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/67c47411-827e-8000-def7-639c33429b4a.jpeg)

Azure Functions for Visual Studio Code 拡張は、Visual Studio Code の拡張機能から `functions` で検索し、上記の拡張をインストールします。

![002.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/dfcbe32d-a74d-ef23-2558-60f8f56b7746.jpeg)

Functions 拡張をインストールした後は、上記のように Functions メニューから、ボタン一発でデプロイしたり新しい Functions を作成したりすることができます。

「Deploy to Functions App...」ボタンをクリックして、コマンドパレットから以下のように入力します。

1. + Create new Functions App in Azure... Advanced を選択
1. Functions 名を入力して Enter
1. Select a runtime stack で `Python 3.6` を選択
1. + Create new resource group を選択
1. 任意のリソースグループ名を入力して Enter
1. Select a location for new resources でデプロイするリージョンを選択
1. Functions の[ホスティングプラン](https://docs.microsoft.com/azure/azure-functions/functions-scale)を選択。`Consumption` で OK
1. + Create new storage account を選択
1. 任意のストレージアカウント名を入力して Enter
1. Application Insight は任意。今はスキップ。

すると自動的に自分のサブスクリプションメニュー配下に新しい Functions が作成されます。いったん作成してしまえば、次からの更新は非常に簡単になります。作成方法に関する詳しい Docs は[こちら](https://docs.microsoft.com/azure/azure-functions/create-first-function-vs-code-python)。

![004.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/354cf6d4-ddf4-5a75-30af-340b4731d547.jpeg)

デプロイが完了すると、VS Code の右下に上図のような通知が表示されます。その後、Functions メニューから、作成した Functions 名を選択し、`Functions->ReadAPi` の順に選択し、右クリックメニューから「Copy Function Url」をクリックすれば、Web API のエンドポイント URL がクリップボードにコピーされます。この URL をスキルセット定義の `uri`項目に貼り付けます。

![005.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/fc44befc-2331-d8b3-527b-1aed4dcb69ea.jpeg)

「Copy Function Url」をクリックすると `[AzureFunctionEndpointUrl]/api/ReadAPI?code=[AzureFunctionDefaultHostKey]` の形式で URL がコピーされます。

# 環境変数のセット
本サンプルをそのまま使用するには、Computer Vision リソースの API キーとエンドポイントを取得しておく必要があります。[Azure Portal](https://portal.azure.com/) から Computer Vision サービスを開き、「キーとエンドポイント」からキーとエンドポイントをコピーします。これを Python コードからどのように使用するかというと、セキュリティ上の理由から直接コードに埋め込むことはしません。

```python
        # get key and endpoint from Environment variable
        computerVisionKey = os.environ['COMPUTER_VISION_KEY']
        computerVisionEndpoint = os.environ['COMPUTER_VISION_ENDPOINT']
```
このように、`COMPUTER_VISION_KEY` と `COMPUTER_VISION_ENDPOINT` を環境変数からロードしています。この環境変数は、Azure Functions の「[アプリケーションの設定](https://docs.microsoft.com/azure/azure-functions/functions-app-settings)」で定義した値を直接使用することができます。これによってキーの一括管理と保存時の暗号化が適用されます。

![006.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/c693b039-94a4-39bb-eed0-e670c9e009d3.jpeg)

VS Code から設定するには、Functions メニューから、「Application Settings」を開き右クリックして「Add New Setting...」からセットすることができます。コマンドパレットからキー名を入力して Enter し、次に値を入力して Enter することで設定できます。Azure Portal から設定するには、Functions を開いて設定→構成を開けば登録できます。

# スキルセット定義
Web API 側の設定が完了したので、Azure Cognitive Search 側のスキル定義を編集します。例では[こちら](https://qiita.com/nohanaga/items/a8a70bb19cdb2710eb72)のハンズオンで作成したスキルセット定義をベースに考えます。

```json
{
    "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill",
    "name": "ReadApiSkill",
    "description": "Reads characters from a document using the Read API 3.2.",
    "uri": "[AzureFunctionEndpointUrl]/api/AnalyzeForm?code=[AzureFunctionDefaultHostKey]",
    "context": "/document/normalized_images/*",
    "inputs": [
        {
            "name": "image",
            "source": "/document/normalized_images/*"
        }
    ],
    "outputs": [
        {
          "name": "ocrtext",
          "targetName": "ocrtext"
        }
    ]
}

```

まず、上記の `uri` 項目をさきほどコピーした Web API のエンドポイント URL で置き換えます。また、必要に応じて以下の[パラメータ](https://docs.microsoft.com/azure/search/cognitive-search-custom-skill-web-api#skill-parameters)を追加してパフォーマンスを調整してください。

- `timeout`: カスタムスキルのタイムアウト値。デフォルト 30 秒
- `batchSize`: 1コール当たりのデータレコード送信数。デフォルト 1000
- `degreeOfParallelism`: 並列コール数。デフォルト 5

下のコメント欄でも議論がありますが、カスタムスキルがコールしている API の処理に時間がかかる場合があるため、ひとまずタイムアウト値を最大の `"timeout": "PT230S"` とすることをお勧めします。

その後上記のカスタムスキル定義をスキルセット定義に追加しますが、すでにハンズオンでは組み込みの OCR スキルが定義されているので、**以下の定義をすべて上の定義で置き換えます。**

```json
    {
      "@odata.type": "#Microsoft.Skills.Vision.OcrSkill",
      "name": "#5",
      "description": null,
      "context": "/document/normalized_images/*",
      "textExtractionAlgorithm": null,
      "lineEnding": "Space",
      "defaultLanguageCode": "ja",
      "detectOrientation": true,
      "inputs": [
        {
          "name": "image",
          "source": "/document/normalized_images/*"
        }
      ],
      "outputs": [
        {
          "name": "text",
          "targetName": "text"
        },
        {
          "name": "layoutText",
          "targetName": "layoutText"
        }
      ]
    }
```

その後にあるマージスキルの `itemsToInsert` を `/document/normalized_images/*/text` から `/document/normalized_images/*/ocrtext` に置換します。

```json
   {
      "@odata.type": "#Microsoft.Skills.Text.MergeSkill",
      "name": "#4",
      "inputs": [
      ...
        {
          "name": "itemsToInsert",
          "source": "/document/normalized_images/*/ocrtext"
        },
      ...
   }
```

これでスキルセット定義の設定は完了です。

# インデクサーの実行
Azure Cognitive Search のインデクサーを実行します。実行後、ステータスが「成功」と表示されれば完了です。

![007.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/7cc6c358-0d4b-9edd-5b2b-410dab54fbee.jpeg)

完了しましたら、インデックスの検索エクスプローラーからうまく検索できるかどうか確認します。
これで最新の Cognitive Services API をコールするカスタムスキルを実装することができました。

#参考記事
[Azure Cognitive Search を使って簡単ナレッジマイニングハンズオン（日本語対応）](https://qiita.com/nohanaga/items/a8a70bb19cdb2710eb72)
[Azure Cognitive Search のカスタムスキルを作成しよう](https://qiita.com/nohanaga/items/986887b82cf76dc7b0a9)
[Azure Cognitive Search のデバッグセッションを使ってカスタムスキルを追加しよう](https://qiita.com/nohanaga/items/26355ffba58ba4c71c6e)