LoginSignup
12
10

Azure AI Search のスキルセットで PDF ファイルを読み込んでチャンク分割してベクトル化する

Last updated at Posted at 2024-02-10

2023/11に追加された以下の機能で Azure AI Search のスキルセットにてチャンク分割とベクトル化ができるようになりました (ただ、2024/02/09時点ではパブリックプレビュー段階なので注意)

よくある Azure AI Search の利用ストーリーである、Azure Blob Storage に格納されている PDF ファイルを読み込んで、チャンク分割して Azure OpenAI Service の Embeddings API でベクトル化して Azure AI Search のインデックスに格納する、を実現するためのインデックス、インデクサー、データソース、スキルセットの定義例(JSON)をメモしておきます。

以下の定義で使っているインデックスプロジェクション等の機能は 2023-10-01-Preview の API を使用しないと使えないのに注意

インデックスの定義例

{
    "name": "",
    "fields": [
        {
            "name": "id",
            "type": "Edm.String",
            "key": true,
            "searchable": true,
            "filterable": false,
            "sortable": false,
            "facetable": false,
            "analyzer": "keyword"
        },
        {
            "name": "parentKey",
            "type": "Edm.String",
            "searchable": false,
            "filterable": true,
            "sortable": false,
            "facetable": false
        },
        {
            "name": "metadata_storage_path",
            "type": "Edm.String",
            "searchable": false,
            "filterable": true,
            "sortable": false,
            "facetable": false
        },
        {
            "name": "metadata_storage_name",
            "type": "Edm.String",
            "searchable": false,
            "filterable": true,
            "sortable": false,
            "facetable": false
        },
        {
            "name": "page",
            "type": "Edm.Int32",
            "searchable": false,
            "filterable": false,
            "sortable": true,
            "facetable": false
        },
        {
            "name": "text",
            "type": "Edm.String",
            "searchable": true,
            "filterable": false,
            "sortable": false,
            "facetable": false,
            "analyzer": "ja.microsoft"
        },
        {
            "name": "textVector",
            "type": "Collection(Edm.Single)",
            "searchable": true,
            "dimensions": 3072,
            "vectorSearchProfile": "vectorProfile"
        }
    ],
    "vectorSearch": {
        "algorithms": [
            {
                "name": "hnsw",
                "kind": "hnsw",
                "hnswParameters": {
                    "m": 4,
                    "efConstruction": 400,
                    "efSearch": 500,
                    "metric": "cosine"
                }
            }
        ],
        "vectorizers": [
            {
                "name": "azureOpenAI",
                "kind": "azureOpenAI",
                "azureOpenAIParameters": {
                    "resourceUri": "",
                    "deploymentId": "text-embedding-3-large",
                    "apiKey": ""
                }
            }
        ],
        "profiles": [
            {
                "name": "vectorProfile",
                "algorithm": "hnsw",
                "vectorizer": "azureOpenAI"
            }
        ]
    },
    "similarity": {
        "@odata.type": "#Microsoft.Azure.Search.BM25Similarity",
        "k1": 1.2,
        "b": 0.75
    }
}

ベクトル検索可能とするために、ベクトルを格納するためのフィールドであるtextVectorフィールドを用意しておく。type は Collection(Edm.Single) で dimensions にはベクトルの次元数を指定する(Azure OpenAI Service の text-embedding-3-large の次元数は 3072)

加えて、テキストでやってきた検索クエリをベクトル化して検索できるようにするためにvectorSearchを定義しておく。vectorSearchの中にはalgorithmsvectorizersprofilesを定義する。algorithmsではベクトル検索に使用するアルゴリズムやそのパラメータを定義する(参考)。vectorizersではベクトル化に使用するモデルを指定する。profilesはアルゴリズムとベクトライザーの組み合わせで、ここで定義したプロファイルを、ベクトルフィールド(ここでのtextVector)でのvectorSearchProfileで指定する。

インデックスプロジェクションに対応するために、key フィールド(今回だとid)はanalyzerとしてkeywordを使用するように設定する必要があり、また親検索ドキュメントのキーを格納するためのフィールド(今回だとparentKey)を用意しておく必要がある(どちらも名前は任意)。

