Azure AI SearchでAzure Document Intelligenceを使ってファイルをマークダウンフォーマット化した後に、画像切り出し、言語化をしてインデックス化しました。
以下がサポートされている要素です。表はHTMLタグで出てくるし、箇条書きのマークダウン化などはされないので注意。また、PDFの見出しがそのままHeadingとして使われるわけでないようなので注意。
完成時のパイプライン
デバッガセッション使うとこんなパイプラインに可視化できます。
前提
REST Client 0.25.1 をVS Codeから使って実行しています。
REST Client 設定で Decode Escaped Unicode Characters を ON にするとHTTP Response Bodyの日本語がデコードされます。
また、以下の記事の1~5までのStepも前提作業です。
Steps
1. インデックス作成
試行錯誤目的で各ステップに削除も末尾に乗せています。
1.0. 固定値定義
固定値を定義しておきます。
## Azure AI Searchのエンドポイント
@endpoint = https://<ai searchresource name>.search.windows.net
## インデックス名
@index_name=test-index-2
## スキルセット名
@skillset_name=test-skillset-2
## データソース名
@datasource_name=test-datasource-2
## インデクサー名
@indexer_name=test-indexer-2
## Azure AI SearchのAPI Key
@admin_key=<ai search key>
## Azure AI SearchのAPI Version
@api_version=2025-09-01
## AOAIのリソース名
@aoai_resourceUri=https://<AOAI resource name>.openai.azure.com
## AOAIのAPI Key
@aoai_apiKey=<key>
## Embeddingモデルのデプロイメント名
@aoai_embedding_deploymentId=text-embedding-3-small
## Embeddingモデルのモデル名
@aoai_embedding_modelName=text-embedding-3-small
## Embeddingモデルの次元数
@aoai_embedding_dimension=1536
## Blob Storageのコンテナ名
@blob_container_name=rag-doc-test
## Blob Storageの接続文字列
@blob_connectionString=<connection string>
# Chat Completion の情報(画像のテキスト化)。デプロイ名も変更必要。
@chatCompletionResourceUri = https://<aoai resource name>.openai.azure.com/openai/deployments/<deploy>/chat/completions?api-version=2025-01-01-preview
# Chat Completion のAPI Key
@chatCompletionKey = <key>
## Blob Storageのコンテナ名(画像格納)
@imageProjectionContainer=images
## AI ServiceのAPI Key
@ai_service_key=<key>
## AI Serviceのendポイント
@ai_service_endpoint=https://<resource name>/cognitiveservices.azure.com
Tokenで認証する場合は、変数@aad_tokenを上記と一緒に定義し、ターミナルで別途以下のコマンドでTokenを取得し変数に値を設定しておきます。
az account get-access-token --resource https://search.azure.com --query accessToken -o tsv
VS Code REST Client では $aadV2Token を使うと AAD トークンを自動取得できるらしいですが、私の環境ではVS Code 側の Microsoft 認証プロバイダが別テナントのアカウントを掴み、AADSTS1001010失敗。
後述のHTTP Requestの記述でapi-key のエントリを消して、Authorization: Bearer でTokenを追加して認証変更
PUT {{search_endpoint}}/skillsets/{{skillset_name}}?api-version={{skillset_api_version}}
Authorization: Bearer {{aad_token}}
Content-Type: application/json
### api-key: {{admin_key}}
1.1. データ ソースを作成
ADLS Gen2を使っています。
### データソース更新
PUT {{endpoint}}/datasources('{{datasource_name}}')?api-version={{api_version}}
Content-Type: application/json
api-key: {{admin_key}}
{
"name": "{{datasource_name}}",
"description": null,
"type": "adlsgen2",
"subtype": null,
"credentials": {
"connectionString": "{{blob_connectionString}};"
},
"container": {
"name": "{{blob_container_name}}",
"query": null
},
"dataChangeDetectionPolicy": null,
"dataDeletionDetectionPolicy": null,
"encryptionKey": null
}
### データソース削除
DELETE {{endpoint}}/datasources/{{datasource_name}}?api-version={{api_version}}
Content-Type: application/json
api-key: {{admin_key}}
Managed ID認証をする場合は、credentialsを以下に置換し、IDにStorage Blob Data Contributorのロールを割当。
"credentials": { "connectionString": "ResourceId={{storage_resource_id}};" },
{{storage_resource_id}}の値は以下の値
/subscriptions/[subscription id]/resourceGroups/[resource group name ]/providers/Microsoft.Storage/storageAccounts/[storage account name]
1.2. インデックスを作成
h3までを入れる項目を作っています。日本語項目のAnalyzerはja.luceneにしています。
ja.microsoftでもいいかと思います。
### インデックス作成
PUT {{endpoint}}/indexes('{{index_name}}')?api-version={{api_version}}
Content-Type: application/json
api-key: {{admin_key}}
{
"name": "{{index_name}}",
"fields": [
{
"name": "chunk_id",
"type": "Edm.String",
"key": true,
"retrievable": true,
"stored": true,
"searchable": true,
"filterable": false,
"sortable": true,
"facetable": false,
"analyzer": "keyword",
"synonymMaps": []
},
{
"name": "parent_id",
"type": "Edm.String",
"key": false,
"retrievable": true,
"stored": true,
"searchable": false,
"filterable": true,
"sortable": false,
"facetable": false,
"synonymMaps": []
},
{
"name": "content_text",
"type": "Edm.String",
"key": false,
"retrievable": true,
"stored": true,
"searchable": true,
"analyzer": "ja.lucene",
"filterable": false,
"sortable": false,
"facetable": false,
"synonymMaps": []
},
{
"name": "title",
"type": "Edm.String",
"key": false,
"retrievable": true,
"stored": true,
"searchable": true,
"analyzer": "ja.lucene",
"filterable": true,
"sortable": true,
"facetable": false,
"synonymMaps": []
},
{
"name": "content_embedding",
"type": "Collection(Edm.Single)",
"key": false,
"retrievable": false,
"stored": false,
"searchable": true,
"filterable": false,
"sortable": false,
"facetable": false,
"synonymMaps": [],
"dimensions": {{aoai_embedding_dimension}},
"vectorSearchProfile": "azureOpenAi-text-profile"
},
{
"name": "header_1",
"type": "Edm.String",
"searchable": true,
"analyzer": "ja.lucene",
"filterable": false,
"retrievable": true,
"stored": true,
"sortable": false,
"facetable": false,
"key": false
},
{
"name": "header_2",
"type": "Edm.String",
"searchable": true,
"analyzer": "ja.lucene",
"filterable": false,
"retrievable": true,
"stored": true,
"sortable": false,
"facetable": false,
"key": false
},
{
"name": "header_3",
"type": "Edm.String",
"searchable": true,
"analyzer": "ja.lucene",
"filterable": false,
"retrievable": true,
"stored": true,
"sortable": false,
"facetable": false,
"key": false
},
{
"name": "image_document_id",
"type": "Edm.String",
"filterable": true,
"retrievable": true
},
{
"name": "content_path",
"type": "Edm.String",
"searchable": false,
"retrievable": true
},
{
"name": "locationMetadata",
"type": "Edm.ComplexType",
"fields": [
{
"name": "pageNumber",
"type": "Edm.Int32",
"searchable": false,
"retrievable": true
},
{
"name": "boundingPolygons",
"type": "Edm.String",
"searchable": false,
"retrievable": true,
"filterable": false,
"sortable": false,
"facetable": false
}
]
}
],
"scoringProfiles": [],
"suggesters": [],
"analyzers": [],
"tokenizers": [],
"tokenFilters": [],
"charFilters": [],
"normalizers": [],
"similarity": {
"@odata.type": "#Microsoft.Azure.Search.BM25Similarity"
},
"semantic": {
"defaultConfiguration": "semantic-configuration",
"configurations": [
{
"name": "semantic-configuration",
"prioritizedFields": {
"titleField": {
"fieldName": "title"
},
"prioritizedContentFields": [
{
"fieldName": "content_text"
}
],
"prioritizedKeywordsFields": []
}
}
]
},
"vectorSearch": {
"algorithms": [
{
"name": "vector-algorithm",
"kind": "hnsw",
"hnswParameters": {
"m": 4,
"efConstruction": 400,
"efSearch": 500,
"metric": "cosine"
}
}
],
"profiles": [
{
"name": "azureOpenAi-text-profile",
"algorithm": "vector-algorithm",
"vectorizer": "azureOpenAi-text-vectorizer"
}
],
"vectorizers": [
{
"name": "azureOpenAi-text-vectorizer",
"kind": "azureOpenAI",
"azureOpenAIParameters": {
"resourceUri": "{{aoai_resourceUri}}",
"deploymentId": "{{aoai_embedding_deploymentId}}",
"apiKey": "{{aoai_apiKey}}",
"modelName": "{{aoai_embedding_modelName}}"
}
}
],
"compressions": []
}
}
Vector ProfileでFoundryへManaged ID認証をする場合は、apiKeyのエントリを削除、authIdentityの値をnullをに設定し、IDにCognitive Services OpenAI Userのロールを割当。
"azureOpenAIParameters": {
"resourceUri": "{{foundry_endpoint}}",
"deploymentId": "{{embedding_deployment}}",
"modelName": "{{embedding_deployment}}",
"authIdentity": null
}
1.3. スキルセットを作成
api-versionはperviewでないやつだとエラーになったので、previewにしています。
一回でマークダウン化と画像切り抜きができなかったので、2回に分けています。画像切り抜きのインプットにマークダウン化したテキストはないので、h1からh3までの見出しは、画像側には入れていません。マークダウン化の出力のはlocationMetadataがない点に注意。
画像化のPromptは日本語にしています。
Chunk Sizeとオーバーラップを日本語に合わせて、英語より少な目の数値にしました。
「GenAI プロンプト スキル」のパラメータは以下を参考にしました。
### Skillset作成
PUT {{endpoint}}/skillsets/{{skillset_name}}?api-version=2025-08-01-preview
content-type: application/json
api-key: {{admin_key}}
{
"name": "{{skillset_name}}",
"description": "Skillset to chunk documents and generate embeddings",
"skills": [
{
"@odata.type": "#Microsoft.Skills.Util.DocumentIntelligenceLayoutSkill",
"name": "layout_markdown_h3",
"description": "Extract Markdown up to h3 (text only)",
"context": "/document",
"outputMode": "oneToMany",
"outputFormat": "markdown",
"markdownHeaderDepth": "h3",
"inputs": [
{ "name": "file_data", "source": "/document/file_data" }
],
"outputs": [
{ "name": "markdown_document", "targetName": "markdownDocument" }
]
},
{
"@odata.type": "#Microsoft.Skills.Util.DocumentIntelligenceLayoutSkill",
"name": "layout_images_with_location",
"description": "Extract normalized images (includes figures/charts) and location metadata",
"context": "/document",
"outputMode": "oneToMany",
"outputFormat": "text",
"extractionOptions": [ "images", "locationMetadata" ],
"inputs": [
{ "name": "file_data", "source": "/document/file_data" }
],
"outputs": [
{ "name": "normalized_images", "targetName": "normalized_images" }
]
},
{
"@odata.type": "#Microsoft.Skills.Text.SplitSkill",
"name": "text_split_skill",
"description": "Split skill to chunk documents",
"context": "/document/markdownDocument/*",
"inputs": [
{
"name": "text",
"source": "/document/markdownDocument/*/content",
"inputs": []
}
],
"outputs": [
{
"name": "textItems",
"targetName": "pages"
}
],
"defaultLanguageCode": "ja",
"textSplitMode": "pages",
"maximumPageLength": 1250,
"pageOverlapLength": 250
},
{
"@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
"name": "text_embedding_skill",
"context": "/document/markdownDocument/*/pages/*",
"inputs": [
{
"name": "text",
"source": "/document/markdownDocument/*/pages/*",
"inputs": []
}
],
"outputs": [
{
"name": "embedding",
"targetName": "text_vector"
}
],
"resourceUri": "{{aoai_resourceUri}}",
"deploymentId": "{{aoai_embedding_deploymentId}}",
"apiKey": "{{aoai_apiKey}}",
"modelName": "{{aoai_embedding_modelName}}",
"dimensions": {{aoai_embedding_dimension}}
},
{
"@odata.type": "#Microsoft.Skills.Custom.ChatCompletionSkill",
"name": "genAI_prompt_skill",
"description": "GenAI Prompt skill for image verbalization",
"uri": "{{chatCompletionResourceUri}}",
"timeout": "PT3M50S",
"apiKey": "{{chatCompletionKey}}",
"extraParameters": {
"reasoning_effort": "low"
},
"extraParametersBehavior": "pass-through",
"context": "/document/normalized_images/*",
"inputs": [
{
"name": "systemMessage",
"source": "='あなたはPDF文書から抽出された画像を分析し、検索可能な日本語テキストに変換するアシスタントです。\n抽出されたテキストはRAG(検索拡張生成)のベクトルストアに格納され、ユーザーの質問に対する検索・回答に使用されます。\n\n## 基本ルール\n- 画像内のすべてのテキストを正確に読み取り、日本語で出力する。\n- 原文が日本語の場合はそのまま正確に書き写す。\n- 日本語以外の言語の場合は日本語に翻訳する。専門用語や固有名詞は原語を括弧で併記する。\n- 判読できない箇所は [判読不能] と記す。\n\n## コンテンツ種別ごとの処理\n### テキスト・文章\n- 見出し・本文の階層構造を維持して出力する。\n- 箇条書きや番号付きリストはそのまま再現する。\n\n### 表・テーブル\n- Markdown形式の表として出力する。\n- 列見出しと行の対応関係を正確に保つ。\n\n### グラフ・チャート\n- グラフの種類(棒グラフ、折れ線グラフ、円グラフ等)を明記する。\n- 軸ラベル、凡例、主要なデータポイントや値を読み取って記述する。\n- 全体の傾向や特徴を簡潔に要約する。\n\n### 図・ダイアグラム\n- 図の種類(フローチャート、構成図、概念図等)を明記する。\n- 各要素とそれらの関係性を構造的に記述する。\n\n### 写真・イラスト\n- 写っている内容を客観的かつ具体的に説明する。\n- テキストが含まれている場合はそれも抽出する。\n\n## 出力形式\n- 装飾的なヘッダーやフッター(区切り線、括弧付きラベル等)は付けず、内容のみを出力する。\n- 画像の文脈が分かるように、必要に応じて簡潔な説明を冒頭に付ける(例:「○○に関するフローチャート」)。\n\n## 禁止事項\n- 画像に存在しない情報の追加や推測による補完。\n- 主観的な評価や解釈の付与。'"
},
{
"name": "userMessage",
"source": "='この画像はPDF文書から抽出されたページの一部です。画像内のすべての情報を日本語テキストとして抽出してください。'"
},
{
"name": "image",
"source": "/document/normalized_images/*/data"
}
],
"outputs": [
{
"name": "response",
"targetName": "verbalizedImage"
}
]
},
{
"@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
"name": "verbalized-image-embedding-skill",
"description": "Embedding skill for verbalized images",
"context": "/document/normalized_images/*",
"inputs": [
{
"name": "text",
"source": "/document/normalized_images/*/verbalizedImage",
"inputs": []
}
],
"outputs": [
{
"name": "embedding",
"targetName": "verbalizedImage_vector"
}
],
"resourceUri": "{{aoai_resourceUri}}",
"deploymentId": "{{aoai_embedding_deploymentId}}",
"apiKey": "{{aoai_apiKey}}",
"dimensions": {{aoai_embedding_dimension}},
"modelName": "{{aoai_embedding_deploymentId}}"
},
{
"@odata.type": "#Microsoft.Skills.Util.ShaperSkill",
"name": "shaper-skill",
"description": "Shaper skill to reshape the data to fit the index schema",
"context": "/document/normalized_images/*",
"inputs": [
{
"name": "imagePath",
"source": "='{{imageProjectionContainer}}/'+$(/document/normalized_images/*/imagePath)",
"inputs": []
}
],
"outputs": [
{
"name": "output",
"targetName": "new_normalized_images"
}
]
}
],
"cognitiveServices": {
"@odata.type": "#Microsoft.Azure.Search.AIServicesByKey",
"key": "{{ai_service_key}}",
"subdomainUrl": "{{ai_service_endpoint}}"
},
"indexProjections": {
"selectors": [
{
"targetIndexName": "{{index_name}}",
"parentKeyFieldName": "parent_id",
"sourceContext": "/document/markdownDocument/*/pages/*",
"mappings": [
{
"name": "content_embedding",
"source": "/document/markdownDocument/*/pages/*/text_vector"
},
{
"name": "content_text",
"source": "/document/markdownDocument/*/pages/*"
},
{
"name": "title",
"source": "/document/title"
},
{
"name": "header_1",
"source": "/document/markdownDocument/*/sections/h1"
},
{
"name": "header_2",
"source": "/document/markdownDocument/*/sections/h2"
},
{
"name": "header_3",
"source": "/document/markdownDocument/*/sections/h3"
}
]
},
{
"targetIndexName": "{{index_name}}",
"parentKeyFieldName": "image_document_id",
"sourceContext": "/document/normalized_images/*",
"mappings": [
{
"name": "content_text",
"source": "/document/normalized_images/*/verbalizedImage"
},
{
"name": "content_embedding",
"source": "/document/normalized_images/*/verbalizedImage_vector"
},
{
"name": "content_path",
"source": "/document/normalized_images/*/new_normalized_images/imagePath"
},
{
"name": "title",
"source": "/document/title"
},
{
"name": "locationMetadata",
"source": "/document/normalized_images/*/locationMetadata"
}
]
} ],
"parameters": {
"projectionMode": "skipIndexingParentDocuments"
}
}
}
### スキルセット削除
DELETE {{endpoint}}/skillsets/{{skillset_name}}?api-version={{api_version}}
content-type: application/json
api-key: {{admin_key}}
AI 使って項目のフローをマーメイド記法で書きました。少し不足はありますが、正しいです。
ただ、Qiitaで見ると小さいので以下のツールなどを使ってみてください。
AzureOpenAIEmbeddingSkill, ChatCompletionSkillでFoundryへManaged ID認証をする場合は、apiKeyのエントリを削除、authIdentityの値をnullをに設定し、IDにCognitive Services OpenAI Userのロールを割当。ChatCompletionSkillはさらに、authResourceIdも追加。
{
"@odata.type": "#Microsoft.Skills.Custom.ChatCompletionSkill",
"authResourceId": "{{foundry_resource_id}}",
"authIdentity": null,
省略
変数foundry_resource_id の値は/subscriptions/[subsucription id]/resourceGroups/[resource group name]/providers/Microsoft.CognitiveServices/accounts/[foundry name]
Document Intelligence へManaged ID 認証する場合は、@odata.typeを変更し、keyを削除
"cognitiveServices": {
"@odata.type": "#Microsoft.Azure.Search.AIServicesByIdentity",
"subdomainUrl": "{{ai_service_endpoint}}"
},
1.4. インデクサー登録
特に変哲なしです。
### インデクサー作成
PUT {{endpoint}}/indexers/{{indexer_name}}?api-version={{api_version}}
Content-Type: application/json
api-key: {{admin_key}}
{
"name": "{{indexer_name}}",
"description": null,
"dataSourceName": "{{datasource_name}}",
"skillsetName": "{{skillset_name}}",
"targetIndexName": "{{index_name}}",
"disabled": null,
"schedule": null,
"parameters": {
"batchSize": null,
"maxFailedItems": null,
"maxFailedItemsPerBatch": null,
"configuration": {
"dataToExtract": "contentAndMetadata",
"parsingMode": "default",
"allowSkillsetToReadFileData": true
}
},
"fieldMappings": [
{
"sourceFieldName": "metadata_storage_name",
"targetFieldName": "title",
"mappingFunction": null
}
],
"outputFieldMappings": [],
"encryptionKey": null
}
### インデクサー削除
DELETE {{endpoint}}/indexers/{{indexer_name}}?api-version={{api_version}}
Content-Type: application/json
api-key: {{admin_key}}
2. 検索
2.1. フル検索
locationMetadataも出力したフル検索
### Query the index
POST {{endpoint}}/indexes/{{index_name}}/docs/search?api-version={{api_version}}
Content-Type: application/json
api-key: {{admin_key}}
{
"search": "*",
"count": true,
"select": "chunk_id, content_text, title, header_1, header_2, header_3, content_path, image_document_id, locationMetadata"
}
画像の検索結果です。テキスト側も似たようにboundingPolygonsが出ます。
{
"@search.score": 1.0,
"chunk_id": "c7b0f04fdd0b_aHR0cHM6Ly9zdG9yYWdlcmFnanBlLmJsb2IuY29yZS53aW5kb3dzLm5ldC9yYWctZG9jLXRlc3QvMDEucGRm0_markdownDocument_3_pages_0",
"content_text": "■ 少子高齢化や自然災害の激甚化、自動車保険市場の縮小等の中長期的な事業環境の変化 など",
"title": "01.pdf",
"header_1": "保険モニタリングレポート【概要】",
"header_2": "環境変化と諸課題",
"header_3": "環境変化",
"image_document_id": null,
"content_path": null,
"locationMetadata": null
},
{
"@search.score": 1.0,
"chunk_id": "6f495a8c939e_aHR0cHM6Ly9zdG9yYWdlcmFnanBlLmJsb2IuY29yZS53aW5kb3dzLm5ldC9yYWctZG9jLXRlc3QvMDEucGRm0_normalized_images_0",
"content_text": "The image features a logo consisting of stylized letters that represent the acronym \"S.\" The design includes overlapping shapes that suggest movement or flow, indicating dynamism and innovation. The composition emphasizes a streamlined form, suggesting efficiency.",
"title": "01.pdf",
"header_1": null,
"header_2": null,
"header_3": null,
"image_document_id": "aHR0cHM6Ly9zdG9yYWdlcmFnanBlLmJsb2IuY29yZS53aW5kb3dzLm5ldC9yYWctZG9jLXRlc3QvMDEucGRm0",
"content_path": "images/aHR0cHM6Ly9zdG9yYWdlcmFnanBlLmJsb2IuY29yZS53aW5kb3dzLm5ldC9yYWctZG9jLXRlc3QvMDEucGRm0/normalized_images_0.jpg",
"locationMetadata": {
"pageNumber": 2,
"boundingPolygons": "[[{\"x\":0.0474,\"y\":0.0402},{\"x\":0.5967,\"y\":0.0403},{\"x\":0.5968,\"y\":0.5351},{\"x\":0.0475,\"y\":0.535}]]"
}
},
2.2. 条件付き検索
サンプルとして残しておきます。locationMetadata/pageNumberを条件に入れたかったですが、画像にしか有効でないのと、そもそもfilterbleに設定しなかったので中止。
### Query the index with filter
POST {{endpoint}}/indexes/{{index_name}}/docs/search?api-version={{api_version}}
Content-Type: application/json
api-key: {{admin_key}}
{
"search": "*",
"count": true,
"filter": "title eq '01point.pdf'",
"orderby": "title asc, locationMetadata/pageNumber asc",
"select": "chunk_id, content_text, title, content_path, header_1, header_2, header_3, image_document_id, locationMetadata/pageNumber"
}
更新情報
- 2026/3/6: Indexでベクトル項目を保存・取得しないように変更(Disk容量節約のため)
- 2026/3/6: SkillSet でChat部分のSkillをタイムアウト230秒に増加(よく落ちるため)
- 2026/3/6: SkillSet でChat部分のSkillでReasoning Effort追加(gpt-5.2のデフォルトのMediumだと遅いため)

