0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PR: アディクシィ株式会社
ADiXiのコーポレートサイト

Azure AI Searchの実務設計:インデックス・更新・ランキングと障害時の調べ方

0
Last updated at Posted at 2026-11-12

はじめに

Azure AI Searchを本番で使うなら、インデックスの項目定義、更新処理、ランキングを一緒に設計する必要があります。

本番で調べるのは順位だけではありません。原本を差し替えたのに旧手順が出る、権限を外した利用者がまだ文書を開ける、更新ジョブは成功したのに検索結果が変わらない。こうした問題は、インデックスと更新処理、検索アプリを通して追う必要があります。

この記事では、社内規程や運用手順書を検索するシステムを題材に、次の順で設計を整理します。

  1. 検索結果として何を返すかを決める。
  2. 文書の追加・変更・削除を追跡できる形で登録する。
  3. 正解が候補に入ることを確認してから順位を調整する。
  4. 更新中の検索、権限変更、再構築まで運用に含める。

仕様確認・技術改訂日:2026年9月24日。

REST APIの例は、確認日時点でMicrosoftが最新の安定版として掲載している 2026-04-01 を使用します。[1]

対象は、サーバー側の検索アプリから検索インデックスへ問い合わせる構成です。検索インデックスは原本から再生成できるデータとして扱い、原本・公開版・認可情報の管理先は別に設けます。エージェントによる検索計画を含むagentic retrievalは扱いません。

実装例は、Azureパブリッククラウド、1536次元の埋め込み、コサイン類似度、独自のグループ権限フィルターを前提にしています。

会話例は障害対応を説明するための創作です。著者の実体験や実在人物の発言ではありません。実際の開発者の報告は、出典と投稿日を添えて別に紹介します。コードは設計例であり、Azure実環境での動作・性能検証は未実施です。

困りごとから読む場合は、次の節を入口にしてください。

いま起きていること 最初に読む節
消した文書や古い手順が出る 1節の初動、5節の更新、6節の削除
原本にある説明が検索されない 4節の抽出・分割、7節の候補取得
管理者では再現しない/利用者ごとに結果が違う 6.4節の権限・キャッシュの再現試験
作り直したインデックスが空/切り戻しが不安 10節の再構築と引き継ぎ
会話が続くと回答がずれる/画面だけ遅い 11.4節の再現条件、12.4節の計測
受入試験で何を確認するか決まっていない 13節の試験表と担当分担

1. 月曜の朝、「その手順書、もう消しましたよ」

架空の障害対応例です。月曜9時12分、利用部門から連絡が入ります。

利用者:「検索の一番上に出た手順で進めたら、申請先が違うと言われました。午前中に出したいんですが」

インフラ担当は監視画面を開きます。サービスは稼働中。アプリ担当が確認した取り込みジョブも成功表示です。文書管理担当は、金曜に原本を差し替えたと説明します。

インフラ担当:「Searchの応答自体は返っています」

アプリ担当:「更新ジョブは完了しています。どの結果を開きましたか」

文書管理担当:「ファイル置き場に旧版はありません」

サービスとジョブの状態は分かりました。ただ、利用者が開いた検索結果は、まだ確認できていません。

1.1 最初に集めるのは、再現できる一件

担当SEは、利用者が開いた結果を特定するため、次の情報を集めます。

集める情報 確認すること
入力した検索語、発生時刻とタイムゾーン、リクエストID 同じ検索処理をログで追えるか
対象テナント、認可条件の識別情報、利用した画面 管理者の別条件で再現していないか
誤って返った原本ID・チャンクID・版 どのデータを返したのか
本来返るべき原本と該当箇所 期待値が利用部門と一致しているか

利用者からトークンやAPIキーを送ってもらう必要はありません。質問や文書の内容は、閲覧を許可された調査先で扱います。問い合わせ票の書式は13.3節に載せます。

この例では、同じ原本の旧チャンクが残っていました。実装は原本IDとチャンク番号からキーを作り、更新時も同じキーへ上書きする方式です。文書が12チャンクから8チャンクに短縮され、先頭8件だけが更新されていました。末尾4件を削除する処理が抜けていたのです。

版ごとに別のキーを作る実装なら、削除しない限り旧版12件が丸ごと残ります。残り方はID設計で変わりますが、どちらも登録だけでは更新を完了できません。

原因が分かると、次の相談は「いつ直るか」と「それまで使ってよいか」です。

運用担当:「その文書だけ止められますか。全部止めると、ほかの部署も困ります」

担当SE:「文書ごとに止められるか確認します。ほかの文書にも旧版が残っていないか、同じ更新処理を調べます」

文書管理担当:「正しい手順の案内先はこちらで確認します」

原本単位で停止できなければ、検索機能全体の停止や利用者への案内を含め、対応責任者が暫定措置を決めます。どこまで止められるかは、実装済みの機能によります。

対象IDと投入履歴を保存し、旧チャンクの返却を止めて削除します。復旧確認には、利用者から受け取った検索語と権限条件を使います。削除と公開切り替えの手順は5節で説明します。

1.2 公開された開発者の声:同じファイル名なら旧チャンクは消えるのか

Azure-Samplesの azure-search-openai-demo では、2025年1月24日に、同名PDFの内容を変更して取り込むと旧チャンクは自動で削除されるのか、という質問が投稿されています。2025年2月4日の回答では、当時の手動取り込みスクリプトはチャンク削除を行わず、必要なら明示的に削除する旨が説明されています。[35]

この回答の対象は、当時のサンプルの手動取り込みです。インデクサーやindex projectionsでは更新・削除の仕組みが異なります。採用する方式で、文書の短縮・差し替え・削除をそれぞれ試してください。

2. インデックス設計は「利用者の質問」から始める

Azure AI Searchのインデックスは、原本とは別に保持される検索用のデータです。[2]

原本のデータ構造をそのままコピーすると、検索結果に必要な情報が足りないことがあります。先に、利用者の質問と返すべき単位を整理します。

利用者の質問 必要な検索・判定 持たせたい情報
出張申請の手順を知りたい 説明文の検索 タイトル、見出し、本文
エラーコードに対応する手順を探したい 識別子の一致 エラーコードの専用フィールド
最新の承認済み手順だけ見たい 公開状態・有効版の判定 状態、原本の版、有効期間
自部署で閲覧できる文書だけ探したい 閲覧権限による絞り込み テナントID、許可グループID
検索結果の根拠を原本で確認したい 出典への遷移 原本ID、URL、ページ・節の情報

先月承認された正式手順と、今朝編集された下書き。業務で必要なのは、日付の新しさだけで決まりません。承認済みか、有効な版かを検索対象の条件に含めます。

2.1 1文書を1件にするか、チャンクに分けるか

登録単位 向いている対象 利点 設計上の負担
原本文書を1件 短いFAQ、商品説明 更新・削除を管理しやすい 長文では話題が混在しやすい
チャンクを1件 長い規程、手順書、RAG 該当箇所を取り出しやすい 親子関係、旧チャンク削除、重複対策が必要
親と子を別インデックス 原本の一覧と段落検索を分けたい場合 役割を分けて設計できる アプリ側で結果を突き合わせる必要がある

Azure AI Searchは検索時のインデックス間JOINを提供しません。親子を分ける場合、必要な情報の統合はアプリ側の仕事になります。[3]

以下は、チャンクを1件として登録し、各チャンクにタイトル・権限・原本情報を持たせる設計です。

2.2 フィールドの属性を「全部true」にしない

属性 用途 この記事での使い方
searchable 全文検索またはベクトル検索 タイトル・見出し・本文・ベクトル
filterable 条件による絞り込み テナント、権限、状態、版
sortable フィールド値による並び替え 原本更新日時、管理用のチャンクID順
facetable 分類別の件数表示 必要になった分類だけ
retrievable 検索結果として値を返す 本文・原本情報。権限一覧とベクトルは返さない

本文に対して、使う予定のない絞り込み・ソート・ファセットを設定しません。余計な索引を作るうえ、文字列フィールドを filterable などにすると、長さの制約にも注意が必要です。[2]

また、REST APIとSDKで既定値が同じとは限りません。設計上重要な属性は明示します。

既存フィールドの filterable や sortable を後から有効にするには、再構築が必要です。画面の検索条件に加え、運用で使う照合クエリも作成前に確認します。[4]

3. インデックス定義の例

次は、自前の取り込み処理からPush APIで投入する場合の例です。index projections用の定義ではありません。

REST Client形式の {{endpoint}} と {{token}} は、実際の値に置き換えます。AzureパブリッククラウドでSearch用アクセストークンを取得する際のスコープは https://search.azure.com/.default です。[30]

操作ごとの実行主体を分けます。構築用IDには Search Service Contributor、投入用IDには Search Index Data Contributor、検索アプリ用IDには Search Index Data Reader を割り当てる構成を例とします。サービス側でRBACを利用できる認証設定も必要です。[5][30]

Search Index Data Contributorは文書の検索もできます。Search Service Contributorはサービス設定やAPIキー取得も可能なので、通常の検索アプリには付与せず、構築用IDに限定します。ロールの割り当て先とスコープは、実行する操作に合わせます。[30]

