Azure AI Searchのセマンティックハイブリッド検索によるRAGの性能向上について

alt

はじめに

以前、弊社より以下の記事を紹介させていただいておりました。
1. Azure OpenAI Serviceで社内版ChatGPTのChatPCAを構築した話
2. Azureで社内文書から回答可能な生成AIチャットサービスを作った話

今回は、以前紹介させていただいたChatPCA*1の社内文書検索機能において、Azure AI SearchのRAGの精度向上の取り組みについてお話しさせていただきます。
そちらにあたって、パーソルキャリアにおける生成AIチャットサービスの概要について気になる方は、まず上記の記事を一読いただけたらと思います。

Azure AI Searchは、ElasticSearchと同様にApache Luceneを使用しており、その理解にはApache Luceneの検索エンジンの仕組みを知ることが大いに役立ちます。一方で、Azure AI Search独自の仕組みもあり、その理解を深めることで、より性能の良いRAG(Retrieval-Augmented Generation)が可能になります。
今回は、そのようなAzure AI Searchの機能の中で、特にセマンティックハイブリッド検索について解説します。
この技術は、従来のフルテキスト検索やベクトル検索に加え、検索クエリと結果の意味を考慮することで、より精度の高い検索結果を提供します。
本記事では、Azure AI Searchのセマンティックハイブリッド検索の仕組みや、検索精度向上のアプローチ、トークン分割結果によるパフォーマンスの違いについて詳しく説明します。

目次

  1. Azure AI Searchの検索手法について
  2. 検索のパフォーマンス向上のアプローチ
  3. トークン分割結果による検索精度の違い
  4. 結論

Azure AI Searchの検索手法について

まず前提知識として、Azure AI Searchの検索の概要に関しては、以下の記事が参考になると思います。 Azure AI Search (旧:Azure Cognitive Search) で使用できる検索方法をまとめた

今回は上記の記事を一読いただいている前提のもとで話を進めさせていただきます。 上記で紹介されているフルテキスト検索とベクトル検索を組み合わせた検索としてハイブリッド検索がありますが、更にそこに対してセマンティック検索を組み合わせたものとしてセマンティックハイブリッド検索があります。

セマンティックハイブリッド検索に関しては、
Azure Cognitive Search のベクトル検索とハイブリッド検索の比較デモネタ
こちらの記事が非常にわかりやすいので一読をお勧めします。
Azure Cognitive Search にベクトル検索機能が搭載されプライベートプレビューが開始
また、こちらの記事にもそれについての紹介があります。

簡単にまとめると、Azure AI Searchには以下の検索メソッドがあります。

  • フルテキスト検索
  • ベクトル検索
  • ハイブリッド検索
  • セマンティックランク付け

こちらに関しては、それぞれの検索におけるスコアリングの意味に関して、以下の記事にてまとめられています。
Azure AI Searchの検索手法とスコアリングについて

検索のパフォーマンス向上のアプローチ

上記の中で特にセマンティックランク付け(セマンティックランカー)は強力なパフォーマンス向上を提供してくれて、前段の検索(フルテキスト or ベクトル or ハイブリッド検索)結果のうち上位50位に絞り、Microsoftで開発しているマルチリンガルなディープラーニングモデルを使用して、セマンティック(意味論的)に関連する最も重要な結果を優先します。 ただ、こちらも高度な計算をする分検索速度に影響が出るデメリットもあります。そちらも考慮に入れつつ検索精度の向上を行う必要があります。

