Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@

<modules>
<module>springboot-modules</module>
<module>springboot-modules2</module>
<module>testContainers</module>
</modules>

Expand Down
61 changes: 61 additions & 0 deletions springboot-modules2/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.kodesastra</groupId>
<artifactId>kodesastra_blog</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<groupId>com.ks</groupId>
<artifactId>springboot-modules2</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>


<modules>
<module>springai-opensearch</module>
</modules>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.maven.plugin.version}</version>
</plugin>
</plugins>
</build>

<properties>
<spring-boot.version>3.5.11</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<springboot.maven.plugin.version>3.5.11</springboot.maven.plugin.version>
</properties>

</project>
65 changes: 65 additions & 0 deletions springboot-modules2/springai-opensearch/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ks</groupId>
<artifactId>springboot-modules2</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<groupId>com.kodesastra</groupId>
<artifactId>springai-opensearch</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-opensearch</artifactId>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-azure-openai</artifactId>
</dependency>
<!-- Source:
https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-pdf-document-reader -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
</dependencies>


<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.19.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ks.opensearch;

import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenSearchAppConfiguration {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ks.opensearch;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OpenSearchSpringAiApplication {
public static void main(String[] args) {
SpringApplication.run(OpenSearchSpringAiApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ks.opensearch.service;

import java.util.List;

import org.springframework.ai.document.Document;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;

public class PdfDocumentCreator {

public static List<Document>fetchDocumentChunksFromPdf(String pdfFilePath) {
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(pdfFilePath);
TokenTextSplitter splitter = new TokenTextSplitter();
return splitter.transform(pdfReader.get());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
spring:
application:
name: spring-ai-azureopenai

ai:
azure:
openai:
api-key: <<AZURE OPENAI DEPLOYMENT API KEY>>
endpoint: <<AZURE OPENAI DEPLOYMENT ENDPOINT>>
chat:
options:
user: springai-azureopenai-user
deployment-name: gpt-5-mini
temperature: 1
embedding:
options:
deployment-name: text-embedding-ada-002
model: text-embedding-ada-002

vectorstore:
opensearch:
uris: http://192.168.1.36:9200
index-name: book-index
initialize-schema: true
similarity-function: cosinesimil
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
services:
opensearch-node1: # This is also the hostname of the container within the Docker network (i.e. https://opensearch-node1/)
image: opensearchproject/opensearch:latest
container_name: opensearch-node1
environment:
- DISABLE_SECURITY_PLUGIN=true
- cluster.name=opensearch-cluster # Name the cluster
- node.name=opensearch-node1 # Name the node that will run in this container
- discovery.seed_hosts=opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster
- cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 # Nodes eligibile to serve as cluster manager
- bootstrap.memory_lock=true # Disable JVM heap memory swapping
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} # Sets the demo admin user password when using demo configuration (for OpenSearch 2.12 and later)
ulimits:
memlock:
soft: -1 # Set memlock to unlimited (no soft or hard limit)
hard: -1
nofile:
soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
hard: 65536
volumes:
- opensearch-data1:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container
ports:
- 9200:9200 # REST API
- 9600:9600 # Performance Analyzer
networks:
- opensearch-net # All of the containers will join the same Docker bridge network
opensearch-node2:
image: opensearchproject/opensearch:latest # This should be the same image used for opensearch-node1 to avoid issues
container_name: opensearch-node2
environment:
- DISABLE_SECURITY_PLUGIN=true
- cluster.name=opensearch-cluster
- node.name=opensearch-node2
- discovery.seed_hosts=opensearch-node1,opensearch-node2
- cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2
- bootstrap.memory_lock=true
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- opensearch-data2:/usr/share/opensearch/data
networks:
- opensearch-net
opensearch-dashboards:
image: opensearchproject/opensearch-dashboards:latest # Make sure the version of opensearch-dashboards matches the version of opensearch installed on other nodes
container_name: opensearch-dashboards
ports:
- 5601:5601 # Map host port 5601 to container port 5601
expose:
- "5601" # Expose port 5601 for web access to OpenSearch Dashboards
environment:
- OPENSEARCH_HOSTS=["http://opensearch-node1:9200","http://opensearch-node2:9200"] # Define the OpenSearch nodes that OpenSearch Dashboards will query
- DISABLE_SECURITY_DASHBOARDS_PLUGIN=true
networks:
- opensearch-net

volumes:
opensearch-data1:
opensearch-data2:

networks:
opensearch-net:
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.ks.opensearch;


import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.pdf.ParagraphPdfDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.opensearch.OpenSearchVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class OpenSearchLiveTest {

private static final Logger logger = LoggerFactory.getLogger(OpenSearchLiveTest.class);

@Autowired
private OpenSearchVectorStore vectorStore;

@Value("classpath:/rag/spring-framework.pdf")
private String filePath;

@Autowired
private AzureOpenAiChatModel azureOpenAiChatModel;

@Test
@Order(1)
void whenSaveDocChunks_thenDocumentsAddedToVectorStore() {
ParagraphPdfDocumentReader paragraphPdfDocumentReader = new ParagraphPdfDocumentReader(filePath);
TokenTextSplitter tokenTextSplitter = new TokenTextSplitter();

List<Document> documentChunks = tokenTextSplitter.apply(paragraphPdfDocumentReader.read());
assertThat(documentChunks).isNotEmpty();
assertThat(documentChunks.size()).isGreaterThan(0);

vectorStore.add(documentChunks);
}


@ParameterizedTest
@ValueSource(strings = {
"What is Inversion of Control in Spring Framework?",
"What happens in Autumn season?"
})
@Order(2)
void whenSearch_thenRelevantDocumentsReturned(String userQuery) {
Message userMessage = new UserMessage(userQuery);

SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("""

You are a helpful assistant for answering questions related to Spring Framework.
You will answer the questions strictly based on the context provided to you:
{context}
If you don't know the answer, say you don't know.

""");

List<Document> relevantDocuments = vectorStore.similaritySearch(userQuery);

assertThat(relevantDocuments).isNotEmpty().hasSizeGreaterThan(0);

String contextContent = relevantDocuments.stream()
.map(Document::getText)
.reduce((a, b) -> a + "\n" + b)
.orElse("");

Message systemMessage = systemPromptTemplate
.createMessage(Map.of("context", contextContent));

Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
ChatResponse chatResponse = azureOpenAiChatModel.call(prompt);

assertThat(chatResponse.getResult().getOutput().getText()).isNotEmpty();

logger.info("User query: {}", userQuery);
logger.info("Chat Response: {}", chatResponse.getResult().getOutput().getText());
}
}
Loading