From 62e6ae2ef6cc1a88f1554676a61a75939a0089de Mon Sep 17 00:00:00 2001 From: Salvatore Mongiardo Date: Fri, 13 Mar 2026 17:51:43 +0100 Subject: [PATCH] Adding milvus example --- README.adoc | 4 +- milvus/input/cities.txt | 24 +++ milvus/pom.xml | 125 +++++++++++ milvus/readme.adoc | 94 ++++++++ .../springboot/milvus/Application.java | 28 +++ .../springboot/milvus/bean/VectorUtils.java | 67 ++++++ .../src/main/resources/application.properties | 13 ++ .../src/main/resources/camel/milvus-rag.yaml | 202 ++++++++++++++++++ pom.xml | 1 + 9 files changed, 557 insertions(+), 1 deletion(-) create mode 100644 milvus/input/cities.txt create mode 100644 milvus/pom.xml create mode 100644 milvus/readme.adoc create mode 100644 milvus/src/main/java/org/apache/camel/example/springboot/milvus/Application.java create mode 100644 milvus/src/main/java/org/apache/camel/example/springboot/milvus/bean/VectorUtils.java create mode 100644 milvus/src/main/resources/application.properties create mode 100644 milvus/src/main/resources/camel/milvus-rag.yaml diff --git a/README.adoc b/README.adoc index b95c2c14..48e9a544 100644 --- a/README.adoc +++ b/README.adoc @@ -27,7 +27,7 @@ readme's instructions. === Examples // examples: START -Number of Examples: 69 (0 deprecated) +Number of Examples: 70 (0 deprecated) [width="100%",cols="4,2,4",options="header"] |=== @@ -35,6 +35,8 @@ Number of Examples: 69 (0 deprecated) | link:ai-agent/README.adoc[Ai Agent] (ai-agent) | AI | An example showing how to work with Camel Spring AI for chat, tools, and vector store +| link:milvus/readme.adoc[Milvus] (milvus) | AI | An example showing vector similarity search on European cities using Camel OpenAI and Milvus + | link:aot-basic/readme.adoc[Aot Basic] (aot-basic) | AOT | Example on how to leverage Spring Boot AOT in Camel Spring Boot | link:endpointdsl/readme.adoc[Endpointdsl] (endpointdsl) | Beginner | Using type-safe Endpoint DSL diff --git a/milvus/input/cities.txt b/milvus/input/cities.txt new file mode 100644 index 00000000..8c1f2ed4 --- /dev/null +++ b/milvus/input/cities.txt @@ -0,0 +1,24 @@ +Rome +Paris +London +Madrid +Berlin +Prague +Vienna +Amsterdam +Lisbon +Barcelona +Dublin +Brussels +Warsaw +Istanbul +Budapest +Copenhagen +Stockholm +Oslo +Helsinki +Athens +Zurich +Milan +Sofia + diff --git a/milvus/pom.xml b/milvus/pom.xml new file mode 100644 index 00000000..0f79557d --- /dev/null +++ b/milvus/pom.xml @@ -0,0 +1,125 @@ + + + + + 4.0.0 + + org.apache.camel.springboot.example + examples + 4.19.0-SNAPSHOT + + + camel-example-spring-boot-milvus + Camel SB Examples :: Milvus + An example showing vector similarity search on European cities using Camel OpenAI and Milvus + + + AI + UTF-8 + UTF-8 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot-version} + pom + import + + + + org.apache.camel.springboot + camel-spring-boot-bom + ${project.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + + + + + org.apache.camel.springboot + camel-spring-boot-starter + + + org.apache.camel.springboot + camel-milvus-starter + + + org.apache.camel.springboot + camel-openai-starter + + + org.apache.camel.springboot + camel-file-starter + + + org.apache.camel.springboot + camel-yaml-dsl-starter + + + org.apache.camel.springboot + camel-jackson-starter + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.apache.camel + camel-test-spring-junit6 + ${camel-version} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot-version} + + + + repackage + + + + + + + diff --git a/milvus/readme.adoc b/milvus/readme.adoc new file mode 100644 index 00000000..4807a81c --- /dev/null +++ b/milvus/readme.adoc @@ -0,0 +1,94 @@ +== Camel Example Spring Boot Milvus Vector Search + +This example shows how to use Apache Camel with Milvus as a vector database to perform +similarity search on European cities based on geographic and demographic features. + +OpenAI (via Ollama) is used to generate city data (country, latitude, longitude, +population in millions) which is stored as a 3-dimensional feature vector in Milvus. +A similarity search then finds cities with the most similar characteristics. + +The pipeline works end-to-end: + +1. **Insert**: LLM returns raw city data (`Rome: Italy;40.41;12.51;2.81`) → `VectorUtils` normalizes to 0-1 range → stored in Milvus as 3D vector +2. **Search**: LLM returns raw Rome coordinates → normalized → Milvus L2 similarity search → finds Rome, Milan, Zurich as the 3 closest cities +3. **Explain**: The results are sent back to the LLM which provides a natural language explanation with approximate distances + +=== Prerequisites + +1. A running Milvus instance (using https://camel.apache.org/manual/camel-jbang.html[Camel JBang]): + +---- +camel infra run milvus +---- + +2. A running Ollama instance with the required model: + +---- +ollama serve +ollama pull granite4:3b +---- + +=== How to run + +You can run this example using: + +---- +mvn spring-boot:run +---- + +=== What happens + +When the application starts, it executes the following pipeline: + +1. **Create Collection** - Creates a `cities` collection in Milvus with four fields: `id` (Int64 primary key), `city` (VarChar), `country` (VarChar), and `features` (3-dimensional FloatVector). The `features` vector encodes each city as `[latitude, longitude, population_millions]`, normalized to 0-1 range. An IVF_FLAT index with L2 distance metric is created on the vector field. +2. **Insert Cities** - Reads city names from `input/cities.txt`. For each city, it asks the LLM via `openai:chat-completion` to provide the country, latitude, longitude, and population (in millions). The response is parsed using Camel Simple expressions and normalized to a float vector via `VectorUtils`, then inserted into Milvus. +3. **Similarity Search** - Searches for the 3 cities most similar to Rome based on L2 distance of their feature vectors. The search vector is also generated by the LLM. +4. **Explain** - The search results are sent back to the LLM which provides a natural language explanation with approximate distances. + +=== Expected output + +---- +Creating Milvus collection: cities +Collection created successfully +Index created successfully +Processing cities from: cities.txt +City data for Rome: Italy;41.90;12.50;2.87 +Inserted Rome into Milvus +City data for Paris: France;48.86;2.35;2.16 +Inserted Paris into Milvus +... +All cities indexed. Starting similarity search... +Searching for three closest cities to: Rome +Search vector for Rome: 41.90;12.50;2.87 +Closest cities to Rome: [{rank=1, city=Rome, country=Italy}, {rank=2, city=Milan, country=Italy}, {rank=3, city=Zurich, country=Switzerland}]. +Answer: Rome: 0 km, Milan: 200 km, Zurich: 400 km +---- + +NOTE: The exact similarity results may vary depending on the LLM responses. + +=== Configuration + +Edit `src/main/resources/application.properties` to configure: + +* `camel.component.milvus.host` - Milvus server host (default: localhost) +* `camel.component.milvus.port` - Milvus gRPC port (default: 19530) +* `camel.component.openai.base-url` - OpenAI-compatible API base URL (default: Ollama at http://localhost:11434/v1) +* `camel.component.openai.model` - Chat completion model (default: granite4:3b) + +To use OpenAI instead of Ollama, change the base URL and set your API key: + +---- +camel.component.openai.base-url=https://api.openai.com/v1 +camel.component.openai.api-key=${OPENAI_API_KEY} +camel.component.openai.model=gpt-4o-mini +---- + +=== Help and contributions + +If you hit any problem using Camel or have some feedback, then please +https://camel.apache.org/support.html[let us know]. + +We also love contributors, so +https://camel.apache.org/contributing.html[get involved] :-) + +The Camel riders! diff --git a/milvus/src/main/java/org/apache/camel/example/springboot/milvus/Application.java b/milvus/src/main/java/org/apache/camel/example/springboot/milvus/Application.java new file mode 100644 index 00000000..e50125c5 --- /dev/null +++ b/milvus/src/main/java/org/apache/camel/example/springboot/milvus/Application.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.example.springboot.milvus; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/milvus/src/main/java/org/apache/camel/example/springboot/milvus/bean/VectorUtils.java b/milvus/src/main/java/org/apache/camel/example/springboot/milvus/bean/VectorUtils.java new file mode 100644 index 00000000..ae840d3a --- /dev/null +++ b/milvus/src/main/java/org/apache/camel/example/springboot/milvus/bean/VectorUtils.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.example.springboot.milvus.bean; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.camel.Exchange; +import org.apache.camel.Processor; + +public class VectorUtils implements Processor { + + private String normalizationRanges; + private float[][] ranges; + + @Override + public void process(Exchange exchange) throws Exception { + // Converts a semicolon-separated string of numeric values into a normalized List feature vector. + String[] parts = exchange.getIn().getBody(String.class).trim().split(";"); + List vector = new ArrayList<>(parts.length); + for (int i = 0; i < parts.length; i++) { + float val = Float.parseFloat(parts[i].trim()); + if (ranges != null && i < ranges.length) { + val = (val - ranges[i][0]) / (ranges[i][1] - ranges[i][0]); + val = Math.max(0.0f, Math.min(1.0f, val)); + } + vector.add(val); + } + exchange.getIn().setBody(vector); + } + + public String getNormalizationRanges() { + return normalizationRanges; + } + + /** + * Each dimension is normalized to a 0-1 range using configurable min/max ranges, so all features contribute equally to distance calculations in Milvus. + */ + public void setNormalizationRanges(String normalizationRanges) { + this.normalizationRanges = normalizationRanges; + if (normalizationRanges != null && !normalizationRanges.isBlank()) { + String[] pairs = normalizationRanges.split(","); + this.ranges = new float[pairs.length][2]; + for (int i = 0; i < pairs.length; i++) { + String[] minMax = pairs[i].trim().split(":"); + this.ranges[i][0] = Float.parseFloat(minMax[0].trim()); + this.ranges[i][1] = Float.parseFloat(minMax[1].trim()); + } + } else { + this.ranges = null; + } + } +} diff --git a/milvus/src/main/resources/application.properties b/milvus/src/main/resources/application.properties new file mode 100644 index 00000000..b08e0120 --- /dev/null +++ b/milvus/src/main/resources/application.properties @@ -0,0 +1,13 @@ +camel.main.name=MilvusCitiesExample +camel.main.run-controller=true + +input.dir=input + +# Milvus +camel.component.milvus.host=localhost +camel.component.milvus.port=19530 + +# OpenAI component (configured for Ollama) +camel.component.openai.base-url=http://localhost:11434/v1 +camel.component.openai.api-key=ollama +camel.component.openai.model=granite4:3b diff --git a/milvus/src/main/resources/camel/milvus-rag.yaml b/milvus/src/main/resources/camel/milvus-rag.yaml new file mode 100644 index 00000000..93b68635 --- /dev/null +++ b/milvus/src/main/resources/camel/milvus-rag.yaml @@ -0,0 +1,202 @@ +- beans: + - name: ragCreateCollection + type: org.apache.camel.component.milvus.rag.RAGCreateCollection + properties: + collectionName: "cities" + dimension: "3" + textFieldName: "city" + vectorFieldName: "features" + textFieldMaxLength: "128" + additionalTextFields: "country,latitude,longitude" + - name: ragCreateIndex + type: org.apache.camel.component.milvus.rag.RAGCreateIndex + properties: + collectionName: "cities" + vectorFieldName: "features" + indexType: "IVF_FLAT" + metricType: "L2" + - name: ragSearch + type: org.apache.camel.component.milvus.rag.RAGSearch + properties: + collectionName: "cities" + outputFields: "city,country,latitude,longitude" + limit: "4" + - name: ragResultExtractor + type: org.apache.camel.component.milvus.rag.RAGResultExtractor + properties: + outputFields: "city,country,latitude,longitude" + - name: ragInsert + type: org.apache.camel.component.milvus.rag.RAGInsert + properties: + collectionName: "cities" + vectorFieldName: "features" + textFieldMappings: "city=cityName,country=cityCountry,latitude=cityLatitude,longitude=cityLongitude" + - name: vectorUtils + type: org.apache.camel.example.springboot.milvus.bean.VectorUtils + properties: + normalizationRanges: "35.0:65.0,-10.0:30.0,0.0:20.0" + +# Retry bad LLM responses, then skip +- onException: + exception: + - java.lang.NumberFormatException + - java.lang.RuntimeException + handled: + constant: "true" + redeliveryPolicy: + maximumRedeliveries: 2 + redeliveryDelay: 1000 + steps: + - log: + message: "Skipping ${variable.cityName} after retries: ${exception.message}" + +# Create collection and index on startup +- route: + id: init-collection + from: + uri: timer:init + parameters: + repeatCount: "1" + delay: "1000" + steps: + - log: + message: "Creating Milvus collection: cities" + - doTry: + steps: + - process: + ref: ragCreateCollection + - to: + uri: milvus:cities + - log: + message: "[milvus] Collection created successfully" + - process: + ref: ragCreateIndex + - to: + uri: milvus:cities + - log: + message: "[milvus] Index created successfully" + doCatch: + - exception: + - java.lang.Exception + steps: + - log: + message: "[milvus] Collection already exists or error: ${exception.message}" + +# Read city names from input file and insert them into Milvus +- route: + id: index-cities + from: + uri: "file:{{input.dir}}" + parameters: + noop: "true" + include: ".*\\.txt" + initialDelay: "3000" + steps: + - log: + message: "Processing cities from: ${header.CamelFileName}" + - convertBodyTo: + type: java.lang.String + - split: + tokenize: "\n" + steps: + - filter: + simple: "${body.trim().length()} > 0" + steps: + - to: + uri: direct:insert-city + - log: + message: "All cities indexed. Starting similarity search..." + - setBody: + expression: + constant: + expression: "Rome" + - to: + uri: direct:search-similar + +# Ask OpenAI for city data information (lat,lon,pop), then insert into Milvus +- route: + id: insert-city + from: + uri: direct:insert-city + steps: + - setVariable: + name: cityName + expression: + simple: + expression: "${body.trim()}" + - setBody: + expression: + simple: + expression: "Reply with exactly one line of 4 semicolon-separated values for ${variable.cityName}: country;latitude;longitude;population_in_millions. All numeric values must be plain float numbers only, no units or text. No headers, no extra text. Example for Tokyo: Japan;35.68;139.69;13.96" + - to: + uri: openai:chat-completion + - log: + message: "[openai] City data for ${variable.cityName}: ${body}" + - setVariable: + name: cityCountry + expression: + simple: + expression: "${body.trim().split(';')[0].trim()}" + - setVariable: + name: cityLatitude + expression: + simple: + expression: "${body.trim().split(';')[1].trim()}" + - setVariable: + name: cityLongitude + expression: + simple: + expression: "${body.trim().split(';')[2].trim()}" + - transform: + expression: + simple: + expression: "${body.trim().replaceFirst('^[^;]*;', '')}" + - process: + ref: vectorUtils + - process: + ref: ragInsert + - to: + uri: milvus:cities + - log: + message: "[milvus] Inserted ${variable.cityName} into Milvus" + +# Search for closest cities to a given city +- route: + id: search-closest + from: + uri: direct:search-closest + steps: + - setVariable: + name: searchCity + expression: + simple: + expression: "${body}" + - log: + message: "Searching for three closest cities to: ${variable.searchCity} " + - setBody: + expression: + simple: + expression: "Reply with exactly one line of 3 semicolon-separated numeric values for ${variable.searchCity}: latitude;longitude;population_in_millions. All values must be plain float numbers only, no units or text. No headers, no extra text. Example: 35.68;139.69;13.96" + - to: + uri: openai:chat-completion + - log: + message: "[openai] Search vector for ${variable.searchCity}: ${body}" + - process: + ref: vectorUtils + - process: + ref: ragSearch + - to: + uri: milvus:cities + - bean: + ref: ragResultExtractor + method: extract + - log: + message: "[milvus] Closest cities to ${variable.searchCity}: ${body}." + - setBody: + expression: + simple: + expression: "Milvus vector search found these cities closest to ${variable.searchCity}: ${body}. Excluding ${variable.searchCity} itself, what is the approximate distance in km from ${variable.searchCity} to each? Reply in a single line, comma-separated. Example format: Milan 480km, Zurich 685km, Vienna 766km" + - to: + uri: openai:chat-completion + - log: + message: "[openai] Answer: ${body}" \ No newline at end of file diff --git a/pom.xml b/pom.xml index 756f8926..e6f9bdb2 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,7 @@ platform-http platform-http-proxy pojo + milvus rabbitmq reactive-streams resilience4j