一方で、それ以外にもAzure AI Searchのパフォーマンスを向上させるための主要なチューニングポイントには以下のようなものがあります。
検索精度と検索速度の両観点がありますが、代表的なものをピックアップさせていただきます。
詳細は、Azure AI Search のパフォーマンス向上に関するヒントをご参照ください。

  1. インデックスサイズとスキーマ:

    • インデックスは小さいほどクエリのパフォーマンスが向上します。これは、スキャンするフィールドが少なく、また、システムが将来のクエリのためにコンテンツをキャッシュする方法によるものです。
    • インデックス構成を定期的に見直し、内容を削減する機会を探ることが推奨されます。
  2. クエリ設計:

    • 検索対象となるフィールドの数を制限することで、サービスの負担を軽減できます。「searchFields」パラメータを使用して、必要なフィールドのみを指定します。
    • 部分一致検索などの計算コストが高い検索を制限します。
  3. サービス容量:

    • サービスのティアやレプリカ、パーティションの数がパフォーマンスに大きく影響します。
    • 必要に応じてサービスのアップグレードや容量の追加が有効です。
  4. 高カーディナリティフィールドの制限:

    • ユニーク値が多いフィールドは、計算リソースを大量に消費するため、これらのフィールドの使用を最小限に抑えることが重要です。
  5. 複雑なタイプへの対応:

    • 複雑なデータタイプは追加のストレージ要件とインデックス作成リソースを必要とします。これを避けるために、より単純なフィールドタイプにマッピングするか、フィールド階層をフラットにすることが有効です。
  6. 正規表現クエリの使用制限:

    • 正規表現クエリは処理能力を大量に消費するため、簡略化するか、より管理しやすい小さなクエリに分割することを検討してください。

これらのチューニングポイントを適切に組み合わせることで、Azure AI Searchのパフォーマンスを効果的に向上させることができます。

今回は比較的直感的にアプローチがしやすいトーカナイザーに関して、検索精度の観点からパフォーマンスの向上を模索してみたいと思います。

トークン分割結果による検索精度の違い

今回は社内用語であるAZに関してRAGで検索をしてみたいと思います。 AZというのはパーソルキャリアの社内用語で「案件増加」の略称です。 また、今回LLMモデルとしてGPT-3.5を用いています。

それでは、以下の問い合わせクエリで回答が生成可能かを図りたいと思います。

AZについて教えてください。

上記のAzure AI Search (旧:Azure Cognitive Search) で使用できる検索方法をまとめたにおいても説明があったように、字句解析のフェーズにおいてアナライザーが適用され、上の問い合わせクエリが各トークンに分割されます。今回日本語を用いていますので、アナライザーとしてはja.luceneを使っています。(参考:Azure AI Search インデックスの文字列フィールドに言語アナライザーを追加する)

トークン分割をした結果は以下の通りです。

"tokens": [
    {
        "token": "az",
        "startOffset": 0,
        "endOffset": 2,
        "position": 0
    },
    {
        "token": "教える",
        "startOffset": 6,
        "endOffset": 8,
        "position": 2
    },
    {
        "token": "くださる",
        "startOffset": 9,
        "endOffset": 13,
        "position": 4
    }
]

わかりやすく「AZ」は小文字に変換され、助詞である「について」は除外され、 動詞の「教えて」と補助動詞の「ください」がそれぞれ基本形である「教える」と「くださる」に変換されていますね。 改めてまとめると以下のような分割がされたことになります。

問い合わせクエリ: "AZについて教えてください。"
  ├── トークン1: "az"
  ├── トークン2: "教える" (元: "教えて")
  └── トークン3: "くださる" (元: "ください")

さらに、文書側に同様の記載がある場合は、同様の変換がされた転置インデックスが作成されることになります。

それではこちらに関して以下のAPIに対してリクエストをかけてみます。 ドキュメントの検索 (Azure AI Search REST API)

POST https://[service name].search.windows.net/indexes/[index name]/docs/search?api-version=[api-version]
Content-Type: application/json
api-key: [admin or query key]

リクエストボディは以下の形になります。

