Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Elasticsearchのハイブリッド検索を用いて高精度なRAGを簡単に実現する

こんにちは。
Acroquestのデータサイエンスチーム「YAMALEX」に所属する@shin0higuchiです😊
YAMALEXチームでは、コンペティションへの参加や自社製品開発、技術研究などに日々取り組んでいます。

はじめに

近年、生成AIの発展により、RAG(Retrieval-Augmented Generation)が注目を集めています。RAGは既存の知識ベースから関連情報を検索し、それを基に生成AIが回答を生成する手法です。本記事では、多くの企業ですでに利用されているElasticsearchを使って、シンプルなRAGシステムを構築する方法をご紹介します。

RAGの基本概念

RAGはおおまかに以下の3つのステップで構成されています:

1. 知識ベースの構築(Indexing)
2. 関連情報の検索(Retrieval)
3. LLMによる回答の生成(Generation)

Elasticsearchは特に1と2のステップで強力な機能を提供し、既存のインフラを活用してRAGを実現できる点が魅力です。



より細かく図解するなら以下のようなイメージになるかと思います。


RAGが良く用いられる事例のひとつとして、製品の「よくある質問」のような内容をデータベースに格納しておき、その情報をもとにLLMに回答させるというものがあります。
たとえば「バックアップシステムが動作しない場合の解決方法を教えてください。」「パスワードを忘れた場合のリセット方法は? 」といった、よくある質問をドキュメントとして取り込んでおくのです。
そうすると、ユーザの質問に近いものがあればLLMがその情報に基づいて答えてくれます。

今回はそういった題材をイメージしてお読みいただければと思います。

Elasticsearchを使ったRAGの実装

1. インデックスの設計

まず、ドキュメントを適切に格納するためのインデックスを設計します。
検索においては、より正確性の高いものを少数返すか、正確性の低いものも含めて関連しそうなものを広く返すか、状況によって戦略が異なります。
近年の高精度なLLMによるRAGにおいては、多少関連性の薄いものが混じったとしても、関連のありそうなものをより広く返すのがメジャーです。

そのため、今回はElasticsearchでキーワード検索だけでなく、ベクトル検索を併せて利用するための設定をおこないます。
以下はマッピングの例です。
titleおよびcontentというフィールドをテキスト検索に利用し、embeddingフィールドをベクトル検索用に利用します。

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "kuromoji"
      },
      "content": {
        "type": "text",
        "analyzer": "kuromoji"
      },
      "embedding": {
        "type": "dense_vector",
        "dims": 768
      }
    }
  }
}

2. ドキュメントの登録

テキストデータをElasticsearchに登録する際、以下の処理を行います

1. テキストの前処理(クリーニング、チャンク分割)
2. ベクトル化(文章をベクトルに変換)
3. Elasticsearchへの登録

def index_document(es_client, text, title):
    # テキストをチャンクに分割
    chunks = split_text_into_chunks(text)
    
    # 各チャンクを処理
    for chunk in chunks:
        # ベクトル化(sentence-transformersなどを使用)
        embedding = get_embedding(chunk)
        
        # ドキュメントの作成
        doc = {
            'title': title,
            'content': chunk,
            'embedding': embedding
        }
        
        # Elasticsearchに登録
        es_client.index(
            index='knowledge_base',
            document=doc
        )

3. 検索の実装

ユーザーの入力文字列をキーワード・ベクトルのハイブリッドで検索します。
キーワードとベクトルのスコアの調整は様々な手法がありますが、ここでは最もシンプルにキーワード検索とベクトル検索のスコアを足し合わせています。
RRFという手法を利用して2つの検索ランキングをマージすることなども可能したり、より細かいスコア調整を行うことも可能です。
詳細はこちらのリファレンスを参照してください→ k-nearest neighbor (kNN) search | Elasticsearch Guide [8.17] | Elastic

def hybrid_search(es_client, query):
    # クエリのベクトル化
    query_vector = get_embedding(query)
    
    # ハイブリッド検索のクエリ
    search_query = {
        "query": {
            "script_score": {
                "query": {
                    "match": {
                        "content": query
                    }
                },
                "script": {
                    "source": "cosineSimilarity(params.query_vector, 'embedding') + 1.0",
                    "params": {
                        "query_vector": query_vector
                    }
                }
            }
        }
    }
    
    return es_client.search(
        index='knowledge_base',
        body=search_query
    )


ここで、ベクトルだけを利用して検索した場合と、キーワード検索を組み合わせた場合とで、検索結果の一例を確認してみます。
次の例は、「パスワードの再設定方法を知りたい」で検索し、
「ベクトル検索のみ」と「キーワード+ベクトル検索」のそれぞれでランキング順に結果を並べたものです。

ベクトル検索だけでもある程度近い意味合いの文章が上位にランクインするのですが、パスワード関連の内容が1位に来ていないなど、精度不足が見て取れます。
一方、「キーワード+ベクトル検索」の方では、意図したとおりにパスワード関連の内容が1,2位と上位に来ています。
ベクトル検索も、「パスワード」という直接的な語を含まない類似文章をヒットさせられるという点で有用なのですが、キーワード検索と組み合わせることでさらに良い結果が得られるイメージですね。

順位 ベクトル検索のみ キーワード+ベクトル検索
1 ITサポートに問い合わせる方法を知りたいです。 パスワードを忘れた場合のリセット方法は?
2 パスワードポリシーを設定するにはどうすればよいですか? パスワードポリシーを設定するにはどうすればよいですか?
3 パスワードを忘れた場合のリセット方法は? ITサポートに問い合わせる方法を知りたいです。
4 ユーザーアカウントの権限変更手順について説明してください。 新しいソフトウェアをインストールする際の注意点は?
5 新しいソフトウェアをインストールする際の注意点は? セキュリティの更新手順について教えてください。
6 セキュリティの更新手順について教えてください。 ユーザーアカウントの権限変更手順について説明してください。
7 ネットワークのパフォーマンスが低下した際の対処法は? バックアップシステムが動作しない場合の解決方法を教えてください。
8 クラウドサービスのストレージ容量を確認するには? ネットワークのパフォーマンスが低下した際の対処法は?
9 バックアップシステムが動作しない場合の解決方法を教えてください。 クラウドサービスのストレージ容量を確認するには?
10 システムログを分析してエラーを特定する手順は? システムログを分析してエラーを特定する手順は?

4. LLMとの連携

検索結果を基に、LLMを使って回答を生成します:

def generate_answer(query, search_results):
    # コンテキストの準備
    context = "\n".join([hit["_source"]["content"] for hit in search_results["hits"]["hits"]])
    
    # プロンプトの構築
    prompt = f"""
以下のコンテキストを元に、質問に回答してください。

コンテキスト:
{context}

質問:
{query}
"""


#LLMによる回答生成
return call_llm_api(prompt)


これにより、Elasticsearchが適切な情報をLLMに渡し、回答生成を行ってくれます。

まとめ

Elasticsearchを活用することで、簡単にRAGシステムを構築する方法をご紹介しました。
Elasticsearchは全文検索とベクトル検索のハイブリッド検索を実現することができる検索エンジンです。また、精度の高い日本語検索を実現可能であるため、一般的なベクトルデータベースに比べると、日本語を扱うRAGシステムにおいて大きな強みがあると言えます。

既存のElasticsearchクラスタがある場合は、特に導入のハードルが低くなります。
是非身近なデータを用いて試してみてください。

今回の記事は以上となります。お読みいただきありがとうございました。



Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com