はじめに
Part 1では、Azure AI Searchの基本概念と用語について学びました。このPart 2では、実際に手を動かして検索システムを構築していきます!
Part 2で学ぶこと:
- 検索サービスの作成
- 検索インデックスの設計と作成
- ベクトル検索の実装
- ハイブリッド検索の設定
- 最新のAPI(2024-07-01)を使った実装
最新のAPI version 2024-07-01 を使用します。これは2024年7月にGAになった安定版で、統合ベクトル化やスカラー量子化などの重要な機能が正式に利用可能になっています。
それでは、実際に作っていきましょう!
1. Azure AI Search サービスの作成
1.1 Azureポータルでの作成
まずは、検索サービスを作成します。
手順:
-
Azureポータルにアクセス
- https://portal.azure.com にログイン
-
リソースの作成
- 「リソースの作成」をクリック
- 「Azure AI Search」を検索
-
基本設定
サブスクリプション: (お使いのサブスクリプション) リソースグループ: mySearchResourceGroup(新規作成) サービス名: my-search-service-001 場所: Japan East(日本東部) 価格レベル: Basic(開発・テスト用)
価格レベルの選び方:
| レベル | 用途 | 特徴 |
|---|---|---|
| Free | 学習・検証 | 1サービス/サブスクリプション、50MB、10,000ドキュメント |
| Basic | 小規模本番 | 2GB、SLA 99.9% |
| Standard (S1) | 中規模本番 | 25GB、高いクエリパフォーマンス |
| Standard (S2/S3) | 大規模本番 | 100GB〜1TB |
初心者の方へのアドバイス:
- 学習目的ならFreeプランでOK
- 本番環境の検証ならBasicから始めましょう
- 一度作成すると価格レベルは変更できないので注意!
1.2 REST APIでの作成確認
サービスが作成できたら、接続情報を確認します。
# エンドポイント
https://my-search-service-001.search.windows.net
# 管理キー(Keysメニューから確認)
Primary admin key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
接続テスト:
curl -X GET "https://my-search-service-001.search.windows.net/servicestats?api-version=2024-07-01" \
-H "api-key: YOUR_ADMIN_KEY"
レスポンスが返ってきたら成功です!
2. 検索インデックスの設計
2.1 インデックススキーマの理解
検索インデックスは、検索したいデータの「設計図」です。
基本的な考え方:
元のデータ(ドキュメント): 商品情報
↓
インデックススキーマ: どのフィールドをどう検索できるようにするか定義
↓
検索可能なインデックス: 高速検索が可能に!
2.2 フィールドタイプとプロパティ
Azure AI Searchで使える主なデータ型:
| データ型 | 説明 | 用途例 |
|---|---|---|
| Edm.String | 文字列 | タイトル、説明文 |
| Edm.Int32 | 整数 | 価格、在庫数 |
| Edm.Double | 浮動小数点 | 評価スコア |
| Edm.Boolean | 真偽値 | 公開/非公開フラグ |
| Edm.DateTimeOffset | 日時 | 作成日、更新日 |
| Collection(Edm.Single) | ベクトル | 埋め込みベクトル |
| Edm.GeographyPoint | 地理座標 | 店舗位置 |
フィールドプロパティ:
{
"name": "title",
"type": "Edm.String",
"searchable": true, // 全文検索対象にするか
"filterable": true, // フィルター可能にするか
"sortable": true, // ソート可能にするか
"facetable": true, // ファセット集計可能にするか
"retrievable": true, // 検索結果に含めるか
"key": false, // 主キーか
"analyzer": "ja.lucene" // 使用するアナライザー
}
2.3 実践的なスキーマ設計
ブログ記事を検索する例で考えてみましょう:
{
"name": "blog-index",
"fields": [
{
"name": "id",
"type": "Edm.String",
"key": true,
"searchable": false
},
{
"name": "title",
"type": "Edm.String",
"searchable": true,
"filterable": false,
"sortable": true,
"analyzer": "ja.lucene"
},
{
"name": "content",
"type": "Edm.String",
"searchable": true,
"filterable": false,
"analyzer": "ja.lucene"
},
{
"name": "category",
"type": "Edm.String",
"filterable": true,
"facetable": true,
"sortable": true
},
{
"name": "publishedDate",
"type": "Edm.DateTimeOffset",
"filterable": true,
"sortable": true
},
{
"name": "tags",
"type": "Collection(Edm.String)",
"filterable": true,
"facetable": true
},
{
"name": "viewCount",
"type": "Edm.Int32",
"filterable": true,
"sortable": true
},
{
"name": "contentVector",
"type": "Collection(Edm.Single)",
"searchable": true,
"dimensions": 1536,
"vectorSearchProfile": "my-vector-profile"
}
]
}
設計のポイント:
-
idフィールドは必須(key: true) - 日本語コンテンツには
ja.luceneアナライザーを使用 - ベクトル検索用フィールドには
dimensionsを指定(OpenAIのtext-embedding-ada-002は1536次元) - フィルターやソートが必要なフィールドには適切なプロパティを設定
3. ベクトル検索の設定
3.1 ベクトル検索プロファイルの作成
ベクトル検索を有効にするには、ベクトル検索の設定を定義する必要があります。
ベクトル検索プロファイルとは?
- どのアルゴリズムを使うか
- どうやって類似度を計算するか
- などを定義するもの
{
"name": "blog-index",
"fields": [ /* 省略 */ ],
"vectorSearch": {
"algorithms": [
{
"name": "my-hnsw-algorithm",
"kind": "hnsw",
"hnswParameters": {
"metric": "cosine",
"m": 4,
"efConstruction": 400,
"efSearch": 500
}
}
],
"profiles": [
{
"name": "my-vector-profile",
"algorithm": "my-hnsw-algorithm"
}
]
}
}
HNSWパラメータの説明:
これらのパラメータは、検索の精度と速度のトレードオフを調整するものです。
| パラメータ | 説明 | デフォルト | アドバイス |
|---|---|---|---|
| metric | 類似度の計算方法 | cosine | cosine推奨(OpenAI埋め込みと相性良い) |
| m | グラフの接続数 | 4 | 大きいほど精度↑、メモリ↑ |
| efConstruction | インデックス構築時の探索範囲 | 400 | 大きいほどインデックス品質↑、構築時間↑ |
| efSearch | 検索時の探索範囲 | 500 | 大きいほど検索精度↑、速度↓ |
初心者向けアドバイス:
最初はデフォルト値で問題ありません。パフォーマンスに問題が出たら調整を検討しましょう。
3.2 スカラー量子化(Scalar Quantization)
2024-07-01でGA になった新機能!
ベクトルデータは大きいので、ストレージとメモリを大量に消費します。量子化を使うと、精度をほとんど落とさずにサイズを削減できます。
通常のベクトル:
[0.234, 0.819, -0.453, ...] ← 各値が32bit(4バイト)
1536次元 × 4バイト = 6,144バイト/ドキュメント
量子化後:
[23, 82, -45, ...] ← 各値が8bit(1バイト)
1536次元 × 1バイト = 1,536バイト/ドキュメント
→ 4分の1に圧縮!
設定方法:
{
"vectorSearch": {
"compressions": [
{
"name": "my-scalar-quantization",
"kind": "scalarQuantization",
"scalarQuantizationParameters": {
"quantizedDataType": "int8"
}
}
],
"algorithms": [
{
"name": "my-hnsw-algorithm",
"kind": "hnsw",
"hnswParameters": {
"metric": "cosine",
"m": 4,
"efConstruction": 400,
"efSearch": 500
}
}
],
"profiles": [
{
"name": "my-vector-profile",
"algorithm": "my-hnsw-algorithm",
"compression": "my-scalar-quantization"
}
]
}
}
メリット:
- ストレージコスト削減(最大75%)
- メモリ使用量削減
- わずかな精度低下(通常1-2%程度)
4. インデックスの作成(REST API)
4.1 完全なインデックス定義
ここまでの内容を統合した完全なインデックス定義です:
{
"name": "blog-index",
"fields": [
{
"name": "id",
"type": "Edm.String",
"key": true,
"searchable": false
},
{
"name": "title",
"type": "Edm.String",
"searchable": true,
"filterable": false,
"sortable": true,
"retrievable": true,
"analyzer": "ja.lucene"
},
{
"name": "content",
"type": "Edm.String",
"searchable": true,
"filterable": false,
"retrievable": true,
"analyzer": "ja.lucene"
},
{
"name": "category",
"type": "Edm.String",
"filterable": true,
"facetable": true,
"sortable": true,
"retrievable": true
},
{
"name": "publishedDate",
"type": "Edm.DateTimeOffset",
"filterable": true,
"sortable": true,
"retrievable": true
},
{
"name": "tags",
"type": "Collection(Edm.String)",
"filterable": true,
"facetable": true,
"retrievable": true
},
{
"name": "contentVector",
"type": "Collection(Edm.Single)",
"searchable": true,
"retrievable": false,
"dimensions": 1536,
"vectorSearchProfile": "my-vector-profile"
}
],
"vectorSearch": {
"compressions": [
{
"name": "my-scalar-quantization",
"kind": "scalarQuantization",
"scalarQuantizationParameters": {
"quantizedDataType": "int8"
}
}
],
"algorithms": [
{
"name": "my-hnsw-algorithm",
"kind": "hnsw",
"hnswParameters": {
"metric": "cosine",
"m": 4,
"efConstruction": 400,
"efSearch": 500
}
}
],
"profiles": [
{
"name": "my-vector-profile",
"algorithm": "my-hnsw-algorithm",
"compression": "my-scalar-quantization"
}
]
},
"semantic": {
"configurations": [
{
"name": "my-semantic-config",
"prioritizedFields": {
"titleField": {
"fieldName": "title"
},
"contentFields": [
{
"fieldName": "content"
}
],
"keywordsFields": [
{
"fieldName": "tags"
}
]
}
}
]
}
}
4.2 REST APIでインデックスを作成
curlコマンド:
curl -X POST "https://my-search-service-001.search.windows.net/indexes?api-version=2024-07-01" \
-H "Content-Type: application/json" \
-H "api-key: YOUR_ADMIN_KEY" \
-d @blog-index.json
Pythonでの実装:
import requests
import json
endpoint = "https://my-search-service-001.search.windows.net"
api_key = "YOUR_ADMIN_KEY"
api_version = "2024-07-01"
# インデックス定義を読み込む
with open('blog-index.json', 'r', encoding='utf-8') as f:
index_definition = json.load(f)
# インデックスを作成
url = f"{endpoint}/indexes?api-version={api_version}"
headers = {
"Content-Type": "application/json",
"api-key": api_key
}
response = requests.post(url, headers=headers, json=index_definition)
if response.status_code == 201:
print("インデックスが正常に作成されました!")
print(json.dumps(response.json(), indent=2, ensure_ascii=False))
else:
print(f"エラーが発生しました: {response.status_code}")
print(response.text)
5. データのアップロード
5.1 ドキュメントの準備
検索インデックスにデータをアップロードしましょう。
重要: ベクトルフィールド(contentVector)には、事前に生成した埋め込みベクトルを含める必要があります。
import openai
# Azure OpenAI の設定
openai.api_type = "azure"
openai.api_base = "https://your-openai.openai.azure.com/"
openai.api_version = "2024-02-01"
openai.api_key = "YOUR_OPENAI_KEY"
def generate_embedding(text):
"""テキストから埋め込みベクトルを生成"""
response = openai.Embedding.create(
engine="text-embedding-ada-002", # デプロイ名
input=text
)
return response['data'][0]['embedding']
# サンプルドキュメント
documents = [
{
"id": "1",
"title": "Azure AI Searchの始め方",
"content": "Azure AI Searchは、Microsoftが提供する強力な検索サービスです。ベクトル検索やハイブリッド検索に対応しており...",
"category": "Azure",
"publishedDate": "2024-10-01T00:00:00Z",
"tags": ["Azure", "AI", "検索"],
"viewCount": 1500,
"contentVector": generate_embedding("Azure AI Searchは、Microsoftが提供する強力な検索サービスです...")
},
{
"id": "2",
"title": "RAGパターンの実装ガイド",
"content": "RAG(Retrieval-Augmented Generation)は、検索と生成AIを組み合わせた強力なパターンです...",
"category": "AI",
"publishedDate": "2024-10-15T00:00:00Z",
"tags": ["AI", "RAG", "ChatGPT"],
"viewCount": 2300,
"contentVector": generate_embedding("RAG(Retrieval-Augmented Generation)は、検索と生成AIを組み合わせた...")
}
]
5.2 ドキュメントのアップロード
def upload_documents(documents):
"""ドキュメントをインデックスにアップロード"""
url = f"{endpoint}/indexes/blog-index/docs/index?api-version={api_version}"
headers = {
"Content-Type": "application/json",
"api-key": api_key
}
# アクションを指定(upload: 新規追加/更新)
payload = {
"value": [
{
"@search.action": "upload",
**doc
}
for doc in documents
]
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
print(f"アップロード結果:")
for item in result['value']:
status = "成功" if item['status'] else "失敗"
print(f" ID {item['key']}: {status}")
else:
print(f"エラー: {response.status_code}")
print(response.text)
# アップロード実行
upload_documents(documents)
バッチアップロードの注意点:
- 1バッチあたり最大1000ドキュメント
- 1バッチあたり最大16MB
- 大量データは複数バッチに分けてアップロード
6. 検索クエリの実装
6.1 フルテキスト検索
まずは基本的なキーワード検索から:
def full_text_search(query_text):
"""フルテキスト検索"""
url = f"{endpoint}/indexes/blog-index/docs/search?api-version={api_version}"
headers = {
"Content-Type": "application/json",
"api-key": api_key
}
payload = {
"search": query_text,
"select": "id,title,content,category,publishedDate",
"top": 5,
"count": True
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
print(f"検索結果: {result['@odata.count']}件")
for doc in result['value']:
print(f"\n--- スコア: {doc['@search.score']:.4f} ---")
print(f"タイトル: {doc['title']}")
print(f"カテゴリ: {doc['category']}")
else:
print(f"エラー: {response.status_code}")
# 実行例
full_text_search("Azure 検索")
6.2 ベクトル検索
意味的な類似性で検索します:
def vector_search(query_text, top_k=5):
"""ベクトル検索"""
# クエリテキストをベクトル化
query_vector = generate_embedding(query_text)
url = f"{endpoint}/indexes/blog-index/docs/search?api-version={api_version}"
headers = {
"Content-Type": "application/json",
"api-key": api_key
}
payload = {
"vectorQueries": [
{
"kind": "vector",
"vector": query_vector,
"fields": "contentVector",
"k": top_k
}
],
"select": "id,title,content,category"
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
print(f"ベクトル検索結果:")
for doc in result['value']:
print(f"\n--- スコア: {doc['@search.score']:.4f} ---")
print(f"タイトル: {doc['title']}")
print(f"カテゴリ: {doc['category']}")
else:
print(f"エラー: {response.status_code}")
# 実行例
vector_search("機械学習を使った検索システムの構築方法")
ベクトル検索のポイント:
- クエリテキストも同じ埋め込みモデルでベクトル化する
-
kパラメータで取得する結果数を指定 - スコアは類似度(1.0に近いほど類似)
6.3 ハイブリッド検索
キーワード検索とベクトル検索を組み合わせた最強の検索!
def hybrid_search(query_text, top_k=5):
"""ハイブリッド検索(キーワード + ベクトル)"""
# クエリテキストをベクトル化
query_vector = generate_embedding(query_text)
url = f"{endpoint}/indexes/blog-index/docs/search?api-version={api_version}"
headers = {
"Content-Type": "application/json",
"api-key": api_key
}
payload = {
"search": query_text, # キーワード検索
"vectorQueries": [ # ベクトル検索
{
"kind": "vector",
"vector": query_vector,
"fields": "contentVector",
"k": 50 # RRFのためにより多くの候補を取得
}
],
"select": "id,title,content,category",
"top": top_k
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
print(f"ハイブリッド検索結果:")
for doc in result['value']:
print(f"\n--- スコア: {doc['@search.score']:.4f} ---")
print(f"タイトル: {doc['title']}")
print(f"カテゴリ: {doc['category']}")
else:
print(f"エラー: {response.status_code}")
# 実行例
hybrid_search("Azure AI Searchの使い方")
ハイブリッド検索のメリット:
- キーワード検索: 正確な用語やコードを見つける
- ベクトル検索: 意味的に関連する内容を見つける
- RRF: 両方の結果を賢く統合
6.4 セマンティックランキング付きハイブリッド検索
究極の検索体験!
def semantic_hybrid_search(query_text, top_k=5):
"""セマンティックランキング付きハイブリッド検索"""
query_vector = generate_embedding(query_text)
url = f"{endpoint}/indexes/blog-index/docs/search?api-version={api_version}"
headers = {
"Content-Type": "application/json",
"api-key": api_key
}
payload = {
"search": query_text,
"vectorQueries": [
{
"kind": "vector",
"vector": query_vector,
"fields": "contentVector",
"k": 50
}
],
"queryType": "semantic", # セマンティックランキング有効化
"semanticConfiguration": "my-semantic-config",
"select": "id,title,content,category",
"top": top_k
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
print(f"セマンティックハイブリッド検索結果:")
for doc in result['value']:
print(f"\n--- スコア: {doc['@search.score']:.4f} ---")
print(f"タイトル: {doc['title']}")
# セマンティックランキングによるキャプションがあれば表示
if '@search.captions' in doc:
captions = doc['@search.captions']
if captions:
print(f"キャプション: {captions[0]['text']}")
else:
print(f"エラー: {response.status_code}")
# 実行例
semantic_hybrid_search("クラウドで動く賢い検索の作り方")
セマンティックランキングの効果:
- より人間の感覚に近い結果順位
- 検索クエリに最も関連する部分をハイライト(キャプション)
- 回答生成用の文脈抽出
7. フィルターとファセット
7.1 フィルター検索
特定の条件で絞り込み:
def filtered_search(query_text, category=None, min_views=None):
"""フィルター付き検索"""
url = f"{endpoint}/indexes/blog-index/docs/search?api-version={api_version}"
headers = {
"Content-Type": "application/json",
"api-key": api_key
}
# フィルター条件を構築
filters = []
if category:
filters.append(f"category eq '{category}'")
if min_views:
filters.append(f"viewCount ge {min_views}")
filter_string = " and ".join(filters) if filters else None
payload = {
"search": query_text,
"filter": filter_string,
"select": "id,title,category,viewCount",
"top": 10
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
print(f"フィルター付き検索結果: {len(result['value'])}件")
for doc in result['value']:
print(f"\nタイトル: {doc['title']}")
print(f"カテゴリ: {doc['category']}, 閲覧数: {doc['viewCount']}")
else:
print(f"エラー: {response.status_code}")
# 実行例
filtered_search("AI", category="Azure", min_views=1000)
7.2 ファセット集計
カテゴリごとの件数を取得:
def faceted_search(query_text):
"""ファセット付き検索"""
url = f"{endpoint}/indexes/blog-index/docs/search?api-version={api_version}"
headers = {
"Content-Type": "application/json",
"api-key": api_key
}
payload = {
"search": query_text,
"facets": ["category,count:10", "tags,count:10"],
"select": "id,title",
"top": 10
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
print("カテゴリ別件数:")
for facet in result.get('@search.facets', {}).get('category', []):
print(f" {facet['value']}: {facet['count']}件")
print("\nタグ別件数:")
for facet in result.get('@search.facets', {}).get('tags', []):
print(f" {facet['value']}: {facet['count']}件")
else:
print(f"エラー: {response.status_code}")
# 実行例
faceted_search("Azure")
8. Python SDKの使用
REST APIの代わりに、公式SDKを使うこともできます。
8.1 SDKのインストール
pip install azure-search-documents==11.5.1
pip install azure-identity
8.2 SDK を使った検索実装
from azure.search.documents import SearchClient
from azure.core.credentials import AzureKeyCredential
# クライアントの初期化
search_client = SearchClient(
endpoint="https://my-search-service-001.search.windows.net",
index_name="blog-index",
credential=AzureKeyCredential("YOUR_ADMIN_KEY")
)
def sdk_hybrid_search(query_text):
"""SDKを使ったハイブリッド検索"""
# クエリベクトルを生成
query_vector = generate_embedding(query_text)
# 検索実行
results = search_client.search(
search_text=query_text,
vector_queries=[{
"kind": "vector",
"vector": query_vector,
"fields": "contentVector",
"k": 50
}],
select=["id", "title", "content", "category"],
top=5
)
print("検索結果:")
for result in results:
print(f"\n--- スコア: {result['@search.score']:.4f} ---")
print(f"タイトル: {result['title']}")
print(f"カテゴリ: {result['category']}")
# 実行
sdk_hybrid_search("Azure クラウド 検索")
SDKのメリット:
- タイプセーフ
- エラーハンドリングが楽
- ドキュメントが充実
- 自動リトライなどの機能
Part 2 のまとめ
お疲れさまでした!ここまでで、Azure AI Searchの実装方法を学びました。
押さえておきたいポイント:
✅ インデックススキーマの設計
- フィールドタイプとプロパティを適切に設定
- ベクトル検索フィールドにはdimensionsを指定
✅ 最新のAPI version 2024-07-01を使用
- 統合ベクトル化機能がGA
- スカラー量子化で効率化
✅ ハイブリッド検索が最強
- キーワード検索とベクトル検索を組み合わせ
- セマンティックランキングでさらに改善
✅ フィルターとファセット
- 柔軟な絞り込み検索
- ユーザー体験の向上
次のPart 3では:
- RAGパターンの実装
- インデクサーとスキルセットの活用
- 本番運用のベストプラクティス
- モニタリングとトラブルシューティング
実装の基礎ができたところで、いよいよRAGアプリケーション開発に進みましょう!
参考リンク
次の記事: Azure AI Searchで始める「次世代検索システム」の作り方 - Part 3: RAG実装編
前の記事: Azure AI Searchで始める「次世代検索システム」の作り方 - Part 1: 基礎編