Azure Cognitive Search のカスタムスキルを作成しよう


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

# 目次
1. [カスタムスキルについて](#カスタムスキルについて)
1. [カスタムスキル作成の流れ](#カスタムスキル作成の流れ)
    1. [スキルの設計](#1-スキルの設計)
    1. [カスタムスキルの作成](#2-カスタムスキルの作成)
        1. [インデクサー内で行われる処理の解説](#21-インデクサー内で行われる処理の解説)
        1. [カスタムスキルの作成詳解](#22-カスタムスキルの作成詳解)
    1. [フィールドマッピングの作成](#3-フィールドマッピングの作成)
    1. [スキルセット定義への追加](#4-スキルセット定義への追加)
1. [Azure Search Power Skills の紹介](#azure-search-power-skills-の紹介)
1. [Form Recognizer を使って帳票を構造化するアーキテクチャ](#form-recognizer-を使って帳票を構造化するアーキテクチャ)
1. [デバッグセッションを使ってカスタムスキルを作成](#デバッグセッションを使ってカスタムスキルを作成)


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

![001.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/a99794dd-84b5-6749-d55a-2545ed3d80d0.jpeg)

Azure Cognitive Search の処理全体像におけるカスタムスキルの位置付けは上の図のようになります。Azure Cognitive Search にはもともと色々な AI 処理ができる組み込みの [Cognitive Skill](https://docs.microsoft.com/azure/search/cognitive-search-predefined-skills) が搭載されていてすぐに使うことができますが、カスタムスキルはこの場所に自分自身が開発したスキルを追加していくことができます。

# カスタムスキル作成の流れ

では、カスタムスキルの作成の流れを簡単にご紹介しましょう。
![002.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/d9ddcfe1-af88-4be6-50b1-6df9becadfa1.jpeg)

## 1. スキルの設計

スキルの設計では、どのような機能をスキルセットに追加するかを検討します。インデクサーからのデータはスキルパイプラインを通して、上から下に流れていきますので、各スキルでは、データの IN/OUT も考慮しておく必要があります。

カスタムスキルの開発は、Postman 等を使っての REST API 経由での開発や、Azure Portal 上からの GUI 開発、Az PowerShell を使っての開発などご自身のスキルセットに合わせて選択することができます。最初は GUI で開発を体験し、慣れてくればインデックスやインデクサー、スキルセット設定を JSON 定義ファイルのかたちで持っておいて、必要な時に Postman を使って登録・編集するというスタイルが良いかと思います。また一部、インデックスの細かい定義（アナライザー、トークナイザー、トークンフィルター）や、同義語 SynonymMaps ファイルの登録は REST API 経由でしかできなかったりします。

## 2. カスタムスキルの作成

カスタムスキルは、JSON 形式の定義ファイルと、処理をするプログラムの 2 つから構成されています。
カスタムスキルの JSON 定義には、コンテキストとインプット、アウトプットを記述する必要があります。

### 2.1. インデクサー内で行われる処理の解説

ここで、カスタムスキルを作成するときに必ず理解しておく必要がある考え方をご説明いたします。

![003.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/4b5cefb4-ecdb-9acd-4456-47b438594cb5.jpeg)

上図では、Azure Cognitive Search のインデクサーがファイルをスキャンした際に内部で行っている処理を示しています。インデクサーによる仮想的構造の構築と、スキルセット（複数のスキルをまとめたもの）によるエンリッチメント結果の格納を示しています。データは左から右に流れています。

1. たとえば図のようなワードファイルのような非構造ファイルをインデクサーがスキャンします
2. 次に、図のような基本構造に従って、メモリー内に一時的な構造化情報が作成されます。この一時的な構造化情報を**エンリッチ処理されたデータ構造**とも呼びます。
 * `/document` はファイル全体、一番上の階層
 * `/content` は本文の階層
     * 本文の中には、各スキルが抽出したタグデータが指定の場所に格納される。
     * 例えば、図のような本文から、エンティティ抽出スキルを使って、Microsoft と LinkedIn という組織エンティティを取り出して、`/organization` に格納します。
     * 言語検出スキルは、ファイル全体に対して言語を推定して、`/language` に格納します。

    イメージ的には、インデクサーが仮想的な木（ドキュメント）を 1 本立てて、その枝葉（階層）ごとに、スキルが花や実で飾っていく（エンリッチメント）感じでしょうか。収穫の際には、対応するカゴに必要な果実を入れていくという感じです。

3. このままでは一時的な情報でしかないので、これらを永続的なインデックスのフィールドにマッピングします。このマッピング設定は、これまで JSON 形式で記述して、インデクサー定義の中で追加する必要がありましたが、新たに追加されたデバッグセッション機能の中の AI エンリッチメントの GUI 上からも紐づけできるようになりました。

    カスタムスキルを追加したときや、後からスキルを追加したときは、一時的な構造と、検索インデックスのフィールドとのマッピング設定が必ず必要になります。ここでは、一時的な情報を**ソースフィールド**、検索インデックスフィールドを**ターゲットフィールド**と呼んでいます。

4. このマッピング設定が正しくできていれば、インデクサー実行時にスキルで抽出された情報がしっかりインデックスに格納されるようになります。この紐付けが最も失敗しやすい部分ですので、サンプル等を実行しながら、構造について理解するようにしてください。

### 2.2. カスタムスキルの作成詳解

では実際にカスタムスキルを作成してみましょう。ここでは、[エンティティ抽出スキル](https://docs.microsoft.com/azure/search/cognitive-search-skill-entity-recognition)で取り出した組織エンティティの値を、[Bing Entity Search API](https://www.microsoft.com/bing/apis/bing-entity-search-api) に投げて、検索結果の説明文を取得する　というカスタムスキルを例にします。

![004.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/e28e28b9-e9b6-b04d-24ae-bf81b94cc545.jpeg)

カスタムスキル定義の中身は JSON 形式で、定義の情報、スキルがホスティングされている Web API エンドポイント URL と、**コンテキスト**、**インプット**、**アウトプット**という構造になっています。

1. **コンテキスト**はスキルが実行される回数やアウトプットが追加される階層の位置を定義しています。末尾に `/*` を追加すると、コレクション内のインスタンスごとにスキルが 1 回呼び出されます。組織エンティティの値はコレクション型つまり配列で入っています。この配列のアイテムごとに、カスタムスキルを呼び出して機能を実行したい場合、パスの末尾に `/*` 追加する必要があります。
この例では、Microsoft で 1 回スキルを実行して、対応する値の下に、検索結果の新たな項目を追加。次に、LinkedIn でも同様に検索結果の新たな項目を追加、というように処理が実行されます。

2. **インプット**には、**ソースフィールド**のパスを指定します。これは Web API のインプットパラメータとなります。`name` に指定した値が、Web API 側の JSON のキー名となります。
3. **アウトプット**には、ターゲット名のみを指定します。パスは記述しません。コンテキストに指定したパスが使われます。
     - `name` に指定した値が**エンリッチ処理されたデータ構造**に項目として追加されます。※インデックス側のターゲットフィールド名ではなく、一時的な構造のほうの**ソースフィールド名**ですので注意してください。
     - `targetName` に指定した値は、Web API 側の JSON の出力のキー名と一致している必要があります。

ちなみに、Web API 側には、以下のような 定型 JSON がインプットとして送られてきますので、これをパースして処理に使います。

```json
{
    "values": [
        {
            "recordId": "r1",
            "data":
            {
            	"query": "Microsoft"
            }
        }
    ]
}
```

そして、Web API 側からのアウトプットは、以下のような形式で返却する必要があります。

```json
{
    "values": [
        {
            "recordId": "r1",
            "data": {
                "companyDescription": "Microsoft は、米国ワシントン州レドモンドに本社を置く、多国籍のテクノ..."
            },
            "errors": [],
            "warnings": []
        }
    ]
}
```

作成したカスタムスキル定義をスキルセット定義に追加して、インデクサーを実行すると図の右部のように、組織エンティティの各値ごとに、Bing Entity Search API で検索してきた結果を追加することができます。

実際に Bing Entity Search サービスを呼び出すコードについては、[コチラ](https://docs.microsoft.com/azure/search/cognitive-search-create-custom-skill-example)を参照ください。

これを一から作ろうとすると失敗する確率が高いので、こちらの [Docs](https://docs.microsoft.com/ja-jp/azure/search/cognitive-search-defining-skillset) のチュートリアルやこの後説明するサンプルの形式を参考に、少しずつ変えながら試してみてください。

## 3. フィールドマッピングの作成

セクション 2.1 でも説明しましたが、カスタムスキルを作成した後は、必ずフィールドマッピングを作成しなければなりません。これは、インデクサーが仮想的に構築した構造化情報と、実際の検索インデックスのフィールドを紐づける重要な作業です。

フィールドマッピング設定は、インデクサー定義の中に存在します。

```json
 "outputFieldMappings": [
    {
      "sourceFieldName": "/document/content/people",
      "targetFieldName": "people"
    },
    {
      "sourceFieldName": "/document/content/organizations",
      "targetFieldName": "organizations"
    },
    {
      "sourceFieldName": "/document/content/locations",
      "targetFieldName": "locations"
    },
    {
      "sourceFieldName": "/document/content/keyphrases",
      "targetFieldName": "keyphrases"
    },
    {
      "sourceFieldName": "/document/language",
      "targetFieldName": "language"
    }
  ],
```

これを手作業で編集してもいいですし、以下のように新機能のデバッグセッションの中で、スキルグラフの最後の`出力フィールドのマッピング`ボックスをクリックすると、右ペインにて、ソースフィールドとターゲットフィールドの紐付けをすることができるようになりました。

![005.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/fff82e91-6ee9-af91-9c40-5b1ce569b56b.jpeg)

デバッグセッションにて、正常に動作することを確認した後、`変更をコミットする` ボタンでインデクサーに直接変更を適用できるのでとても便利です。

## 4. スキルセット定義への追加

スキルセットは、各スキルの定義とその実行順を JSON 形式で定義したものです。この実行順は複雑なため、Azure Portal のスキルセットタブからは内容を編集することができず、JSON の直接編集によって定義する必要がありました。

```json
{
  "@odata.context": "https://{{search_service}}.search.windows.net/$metadata#skillsets/$entity",
  "name": "azureblob-skillset-default",
  "description": "Skillset created from the portal.",
  "skills": [
    {
      "@odata.type": "#Microsoft.Skills.Text.EntityRecognitionSkill",
      "name": "#1",
    ...
    },
    {
      "@odata.type": "#Microsoft.Skills.Text.KeyPhraseExtractionSkill",
      "name": "#2",
    ...
    },

    ...
   ]
}
```

スキルセットの定義は、これまでは JSON とにらめっこしながら実行順や IN/OUT のつながりを編集していたのですが、デバッグセッション機能によって、テストファイルを指定して、そのファイルにスキルがどのような順で実行されているのかをライブで確認しながら、編集もできるようになりました。私はこの機能にとても感動しています。なぜこれまでなかったんだ　と。

![007.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/ae59b45d-953f-33b6-f334-5f13c902b6f9.jpeg)

新機能のデバッグセッションでは、左ペインに`スキルグラフ`という、スキルの処理順を可視化したダイアグラムが追加されました。これによって、画面を見ながら、各スキルの実行順を決めたり、同時に右ペインでスキルの定義を行うことができるようになりました。ここで編集してから、ツールバーの実行ボタンを押せば、デバッグセッション内だけで、変更をテストできます。Cool...😎

![008.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/cef56405-0ddf-112d-49a2-a5a624eb92f5.jpeg)

デバッグ実行後は、`エンリッチ処理されたデータ構造`タブで、各スキルによる実行結果が正しく格納されているかを確認できます。
これはセクション 2.1 で説明したような、**一時的な構造化情報**の中身です。これまではこのデータを見ることができず、毎回インデクサーを回してテストする必要がありました。

右側の`エラー/警告`タブでどのような問題が起きているか確認しながら、値が入っていなかったり、違う階層に値が入ってしまっているような場合は、右側のスキルの`実行`タブで、インプットとアウトプットパスの記述について実際に値を確認しながら評価できます。

デバッグセッションの詳しい使い方は[こちら](https://qiita.com/nohanaga/items/26355ffba58ba4c71c6e)の記事にて解説したいと思います。

# Azure Search Power Skills の紹介

[Azure Search パワースキル](https://github.com/Azure-Samples/azure-search-power-skills)は、Azure Cognitive Search のカスタムスキルとしてデプロイ可能な便利なサンプル集です。パワースキルは、独自のカスタムスキルのテンプレートまたは出発点として使用できます。または、要件を満たしている場合は、そのままデプロイして使用することもできます。また、プルリクを送信して、これらのコードに contribute することをお勧めします。ライセンスは MIT ライセンスです。

| スキル | 説明 | タイプ |
|:----:|:-------------|:-------------|
| [GeoPointFromName](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Geo/GeoPointFromName/README.md) | 地名や住所から座標を返す | Geography
| [AcronymLinker](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Text/AcronymLinker/README.md) | 既知の略語の定義を返す | Text
| [BingEntitySearch](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Text/CustomEntitySearch) | 有名人、場所、または組織に関する豊富で構造化された情報を検索 | Text
| [Distinct](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Text/Distinct/README.md) | 用語のリストの重複を削除する | Text
| [Tokenizer](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Text/Tokenizer/README.md) | テキストから非ストップワードを抽出する | Text
| [Summarizer](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Text/TextSummarization/README.md) | HuggingFace/FaceBook BART モデルを使ってテキストを要約する BART-Large-CNN | Text
| [AbbyyOCR](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Text/AbbyyOCR/README.md) | 外部 ABBYY Cloud OCR を使用して、画像からテキストを抽出する OCR | Vision
| [AnalyzeForm](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Vision/AnalyzeFormV2) | ドキュメントからフォームフィールド（Key-Value ペア）を抽出するためのカスタム Azure Form Recognizer モデルのトレーニングと展開 | Vision
| [AnalyzeInvoice](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Vision/AnalyzeFormV2) | 事前に学習された Azure Form Recognizer モデルを使用して、ドキュメントから Key-Value ペアとして請求書フィールドを抽出 | Vision
| [ExtractTables](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Vision/AnalyzeFormV2) | 事前に学習された Azure Form Recognizer モデルを使って、文書から表を抽出 | Vision
| [CustomVision](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Vision/CustomVision/README.md) | Custom Vision モデルによるドキュメントの分類 | Vision
| [HocrGenerator](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Vision/HocrGenerator/README.md) | OCR の結果を hOCR 形式に変換 | Vision
| [ImageClustering](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Vision/ImageClusteringSkill/README.md) | クラスタリングを利用して画像を自動的にグループ化し、ラベル付けする | Vision
| [ImageSimilarity](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Vision/ImageSimilarity/README.md) | ResNet を使って、上位 n 位までの類似画像を見つける | Vision
| [P&ID Parser](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Vision/PID/README.md) | 配管図や計装図から機器タグやテキストブロックを抽出 | Vision
| [DecryptBlobFile](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Utils/DecryptBlobFile/README.md) | 暗号化された状態で Azure Blob Storage に保存されていたファイルをダウンロード、復号化して返す | Utility
| [GetFileExtension](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Utils/GetFileExtension/README.md) | ファイル名と拡張子を別々の値として返すので、ドキュメントの種類でフィルタリングすることができる | Utility
| [ImageStore](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Vision/ImageStore/README.md) | base64 エンコードされた画像を Blob ストレージ に保存したり、Blob ストレージから取得する。ナレッジストアは、画像をストレージに保存するパターンをよりクリーンに実装したもの。 | Utility
| [HelloWorld](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Template/HelloWorld/README.md) | 自分のスキルの出発点やテンプレートとして利用できる最低限のスキル | Template
| [PythonFastAPI](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Template/PythonFastAPI/README.md) | Python Power Skill のための商用 Web サーバーと API の出発点となるもの | Template

このリストを見るだけでも、カスタムスキルの潜在的なパワーを感じていただけるのではないでしょうか。
地名や住所を座標に変換すれば、Cognitive Search の[地理空間検索](https://docs.microsoft.com/azure/search/search-query-odata-geo-spatial-functions)を利用できるようになりますし、Bing API と連携してより幅広い情報を取得したり、テキスト要約モデルとも組み合わせてもいいかもしれません。

画像認識も幅広いです。特定の文字に特化した外部の OCR へ飛ばしてもいいですし、[Form Recognizer](https://azure.microsoft.com/services/cognitive-services/form-recognizer/) を使って、スキャンされた帳票の中身を構造化するというのもニーズが高いと思います。ご自分の画像認識モデルや画像処理プログラムをビルトインしていただければと思います。

簡単にカスタムスキルを開発するための [Hello World](https://github.com/Azure-Samples/azure-search-power-skills/blob/main/Template/HelloWorld/README.md) ももちろん準備してありますので、開発の出発点としてご利用ください。

# Form Recognizer を使って帳票を構造化するアーキテクチャ

[Form Recognizer](https://azure.microsoft.com/services/cognitive-services/form-recognizer/) と OCR を活用してスキャンした帳票の中から Key-Value の構造を取り出し、それを全文検索できるようにするソリューションのアーキテクチャを紹介します。

![009.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/afdeb42c-e6c9-714e-9f5c-c1d275fa4aff.jpeg)

今回作成したカスタムスキルは、Python コードで記述されており、Azure Functions 上に Web API エンドポイントとしてデプロイしています。コードは Azure Blob Storage から PDF データを取得し、データを Form Recognizer API に投入します。Blob Storage にアクセスするための、SAS トークンやストレージパスはインデクサーからインプットデータとして渡しています。

Form Recognizer では事前に作成しておいたモデルを使って、帳票から項目の Key-Value を抽出し、JSON で返却します。
それを受け取った Python コードは、結果をカスタムスキルの I/F を通して、インデックスフィールドに格納します。

実際に動画にて本デモを収録しておりますので、後ほど Youtube の [Microsoft Partner Network Japan](https://www.youtube.com/c/MicrosoftPartnerNetworkJapan/videos) にアップしたいと思います。

# デバッグセッションを使ってカスタムスキルを作成
記事の中で触れたデバッグセッション機能を使ってカスタムスキルを作成するハンズオンは[こちら](https://qiita.com/nohanaga/items/26355ffba58ba4c71c6e)の記事で紹介しています。