vectorizersでは Azure OpenAI Service のモデルだけでなく、他のモデルも指定することができるが、それは Web API 経由でアクセスできる必要があるので、Azure AI Search 自体にモデルを配置してベクトル化処理をして、みたいなことではない。また、ここでのベクトル化の設定は、テキストでやってきた検索クエリーをベクトル化してベクトル検索できるようにするためであって、格納されたテキストをベクトル化するのはスキルセットの役目である。

参考:Indexes - Create - REST API (Azure Search Service) | Microsoft Learn

インデクサーの定義例

{
    "name": "",
    "dataSourceName": "",
    "skillsetName": "",
    "targetIndexName": "",
    "parameters": {
        "configuration": {
            "dataToExtract": "contentAndMetadata",
            "parsingMode": "default",
            "imageAction": "generateNormalizedImagePerPage",
            "allowSkillsetToReadFileData": true
        }
    }
}

特筆すべき点はないです。よくあるPDFファイルを処理するためのインデクサーの定義って感じ。

参考:Indexers - Create - REST API (Azure Search Service) | Microsoft Learn

データソースの定義例

{
    "name": "",
    "type": "azureblob",
    "name": "",
    "credentials": {
        "connectionString": ""
    },
    "container": {
        "name": ""
    },
    "dataDeletionDetectionPolicy": {
        "@odata.type": "#Microsoft.Azure.Search.SoftDeleteColumnDeletionDetectionPolicy",
        "softDeleteColumnName": "IsDeleted",
        "softDeleteMarkerValue": "true"
    }
}

これも特筆すべき点はないです。connectionStringには、キー認証を使うのであれば使用する Azure Blob Storage の接続文字列を、マネージドID認証を使うのであれば Storage のリソースIDを指定する。マネージドID認証を行う場合は、使用する Azure AI Search アカウントのマネージドID設定をオンにして、そのマネージドIDに使用する Azure Blob Storage へのストレージ BLOB データ閲覧者ストレージ BLOB データ共同作成者を付けておくこと。一応、データ削除検出を行うようにしている。

スキルセットの定義例

{
    "name": "",
    "skills": [
        {
            "@odata.type": "#Microsoft.Skills.Vision.OcrSkill",
            "context": "/document/normalized_images/*",
            "defaultLanguageCode": "ja",
            "detectOrientation": true,
            "inputs": [
                {
                    "name": "image",
                    "source": "/document/normalized_images/*"
                }
            ],
            "outputs": [
                {
                    "name": "text",
                    "targetName": "text"
                },
                {
                    "name": "layoutText",
                    "targetName": "layoutText"
                }
            ]
        },
        {
            "@odata.type": "#Microsoft.Skills.Text.SplitSkill",
            "context": "/document/normalized_images/*",
            "textSplitMode": "pages",
            "maximumPageLength": 1000,
            "pageOverlapLength": 250,
            "defaultLanguageCode": "ja",
            "inputs": [
                {
                    "name": "text",
                    "source": "/document/normalized_images/*/text"
                }
            ],
            "outputs": [
                {
                    "name": "textItems",
                    "targetName": "chunks"
                }
            ]
        },
        {
            "@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
            "context": "/document/normalized_images/*/chunks/*",
            "resourceUri": "",
            "deploymentId": "text-embedding-3-large",
            "apiKey": "",
            "inputs": [
                {
                    "name": "text",
                    "source": "/document/normalized_images/*/chunks/*"
                }
            ],
            "outputs": [
                {
                    "name": "embedding",
                    "targetName": "vector"
                }
            ]
        }
    ],
    "indexProjections": {
        "selectors": [
            {
                "targetIndexName": "",
                "parentKeyFieldName": "parentKey",
                "sourceContext": "/document/normalized_images/*/chunks/*",
                "mappings": [
                    {
                        "name": "page",
                        "source": "/document/normalized_images/*/pageNumber"
                    },
                    {
                        "name": "text",
                        "source": "/document/normalized_images/*/chunks/*"
                    },
                    {
                        "name": "textVector",
                        "source": "/document/normalized_images/*/chunks/*/vector"
                    },
                    {
                        "name": "metadata_storage_path",
                        "source": "/document/metadata_storage_path"
                    },
                    {
                        "name": "metadata_storage_name",
                        "source": "/document/metadata_storage_name"
                    }
                ]
            }
        ],
        "parameters": {
            "projectionMode": "skipIndexingParentDocuments"
        }
    }
}

