diff --git a/rag/rag-springai-ollama-llm/docker/docker-compose.yml b/rag/rag-springai-ollama-llm/docker/docker-compose.yml
index 928f97c..e9d1046 100644
--- a/rag/rag-springai-ollama-llm/docker/docker-compose.yml
+++ b/rag/rag-springai-ollama-llm/docker/docker-compose.yml
@@ -1,19 +1,62 @@
services:
- ollama:
- image: langchain4j/ollama-llama3:latest
- ports:
- - '11434:11434'
- redis-stack:
- image: redis/redis-stack-server
- ports:
- - '6379:6379'
- lgtm-stack:
- image: grafana/otel-lgtm:0.8.0
- extra_hosts: ['host.docker.internal:host-gateway']
- container_name: lgtm-stack
- environment:
- - OTEL_METRIC_EXPORT_INTERVAL=500
- ports:
- - "3000:3000"
- - "4317:4317"
- - "4318:4318"
\ No newline at end of file
+
+ ollama:
+ container_name: ollama
+ image: ollama/ollama:latest
+ ports:
+ - '11434:11434'
+
+ postgresqldb:
+ container_name: postgresqldb
+ image: pgvector/pgvector:pg17
+ extra_hosts: [ 'host.docker.internal:host-gateway' ]
+ restart: always
+ environment:
+ - POSTGRES_USER=appuser
+ - POSTGRES_PASSWORD=secret
+ - POSTGRES_DB=appdb
+ - PGPASSWORD=secret
+ logging:
+ options:
+ max-size: 10m
+ max-file: "3"
+ ports:
+ - '5432:5432'
+ healthcheck:
+ test: "pg_isready -U appuser -d appdb"
+ interval: 2s
+ timeout: 20s
+ retries: 10
+
+ pgadmin:
+ container_name: pgadmin_container
+ image: dpage/pgadmin4
+ extra_hosts: [ 'host.docker.internal:host-gateway' ]
+ environment:
+ PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org}
+ PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
+ PGADMIN_CONFIG_SERVER_MODE: "False"
+ PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
+ ports:
+ - "${PGADMIN_PORT:-5050}:80"
+ depends_on:
+ postgresqldb:
+ condition: service_healthy
+ volumes:
+ - ./docker_pgadmin_servers.json:/pgadmin4/servers.json
+ entrypoint:
+ - "/bin/sh"
+ - "-c"
+ - "/bin/echo 'postgresqldb:5432:*:appuser:secret' > /tmp/pgpassfile && chmod 600 /tmp/pgpassfile && /entrypoint.sh"
+
+ lgtm-stack:
+ image: grafana/otel-lgtm:0.8.1
+ extra_hosts: [ 'host.docker.internal:host-gateway' ]
+ container_name: lgtm-stack
+ environment:
+ - OTEL_METRIC_EXPORT_INTERVAL=500
+ ports:
+ - "3000:3000"
+ - "4317:4317"
+ - "4318:4318"
+ - "9090:9090"
diff --git a/rag/rag-springai-ollama-llm/docker/docker_pgadmin_servers.json b/rag/rag-springai-ollama-llm/docker/docker_pgadmin_servers.json
new file mode 100644
index 0000000..7e97769
--- /dev/null
+++ b/rag/rag-springai-ollama-llm/docker/docker_pgadmin_servers.json
@@ -0,0 +1,14 @@
+{
+ "Servers": {
+ "1": {
+ "Name": "Docker Compose DB",
+ "Group": "Servers",
+ "Port": 5432,
+ "Username": "appuser",
+ "Host": "postgresqldb",
+ "SSLMode": "prefer",
+ "MaintenanceDB": "appdb",
+ "PassFile": "/tmp/pgpassfile"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rag/rag-springai-ollama-llm/pom.xml b/rag/rag-springai-ollama-llm/pom.xml
index 3fd60de..1730d20 100644
--- a/rag/rag-springai-ollama-llm/pom.xml
+++ b/rag/rag-springai-ollama-llm/pom.xml
@@ -5,7 +5,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.3.6
+ 3.4.1
com.learning.ai
@@ -16,7 +16,8 @@
21
- 1.0.0-M4
+ 1.0.0-M5
+ 2.10.0-alpha
2.43.0
@@ -39,11 +40,7 @@
org.springframework.ai
- spring-ai-redis-store-spring-boot-starter
-
-
- org.apache.commons
- commons-pool2
+ spring-ai-pgvector-store-spring-boot-starter
org.springframework.ai
@@ -56,7 +53,14 @@
org.springdoc
springdoc-openapi-starter-webmvc-ui
- 2.6.0
+ 2.7.0
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
@@ -76,6 +80,17 @@
io.micrometer
micrometer-registry-otlp
+
+ net.ttddyy.observation
+ datasource-micrometer-spring-boot
+ 1.0.6
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-logback-appender-1.0
+ runtime
+
+
org.springframework.boot
spring-boot-starter-test
@@ -97,16 +112,14 @@
test
- com.redis.testcontainers
- testcontainers-redis
+ org.testcontainers
+ grafana
test
- 1.6.4
org.testcontainers
- grafana
+ postgresql
test
- 1.20.4
io.rest-assured
@@ -124,6 +137,13 @@
pom
import
+
+ io.opentelemetry.instrumentation
+ opentelemetry-instrumentation-bom-alpha
+ ${otelInstrumentation.version}
+ pom
+ import
+
@@ -144,7 +164,7 @@
- 2.47.0
+ 2.50.0
diff --git a/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/config/RestClientBuilderConfig.java b/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/config/RestClientBuilderConfig.java
deleted file mode 100644
index 8adbc8d..0000000
--- a/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/config/RestClientBuilderConfig.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.learning.ai.llmragwithspringai.config;
-
-import java.time.Duration;
-import org.springframework.boot.web.client.ClientHttpRequestFactories;
-import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
-import org.springframework.boot.web.client.RestClientCustomizer;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration(proxyBeanMethods = false)
-public class RestClientBuilderConfig {
-
- @Bean
- RestClientCustomizer restClientCustomizer() {
- return restClientBuilder -> restClientBuilder.requestFactory(
- ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS
- .withConnectTimeout(Duration.ofSeconds(60))
- .withReadTimeout(Duration.ofMinutes(5))));
- }
-}
diff --git a/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/service/AIChatService.java b/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/service/AIChatService.java
index 481e7b0..44e8f18 100644
--- a/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/service/AIChatService.java
+++ b/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/service/AIChatService.java
@@ -3,10 +3,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
-import org.springframework.ai.chat.model.ChatResponse;
-import org.springframework.ai.chat.model.Generation;
-import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.chat.client.advisor.RetrievalAugmentationAdvisor;
+import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
+import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
@@ -15,33 +14,29 @@ public class AIChatService {
private static final Logger LOGGER = LoggerFactory.getLogger(AIChatService.class);
- private static final String template =
- """
- You are a helpful assistant, conversing with a user about the subjects contained in a set of documents.
- Use the information from the DOCUMENTS section to provide accurate answers. If unsure or if the answer
- isn't found in the DOCUMENTS section, simply state that you don't know the answer.
+ private final ChatClient aiClient;
- DOCUMENTS:
- {question_answer_context}
+ public AIChatService(ChatClient.Builder builder, VectorStore vectorStore) {
- """;
+ var documentRetriever = VectorStoreDocumentRetriever.builder()
+ .vectorStore(vectorStore)
+ .similarityThreshold(0.50)
+ .build();
- private final ChatClient aiClient;
- private final VectorStore vectorStore;
+ var queryAugmenter =
+ ContextualQueryAugmenter.builder().allowEmptyContext(true).build();
- public AIChatService(ChatClient.Builder builder, VectorStore vectorStore) {
- this.aiClient = builder.build();
- this.vectorStore = vectorStore;
+ RetrievalAugmentationAdvisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
+ .documentRetriever(documentRetriever)
+ .queryAugmenter(queryAugmenter)
+ .build();
+ this.aiClient =
+ builder.clone().defaultAdvisors(retrievalAugmentationAdvisor).build();
}
public String chat(String query) {
- ChatResponse aiResponse = aiClient.prompt()
- .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.query(query), template))
- .user(query)
- .call()
- .chatResponse();
+ String aiResponse = aiClient.prompt().user(query).call().content();
LOGGER.info("Response received from call :{}", aiResponse);
- Generation generation = aiResponse.getResult();
- return (generation != null) ? generation.getOutput().getContent() : "";
+ return aiResponse;
}
}
diff --git a/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/service/DataIndexerService.java b/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/service/DataIndexerService.java
index aed87d0..8abe342 100644
--- a/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/service/DataIndexerService.java
+++ b/rag/rag-springai-ollama-llm/src/main/java/com/learning/ai/llmragwithspringai/service/DataIndexerService.java
@@ -1,6 +1,7 @@
package com.learning.ai.llmragwithspringai.service;
import java.util.Map;
+import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.DocumentReader;
@@ -58,11 +59,11 @@ public void loadData(Resource documentResource) {
return documents;
};
vectorStore.accept(metadataEnricher.apply(tokenTextSplitter.apply(documentReader.get())));
- LOGGER.info("Loaded document to redis vector database.");
+ LOGGER.info("Loaded document to vector database.");
}
}
public long count() {
- return this.vectorStore.similaritySearch("*").size();
+ return Objects.requireNonNull(this.vectorStore.similaritySearch("*")).size();
}
}
diff --git a/rag/rag-springai-ollama-llm/src/main/resources/application-local.properties b/rag/rag-springai-ollama-llm/src/main/resources/application-local.properties
new file mode 100644
index 0000000..d500714
--- /dev/null
+++ b/rag/rag-springai-ollama-llm/src/main/resources/application-local.properties
@@ -0,0 +1,11 @@
+## WARNING: Development-only configuration
+## The following setting will DELETE existing vector store data on startup
+spring.ai.vectorstore.pgvector.removeExistingVectorStoreTable=true
+spring.ai.ollama.baseUrl=http://localhost:11434
+
+
+logging.level.org.springframework.ai.rag=debug
+
+spring.datasource.url=jdbc:postgresql://localhost/appdb
+spring.datasource.username=appuser
+spring.datasource.password=secret
diff --git a/rag/rag-springai-ollama-llm/src/main/resources/application.properties b/rag/rag-springai-ollama-llm/src/main/resources/application.properties
index 6a0724c..4d7301f 100644
--- a/rag/rag-springai-ollama-llm/src/main/resources/application.properties
+++ b/rag/rag-springai-ollama-llm/src/main/resources/application.properties
@@ -3,6 +3,7 @@ spring.application.name=rag-springai-ollama-llm
spring.threads.virtual.enabled=true
spring.mvc.problemdetails.enabled=true
+spring.ai.ollama.init.pull-model-strategy=WHEN_MISSING
spring.ai.ollama.chat.options.model=mistral
spring.ai.ollama.chat.options.temperature=0.3
spring.ai.ollama.chat.options.top-k=2
@@ -10,19 +11,24 @@ spring.ai.ollama.chat.options.top-p=0.2
spring.ai.ollama.embedding.options.model=nomic-embed-text
-spring.ai.vectorstore.redis.index=vector_store
-spring.ai.vectorstore.redis.prefix=ai
-spring.ai.vectorstore.redis.initialize-schema=true
+#PgVector
+spring.ai.vectorstore.observations.include-query-response=true
+spring.ai.vectorstore.pgvector.initialize-schema=true
-spring.ai.ollama.baseUrl=http://localhost:11434
+spring.http.client.connect-timeout=PT1M
+spring.http.client.read-timeout=PT5M
spring.testcontainers.beans.startup=parallel
##Observability
spring.ai.chat.observations.include-completion=true
spring.ai.chat.observations.include-prompt=true
+spring.ai.chat.client.observations.include-input=true
management.endpoints.web.exposure.include=*
management.metrics.tags.service.name=${spring.application.name}
management.tracing.sampling.probability=1.0
management.otlp.tracing.endpoint=http://localhost:4318/v1/traces
+management.otlp.logging.endpoint=http://localhost:4318/v1/logs
+
+logging.level.org.springframework.ai.rag=info
diff --git a/rag/rag-springai-ollama-llm/src/main/resources/logback-spring.xml b/rag/rag-springai-ollama-llm/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..1e2ef31
--- /dev/null
+++ b/rag/rag-springai-ollama-llm/src/main/resources/logback-spring.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ true
+ true
+ true
+
+
+
+
+
+
+
diff --git a/rag/rag-springai-ollama-llm/src/test/java/com/learning/ai/llmragwithspringai/config/TestcontainersConfiguration.java b/rag/rag-springai-ollama-llm/src/test/java/com/learning/ai/llmragwithspringai/config/TestcontainersConfiguration.java
index 4c7deda..b40831c 100644
--- a/rag/rag-springai-ollama-llm/src/test/java/com/learning/ai/llmragwithspringai/config/TestcontainersConfiguration.java
+++ b/rag/rag-springai-ollama-llm/src/test/java/com/learning/ai/llmragwithspringai/config/TestcontainersConfiguration.java
@@ -1,13 +1,10 @@
package com.learning.ai.llmragwithspringai.config;
-import com.redis.testcontainers.RedisStackContainer;
-import java.io.IOException;
import java.time.Duration;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Scope;
-import org.springframework.test.context.DynamicPropertyRegistry;
+import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.grafana.LgtmStackContainer;
import org.testcontainers.ollama.OllamaContainer;
import org.testcontainers.utility.DockerImageName;
@@ -17,30 +14,21 @@ public class TestcontainersConfiguration {
@Bean
@ServiceConnection
- OllamaContainer ollama() throws IOException, InterruptedException {
- // The model name to use (e.g., "orca-mini", "mistral", "llama2", "codellama", "phi", or
- // "tinyllama")
- OllamaContainer ollamaContainer = new OllamaContainer(
- DockerImageName.parse("langchain4j/ollama-mistral:latest").asCompatibleSubstituteFor("ollama/ollama"));
- ollamaContainer.start();
- ollamaContainer.execInContainer("ollama", "pull", "nomic-embed-text");
- return ollamaContainer;
+ OllamaContainer ollama() {
+ return new OllamaContainer(DockerImageName.parse("ollama/ollama"));
}
@Bean
- RedisStackContainer redisContainer(DynamicPropertyRegistry properties) {
- RedisStackContainer redis = new RedisStackContainer(
- RedisStackContainer.DEFAULT_IMAGE_NAME.withTag(RedisStackContainer.DEFAULT_TAG));
- properties.add("spring.ai.vectorstore.redis.uri", () -> "redis://%s:%d"
- .formatted(redis.getHost(), redis.getMappedPort(6379)));
- return redis;
+ @ServiceConnection
+ PostgreSQLContainer> postgreSQLContainer() {
+ return new PostgreSQLContainer<>(DockerImageName.parse("pgvector/pgvector:pg17"))
+ .withStartupTimeout(Duration.ofMinutes(2));
}
@Bean
- @Scope("singleton")
- @ServiceConnection("otel/opentelemetry-collector-contrib")
+ @ServiceConnection
LgtmStackContainer lgtmStackContainer() {
- return new LgtmStackContainer(DockerImageName.parse("grafana/otel-lgtm").withTag("0.7.1"))
+ return new LgtmStackContainer(DockerImageName.parse("grafana/otel-lgtm").withTag("0.8.1"))
.withStartupTimeout(Duration.ofMinutes(2));
}
}