Vespaで試す検索とLLM(RAG)

    この投稿をXにポストするこの投稿をFacebookにシェアするこのエントリーをはてなブックマークに追加

今回のモチベーション

ざっくり下記のようなモチベーションから良いものはないかということで、Vespaに興味を持ちだしました。

  • RAGの利用を検討すると、必然的にベクトルDBのあり方を考えることは多い
  • ただし、それはRAGを機能として実現するために!という観点で、実際の業務や導入も考えると、シンプルな検索、シンプルなLLMの利用など使い分けも必要とされる
  • また、Docに対しての「参照」のバリエーションが増えることで、RAGを用いたバリエーションも増える
  • なので、「シンプルな全文やキーワード検索」「ベクトルベースでの近傍検索」などと、LLMの利用をいい感じで組み合わせたり手軽に行いたい
  • Langchainで組み合わせればできるが、面白味に欠けるし、そもそもプロダクションとしては将来的にキツイのでは(要らないものが多すぎて...)

Vespaについて

検索界隈はほとんど分からないのですが、検索エンジンとして Elastic を知っていらっしゃる方も多いのではないでしょうか。
ちなみに、Chat GPTでVespaについて教えてというと、下記のような回答が得られます。

Vespaの概要
 用途: Vespaは大規模な検索エンジンやデータ管理システムとして利用されます。特に、企業が大量のデータから情報を迅速に検索・分析するためのソリューションとして設計されています。
 背景: Vespaは、元々はYahoo!の検索エンジン技術として開発されていました。Yahoo!が買収したAltavistaの技術を基に、さらに進化させたものです。
    現在、Vespaはオープンソースとして利用されており、企業や開発者が自分たちのニーズに合わせてカスタマイズできるようになっています。

主な機能:

 検索機能: 高速でスケーラブルな検索を提供します。特に大規模なデータセットに対して、リアルタイムの検索結果を提供できます。
 データストレージ: テキストデータだけでなく、構造化データや非構造化データの保存と管理も可能です。
 ランキング: 高度なランキング機能を備えており、ユーザーのクエリに基づいて関連性の高い結果を優先的に表示できます。
 リアルタイム更新: データの変更をリアルタイムで反映できるため、最新の情報に基づいた検索結果を提供します。

アーキテクチャ:

 分散型システム: Vespaは分散型アーキテクチャを採用しており、複数のサーバーにまたがるデータストレージと処理が可能です。
 スケーラビリティ: 大量のデータやリクエストに対応できるように設計されています。必要に応じてシステムをスケールアップすることができます。

利用例:

 検索エンジン: 特定の業界や企業内でのカスタム検索エンジンの構築に利用されることが多いです。
 データ分析: 大規模なデータセットを迅速に検索・分析するために使用されます。
 コンテンツ管理: 大規模なコンテンツライブラリの管理や検索に利用されることもあります。
 Vespaは、高度な検索機能と大規模データの処理能力を兼ね備えた強力なツールであり、多くの企業や開発者が利用しています。

もう少し踏み込んで、回答をしてもらうと

Vespaの優れた点
リアルタイム処理とスケーラビリティ:

 Vespaはリアルタイムのデータ処理と検索の能力が高く、特に大規模なデータセットに対して強力です。データの更新が即座に反映されるため、最新の情報を迅速に検索結果に反映させることができます。
 分散型アーキテクチャにより、大規模なデータと高トラフィックな要求にスケールする能力があります。データのパーティショニングと複製に優れ、水平スケーリングが得意です。

複雑なランキングと検索機能:

 Vespaは高度なカスタマイズ可能なランキング機能を提供します。特定のビジネスロジックやドメイン固有のニーズに合わせて、検索結果の順位を細かく調整できます。
 複雑なクエリやフィルタリング、集計を効率的に処理できるため、特定のユースケースやカスタム要求に柔軟に対応できます。

統合されたデータストレージと検索:

 Vespaはデータストレージと検索機能を統合しており、検索エンジンとしてだけでなく、データベースとしても使用できます。これにより、データの一貫性と検索性能の両方を確保できます。

リッチなAPIと柔軟性:

 VespaのAPIは非常に柔軟で、複雑な検索クエリやカスタムの処理を実装する際に便利です。カスタムスコアリングやパーソナライズされた検索結果を提供するための機能が豊富です。

リアルタイムのデータ更新:

 Vespaはデータのリアルタイム更新に非常に強く、変更が即座に検索結果に反映されます。これにより、ダイナミックなデータが要求されるアプリケーションで優れたパフォーマンスを発揮します。

