生成AI時代の様々な検索手法を検証する 〜Azure AI Searchによるベクトル/セマンティック/ハイブリッド検索〜

◆ Live配信スケジュール ◆
サイオステクノロジーでは、Microsoft MVPの武井による「わかりみの深いシリーズ」など、定期的なLive配信を行っています。
⇒ 詳細スケジュールはこちらから
⇒ 見逃してしまった方はYoutubeチャンネルをご覧ください
【5/21開催】Azure OpenAI ServiceによるRAG実装ガイドを公開しました
生成AIを活用したユースケースで最も一番熱いと言われているRAGの実装ガイドを公開しました。そのガイドの紹介をおこなうイベントです!!
https://tech-lab.connpass.com/event/315703/

こんにちは、サイオステクノロジー武井です。今回は、Azure AI Searchによるベクトル検索・セマンティック検索・ハイブリッド検索について一筆したためたいと思います。

生成AI時代の様々な検索手法

最もメジャーな検索手法は、単語の出現頻度に基づいてランキングを行う「キーワード検索」です。この検索方法は、ユーザーが入力した特定のキーワードやフレーズを検索対象のテキストと直接的に比較してマッチングを行います。キーワードの出現頻度や配置に基づいて結果を決定しますので、シンプルで直感的ですが、文脈の理解や言葉の意味の深さを捉えるのには限界があります。

また、ベクトル検索という手法もあります。テキスト内容を数値のベクトルに変換し、これらのベクトル間の類似度を計算して検索結果を提供します。深層学習に基づくモデルを使用して文脈や意味の理解を行うため、より精度の高い検索結果を得ることができます。この方法は、単語の類似性や文脈的な意味合いを捉え、複雑なクエリに対しても効果的な結果を提供します。

キーワード検索とベクトル検索について詳細については、以下のブログをご参考ください。

 

ベクトル検索の有用性をキーワード検索と比較する

 

そして、最近では、キーワード検索とベクトル検索を融合した「ハイブリッド検索」、さらにはAzureが提供するマネージドな全文検索サービス「Azure AI Search」が提供する「セマンティック検索」など、生成AI時代のニーズにマッチした様々な検索手法が登場しています。

今回は、キーワード検索、ベクトル検索、ハイブリッド検索など、さまざまな検索手法をAzure AI Searchに適用し、実際のデータを使って試してみました。これにより、各検索手法の有効性を深く理解し、その利点を探ることが目的です。

検証方法

今回の検証では、Azureが提供するマネージド型全文検索サービス「Azure AI Search」を使用します。検索サービスに登録するデータは、Wikipediaからスターウォーズの登場人物をAPIを通じて取得し、その後、Azure OpenAI ServiceのEmbeddings APIを利用して、これらのデータをベクトル化し、テキストデータとともにAzure AI Searchに登録します。

以下の図は、このプロセスを視覚的に表現したものです。

上図の各プロセスの詳細は以下の通りとなります。

1. Wikipedia本文取得

Pythonで作成されたプログラムを用いて、WikipediaのAPIを通じ、スターウォーズの登場人物に関するドキュメントのタイトルとコンテンツを取得します。

2. LangChainによるドキュメントのチャンク化

取得したドキュメントをAzure OpenAI ServiceのEmbeddings APIでベクトル化する前に、APIのトークン数制限に対応するため、LangChainを利用してドキュメントを小さいチャンクに分割します。

3. Embeddings API発行

ステップ2でチャンク化されたドキュメントをベクトル化するため、Azure OpenAI ServiceのEmbeddings APIへリクエストを送ります。

4. ベクトル化データ取得

Embeddings APIからのレスポンスとして返されるベクトル化データを受け取ります。

5. ドキュメント登録

ステップ1で取得したドキュメント及びベクトル化したデータをAzure AI Searchに登録します。

では、上記のステップを実行して、Azure AI Searchに必要なデータを登録します。

まずは、Azure AI Searchにインデックスを作成します。curlコマンドにより、以下のAPIを発行します。

