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
の中にはalgorithms
とvectorizers
、profiles
を定義する。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"
}
}
}
ここが一番重要。以下のスキルセットを実行している。
- Microsoft.Skills.Vision.OcrSkill (PDFファイルからテキストを抽出)
- Microsoft.Skills.Text.SplitSkill (テキストをチャンク分割)
- Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill (各チャンクをベクトル化)
OCRスキルで PDF ファイルからテキストを抽出している。ここはドキュメント抽出スキルでも良いのかもしれない(今度試してみる)
チャンク分割スキルでは分割対象のテキストを指定すると、指定した文字数&重複文字数でチャンク分割してくれる。defaultLanguageCode
が重要で、ja
(日本語)に指定しておくと、句読点とかで良い感じに分割してくれる。textSplitMode
はpages
かsentences
のどちらかを指定することができる。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検索ドキュメントに格納されるのではなく、複数の検索ドキュメントに分かれて格納されることだと思われる。新登場のインデックスプロジェクションはこの問題を解決することができる。
インデックスプロジェクションでは主にフィールドマッピングを行う。ここでフィールドマッピングをしておくと、インデクサーでマッピングを行う必要がなくなる。なぜか対象インデックスも指定しないといけない(イマイチな仕様)。一方で、対象インデックスとマッピングの定義先である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