Java×Spring AIで始めるAIプログラミングの入門連載。前回はベクトルストアの基本的な使い方について説明しました。今回は、RAGとして使うためのデータソースとしてリレーショナルデータベースを使い、さらに外部データを登録するまでの流れについて解説します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
2025年8月初旬に、Spring AIバージョン1.0.1がリリースされました。これまでご紹介した内容は引き続き1.0.1でも利用できますが、今回紹介するコードは1.0.1以上でないと動作しない部分があります。リスト1のように、ビルドファイルでバージョンを変更してください。
extra["springAiVersion"] = "1.0.1"
前回はベクトルストアの役割や機能が分かりやすいようにSimpleVectorStoreを利用しましたが、より本格的な利用を想定して、本稿ではMariaDBに対応したMariaDBVectorStoreを使ってみましょう。
MariaDBVectorStoreを利用するには、リスト2のようにビルドファイルに依存関係を追加する必要があります。
dependencies { // (省略) // (1) MariaDBのVectorStoreを使うためのビルド設定 implementation("org.springframework.ai:spring-ai-starter-vector-store-mariadb") // (2)3.5.1以上でないとエラーになる implementation("org.mariadb.jdbc:mariadb-java-client:3.5.4") }
レファレンスでは(1)の指定だけが記述されていますが、筆者の環境ではサポートされていない古いMariaDB用のJDBCドライバが既にキャッシュにあったようで、エラーが発生したため、(2)を追加しました。
アプリケーション設定(application.properties)には、リスト3のJDBC接続情報と、VectorStore用の設定が必要になります。
spring.datasource.url=jdbc:mariadb://<host>:<port>/<dbname> spring.datasource.username=<username> spring.datasource.password=<password> spring.ai.vectorstore.mariadb.initialize-schema=true spring.ai.vectorstore.mariadb.distance-type=COSINE
initialize-schemaは、データベース接続時に必要なテーブルがない場合には自動で作成します。そしてdistance-typeは、類似性検索としてCOSINE(コサイン類似度)または、EUCLIDEAN(ユークリッド距離)のいずれかのルールを利用するかを選択できます。
ここまで設定すれば、後はリスト4のようにSpringのApplicationContextからインスタンスを取得するだけで利用が可能です。
private final MariaDBVectorStore vectorStore; public MariaVectorCommand(ApplicationContext context){ this.context = context; // MariaDBでのベクトルストアインスタンスを取得 vectorStore = context.getBean(MariaDBVectorStore.class); }
データの登録の方法や検索方法などは、前回紹介したSimpleVectorStoreと同じです。保存用に利用するテーブル名やフィールド名はカスタマイズすることも可能です。その場合には、initialize-schemaにfalseを指定し、自分でテーブルを準備してください。また、MariaDBVectorStoreのインスタンスを作成するためのBean定義も必要になりますので、そちらはサンプルコードを参考にしてください。
これまでは既存のVectorStore実装を利用するケースを中心に説明しました。基本的な仕組みは共通しており、そのまま使う分にはさほど難しくありません。しかし、既存のテーブルを使った独自のデータ管理や、Spring AIがサポートしていないデータソースを使いたい場合は、VectorStore自体をカスタマイズする必要があります。
もっとも、SimpleVectorStoreやMariaDBVectorStoreの実装コードを見ると、カスタマイズそのものはそこまで難しいことではありません。例えば、リスト5はSQLを直接発行して類似度検索を実施する例です。
@ShellMethod(key = "maria-sql") public String nativeSql(String query, String category){ EmbeddingModel embeddingModel = context.getBean(EmbeddingModel.class); // (1) 検索文字列をベクトル化する float[] embedding = embeddingModel.embed(query); // (2) JdbcTemplateを取得する Optional<JdbcTemplate> op = vectorStore.getNativeClient(); if(op.isPresent()){ JdbcTemplate jdbcTemplate = op.get(); // (3) SQLを組み立てる final String sql = String.format( "SELECT * FROM (select id, content, metadata, VEC_DISTANCE_COSINE(embedding, ?) as distance " + "from vector_store) as t where ( JSON_VALUE(metadata, '$.category') = ? ) order by distance asc LIMIT 10"); // (4) 実行する List<Map<String, Object>> results = jdbcTemplate.queryForList(sql, embedding, category); StringBuffer sb = new StringBuffer(); // (省略) } // (省略) }
まず、(1)で検索に使う文字列をベクトル化します。検索のたびにベクトル化しなくても既存のベクトル化されたキーワードのデータベースなどがあれば、それを利用することもできます。
続いて、vectorStoreオブジェクトからネイティブなデータベースクライアント(JdbcTemplate)を取得しようとしています(2)。取得できるクラスはVectorStoreの実装によって異なりますが、サブクラスをカスタマイズする際には、このような方法で低レイヤーのデータアクセスオブジェクトを取得します。
後は(3)〜(4)でSQLを作成、実行しています。SQL内で呼び出しているVEC_DISTANCE_COSINEは、コサイン類似度を求めるためのMariaDBの関数です。
このSQL文は、ベクトルストア(vector_storeテーブル)から、クエリベクトルと各レコードの埋め込みベクトルとのコサイン類似度(VEC_DISTANCE_COSINE関数)を計算し、その値を distanceとして取得します。つまり、この例であれば、
という意味になります。つまり、与えられたクエリに最も近いテキストを上位10件だけ取得する、というわけです。
このコマンドを実行すると、以下のような結果になります。
shell:>maria-vec-store --text ザ・ビートルズ --category 洋楽 (データを登録) shell:>maria-sql --query ビートルズ --category 洋楽 (データを取得) Score : 0.06284758960552905 , Text : ザ・ビートルズ
アプリを作成する際、テーブル構造などを自由に定義したいことはよくあります。そのとき、このような内部的な挙動を理解しておくことが役立つはずです。
ベクトルストアに個別のデータ(Document)を登録する方法は既に説明しました。ただし、その中身は単なるテキストファイルだけでなく、PDFやWord、PowerPointなど種々の形式に及ぶことはよくあります。この場合、課題となるのは、「どのようにして必要なデータを抽出し、そして、効果的に加工および変換するか」という点です。
もちろん、実用上、データを最適化するには個別の実装が必要ですが、大まかな流れや手法には共通する部分があります。Spring AIでは、このような一連の流れを標準で提供しています。これがELTパイプライン(図1)です。
ここからは、図の流れを念頭に置きながら、個々の処理を実装していきましょう。
最初は、オリジナルデータから、AI(人工知能)で扱えるデータ(Document)を取り出す必要があります。このために、Spring AIではあらかじめ以下のようなクラスが用意されています。
今回は、この中からPdfPageDocumentReaderを使ってPDFファイルからデータを取り出すための方法を紹介します。
必要なクラスを準備するために、リスト6のようにビルド指定を追加します。
dependencies { // (省略) implementation("org.springframework.ai:spring-ai-pdf-document-reader") }
これ以外のParagraphPdfDocumentReaderやTikaDocumentReaderも同様に、追加のビルド設定が必要になります。
PDFデータから実際にテキストデータを取り出してみましょう(リスト7)。テキストデータを含んだPDFファイルであれば、どのようなものでも構いません。例えば、国交省から配布されているマンション規約のひな型文章などは、RAG(Retrieval-Augmented Generation:検索拡張生成)の効果が分かりやすいドキュメントサンプルとして利用できます。
@ShellMethod(key = "elt-extract-pdf") public void extractFromPdf(@ShellOption(value = "--filename") String filename, @ShellOption(value = "--page", defaultValue = "1") int perPage, @ShellOption(value = "--margin-top", defaultValue = "0") int marginTop, @ShellOption(value = "--margin-bottom", defaultValue = "0") int marginBottom ) { File file = new File(filename); // (省略) // (1) リソース型に変換する Resource resource = new FileSystemResource(file); // (2) インスタンスを作成する PagePdfDocumentReader reader = new PagePdfDocumentReader(resource, // (3) 読み取りのルールを設定 PdfDocumentReaderConfig.builder() .withPageTopMargin(marginTop) // マージン(上)を設定 .withPageBottomMargin(marginBottom) // マージン(下)を設定 .withPagesPerDocument(perPage) // Documentごとのページ数 .build() ); // (4) データを取り出す List<Document> output = reader.get(); // (省略) }
まず、指定されたファイル名からFileオブジェクトを、さらに、FileSystemResourceを使ってSpringで利用できるリソース型を準備します(1)。
続いて、PagePdfDocumentReaderのインスタンスを作成します。ビルダーで設定しているのは読み取りのルールです。例えば、PDFの各ページにヘッダやフッタがある場合にそれが邪魔になることがあるので、マージンを指定してそれらを含めないようにすることができます。ちなみに、マージンの単位はptで、72DPIを基準としています。つまり、A4サイズであれば縦が約842ptとなります。また、PDFデータによっては見開きが前提になっているなどのケースもあり、1つのDocument単位を何ページ分とするかなども指定可能です。
最後に、DocumentReaderインタフェースのgetメソッド(4)で、Documentデータを取得します。
以上のコードを実行すると、以下のような結果を得られます。
shell:>elt-extract-pdf --filename doc.pdf 2025-08-18T20:48:50.750+09:00 INFO 3045345 --- [part1] [ main] j.e.spring_ai.part5.command.ELTCommand : documents.size : 1 2025-08-18T20:48:50.750+09:00 INFO 3045345 --- [part1] [ main] j.e.spring_ai.part5.command.ELTCommand : doc.text : metadata({page_number=1, file_name=doc_page1.pdf}) - 令和6年6月7日改正版
ちなみに、PagePdfDocumentReaderクラスの内部では、PDFBoxを使ってテキストを抽出しています。従って、PDF内に含まれる画像やオブジェクト内のテキストまでは取得できません。
以上の手順でテキストは取得できましたが、RAG用データとしてこのまま使うには大き過ぎますし、使いづらさがあります。そこで、文章を適切なサイズに分割するチャンキングという処理が欠かせません。Spring AIには、データ変換用のクラスが用意されています。
なお、「適切」なチャンキングサイズは、常に同じ方法で決まるわけではありません。実際にアプリを開発するに際しては、AIが扱いやすいように試行錯誤しながら調整する必要が出てくるでしょう。
まず、現在のデータは大き過ぎます。1ページのテキストがそのまま1つのDocumentデータになっているためです。そこで、TokenTextSplitterを使ってデータを分割してみましょう(リスト8)。
@ShellMethod(key = "elt-transform-token") public void transformToken() { // (省略) // (1) 分割用のインスタンスを作成 TokenTextSplitter splitter = TokenTextSplitter.builder() .withChunkSize(800) // トークン数の目標サイズ(デフォルト:800) .withKeepSeparator(true) // 改行を保持(デフォルト:true) .build(); // (2) 分割を実行 List<Document> splitDocs = splitter.apply(documents); }
データを分割するサイズ(withChunkSize)や、改行の扱い(withKeepSeparator)などを指定し、分割のためのTokenTextSplitterオブジェクトを生成します(1)。チャンクとは、AIモデルが処理するトークン(課金単位)をもとに一定の長さでテキストを分割した断片です。チャンクは複数のトークンから構成されるものであり、withChunkSizeはそのトークン数の目安を表すものです。あくまで目安なので、必ずしも指定した数のトークンで正確に分割されるわけではありません。
TokenTextSplitterを生成できたら、後はそのapplyメソッドで分割を実行します。戻り値はDocumentのリストです。
なお、TokenTextSplitterはテキストを重複なく分割します。つまり、テキストの改行位置によっては文章として成立しない場合もあります。特にPDFなどからテキストを抽出した場合、印刷レイアウト上から現れる改行や改ページがあることで、うまくこの部分が処理しにくくなります。
そのような場合、文章の意味が壊れないように、図2のようにそれぞれの文章を重複して分割したいニーズがありますが、残念ながら現時点ではそのような機能は備えていません。このような部分は今後の機能追加もしくは代替ソリューションが現れることが望まれます。
続いて、分割したそれぞれのテキストについてキーワードを付けたいと思います。もちろん、人がそれぞれの文章を見て最適なキーワードを付けるのも問題ありませんが、せっかくのAIプログラミングなので、キーワード付けもAIに任せてしまいましょう。これには、KeywordMetadataEnricherを使います(リスト9)。
@ShellMethod(key = "elt-transform-keyword") public void transformKeyword(@ShellOption(value = "count", defaultValue = "5") int count){ // (省略) // (1) AIを使うためにChatModelを準備する ChatModel model = context.getBean(ChatModel.class); // (2) キーワードを作成するためのプロンプト String template = """ 以下の内容について、キーワードを%s個作成してください。また、出力はカンマ区切りで出力してください。 内容:{context_str} キーワード: """; // (3) テンプレートを作成 PromptTemplate prompt = PromptTemplate.builder() .template(String.format(template, count)).build(); // (4) インスタンスを作成 KeywordMetadataEnricher enricher = KeywordMetadataEnricher .builder(model) .keywordsTemplate(prompt) // .keywordCount(count) テンプレートを設定する場合にはいらない .build(); // (省略) }
まず、AIで自動的にキーワードを作成するためのChatModelを(1)、また、キーワードを作成するためのプロンプトメッセージ(2)を、それぞれ用意します。プロンプトメッセージはデフォルトでも用意されていますが、このような細かい部分が後で違いとして現れることもあるため、目的により適したキーワードを作成するためのプロンプトを用意しましょう。
後は、プロンプトを基にテンプレートを作成し(3)、KeywordMetadataEnricherのインスタンスを作成します(4)。
以上を理解したら、実際に実行してみましょう。
shell:>elt-transform-keyword
これでキーワード付きのDocumentオブジェクトへ変換されました。
最後に、データベースに用意したデータ(Document)を登録するには、DocumentWriterのインタフェースを持つクラスで登録します。ここは前回紹介した方法と同じです。VectorStoreクラスはDocumentWriterのインタフェースを保持しているので、リスト10のように保存できます。
@ShellMethod(key = "elt-write-mariadb") public void writeMariaDB(){ // List<Document> documents = ... // 実際の入力はクラスフィールドとして共有されていますが、ここでは型の参考として記述しています。 VectorStore vectorStore = context.getBean(MariaDBVectorStore.class); vectorStore.add(documents); }
以上を理解したら、実際に実行してみましょう。
shell:>elt-write-mariadb
登録されたデータベースの内容を参照すると図3のようにデータが登録されています。
今回は、RAGのためのデータ加工と管理方法、そしてそのデータを登録するまでの流れを解説しました。Spring AIの標準クラスだけでは対応が難しい場合も多く、また、AI技術は既存システムとの連携が重要になるケースも多々あります。例えば、既存データベースやネットワークストレージのデータを活用する場合など、これまでの技術や経験が役立つ場面が多々あります。
次回は、今回作成したベクトルデータを使ったRAGシステムの構築方法を紹介します。
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.