PDFファイルをRAG用ベクトルストアにETL処理する方法をSpring AIで理解するSpring AIで始める生成AIプログラミング(5)

Java×Spring AIで始めるAIプログラミングの入門連載。前回はベクトルストアの基本的な使い方について説明しました。今回は、RAGとして使うためのデータソースとしてリレーショナルデータベースを使い、さらに外部データを登録するまでの流れについて解説します。

» 2025年09月25日 05時00分 公開

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「Spring AIで始める生成AIプログラミング」のインデックス

連載:Spring AIで始める生成AIプログラミング

本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。


Spring AI 1.0.1への対応

 2025年8月初旬に、Spring AIバージョン1.0.1がリリースされました。これまでご紹介した内容は引き続き1.0.1でも利用できますが、今回紹介するコードは1.0.1以上でないと動作しない部分があります。リスト1のように、ビルドファイルでバージョンを変更してください。

extra["springAiVersion"] = "1.0.1"
リスト1 ビルド指定(src/build.gradle.ktsの抜粋)

ベクトルデータのストアとしてデータベースを設定

 前回はベクトルストアの役割や機能が分かりやすいようにSimpleVectorStoreを利用しましたが、より本格的な利用を想定して、本稿ではMariaDBに対応したMariaDBVectorStoreを使ってみましょう。

[1]依存関係を設定する

 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")
}
リスト2 MariaDBのVectorStoreを使うためのビルド設定(src/build.gradle.ktsの抜粋)

 レファレンスでは(1)の指定だけが記述されていますが、筆者の環境ではサポートされていない古いMariaDB用のJDBCドライバが既にキャッシュにあったようで、エラーが発生したため、(2)を追加しました。

[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
リスト3 MariaDBのVectorStoreを使うためのアプリケーション設定(src/resource/application.propertiesの抜粋)

 initialize-schemaは、データベース接続時に必要なテーブルがない場合には自動で作成します。そしてdistance-typeは、類似性検索としてCOSINE(コサイン類似度)または、EUCLIDEAN(ユークリッド距離)のいずれかのルールを利用するかを選択できます。

[3]VectorStoreインスタンスを取得する

 ここまで設定すれば、後はリスト4のようにSpringのApplicationContextからインスタンスを取得するだけで利用が可能です。

private final MariaDBVectorStore vectorStore;
public MariaVectorCommand(ApplicationContext context){
    this.context = context;
    //  MariaDBでのベクトルストアインスタンスを取得
    vectorStore = context.getBean(MariaDBVectorStore.class);
}
リスト4 MariaDB用のVectorStoreインスタンスを取得するコード例(src/main/java/jp/enbind/spring_ai/part5/command/MariaVectorCommand.javaの抜粋)

 データの登録の方法や検索方法などは、前回紹介したSimpleVectorStoreと同じです。保存用に利用するテーブル名やフィールド名はカスタマイズすることも可能です。その場合には、initialize-schemaにfalseを指定し、自分でテーブルを準備してください。また、MariaDBVectorStoreのインスタンスを作成するためのBean定義も必要になりますので、そちらはサンプルコードを参考にしてください。

SQLを自分で実行

 これまでは既存の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();
        // (省略)
    }
    // (省略)
}
リスト5 SQLを直接実行する場合のコード例(src/main/java/jp/enbind/spring_ai/part5/command/MariaVectorCommandの抜粋)

 まず、(1)で検索に使う文字列をベクトル化します。検索のたびにベクトル化しなくても既存のベクトル化されたキーワードのデータベースなどがあれば、それを利用することもできます。

 続いて、vectorStoreオブジェクトからネイティブなデータベースクライアント(JdbcTemplate)を取得しようとしています(2)。取得できるクラスはVectorStoreの実装によって異なりますが、サブクラスをカスタマイズする際には、このような方法で低レイヤーのデータアクセスオブジェクトを取得します。

 後は(3)〜(4)でSQLを作成、実行しています。SQL内で呼び出しているVEC_DISTANCE_COSINEは、コサイン類似度を求めるためのMariaDBの関数です。

 このSQL文は、ベクトルストア(vector_storeテーブル)から、クエリベクトルと各レコードの埋め込みベクトルとのコサイン類似度(VEC_DISTANCE_COSINE関数)を計算し、その値を distanceとして取得します。つまり、この例であれば、

  • サブクエリでid、content、metadataおよびコサイン類似度(distance)を抽出し、これを一時テーブルtとして取得
  • metadataのcategory が指定されたカテゴリーと一致するレコードのみを抽出
  • 最終的に、類似度(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)です。

図1 ELTパイプライン処理の概要 図1 ELTパイプライン処理の概要

 ここからは、図の流れを念頭に置きながら、個々の処理を実装していきましょう。