curl -X PUT "https://[Azure AI Searchのサービス名].search.windows.net/indexes/wiki?api-version=2023-11-01" \
     -H "Content-Type: application/json" \
     -H "api-key: [APIキー]" \
     -d '{
            "name": "wiki",
            "fields": [
                {
                    "name": "id",
                    "type": "Edm.String",
                    "key": true,
                    "searchable": false,
                    "filterable": false,
                    "retrievable": true,
                    "sortable": false,
                    "facetable": false
                },
                {
                    "name": "title",
                    "type": "Edm.String",
                    "searchable": false,
                    "filterable": true,
                    "retrievable": true,
                    "sortable": false,
                    "facetable": true
                },
                {
                    "name": "content",
                    "type": "Edm.String",
                    "searchable": true,
                    "filterable": true,
                    "retrievable": true,
                    "sortable": false,
                    "facetable": false,
                    "analyzer": "ja.lucene"
                },
                {
                    "name": "contentVector",
                    "type": "Collection(Edm.Single)",
                    "searchable": true,
                    "retrievable": true,
                    "sortable": false,
                    "dimensions": 1536,
                    "vectorSearchProfile": "my-vector-profile"
                }
            ],
            "semantic": {
                "defaultConfiguration": null,
                "configurations": [
                    {
                        "name": "config1",
                        "prioritizedFields": {
                            "titleField": {
                                "fieldName": "title"
                            },
                            "prioritizedContentFields": [
                                {
                                    "fieldName": "content"
                                }
                            ],
                            "prioritizedKeywordsFields": []
                        }
                    }
                ]
            },
            "vectorSearch": {
                "algorithms": [
                    {
                        "name": "my-hnsw-vector-config-1",
                        "kind": "hnsw",
                        "hnswParameters": 
                        {
                            "m": 4,
                            "efConstruction": 400,
                            "efSearch": 500,
                            "metric": "cosine"
                        }
                    }
                ],
                "profiles": [      
                    {
                        "name": "my-vector-profile",
                        "algorithm": "my-hnsw-vector-config-1"
                    }
                ]
            }
        }'

上記のコマンドでは、以下の構成のインデックスを作成します。

idドキュメントのIDです。key: trueとすることで、このフィールドがインデックス内でユニークなキーとして機能するようになります。このフィールドにはWikipediaのタイトルをURLをBease64エンコードした値を登録します。
title
Wikipediaのページのタイトルを登録します。
contentWikipediaのページの本文を登録します。。ベクトル検索したときとの結果を比較や、後に説明するセマンティック検索を行うために、テキストでも格納しています。analyzer: ja:luceneとすることで、日本語のテキスト解析に適したLuceneアナライザーを使用するようになります。
contentVector
「content」に格納したテキストのベクトル化したデータを格納するフィールドです。

 

次に、Azure AI Searchに登録するためのデータをWikipediaから取得します。今回は以下の登場人物のデータを取得します。

  • ルーク・スカイウォーカー
  • ダース・ベイダー
  • レイア・オーガナ
  • ハン・ソロ
  • ヨーダ
  • オビ=ワン・ケノービ
  • チューバッカ
  • シーヴ・パルパティーン
  • アナキン・スカイウォーカー
  • ランド・カルリジアン
  • パドメ・アミダラ
  • ボバ・フェット
  • キャプテン・ファズマ
  • C-3PO
  • レイ (スター・ウォーズ)

 

そのためのPythonのプログラムは以下の通りとなります。Wikipediaから取得したデータは「data」ディレクトリにファイルとして保存します。ファイル名の命名規則は「Wikipediaのタイトル-NN.txt」となっており、「NN」はチャンク化されたドキュメントの個数に応じて数字が割り振られます。例えば、ドキュメントが2つにチャンク化された場合、ファイル名は「01」と「02」といった形で連番が付けられます。例えば、ダースベイダーに関するドキュメントを取得し、2つにチャンク化された場合は、「ダースベイダー-01.txt」「ダースベイダー-02.txt」となります。

import wikipedia
from langchain.text_splitter import RecursiveCharacterTextSplitter
import os