PUT {{endpoint}}/indexes/knowledge-v1?api-version=2026-04-01
Authorization: Bearer {{token}}
Content-Type: application/json

リクエスト本文は次のとおりです。

{
  "name": "knowledge-v1",
  "fields": [
    {
      "name": "chunkId", "type": "Edm.String", "key": true,
      "searchable": false, "filterable": true, "sortable": true,
      "facetable": false, "retrievable": true
    },
    {
      "name": "parentId", "type": "Edm.String",
      "searchable": false, "filterable": true, "sortable": false,
      "facetable": false, "retrievable": true
    },
    {
      "name": "revision", "type": "Edm.String",
      "searchable": false, "filterable": true, "sortable": false,
      "facetable": false, "retrievable": true
    },
    {
      "name": "publicationId", "type": "Edm.String",
      "searchable": false, "filterable": true, "sortable": false,
      "facetable": false, "retrievable": true
    },
    {
      "name": "embeddingContractId", "type": "Edm.String",
      "searchable": false, "filterable": true, "sortable": false,
      "facetable": false, "retrievable": true
    },
    {
      "name": "indexGeneration", "type": "Edm.String",
      "searchable": false, "filterable": false, "sortable": false,
      "facetable": false, "retrievable": true
    },
    {
      "name": "tenantId", "type": "Edm.String",
      "searchable": false, "filterable": true, "sortable": false,
      "facetable": false, "retrievable": false
    },
    {
      "name": "allowedGroupIds", "type": "Collection(Edm.String)",
      "searchable": false, "filterable": true, "sortable": false,
      "facetable": false, "retrievable": false
    },
    {
      "name": "status", "type": "Edm.String",
      "searchable": false, "filterable": true, "sortable": false,
      "facetable": false, "retrievable": true
    },
    {
      "name": "title", "type": "Edm.String",
      "searchable": true, "filterable": false, "sortable": false,
      "facetable": false, "retrievable": true, "analyzer": "ja.microsoft"
    },
    {
      "name": "sectionTitle", "type": "Edm.String",
      "searchable": true, "filterable": false, "sortable": false,
      "facetable": false, "retrievable": true, "analyzer": "ja.microsoft"
    },
    {
      "name": "content", "type": "Edm.String",
      "searchable": true, "filterable": false, "sortable": false,
      "facetable": false, "retrievable": true, "analyzer": "ja.microsoft"
    },
    {
      "name": "sourceUri", "type": "Edm.String",
      "searchable": false, "filterable": false, "sortable": false,
      "facetable": false, "retrievable": true
    },
    {
      "name": "sourceUpdatedAt", "type": "Edm.DateTimeOffset",
      "searchable": false, "filterable": true, "sortable": true,
      "facetable": false, "retrievable": true
    },
    {
      "name": "contentVector", "type": "Collection(Edm.Single)",
      "searchable": true, "filterable": false, "sortable": false,
      "facetable": false, "retrievable": false, "stored": true,
      "dimensions": 1536, "vectorSearchProfile": "content-profile"
    }
  ],
  "vectorSearch": {
    "algorithms": [
      {
        "name": "content-hnsw",
        "kind": "hnsw",
        "hnswParameters": { "metric": "cosine" }
      }
    ],
    "profiles": [
      { "name": "content-profile", "algorithm": "content-hnsw" }
    ]
  },
  "semantic": {
    "configurations": [
      {
        "name": "content-semantic",
        "rankingOrder": "RerankerScore",
        "prioritizedFields": {
          "titleField": { "fieldName": "title" },
          "prioritizedContentFields": [
            { "fieldName": "sectionTitle" },
            { "fieldName": "content" }
          ]
        }
      }
    ]
  }
}

1536 はこの例で選んだ次元数で、Azure AI Search共通の固定値ではありません。登録側と検索側で、モデル・モデルバージョン・次元数・モデルが要求する入力形式を管理し、対応する埋め込みを使います。次元数が一致するだけでは互換性を確認できません。類似度関数もモデルに合わせて選びます。[32]

版を示す項目は、次のように用途を分けます。いずれもアプリ独自のメタデータで、Azureが内容の互換性や公開可否を自動判定する機能ではありません。

項目 識別するもの
revision 原本側の版
publicationId その原本から作り、ひとまとまりとして公開するチャンク集合
embeddingContractId モデル・バージョン・次元数・入力形式と、文書側/検索側それぞれの前処理
indexGeneration knowledge-v1 など、投入先の物理インデックスの世代

同じ原本の版でも、抽出方法や分割方法を変えれば別のチャンク集合になります。そのため、返却時の公開判定を revision だけに任せず、5節の publicationId を照合します。モデルや前処理を変える場合は生成条件IDも見直し、再埋め込みの要否を判断します。

本文と検索語の埋め込みはアプリ側で生成して渡すため、この定義にvectorizerは含めていません。chunkId の sortable: true は、5.5節でチャンクIDを順番に列挙するために使います。

ACLの部分更新を行うため、contentVector は stored: true にしています。stored: false では、部分更新時にベクトルを省略すると、既存のベクトルがエラーや警告なしに失われる場合があります。retrievable は検索結果への返却、stored は保存用コピーの保持を制御する別の設定です。既存フィールドの stored は変更できません。[7][26]

retrievable: false はアクセス制御ではありません。フィールド値を通常の文書結果として返さない設定であり、検索機能を経由した情報露出まで一律に防ぐものではありません。[2][8]

また、スキーマを作っても status や権限の値は自動で補完されません。投入前に、必須メタデータの欠落を検出します。グループIDとテナントIDは取り込み側・検索側で同じ表記へ正規化します。

社内限定の原本への sourceUri は、閲覧時に認可できるURLまたはパスにします。SAS付きURLなど、URLを知るだけで原本を開ける値は保存しません。引用箇所を再現できるよう、必要に応じてページ・節・原本の版も持たせます。

4. 日本語、識別子、チャンク分割を別々に確認する

4.1 「画面にはあるのに検索できない」を切り分ける

日本語アナライザーには ja.microsoft と ja.lucene があります。この記事は前者を使いますが、採用時には実際の文書と検索語で比較します。[7][9]

自然文、製品名、部品番号、エラーコードでは、期待する分割が違います。

たとえば AB-1200-X を検索したいのに、自然文向けの解析だけに任せると、期待した一致条件にならない可能性があります。実際のトークンを確認します。

POST {{endpoint}}/indexes/knowledge-v1/analyze?api-version=2026-04-01
Authorization: Bearer {{token}}
Content-Type: application/json

{
  "text": "申請取次 AB-1200-X 再起動手順",
  "analyzer": "ja.microsoft"
}

Analyze APIで返ったトークンを確認し、型番や日本語の複合語が、想定どおりに分割されているかを調べます。[9]

識別子を厳密に扱うなら、本文と別に productCode などのフィールドを設計します。完全一致でよければ、正規化ルールを決めたうえで filterable な文字列として比較できます。この専用フィールドは上の最小スキーマには含めていません。

ハイブリッド検索の search に型番を書いても、ベクトル側の候補すべてに型番一致を要求したことにはなりません。型番一致が必須なら、その条件を認可条件とともにトップレベルの filter へ入れる、または識別子検索を専用の経路に分けます。[16]

ただし、識別子が似ているだけで同じ製品とは限りません。ハイフン除去、大文字小文字の統一、同義語展開は、業務上同一と扱える範囲を確認してから適用します。

4.2 「何文字で切るか」より、条件と結論が一緒に残るか

規程を機械的に分割すると、次のような問題が起こり得ます。

  • 「原則として承認不要」だけが残り、例外条件が別チャンクになる。
  • 表の数値が残り、単位や列見出しが失われる。
  • 「前項の定めによる」の参照先が分からなくなる。
  • ページごとのヘッダーやフッターが本文に混入する。

分割の評価は、抽出後のテキストを人が読むところから始めます。

比較を始めるなら、たとえば400・800・1,200トークン程度の3案を用意します。ここでの提案値であり、Microsoftの推奨値ではありません。長さは文字数から推測せず、使うモデルのトークナイザーで測ります。

見出し単位の分割、条件文を含めた境界調整、必要な範囲の重なりを比較します。重なりを増やした場合は、検索上位が同じ原本の似た段落ばかりにならないかも確認します。

4.3 架空シナリオ:「PDFには書いてあります。検索の設定がおかしいのでは」

利用部門:「旅費の上限はこの表に書いてあります。なぜ違う区分の金額を答えるんですか」

AI担当:「回答の根拠に使った段落を開いてみましょう」

取り込み担当:「金額はありますね。でも、これだけだと国内向けか海外向けか分かりません」

この例では、国内・海外の列見出しと金額が別のチャンクに入っていました。原本PDF、抽出後テキスト、登録済みチャンクの同じ箇所を並べると、どこで対応関係が崩れたかを確認できます。

切り分けは次の順にします。

  1. 抽出後の時点で列・単位・脚注が失われていたら、抽出処理を調べる。
  2. 抽出後にはそろっているが分割後に離れていたら、チャンク境界と見出しの付与を調べる。
  3. 必要な条件がチャンクに入っているのに候補に出なければ、7節の検索条件を調べる。
  4. 候補にはあるがLLMへ渡る前に落ちていたら、アプリの選別・入力長制限を調べる。