外部データからテキスト情報を抽出

 最初は、オリジナルデータから、AI(人工知能)で扱えるデータ(Document)を取り出す必要があります。このために、Spring AIではあらかじめ以下のようなクラスが用意されています。

  • TextReader:テキストファイルからテキストデータを取り出す
  • JsonReader:JSONデータからデータを取り出す
  • JsoupDocumentReader:HTMLからデータを取り出す
  • PdfPageDocumentReader:PDFファイルからデータを取り出す
  • ParagraphPdfDocumentReader:PDFファイルから見出し情報を利用しデータを取り出す
  • MarkdownDocumentReader:Markdownからデータを取り出す
  • TikaDocumentReader:Tikaライブラリを使ってWordやPowerPointなどからデータを取り出す

 今回は、この中からPdfPageDocumentReaderを使ってPDFファイルからデータを取り出すための方法を紹介します。

[1]依存関係を設定する

 必要なクラスを準備するために、リスト6のようにビルド指定を追加します。

dependencies {
    // (省略)
    implementation("org.springframework.ai:spring-ai-pdf-document-reader")
}
リスト6 PdfPageDocumentReaderを使うためのビルド設定(src/build.gradle.ktsの抜粋)

 これ以外のParagraphPdfDocumentReaderTikaDocumentReaderも同様に、追加のビルド設定が必要になります。

[2]PDFファイルからテキストを抽出する

 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();
    // (省略)
}
リスト7 PDFファイルからテキストデータを取り出すコード例(src/main/java/jp/enbind/spring_ai/part5/command/ELTCommandの抜粋)

 まず、指定されたファイル名からFileオブジェクトを、さらに、FileSystemResourceを使ってSpringで利用できるリソース型を準備します(1)。

 続いて、PagePdfDocumentReaderのインスタンスを作成します。ビルダーで設定しているのは読み取りのルールです。例えば、PDFの各ページにヘッダやフッタがある場合にそれが邪魔になることがあるので、マージンを指定してそれらを含めないようにすることができます。ちなみに、マージンの単位はptで、72DPIを基準としています。つまり、A4サイズであれば縦が約842ptとなります。また、PDFデータによっては見開きが前提になっているなどのケースもあり、1つのDocument単位を何ページ分とするかなども指定可能です。

 最後に、DocumentReaderインタフェースのgetメソッド(4)で、Documentデータを取得します。

[3]サンプルを実行する

 以上のコードを実行すると、以下のような結果を得られます。

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には、データ変換用のクラスが用意されています。

  • TokenTextSplitter:テキストを指定したトークンごとに分割するためのクラス
  • KeywordMetadataEnricher:キーワードを作成し、メタデータに付与するためのクラス
  • SummaryMetadataEnricher:内容を要約しメタデータに付与するためのクラス

 なお、「適切」なチャンキングサイズは、常に同じ方法で決まるわけではありません。実際にアプリを開発するに際しては、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);
}
リスト8 データを分割する際のコード例(src/main/java/jp/enbind/spring_ai/part5/command/ELTCommandの抜粋)

 データを分割するサイズ(withChunkSize)や、改行の扱い(withKeepSeparator)などを指定し、分割のためのTokenTextSplitterオブジェクトを生成します(1)。チャンクとは、AIモデルが処理するトークン(課金単位)をもとに一定の長さでテキストを分割した断片です。チャンクは複数のトークンから構成されるものであり、withChunkSizeはそのトークン数の目安を表すものです。あくまで目安なので、必ずしも指定した数のトークンで正確に分割されるわけではありません。

 TokenTextSplitterを生成できたら、後はそのapplyメソッドで分割を実行します。戻り値はDocumentのリストです。

 なお、TokenTextSplitterはテキストを重複なく分割します。つまり、テキストの改行位置によっては文章として成立しない場合もあります。特にPDFなどからテキストを抽出した場合、印刷レイアウト上から現れる改行や改ページがあることで、うまくこの部分が処理しにくくなります。

 そのような場合、文章の意味が壊れないように、図2のようにそれぞれの文章を重複して分割したいニーズがありますが、残念ながら現時点ではそのような機能は備えていません。このような部分は今後の機能追加もしくは代替ソリューションが現れることが望まれます。

図2 重複したテキスト分割 図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();
    // (省略)
 }
リスト9 キーワードを付与するためのコード例(src/main/java/jp/enbind/spring_ai/part5/command/ELTCommand.javaの抜粋)

 まず、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);
}
リスト10 ベクトルストア(MariaDB)へのデータ登録(src/main/java/jp/enbind/spring_ai/part5/command/ELTCommand.javaの抜粋)

 以上を理解したら、実際に実行してみましょう。

shell:>elt-write-mariadb

 登録されたデータベースの内容を参照すると図3のようにデータが登録されています。

図3 登録されたデータイメージ 図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.

アイティメディアからのお知らせ

スポンサーからのお知らせPR

注目のテーマ

4AI by @IT - AIを作り、動かし、守り、生かす
Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。