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?

Azure AI Searchで始める「次世代検索システム」の作り方 - Part 2: 実装編

Posted at

はじめに

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ポータルでの作成

まずは、検索サービスを作成します。

手順:

  1. Azureポータルにアクセス

  2. リソースの作成

    • 「リソースの作成」をクリック
    • 「Azure AI Search」を検索
  3. 基本設定

    サブスクリプション: (お使いのサブスクリプション)
    リソースグループ: 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: 基礎編

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?