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

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

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

今回は「Azere Cognitive SearchとAzure Form Recognizerの連携を試してみた ~②Azure Functions編~」ということで、Azure FunctionsからForm RecognizerのAPIを呼び出す部分について解説します。
![Cognitive_arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/52156a7d-7ea8-18a2-398e-7ee1991e09ae.png)
フローでいうと③->④、⑥->⑦の部分となります。具体的にはAzure Functionsのコードを記述・デプロイして、外部からAPIを叩いて正しいレスポンスが得られることをゴールとします。

# 開発環境の用意
筆者のAzure Functionsの開発環境は下記の通りです。
- Ubuntu 20.04 on WSL 2 
- VSCode
- Python 3.8

WSLを使うかどうかは好みですが、VSCodeはFunctionsのコードを開発する上でとても便利なので、入れておくことを推奨します。VSCodeの拡張機能として**Azure Account**と**Azure Functions**も入れておきましょう。
![画像2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/69a09f35-120b-dc28-84ec-d558d2841eea.png)

# コード開発
それではFuntionsのコードを書いていきます。といっても、Cgnitive SearchからForm Recognizerを呼び出すためのテンプレートがGitHubに公開されているので、`git clone`して、必要な部分だけ切り出します。
```bash
$ git clone https://github.com/Azure-Samples/azure-search-power-skills.git
$ cp -r azure-search-power-skills/Vision/FormRecognizer ./
$ code FormRecognizer
```
コードのレポジトリはこちらです。

https://github.com/Azure-Samples/azure-search-power-skills/tree/main/Vision/FormRecognizer

FormRecognizerのディレクトリをVSCodeで開くと、Functionsの拡張機能が勝手にフォルダを認識し、Yesをクリックすると開発に必要な設定(.venvや.vscode)を入れてくれます。RuntimeはPythonを選択しましょう。
![画像3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/4efc339b-43b4-229c-d207-5702a81dd1e6.png)

こちらのコードですが、GitHubにもかいてある通りFormRecognizerの**エンドポイント**と**キー**を環境変数として設定する必要があります。
>This function requires a FORMS_RECOGNIZER_ENDPOINT and a FORMS_RECOGNIZER_KEY settings set to a valid Azure Forms Recognizer API key and to your custom Form Recognizer 2.1-preview endpoint. If running locally, this can be set in your project's local environment variables. This ensures your key won't be accidentally checked in with your code. If running in an Azure function, this can be set in the application settings.

ルートディレクトリ(`FormRecognizer/`)に`local.settings.json`を作成することで、Functionsのローカル実行時環境変数等の設定を読み取ってくれます。下記の通り`FORM_RECOGNIZER_ENDPOINT`と`FORM_RECOGNIZER_KEY`を記述します。
```json:local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "FORM_RECOGNIZER_ENDPOINT": "https://formrecognizer-test-1234.cognitiveservices.azure.com/",
    "FORM_RECOGNIZER_KEY": "XXXXXXXXXXXXXXXXXXXXXXXXXXX"
  }
}
```
`FORM_RECOGNIZER_ENDPOINT`と`FORM_RECOGNIZER_KEY`はAzure Portalから取得します。
![画像4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/341316ae-50fb-c53f-ae1a-310c25f8d551.png)

# Azure Functions Core Tools のインストール
次にFunctionsのCLIをインストールします。OS毎にインストールの方法が異なるので、こちらの公式ドキュメントをご参照ください。

https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-run-local?tabs=v4%2Clinux%2Ccsharp%2Cportal%2Cbash#install-the-azure-functions-core-tools

Ubuntuの場合は下記のコマンドを実行します。
```
$ curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
$ sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
$ sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list'
$ sudo apt-get update
$ sudo apt-get install azure-functions-core-tools-4
```

# ローカル実行
一通り実行の準備が整ったので、ローカルでコードを実行してみます。
Pythonの仮想環境に入って、`azure-ai-formrecognizer`のバージョンを上書きします。
```
$ source .venv/bin/activate
$ func start
Found Python version 3.8.10 (python3).

Azure Functions Core Tools
Core Tools Version:       4.0.4653 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.6.1.18388


Functions:

        AnalyzeDocument: [GET,POST] http://localhost:7071/api/AnalyzeDocument

For detailed output, run func with --verbose flag.
[2022-08-25T06:38:55.703Z] Worker process started and initialized.
[2022-08-25T06:39:00.230Z] Host lock lease acquired by instance ID '0000000000000000000000007EA5C856'.
```
上記のような出力が表示されれば、正しく動作しています。

