Skip to content

Qdrantに保存されたドキュメントを基に質問に回答するSpring AIを活用したKotlinアプリの構築 — チュートリアル

このチュートリアルでは、Spring AI を使用してLLMに接続し、ドキュメントをベクトルデータベースに保存し、それらのドキュメントのコンテキストを使用して質問に回答するKotlinアプリの構築方法を学びます。

このチュートリアルでは、以下のツールを使用します:

  • Webアプリケーションの設定と実行の基盤としてSpring Boot
  • LLMとのインタラクションおよびコンテキストベースの検索のためにSpring AI
  • プロジェクトの生成とアプリケーションロジックの実装のためにIntelliJ IDEA
  • 類似性検索のためのベクトルデータベースとしてQdrant
  • Qdrantをローカルで実行するためにDocker
  • LLMプロバイダーとしてOpenAI

開始する前に

  1. IntelliJ IDEA Ultimate Edition の最新バージョンをダウンロードしてインストールします。

    IntelliJ IDEA Community Edition または別のIDEを使用している場合は、Webベースのプロジェクトジェネレーター を使用してSpring Bootプロジェクトを生成できます。

  2. APIにアクセスするために、OpenAIプラットフォーム でOpenAI APIキーを作成します。

  3. Qdrantベクトルデータベースをローカルで実行するためにDocker をインストールします。

  4. Dockerのインストール後、ターミナルを開き、以下のコマンドを実行してコンテナを起動します:

    bash
    docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant

プロジェクトを作成する

代替手段として、Spring BootのWebベースプロジェクトジェネレーター を使用してプロジェクトを生成することもできます。

IntelliJ IDEA Ultimate Editionで新しいSpring Bootプロジェクトを作成します:

  1. IntelliJ IDEAで、File | New | Project を選択します。

  2. 左側のパネルで、New Project | Spring Boot を選択します。

  3. New Project ウィンドウで以下のフィールドとオプションを指定します:

    • Name: springAIDemo

    • Language: Kotlin

    • Type: Gradle - Kotlin

      このオプションはビルドシステムとDSLを指定します。

    • Package name: com.example.springaidemo

    • JDK: Java JDK

      このチュートリアルでは、Oracle OpenJDK バージョン 21.0.1 を使用しています。JDKがインストールされていない場合は、ドロップダウンリストからダウンロードできます。

    • Java: 17

      Java 17がインストールされていない場合は、JDKドロップダウンリストからダウンロードできます。

    Create Spring Boot project

  4. すべてのフィールドを指定したことを確認し、Next をクリックします。

  5. Spring Boot フィールドで最新の安定版Spring Bootバージョンを選択します。

  6. このチュートリアルに必要な以下の依存関係を選択します:

    • Web | Spring Web
    • AI | OpenAI
    • SQL | Qdrant Vector Database

    Set up Spring Boot project

  7. Create をクリックしてプロジェクトを生成および設定します。

    IDEが新しいプロジェクトを生成して開きます。プロジェクトの依存関係のダウンロードとインポートには時間がかかる場合があります。

これを行うと、Project view に以下の構造が表示されます:

Spring Boot project view

生成されたGradleプロジェクトは、Mavenの標準ディレクトリレイアウトに対応しています:

  • アプリケーションに属するパッケージとクラスはmain/kotlinフォルダの下にあります。
  • アプリケーションのエントリポイントは、SpringAiDemoApplication.ktファイルのmain()メソッドです。