文書担当者には「結果はよいですか」とまとめて聞かず、「この金額は何に対する上限ですか」「例外になる条件はどこですか」と原本上で確認してもらいます。その回答を評価用の期待値に残せば、分割方式を変更した後も比較できます。

5. 更新処理は「再実行できること」まで設計する

取り込み方式には、大きくPush APIとインデクサーがあります。[10]

方式 利点 注意点 選ぶ条件
Push API 更新順序や独自の加工を制御しやすい 再試行、削除、差分管理を自分で実装する 独自データソース、複雑な承認・更新フロー
インデクサー 対応データソースの取り込みを任せられる 変更・削除検出の対応条件を確認する 対応するデータソースを定期取り込みする
インデクサー+スキル+index projections 分割や子ドキュメント生成を連携できる 投影の設定と親子更新の仕様を理解する マネージドな文書加工パイプラインを使いたい

Push APIを選ぶ場合、再試行・削除・順序制御を自分たちで実装し、保守する工数を見込みます。

5.1 upload、merge、mergeOrUploadの使い分け

操作 既存のキー 存在しないキー 使いどころ
upload 文書全体を置換 新規作成 完成した文書の登録・置換
merge 指定フィールドを更新 失敗 既存文書の部分更新
mergeOrUpload 部分更新 新規作成 新規・既存をまとめて処理
delete 文書を削除 存在しなくても成功扱い キーを指定した削除

mergeOrUpload は既存文書の完全置換ではありません。省略した通常のフィールドは既存値が残るため、削除したつもりの値を保持することがあります。ただし、stored: false のベクトルは省略時に保持されないため、同じ説明を当てはめないでください。[11][26]

逆に、ACLしか送らない処理で mergeOrUpload を使うと、キーが誤っていた場合に本文のない文書を新規作成してしまいます。既存文書だけを更新する処理では merge とし、対象がなければ調査対象にする方が意図を明確にできます。全文置換に upload を使う場合は、本文・ベクトル・ACLを含む必要な全フィールドを組み立ててから送ります。

また、コレクションへの merge は要素追加ではなく、渡した配列への置換です。

既存の権限が次の場合、

{ "allowedGroupIds": ["group-a", "group-b"] }

次の値を merge すると、

{ "allowedGroupIds": ["group-c"] }

結果は group-c だけになります。3グループにしたいなら、更新後の配列全体を送ります。[11]

5.2 本文だけ更新し、ベクトルを更新し忘れる

次も架空の例です。夕方、手順変更を反映した担当者が、検索結果の本文を確認します。画面の文章は新版になっています。

ところが翌日、「前の手順を探す質問では出るのに、新しい手順を尋ねると出にくい」と連絡が来ます。

調査では、まず同じ認可条件で全文検索とベクトル検索を分けて実行します。全文検索では新版の言葉で見つかる一方、ベクトル検索では期待した候補が出ませんでした。

アプリ担当:「本文は新版なので、取り込みは終わっているはずです」

AI担当:「埋め込みも作り直しましたか。そのときに送った本文を確認したいです」

検索結果の違いだけでは原因を断定できません。投入内容と埋め込み生成の記録を突き合わせたところ、この例では content だけを更新し、contentVector は旧本文から作ったままだと分かりました。

本記事のように外部で埋め込みを生成する構成では、本文の merge だけでベクトルが自動更新されるわけではありません。

対策として、本文変更を処理するときは、同じ原本の版から本文・ベクトル・メタデータを作り、一つのチャンク更新にまとめます。埋め込みに失敗した場合は、その失敗を未完了として管理します。

ACLだけの変更で、埋め込み対象のテキストが変わらなければ、再埋め込み計算は不要です。ただし、stored: false を採用した構成では、部分更新にも既存ベクトル全体の再送が必要です。再計算を省けることと、APIへの再送を省けることは分けます。[26]

この基本例は stored: true なので、ACLの merge にベクトルを含めない運用ができます。保存量の削減を検討するときは、先にこの更新経路を洗い出します。

5.3 IDと更新台帳を用意する

途中で失敗した更新を再開できるよう、取り込み処理の台帳を検索インデックスの外に持たせます。

管理項目 目的
テナントID+原本ID 別テナント・別原本との衝突を防ぐ
原本の版・内容ハッシュ 更新の要否と処理対象を確認する
ACLの版または変更番号 本文投入の再試行で古い権限を戻さないよう照合する
チャンクID一覧 旧チャンクを漏れなく削除する
抽出・分割処理のバージョン 同じ原本を再加工するときの生成条件を特定する
埋め込みモデル・バージョン・次元数・入力ハッシュ どの入力と設定からベクトルを作ったかを追跡する
登録成功・検索確認・削除完了の状態 途中失敗から再開する
公開中/処理中の publicationId・返却停止状態 同じ原本版の再処理も含めて返却可否を判定する
投入先の indexGeneration・処理済みイベント位置・削除記録 投入先ごとの進捗を追い、古いイベントでの復活と差分欠落を防ぐ

publicationId は、原本の版・内容ハッシュ・抽出/分割の設定・埋め込み設定が定まった一つのチャンク集合に付けます。処理開始時に台帳へ保存し、同じ内容の再試行ではID、本文、ベクトルを再利用します。本文や分割結果を変えるなら、原本の revision が同じでも新しいIDにします。

公開中の publicationId と処理状態は、テナントID・原本ID・indexGeneration の組で管理します。再構築中は、同じ原本でも投入先によって分割結果や処理の進み具合が違うためです。

チャンクIDは、テナントID・原本ID・publicationId・チャンク番号を、順序が固定された形式でエンコードし、SHA-256の16進文字列などのキーへ変換する方法があります。publicationId やチャンクIDを再試行のたびに作り直すと、重複登録になります。ACLだけの変更では publicationId を変えず、権限情報の版と更新順序を別に管理します。

単純な文字列連結は区切りの解釈が曖昧になり得るため、JSON配列などで各要素を区別します。投入前に同一バッチ内のキー重複も検出します。

キーを版ごとに変える方式では、旧版のIDを台帳に残し、削除完了まで追跡します。

revision を保存しただけでは古い書き込みを拒否できません。同じ投入先・原本への処理を順序制御し、原本側の変更番号などで古いイベントを除外します。日時を使う場合は、同時刻の更新や時計ずれも扱います。

ACL更新も、この順序制御の対象です。新しいACLを merge した後で、失敗した本文投入を古いACL付きの upload で再送すると、権限まで戻してしまいます。再試行時はACLの版を正本と照合し、古ければ投入内容を更新します。確認と送信の間に別のACL更新が割り込まないよう、同じ原本の更新を調整します。成否不明の要求が遅れて反映されるケースには、定期照合と6.2節の返却前の認可を併用します。[11]

インデックス定義などのリソースにはETagによる更新競合の制御がありますが、これを文書の業務上の版チェックと混同しません。文書投入APIに revision の大小比較を任せることはできません。[11][28]

通信タイムアウト後に書き込みが完了していたケースも扱います。削除記録を残し、原本・更新台帳・インデックスを定期照合して、遅着した処理による孤立チャンクを回収します。

5.4 HTTP 207を「2xxだから成功」にしない

バッチ投入には、1,000ドキュメントと16 MBの制限があります。件数だけでなく、ベクトルを含めてシリアライズしたリクエスト全体のサイズを確認します。[12]

さらに、一部の文書が失敗した場合にはHTTP 207が返ることがあります。各文書の status、statusCode、errorMessage を確認します。[4]

下記は、投入応答を送信したキーと照合し、失敗した文書を返す関数です。200/207以外の応答や通信エラーの再試行は、呼び出し側で扱います。

def failed_index_items(http_status, response_body, sent_keys):
    if http_status not in (200, 207):
        raise RuntimeError(f"投入要求全体の失敗: HTTP {http_status}")
    if not sent_keys or len(set(sent_keys)) != len(sent_keys):
        raise ValueError("送信キーが空、または重複しています")
    if not isinstance(response_body, dict):
        raise ValueError("投入応答の形式を確認できません")

    results = response_body.get("value")
    if not isinstance(results, list):
        raise ValueError("投入結果の一覧を確認できません")
    if any(not isinstance(item, dict) for item in results):
        raise ValueError("文書単位の応答形式が不正です")

    returned_keys = [item.get("key") for item in results]
    if any(not isinstance(key, str) for key in returned_keys):
        raise ValueError("応答に文書キーがありません")
    if len(returned_keys) != len(sent_keys) or set(returned_keys) != set(sent_keys):
        raise ValueError("送信キーと応答キーが一致しません")
    if any(type(item.get("status")) is not bool for item in results):
        raise ValueError("文書単位の成否を確認できません")

    return [item for item in results if item["status"] is False]

requests を使うなら、応答受信後に failed_index_items(response.status_code, response.json(), sent_keys) を呼び出します。raise_for_status() だけでは、207内の文書単位の失敗は検出できません。

返った失敗一覧を無視してジョブを完了させないでください。原本・処理IDにひも付けて記録し、下記の方針で処理します。[4][10]