Elasticsearchとの比較
Elasticsearchの優れた点:

 簡単なセットアップ: Elasticsearchは設定と運用が比較的簡単で、初心者でも扱いやすいです。
 エコシステム: Elasticsearchは豊富なエコシステムを持ち、Kibanaなどのツールと統合することで、データの可視化や管理が容易です。
 コミュニティとサポート: Elasticsearchは広範なコミュニティとサポートがあり、豊富なドキュメントとリソースが提供されています。

Vespaの使用に向いているシナリオ:

 高度なリアルタイム処理や大規模データセットの検索・管理が求められる場合。
 複雑なカスタムランキングやパーソナライズ機能が必要な場合。
 データベースと検索エンジンの両方の機能が必要な場合。

今回触ってみたざっくりの感覚としては、純粋な検索に対して 多機能カスタマサイズ性が高い という印象であり
先に記載のある通り、 複雑なランキングの実装ベクトルの管理 がデフォルトで可能です。MLやDeepが流行り出したタイミングには機能として実装されていたと思いますが
検索 x MLというユースケースが大きく開いてない点や、その上でベクトルストアやフィーチャーストアとして機能を求められるケースが
どちらかというニッチな形だったのではないかという印象です。その点、LLMの普及や検索との組み合わせ・管理が必要とされる現在では、有用なシーンも多いのではと思いました。

その他、確かにGPTの回答の通り、Elasticの方がお手軽(敷居が低い)な印象も持ちましたが
それは日本語Documentの豊富さなども起因しているかもしれません...(Vespa本当に少ない)


ただ、昔に書かれた下記の日本語tutorialドキュメントは、現役で利用できる素晴らしいまとめで、大変助かりました!


Vespaの環境準備

今回はDocker環境上に試用で立ち上げてみたので、上記の日本語tutorialと、公式のクイックスタートなどを参考にしました。


今回は日本語を扱う検証を行いたかったので、Linguisticsは kuromoji を利用させてもらいました。
こちらも日本語tutorialやrepo側のREADMEに記載されている手順で進めれば問題なく、反映されます。

事前に別言語ベースでスキーマを作っている場合などは、明確に再作成が必要となりますので注意ください。


スキーマ

では、検証に向けて簡単なスキーマを用意してみましょう。

schema qa {
    document qa {

        field language type string {
            indexing: "ja" | set_language
        }

        field category type string {
            indexing: summary | index
        }

        field title_text type string {
            indexing: summary | index
        }

        field qa_text type string {
            indexing: summary | index
        }

    }

    field embeddings type tensor<bfloat16>(x[384]) {
        indexing {
            input title_text | embed e5 | attribute | index
        }
    }

    rank-profile default {
        inputs {
            query(q) tensor<float>(x[384])
        }
        first-phase {
            expression: cos(distance(field,embeddings))
        }
   }
}

以下、箇条書きで補足です。

  • QAのデータを入れるつもりで、基本的に全てのフィールドに日本語が入る
  • フィールドの embeddings にはベクトル情報が入ります。後ほど補足
  • rank-profile 部分には inputsを持ち、検索インプットに対してベクトルのコサイン距離で結果を返すことを明示

データ

NTT docomo よくあるご質問 からピックアップして、スキーマに合わせる形で作成しました。

{"put": "id:mynamespace:qa::1", "fields": {"category": "新規契約", "title_text": "乗り換え(MNP)の手数料はいくらですか?", "qa_text": "契約事務手数料3,850円(税込)がかかります。なお、ドコモオンラインショップでお手続きされる場合、新規契約事務手数料は無料です" }}
{"put": "id:mynamespace:qa::2", "fields": {"category": "新規契約", "title_text": "新規契約は代理人でも手続きできますか?", "qa_text": "契約者のご家族/法定代理人であればお手続きできます。" }}
{"put": "id:mynamespace:qa::3", "fields": {"category": "請求・お支払い方法", "title_text": "dカードの明細を確認したい", "qa_text": "カードご利用明細照会へログインしてご確認いただけます" }}
{"put": "id:mynamespace:qa::4", "fields": {"category": "請求・お支払い方法", "title_text": "ドコモの利用料金の支払い状況を確認したい", "qa_text": "お支払い状況は、NTTファイナンスが提供するWebビリングにログイン後、「お支払い・請求状況」からご確認ください。" }}
{"put": "id:mynamespace:qa::5", "fields": {"category": "dポイント", "title_text": "dポイントカード番号を確認したい", "qa_text": "dポイントカード、モバイルdポイントカードのバーコードの下に、15桁の番号が記載されています。また、My docomo(Web)の「dポイントカード登録情報」からも、先頭から8桁の番号を確認できます。" }}
{"put": "id:mynamespace:qa::6", "fields": {"category": "dポイント", "title_text": "dポイントをあげる/もらうことはできますか?", "qa_text": "dポイントクラブ会員同士であれば、dポイントをあげる/もらうことができます。なお、ポイントを送るにはd払いアプリが必要です。" }}

services.xml