def save_chunks_to_files(title, chunk_size, chunk_overlap, output_dir='data', lang='ja'):
    # Wikipediaページの取得
    wikipedia.set_lang(lang)

    page = wikipedia.page(title)
    text = page.content

    # テキストをチャンクに分割
    splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
        encoding_name='cl100k_base',
        chunk_size=chunk_size, 
        chunk_overlap=chunk_overlap
    )
    chunks = splitter.split_text(text)

    # 出力ディレクトリの作成
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    # 各チャンクをファイルに保存
    for i, chunk in enumerate(chunks):
        file_name = f"{title}-{i+1:02d}.txt"
        file_path = os.path.join(output_dir, file_name)
        with open(file_path, 'w', encoding='utf-8') as file:
            file.write(chunk)
        print(f"Saved: {file_path}")

characters = ["ルーク・スカイウォーカー", "ダース・ベイダー", "レイア・オーガナ", "ハン・ソロ", "ヨーダ", "オビ=ワン・ケノービ", "チューバッカ", "シーヴ・パルパティーン", "アナキン・スカイウォーカー", "ランド・カルリジアン", "パドメ・アミダラ", "ボバ・フェット", "キャプテン・ファズマ", "C-3PO", "レイ (スター・ウォーズ)"]

    
# チャンクサイズとオーバーラップを設定
chunk_size = 1000  # チャンクサイズ
chunk_overlap = 50  # チャンクのオーバーラップ

# 各果物に関するWikipediaの記事をチャンクに分割して保存
for character in characters:
    save_chunks_to_files(character, chunk_size, chunk_overlap)

 

これが最後のステップです。以下のプログラムで、Wikipediaから取得済みのチャンク化された本文をAzure AI Searchに登録します。

import requests  # HTTPリクエストを送信するためのライブラリ
import os  # OSの機能を利用するためのライブラリ
import base64  # データをBase64形式でエンコード/デコードするためのライブラリ

# ファイルをAzure AI SearchのAPIに登録する関数
def post_file_to_search_api(file_path):
    # Azure AI SearchのAPIのエンドポイントURL
    srch_api_endpoint = "https://[Azure AI Searchのサービス名].search.windows.net/indexes/wiki/docs/index?api-version=2023-11-01"
    # Azure AI SearchのAPIのキー
    srch_api_key = "XXXXXX"

    # AOAI APIのエンドポイントURL
    aoai_api_endpoint = "https://[Azure OpenAI Serviceのサービス名].openai.azure.com/openai/deployments/[モデルtext-embedding-ada-002のデプロイ名]/embeddings?api-version=2023-05-15"
    # AOAI APIのキー
    aoai_api_key = "XXXXXX"

    # ファイル名を取得(拡張子を除く)
    file_name = os.path.splitext(os.path.basename(file_path))[0]
    # ファイル名をBase64でエンコードしてIDとする
    id = base64.urlsafe_b64encode(file_name.encode()).decode()
    
    # ファイルを開いて内容を読み取る
    with open(file_path, 'r', encoding='utf-8') as file:
        content = file.read()

    # AOAI APIに送信するデータを作成
    aoai_data = {
        "input": content
    }

    # AOAI APIに送信するためのヘッダーを作成
    aoai_headers = {
        "Content-Type": "application/json",
        "api-key": aoai_api_key
    }

    # AOAI APIにPOSTリクエストを送信して、コンテンツをベクトル化
    vectorized_content = requests.post(aoai_api_endpoint, headers=aoai_headers, json=aoai_data)

    # 検索APIに送信するデータを作成
    srch_data = {
        "value": [
            {
                "@search.action": "upload",
                "id": id,
                "title": file_name,
                "content": content,
                "contentVector": vectorized_content.json()["data"][0]["embedding"]
            }
        ]
    }

# ディレクトリ内のファイルを読み取る
directory = "data"  # ファイルを読み取るディレクトリの名前

# ディレクトリ内の各ファイルに対して処理を行う
for filename in os.listdir(directory):
    # ファイルが.txtで終わる場合
    if filename.endswith(".txt"):
        # ファイルのフルパスを作成
        file_path = os.path.join(directory, filename)
        # ファイルをAzure AI SearchのAPIにPOSTする関数を呼び出し、レスポンスを取得
        response = post_file_to_search_api(file_path)
        # ファイル名とレスポンスのテキストを出力
        print(f"Posted {filename}: {response.text}")

これで準備は整いました。

では、次の章から様々な検索手法を検証してみましょう。

