こんにちは、建設AIエンジニアの内田沙織です。
建設現場では、数万枚に及ぶ図面や膨大な施工記録の中から、必要な情報を瞬時に取り出す必要があります。粉塵舞う現場でタブレットを操作する監督にとって、「検索しても出てこない」はただのストレスではなく、工期の遅れや安全リスクに直結する死活問題です。
最近はRAG(Retrieval-Augmented Generation)の構築において、Azure AI Searchを採用するケースが増えてきました。しかし、導入したものの「思ったようなドキュメントがヒットしない」「精度が良いのか悪いのか判断がつかない」という相談をよく受けます。
検索システムの精度調整において、最も避けるべきは「なんとなく良くなった気がする」という感覚的な評価です。
本記事では、Azure AI Searchのセマンティックランク付け機能を対象に、Python SDKを用いて検索精度を「数値化」し、論理的にチューニングする手法を共有します。ブラックボックスになりがちな検索エンジンの挙動を可視化し、エンジニアとして自信を持って「精度が向上した」と言える状態を目指しましょう。
なぜ「デフォルト設定」では不十分なのか:セマンティック検索の落とし穴
Azure AI Searchは非常に優秀なPaaSですが、「とりあえずセマンティック検索をオンにすれば魔法のように解決する」わけではありません。特に専門用語が飛び交う建設業界や、独自の文書構造を持つデータセットにおいては、デフォルト設定のままでは期待通りの関連性(Relevance)が出ないことが多々あります。
ハイブリッド検索とセマンティックリランクの仕組み
まず、私たちが扱っている技術の正体を整理しておきましょう。現在の主流は、キーワード検索(BM25)とベクトル検索(kNN)を組み合わせたハイブリッド検索を行い、その上位結果に対して、より高度な言語モデルを用いて並び替えを行うセマンティックリランク(Semantic Reranking)という構成です。
ここで重要なのが、この「組み合わせ」のロジックが進化している点です。Azure AI Searchの最新仕様(公式ドキュメント参照)では、単に両者を混ぜるだけでなく、ハイブリッドランク付けにおけるBM25の検索結果量(リコールサイズ)を明示的に制御したり、ベクトル検索とキーワード検索の重み付けをクエリ単位で調整したりする機能が強化されています。つまり、以前よりも「エンジニアが意図を持って調整すべきパラメータ」が増えており、デフォルト任せではそのポテンシャルを活かしきれないのです。
このリランク処理において、Azure AI Searchはドキュメント内のどのフィールドを「意味のある情報」として重視するかを決定しています。デフォルトでは、インデックス作成時に自動的にフィールドが割り当てられることもありますが、これが意図しない設定になっているケースが散見されます。
精度低下を招く典型的な設定ミス
建設ドキュメントを例に挙げると、「工事名称(Title)」よりも「特記事項(Content)」の方に重要な制約条件が書かれている場合があります。しかし、セマンティック構成でタイトルの重み付けが過剰に高いと、中身が薄いドキュメントでもタイトルがクエリにマッチしているだけで上位に来てしまいます。
また、社内用語や略語がキーワードフィールドに入っているのに、それがセマンティックランク付けの対象から漏れていることもあります。これらは設定を見直せば改善できる問題ですが、そもそも「何が悪いのか」を特定するためには、現状を正しく計測する必要があります。
チューニングにおける「定量評価」の重要性
「昨日より検索結果が良くなった」と上司やクライアントに報告する際、何を根拠にしますか? 特定のクエリ1つだけを見て判断するのは危険です。あるクエリで改善しても、別のクエリで改悪されている可能性があるからです。
だからこそ、NDCG(Normalized Discounted Cumulative Gain) のような指標を用いて、データセット全体でのスコアを算出する評価パイプラインが必要になります。感覚値ではなく、数値に基づいた改善サイクルを回すことこそが、実用的な検索システム構築の鍵となります。
評価環境の構築:検索精度を「数値化」するPythonスクリプト
では、実際に手を動かしていきましょう。ここではPythonを用いて、Azure AI Searchの検索結果を評価する環境を構築します。感覚的な「なんとなく良い検索結果」から脱却し、数値に基づいたエンジニアリングを行うための第一歩です。
Azure SDK for Pythonのセットアップ
まずは必要なライブラリをインストールし、クライアントを初期化します。ここでは azure-search-documents の最新版を使用します。
# 必要なライブラリのインストール
# pip install azure-search-documents azure-core numpy pandas
import os
from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient
from azure.search.documents.models import VectorizedQuery
import numpy as np
import pandas as pd
# 環境変数から設定を読み込む(セキュリティ対策)
service_endpoint = os.environ["AZURE_SEARCH_SERVICE_ENDPOINT"]
index_name = os.environ["AZURE_SEARCH_INDEX_NAME"]
key = os.environ["AZURE_SEARCH_API_KEY"]
credential = AzureKeyCredential(key)
client = SearchClient(endpoint=service_endpoint,
index_name=index_name,
credential=credential)
テストデータセット(Golden Dataset)の定義構造
評価には「正解データ」が必要です。これをGolden Datasetと呼びます。クエリと、そのクエリに対してヒットすべきドキュメントID、そしてその重要度(relevance score)を定義します。
建設現場の例で言えば、「高所作業 安全対策」というクエリに対し、安全マニュアル(ID: DOC-001)は必須(スコア3)、朝礼ガイド(ID: DOC-005)は関連あり(スコア2)、資材リスト(ID: DOC-099)は無関係(スコア0)、といった具合です。
# Golden Datasetの例
# query: 検索クエリ
# expected_docs: {ドキュメントID: 重要度スコア(3=高, 2=中, 1=低)}
golden_dataset = [
{
"query": "高所作業 安全対策",
"expected_docs": {"DOC-001": 3, "DOC-005": 2, "DOC-010": 1}
},
{
"query": "コンクリート 打設 手順",
"expected_docs": {"DOC-200": 3, "DOC-201": 3, "DOC-205": 1}
},
# ... 他のテストケースを追加
]
NDCG@10(正規化割引累積利得)計算の実装
NDCGは、検索結果の上位に正解が含まれているかだけでなく、「順位」も考慮した指標です。1位に正解がある方が、10位にあるより高く評価されます。
以下は、Azure AI Searchに対して検索を実行し、その結果とGolden Datasetを突き合わせてNDCG@10を算出する関数です。
注意: ベクトル検索に使用するEmbeddingモデルは、OpenAIの最新モデル(text-embedding-3シリーズ等)の使用を推奨します。古いモデルは廃止または非推奨となっている可能性があるため、実装前に必ず公式ドキュメントで利用可能なモデルを確認してください。
def calculate_dcg(scores):
"""Discounted Cumulative Gainを計算"""
return np.sum([
(2**score - 1) / np.log2(i + 2)
for i, score in enumerate(scores)
])
def calculate_ndcg(expected_docs, search_results, k=10):
"""NDCG@kを計算"""
# 検索結果の上位k件のIDを取得
retrieved_ids = [doc['id'] for doc in search_results[:k]]
# 検索結果の各ドキュメントに対する重要度スコアを取得(正解になければ0)
relevance_scores = [expected_docs.get(doc_id, 0) for doc_id in retrieved_ids]
# 理想的な順序(IDCG用):重要度の高い順にソート
ideal_scores = sorted(expected_docs.values(), reverse=True)
# k個に満たない場合は0で埋める
if len(ideal_scores) < k:
ideal_scores += [0] * (k - len(ideal_scores))
ideal_scores = ideal_scores[:k]
dcg = calculate_dcg(relevance_scores)
idcg = calculate_dcg(ideal_scores)
return dcg / idcg if idcg > 0 else 0
# 評価実行ループ
ndcg_scores = []
for case in golden_dataset:
query = case["query"]
# ここではハイブリッド検索 + セマンティックランク付けを実行
# 重要: ベクトル化にはOpenAIの最新Embeddingモデルを使用してください。
# 旧モデル(text-embedding-ada-002の古いバージョン等)は廃止されている可能性があります。
# vector_query = ...
results = client.search(
search_text=query,
# vector_queries=[vector_query], # ハイブリッド検索時は有効化
select=["id", "title"],
top=10,
query_type="semantic",
semantic_configuration_name="my-semantic-config", # ここをチューニング対象とする
query_language="ja-jp"
)
search_results = list(results)
score = calculate_ndcg(case["expected_docs"], search_results)
ndcg_scores.append(score)
print(f"Query: {query}, NDCG@10: {score:.4f}")
print(f"\nAverage NDCG@10: {np.mean(ndcg_scores):.4f}")
このスクリプトを実行することで、現在の検索設定における「平均スコア」が算出されます。これをベースラインとして、インデックス設定やクエリロジックを変更した際にスコアがどう変化するかを客観的に検証していきます。
実践チューニング1:セマンティック構成(Semantic Configuration)の最適化
ベースラインが決まったら、いよいよAzure AI Searchの中身を調整します。最初に見直すべきは「セマンティック構成」です。
フィールド優先順位(Title/Content/Keyword)の再設計
セマンティックランク付けでは、PrioritizedFields というプロパティで、どのフィールドをタイトル、コンテンツ、キーワードとして扱うかを定義します。
例えば、建設現場の技術文書の場合、ファイル名(Title)よりも、施工要領書の本文(Content)や特記事項の方がクエリとの意味的関連性が高いケースは珍しくありません。逆に、FAQデータや機器マニュアルであれば、質問や機器名(Title)の一致が最重要になります。
semantic_configuration_nameの動的切り替え実装
インデックス定義を更新するPythonコードの例です。ここでは、title_field の優先度をあえて下げ、content_fields を重視する構成を作成してみます。
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.indexes.models import (
SemanticConfiguration,
SemanticPrioritizedFields,
SemanticField,
SemanticSearch
)
# インデックス操作用クライアント
admin_client = SearchIndexClient(endpoint=service_endpoint, credential=credential)
# 既存のインデックスを取得
index = admin_client.get_index(index_name)
# 新しいセマンティック構成の定義
new_config_name = "content-focused-config"
new_config = SemanticConfiguration(
name=new_config_name,
prioritized_fields=SemanticPrioritizedFields(
title_field=SemanticField(field_name="title"), # タイトル
content_fields=[
SemanticField(field_name="content"), # 本文を最優先
SemanticField(field_name="summary") # 要約も考慮
],
keywords_fields=[
SemanticField(field_name="tags"), # タグ情報
SemanticField(field_name="category") # カテゴリ
]
)
)
# インデックスに構成を追加(または更新)
if index.semantic_search is None:
index.semantic_search = SemanticSearch(configurations=[new_config])
else:
# 既存リストに追加
# ※実運用では既存名のチェックが必要
index.semantic_search.configurations.append(new_config)
# インデックス更新
admin_client.create_or_update_index(index)
print(f"Semantic configuration '{new_config_name}' created/updated.")
フィールドマッピング変更によるスコア変動の検証
この設定を適用した後、先ほどのNDCG評価スクリプトの semantic_configuration_name を content-focused-config に変更して再実行します。
もしスコアが上がれば、対象のデータセットでは「タイトルよりも本文の内容重視」が正解だったということです。逆に下がれば、ユーザーはタイトルで検索している傾向が強いのかもしれません。
さらに、Azure AI Searchの最新環境では、セマンティック構成の調整だけでなく、その前段階であるハイブリッド検索のパラメータ制御も精度に大きく影響します。
- 初期検索の質: セマンティックランカーは、初期検索(BM25やベクトル検索)で抽出された上位候補(通常トップ50件)をリランクします。
- 結果量の制御: 公式ドキュメントによると、ハイブリッド検索時にBM25が取得する候補数を制御するパラメータ(
MaxTextRecallSizeなど)や、クエリの重み付けを調整することで、リランク対象となる母集団の質を向上させることが可能です。
したがって、NDCGでの評価を行う際は、セマンティック構成の変更と合わせて、これらの検索パラメータの組み合わせも検証することをお勧めします。この地道な試行錯誤こそが、現場で使える検索システムを作るための近道です。
実践チューニング2:スコアリングプロファイルとの高度な組み合わせ
セマンティックランク付けは「意味」を見ますが、「ビジネス要件」までは汲み取ってくれません。例えば、「古いマニュアルより新しいマニュアルを優先したい」「特定のタグがついている重要文書を上げたい」といった要望です。
これに応えるのが スコアリングプロファイル(Scoring Profiles) です。セマンティック検索と組み合わせることで、意味的関連性を保ちつつ、ビジネスロジックを加味できます。
鮮度(Freshness)やタグ重み付けの関数定義
建設現場では「最新の図面」であることが命です。古い図面で施工したら大惨事になります。そこで、last_modified フィールドが新しいほどスコアが高くなる関数を定義します。
from azure.search.documents.indexes.models import (
ScoringProfile,
ScoringFunctionAggregation,
FreshnessScoringFunction,
FreshnessScoringParameters,
TagScoringFunction,
TagScoringParameters
)
# 鮮度ブースト関数の定義
freshness_func = FreshnessScoringFunction(
field_name="last_modified",
boost=3.0, # スコアを最大3倍にする
interpolation="linear",
parameters=FreshnessScoringParameters(
boosting_duration="P365D" # 1年以内のものを優遇
)
)
# タグブースト関数の定義("important"タグがある文書を優遇)
# ※タグはCollection(Edm.String)である前提
tag_func = TagScoringFunction(
field_name="tags",
boost=2.0,
parameters=TagScoringParameters(
tags_parameter="tags_to_boost" # 検索時にパラメータとして渡す名前
)
)
# プロファイルの作成
scoring_profile = ScoringProfile(
name="freshness-boost",
functions=[freshness_func, tag_func],
function_aggregation=ScoringFunctionAggregation.SUM
)
# インデックスにプロファイルを追加
index.scoring_profiles = [scoring_profile]
index.default_scoring_profile = "freshness-boost" # デフォルトに設定
admin_client.create_or_update_index(index)
ベーススコアとセマンティックスコアの統合ロジック
ここで重要な注意点があります。Azure AI Searchでは、スコアリングプロファイルはベースの検索スコア(BM25など)に影響を与えます。セマンティックリランカーは、このベーススコアで抽出された上位候補(L1)に対して再ランク付け(L2)を行います。
つまり、スコアリングプロファイルを使って「新しい文書」をL1の上位に食い込ませることで、セマンティックリランカーの土俵に乗せやすくするという戦略が有効です。
継続的な改善サイクル:MLOps的な評価パイプラインの自動化
一度チューニングして終わりではありません。ドキュメントは日々追加され、ユーザーの検索トレンドも変化します。例えば建設現場で新しい工法が導入されれば、検索クエリも変わりますし、安全基準の改定で用語の重み付けが変わることもあります。
評価実行の自動化スクリプト
辞書の更新やインデックスの再構築を行うタイミングで、自動的にNDCG評価スクリプトを実行する運用が推奨されます。一般的に、CI/CDパイプライン(GitHub ActionsやAzure DevOpsなど)に評価プロセスを組み込み、スコアが許容範囲を下回った場合にアラートを出す仕組みを構築します。
なお、CI/CDツールやMLOpsプラットフォームの機能は頻繁にアップデートされます。実装の際は、各ツールの公式ドキュメントで最新の連携方法や利用可能なランナー(Runner)の仕様を確認してください。
結果のログ保存と可視化
評価結果はCSVやデータベースに保存し、精度の推移を可視化することが重要です。
# 結果の保存例
results_df = pd.DataFrame({
"date": [pd.Timestamp.now()],
"config_name": ["content-focused-config"],
"avg_ndcg": [np.mean(ndcg_scores)]
})
# 履歴ファイルに追記
results_df.to_csv("search_evaluation_history.csv", mode='a', header=not os.path.exists("search_evaluation_history.csv"))
このように記録を残すことで、「先月の辞書更新から検索精度が0.05ポイント向上しました」と定量的に報告できるようになります。エンジニアとして、感覚値ではない客観的な成果を示すための強力な武器となります。
まとめ:検索精度は「運用」で育てるもの
Azure AI Searchのセマンティックランク付け機能は強力ですが、それを現場で使いこなすには「データの特性に合わせた設定」と「数値による評価」が不可欠です。
- Golden Datasetを作成する: 現場の知見を反映した正解基準を作る。
- NDCGで現状を計測する: 「なんとなく検索しにくい」という感覚を数値化する。
- セマンティック構成とスコアリングプロファイルを調整する: 仮説検証を繰り返す。
- 自動化して監視する: データの鮮度と共に精度を維持する。
このサイクルを回すことで、検索システムはユーザーにとって真に役立つ「相棒」へと育っていきます。建設現場における図面検索や、技術資料のナレッジベースなど、確実性が求められる領域こそ、こうした地道なエンジニアリングが価値を発揮します。
コメント