{
    "search": "AZについて教えてください。",
    "vectorQueries": [
        {
            "kind": "vector",
            "k": 5,
            "oversampling": null,
            "fields": "vector",
            "vector": [
                0.003600474,
                ...,
                -0.04504809
            ],
            "text": null,
            "exhaustive": null,
            "weight": null
        }
    ],
    "queryType": "semantic",
    "semanticConfiguration": "vector-test-configuration",
    "captions": "extractive|highlight-true",
    "answers": "extractive|count-3",
    "queryLanguage": "ja-JP",
    "top": 6,
    "select": "doc_chunk_id, doc_id, doc_chunk, title",
    "count": "true"
}

vector-test-configurationというsemanticの設定がされている前提です。 今回ベクトルの埋め込みにはtext-embedding-ada-002を用いています。

特出するべき点として、まず上記の点でセマンティックハイブリッド検索を実現していて、セマンティックアンサーとセマンティックキャプションのハイライトを取得したいため、captionsとanswersの設定をしています。

それではAPIをコールしてみます。

{
    "@odata.context": "xxxx",
    "@odata.count": xxxx,
    "@search.answers": [],
    "value": [
        {
            "@search.score": 0.012500000186264515,
            "@search.rerankerScore": 2.228304862976074,
            "@search.captions": [
                {
                    "text": "社内用語に関するキャプション",
                    "highlights": ""
                }
            ],
            "doc_chunk_id": "doc_chunk_id1",
            "doc_id": "doc_id1",
            "doc_chunk": "社内用語に関するチャンク",
            "title": "社内用語.pdf"
        },
        {
            "@search.score": 0.009803921915590763,
            "@search.rerankerScore": 1.734757900238037,
            ...
        },
        {
            "@search.score": 0.010526316240429878,
            "@search.rerankerScore": 1.6390165090560913,
            ...
        },
        {
            "@search.score": 0.01075268816202879,
            "@search.rerankerScore": 1.5899969339370728,
            ...
        },
        {
            "@search.score": 0.010989011265337467,
            "@search.rerankerScore": 1.5654871463775635,
            ...
        },
        {
            "@search.score": 0.016393441706895828,
            "@search.rerankerScore": 1.5651041269302368,
            ...
        }
    ]
}

リクエストでtopを6としたので、セマンティックランカーによるリランキングされたスコアの高い順上から6件のみが返却されました。ただ、残念なことに@search.answersとsearch.captionsのhighlightsが空になっています。こちらはトークン分割された際の「教える」と「くださる」がノイズになってしまっていることが想像できます。社内用語を解説する際にこういった用語は不要なことが多いですからね。

それでは今度はこの不要な用語を弾くために名詞である「AZ」のみを用いてリクエストをかけてみたいと思います。アプリ上で実現したい場合はkuromojiやmecab等の形態素解析のツールを使って実現ができると思います。

{
    "search": "AZ",
    "vectorQueries": [
        {
            ...
            "vector": [
                        0.001371399,
                        ...,
                        -0.030971114
                    ],
                ...
                }
            ],
    ...
}

結果は以下の通りです。

{
    "@odata.context": "xxxx",
    "@odata.count": xxxx,
    "@search.answers": [
        {
            "key": "key1",
            "text": "社内用語に関するセマンティックアンサー",
            "highlights": "",
            "score": 0.77685546875
        }
    ],
    "value": [
        {
            "@search.score": 0.01666666753590107,
            "@search.rerankerScore": 2.1953699588775635,
            "@search.captions": [
                {
                    "text": "社内用語に関するキャプション"
                }
            ],
            "doc_chunk_id": "doc_chunk_id",
            "doc_id": "doc_id",
            "doc_chunk": "社内用語に関するチャンク",
            "title": "社内用語.pdf"
        },
        {
            "@search.score": 0.016393441706895828,
            "@search.rerankerScore": 1.7586933374404907,
            ...
        },
        {
            "@search.score": 0.016129031777381897,
            "@search.rerankerScore": 1.2303920984268188,
            ...
        },
        {
            "@search.score": 0.01666666753590107,
            "@search.rerankerScore": 1.0894607305526733,
            ...
        },
        {
            "@search.score": 0.015625,
            "@search.rerankerScore": 1.051164150238037,
            ...
        },
        {
            "@search.score": 0.01587301678955555,
            "@search.rerankerScore": 1.029718041419983,
            ...
        }
    ]
}