ベクトル検索

本章ではベクトル検索の効果を検証してみます。

先ほどAzure AI Searchに登録したデータを用いて、次に示すクエリを実行します。この際、キーワード検索とベクトル検索の結果を比較検討します。キーワード検索は、特定の単語やフレーズを含む文書を検索する方法です。一方、ベクトル検索は、文書の内容を数値化したベクトルとクエリのベクトルとの間の類似度に基づいて検索を行います。これにより、どちらの検索方法がより効果的なのかを検証できます。

エピソード1〜3で語られるダースベーダーの本当の名前は?

 

まずはキーワード検索の場合のAPIリクエストを以下のcurl文によって実行します。

$ curl -X POST "https://[Azure AI Searchのサービス名].search.windows.net/indexes/wiki/docs/search?api-version=2023-11-01" \
     -H "Content-Type: application/json" \
     -H "api-key: [Azure AI SearchのAPIキー]" \
     -d '{
            "queryType": "simple",
            "search": "エピソード1〜3で語られるダースベーダーの本当の名前は?",
            "searchFields":"content",
            "select": "title, content",
            "highlight": "content",
            "highlightPreTag": "<b>",
            "highlightPostTag": "</b>",
            "count": true
        }' \
| jq 

 

次に同じクエリを用いて、登録されたドキュメントに対してベクトル検索します。まずはAzure OpenAI ServiceのEmbeddings APIにリクエストを発行し、クエリをベクトル化した後に、embeddingsという変数に格納します。

$ embedding=$(curl -s -X POST "https://[Azure OpenAI Serviceのサービス名].openai.azure.com/openai/deployments/[モデルtext-embedding-ada-002のデプロイ名]/embeddings?api-version=2023-05-15" \
  -H 'Content-Type: application/json' \
  -H 'api-key: [Azure OpenAI ServiceのAPIキー]' \
  -d '{"input": "エピソード1〜3で語られるダースベーダーの本当の名前は?"}' \
| jq '.data[0].embedding')

 

ベクトル化したクエリを用いて、Azure AI Searchに登録されたドキュメントに対してベクトル検索を行います。

$ curl -X POST "https://[Azure AI Searchのサービス名].search.windows.net/indexes/wiki/docs/search?api-version=2023-11-01" \
     -H "Content-Type: application/json" \
     -H "api-key: [Azure AI SearchのAPIキー]" \
     -d '{
            "count": true,
            "select": "title, content",
            "vectorQueries": [
                {
                    "fields": "contentVector",
                    "kind": "vector",
                    "vector": '$embedding',
                    "exhaustive": true
            }
        ]
    }' | jq

 

キーワード検索とベクトル検索の結果を以下のように並べて比較することとします。

キーワード検索の場合は、クエリ内の「エピソード」「本当」という単語に引っ張られすぎて、関連のないドキュメントのスコアが高くなり上位に来ています。「本当の名前」という言い方もあまりしませんしね。。。よって、ユーザーの意図である「ダースベイダーの本名を知りたい」にマッチした回答は得られていません。

一方で、ベクトル検索ではどうでしょうか?「本名およびジェダイの騎士であった頃の名はアナキン・スカイウォーカーで・・・」というフレーズを含むドキュメントが最も高いスコアになっていることがわかります。ベクトル検索では見事にユーザーの意図にマッチした回答を返しています。

セマンティック検索

次はセマンティック検索です。セマンティック検索とは、Bingで提供されている深層学習モデルを利用して、単にキーワードをマッチングするだけでなく、クエリの意味内容や文脈を理解することで、検索結果の関連性を高める機能です。

セマンティック検索は、以下の3つのステップを通じて、より高い精度の検索結果を提供します。この例は、「ルーク・スカイウォーカーの師匠はだれですか?」というクエリに対する検索結果になります。

1. BM25もしくはRRFによるランキング

まずは最初のステップです。検索クエリの対象となるドキュメントに対して、BM25もしくはRRF(後述)によるランキングを行います。これは従来のキーワード検索の方法と変わりません。

2. Bingから変化した深層学習モデルによる再ランキング