# コードテスト
次にコードをテストしましょう。[GitHubのドキュメント](https://github.com/Azure-Samples/azure-search-power-skills/tree/main/Vision/FormRecognizer#sample-input)によると、APIの入力フォーマットは下記の通りになります。
```json
{
    "values": [
        {
            "recordId": "record1",
            "data": { 
                "model": "prebuilt-invoice",
                "formUrl": "https://github.com/Azure-Samples/azure-search-power-skills/raw/master/SampleData/Invoice_4.pdf",
                "formSasToken":  "?st=sasTokenThatWillBeGeneratedByCognitiveSearch"
            }
        }
    ]
}
```
今回モデルはカスタムモデルを使うので、modelの部分は前回作成した`train-receipt-model-v2`を使います。
![画像5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/81e49e47-a917-3abd-df9f-0b43e576ad34.png)
`formUrl`と`formSasToken`はAzure Data Lake StorageにアップロードしたpdfファイルからSAS生成で取得しましょう。
![画像6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/bdd366bd-ec6e-c85c-a23b-4f3668407ae4.png)

入力データとして`input.json`をルート以下に作りました。

```json:input.json
{
    "values": [
        {
            "recordId": "record1",
            "data": { 
                "model": "train-receipt-model-v2",
                "formUrl": "https://sampledata234.blob.core.windows.net/data/pdf/train-receipt/%E6%96%B0%E5%B9%B9%E7%B7%9A_220728.pdf",
                "formSasToken": "?sp=r&st=2022-08-25T06:53:43Z&se=2023-07-13T14:53:43Z&spr=https&sv=2021-06-08&sr=b&sig=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
            }
        }
    ]
}
```

::: note warn
`formSasToken`の先頭には必ず"?"を付けましょう。PortalからBLOB SAS トークンをそのままコピーすると先頭に"?"がついていませんのでご注意を。
:::

このJSONファイルを入力に、APIを叩いてみましょう。
```bash
$ curl -X POST -H "Content-Type: application/json" -d '@input.json' http://localhost:7071/api/AnalyzeDocument
```
レスポンスがこちらです。
```json:APIレスポンス
{
    "values": [
        {
            "recordId": "record1",
            "data": {
                "documents": [
                    {
                        "name": "山田太郎",
                        "card_company": "JCB",
                        "from": "品川",
                        "to": "新大阪"
                    }
                ],
                "tables": [
      ...
```
kvpのフィールドを見ると、前回取得できていた日時とか合計金額が消えていることが分かります。Form Recognizerが返すレスポンスを見てみると日時や合計金額は正しく取得できていました。実際にはFunctionsの出力結果に変換する際、String型のフィールドのみkvpに追加する、というコードになっていました。
```python:__init__.py
def get_fields(result):
    fields = []
    for idx, doc in enumerate(result.documents):
        kvp = {}
        for kv_pair in doc.fields.items():
            if kv_pair[1].value_type == "string":  #この部分
                kvp[kv_pair[0]] = kv_pair[1].content
            elif kv_pair[1].value_type == "list":
                line_items = []
                items = {}
                for rows in kv_pair[1].value:
                    for row in rows.value.items():
                        items[row[0]] = row[1].content
                        
                    line_items.append(items)

                kvp[kv_pair[0]] = line_items

        fields.append(kvp)
    return fields
```

なので、下記の通り、思い切って全フィールドをstring型に変換してkvpに追加する、というコードに書き換えました。
```python:__init__.py
def get_fields(result):
    fields = []
    for idx, doc in enumerate(result.documents):
        kvp = {}
        for kv_pair in doc.fields.items():
            kvp[kv_pair[0]] = str(kv_pair[1].content) # この部分

        fields.append(kvp)
    return fields
```

もう一度APIを叩くとすべてのフィールドが取得できるようになっていました。

```json:APIレスポンス
{
    "values": [
        {
            "recordId": "record1",
            "data": {
                "documents": [
                    {
                        "name": "山田太郎",
                        "reservation_number": "2010",
                        "total_amount": "¥14,720",
                        "date_of_purchase": "2022年7月27日",
                        "date_of_departure": "7月28日",
                        "card_company": "JCB",
                        "from": "品川",
                        "to": "新大阪"
                    }
                ],
               ...
```

::: note
今回は簡単なコード修正にとどめておきますが、Functionsでのレスポンスがほとんどそのままインデックスになるので、このフェーズで変数型のチェックや値のチェック、必要に応じてデータ加工を行うべきです。今回で言うと、"total_amount"の"¥"や","を削除する、"date_of_departure"を"yyyymmdd"に変換する、等です。
:::



# コードデプロイ
Azure Accountsの拡張機能からAzureにSign Inすると下記のようにサブスクリプションのリソースが一覧で確認できるようになります。Function Appを右クリックしFuntion Appのリソースを作成しましょう。リソース名を適当に設定し、ランタイムはPythonを選択します。リソースがが作成できたことを確認しましょう。
![画像7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/ef81e33d-dfdd-d2ba-2777-c200a42f8d3b.png)

次にWORKSPACEを見ると、先ほどローカルで実行したFunctionsのプロジェクトが確認できます。右上のアイコンからローカルプロジェクトをAzureにデプロイします。リソースは先ほど作成したものを選択します。
![画像8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/ccface0d-a65c-89ac-095b-a24b7b218850.png)
Azure Portal側からもデプロイできていることが確認できます。
![画像9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/ba20ed75-c492-84f5-5ec6-fe91d91065ce.png)
次に環境変数を設定します。[構成]->[アプリケーション設定]->[新しいアプリケーション設定]をクリックします。
![画像10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/39944383-05cf-d6a7-b363-035024db6028.png)
先ほどローカルで設定した環境変数`FORM_RECOGNIZER_ENDPOINT`と`FORM_RECOGNIZER_KEY`を同様に設定しましょう。
![画像11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/40f7a1e3-2ded-d758-ede1-c49eab45ed05.png)

# Functionsのテスト
最後に[コードとテスト]->[テストと実行]をクリックし、ボディに先ほど作成した`input.json`の内容をコピペし、実行をクリックします。
![画像12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/956a94a7-878e-e5c8-1a4e-d9e51acf5452.png)
レスポンスが返ってくることを確認できました。
![画像13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2681297/a6875b9e-3d1d-f692-1b1e-b2c4431c493f.png)

# まとめ
今回はCognitive SearchとForm Recognizerの連携部分でAzure Functionsについて解説しました。少しコードを読み解いて改修する部分はありますが、テンプレートが用意されているのでそれほど手間はかからないと思います。次回はCognitive Searchの設定部分について解説予定です。

https://qiita.com/tmiyata25/items/861f1691b5ca56706242