From 2ca40cd9a979d5fe165eff5e11399c7c611ee32a Mon Sep 17 00:00:00 2001 From: Thomas Vitale Date: Sun, 28 Jul 2024 14:35:12 +0200 Subject: [PATCH] Enhance use cases with Ollama and PGVector --- 00-use-cases/question-answering/README.md | 14 ++++++++------ 00-use-cases/question-answering/build.gradle | 2 ++ .../com/thomasvitale/ai/spring/ChatService.java | 3 +-- .../ai/spring/IngestionPipeline.java | 2 +- .../ai/spring/QuestionAnswering.java | 9 --------- .../src/main/resources/application.yml | 8 +++++++- .../ai/spring/TestQuestionAnswering.java | 10 ++++++++++ 00-use-cases/semantic-search/README.md | 15 ++++++++------- 00-use-cases/semantic-search/build.gradle | 2 ++ .../thomasvitale/ai/spring/SemanticSearch.java | 9 --------- .../src/main/resources/application.yml | 7 ++++++- ...assification.java => TestSemanticSearch.java} | 16 +++++++++++++--- .../thomasvitale/ai/spring/PatientJournal.java | 9 ++++++++- .../spring/StructuredDataExtractionService.java | 1 - 00-use-cases/text-classification/README.md | 2 +- .../TestTextClassificationApplication.java | 13 +++++++++++++ README.md | 14 +++++++------- 17 files changed, 87 insertions(+), 49 deletions(-) rename 00-use-cases/semantic-search/src/test/java/com/thomasvitale/ai/spring/{TestClassification.java => TestSemanticSearch.java} (60%) diff --git a/00-use-cases/question-answering/README.md b/00-use-cases/question-answering/README.md index c16255c..0c2e0c4 100644 --- a/00-use-cases/question-answering/README.md +++ b/00-use-cases/question-answering/README.md @@ -1,37 +1,39 @@ # Question Answering (RAG) -Ask questions about documents with LLMs via Ollama. +Ask questions about documents with LLMs via Ollama and PGVector. ## Running the application The application relies on Ollama for providing LLMs. You can either run Ollama locally on your laptop, or rely on the Testcontainers support in Spring Boot to spin up an Ollama service automatically. +Furthermore, the application relies on the native Testcontainers support in Spring Boot to spin up a PostgreSQL database with the pgvector extension for embeddings. ### Ollama as a native application First, make sure you have [Ollama](https://ollama.ai) installed on your laptop. -Then, use Ollama to run the _mistral_ large language model. +Then, use Ollama to run the _mistral_ and _nomic-embed-text_ large language models. ```shell ollama run mistral +ollama run nomic-embed-text ``` Finally, run the Spring Boot application. ```shell -./gradlew bootRun +./gradlew bootTestRun ``` ### Ollama as a dev service with Testcontainers -The application relies on the native Testcontainers support in Spring Boot to spin up an Ollama service with a _mistral_ model at startup time. +The application relies on the native Testcontainers support in Spring Boot to spin up an Ollama service with _mistral_ and _nomic-embed-text_ models at startup time. ```shell -./gradlew bootTestRun +./gradlew bootTestRun --args='--spring.profiles.active=ollama-image' ``` ## Calling the application -You can now call the application that will use Ollama and _mistral_ to load text documents as embeddings and generate an answer to your questions based on those documents (RAG pattern). +You can now call the application that will use Ollama with _nomic-embed-text_ and _mistral_ to load text documents as embeddings and generate an answer to your questions based on those documents (RAG pattern). This example uses [httpie](https://httpie.io) to send HTTP requests. ```shell diff --git a/00-use-cases/question-answering/build.gradle b/00-use-cases/question-answering/build.gradle index 734c5e7..3f336dd 100644 --- a/00-use-cases/question-answering/build.gradle +++ b/00-use-cases/question-answering/build.gradle @@ -24,6 +24,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter' + implementation 'org.springframework.ai:spring-ai-pgvector-store-spring-boot-starter' testAndDevelopmentOnly 'org.springframework.boot:spring-boot-devtools' @@ -33,6 +34,7 @@ dependencies { testImplementation 'org.springframework.ai:spring-ai-spring-boot-testcontainers' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:ollama' + testImplementation 'org.testcontainers:postgresql' } tasks.named('test') { diff --git a/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/ChatService.java b/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/ChatService.java index e3a701a..c128188 100644 --- a/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/ChatService.java +++ b/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/ChatService.java @@ -2,7 +2,6 @@ import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; -import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; @@ -19,7 +18,7 @@ class ChatService { String chatWithDocument(String message) { return chatClient.prompt() - .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults().withTopK(5))) + .advisors(new QuestionAnswerAdvisor(vectorStore)) .user(message) .call() .content(); diff --git a/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/IngestionPipeline.java b/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/IngestionPipeline.java index 4d19a8d..a0c7c05 100644 --- a/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/IngestionPipeline.java +++ b/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/IngestionPipeline.java @@ -48,7 +48,7 @@ public void run() { documents.addAll(textReader2.get()); logger.info("Creating and storing Embeddings from Documents"); - vectorStore.add(new TokenTextSplitter(300, 300, 5, 1000, true).split(documents)); + vectorStore.add(new TokenTextSplitter().split(documents)); } } diff --git a/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/QuestionAnswering.java b/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/QuestionAnswering.java index 263a44b..ad7ae34 100644 --- a/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/QuestionAnswering.java +++ b/00-use-cases/question-answering/src/main/java/com/thomasvitale/ai/spring/QuestionAnswering.java @@ -1,11 +1,7 @@ package com.thomasvitale.ai.spring; -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.vectorstore.SimpleVectorStore; -import org.springframework.ai.vectorstore.VectorStore; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; @SpringBootApplication public class QuestionAnswering { @@ -14,9 +10,4 @@ public static void main(String[] args) { SpringApplication.run(QuestionAnswering.class, args); } - @Bean - VectorStore vectorStore(EmbeddingModel embeddingModel) { - return new SimpleVectorStore(embeddingModel); - } - } diff --git a/00-use-cases/question-answering/src/main/resources/application.yml b/00-use-cases/question-answering/src/main/resources/application.yml index 5090f64..70b6a7c 100644 --- a/00-use-cases/question-answering/src/main/resources/application.yml +++ b/00-use-cases/question-answering/src/main/resources/application.yml @@ -4,6 +4,12 @@ spring: chat: options: model: mistral + num-ctx: 4096 embedding: options: - model: mistral + model: nomic-embed-text + vectorstore: + pgvector: + initialize-schema: true + dimensions: 768 + index-type: hnsw diff --git a/00-use-cases/question-answering/src/test/java/com/thomasvitale/ai/spring/TestQuestionAnswering.java b/00-use-cases/question-answering/src/test/java/com/thomasvitale/ai/spring/TestQuestionAnswering.java index 1e1d456..f37c1c9 100644 --- a/00-use-cases/question-answering/src/test/java/com/thomasvitale/ai/spring/TestQuestionAnswering.java +++ b/00-use-cases/question-answering/src/test/java/com/thomasvitale/ai/spring/TestQuestionAnswering.java @@ -5,6 +5,8 @@ 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.Profile; +import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.ollama.OllamaContainer; import org.testcontainers.utility.DockerImageName; @@ -14,6 +16,14 @@ public class TestQuestionAnswering { @Bean @RestartScope @ServiceConnection + PostgreSQLContainer pgvectorContainer() { + return new PostgreSQLContainer<>(DockerImageName.parse("pgvector/pgvector:pg16")); + } + + @Bean + @Profile("ollama-image") + @RestartScope + @ServiceConnection OllamaContainer ollama() { return new OllamaContainer(DockerImageName.parse("ghcr.io/thomasvitale/ollama-mistral") .asCompatibleSubstituteFor("ollama/ollama")); diff --git a/00-use-cases/semantic-search/README.md b/00-use-cases/semantic-search/README.md index a5065a6..49b78c6 100644 --- a/00-use-cases/semantic-search/README.md +++ b/00-use-cases/semantic-search/README.md @@ -1,37 +1,38 @@ # Semantic Search -Semantic search with LLMs via Ollama. +Semantic search with LLMs via Ollama and PGVector. ## Running the application The application relies on Ollama for providing LLMs. You can either run Ollama locally on your laptop, or rely on the Testcontainers support in Spring Boot to spin up an Ollama service automatically. +Furthermore, the application relies on the native Testcontainers support in Spring Boot to spin up a PostgreSQL database with the pgvector extension for embeddings. ### Ollama as a native application First, make sure you have [Ollama](https://ollama.ai) installed on your laptop. -Then, use Ollama to run the _mistral_ large language model. +Then, use Ollama to run the _nomic-embed-text_ large language model. ```shell -ollama run mistral +ollama run nomic-embed-text ``` Finally, run the Spring Boot application. ```shell -./gradlew bootRun +./gradlew bootTestRun ``` ### Ollama as a dev service with Testcontainers -The application relies on the native Testcontainers support in Spring Boot to spin up an Ollama service with a _mistral_ model at startup time. +The application relies on the native Testcontainers support in Spring Boot to spin up an Ollama service with a _nomic-embed-text_ model at startup time. ```shell -./gradlew bootTestRun +./gradlew bootTestRun --args='--spring.profiles.active=ollama-image' ``` ## Calling the application -You can now call the application that will use Ollama and _mistral_ to perform a semantic search. +You can now call the application that will use Ollama and _nomic-embed-text_ to perform a semantic search. This example uses [httpie](https://httpie.io) to send HTTP requests. ```shell diff --git a/00-use-cases/semantic-search/build.gradle b/00-use-cases/semantic-search/build.gradle index 734c5e7..3f336dd 100644 --- a/00-use-cases/semantic-search/build.gradle +++ b/00-use-cases/semantic-search/build.gradle @@ -24,6 +24,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter' + implementation 'org.springframework.ai:spring-ai-pgvector-store-spring-boot-starter' testAndDevelopmentOnly 'org.springframework.boot:spring-boot-devtools' @@ -33,6 +34,7 @@ dependencies { testImplementation 'org.springframework.ai:spring-ai-spring-boot-testcontainers' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:ollama' + testImplementation 'org.testcontainers:postgresql' } tasks.named('test') { diff --git a/00-use-cases/semantic-search/src/main/java/com/thomasvitale/ai/spring/SemanticSearch.java b/00-use-cases/semantic-search/src/main/java/com/thomasvitale/ai/spring/SemanticSearch.java index f0f9e83..76ca28f 100644 --- a/00-use-cases/semantic-search/src/main/java/com/thomasvitale/ai/spring/SemanticSearch.java +++ b/00-use-cases/semantic-search/src/main/java/com/thomasvitale/ai/spring/SemanticSearch.java @@ -1,11 +1,7 @@ package com.thomasvitale.ai.spring; -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.vectorstore.SimpleVectorStore; -import org.springframework.ai.vectorstore.VectorStore; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; @SpringBootApplication public class SemanticSearch { @@ -14,9 +10,4 @@ public static void main(String[] args) { SpringApplication.run(SemanticSearch.class, args); } - @Bean - VectorStore vectorStore(EmbeddingModel embeddingModel) { - return new SimpleVectorStore(embeddingModel); - } - } diff --git a/00-use-cases/semantic-search/src/main/resources/application.yml b/00-use-cases/semantic-search/src/main/resources/application.yml index 143de4c..5295e44 100644 --- a/00-use-cases/semantic-search/src/main/resources/application.yml +++ b/00-use-cases/semantic-search/src/main/resources/application.yml @@ -3,4 +3,9 @@ spring: ollama: embedding: options: - model: mistral + model: nomic-embed-text + vectorstore: + pgvector: + initialize-schema: true + dimensions: 768 + index-type: hnsw diff --git a/00-use-cases/semantic-search/src/test/java/com/thomasvitale/ai/spring/TestClassification.java b/00-use-cases/semantic-search/src/test/java/com/thomasvitale/ai/spring/TestSemanticSearch.java similarity index 60% rename from 00-use-cases/semantic-search/src/test/java/com/thomasvitale/ai/spring/TestClassification.java rename to 00-use-cases/semantic-search/src/test/java/com/thomasvitale/ai/spring/TestSemanticSearch.java index b10a921..198aadb 100644 --- a/00-use-cases/semantic-search/src/test/java/com/thomasvitale/ai/spring/TestClassification.java +++ b/00-use-cases/semantic-search/src/test/java/com/thomasvitale/ai/spring/TestSemanticSearch.java @@ -5,22 +5,32 @@ 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.Profile; +import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.ollama.OllamaContainer; import org.testcontainers.utility.DockerImageName; @TestConfiguration(proxyBeanMethods = false) -public class TestClassification { +public class TestSemanticSearch { @Bean @RestartScope @ServiceConnection + PostgreSQLContainer pgvectorContainer() { + return new PostgreSQLContainer<>(DockerImageName.parse("pgvector/pgvector:pg16")); + } + + @Bean + @Profile("ollama-image") + @RestartScope + @ServiceConnection OllamaContainer ollama() { - return new OllamaContainer(DockerImageName.parse("ghcr.io/thomasvitale/ollama-mistral") + return new OllamaContainer(DockerImageName.parse("ghcr.io/thomasvitale/ollama-nomic-embed-text") .asCompatibleSubstituteFor("ollama/ollama")); } public static void main(String[] args) { - SpringApplication.from(SemanticSearch::main).with(TestClassification.class).run(args); + SpringApplication.from(SemanticSearch::main).with(TestSemanticSearch.class).run(args); } } diff --git a/00-use-cases/structured-data-extraction/src/main/java/com/thomasvitale/ai/spring/PatientJournal.java b/00-use-cases/structured-data-extraction/src/main/java/com/thomasvitale/ai/spring/PatientJournal.java index 8de6216..2a5c3b1 100644 --- a/00-use-cases/structured-data-extraction/src/main/java/com/thomasvitale/ai/spring/PatientJournal.java +++ b/00-use-cases/structured-data-extraction/src/main/java/com/thomasvitale/ai/spring/PatientJournal.java @@ -3,6 +3,13 @@ import java.util.List; public record PatientJournal(String fullName, List observations, Diagnosis diagnosis) { - public record Observation(String type, String content) {} + public record Observation(Type type, String content) {} public record Diagnosis(String content) {} + + enum Type { + BODY_WEIGHT, + TEMPERATURE, + VITAL_SIGNS, + OTHER + } } diff --git a/00-use-cases/structured-data-extraction/src/main/java/com/thomasvitale/ai/spring/StructuredDataExtractionService.java b/00-use-cases/structured-data-extraction/src/main/java/com/thomasvitale/ai/spring/StructuredDataExtractionService.java index 89d0d0b..a1109f2 100644 --- a/00-use-cases/structured-data-extraction/src/main/java/com/thomasvitale/ai/spring/StructuredDataExtractionService.java +++ b/00-use-cases/structured-data-extraction/src/main/java/com/thomasvitale/ai/spring/StructuredDataExtractionService.java @@ -24,7 +24,6 @@ PatientJournal extract(String message) { Extract structured data from the provided text. If you do not know the value of a field asked to extract, do not include any value for the field in the result. - Finally, save the object in the database. --------------------- TEXT: diff --git a/00-use-cases/text-classification/README.md b/00-use-cases/text-classification/README.md index 82dd676..7bc1738 100644 --- a/00-use-cases/text-classification/README.md +++ b/00-use-cases/text-classification/README.md @@ -12,7 +12,7 @@ First, make sure you have [Ollama](https://ollama.ai) installed on your laptop. Then, use Ollama to pull the _mistral_ large language model. ```shell -ollama pull mistral +ollama pull mistral-nemo ``` Finally, run the Spring Boot application. diff --git a/00-use-cases/text-classification/src/test/java/com/thomasvitale/ai/spring/TestTextClassificationApplication.java b/00-use-cases/text-classification/src/test/java/com/thomasvitale/ai/spring/TestTextClassificationApplication.java index 94b6b18..a387758 100644 --- a/00-use-cases/text-classification/src/test/java/com/thomasvitale/ai/spring/TestTextClassificationApplication.java +++ b/00-use-cases/text-classification/src/test/java/com/thomasvitale/ai/spring/TestTextClassificationApplication.java @@ -1,9 +1,22 @@ package com.thomasvitale.ai.spring; import org.springframework.boot.SpringApplication; +import org.springframework.boot.devtools.restart.RestartScope; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.ollama.OllamaContainer; +import org.testcontainers.utility.DockerImageName; public class TestTextClassificationApplication { + @Bean + @RestartScope + @ServiceConnection + OllamaContainer ollama() { + return new OllamaContainer(DockerImageName.parse("ghcr.io/thomasvitale/ollama-mistral-nemo") + .asCompatibleSubstituteFor("ollama/ollama")); + } + public static void main(String[] args) { SpringApplication.from(TextClassificationApplication::main) .with(TestcontainersConfiguration.class).run(args); diff --git a/README.md b/README.md index 4f0126e..4f9e0e6 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ Samples showing how to build Java applications powered by Generative AI and LLMs ### 0. Use Cases -| Project | Description | -|-----------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------| -| [chatbot](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/chatbot) | Chatbot using LLMs via Ollama. | -| [question-answering](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/question-answering) | Question answering with documents (RAG) using LLMs via Ollama. | -| [semantic-search](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/semantic-search) | Semantic search using LLMs via Ollama. | -| [structured-data-extraction](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/structured-data-extraction) | Structured data extraction using LLMs via Ollama. | -| [text-classification](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/text-classification) | Text classification using LLMs via Ollama. | +| Project | Description | +|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| +| [chatbot](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/chatbot) | Chatbot using LLMs via Ollama. | +| [question-answering](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/question-answering) | Question answering with documents (RAG) using LLMs via Ollama and PGVector. | +| [semantic-search](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/semantic-search) | Semantic search using LLMs via Ollama and PGVector. | +| [structured-data-extraction](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/structured-data-extraction) | Structured data extraction using LLMs via Ollama. | +| [text-classification](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/00-use-cases/text-classification) | Text classification using LLMs via Ollama. | ### 1. Chat Completion Models