ここからがセマンティック検索独自のステップです。Azure AI Searchのセマンティックランキングでは、特別に訓練された深層学習モデルを使います。これらのモデルは、大量のデータから文脈と意味を理解する方法を学んでおり、単なるキーワードマッチングを超えた検索が可能です。

クエリが投げられると、システムはそのクエリの言葉だけでなく、クエリが意図する全体的な文脈や目的を解析します。例えば、「ジャガイモの育て方」を検索する場合、キーワード検索では「ジャガイモ」という単語に一致するコンテンツを提供しますが、セマンティック検索では「育て方」という意図も考慮に入れ、栽培方法に特化した情報を提供します。

最初にBM25もしくはRFFによってランキングされた結果に対して、セマンティックモデルはそれぞれの検索結果がクエリにどれだけ適合しているかを評価し、適合度に基づいて検索結果を再ランキングします。これにより、単にキーワードが含まれているドキュメントではなく、クエリの意図を最もよく反映していると考えられるドキュメントが上位に表示されるようになります。

ベクトル検索との違いが気になるところですよね。セマンティックランキングとベクトル検索は、どちらも文脈を理解しようとする点で似ていますが、アプローチに違いがあります。

ベクトル検索は、テキストを多次元のベクトル空間にマッピングし、クエリとドキュメントのベクトル間の類似度(コサイン類似度などによって算出)に基づいて検索結果を提供する手法です。これにより、単語の使い方や文脈が似ている文書を検索することができ、伝統的なキーワードベースの検索では見逃されがちな、意味的な関連性を持つ結果を見つけ出すことができます。

一方で、セマンティックランキングは、検索クエリの意図を理解し、それに基づいて既にランク付けされた検索結果を再ランキングするプロセスです。ここでは、深層学習モデルがクエリの背後にある意味を解釈し、それに最も関連するドキュメントを上位に昇格させることを目的としています。セマンティックランキングは、ユーザーのニーズに最も適した結果を先頭に置くことで、検索体験を改善します。

つまり、ベクトル検索は「文書の内容」そのものの意味的な表現をベクトル化し、それに基づいてマッチングを行うのに対し、セマンティックランキングは「検索クエリの意図」を理解して既存の結果を最適化します。両者は共に意味理解に基づく検索を可能にするものの、焦点を当てるポイントが異なります。

ここで注意していただきたいのが、セマンティックランキングにおける再ランキングは、先行するランキング手法、つまりBM25やRRFなどで選ばれた上位50件の検索結果に限定されて行われます。これは言い換えれば、最初のランキングで上位に位置している、関連性が高いと判断されたデータのみが、セマンティックランキングのプロセスでさらに評価の見直しを受けるということです。従って、初期ランキングで上位に来なかったデータには、セマンティックランキングのメリットは適用されないというわけです。

3. セマンティックアンサーやセマンティックキャプションの生成

セマンティックアンサーは、ユーザーが質問形式で入力した検索クエリに対して、直接的な答えを提供する機能です。システムはドキュメント内のテキストを分析し、クエリに対する簡潔で具体的な回答を生成します。つまり、クエリに対する回答として返されたドキュメントの中から、最もユーザーの意図と近いと思われる一節を抜き出す機能です。

セマンティックキャプションは、検索結果に含まれるドキュメントから重要な情報を抽出し、そのコンテンツの要約を提供する機能です。先の図の例で言えば、「ルーク・スカイウォーカーの師匠はだれですか?」というクエリに対する回答として「オビ=ワン・ケノービとヨーダ」という部分が強調表示されていますが、まさにそれがセマンティックキャプションになります。

 

前置きはここまでとして、ベクトル検索のときと同様、Azure AI Searchに登録したデータに対して以下のクエリを投げかけ、その結果を評価することでセマンティック検索の効果を検証します。

ルーク・スカイウォーカーの師匠はだれですか?

 

このクエリでセマンティック検索を行うためには、以下のAPIを発行します。

$ curl -X POST "https://[Azure AI Searchのサービス名].search.windows.net/indexes/wiki/docs/search?api-version=2023-11-01" \
     -H "Content-Type: application/json" \
     -H "api-key: [Azure AI SearchのAPIキー]" \
     -d '{
            "queryType": "semantic",
            "search": "ルーク・スカイウォーカーの師匠はだれですか?",
            "semanticConfiguration": "config1",
            "answers": "extractive",
            "select": "title, content",
            "count": true,
            "top": 5
        }' | jq