前回と違う点はまずセマンティックアンサーを取得できるようになった点です。 残念ながら今回もセマンティックアンサーにおいてもキャプションにおいてもハイライトは取得できませんでした。 また、同じチャンクを取得する箇所でsearch.scoreが上がり、search.rerankerScoreが下がっています。 こちらについて考えてみましょう。

説明のため、前者(トークン分割結果がAZ, 教える, くださる)のケースを「名詞+動詞」、後者(トークン分割結果がAZ)のケースを「名詞のみ」と表現します。
上記の2パターンの検索について以下のように整理し直してみます。

「名詞+動詞」の場合:
  - search.score: 0.0125
  - search.rerankerScore: 2.22

「名詞のみ」の場合:
  - search.score: 0.0166
  - search.rerankerScore: 2.19

*: search.scoreはハイブリッド検索(全文検索とベクトル検索の
組み合わせ)による結果を表し、search.rerankerScoreはそこに
更にセマンティックランカーを通してセマンティックハイブリッド
検索の結果を表す

まず「名詞のみ」において、search.scoreが(0.0125→0.0166に)上がる理由に関して説明します。
「教える」と「くださる」という表記が除外されましたが、まずフルテキスト検索においてはドキュメント内に「教える」と「くださる」の表現が登場しないケースのため、「名詞+動詞」と「名詞のみ」においてのBM25のスコアリングに差は出ないです。
(なぜこのケースでスコアリングに差が出ないかが気になる方は【自然言語処理】BM25 - tf-idfの進化系の実践類似度分析【Elasticsearch への道②】)、こちらの動画を観てみるのをお勧めします。

フルテキスト検索:
  - クエリ: "AZについて教えてください。"
  - 「名詞+動詞」と「名詞のみ」でスコアリングに差が出ない。

ただ、「名詞+動詞」のベクトル検索においては逆にこの2単語の登場がノイズとなることで、「名詞のみ」よりも「名詞+動詞」のスコアリングを下げてしまい、結果的にRRFの計算結果であるsearch.scoreの値が「名詞+動詞」の方が下がってしまっている形になります。
ですので、もしハイブリッド検索のみを扱っている場合は今回のケースですと、「名詞のみ」の形でトークン分割の際に動詞は混ぜない形でカスタマイズするのが良いでしょう。

ベクトル検索:
  - クエリ: "AZについて教えてください。"
  - 「名詞+動詞」の場合:
    - ベクトル: [0.003600474, ..., -0.04504809]
    - スコア: 低
  - 「名詞のみ」の場合:
    - ベクトル: [0.001371399, ..., -0.030971114]
    - スコア: 高

→フルテキスト検索のスコアリングに差はないため、結果として
ベクトル検索のスコアリングが高い「名詞のみ」のケースの方が
ハイブリッド検索のスコアリングが高くなる

ただ、今回はセマンティック検索によるリランキングが走っていて、そこで「名詞+動詞」においてスコアリングが逆転しているのがわかると思います。 こちらについて考えてみます。

「名詞+動詞」の場合:
  - search.score: 0.0125(こちらの方がスコアリングは低いが)
  - search.rerankerScore: 2.22(こちらで逆転している)

「名詞のみ」の場合:
  - search.score: 0.0166
  - search.rerankerScore: 2.19

