Build a Kotlin app that uses Spring AI to answer questions based on documents stored in Qdrant — tutorial
In this tutorial, you'll learn how to build a Kotlin app that uses Spring AI to connect to an LLM, store documents in a vector database, and answer questions using context from those documents.
You will use the following tools during this tutorial:
Spring Boot as the base to configure and run the web application.
Spring AI to interact with the LLM and perform context-based retrieval.
IntelliJ IDEA to generate the project and implement the application logic.
Qdrant as the vector database for similarity search.
Docker to run Qdrant locally.
OpenAI as the LLM provider.
Before you start
Download and install the latest version of IntelliJ IDEA Ultimate Edition.
Create an OpenAI API key on the OpenAI platform to access the API.
Install Docker to run the Qdrant vector database locally.
After installing Docker, open your terminal and run the following command to start the container:
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
Create the project
Create a new Spring Boot project in IntelliJ IDEA Ultimate Edition:
In IntelliJ IDEA, select File | New | Project.
In the panel on the left, select New Project | Spring Boot.
Specify the following fields and options in the New Project window:
Name: springAIDemo
Language: Kotlin
Type: Gradle - Kotlin
Package name: com.example.springaidemo
JDK: Java JDK
Java: 17
Make sure that you have specified all the fields and click Next.
Select the latest stable Spring Boot version in the Spring Boot field.
Select the following dependencies required for this tutorial:
Web | Spring Web
AI | OpenAI
SQL | Qdrant Vector Database
Click Create to generate and set up the project.
After this, you can see the following structure in the Project view:

The generated Gradle project corresponds to the Maven's standard directory layout:
There are packages and classes under the
main/kotlin
folder that belong to the application.The entry point to the application is the
main()
method of theSpringAiDemoApplication.kt
file.
Update the project configuration
Update your
build.gradle.kts
Gradle build file with the following:plugins { kotlin("jvm") version "2.1.21" kotlin("plugin.spring") version "2.1.21" // Rest of the plugins }Update your
springAiVersion
to1.0.0-M6
:extra["springAiVersion"] = "1.0.0-M6"Click the Sync Gradle Changes button to synchronize the Gradle files.
Update your
src/main/resources/application.properties
file with the following:# 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=trueRun the
SpringAiDemoApplication.kt
file to start the Spring Boot application. Once it's running, open the Qdrant collections page in your browser to see the result:
Create a controller to load and search documents
Create a Spring @RestController
to search documents and store them in the Qdrant collection:
In the
src/main/kotlin/org/example/springaidemo
directory, create a new file namedKotlinSTDController.kt
, and add the following code: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 // Data class representing the chat request payload data class ChatRequest(val query: String, val topK: Int = 3) @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 } }Update the
SpringAiDemoApplication.kt
file to declare aRestTemplate
bean: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) }Run the application.
In the terminal, send a POST request to the
/kotlin/load-docs
endpoint to load the documents:curl -X POST http://localhost:8080/kotlin/load-docsOnce the documents are loaded, you can search for them with a GET request:
curl -X GET http://localhost:8080/kotlin/docs
Implement an AI chat endpoint
Once the documents are loaded, the final step is to add an endpoint that answers questions using the documents in Qdrant through Spring AI's Retrieval-Augmented Generation (RAG) support:
Open the
KotlinSTDController.kt
file, and import the following classes:import org.springframework.ai.chat.client.ChatClient import org.springframework.ai.chat.client.advisor.RetrievalAugmentationAdvisor import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor import org.springframework.ai.chat.prompt.Prompt import org.springframework.ai.chat.prompt.PromptTemplate import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever import org.springframework.web.bind.annotation.RequestBodyAdd
ChatClient.Builder
to the controller's constructor parameters:class KotlinSTDController( private val chatClientBuilder: ChatClient.Builder, private val restTemplate: RestTemplate, private val vectorStore: VectorStore, )Inside the controller class, create a
ChatClient
instance and a query transformer:// Builds the chat client with a simple logging advisor private val chatClient = chatClientBuilder.defaultAdvisors(SimpleLoggerAdvisor()).build() // Builds the query transformer used to rewrite the input query private val rqtBuilder = RewriteQueryTransformer.builder().chatClientBuilder(chatClientBuilder)At the bottom of your
KotlinSTDController.kt
file, add a newchatAsk()
endpoint, with the following logic:@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 {target} documentation. """.trimIndent() ) // Creates the prompt by substituting placeholders with actual values val prompt: Prompt = promptTemplate.create(mapOf("query" to request.query, "target" to "Kotlin standard library")) // Configures the retrieval advisor to augment the query with relevant documents val retrievalAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever( VectorStoreDocumentRetriever.builder() .similarityThreshold(0.7) .topK(request.topK) .vectorStore(vectorStore) .build() ) .queryTransformers(rqtBuilder.promptTemplate(promptTemplate).build()) .build() // Sends the prompt to the LLM with the retrieval advisor and get the response val response = chatClient.prompt(prompt) .advisors(retrievalAdvisor) .call() .content() logger.info("Chat response generated for query: '${request.query}'") return response }Run the application.
In the terminal, send a POST request to the new endpoint to see the results:
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}'
Congratulations! You now have a Kotlin app that connects to OpenAI and answers questions using context retrieved from documentation stored in Qdrant. Try experimenting with different queries or importing other documents to explore more possibilities.
You can view the completed project in the Spring AI demo GitHub repository, or explore other Spring AI examples in Kotlin AI Examples.