状態 対応方針
文書単位の400 データ・型・スキーマを修正する。無変更では再試行しない
merge の404 キーと更新順序を確認する。自動で新規作成へ変えない
409・一時的な422・503 同一原本への並行書き込みも確認し、上限付きで再試行する
429 容量・クォータ・負荷を調べる。待てば解消するとは限らない
HTTP 413 バッチを小さくする。単独文書でも超過するなら分割設計を直す
通信タイムアウト 成否不明として扱う。古い版を上書きしないよう順序制御したうえで再実行する

ジョブ通知には、その試行の送信・成功・失敗件数と、再試行待ち・削除待ちの件数を出します。再試行による延べ件数と、処理対象の実件数は分けて集計します。「終了」の一言では、当番が未処理を見落とします。

一時障害では待機を段階的に延ばし、少量のランダムな待ち時間を加え、並列数と総再試行時間に上限を設けます。部分失敗時は失敗した文書を対象にしますが、再試行までに原本が新版へ進んでいたら、古い書き込みをそのまま再送しません。

また、同じ文書キーに対する複数操作を一つのバッチに入れ、配列順に実行されると想定してはいけません。[4]

5.5 投入成功と検索可能化は別に確認する

投入APIの成功後、検索可能になるまでには遅延があります。[4]

以下は、更新途中の手順を返せない業務向けの設計案です。対象原本を一時的に検索結果から外すため、その間は見つからなくなります。更新中も旧版を返せる業務なら、10.3節の別方式と比較してください。

  1. 投入先の indexGeneration、原本の版、生成条件、新しい publicationId と新旧チャンクID一覧を更新台帳へ記録する。
  2. 対象原本を更新中として扱い、アプリはその原本の検索結果を利用者やLLMへ渡さない。
  3. 確定したチャンクを全件投入し、それぞれの成否を照合する。
  4. 管理用検索で tenantId・parentId・publicationId を指定し、取得したID集合を期待値と照合する。
  5. 旧IDを明示的に削除し、失敗がないことと管理用検索の結果を確認する。
  6. 台帳の処理中IDが自分の処理対象と一致することを条件に、公開中IDの切り替えと返却再開を一つの状態更新として確定する。

照合は対象の物理インデックスへ行い、search を省略したフィルター検索でIDを列挙します。ベクトル検索やセマンティックランキングは指定しません。[6]

複数ページに分かれる場合は、orderby: "chunkId asc" で順序を固定します。テナント・原本・publicationId の条件を保ち、SDKのページング機能、またはRESTの top・skip と継続応答を処理して全件を取得します。3節の chunkId は、このために sortable を有効にしています。[18][39]

skip の上限は100,000です。この例は、その上限内で列挙できるチャンク集合を対象にします。それを超える場合は、数値型の連番フィールドを追加して範囲で区切るなど、列挙方法を変えます。文字列型の chunkId には、gt による範囲比較を使えません。[18][40]

ページングはスナップショットではありません。照合中は同じチャンク集合への書き込みを止め、取得したID集合を期待値と比較します。反映途中で欠落していれば、上限時間内で先頭から照合し直します。件数だけの一致では、欠落と余分な文書を見逃します。IDがそろっていても本文・ベクトルの正しさまでは確認できないため、投入データの検証と11節の検索評価も行います。[39]

確認にはタイムアウトを設けます。途中失敗した原本は公開を再開せず、記録した処理状態から復旧します。削除対象が多い場合も、キー一覧を確定してからバッチ削除します。Push APIの delete はキー指定であり、parentId を渡すだけで子を一括削除する操作ではありません。[11]

この照合で、全レプリカへの同時反映を保証することはできません。通常検索でも、結果の indexGeneration をリリース構成と照合したうえで、テナントID・原本ID・その世代に対応する台帳を参照します。公開中の publicationId と一致し、返却停止中でもないチャンクだけを通します。台帳を確認できなければ返却を止めます。

台帳の条件付き更新は、選んだデータストアで実装します。古いワーカーによる公開状態の巻き戻りと、台帳キャッシュの遅延も試験対象です。この制御により旧版の返却を抑えられますが、除外した分だけ検索結果は減ります。文書の認可は6節の処理で別途確認します。

6. 文書削除と権限剥奪は、通常の更新より厳しく扱う

6.1 元ファイルを消せば検索結果も消える、とは限らない

Azure Storageのインデクサーでは、変更検出と削除検出を分けて考えます。削除検出ポリシーを後から追加しても、それ以前に失われた情報をすべて復元して削除できるわけではありません。[13]

さらに、「1つの原本から複数件を作る」という見た目が同じでも、方式によって挙動が違います。

取り込み方式 削除時に確認すること
自前のチャンク生成+Push API 管理している旧チャンクIDを明示的に削除する
Blobの解析モードによるone-to-many Native Blob Soft Deleteとカスタムメタデータによるソフト削除は適用されないため、削除APIで処理する
スキルセットのindex projections 原本短縮による子の削除と、親削除検出の条件を確認する

index projectionsでは、文書短縮で不要になった子ドキュメントの削除が説明されています。一方、親文書全体の削除を子へ反映するには、データソース側の削除検出が必要です。[3][13]

Native Blob Soft Deleteでは、インデクサーがsoft-deleted状態を保持期間内に処理できることが必要です。保持期間は通常の実行間隔だけでなく、障害による停止と再処理の時間も含めて設計します。また、公式資料ではストレージアカウントでBlobバージョン管理を有効にしている場合は、この削除検出方式を使用できないとされています。[13]

解析モードによるone-to-manyとindex projectionsの説明を混ぜず、実際のデータソース・分割方法の組み合わせで、原本削除と短縮の両方を受入試験に入れます。

6.2 権限フィルターはサーバーで組み立てる

この記事の方式は、権限を文字列フィールドとフィルターで表現するsecurity filter patternです。このフィールド自体がユーザーを認証するわけではありません。[8]

検索リクエストへ適用する条件は、たとえば次の形です。

tenantId eq 'tenant-a'
and status eq 'published'
and allowedGroupIds/any(g: search.in(g, 'group-a,group-b', ','))

実際には、認証・検証済みの情報からサーバー側で構築します。ここでいう tenantId はアプリ上のデータ所有テナントです。認証トークンのテナントIDと必ず一致するとは限らず、アプリ側で利用者の所属先・アクセス先を認可して対応付けます。

ブラウザーが送ってきたテナントIDやグループIDを、そのまま信用してはいけません。グループ一覧の取得失敗・省略・期限切れも扱います。汎用文字列を使う実装ではOData文字列のエスケープが必要です。8節では扱うIDをUUID形式に限定し、正規化・形式検証する例にします。[31]

この例の権限が未取得・空の場合は、検索しない扱いにします。条件が作れないから権限フィルターを外す、というフォールバックは避けます。

検索後に画面上だけ隠す実装では、先に本文をLLMやログへ渡してしまう可能性があります。絞り込みは検索リクエストに適用し、原本へのアクセスでも権限を確認します。

権限剥奪を即時に反映する要件では、検索インデックスの更新だけに依存しません。本文・タイトル・引用などを利用者やLLMへ渡す前に、認可の正本で確認する経路と、認可情報を取得できない場合の停止動作を設けます。「即時」が何秒以内なのか、進行中の応答をどう扱うかも要件化します。

Searchが返した件数やファセットにも注意します。アプリで認可・チャンク集合を再確認して結果を除外する場合、Search側の集計値はその除外を反映しません。本記事のような構成では、確認前の集計値を閲覧可能な件数として表示せず、必要なら件数表示を省くか、別に集計方法を設計します。

キャッシュは検索語だけをキーにしません。データ所有テナント、認可範囲または利用者、公開世代、クエリ設定などを区別し、返却時にも権限と公開版を再確認します。権限剥奪後も古いキャッシュを返せば、Search側だけ修正しても問題は残ります。

6.3 詳細取得APIと直接アクセスを抜け道にしない

Documents Getはキーで文書を取得するAPIで、通常のSearch POSTと同じ filter を指定するAPIではありません。検索一覧では認可していても、詳細表示が任意の chunkId を受け取ってGetするだけなら、この独自フィルター方式を迂回します。[27]

詳細取得も、共通の認可条件にチャンクID条件を加えた検索にするか、取得前後にアプリで文書単位の認可を必ず行います。原本リンク、サジェスト、エクスポートなどの経路も同様です。

この構成では、エンドユーザーへSearchのAPIキーや検索アプリのアクセストークンを配布しません。フィルターを任意に外せる直接アクセスを許さず、サーバー側の経路へ集約します。ネットワークの閉域化だけでは、この文書単位の認可を代替できません。[8][30]

6.4 架空シナリオ:「管理者の画面では直っています」

利用者:「権限を外した後も、さっきの検索結果が開けます」

保守担当:「管理画面ではグループから外れています」

認証担当:「利用者側はログインしたままですか。検索結果のキャッシュも確認しましょう」

この例では、新たなSearchへの問い合わせでは対象文書が除外されていました。しかし、既存セッションにはアプリが変更前の検索結果をキャッシュから返していました。管理画面の設定確認だけでは、この経路を見つけられません。

再現試験には、承認された試験用アカウントを使います。権限あり・権限なしの状態に加え、権限ありから剥奪へ変化した状態を確認します。利用者のアクセストークンを担当者間で受け渡して再現する運用にはしません。