プロジェクト設定を更新する

  1. build.gradle.kts Gradleビルドファイルを次のように更新します:

    kotlin
    plugins {
        kotlin("jvm") version "2.2.10"
        kotlin("plugin.spring") version "2.2.10"
        // Rest of the plugins
    }
  2. springAiVersion1.0.0に設定します:

    kotlin
    extra["springAiVersion"] = "1.0.0"
  3. Sync Gradle Changes ボタンをクリックしてGradleファイルを同期します。

  4. src/main/resources/application.propertiesファイルを次のように更新します:

    text
    # OpenAI
    spring.ai.openai.api-key=YOUR_OPENAI_API_KEY
    spring.ai.openai.chat.options.model=gpt-4o-mini
    spring.ai.openai.embedding.options.model=text-embedding-ada-002
    # Qdrant
    spring.ai.vectorstore.qdrant.host=localhost
    spring.ai.vectorstore.qdrant.port=6334
    spring.ai.vectorstore.qdrant.collection-name=kotlinDocs
    spring.ai.vectorstore.qdrant.initialize-schema=true

    OpenAI APIキーをspring.ai.openai.api-keyプロパティに設定します。

  5. SpringAiDemoApplication.ktファイルを実行してSpring Bootアプリケーションを開始します。実行後、ブラウザでQdrant collections ページを開いて結果を確認します:

    Qdrant collections

ドキュメントをロードして検索するコントローラーを作成する

ドキュメントを検索し、Qdrantコレクションに保存するためのSpring @RestController を作成します:

  1. src/main/kotlin/org/example/springaidemoディレクトリに、KotlinSTDController.ktという新しいファイルを作成し、以下のコードを追加します:

    kotlin
    package org.example.springaidemo
    
    // Imports the required Spring and utility classes
    import org.slf4j.LoggerFactory
    import org.springframework.ai.document.Document
    import org.springframework.ai.vectorstore.SearchRequest
    import org.springframework.ai.vectorstore.VectorStore
    import org.springframework.web.bind.annotation.GetMapping
    import org.springframework.web.bind.annotation.PostMapping
    import org.springframework.web.bind.annotation.RequestMapping
    import org.springframework.web.bind.annotation.RequestParam
    import org.springframework.web.bind.annotation.RestController
    import org.springframework.web.client.RestTemplate
    import kotlin.uuid.ExperimentalUuidApi
    import kotlin.uuid.Uuid
    
    @RestController
    @RequestMapping("/kotlin")
    class KotlinSTDController(
    private val restTemplate: RestTemplate,
    private val vectorStore: VectorStore,
    ) {
    private val logger = LoggerFactory.getLogger(this::class.java)
    
        @OptIn(ExperimentalUuidApi::class)
        @PostMapping("/load-docs")
        fun load() {
            // Loads a list of documents from the Kotlin documentation
            val kotlinStdTopics = listOf(
                "collections-overview", "constructing-collections", "iterators", "ranges", "sequences",
                "collection-operations", "collection-transformations", "collection-filtering", "collection-plus-minus",
                "collection-grouping", "collection-parts", "collection-elements", "collection-ordering",
                "collection-aggregate", "collection-write", "list-operations", "set-operations",
                "map-operations", "read-standard-input", "opt-in-requirements", "scope-functions", "time-measurement",
            )
            // Base URL for the documents
            val url = "https://raw.githubusercontent.com/JetBrains/kotlin-web-site/refs/heads/master/docs/topics/"
            // Retrieves each document from the URL and adds it to the vector store
            kotlinStdTopics.forEach { topic ->
                val data = restTemplate.getForObject("$url$topic.md", String::class.java)
                data?.let { it ->
                    val doc = Document.builder()
                        // Builds a document with a random UUID
                        .id(Uuid.random().toString())
                        .text(it)
                        .metadata("topic", topic)
                        .build()
                    vectorStore.add(listOf(doc))
                    logger.info("Document $topic loaded.")
                } ?: logger.warn("Failed to load document for topic: $topic")
            }
        }
    
        @GetMapping("docs")
        fun query(
            @RequestParam query: String = "operations, filtering, and transformations",
            @RequestParam topK: Int = 2
        ): List<Document>? {
            val searchRequest = SearchRequest.builder()
                .query(query)
                .topK(topK)
                .build()
            val results = vectorStore.similaritySearch(searchRequest)
            logger.info("Found ${results?.size ?: 0} documents for query: '$query'")
            return results
        }
    }
  2. SpringAiDemoApplication.ktファイルを更新し、RestTemplate beanを宣言します:

    kotlin
    package org.example.springaidemo
    
    import org.springframework.boot.autoconfigure.SpringBootApplication
    import org.springframework.boot.runApplication
    import org.springframework.context.annotation.Bean
    import org.springframework.web.client.RestTemplate
    
    @SpringBootApplication
    class SpringAiDemoApplication {
    
        @Bean
        fun restTemplate(): RestTemplate = RestTemplate()
    }
    
    fun main(args: Array<String>) {
        runApplication<SpringAiDemoApplication>(*args)
    }
  3. アプリケーションを実行します。

  4. ターミナルで、/kotlin/load-docsエンドポイントにPOSTリクエストを送信してドキュメントをロードします:

    bash
    curl -X POST http://localhost:8080/kotlin/load-docs
  5. ドキュメントがロードされたら、GETリクエストで検索できます:

    Bash
    curl -X GET http://localhost:8080/kotlin/docs

    GET request results