今回の肝となる部分ですが、Vespa側の公式Docには下記のような記事があります
Large Language Models in Vespa
Retrieval-augmented generation (RAG) in Vespa


要約すると下記です。いい感じで組み込まれている印象です。

  • Vespa側でLLMのクライアントは用意されている(Vespa バージョン 8.327 以上)
  • LLM側のパラメータの簡易なものは利用可能で、LLMのモデルなども指定可能(ニッチなものでも、おそらくコンポーネントをカスタムすることで柔軟に対応可能なはず)
  • RAGについてもVespa側でカバー済み、yql (SQLに近しいクエリでの検索) の結果をもとに、LLMでの回答を1ライナーで実行できる

上記の機能を利用するために、services.xml 側に追記を進めましょう。
LLMのモデルは今回 GPT-4o mini を使ってみたので、OpenAI を指定しています。
また、先ほどのスキーマにあったembeddingのモデルをこちらで指定しており、e5-small-v2 を利用しています。そのembeddingのサイズが 384 設定のため
先ほどのスキーマ側にもinputとして同じサイズを受けるように設定をしています。地味にembedの指定のみでベクトル化されるのも便利なところです(失念しましたが、起動条件にVespaのバージョン指定があったと思います)
詳細は 公式のEmbedding を参考にしてください。

※一点追記ですが、e5-small-v2は英語推奨モデルとhugging face側に記載されています、、この後の検証では日本語でも起動した、かつ、少ないデータセットだったのでそのまま検証を終えましたが、実際は未知語などで捉えられており精度は低いはずなので、multilingual-e5-base などを試用するのがよさそうです。

<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
<services version="1.0" xmlns:deploy="vespa" xmlns:preprocess="properties">

    <container id="default" version="1.0">
        <component id="jp.co.yahoo.vespa.language.lib.kuromoji.KuromojiLinguistics" bundle="kuromoji-linguistics">
            <config name="language.lib.kuromoji.kuromoji">
                <mode>search</mode>
                <ignore_case>true</ignore_case>
            </config>
        </component>

        <component id="openai" class="ai.vespa.llm.clients.OpenAI">

          <!-- Optional configuration: -->
          <config name="ai.vespa.llm.clients.llm-client">
            <apiKeySecretName>OpenAI-API</apiKeySecretName>
            <endpoint>https://api.openai.com/v1/chat/completions</endpoint>
          </config>

        </component>

        <component id="e5" type="hugging-face-embedder">
            <transformer-model url="https://huggingface.co/intfloat/e5-small-v2/resolve/main/model.onnx"/>
            <tokenizer-model url="https://huggingface.co/intfloat/e5-small-v2/raw/main/tokenizer.json"/>
        </component>

        <search>
          <chain id="llm" inherits="vespa">
            <searcher id="ai.vespa.search.llm.LLMSearcher">
              <config name="ai.vespa.search.llm.llm-searcher">
                <providerId>openai</providerId>
              </config>
            </searcher>
          </chain>

          <chain id="rag" inherits="vespa">
            <searcher id="ai.vespa.search.llm.RAGSearcher">
              <config name="ai.vespa.search.llm.llm-searcher">
                <providerId>openai</providerId>
              </config>
            </searcher>
          </chain>
        </search>

        <document-api/>

        <nodes>
            <node hostalias="node1" />
        </nodes>
    </container>

    <content id="qa" version="1.0">
        <redundancy>2</redundancy>
        <documents>
            <document type="qa" mode="index" />
        </documents>
        <nodes>
            <node hostalias="node1" distribution-key="0" />
        </nodes>
    </content>

</services>

準備が終われば、CLIにおける vespa deployvespa feed を叩いて、環境を整えましょう。


CLIでリクエストしてみる

yqlのみでの検索

  • まず確認として、日本語での検索が機能しているか確認してみましょう
takumi@DESKTOP-DG7VUKU:~$ vespa query 'select * from qa where title_text contains "カード"'
{
    "root": {
        "id": "toplevel",
        "relevance": 1.0,
        "fields": {
            "totalCount": 2
        },
        "coverage": {
            "coverage": 100,
            "documents": 6,
            "full": true,
            "nodes": 1,
            "results": 1,
            "resultsFull": 1
        },
        "children": [
            {
                "id": "id:mynamespace:qa::3",
                "relevance": -0.9999876894265599,
                "source": "qa",
                "fields": {
                    "sddocname": "qa",
                    "documentid": "id:mynamespace:qa::3",
                    "category": "請求・お支払い方法",
                    "title_text": "dカードの明細を確認したい",
                    "qa_text": "カードご利用明細照会へログインしてご確認いただけます"
                }
            },
            {
                "id": "id:mynamespace:qa::5",
                "relevance": -0.9999876894265599,
                "source": "qa",
                "fields": {
                    "sddocname": "qa",
                    "documentid": "id:mynamespace:qa::5",
                    "category": "dポイント",
                    "title_text": "dポイントカード番号を確認したい",
                    "qa_text": "dポイントカード、モバイルdポイントカードのバーコードの下に、15桁の番号が記載されています。また、My docomo(Web)の「dポイントカード登録情報」からも、先頭から8桁の番号を確認できます。"
                }
            }
        ]
    }
}