試験 確認する経路
閲覧できる利用者が検索 一覧と原本が期待どおり表示される
権限のない利用者が同じ検索語で検索 本文だけでなくタイトル・引用・件数も確認する
権限剥奪後に、既存セッションで再検索 認可情報の更新とキャッシュ返却を確認する
検索一覧を通さず、保存した詳細URLを開く 詳細取得と原本取得でも認可される

剥奪から停止までの許容時間は、試験前に決めます。インデックス更新、グループ情報の反映、アプリの認可キャッシュを別々に追い、どの段階まで反映されたかを記録します。再ログインした試験だけで、既存セッションも同じ挙動になるとは判定しません。

7. ランキングは「候補を拾う段階」と「並べ直す段階」に分ける

順位を調整する前に、正解文書が候補へ入っているかを確認します。候補取得と再ランキングを分けて見ると、調整する場所を絞れます。

上図は、Search内部の検索・ランキングを示したものです。アプリが利用者やLLMへ返す前には、6節の認可・公開版の確認を行います。後段の業務ブーストは9節で説明します。[14][15]

方法 担当すること 注意点
全文検索 単語・語句の一致に基づく候補取得 アナライザーと表記に影響される
ベクトル検索 埋め込み空間で近い候補の取得 識別子の厳密一致を保証しない
ハイブリッド検索 全文・ベクトルの結果を統合 両方の候補数や重みを評価する必要がある
セマンティックランカー 取得済み候補を文脈で並べ直す 元の候補にない文書は、この再ランキングでは追加できない

7.1 RRFは、BM25とベクトルの生スコアの足し算ではない

ハイブリッド検索では、全文検索とベクトル検索の順位をRRFで統合します。異なる尺度のスコアを、そのまま加算するわけではありません。[14]

したがって、全文検索時の @search.score と、ハイブリッド検索時の @search.score を、同じしきい値で評価しません。

たとえば全文検索で使っていた「スコア5以上」という条件を移植すると、ハイブリッド検索の候補を落としてしまう可能性があります。

7.2 topとkを混同しない

セマンティックランカーが再ランキングするのは、最大50件の候補です。公式ドキュメントでは、ベクトルを併用する場合に k: 50 を設定するよう案内されています。[15][16]

パラメーター 意味
ベクトルクエリの k ベクトル検索から得る近傍候補数
検索リクエストの top 最終的に返す結果件数

画面に5件表示するからといって、k も5にする必要はありません。この記事では k: 50、top: 5 を出発点にします。

k: 50 で正解が必ず入るわけではありません。これはベクトル側の候補数であり、RRFへ入る全文検索側の候補数や、最終的な統合結果数とは別です。重複やフィルターにより、実際の候補数も変わります。[16]

さらに、アプリで公開版照合や同じ原本の重複排除を行うなら、Searchから5件受け取っても画面には5件残らない場合があります。必要に応じて top を増やして検証しますが、件数をそろえるために認可条件を緩めてはいけません。

7.3 preFilterは候補を確保しやすいが、性能評価が必要

preFilter は、HNSWの探索中に絞り込み条件を適用します。postFilter では候補取得後の絞り込みによって、条件を満たす文書があっても候補から落ちることがあります。[17]

本記事では preFilter を使用します。ただし、対象がごく少数になる条件では、探索負荷やレイテンシーが増える可能性があります。

フィルターを適用する位置で、候補の取りこぼしと処理時間が変わります。HNSW自体も近似探索なので、厳密な近傍集合との差は11.2節の方法で確認します。

本記事はベクトルサブクエリ固有のフィルター上書きを使いません。プレビューの filterOverride を導入する場合は、グローバルな認可フィルターも置き換わる仕様に注意が必要です。[16]

7.4 セマンティックランカーも全文を無制限に読むわけではない

再ランキングには、セマンティック設定で指定したテキストを使います。入力長に上限があり、優先度の低いフィールドや長い本文の後半が切り捨てられる場合があります。[15]

したがって、埋め込みモデルの入力上限に収まっただけで十分とは判断しません。タイトル・見出し・本文の優先順と長さを確認し、必要な例外条件が再ランキングや、その後のLLM入力まで残るかを評価します。埋め込み用、セマンティック処理用、回答生成用のトークン予算は別です。

8. ハイブリッド検索のリクエスト例

次はPython 3.10以降の標準ライブラリだけで、検索本文を組み立てる例です。HTTP送信・認証・埋め込み生成・返却前の認可/チャンク集合の照合は含めません。

query-embedding.json には、信頼する埋め込み生成処理が出力したオブジェクトを保存します。必要な項目は query、embeddingContractId、vector です。vector には実際に生成した1536次元の配列を入れます。

検索語と生成条件をベクトルに添えて、ファイルの取り違えを検出します。この照合で生成元の真正性は検証できないため、ファイルの作成・変更権限も管理します。

import json
import math
import struct
from pathlib import Path
from uuid import UUID


def build_security_filter(authorized_tenant_id, authorized_group_ids):
    # 引数はサーバーが認証・認可を済ませた値だけを渡す。
    # UUIDの形式検証そのものは認証・認可ではない。
    if not isinstance(authorized_tenant_id, str):
        raise ValueError("データ所有テナントIDがありません")
    if not isinstance(authorized_group_ids, list) or not authorized_group_ids:
        raise PermissionError("グループ権限を確認できません")
    if any(not isinstance(value, str) for value in authorized_group_ids):
        raise ValueError("グループIDの形式が不正です")

    tenant_id = str(UUID(authorized_tenant_id))
    group_ids = sorted({str(UUID(value)) for value in authorized_group_ids})
    group_list = ",".join(group_ids)
    return (
        f"tenantId eq '{tenant_id}' "
        "and status eq 'published' "
        f"and allowedGroupIds/any(g: search.in(g, '{group_list}', ','))"
    )


def read_query_vector(path, query, contract_id):
    data = json.loads(Path(path).read_text(encoding="utf-8"))
    if not isinstance(data, dict):
        raise ValueError("埋め込みファイルの形式が不正です")
    if data.get("query") != query or data.get("embeddingContractId") != contract_id:
        raise ValueError("検索語または埋め込み生成条件が一致しません")

    vector = data.get("vector")
    if not isinstance(vector, list) or len(vector) != 1536:
        raise ValueError("検索ベクトルは1536次元の配列にしてください")
    vector32 = []
    for value in vector:
        if isinstance(value, bool) or not isinstance(value, (int, float)):
            raise ValueError("数値以外のベクトル要素があります")
        try:
            number = float(value)
        except (OverflowError, ValueError):
            raise ValueError("ベクトル要素を数値として扱えません") from None
        if not math.isfinite(number) or abs(number) > 3.4028234663852886e38:
            raise ValueError("Edm.Singleで扱えないベクトル要素があります")
        # Edm.Single相当の丸めで、全要素がゼロになるケースも確認する。
        vector32.append(struct.unpack("<f", struct.pack("<f", number))[0])
    if not any(value != 0 for value in vector32):
        raise ValueError("32ビット変換後のゼロベクトルは使用できません")
    return vector32


query = "出張申請で事前承認が必要になる条件"
contract_id = "knowledge-embedding-v1"  # アプリ独自の生成条件ID
query_vector = read_query_vector("query-embedding.json", query, contract_id)

# 下記UUIDは例示用。本番では認可済みコンテキストから渡す。
security_filter = build_security_filter(
    "11111111-1111-4111-8111-111111111111",
    ["22222222-2222-4222-8222-222222222222"]
)
contract_literal = contract_id.replace("'", "''")

payload = {
    "search": query,
    "searchFields": "title,sectionTitle,content",
    "filter": f"{security_filter} and embeddingContractId eq '{contract_literal}'",
    "vectorFilterMode": "preFilter",
    "vectorQueries": [
        {
            "kind": "vector",
            "vector": query_vector,
            "fields": "contentVector",
            "k": 50,
            "weight": 1.0
        }
    ],
    "queryType": "semantic",
    "semanticConfiguration": "content-semantic",
    "semanticErrorHandling": "fail",
    "select": "chunkId,parentId,revision,publicationId,indexGeneration,embeddingContractId,"
              "title,sectionTitle,content,sourceUri",
    "top": 5
}

Path("query.json").write_text(
    json.dumps(payload, ensure_ascii=False, allow_nan=False),
    encoding="utf-8"
)

この例ではIDをUUIDに限定しています。tenant-a や group-a などの概念説明用の値は受け付けません。インデックスへ保存するIDも、同じ小文字・ハイフン付きの表記へ統一します。公開文書やユーザー個別権限は、このグループ専用の例とは別に設計してください。

モデルを変更するときは、別フィールドまたは別インデックスで比較します。生成条件IDの照合は、移行中に誤った条件のベクトルを検索へ渡す事故を検出するために使います。

生成した query.json を本文として、次のAPIへ送ります。[16][18]

POST {{endpoint}}/indexes/knowledge-v1/docs/search?api-version=2026-04-01
Authorization: Bearer {{token}}
Content-Type: application/json

セマンティックランカーを利用できるリージョン・サービス設定・課金プランも確認してください。[15]