キーワード検索とは異なり、「queryType」フィールドに「semantic」を指定します。また、「answers」フィールドに「extractive」を指定することでセマンティックアンサーを返すようになります。

キーワード検索とセマンティック検索の結果を以下のように並べて比較することとします。

キーワード検索では、ユーザーの意図にマッチしたドキュメント(ルーク・スカイウォーカー-02.txt)が3位にランキングされています。まぁまぁ、いい結果ですが、上位2つの結果を見てみると、「スカイウォーカー」や「ルーク」という言葉に引っ張られているのがわかります。

セマンティック検索では、ユーザーの意図にマッチしたドキュメント(ルーク・スカイウォーカー-02.txt)が1位に浮上しました。そしてさらにセマンティックアンサーが作成されています。ドキュメントの中から最もユーザーの意図に近いと思われる一節(師匠はオビ=ワン・ケノービとヨーダ。 …)が取得できています。

さらにセマンティックキャプションも生成されています。ユーザーのクエリである「ルーク・スカイウォーカーの師匠はだれですか?」に対して「オビ=ワン・ケノービとヨーダ」という一節が強調表示されています。

セマンティック検索では、キーワード検索は十分に把握できなかったユーザーの意図を把握し、より高い精度の検索結果を提供することがわかりました。

ハイブリッド検索

Azure AI Searchでは、キーワード検索とベクトル検索の結果を融合して、さらに精度の高い回答を返すことが可能である「ハイブリッド検索」が提供されています。

この方法では、クエリに対するキーワード検索とベクトル検索の結果を、RRF(Reciprocal Rank Fusion、逆ランク融合)を用いて統合し、再ランキングして検索結果を提供します。RRFは、異なる検索手法から得られたランキングを組み合わせて、最終的な検索結果の順位を決定する方法です。これにより、キーワード検索の精度とベクトル検索の文脈理解の利点を組み合わせ、より適切な検索結果を得ることが可能になります。

ここで、RRFについてより深く掘り下げてみます。RFFは数式に表すと以下になります。

つまり、各検索システムの順位の逆数の和を取ります。kは定数であり、kの値を大きくすることで、検索結果のスコアの差が縮小され、結果的に検索リストがより均一になります。これは、k の値が高い場合、ランキングの順位によるスコアの影響が小さくなり、低いランクのドキュメントも高いランクのドキュメントと同様に重視されるようになるためです。結果として、検索結果は全体的に平均化され、各ドキュメント間の重要度の差が少なくなります。

例えばドキュメントAがあったとします。あるクエリで検索システムAと検索システムBに検索をかけた結果、検索システムAでのドキュメントAの順位は2位、検索システムBでは4位だったとします。この場合のドキュメントAのRRFは以下となります。(計算結果がわかりやすいように定数kは0としています)

RRFScore(ドキュメントA) = 1/2 + 1/4 = 0.5 + 0.25 = 0.75

また、もう一つの具体例を交えて説明します。ドキュメントA〜Hの8個のドキュメントが検索システムAと検索システムBに登録されているとします。これらのドキュメントに対して、特定のクエリで検索システムAと検索システムBに検索をかけたときの、それぞれのドキュメントの検索順位は以下であるとします。

 

これらのドキュメントのRRFを求めた結果は以下の通りとなります。

 

これらの結果より、検索システムAと検索システムBの結果を融合した検索結果は以下の通りとなります。

 

 

ドキュメントAは検索システムAでは1位、検索システムBでは2位なので、融合した後の結果でも1位とかなり高いランクに位置しています。ドキュメントGは検索システムBでは1位に位置していますが、検索システムBには登場していないので、ドキュメントAよりは低スコアになります。

ドキュメントEは、検索システムAと検索システムBの両方で4位にランクされていました。しかし、RRF(Reciprocal Rank Fusion)を用いて両システムの結果を統合した後、ドキュメントEの最終的なランキングは3位に上昇しました。これは、両方のシステムで一貫して高いランキングを得ているドキュメントは、結果が融合されるとさらに重要度が高まるためです。つまり、複数の検索システムからのランキングが合わさることで、そのドキュメントの総合的な評価が向上するのです。