LLM単体を用いた回答生成

  • LLM側の呼び出しも可能か確認してみます。
  • (ちなみに金額の正解はfeedfで取り込んだデータにある3,850円が正しいはずなので、3,300円はLLMが取得した古い情報かと思われます)
takumi@DESKTOP-DG7VUKU:~$ vespa query --header=[OpenAIのAPIシークレットキーを入力] \
  prompt="Docomoの乗り換え(MNP)の手数料はいくらですか" \
  searchChain=llm \
  format=sse \
  llm.model=gpt-4o-mini \
  llm.maxTokens=100

2023年時点で、NTTドコモの乗り換え(MNP)手数料は通常3,300円(税込)となっています。ただし、キャンペーンや特別な条件によって変更されることもあるため、最新の情報を確認するためには公式サイトや店舗での確認をおすすめします。また、他のキャリアへの乗り換え時にも、それぞれのキャリアが異なる手数料を設定

RAGを用いた回答生成(yql)

  • ではyqlの検索結果をベースにRAGで回答できるか確認してみましょう
takumi@DESKTOP-DG7VUKU:~$ vespa query --header=[OpenAIのAPIシークレットキーを入力] \
  yql='select * from qa where title_text contains "手数料"' \
  prompt="Docomoの乗り換え(MNP)の手数料はいくらですか" \
  searchChain=rag \
  format=sse \
  llm.model=gpt-4o-mini \
  llm.maxTokens=100

乗り換え(MNP)の手数料は契約事務手数料3,850円(税込)です。ただし、ドコモオンラインショップで手続きを行う場合、新規契約事務手数料は無料になります。

RAGを用いた回答生成(ベクトル検索)

  • 最終形態として、ベクトル検索を用いた形でRAGでの回答が可能か確認してみましょう
takumi@DESKTOP-DG7VUKU:~$ vespa query --header=[OpenAIのAPIシークレットキーを入力] \
  'yql=select * from qa where {targetHits:1}nearestNeighbor(embeddings, q)' 'input.query(q)=embed(e5, @query)' 'query=譲渡' \
  prompt="Docomoのdポイントをあげる際に何か方法の制限はありますか?" \
  searchChain=rag \
  format=sse \
  llm.model=gpt-4o-mini \
  llm.maxTokens=100

dポイントをあげる際には、dポイントクラブ会員同士であることが条件です。また、ポイントを送るにはd払いアプリが必要です。他に特別な制限は明示されていませんが、利用規約やポイントの有効期限などには注意が必要です。詳細については、公式の情報を確認することをおすすめします。

総括

データ量は少ないですが、想定していた結果が取れています。
あと、SQLに慣れているので気軽に試せるかつ1ライナーで使い分けは大きいです。

CLIでのリクエストを行いましたが、APIベースでも同じような挙動が稼働なはずなので
その点も加味すると、非常にシンプルに使い分けを行うことが可能だと思います。

また、複雑なクエリやRankingの設定、Linguisticsのカスタマイズ、検索とRAGの使い分けなど
参照したい情報が取れないシーンに対して、Vespaベースでたくさんの打ち手が考えられるので
その点は悩みを色々なコンポーネントに分散しなくてもよいので良いなという印象でした。

にしても、やはりユースケースが広がる良い検証となりました。


さいごに

下記のようにLangchainはVespaへのインタフェースも持っているので、主にベクトルDB側としての利用用途が記載されています。
Turbocharge RAG with LangChain and Vespa Streaming Mode for Sharded Data

ですが、本文の最初に記載した通りLangchainを主軸にするというよりは
Vespaを主軸に検索ユースケースメインでの、LLM利用をより広げるという形でも面白そうだなと思いました。(これは個人的にです)
また、上記記事にもあるストリーミングモードや、非構造データへの対応なども含めて
今後も検証できそうなユースケースは多そうな印象です。引き続き、タイミングを見て検証できればと思います!


宣伝

Snowflakeを用いたデータ基盤運用を検討していきたい方はもちろんのこと
関連サービスの利用を検討している方や、LLMの利用検討を始めたが進め方に懸念がある方に対して
弊社では支援やサポートを行っております。ぜひHPより気軽にご相談ください。

また、採用と雑談もよければお待ちしています!


    この投稿をXにポストするこの投稿をFacebookにシェアするこのエントリーをはてなブックマークに追加