この例は semanticErrorHandling: fail を明示し、セマンティック処理の失敗を通常結果として扱わない方針です。可用性を優先して partial を採用する場合は、部分応答の理由・種類とスコアの有無を判定し、通常のセマンティック検索と区別して記録します。存在しない再ランキングスコアを0で補って、関連性なしと判定してはいけません。[18]

HTTP接続・読取りのタイムアウト、埋め込みを含む全体の待ち時間上限、再試行回数も別途設定します。フォールバックを実装する場合も認可フィルターと公開版照合は維持します。

この例の status eq 'published' は、承認済み文書の絞り込みであって、有効版の自動選択ではありません。旧版も published のまま残っていれば検索対象になります。更新処理と公開ルールの両方が必要です。

「最新版が上に出ないからorderbyを足す」は別の設計になる

orderby で更新日時順にすると、通常の関連度順とは別の並びになります。また、セマンティックランキングにフィールド指定の並び替えを組み合わせると、HTTP 400になると説明されています。[19]

「新着順で一覧を見たい」のか、「質問に合う文書の中で新しいものを優先したい」のかを分けます。前者はソート、後者は対象条件とランキングの設計です。

9. スコアリングプロファイルは、業務要件を小さく反映する

架空のレビュー場面です。

利用部門:「古い資料が上に来るので、新しいものを優先してください」

担当SE:「更新日時が新しい資料ですか。それとも、承認済みの有効な手順ですか」

利用部門:「実作業に使える方です。会議メモは新しくても、手順としては使えません」

この確認をせずに更新日時のブーストを強くすると、今朝保存されたメモが、現在も有効な正式手順より上に来る設計になり得ます。

まず公開状態・有効版・文書種別で検索対象を決めます。手順書と会議メモを区別するなら、documentType などの専用フィールドを追加します。3節の定義には、まだ文書種別のフィールドはありません。

重みを変えるレビューでは、「直したい質問」と「今は正しく出ている質問」を並べます。指摘された一問だけ順位が上がっても、別の重要な手順が落ちていないか確認できるようにします。何を正解とするかは文書の業務責任者と合意し、パラメーター調整の担当者だけで決めません。

9.1 タイトル一致と新しさを評価する設定例

次は、インデックス定義へ追加する scoringProfiles の断片です。これだけをインデックス全体の置き換え本文として送信しないでください。取得した既存定義へ組み込み、他の定義を保持して更新します。

複数担当者やデプロイ処理が設定を変更する場合は、取得したETagを使う条件付き更新を採用し、競合したら最新定義を読み直して差分を適用します。別の担当者が追加した設定を、古い定義で上書きしないためです。[28]

{
  "scoringProfiles": [
    {
      "name": "title-and-recency",
      "text": {
        "weights": {
          "title": 3,
          "sectionTitle": 2,
          "content": 1
        }
      },
      "functions": [
        {
          "type": "freshness",
          "fieldName": "sourceUpdatedAt",
          "boost": 1.5,
          "interpolation": "linear",
          "freshness": { "boostingDuration": "P90D" }
        }
      ],
      "functionAggregation": "sum"
    }
  ]
}

検索リクエストには "scoringProfile": "title-and-recency" を加えます。

重み3・2・1、ブースト1.5、期間90日は比較用の提案値です。効果を測定して採用を判断します。タイトルを3にしたから最終スコアが常に3倍になる、という意味ではありません。[7][20]

sourceUpdatedAt は、取り込みを実行した時刻とは分けます。再構築のたびに全件を「今更新された文書」にしてしまうと、新しさを評価する意味が変わります。

9.2 再ランキング後のブーストと、並べ替えに使うスコアを分ける

現在の仕様では、セマンティック検索で指定したスコアリングプロファイルの関数を、再ランキング後のスコアにも反映できます。rankingOrder は、そのブースト後のスコアを最終的な並べ替えに使うかを指定します。[7][21]

項目 役割
@search.score 初段の検索スコア。検索方式によって尺度が異なる
@search.rerankerScore セマンティック再ランキングのスコア
@search.rerankerBoostedScore スコアリング関数を反映した再ランキング後のスコア

3節は RerankerScore を指定し、通常の再ランキングスコアで並べます。関数のブーストを最終順位に反映する場合は、BoostedRerankerScore へ変更します。RerankerScore を指定した場合も、応答にはブースト後のスコアが含まれることがあります。[21]

同じ検索リクエストで scoringProfile も指定して比較します。後段でのブースト対象はスコアリング関数です。テキストフィールドの重みだけで構成したプロファイルには、この後段ブーストはありません。[21]

RerankerScore を指定しても、スコアリングプロファイルを指定した場合の初段への影響まで無効化されるわけではありません。比較は「プロファイルなし」「同じプロファイル+RerankerScore」「同じプロファイル+BoostedRerankerScore」を分けます。「プロファイルなし」の基準では、リクエストだけでなくインデックスの defaultScoringProfile も確認します。[20]

また、通常の @search.rerankerScore は0~4ですが、@search.rerankerBoostedScore は同じ範囲に収まりません。どちらも「回答が正しい確率」として扱わないでください。[15][21]

10. 再構築は、切り替えより差分の追随が難しい

フィールド名・型・一部の属性・インデックス時のアナライザーを変更する場合は、再構築が必要です。一方、フィールド追加、スコアリングプロファイルやセマンティック設定の変更などは、再構築せずに行えるものがあります。[4]

ただし、新しいフィールドを追加しても、既存文書の値が勝手に埋まるわけではありません。原本からの再投入・再処理を別途計画します。

本番では、旧インデックスを残して新インデックスを作り、検索結果と更新状態を確認して切り替える方法が考えられます。

手順 確認すること
knowledge-v2 を作成 スキーマと生成条件を固定したか
原本から全件投入 失敗文書・権限欠落がないか
構築中の差分を追随 スナップショット境界から更新・削除・権限剥奪を再生したか
新旧を比較 回帰質問、件数、権限、応答時間を確認したか
検索先を切り替え 切り替え期間中の新旧混在を許容できるか
旧版を保持 戻す場合のデータ鮮度を維持できるか

差分追随には基準点が必要です。原本のスナップショットに対応するイベント位置を記録し、そこからの変更を再生します。その仕組みを用意できない場合は、原本更新を止めるメンテナンス時間を設けて整合を取る方法を比較します。

投入先ごとの処理済み位置と失敗一覧を保持し、「差分ジョブが一度成功した」だけで追随完了としません。原本が継続更新される場合は、どの時点まで反映した状態を公開するかも決めます。

公開された開発者の声として、2023年10月26日のIssue #865では、インデックスを削除して再作成しても、取り込み側がファイルを「変更なし」と判定してスキップし、インデックスが空になるという報告があります。[36]

当時のサンプル実装に関する報告で、現在の版での再現は未確認です。再構築では、原本の変更履歴と投入先ごとの処理済み記録を分けて管理します。新インデックスへの初回投入が、旧インデックスの処理済み記録を理由に省略されないことを確認してください。

10.1 インデックスエイリアスを使う

確認日時点では、インデックスエイリアスは安定版APIで利用できます。たとえば検索アプリは knowledge-live を参照し、その参照先を変更します。[22]

PUT {{endpoint}}/aliases/knowledge-live?api-version=2026-04-01
Authorization: Bearer {{token}}
Content-Type: application/json

{
  "name": "knowledge-live",
  "indexes": ["knowledge-v2"]
}

1つのエイリアスが指せるインデックスは1つです。また、インデクサーの targetIndexName にエイリアスを指定することはできません。[22]

公式ドキュメントでは、エイリアス変更の反映に最大10秒かかる可能性が示されています。アプリ全体の切り替え完了を保証する時間ではありません。進行中のリクエスト、キャッシュ、切り戻しの期間を考慮して、旧インデックスを保持します。[22]

Push方式の投入ワーカーには、書き込み先の物理インデックス名を持たせます。検索用エイリアスの切り替えで、再試行の投入先まで変わることを防ぐためです。

エイリアスだけの切り替えは、検索リクエストの互換性を保てることが前提です。エイリアスはアプリ側の埋め込み生成設定まで変更しません。新旧でモデル・次元数・生成条件IDが異なれば、旧設定で作った検索ベクトルを新版へ送る問題が残ります。[22][32]

その場合は、物理インデックス名・埋め込み生成設定・検索設定・公開台帳の参照世代を一つの構成として切り替えます。各リクエストは開始時に選んだ構成を処理完了まで使います。ロールバックも、この組で戻します。

分割方法を変えて再構築すると、同じ原本でも新旧インデックスの publicationId が変わります。公開台帳を世代ごとに管理しなければ、一方を公開した途端に他方の結果をすべて除外することになります。旧世代も公開可能かどうかは、差分追随と次節の切り戻し条件で判断します。

10.2 「戻せる」は、旧インデックスが残っているだけでは足りない

新インデックスへ切り替えた後、旧インデックスへの更新を止めていたとします。

そこでロールバックすると、新たに廃止した文書や、権限を剥奪した文書が復活する可能性があります。

戻す場合にも、更新・削除・権限変更の差分を追随させる必要があります。旧スキーマでは新しいデータを表現できない場合もあるため、ロールバック可能な期間と条件を決めます。