このようにして各ドキュメントのRRFスコアを計算し、それを基にドキュメントを再ランキングすることで、複数の検索システムの結果を融合した全体的なランキングリストを作成します。RRFは、個々の検索システムがどのような結果を出したかに関わらず、それぞれの強みを平等に反映させることができるため、よりバランスの取れた検索結果を得ることが可能になります。

少々迂遠になりましたが、ではベクトル検索のときと同様、Azure AI Searchに登録したデータに対して以下のクエリを投げてみます。

アナキン・スカイウォーカーによって作成されて、とてもたくさんの言葉が話せる登場人物は?

もちろん期待する回答はC-3POです。

上記のクエリを用いて、登録されたドキュメントに対してベクトル検索します。まずはAzure OpenAI ServiceのEmbeddings APIにリクエストを発行し、クエリをベクトル化した後に、embeddingsという変数に格納します。

$ embedding=$(curl -s -X POST "https://[Azure OpenAI Serviceのサービス名].openai.azure.com/openai/deployments/text-embedding-ada-002-depoly/embeddings?api-version=2023-05-15" \
  -H 'Content-Type: application/json' \
  -H 'api-key: [Azure OpenAI ServiceのAPIキー]' \
  -d '{"input": "アナキン・スカイウォーカーによって作成されて、とてもたくさんの言葉が話せる登場人物は?"}' \
| jq '.data[0].embedding')

 

ベクトル化したクエリを用いて、Azure AI Searchに登録されたドキュメントに対してハイブリッド検索を行います。

$ curl -X POST "https://[Azure AI Searchのサービス名].search.windows.net/indexes/wiki/docs/search?api-version=2023-11-01" \
     -H "Content-Type: application/json" \
     -H "api-key: [Azure AI SearchのAPIキー]" \
     -d '{
            "count": true,
            "select": "title, content",
            "vectorQueries": [
                {
                    "fields": "contentVector",
                    "kind": "vector",
                    "vector": '$embedding',
                    "exhaustive": true,
                    "k": 30
                }
            ],
            "search": "アナキン・スカイウォーカーによって作成されて、とてもたくさんの言葉が話せる登場人物は?"
        }' | jq

上記のAPIを見てみますと、「vectorQueries」フィールドによるベクトル検索に加えて、「search」フィールドによるキーワード検索も行っています。

「アナキン・スカイウォーカーによって作成されて、とてもたくさんの言葉が話せる登場人物は?」というクエリに対して、それぞれキーワード検索、ベクトル検索、ハイブリッド検索を行ったときの結果を以下のように並べて比較することとします。

まずキーワード検索ですが、こちらはクエリを形態素解析した結果によって生成された「アナキン」「スカイウォーカー」という言葉に引っ張られて、ユーザーの意図とは関係のないドキュメントが上位に来てしまっています。結果として、ユーザーの意図が含まれるであろうドキュメント(C-3PO-03.txt)は、19位という低いランキングになっています。

ベクトル検索はどうでしょうか?こちらもキーワード検索と同様に、ユーザーの意図した回答が含まれるドキュメントは20位という低いランクに位置しています。

ハイブリッド検索では、これらの結果をRFFによって融合して、9位という高い結果に位置することになりました。

ちなみにこのときのスコアは、0.02547874115407467なので、これより定数kを求めてみます。RRFにより、以下の計算式が成り立ちます。

1/(k + 20) + 1/(k + 19) = 0.02547874115407467

この式から計算すると、Azure AI Searchでは、kは59のようです。

セマンティックハイブリッド検索

さらにここからがAzure AI Searchの真骨頂になります。先程のハイブリッド検索の結果をセマンティック検索で再ランキングしてさらに精度を高める「セマンティックハイブリッド検索」と呼ばれるスゴ技が提供されています。

実施方法はハイブリッド検索とさほどかわりはありません。ハイブリッド検索のときと同様に、まずはAzure OpenAI ServiceのEmbeddings APIにリクエストを発行し、検索クエリをベクトル化した後に、embeddingsという変数に格納します。