結果はQdrant collections ページでも確認できます。

AIチャットエンドポイントを実装する

ドキュメントがロードされたら、最後のステップは、Spring AIのRetrieval-Augmented Generation(RAG)サポートを介してQdrant内のドキュメントを使用して質問に回答するエンドポイントを追加することです:

  1. KotlinSTDController.ktファイルを開き、以下のクラスをインポートします:

    kotlin
    import org.springframework.ai.chat.client.ChatClient
    import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor
    import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor
    import org.springframework.ai.chat.prompt.Prompt
    import org.springframework.ai.chat.prompt.PromptTemplate
    import org.springframework.web.bind.annotation.RequestBody
  2. ChatRequestデータクラスを定義します:

    kotlin
    // Represents the request payload for chat queries
    data class ChatRequest(val query: String, val topK: Int = 3)
  3. コントローラーのコンストラクタパラメータにChatClient.Builderを追加します:

    kotlin
    class KotlinSTDController(
        private val chatClientBuilder: ChatClient.Builder,
        private val restTemplate: RestTemplate,
        private val vectorStore: VectorStore,
    )
  4. コントローラークラス内で、ChatClientインスタンスを作成します:

    kotlin
    // Builds the chat client with a simple logging advisor
    private val chatClient = chatClientBuilder.defaultAdvisors(SimpleLoggerAdvisor()).build()
  5. KotlinSTDController.ktファイルの最後に、以下のロジックを持つ新しいchatAsk()エンドポイントを追加します:

    kotlin
    @PostMapping("/chat/ask")
    fun chatAsk(@RequestBody request: ChatRequest): String? {
        // Defines the prompt template with placeholders
        val promptTemplate = PromptTemplate(
            """
            {query}.
            Please provide a concise answer based on the "Kotlin standard library" documentation.
        """.trimIndent()
        )
    
        // Creates the prompt by substituting placeholders with actual values
        val prompt: Prompt =
            promptTemplate.create(mapOf("query" to request.query))
    
        // Configures the retrieval advisor to augment the query with relevant documents
        val retrievalAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
            .searchRequest(
                SearchRequest.builder()
                    .similarityThreshold(0.7)
                    .topK(request.topK)
                    .build()
            )
            .promptTemplate(promptTemplate)
            .build()
    
        // Sends the prompt to the LLM with the retrieval advisor and retrieves the generated content
        val response = chatClient.prompt(prompt)
            .advisors(retrievalAdvisor)
            .call()
            .content()
        logger.info("Chat response generated for query: '${request.query}'")
        return response
    }
  6. アプリケーションを実行します。

  7. ターミナルで、新しいエンドポイントにPOSTリクエストを送信して結果を確認します:

    bash
    curl -X POST "http://localhost:8080/kotlin/chat/ask" \
         -H "Content-Type: application/json" \
         -d '{"query": "What are the performance implications of using lazy sequences in Kotlin for large datasets?", "topK": 3}'

    OpenAI answer to chat request

おめでとうございます!これで、OpenAIに接続し、Qdrantに保存されたドキュメントから取得したコンテキストを使用して質問に回答するKotlinアプリができました。 さまざまなクエリを試したり、他のドキュメントをインポートして、さらに多くの可能性を探ってみてください。

完成したプロジェクトはSpring AI demo GitHubリポジトリで確認できるほか、Kotlin AI Examplesで他のSpring AIの例を探索することもできます。