10.3 複数チャンクの入れ替えは一括トランザクションと考えない

新チャンクを登録してから旧チャンクを削除する方式には、両方が見える期間があります。逆に先に旧チャンクを消すと、検索できない期間ができます。

isCurrent のようなフラグを設けても、複数チャンクの更新が一斉に検索結果へ反映される保証にはなりません。

方針 利点 代償
新版登録後に旧版削除 検索できない時間を抑えやすい 一時的な新旧混在を扱う必要がある
更新中は対象文書の返却を止める 不完全な版の返却を防ぎやすい 一時的に検索対象が減る
世代別インデックスで切り替える 大きな変更をまとめて検証できる 並行保持の容量と差分追随が必要

どれを選ぶかは、旧手順を返すリスクと一時的に返さないリスクを比較して決めます。常に最新の権限が必要な場合は、どの方式でも認可側の対策が別途必要です。

10.4 架空シナリオ:「切り替えは完了しました。あとは監視をお願いします」

切り替え担当:「新しいインデックスへ切り替えました。検索も返っています」

夜間の当番:「戻す手順はエイリアスだけですか。今回は埋め込みモデルも変えていますよね」

切り替え担当:「アプリの設定を戻す手順が抜けていました。追記します」

10.1節のように生成条件を変更しているなら、参照先だけを戻しても構成がそろいません。引き継ぎには、作業完了の連絡とともに次の情報を残します。

引き継ぐもの 記録する内容
稼働する構成 インデックス名、埋め込み設定ID、検索設定の版、公開台帳の参照世代
差分追随の到達点 どのイベント位置まで反映したか、未処理があるか
再現用の質問 許可された試験アカウントと、期待する原本・チャンク集合
中止・切り戻し条件 権限外の表示、旧版混入、品質低下、遅延などの判定基準
戻す構成と判断者 旧構成の保存先、鮮度確認の記録、実施判断者と連絡先

中止や切り戻しのしきい値、観測時間、判断者は切り替え前に決めます。夜間担当へ渡す時点で未決なら、引き継ぎ事項として明記し、判断者を決めてから切り替えます。

11. 品質評価を「何となくよくなった」で終わらせない

最初のレビュー用として、たとえば50問程度を用意します。実際の問い合わせから、次の種類を含めて選びます。

調整に使う質問と、最後に確認する質問を分け、同じ質問に合わせ込みすぎないようにします。比較時は原本の版、権限条件、埋め込み条件、インデックス設定、質問集合を固定します。freshnessを使う比較は時間経過で条件が変わるため、比較時刻も記録します。

質問の種類 例 見ること
文書名に近い質問 出張申請手順 基本的な到達性
言い換え 出張前に誰の承認を取るか ベクトル・セマンティックの効果
識別子 AB-1200-Xの復旧方法 誤った型番の混入
例外条件 緊急時も事前承認が必要か 条件と結論の分断
旧版と新版 変更後の申請先 旧チャンクの残存
権限外の情報 他部署の限定手順 本文・件数・キャッシュの露出
正解がない質問 未導入製品の社内手順 無関係な結果を採用しないか

比較は、全文検索、ベクトル検索、ハイブリッド検索、ハイブリッド+セマンティックの順で行います。どの段階で改善・悪化したかを追えるよう、一度に変える条件を絞ります。

指標も目的に合わせます。

指標 確認すること
Hit@5 上位5件に、正解が少なくとも1件ある質問の割合
Recall@50 定義した正解集合のうち、上位50件に入った割合
MRR 各質問で最初の正解順位の逆数を求め、その平均を取る。評価範囲に正解がなければ0
nDCG@10 段階的な関連度を付けたときの上位10件の順位品質
更新反映時間 原本の変更から検索確認までの時間
p95応答時間 対象リクエストの95%が収まる応答時間。計測区間と期間も記録する

正解を原本文書で評価するのか、チャンクで評価するのかも固定します。同じ原本のチャンクが5件並んだ結果を、「5件とも正解だから優秀」と扱うと、他の必要文書が出ない問題を見落とします。

正解が1件でも出たかを測るHit率と、定義した正解集合の回収率は別の指標です。正解集合の付与範囲を明記し、未発見の正解を含む全体のRecallを測定できたとは断定しません。正解が存在しない質問はRecallの分母を定義できないため、別群で評価します。

11.1 ベクトルで候補が返ることと、答えがあることは別

ベクトル検索は近傍を探すため、十分な文書があれば、関連性が低い候補でも指定数まで返ることがあります。フィルターや対象件数により件数は変わりますが、結果が空でないことは回答可能性の根拠になりません。[29]

正解のない質問では、無関係な手順を根拠として採用しなかったかを評価します。スコアの採用基準を設ける場合は検索方式ごとに検証し、しきい値を正解率と同一視しません。

11.2 HNSWの取りこぼしと、埋め込みの限界を切り分ける

この記事の未圧縮HNSWフィールドでは、同じベクトル・距離関数・フィルターで、通常の検索と exhaustive: true を比較できます。まずベクトル単独のクエリで、インデックスの内容、preFilter、k、top などをそろえます。全文検索との統合、セマンティック再ランキング、スコアリングプロファイルを外して比較し、近似探索による候補集合の差を調べます。[18][29][33]

厳密な近傍集合に入る文書がHNSWで落ちるなら、探索設定を検討する材料になります。厳密探索でも業務上の正解が上がらない場合は、文書抽出、分割、埋め込み、識別子条件などを調べます。厳密な近傍順位が、業務上の正解順位を保証するわけではありません。

量子化や再スコアリングを導入した後は、その設定をそろえて再評価します。厳密探索を本番へ採用する場合は、全件走査の負荷も測定します。[24][33]

11.3 検索品質とRAGの回答品質を別々に測る

本記事の検索APIが返すのは、回答の根拠となる候補です。RAGでは、この後に選別した情報をLLMへ渡す工程があります。検索上位へ正解文書が入ることと、生成回答がその根拠に忠実であることは別の評価項目です。[34]

回答生成も行うなら、根拠のない断定、例外条件の脱落、出典と記述の対応、答えがない場合の応答を別に評価します。セマンティック検索の answers や captions はインデックス内のテキストからの抽出で、LLMが新しい回答を生成する処理とは異なります。本記事のリクエスト例では、これらの抽出機能も指定していません。[15]

11.4 公開された開発者の声:一問目はよくても、会話が続くと検索語がずれる

2023年9月16日のIssue #641では、PoCを社内向けに実用化していた投稿者が、長い会話では検索用クエリの生成が乱れることがあると報告しています。質問と生成された検索語の履歴を、回答を含む会話履歴とは分ける案を提示しています。投稿は機能改善の提案で、Azure AI Search自体の障害報告ではありません。[37]

8節の例は固定した検索語を使います。会話履歴から検索語を生成する機能を追加したら、その処理も評価します。サンプルの公式ガイドも、検索語生成・検索結果・回答生成を分けて調べる手順を示しています。[38]

「同じ質問を入れたのに再現しない」ときは、質問文だけでなく、その直前までの会話が同じかを確認します。調査に必要な範囲で、次を一つのリクエストIDへ対応付けます。

比較するもの 判断の手掛かり
利用者の質問と、実際にSearchへ送った検索語 必要な型番や条件が検索前に落ちていないか
検索語と埋め込みの生成条件 別の質問用ベクトルや意図しない生成条件を使っていないか
Searchの候補と、アプリが残した候補 チャンク集合の照合・重複排除で何が落ちたか
LLMへ渡した根拠と生成回答 根拠の選別と回答生成のどちらを直すか

会話履歴や根拠本文は常時すべて記録する前提にはしません。調査用データの閲覧権限と保持期間を決め、可能なら許可された検証データで再現します。同一条件でも出力の変動はあり得るため、一度の成功だけで改善を確定しません。

12. 監視と費用は、検索本体の外側も含めて見る

12.1 正常応答だけでは品質を確認できない

層 記録・確認したい情報
原本 更新版、削除、権限変更の時刻
取り込み 文書単位の失敗、再試行、未処理件数、更新遅延
検索 QPS、レイテンシー、スロットリング、結果件数
アプリ 埋め込み待ち、検索待ち、認可処理、タイムアウト
品質 評価質問での退行、旧版混入、出典不一致

Azure AI Searchのメトリックで確認できるものと、アプリ側で計測するものを分けます。Search側の平均レイテンシーだけで、アプリ全体のp95応答時間は分かりません。[23]

調査用には、リクエストID、要求先のエイリアスまたは物理インデックス名、クエリ設定の版、取得したチャンクID・原本の版・publicationId・indexGeneration を記録します。

エイリアスの設定を別途取得しただけでは、切り替え中にその検索要求が実際に参照した世代を断定できません。結果に付けた世代値と切り替え履歴を併用し、結果0件などで世代を特定できない場合は不明として記録します。

質問本文や検索結果には機密情報が含まれる可能性があるため、全文ログを常時保存する前に、記録範囲・保持期間・閲覧権限を決めます。

12.2 容量試算で、生ベクトルと実際の使用量を混同しない

説明用に、100万チャンク、1チャンクにつき1536次元のベクトル1本、各要素4バイトと仮定します。