ここが一番重要。以下のスキルセットを実行している。

  1. Microsoft.Skills.Vision.OcrSkill (PDFファイルからテキストを抽出)
  2. Microsoft.Skills.Text.SplitSkill (テキストをチャンク分割)
  3. Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill (各チャンクをベクトル化)

OCRスキルで PDF ファイルからテキストを抽出している。ここはドキュメント抽出スキルでも良いのかもしれない(今度試してみる)

チャンク分割スキルでは分割対象のテキストを指定すると、指定した文字数&重複文字数でチャンク分割してくれる。defaultLanguageCodeが重要で、ja(日本語)に指定しておくと、句読点とかで良い感じに分割してくれる。textSplitModepagessentencesのどちらかを指定することができる。sentencesを指定するとやたら短く分割されるようになるので、pagesを指定した方が良い気がする。重要なのは、ちゃんとcontextを指定することで、このスキルの前段階で実行されるOCRスキルでは、ページごとの抽出結果が文字列の配列で出力されるので、このcontextで配列内の文字列ごとにチャンク分割処理をすることを明示しないとエラーになる (指定しないと文字列配列がチャンク分割スキルに渡されるので)。

難解で仕方がない context の仕様を理解するためには以下のドキュメントを読むと良いかもしれない
スキル コンテキストと入力注釈の参照言語 - Azure AI Search | Microsoft Learn

ベクトル化スキルではチャンク分割したテキストをベクトル化している。ここでもcontextをちゃんと設定して配列内のテキストごとに処理するようにさせる。resourceUriには使用する Azure OpenAI Service のエンドポイント(例:https://xxx.openai.azure.com)を、apiKeyにはその認証キーを指定する。マネージドID認証もできるはず。

特に重要なのが、インデックスプロジェクションの設定。indexProjections以下で設定することになる。なぜ重要かというと、Azure AI Search で Azure Blob Storage のデータをインデクサー経由で JSON, JSONL, CSV 以外のファイル取り込んだ場合、1ファイル1検索ドキュメントとしてインデックスに登録される、という仕様になっているため。この場合、たとえ 100 ページ以上の PDF ファイルであっても1検索ドキュメントとして登録されてしまう(OCRスキルを使用した場合はページごとにテキスト抽出がされるので、文字列の配列が対象のインデックスのフィールドに格納されることになる)。普通に考えて文字列の配列が1検索ドキュメントに格納されるのではなく、複数の検索ドキュメントに分かれて格納されることだと思われる。新登場のインデックスプロジェクションはこの問題を解決することができる。

参考:インデックス プロジェクションの概念 - Azure AI Search | Microsoft Learn

インデックスプロジェクションでは主にフィールドマッピングを行う。ここでフィールドマッピングをしておくと、インデクサーでマッピングを行う必要がなくなる。なぜか対象インデックスも指定しないといけない(イマイチな仕様)。一方で、対象インデックスとマッピングの定義先であるselectorsは配列であるため、理論上、複数のインデックスに格納するといったことができると思われる(今度試してみる)。parentKeyFieldNameには格納先のインデックスで用意しておいた親ドキュメントのキーを格納するフィールド名(今回だとparentKey)を指定する。parameters.projectionModeでは親ドキュメントもインデックスに格納するかどうかを指定できる。今回は親ドキュメントを作成しない(skipIndexingParentDocuments)にしておいているが、親ドキュメントを作成しておくと兄弟ドキュメントが検索しやすくなる、みないなことができるのでは?と推測する。

参考:
Skillsets - Create - REST API (Azure Search Service) | Microsoft Learn

JSONファイルからインデクサー等を作成する方法

Azure Portal で作成するか、REST API で作成する。
Pythonで作成する簡単な例は以下の通り。

import requests

search_name = ""
api_key = ""
api_version = "2023-10-01-Preview"
headers = {"Content-Type": "application/json", "api-key": api_key}

with open("index.json", "r") as f:
    data = f.read()

url = f"https://{search_name}.search.windows.net/indexes?api-version={api_version}"
requests.post(url, data=data, headers=headers)

curl で作成する簡単な例は以下の通り。

curl -X PUT https://$cognitiveSearchName.search.windows.net/indexes/$cognitiveSearchIndexName?api-version=2023-10-01-Preview \
    -H 'Content-Type: application/json' \
    -H 'api-key: '$cognitiveSearchApiKey \
    -d @index.json
12
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10