AZは一般的にはAvailability ZoneやAstraZenecaの略称だったりします。ですので、そのままOpenAI等のモデルに問い合わせた場合はそちらの情報に誘導されてハルシネーションを起こす可能性が高いです。
上記で紹介したAzure Cognitive Search のベクトル検索とハイブリッド検索の比較デモネタ の記事の解説であったように、今回も「AZ」に関してMicrosoft 製のLLMである Turing モデルが文章中から抽出的要約タスクを実行するのですが、AZというものが何かわからないために、文字通り「AZに関して解説している文書」を取得しようとするはずです。今回はAZが社内用語として案件増加であることを解説する文書が含まれているため、「名詞のみ」という形でシンプルに「AZ」のみで問い合わせる時よりも「名詞+動詞」の形で問い合わせる方がそういった文書を探し当てたい意図がよく伝わり、スコアリングが上がっていることが理解できると思います。

セマンティック検索リランキング:
  - クエリ: "AZについて教えてください。"
  - 「名詞+動詞」の場合:
    - AZの文脈理解が向上し、関連性の高い文書を特定。
    - セマンティックランキングスコアが高くなる。
  - 「名詞のみ」の場合:
    - AZが一般的な略称と解釈される可能性。
    - ハルシネーションが発生しやすく、スコアが低くなる。

ですので、セマンティックランカーを使えばベクトル検索のノイズ影響を無視できる水準でスコアリングを向上できるので、求める文書をより特定しやすくなるメリットがあります。
もしこのセマンティックランカーのモデルもfine-tuningが可能であれば、AZと案件増加という用語の紐付けがより容易になり、文書特定の精度もより上がると思います。ただ、現状はそれが不可能なので、RAG文書側で明示的にAZの記載を増やすなどして検索の精度を上げる必要があると思います。

セマンティックハイブリッド検索についてまとめると:
  - 「名詞+動詞」のクエリにより、社内用語「AZ」を含む文書の特定精度が向上。
  - ベクトル検索のノイズ影響が少なく、リランキングにより高スコアを取得。
  - セマンティック検索モデルのファインチューニングが可能であれば、更に精度向上が期待できる。
  - 現状では、RAG文書内でAZの記載を増やすことで精度向上を図る。

余談ですが、トークン分割の際に独自の日本語アナライザーを実装する方法もありますので(Azure Cognitive Search に独自の日本語アナライザーを実装する?)、一度試してみても良いかもしれませんが、そうすることで考慮する事項も増えてしまうデメリットも存在します(詳細はこちらの記事をご覧ください。)ので、どうしても独自に持たないとパフォーマンスの向上が見込めない場合を除いては、Microsoft側のja.luceneの発展を待つ形の方が良いかと思っています。

結論

セマンティックハイブリッド検索は、Azure AI Searchの強力な機能であり、検索の精度と関連性を大幅に向上させます。今回のケースでは、「AZ」という社内用語に対応するために、トークン分割やクエリの最適化を行い、より適切な検索結果を得ることができました。特に、セマンティックランカーを活用することで、ユーザーの意図に沿った高精度な検索結果を提供できる点が大きな利点です。

パフォーマンスチューニングにおいては、インデックスサイズの最適化やクエリ設計の工夫が重要であり、これらの調整を通じて検索の効率性を高めることが可能です。こちらに関してはまた別途記事を書けたらと思います。 また、今回はAzure AI Searchにフォーカスを当てていますが、他にも様々なベクトルDBが存在しますので、比較記事等執筆をできたらと思っています。

注釈

  • *1: ChatPCAとは、Chat + PCA(パーソルキャリアの略称)のこと

梅本 誠也 Seiya Umemoto

デジタルテクノロジー統括部 デジタルソリューション部 クライアントエンジニアグループ エンジニア

韓国で5年間正規留学し、その間に業務委託で機械学習とデータエンジニアリング方面の開発を経験。新卒でアプリケーションエンジニアとしてフロントエンド、バックエンド、インフラを幅広く経験。パーソルキャリア入社後はデータエンジニアとして、社内のデータ分析基盤の構築と運用保守を担当。一方で、生成系AIを用いたアプリケーション開発にも携わっている。

※2024年5月現在の情報です。