$ embedding=$(curl -s -X POST "https://[Azure OpenAI Serviceのサービス名].openai.azure.com/openai/deployments/text-embedding-ada-002-depoly/embeddings?api-version=2023-05-15" \
  -H 'Content-Type: application/json' \
  -H 'api-key: [Azure OpenAI ServiceのAPIキー]' \
  -d '{"input": "アナキン・スカイウォーカーによって作成されて、とてもたくさんの言葉が話せる登場人物は?"}' \
| jq '.data[0].embedding')

ベクトル化したクエリを用いて、Azure AI Searchに登録されたドキュメントに対して、以下のcurlコマンドによってセマンティックハイブリッド検索を行います。

$ curl -X POST "https://[Azure AI Searchのサービス名].search.windows.net/indexes/wiki/docs/search?api-version=2023-11-01" \
     -H "Content-Type: application/json" \
     -H "api-key: [Azure AI SearchのAPIキー]" \
     -d '{
            "count": true,
            "select": "title, content",
            "vectorQueries": [
                {
                    "fields": "contentVector",
                    "kind": "vector",
                    "vector": '$embedding',
                    "exhaustive": true
                }
            ],
            "queryType": "semantic",
            "search": "アナキン・スカイウォーカーによって作成されて、とてもたくさんの言葉が話せる登場人物は?",
            "semanticConfiguration": "config1",
            "answers": "extractive"
        }' | jq

先程のハイブリッド検索のコマンドと見比べてみますと、「queryType」フィールドに「semantic」を指定しています。「answers」や「semanticConfiguration」フィールドもあり、まさにセマンティック検索のリクエストの際に指定したフィールドをベクトル検索のリクエストに追加した形になっていることがわかります。

「アナキン・スカイウォーカーによって作成されて、とてもたくさんの言葉が話せる登場人物は?」というクエリに対して、それぞれキーワード検索、ベクトル検索、ハイブリッド検索、セマンティックハイブリッド検索を行ったときの結果を以下のように並べて比較することとします。

セマンティックハイブリッド検索では、ユーザーの意図している回答を含むドキュメント(C-3PO-03.txt)がさらに上位に位置し、2位になっていることがわかります。

キーワード検索→ベクトル検索→ハイブリッド検索→セマンティックハイブリッド検索と実施するごとに、19位→20位→9位→2位という形で順位が上がり、検索結果の精度が向上することがわかりました。

Azure AI Searchではこれらの検索手法を効果的に組み合わせることにより、検索結果の精度をかなり高めることができます。

まとめ

RAG全盛期である昨今、やはり検索システムの精度向上は最重要課題です。Azure AI Searchで提供される様々な機能を利用すれば、よりユーザーの意図にマッチした回答を得ることができます。これには、セマンティック検索やハイブリッド検索といった先進的な技術が鍵となります。

セマンティック検索は、クエリの深い意味を解析し、単なるキーワードマッチングを超えた結果を提供することで、情報の精度と関連性を大幅に向上させます。また、ハイブリッド検索では、従来のキーワード検索とベクトル検索の利点を組み合わせることで、より精度の高い検索結果を提供します。。

これらの機能により、Azure AI Searchは情報検索の新たな地平を切り開き、ユーザーが求める正確な情報へと迅速にアクセスできるようになります。データの質と検索技術の進化が進む中で、これらのツールを最大限に活用することは、企業や開発者にとって重要な戦略の一つとなるでしょう。

※ 本記事執筆については以下の素晴らしいサイトを参考にさせていただきました。

■ Azure Cognitive Search 全文検索の新機能セマンティックアンサー・キャプションを表示するデモ
https://qiita.com/nohanaga/items/4b9506c62b9e5796f405

アバター画像
About 武井 宜行 269 Articles
Microsoft MVP for Azure🌟「最新の技術を楽しくわかりやすく」をモットーにブログtech-lab.sios.jp)で情報を発信🎤得意分野はAzureによるクラウドネイティブな開発(Javaなど)💻「世界一わかりみの深いクラウドネイティブ on Azure」の動画を配信中📹 https://t.co/OMaJYb3pRN
ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

3人がこの投稿は役に立ったと言っています。


ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

Be the first to comment

Leave a Reply

Your email address will not be published.


*


質問はこちら 閉じる