生成AIで重要な言語のベクトル化とは――Spring AIでベクトルデータベースに対する類似度検索を実装する:Spring AIで始める生成AIプログラミング(4)
Java×Spring AIで始めるAIプログラミングの入門連載。前回はSpring AIにおける構造化出力について説明しました。今回は、生成AIで拡張検索(RAG)を支える重要な技術であるベクトル化を利用する方法を解説します。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
目次
生成AIの裏で動く言語のベクトル化
生成AI(人工知能)システムでは、多くの場合、LLM(大規模言語モデル)内のデータだけでなく、企業独自の情報を基に回答を生成したいニーズがあります。この目的を達成するための技術が、拡張検索(RAG:Retrieval-Augmented Generation)です。拡張検索は応用範囲が広く、実装方法も多岐にわたるため、単なる技術用語ではなく、より広範なコンセプトを示す言葉とも言えます。
今回は、この拡張検索を実現する基礎技術であるベクトル化をSpring AIで利用する方法を解説します。
ベクトル化とは?
ベクトル化とは、テキストや画像などのデータを「数値の集合」に変換することを言います。ベクトル化によって、データ間の類似度合いを数学的に判定できるようになります。例えば検索エンジンやレコメンドシステムなどで利用される仕組みです(図1)。
Spring AIでは、ベクトル化機能を管理するモデルを「Embeddings Model」といいます。そのため、日本語では「埋め込み」と表現されることもあります。
なお、「ベクトル」や「埋め込み」という言葉は、文脈によって「概念」「機能」「データ」のいずれかの意味で使われることがあります。本稿では分かりやすさを優先し、機能を示す場合は「ベクトル化」、データを示す場合は「ベクトルデータ」と表記します。
本稿では、Spring AIでテキストデータをベクトル化する方法を学ぶとともに、データベース上で管理されたベクトルデータを基に、類似データを検索する方法を学びます。
ベクトルデータの作成
早速、Embeddings Modelを利用して、与えられたメッセージからベクトルデータを作成してみましょう(リスト1)。
import org.springframework.ai.embedding.EmbeddingModel; // (省略) @ShellMethod(key = "vector-create") public String create(String message){ // (1) EmbeddingModelの取得 EmbeddingModel embeddingModel = context.getBean(EmbeddingModel.class); // (2) ベクトルデータを作成 float[] vector = embeddingModel.embed(message); return Arrays.toString(vector); }
(1)で、ベクトルデータを作成するためのモデルを取得します。インタフェースクラスであるEmbeddingModelを取得していますが、実際のOpenAIで利用するのはorg.springframework.ai.openai.OpenAiEmbeddingModelクラスとなります。
次にベクトルデータを取得します(2)。ベクトルデータの次元はモデルに依存しますが、OpenAIの場合には1536次元のfloatとしてベクトル化されます。以下は、その具体的な実行例です。
shell:>vector-create 喜び [0.0149485655, -0.009776097, 0.008102652,.....,-0.0020471597] ・・(省略)
ベクトルデータベース(VectorStore)とは?
作成したベクトルデータを保存し、それらのデータとの類似度を計算、管理するために、ベクトルデータベースを使用します。実際のデータ管理システムとしては、クラウドサービスを利用することもできますし、最近の主要なリレーショナルデータベース(RDB)もベクトルデータをサポートしています。
Spring AIでは、図2のような流れでベクトルデータの登録と検索を行います。
ベクトルデータベースを扱う場合、先ほど紹介したfloat[]型のベクトルデータは、直接コード上には現れず、ベクトルデータベースの実装内に隠蔽されています。また、メタ情報なども一緒に管理できるようになり、これらはDocumentクラスで管理されます。このクラスは、ベクトル検索の結果もList型で取得できます。
どのベクトルデータベースを選択してもVectorStoreインタフェースを介してアクセスします。データベースの種類に応じて実装クラスが用意されているため、VectorStoreのインスタンスを作成する際には、利用するデータベースに合わせた実装クラスを個別に指定し、それぞれに必要な設定を行う必要があります(図3)。
検索にはSearchRequestクラスを利用します。このクラスは、どのベクトルデータベース実装を選んでも共通して利用できます。
SimpleVectorStore利用のための準備
まずは簡単なSimpleVectorStoreクラスで試してみます。SimpleVectorStoreはインメモリでデータを保持するためのクラスで、JSONファイルでの保存やロードもサポートしています。利用に際して前準備もいらず、ベクトルデータベースの機能を簡易的かつ手軽に試すことができます。最初にベクトルデータベースを試すには、うってつけのクラスといえるでしょう。
1. SimpleVectorStoreを使うための設定を追加する
SimpleVectorStoreを使うには、まず、Buildファイル(build.gradle.kts)にリスト2の指定を追加します。
dependencies { // (省略) // 以下を追加 implementation("org.springframework.ai:spring-ai-vector-store") }
このパッケージ追加にはVectorStoreの基本クラスが含まれています。後に紹介するMariaDBなどを利用する場合には、このパッケージは自動で追加されるので指定は必要ありませんが、SimpleVectorStoreのみを利用する場合には指定が必要です。
2. ベクトルデータベースにデータを保存/ロードする
リスト3は、SimpleVectorStore経由で、ファイルにベクトルデータを保存/ロードするためのサンプルです。
@ShellComponent public class VectorCommand { private final SimpleVectorStore vectorStore; public VectorCommand(ApplicationContext context){ this.context = context; // EmbeddingModelモデルと取得する EmbeddingModel embeddingModel = context.getBean(EmbeddingModel.class); // (1) SimpleVectorStoreのインスタンスを作成する vectorStore = SimpleVectorStore.builder(embeddingModel).build(); } // (省略) // (2) ファイルにデータを保存する @ShellMethod(key = "vector-save") public void save(String filename){ vectorStore.save(new File(filename)); } // (3) ファイルからデータを読みこむ @ShellMethod(key = "vector-load") public void load(String filename){ vectorStore.load(new File(filename)); } }
SimpleVectorStoreのインスタンスを作成するには、SimpleVectorStore.builderメソッドにEmbeddingModelモデルを渡すだけです(1)。buildメソッドでSimpleVectorStoreのインスタンスが返されます。これで、渡されたEmbeddingModelの内容がSimpleVectorStoreで操作できるようになります。
(2)(3)は、EmbeddingModelで表されたベクトルデータをファイルに保存、ロードするためのコードです。リスト4は、saveメソッド経由で保存したファイルの例です。内部的にはJSON形式で保存されるので、VectorStoreがどのようにベクトルデータを管理しているのか、見た目にもイメージしやすいですね。
{ "9977d040-28a1-4542-93a8-72002e7534ad" : { "text" : "犬", "embedding" : [ -0.0200862, ..(省略)...,-0.017477257 ], "id" : "9977d040-28a1-4542-93a8-72002e7534ad", "metadata" : { "category" : "動物" } } }
VectorStoreへの登録
続いて、VectorStoreに対してベクトルデータを登録します(リスト5)。
@ShellMethod(key = "vector-store") public String storeText(@ShellOption(value = "--text") String text , @ShellOption(value = "--category" , defaultValue = "general") String category, @ShellOption(value = "--tags", defaultValue = "") String tags){ // (1) メタデータ(Map) Map<String, Object> metadata = new HashMap<>(); metadata.put("category",category); List<String> tagList = List.of(tags.split(",")); metadata.put("tags",tagList); // (2) データの作成 Document doc = Document.builder() .metadata(metadata).text(text) .build(); // (3) データの登録 vectorStore.add(List.of(doc)); // (省略) }
まず、(1)でベクトルデータに追加するメタデータをMap形式で準備します。メタデータとして管理するキーはどのようなものでも構いませんが、ここではcategoryとtagsを用意しています。
後は、おなじみのBuilderパターンでドキュメント(Document)を生成し、メタデータ(metadata)とテキスト本体(text)とを登録します(2)。このドキュメントはaddメソッドでVectorStoreに登録できます(3)。
以上を理解したら、実際に実行してみましょう。
shell:>vector-store --text ザ・ビートルズ --category 洋楽 --tags 音楽,イギリス
VectorStoreを使った類似度検索
これでデータベースの準備ができたので、ここからは登録したデータから検索する方法を紹介します。あらかじめ、先ほどの例を基に他のテキストなども皆さん自身でいろいろ登録しておきましょう。また、以下のコマンドを実行すると登録済みのデータが利用できます。
shell:>vector-load sample.json
まず紹介するのは、用語のみを指定して検索する、最も基本的な例です(リスト6)。
@ShellMethod(key = "vector-search-simple") public String search(String query){ // (1) 検索用のリクエスト作成 SearchRequest searchRequest = SearchRequest.builder() .query(query) // 検索用語 .topK(5) // 最大で取得する件数(SQLのlimit相当) .build(); // (2) 検索の実行 List<Document> results = vectorStore.similaritySearch(searchRequest); // (3) 検索結果を順にリスト表示 StringBuffer sb = new StringBuffer(); results.forEach(doc -> { sb.append(String.format("Score : %s , Text : %s , Category : %s\n", doc.getScore(), doc.getText(),doc.getMetadata().get("category"))); }); return sb.toString(); }
検索リクエストを作成しているのは(1)です。検索用語、取得する最大件数を指定したのち、buildメソッドを呼び出し、SearchRequestインスタンスを生成します。SearchRequestインスタンスが生成できてしまえば、後はsimilaritySearchメソッドにこれを渡して検索を実行するだけです。
なお、similaritySearchメソッドが実行するのは、正確には類似度検索です。(1)で指定した検索用語(query)がベクトル化された上で、類似度判定されます。
実行結果はDocument型のリストとして返されるので、(3)ではそれぞれのドキュメントから「一致度(getStore)、テキスト(getText)、メタデータ(getMetadata)」を取り出し、リストに整形しています。
以上を理解したら、検索を実行してみましょう。
shell:>vector-search-simple ビートルズ Score : 0.8974715547450139 , Text : ビートルズ , Category : 洋楽 Score : 0.889434231587302 , Text : ザ・ビートルズ , Category : 洋楽 Score : 0.8469923938240349 , Text : ビートルジュース , Category : 映画 Score : 0.8336792983536193 , Text : THE BEATLES , Category : 洋楽 Score : 0.8332844177826466 , Text : BEATLES , Category : 洋楽
結果は類似度が高い順から取得できます。このような単純な単語でも、単純一致だけでなく、自然な類似度から結果を取得できていることが見て取れます。
メタデータを用いた条件付き検索
上の結果を見ても分かるように、本来であれば対象にしてほしくないデータまで検索され、対象にされてしまっています(この例であれば「ビートルジュース」)。このように、ベクトルデータだけでは本来対象にしたくないデータを除外するなどの処理が困難ですが、その際にもメタデータ(ここではcategory)を利用することで、より意図した結果を取得できるようになります(リスト7)。
@ShellMethod(key = "vector-search") public void searchMetadataString( @ShellOption(value = "--query") String query, @ShellOption(value = "--category") String category, @ShellOption(value = "--threshold", defaultValue = "0.0") String threshold) { double doubleThreshold = Double.parseDouble(threshold); SearchRequest searchRequest = SearchRequest.builder() .query(query) .filterExpression("category == '" + category + "'") // (1) 指定したカテゴリとの一致条件を指定する .similarityThreshold(doubleThreshold) // (2) 指定したスコア以上 .topK(5).build(); //(以下、省略) 検索の実行部分は先ほどと同様 }
メタデータに対する条件を指定する場合には、(1)のようにSearchRequest.builderのfilterExpressionメソッドを使って指定します。条件指定には、一致条件以外にも、大小の比較演算子である「>」や「<」など、SQLでのWHERE句に相当する演算子を利用できます。詳しくはレファレンスを参照してください。
(2)は類似度の閾値(しきいち)を表します。この例では、指定したスコア以上の値のみを取得します。この閾値は、利用するVectorStoreの種類(利用するベクトルデータベース)やその類似度の計算ロジックなどによって変わるので、その意味に応じて設定します。今回はコサイン類似度という方法を使っているため、最大で1の値で、1に近いほど類似度が高くなります。
これを実行すると、以下のような結果が得られます。
shell:>vector-search --query ビートルズ --category 洋楽 --threshold 0.81 Score : 0.8975537320438945 , Text : ビートルズ , Category : 洋楽 Score : 0.8895223014114596 , Text : ザ・ビートルズ , Category : 洋楽 Score : 0.8338045628988857 , Text : THE BEATLES , Category : 洋楽 Score : 0.8334154636622575 , Text : BEATLES , Category : 洋楽
また、条件をよりプログラム的に組み立てることもできます。これには、FilterExpressionBuilderを利用してください(リスト8)。
// query = '...' OR tags in ['...'] と同様の指定 FilterExpressionBuilder builder = new FilterExpressionBuilder(); var op1 = builder.eq("category",category); var op2 = builder.in("tags",List.of(tag)); // 作成したオブジェクトをfilterExpressionにて指定 Filter.Expression expression = builder.or(op1,op2).build();
まとめ
今回は、AIプログラムでよく使われる検索拡張生成(RAG)を支える技術であるベクトルデータベースについて紹介しました。ベクトルデータは、生成AIを使ったチャットシステムだけではなく、より広範囲に利用しやすく生成AIの機能中でも非常に利用しやすい技術といえるでしょう。
次回は、RDBを使ったVectorStoreについての利用方法について紹介します。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・X: @WingsPro_info(https://x.com/WingsPro_info)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.