1,000,000 × 1,536 × 4
= 6,144,000,000バイト
≒ 6.14 GB(10進)

これは単純な生ベクトル量の計算です。HNSWの構造、本文、メタデータ、検索用索引、保存用のコピーなどを含む実際のストレージ使用量や、必要SKUを示すものではありません。

代表データを投入し、実際のインデックスサイズとベクトル領域の使用量を測定します。本文中心の短文だけでなく、長文・表・大きなACLを含む代表データを使います。再構築で新旧を保持する期間と、更新と検索を同時に行う時間帯も測定対象です。

ベクトルの量子化、次元数の変更、保存用コピーの削減は別の施策です。特に stored: false は検索用ベクトル索引のメモリ削減と同義ではなく、部分更新の条件も変えます。先に5節の更新経路を確認し、品質・保存量・更新時間を比較します。[24][26]

12.3 Dedicatedの費用はSUだけで完結しない

Dedicated構成では、Search Unitはレプリカ数とパーティション数の積です。[25]

2レプリカ × 2パーティション = 4 SU

ここではSUの計算だけを示しています。レプリカ数は、必要な冗長性と契約するSLAの条件も確認して決めます。

パーティションは容量や投入性能、レプリカはクエリ負荷や可用性を検討する際の主な調整対象です。ただし、実際のボトルネックを測定せずに増やしても、期待する効果が出るとは限りません。

見積もりには、少なくとも次を含めます。

  • Azure AI Search本体の稼働費用。
  • セマンティックランカーの利用料。
  • 文書登録時と検索時の埋め込み生成費用。
  • 文書抽出・OCRなどの加工費用。
  • ログ、ストレージ、ネットワーク関連費用。
  • 再構築中に新旧インデックスを保持するための容量。

同じサービス内にインデックスを2つ作っただけで、本体料金が自動的に2倍になるわけではありません。ただし、必要容量や負荷が増えてSU追加が必要になれば、本体費用も増えます。

なお、確認日時点ではServerless Developerはプレビューです。Dedicatedと同じ「レプリカ数×パーティション数」の課金モデルではなく、公式には本番用途を推奨しないとされています。[25]

月額費用は、対象リージョン・SKU・負荷条件を決め、採用時の料金で見積もります。

12.4 架空シナリオ:「Searchは速いのに、利用者は遅いと言っています」

インフラ担当:「Searchのレイテンシーは、いつもの範囲です」

利用部門:「でも画面は待たされたままです。増強できませんか」

アプリ担当:「遅かった操作の時刻を教えてください。検索前後も含めてログを追います」

Searchのメトリックだけでは、検索語生成、埋め込み生成、認可、アプリ内の待ち行列、回答生成の待ち時間は分かりません。まず同じリクエストの工程別時刻を対応付けます。[23]

遅くなっている区間 次に確認すること
Searchへ送信する前 検索語生成、埋め込み、認可、アプリの待ち行列と再試行
Searchへの要求から応答まで 同時間帯のクエリ条件、取り込み負荷、スロットリング、アプリ側通信時間
Search応答後から回答表示まで 根拠の選別、LLM入力の長さ、生成処理、フロントエンドへの転送
一部の要求だけ長い その要求の条件、再試行、同時実行数、キャッシュ状態

各工程のp95を足した値は、アプリ全体のp95と一致するとは限りません。全体の所要時間はリクエスト単位で測り、工程別の計測は原因を絞るために使います。

増強を提案するときは、短縮できる見込みの工程、追加費用、変更後の測定条件を示します。埋め込み生成で待っているなら、Searchのレプリカを増やしても、その待ち時間は短くなりません。

13. 受入試験と、障害を引き継ぐための資料

13.1 検索できるかに加え、変更した後を試す

次は、本記事の設計案を検証するための受入試験表です。期待結果は業務要件に合わせて確定し、実施結果・証拠の保存先・確認者を追記して使います。

試験 操作・再現条件 確認する結果
文書の短縮 一つの原本のチャンク数を減らす 旧チャンクが公開対象に残らず、削除対象の照合も完了する
同じ版の再分割 原本の版を維持して分割設定を変える 新旧の publicationId を区別し、未公開のチャンク集合を返さない
本文の変更 手順と関連する用語を変更する 本文とベクトルが同じ生成対象に対応する
ACLだけの変更 閲覧グループを変更し、古い本文投入の再試行も発生させる ベクトルを保持し、古いACLが再公開されないことを確認する
原本の削除 原本を削除する 対象チャンクの回収と返却停止が定めた時間内に完了する
投入の部分失敗 検証用応答で一部の文書を失敗させる ジョブが全件成功と表示されず、失敗分を追跡できる
重複・順序逆転 同一イベントの再実行、古いイベントの遅着を再現する 重複や公開状態の巻き戻りを起こさない
権限剥奪 既存セッションとキャッシュを残して権限を外す 合意した反映時間内に、一覧・詳細・原本の各経路で制限する
再構築・切り戻し 分割方式を変え、構築中にも更新・削除・権限変更を発生させる 世代ごとの公開台帳と差分の到達点を追え、戻す構成でも制約を満たす
正解のない質問 対象に存在しない手順を尋ねる 関連の薄い文書を正解扱いせず、決めた応答方針になる

部分失敗や順序逆転は、検証環境や応答を差し替えられる試験で再現します。ローカルで確認した範囲と、Azureへ接続して確認した範囲を分けて記録します。

13.2 誰が何をもって完了とするか

次の分担は一例です。一人が兼務する場合でも、確認する役割は分けて残します。

役割 完了判断に必要な確認
文書の業務責任者 正しい原本・有効版・例外条件・閲覧範囲を確定する
取り込み担当 登録・削除・再試行の結果と、チャンク集合の対応を確認する
アプリ/AI担当 実際の検索条件、候補選別、キャッシュ、生成回答を確認する
インフラ担当 権限・接続・容量・負荷・監視の状態を確認する
リリース/障害対応責任者 影響範囲、暫定対応、再開・中止・切り戻しの判断を記録する

「取り込みジョブが終了した」だけでは、業務責任者が確認する原本の正しさまで確定しません。復旧報告には、何を直したかに加え、どの質問・権限条件・チャンク集合で確認したか、未確認の範囲があるかを残します。

13.3 そのまま使える問い合わせ票

以下は記入用のひな形です。未確認の項目は、推測せず「未確認」と記入します。

発生日時・タイムゾーン:
対象環境・画面/API:
リクエストID:
対象テナント・認可条件の識別情報:
検索語と、再現に必要な直前の操作/会話:
期待する原本・版・該当箇所:
実際に返った原本ID・チャンクID・publicationId:
要求先インデックス/エイリアス・検索設定の版:
最後に正常を確認した時刻:
直前の文書更新・権限変更・リリース:
既存セッション/キャッシュの有無:
影響範囲と業務上の期限:
実施済みの暫定対応:
確認済みの事実と証拠の保存先:
未確認事項・次に確認する担当:
再開/切り戻しを判断する担当:

ここへAPIキー、アクセストークン、権限外の文書本文を貼り付けません。詳細な証拠は閲覧権限のある保存先へ置きます。

引き継ぐときは、この票に再現できた検索条件と未確認事項を残します。次の担当者が、利用者への聞き取りからやり直さずに調査を続けられます。

参考資料

確認日:2026年9月24日。1~34・39・40はMicrosoft公式ドキュメント、35~37はAzure-Samplesリポジトリの質問・報告、38は同サンプルの公式ガイドです。公開投稿は当時の実装についての報告として紹介しています。本文中の番号に対応します。

  1. Data plane REST API versions
  2. Search indexes in Azure AI Search
  3. Define index projections
  4. Update or rebuild an index
  5. Quickstart: Keyless connection
  6. Create a full-text query
  7. Indexes - Create:2026-04-01
  8. Security filter pattern
  9. Analyzers for linguistic and text processing
  10. Load an index
  11. Documents - Index:2026-04-01
  12. Service limits
  13. Change and delete detection for Azure Storage
  14. Hybrid search scoring:RRF
  15. Semantic ranking overview
  16. Create a hybrid query
  17. Vector query filters
  18. Documents - Search Post:2026-04-01
  19. Add semantic ranking to queries
  20. Add scoring profiles
  21. Use scoring profiles with semantic ranking
  22. Create an index alias
  23. Monitor queries
  24. Choose vector optimization
  25. Plan and manage costs
  26. Eliminate optional vector instances
  27. Documents - Get:2026-04-01
  28. Manage concurrent writes
  29. Create a vector query
  30. Connect using Azure roles
  31. OData search.in function
  32. Create a vector index
  33. Relevance in vector search
  34. Retrieval-augmented generation in Azure AI Search
  35. Discussion #2301:同名PDF更新時の旧チャンク(質問2025年1月24日、回答2025年2月4日)
  36. Issue #865:インデックス再作成時に変更なしとして取り込みが省略される報告(2023年10月26日)
  37. Issue #641:会話履歴を使った検索語生成の改善提案(2023年9月16日)
  38. RAG chat: Customizing the chat app
  39. Shape search results:ページングと並び順
  40. Troubleshoot OData collection filters:文字列の比較演算子
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?