This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch hungry-quark in repository https://gitbox.apache.org/repos/asf/camel.git
commit 14b82dbf162207c5918c2019db84be777f156444 Author: Guillaume Nodet <[email protected]> AuthorDate: Mon Mar 23 21:52:50 2026 +0100 CAMEL-21540: Add PGVector component for PostgreSQL vector database - New camel-pgvector component under components/camel-ai/ - Supports CREATE_TABLE, DROP_TABLE, UPSERT, DELETE, SIMILARITY_SEARCH actions - Uses PostgreSQL pgvector extension via JDBC with com.pgvector library - Supports cosine, euclidean, and inner product distance types - LangChain4j data type transformers: pgvector:embeddings and pgvector:rag - Integration tests with testcontainers pgvector image - LangChain4j embeddings integration test with AllMiniLmL6V2 model Co-Authored-By: Claude Opus 4.6 <[email protected]> --- catalog/camel-allcomponents/pom.xml | 5 + .../camel-ai/camel-langchain4j-embeddings/pom.xml | 11 + ...Chain4jEmbeddingsComponentPgVectorTargetIT.java | 141 ++++++++++++ components/camel-ai/camel-pgvector/pom.xml | 90 ++++++++ .../pgvector/PgVectorComponentConfigurer.java | 87 ++++++++ .../pgvector/PgVectorConfigurationConfigurer.java | 60 ++++++ .../pgvector/PgVectorEndpointConfigurer.java | 71 ++++++ .../pgvector/PgVectorEndpointUriFactory.java | 74 +++++++ .../apache/camel/component/pgvector/pgvector.json | 48 +++++ .../services/org/apache/camel/component.properties | 7 + .../services/org/apache/camel/component/pgvector | 2 + ....camel.component.pgvector.PgVectorConfiguration | 2 + .../org/apache/camel/configurer/pgvector-component | 2 + .../org/apache/camel/configurer/pgvector-endpoint | 2 + .../org/apache/camel/transformer.properties | 7 + .../apache/camel/transformer/pgvector-embeddings | 2 + .../camel/transformer/pgvector-embeddings.json | 14 ++ .../org/apache/camel/transformer/pgvector-rag | 2 + .../org/apache/camel/transformer/pgvector-rag.json | 14 ++ .../org/apache/camel/urifactory/pgvector-endpoint | 2 + .../src/main/docs/pgvector-component.adoc | 60 ++++++ .../apache/camel/component/pgvector/PgVector.java | 24 +++ .../camel/component/pgvector/PgVectorAction.java | 25 +++ .../component/pgvector/PgVectorComponent.java | 68 ++++++ .../component/pgvector/PgVectorConfiguration.java | 100 +++++++++ .../camel/component/pgvector/PgVectorEndpoint.java | 97 +++++++++ .../camel/component/pgvector/PgVectorHeaders.java | 44 ++++ .../camel/component/pgvector/PgVectorProducer.java | 238 +++++++++++++++++++++ .../PgVectorEmbeddingsDataTypeTransformer.java | 76 +++++++ ...VectorReverseEmbeddingsDataTypeTransformer.java | 46 ++++ .../component/pgvector/PgVectorComponentIT.java | 191 +++++++++++++++++ components/camel-ai/pom.xml | 1 + parent/pom.xml | 6 + .../apache/camel/maven/packaging/MojoHelper.java | 2 +- 34 files changed, 1620 insertions(+), 1 deletion(-) diff --git a/catalog/camel-allcomponents/pom.xml b/catalog/camel-allcomponents/pom.xml index 7f8782e412b6..65d277816e81 100644 --- a/catalog/camel-allcomponents/pom.xml +++ b/catalog/camel-allcomponents/pom.xml @@ -1682,6 +1682,11 @@ <artifactId>camel-pgevent</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-pgvector</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-pinecone</artifactId> diff --git a/components/camel-ai/camel-langchain4j-embeddings/pom.xml b/components/camel-ai/camel-langchain4j-embeddings/pom.xml index 9ea004fe94ca..9993ff4d8825 100644 --- a/components/camel-ai/camel-langchain4j-embeddings/pom.xml +++ b/components/camel-ai/camel-langchain4j-embeddings/pom.xml @@ -87,6 +87,11 @@ <artifactId>camel-milvus</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-pgvector</artifactId> + <scope>test</scope> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-pinecone</artifactId> @@ -156,6 +161,12 @@ <version>${project.version}</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-test-infra-postgres</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-test-infra-weaviate</artifactId> diff --git a/components/camel-ai/camel-langchain4j-embeddings/src/test/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsComponentPgVectorTargetIT.java b/components/camel-ai/camel-langchain4j-embeddings/src/test/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsComponentPgVectorTargetIT.java new file mode 100644 index 000000000000..cb1f75b825ef --- /dev/null +++ b/components/camel-ai/camel-langchain4j-embeddings/src/test/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsComponentPgVectorTargetIT.java @@ -0,0 +1,141 @@ +/* + * 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.component.langchain4j.embeddings; + +import java.util.List; + +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.pgvector.PgVector; +import org.apache.camel.component.pgvector.PgVectorAction; +import org.apache.camel.component.pgvector.PgVectorComponent; +import org.apache.camel.component.pgvector.PgVectorHeaders; +import org.apache.camel.spi.DataType; +import org.apache.camel.test.infra.postgres.services.PostgresService; +import org.apache.camel.test.infra.postgres.services.PostgresVectorServiceFactory; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.postgresql.ds.PGSimpleDataSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class LangChain4jEmbeddingsComponentPgVectorTargetIT extends CamelTestSupport { + + public static final String PGVECTOR_URI = "pgvector:embeddings"; + + @RegisterExtension + static PostgresService POSTGRES = PostgresVectorServiceFactory.createService(); + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext context = super.createCamelContext(); + + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(POSTGRES.jdbcUrl()); + dataSource.setUser(POSTGRES.userName()); + dataSource.setPassword(POSTGRES.password()); + + PgVectorComponent component = context.getComponent(PgVector.SCHEME, PgVectorComponent.class); + component.getConfiguration().setDataSource(dataSource); + context.getRegistry().bind("embedding-model", new AllMiniLmL6V2EmbeddingModel()); + + return context; + } + + @Test + @Order(1) + public void createTable() { + Exchange result = fluentTemplate.to(PGVECTOR_URI) + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.CREATE_TABLE) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + } + + @Test + @Order(2) + public void upsertEmbedding() { + Exchange result = fluentTemplate.to("direct:in") + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.UPSERT) + .withHeader(PgVectorHeaders.RECORD_ID, "embed-1") + .withBody("The sky is blue") + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(String.class)).isEqualTo("embed-1"); + } + + @Test + @Order(3) + public void upsertSecondEmbedding() { + Exchange result = fluentTemplate.to("direct:in") + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.UPSERT) + .withHeader(PgVectorHeaders.RECORD_ID, "embed-2") + .withBody("The ocean is deep") + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + } + + @Test + @Order(4) + @SuppressWarnings("unchecked") + public void queryEmbeddings() { + Exchange result = fluentTemplate.to("direct:query") + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.SIMILARITY_SEARCH) + .withBody("blue sky") + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + + List<String> results = result.getMessage().getBody(List.class); + assertThat(results).isNotEmpty(); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + from("direct:in") + .to("langchain4j-embeddings:test") + .transformDataType(new DataType("pgvector:embeddings")) + .to(PGVECTOR_URI); + + from("direct:query") + .to("langchain4j-embeddings:test") + .transformDataType(new DataType("pgvector:embeddings")) + .setHeader(PgVectorHeaders.ACTION, constant(PgVectorAction.SIMILARITY_SEARCH)) + .to(PGVECTOR_URI) + .transformDataType(new DataType("pgvector:rag")); + } + }; + } +} diff --git a/components/camel-ai/camel-pgvector/pom.xml b/components/camel-ai/camel-pgvector/pom.xml new file mode 100644 index 000000000000..b02e9208d361 --- /dev/null +++ b/components/camel-ai/camel-pgvector/pom.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<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/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>camel-ai-parent</artifactId> + <groupId>org.apache.camel</groupId> + <version>4.19.0-SNAPSHOT</version> + </parent> + + <artifactId>camel-pgvector</artifactId> + <packaging>jar</packaging> + <name>Camel :: AI :: PGVector</name> + <description>Camel PGVector support</description> + + <properties> + <camel.surefire.parallel>true</camel.surefire.parallel> + <camel.surefire.parallel.factor>4</camel.surefire.parallel.factor> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-support</artifactId> + </dependency> + + <dependency> + <groupId>dev.langchain4j</groupId> + <artifactId>langchain4j-core</artifactId> + <version>${langchain4j-version}</version> + </dependency> + + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <version>${pgjdbc-driver-version}</version> + </dependency> + + <dependency> + <groupId>com.pgvector</groupId> + <artifactId>pgvector</artifactId> + <version>${pgvector-version}</version> + </dependency> + + <!-- test infra --> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-test-infra-postgres</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + + <!-- test dependencies --> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-test-junit6</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + +</project> diff --git a/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorComponentConfigurer.java b/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorComponentConfigurer.java new file mode 100644 index 000000000000..c9b6b44f7636 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorComponentConfigurer.java @@ -0,0 +1,87 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.component.pgvector; + +import javax.annotation.processing.Generated; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.ExtendedPropertyConfigurerGetter; +import org.apache.camel.spi.PropertyConfigurerGetter; +import org.apache.camel.spi.ConfigurerStrategy; +import org.apache.camel.spi.GeneratedPropertyConfigurer; +import org.apache.camel.util.CaseInsensitiveMap; +import org.apache.camel.support.component.PropertyConfigurerSupport; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.EndpointSchemaGeneratorMojo") +@SuppressWarnings("unchecked") +public class PgVectorComponentConfigurer extends PropertyConfigurerSupport implements GeneratedPropertyConfigurer, PropertyConfigurerGetter { + + private org.apache.camel.component.pgvector.PgVectorConfiguration getOrCreateConfiguration(PgVectorComponent target) { + if (target.getConfiguration() == null) { + target.setConfiguration(new org.apache.camel.component.pgvector.PgVectorConfiguration()); + } + return target.getConfiguration(); + } + + @Override + public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) { + PgVectorComponent target = (PgVectorComponent) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "autowiredenabled": + case "autowiredEnabled": target.setAutowiredEnabled(property(camelContext, boolean.class, value)); return true; + case "configuration": target.setConfiguration(property(camelContext, org.apache.camel.component.pgvector.PgVectorConfiguration.class, value)); return true; + case "datasource": + case "dataSource": getOrCreateConfiguration(target).setDataSource(property(camelContext, javax.sql.DataSource.class, value)); return true; + case "dimension": getOrCreateConfiguration(target).setDimension(property(camelContext, int.class, value)); return true; + case "distancetype": + case "distanceType": getOrCreateConfiguration(target).setDistanceType(property(camelContext, java.lang.String.class, value)); return true; + case "lazystartproducer": + case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true; + default: return false; + } + } + + @Override + public String[] getAutowiredNames() { + return new String[]{"dataSource"}; + } + + @Override + public Class<?> getOptionType(String name, boolean ignoreCase) { + switch (ignoreCase ? name.toLowerCase() : name) { + case "autowiredenabled": + case "autowiredEnabled": return boolean.class; + case "configuration": return org.apache.camel.component.pgvector.PgVectorConfiguration.class; + case "datasource": + case "dataSource": return javax.sql.DataSource.class; + case "dimension": return int.class; + case "distancetype": + case "distanceType": return java.lang.String.class; + case "lazystartproducer": + case "lazyStartProducer": return boolean.class; + default: return null; + } + } + + @Override + public Object getOptionValue(Object obj, String name, boolean ignoreCase) { + PgVectorComponent target = (PgVectorComponent) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "autowiredenabled": + case "autowiredEnabled": return target.isAutowiredEnabled(); + case "configuration": return target.getConfiguration(); + case "datasource": + case "dataSource": return getOrCreateConfiguration(target).getDataSource(); + case "dimension": return getOrCreateConfiguration(target).getDimension(); + case "distancetype": + case "distanceType": return getOrCreateConfiguration(target).getDistanceType(); + case "lazystartproducer": + case "lazyStartProducer": return target.isLazyStartProducer(); + default: return null; + } + } +} + diff --git a/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorConfigurationConfigurer.java b/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorConfigurationConfigurer.java new file mode 100644 index 000000000000..8bedda0cef91 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorConfigurationConfigurer.java @@ -0,0 +1,60 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.component.pgvector; + +import javax.annotation.processing.Generated; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.ExtendedPropertyConfigurerGetter; +import org.apache.camel.spi.PropertyConfigurerGetter; +import org.apache.camel.spi.ConfigurerStrategy; +import org.apache.camel.spi.GeneratedPropertyConfigurer; +import org.apache.camel.util.CaseInsensitiveMap; +import org.apache.camel.component.pgvector.PgVectorConfiguration; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo") +@SuppressWarnings("unchecked") +public class PgVectorConfigurationConfigurer extends org.apache.camel.support.component.PropertyConfigurerSupport implements GeneratedPropertyConfigurer, PropertyConfigurerGetter { + + @Override + public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) { + org.apache.camel.component.pgvector.PgVectorConfiguration target = (org.apache.camel.component.pgvector.PgVectorConfiguration) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "datasource": + case "dataSource": target.setDataSource(property(camelContext, javax.sql.DataSource.class, value)); return true; + case "dimension": target.setDimension(property(camelContext, int.class, value)); return true; + case "distancetype": + case "distanceType": target.setDistanceType(property(camelContext, java.lang.String.class, value)); return true; + default: return false; + } + } + + @Override + public Class<?> getOptionType(String name, boolean ignoreCase) { + switch (ignoreCase ? name.toLowerCase() : name) { + case "datasource": + case "dataSource": return javax.sql.DataSource.class; + case "dimension": return int.class; + case "distancetype": + case "distanceType": return java.lang.String.class; + default: return null; + } + } + + @Override + public Object getOptionValue(Object obj, String name, boolean ignoreCase) { + org.apache.camel.component.pgvector.PgVectorConfiguration target = (org.apache.camel.component.pgvector.PgVectorConfiguration) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "datasource": + case "dataSource": return target.getDataSource(); + case "dimension": return target.getDimension(); + case "distancetype": + case "distanceType": return target.getDistanceType(); + default: return null; + } + } +} + diff --git a/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorEndpointConfigurer.java b/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorEndpointConfigurer.java new file mode 100644 index 000000000000..faa4cd792493 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorEndpointConfigurer.java @@ -0,0 +1,71 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.component.pgvector; + +import javax.annotation.processing.Generated; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.ExtendedPropertyConfigurerGetter; +import org.apache.camel.spi.PropertyConfigurerGetter; +import org.apache.camel.spi.ConfigurerStrategy; +import org.apache.camel.spi.GeneratedPropertyConfigurer; +import org.apache.camel.util.CaseInsensitiveMap; +import org.apache.camel.support.component.PropertyConfigurerSupport; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.EndpointSchemaGeneratorMojo") +@SuppressWarnings("unchecked") +public class PgVectorEndpointConfigurer extends PropertyConfigurerSupport implements GeneratedPropertyConfigurer, PropertyConfigurerGetter { + + @Override + public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) { + PgVectorEndpoint target = (PgVectorEndpoint) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "datasource": + case "dataSource": target.getConfiguration().setDataSource(property(camelContext, javax.sql.DataSource.class, value)); return true; + case "dimension": target.getConfiguration().setDimension(property(camelContext, int.class, value)); return true; + case "distancetype": + case "distanceType": target.getConfiguration().setDistanceType(property(camelContext, java.lang.String.class, value)); return true; + case "lazystartproducer": + case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true; + default: return false; + } + } + + @Override + public String[] getAutowiredNames() { + return new String[]{"dataSource"}; + } + + @Override + public Class<?> getOptionType(String name, boolean ignoreCase) { + switch (ignoreCase ? name.toLowerCase() : name) { + case "datasource": + case "dataSource": return javax.sql.DataSource.class; + case "dimension": return int.class; + case "distancetype": + case "distanceType": return java.lang.String.class; + case "lazystartproducer": + case "lazyStartProducer": return boolean.class; + default: return null; + } + } + + @Override + public Object getOptionValue(Object obj, String name, boolean ignoreCase) { + PgVectorEndpoint target = (PgVectorEndpoint) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "datasource": + case "dataSource": return target.getConfiguration().getDataSource(); + case "dimension": return target.getConfiguration().getDimension(); + case "distancetype": + case "distanceType": return target.getConfiguration().getDistanceType(); + case "lazystartproducer": + case "lazyStartProducer": return target.isLazyStartProducer(); + default: return null; + } + } +} + diff --git a/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorEndpointUriFactory.java b/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorEndpointUriFactory.java new file mode 100644 index 000000000000..c41db64d0fcb --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/java/org/apache/camel/component/pgvector/PgVectorEndpointUriFactory.java @@ -0,0 +1,74 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.component.pgvector; + +import javax.annotation.processing.Generated; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.camel.spi.EndpointUriFactory; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.GenerateEndpointUriFactoryMojo") +public class PgVectorEndpointUriFactory extends org.apache.camel.support.component.EndpointUriFactorySupport implements EndpointUriFactory { + + private static final String BASE = ":collection"; + + private static final Set<String> PROPERTY_NAMES; + private static final Set<String> SECRET_PROPERTY_NAMES; + private static final Map<String, String> MULTI_VALUE_PREFIXES; + static { + Set<String> props = new HashSet<>(5); + props.add("collection"); + props.add("dataSource"); + props.add("dimension"); + props.add("distanceType"); + props.add("lazyStartProducer"); + PROPERTY_NAMES = Collections.unmodifiableSet(props); + SECRET_PROPERTY_NAMES = Collections.emptySet(); + MULTI_VALUE_PREFIXES = Collections.emptyMap(); + } + + @Override + public boolean isEnabled(String scheme) { + return "pgvector".equals(scheme); + } + + @Override + public String buildUri(String scheme, Map<String, Object> properties, boolean encode) throws URISyntaxException { + String syntax = scheme + BASE; + String uri = syntax; + + Map<String, Object> copy = new HashMap<>(properties); + + uri = buildPathParameter(syntax, uri, "collection", null, true, copy); + uri = buildQueryParameters(uri, copy, encode); + return uri; + } + + @Override + public Set<String> propertyNames() { + return PROPERTY_NAMES; + } + + @Override + public Set<String> secretPropertyNames() { + return SECRET_PROPERTY_NAMES; + } + + @Override + public Map<String, String> multiValuePrefixes() { + return MULTI_VALUE_PREFIXES; + } + + @Override + public boolean isLenientProperties() { + return false; + } +} + diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/org/apache/camel/component/pgvector/pgvector.json b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/org/apache/camel/component/pgvector/pgvector.json new file mode 100644 index 000000000000..69d46599f91e --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/org/apache/camel/component/pgvector/pgvector.json @@ -0,0 +1,48 @@ +{ + "component": { + "kind": "component", + "name": "pgvector", + "title": "PGVector", + "description": "Perform operations on the PostgreSQL pgvector Vector Database.", + "deprecated": false, + "firstVersion": "4.19.0", + "label": "database,ai", + "javaType": "org.apache.camel.component.pgvector.PgVectorComponent", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-pgvector", + "version": "4.19.0-SNAPSHOT", + "scheme": "pgvector", + "extendsScheme": "", + "syntax": "pgvector:collection", + "async": false, + "api": false, + "consumerOnly": false, + "producerOnly": true, + "lenientProperties": false, + "browsable": false, + "remote": true + }, + "componentProperties": { + "configuration": { "index": 0, "kind": "property", "displayName": "Configuration", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.component.pgvector.PgVectorConfiguration", "deprecated": false, "autowired": false, "secret": false, "description": "The configuration;" }, + "dataSource": { "index": 1, "kind": "property", "displayName": "Data Source", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "javax.sql.DataSource", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.pgvector.PgVectorConfiguration", "configurationField": "configuration", "description": "The DataSource to use for connecting to the PostgreSQL database with pgvector extension." }, + "dimension": { "index": 2, "kind": "property", "displayName": "Dimension", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 384, "configurationClass": "org.apache.camel.component.pgvector.PgVectorConfiguration", "configurationField": "configuration", "description": "The dimension of the vectors to store." }, + "distanceType": { "index": 3, "kind": "property", "displayName": "Distance Type", "group": "producer", "label": "producer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "cosine", "configurationClass": "org.apache.camel.component.pgvector.PgVectorConfiguration", "configurationField": "configuration", "description": "The distance type to use for similarity search." }, + "lazyStartProducer": { "index": 4, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...] + "autowiredEnabled": { "index": 5, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...] + }, + "headers": { + "CamelPgVectorAction": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "enum": [ "CREATE_TABLE", "DROP_TABLE", "UPSERT", "DELETE", "SIMILARITY_SEARCH" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The action to be performed.", "constantName": "org.apache.camel.component.pgvector.PgVectorHeaders#ACTION" }, + "CamelPgVectorRecordId": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The id of the vector record.", "constantName": "org.apache.camel.component.pgvector.PgVectorHeaders#RECORD_ID" }, + "CamelPgVectorQueryTopK": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "3", "description": "The maximum number of results to return for similarity search.", "constantName": "org.apache.camel.component.pgvector.PgVectorHeaders#QUERY_TOP_K" }, + "CamelPgVectorTextContent": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The text content to store alongside the vector embedding.", "constantName": "org.apache.camel.component.pgvector.PgVectorHeaders#TEXT_CONTENT" }, + "CamelPgVectorMetadata": { "index": 4, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The metadata associated with the vector record, stored as JSON.", "constantName": "org.apache.camel.component.pgvector.PgVectorHeaders#METADATA" } + }, + "properties": { + "collection": { "index": 0, "kind": "path", "displayName": "Collection", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The collection (table) name" }, + "dataSource": { "index": 1, "kind": "parameter", "displayName": "Data Source", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "javax.sql.DataSource", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.pgvector.PgVectorConfiguration", "configurationField": "configuration", "description": "The DataSource to use for connecting to the PostgreSQL database with pgvector extens [...] + "dimension": { "index": 2, "kind": "parameter", "displayName": "Dimension", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 384, "configurationClass": "org.apache.camel.component.pgvector.PgVectorConfiguration", "configurationField": "configuration", "description": "The dimension of the vectors to store." }, + "distanceType": { "index": 3, "kind": "parameter", "displayName": "Distance Type", "group": "producer", "label": "producer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "cosine", "configurationClass": "org.apache.camel.component.pgvector.PgVectorConfiguration", "configurationField": "configuration", "description": "The distance type to use for similarity search." }, + "lazyStartProducer": { "index": 4, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...] + } +} diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/component.properties b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/component.properties new file mode 100644 index 000000000000..cad7e0ed6691 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/component.properties @@ -0,0 +1,7 @@ +# Generated by camel build tools - do NOT edit this file! +components=pgvector +groupId=org.apache.camel +artifactId=camel-pgvector +version=4.19.0-SNAPSHOT +projectName=Camel :: AI :: PGVector +projectDescription=Camel PGVector support diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/component/pgvector b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/component/pgvector new file mode 100644 index 000000000000..3945951638c5 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/component/pgvector @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.pgvector.PgVectorComponent diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.pgvector.PgVectorConfiguration b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.pgvector.PgVectorConfiguration new file mode 100644 index 000000000000..cf12de7c025d --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.pgvector.PgVectorConfiguration @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.pgvector.PgVectorConfigurationConfigurer diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/configurer/pgvector-component b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/configurer/pgvector-component new file mode 100644 index 000000000000..fd431f002970 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/configurer/pgvector-component @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.pgvector.PgVectorComponentConfigurer diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/configurer/pgvector-endpoint b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/configurer/pgvector-endpoint new file mode 100644 index 000000000000..483f077e2040 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/configurer/pgvector-endpoint @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.pgvector.PgVectorEndpointConfigurer diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer.properties b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer.properties new file mode 100644 index 000000000000..27a4f45b1c78 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer.properties @@ -0,0 +1,7 @@ +# Generated by camel build tools - do NOT edit this file! +transformers=pgvector:embeddings pgvector:rag +groupId=org.apache.camel +artifactId=camel-pgvector +version=4.19.0-SNAPSHOT +projectName=Camel :: AI :: PGVector +projectDescription=Camel PGVector support diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-embeddings b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-embeddings new file mode 100644 index 000000000000..4b6963457fb0 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-embeddings @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.pgvector.transform.PgVectorEmbeddingsDataTypeTransformer diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-embeddings.json b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-embeddings.json new file mode 100644 index 000000000000..2a5efd26fd33 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-embeddings.json @@ -0,0 +1,14 @@ +{ + "transformer": { + "kind": "transformer", + "name": "pgvector:embeddings", + "title": "Pgvector (Embeddings)", + "description": "Prepares the message to become an object writable by PgVector component", + "deprecated": false, + "javaType": "org.apache.camel.component.pgvector.transform.PgVectorEmbeddingsDataTypeTransformer", + "groupId": "org.apache.camel", + "artifactId": "camel-pgvector", + "version": "4.19.0-SNAPSHOT" + } +} + diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-rag b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-rag new file mode 100644 index 000000000000..8ddcd235af2a --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-rag @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.pgvector.transform.PgVectorReverseEmbeddingsDataTypeTransformer diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-rag.json b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-rag.json new file mode 100644 index 000000000000..bde0480f5720 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/transformer/pgvector-rag.json @@ -0,0 +1,14 @@ +{ + "transformer": { + "kind": "transformer", + "name": "pgvector:rag", + "title": "Pgvector (Rag)", + "description": "Prepares the PgVector similarity search results to become a List of String for LangChain4j RAG", + "deprecated": false, + "javaType": "org.apache.camel.component.pgvector.transform.PgVectorReverseEmbeddingsDataTypeTransformer", + "groupId": "org.apache.camel", + "artifactId": "camel-pgvector", + "version": "4.19.0-SNAPSHOT" + } +} + diff --git a/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/urifactory/pgvector-endpoint b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/urifactory/pgvector-endpoint new file mode 100644 index 000000000000..a9f8e7a6c555 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/generated/resources/META-INF/services/org/apache/camel/urifactory/pgvector-endpoint @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.pgvector.PgVectorEndpointUriFactory diff --git a/components/camel-ai/camel-pgvector/src/main/docs/pgvector-component.adoc b/components/camel-ai/camel-pgvector/src/main/docs/pgvector-component.adoc new file mode 100644 index 000000000000..6d2dca26aa99 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/docs/pgvector-component.adoc @@ -0,0 +1,60 @@ += PGVector Component +:doctitle: PGVector +:shortname: pgvector +:artifactid: camel-pgvector +:description: Perform operations on the PostgreSQL pgvector Vector Database. +:since: 4.19 +:supportlevel: Preview +:tabs-sync-option: +:component-header: Only producer is supported +//Manually maintained attributes +:group: AI +:camel-spring-boot-name: pgvector + +*Since Camel {since}* + +*{component-header}* + +The PGVector Component provides support for interacting with https://github.com/pgvector/pgvector[pgvector], +the open-source vector similarity search extension for PostgreSQL. + +== URI format + +---- +pgvector:collection[?options] +---- + +Where *collection* represents the table name used to store vectors in the PostgreSQL database. + +== Configuring the DataSource + +A `javax.sql.DataSource` must be provided. It is recommended to use a connection pooling DataSource +(such as HikariCP) for production deployments. The DataSource can be set on the component or endpoint +configuration, or autowired from the registry. + +== Actions + +The following actions are supported via the `CamelPgVectorAction` header: + +- `CREATE_TABLE` - Creates the pgvector extension and a table with columns: id, text_content, metadata, embedding +- `DROP_TABLE` - Drops the table +- `UPSERT` - Inserts or updates a vector record. The body must be a `List<Float>`. Set `CamelPgVectorRecordId` for the ID (auto-generated UUID if not set), `CamelPgVectorTextContent` for text, and `CamelPgVectorMetadata` for metadata +- `DELETE` - Deletes a record by `CamelPgVectorRecordId` +- `SIMILARITY_SEARCH` - Searches for similar vectors. The body must be a `List<Float>` query vector. Set `CamelPgVectorQueryTopK` for max results (default 3). Returns a `List<Map<String, Object>>` with keys: id, text_content, metadata, distance + +== LangChain4j Integration + +This component provides data type transformers for LangChain4j integration: + +- `pgvector:embeddings` - Transforms LangChain4j embedding output into a format suitable for the PGVector UPSERT action +- `pgvector:rag` - Transforms similarity search results into a `List<String>` for RAG pipelines + + +// component options: START +include::partial$component-configure-options.adoc[] +include::partial$component-endpoint-options.adoc[] +include::partial$component-endpoint-headers.adoc[] +// component options: END + + +include::spring-boot:partial$starter.adoc[] diff --git a/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVector.java b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVector.java new file mode 100644 index 000000000000..0024a9375b6a --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVector.java @@ -0,0 +1,24 @@ +/* + * 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.component.pgvector; + +public class PgVector { + public static final String SCHEME = "pgvector"; + + private PgVector() { + } +} diff --git a/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorAction.java b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorAction.java new file mode 100644 index 000000000000..1a647a4bc7a1 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorAction.java @@ -0,0 +1,25 @@ +/* + * 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.component.pgvector; + +public enum PgVectorAction { + CREATE_TABLE, + DROP_TABLE, + UPSERT, + DELETE, + SIMILARITY_SEARCH +} diff --git a/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorComponent.java b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorComponent.java new file mode 100644 index 000000000000..363050f9d4ca --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorComponent.java @@ -0,0 +1,68 @@ +/* + * 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.component.pgvector; + +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.Endpoint; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.annotations.Component; +import org.apache.camel.support.DefaultComponent; + +@Component(PgVector.SCHEME) +public class PgVectorComponent extends DefaultComponent { + + @Metadata + private PgVectorConfiguration configuration; + + public PgVectorComponent() { + this(null); + } + + public PgVectorComponent(CamelContext context) { + super(context); + + this.configuration = new PgVectorConfiguration(); + } + + public PgVectorConfiguration getConfiguration() { + return configuration; + } + + /** + * The configuration; + */ + public void setConfiguration(PgVectorConfiguration configuration) { + this.configuration = configuration; + } + + @Override + protected Endpoint createEndpoint( + String uri, + String remaining, + Map<String, Object> parameters) + throws Exception { + + PgVectorConfiguration configuration = this.configuration.copy(); + + PgVectorEndpoint endpoint = new PgVectorEndpoint(uri, this, remaining, configuration); + setProperties(endpoint, parameters); + + return endpoint; + } +} diff --git a/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorConfiguration.java b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorConfiguration.java new file mode 100644 index 000000000000..e61e1849f097 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorConfiguration.java @@ -0,0 +1,100 @@ +/* + * 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.component.pgvector; + +import java.util.Set; + +import javax.sql.DataSource; + +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.spi.Configurer; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.UriParam; +import org.apache.camel.spi.UriParams; + +@Configurer +@UriParams +public class PgVectorConfiguration implements Cloneable { + + private static final Set<String> VALID_DISTANCE_TYPES = Set.of("cosine", "euclidean", "innerProduct"); + + @Metadata(autowired = true, + description = "The DataSource to use for connecting to the PostgreSQL database with pgvector extension.") + @UriParam + private DataSource dataSource; + + @Metadata(label = "producer", + description = "The dimension of the vectors to store.") + @UriParam(defaultValue = "384") + private int dimension = 384; + + @Metadata(label = "producer", + description = "The distance type to use for similarity search.", + enums = "cosine,euclidean,innerProduct") + @UriParam(defaultValue = "cosine") + private String distanceType = "cosine"; + + public DataSource getDataSource() { + return dataSource; + } + + /** + * The DataSource to use for connecting to the PostgreSQL database with pgvector extension. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + public int getDimension() { + return dimension; + } + + /** + * The dimension of the vectors to store. + */ + public void setDimension(int dimension) { + this.dimension = dimension; + } + + public String getDistanceType() { + return distanceType; + } + + /** + * The distance type to use for similarity search. Supported values: cosine, euclidean, innerProduct. + */ + public void setDistanceType(String distanceType) { + if (!VALID_DISTANCE_TYPES.contains(distanceType)) { + throw new IllegalArgumentException( + "Invalid distanceType: " + distanceType + ". Valid values are: " + VALID_DISTANCE_TYPES); + } + this.distanceType = distanceType; + } + + // ************************ + // + // Clone + // + // ************************ + public PgVectorConfiguration copy() { + try { + return (PgVectorConfiguration) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeCamelException(e); + } + } +} diff --git a/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorEndpoint.java b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorEndpoint.java new file mode 100644 index 000000000000..a22f811cd0cb --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorEndpoint.java @@ -0,0 +1,97 @@ +/* + * 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.component.pgvector; + +import javax.sql.DataSource; + +import org.apache.camel.Category; +import org.apache.camel.Component; +import org.apache.camel.Consumer; +import org.apache.camel.Processor; +import org.apache.camel.Producer; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.UriEndpoint; +import org.apache.camel.spi.UriParam; +import org.apache.camel.spi.UriPath; +import org.apache.camel.support.DefaultEndpoint; + +/** + * Perform operations on the PostgreSQL pgvector Vector Database. + */ +@UriEndpoint( + firstVersion = "4.19.0", + scheme = PgVector.SCHEME, + title = "PGVector", + syntax = "pgvector:collection", + producerOnly = true, + category = { + Category.DATABASE, + Category.AI + }, + headersClass = PgVectorHeaders.class) +public class PgVectorEndpoint extends DefaultEndpoint { + + @Metadata(required = true) + @UriPath(description = "The collection (table) name") + private final String collection; + + @UriParam + private PgVectorConfiguration configuration; + + public PgVectorEndpoint( + String endpointUri, + Component component, + String collection, + PgVectorConfiguration configuration) { + + super(endpointUri, component); + + this.collection = collection; + this.configuration = configuration; + } + + public PgVectorConfiguration getConfiguration() { + return configuration; + } + + public String getCollection() { + return collection; + } + + public DataSource getDataSource() { + return this.configuration.getDataSource(); + } + + @Override + public Producer createProducer() throws Exception { + return new PgVectorProducer(this); + } + + @Override + public Consumer createConsumer(Processor processor) throws Exception { + throw new UnsupportedOperationException("Consumer is not implemented for this component"); + } + + @Override + public void doStart() throws Exception { + super.doStart(); + + if (configuration.getDataSource() == null) { + throw new IllegalArgumentException("DataSource must be configured on the pgvector component or endpoint"); + } + } +} diff --git a/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorHeaders.java b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorHeaders.java new file mode 100644 index 000000000000..eb80eec3fc07 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorHeaders.java @@ -0,0 +1,44 @@ +/* + * 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.component.pgvector; + +import org.apache.camel.spi.Metadata; + +public class PgVectorHeaders { + + @Metadata(description = "The action to be performed.", + javaType = "String", + enums = "CREATE_TABLE,DROP_TABLE,UPSERT,DELETE,SIMILARITY_SEARCH") + public static final String ACTION = "CamelPgVectorAction"; + + @Metadata(description = "The id of the vector record.", + javaType = "String") + public static final String RECORD_ID = "CamelPgVectorRecordId"; + + @Metadata(description = "The maximum number of results to return for similarity search.", + javaType = "Integer", defaultValue = "3") + public static final String QUERY_TOP_K = "CamelPgVectorQueryTopK"; + + @Metadata(description = "The text content to store alongside the vector embedding.", + javaType = "String") + public static final String TEXT_CONTENT = "CamelPgVectorTextContent"; + + @Metadata(description = "The metadata associated with the vector record, stored as JSON.", + javaType = "String") + public static final String METADATA = "CamelPgVectorMetadata"; + +} diff --git a/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorProducer.java b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorProducer.java new file mode 100644 index 000000000000..02447160f59c --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/PgVectorProducer.java @@ -0,0 +1,238 @@ +/* + * 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.component.pgvector; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.pgvector.PGvector; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.NoSuchHeaderException; +import org.apache.camel.support.DefaultProducer; +import org.apache.camel.support.ExchangeHelper; + +public class PgVectorProducer extends DefaultProducer { + + public PgVectorProducer(PgVectorEndpoint endpoint) { + super(endpoint); + } + + @Override + public PgVectorEndpoint getEndpoint() { + return (PgVectorEndpoint) super.getEndpoint(); + } + + @Override + public void process(Exchange exchange) { + final Message in = exchange.getMessage(); + final PgVectorAction action = in.getHeader(PgVectorHeaders.ACTION, PgVectorAction.class); + + try { + if (action == null) { + throw new NoSuchHeaderException("The action is a required header", exchange, PgVectorHeaders.ACTION); + } + + switch (action) { + case CREATE_TABLE: + createTable(exchange); + break; + case DROP_TABLE: + dropTable(exchange); + break; + case UPSERT: + upsert(exchange); + break; + case DELETE: + delete(exchange); + break; + case SIMILARITY_SEARCH: + similaritySearch(exchange); + break; + default: + throw new UnsupportedOperationException("Unsupported action: " + action.name()); + } + } catch (Exception e) { + exchange.setException(e); + } + } + + // *************************************** + // + // Actions + // + // *************************************** + + private void createTable(Exchange exchange) throws SQLException { + String tableName = getEndpoint().getCollection(); + int dimension = getEndpoint().getConfiguration().getDimension(); + + try (Connection conn = getEndpoint().getDataSource().getConnection()) { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE EXTENSION IF NOT EXISTS vector"); + } + + String sql = String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "id VARCHAR(36) PRIMARY KEY, " + + "text_content TEXT, " + + "metadata TEXT, " + + "embedding vector(%d))", + sanitizeIdentifier(tableName), dimension); + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate(sql); + } + + exchange.getMessage().setBody(true); + } + } + + private void dropTable(Exchange exchange) throws SQLException { + String tableName = getEndpoint().getCollection(); + + try (Connection conn = getEndpoint().getDataSource().getConnection()) { + String sql = String.format("DROP TABLE IF EXISTS %s", sanitizeIdentifier(tableName)); + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate(sql); + } + + exchange.getMessage().setBody(true); + } + } + + private void upsert(Exchange exchange) throws Exception { + final Message in = exchange.getMessage(); + String tableName = getEndpoint().getCollection(); + String id = in.getHeader(PgVectorHeaders.RECORD_ID, () -> UUID.randomUUID().toString(), String.class); + + List<Float> vector = in.getMandatoryBody(List.class); + float[] vectorArray = toFloatArray(vector); + + String textContent = in.getHeader(PgVectorHeaders.TEXT_CONTENT, String.class); + String metadata = in.getHeader(PgVectorHeaders.METADATA, String.class); + + String sql = String.format( + "INSERT INTO %s (id, text_content, metadata, embedding) VALUES (?, ?, ?, ?) " + + "ON CONFLICT (id) DO UPDATE SET text_content = EXCLUDED.text_content, " + + "metadata = EXCLUDED.metadata, embedding = EXCLUDED.embedding", + sanitizeIdentifier(tableName)); + + try (Connection conn = getEndpoint().getDataSource().getConnection()) { + PGvector.addVectorType(conn); + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, id); + pstmt.setString(2, textContent); + pstmt.setString(3, metadata); + pstmt.setObject(4, new PGvector(vectorArray)); + pstmt.executeUpdate(); + } + } + + exchange.getMessage().setBody(id); + } + + private void delete(Exchange exchange) throws Exception { + String tableName = getEndpoint().getCollection(); + String id = ExchangeHelper.getMandatoryHeader(exchange, PgVectorHeaders.RECORD_ID, String.class); + + String sql = String.format("DELETE FROM %s WHERE id = ?", sanitizeIdentifier(tableName)); + + try (Connection conn = getEndpoint().getDataSource().getConnection()) { + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, id); + int deleted = pstmt.executeUpdate(); + exchange.getMessage().setBody(deleted > 0); + } + } + } + + private void similaritySearch(Exchange exchange) throws Exception { + final Message in = exchange.getMessage(); + String tableName = getEndpoint().getCollection(); + int topK = in.getHeader(PgVectorHeaders.QUERY_TOP_K, 3, Integer.class); + String distanceType = getEndpoint().getConfiguration().getDistanceType(); + + List<Float> queryVector = in.getMandatoryBody(List.class); + float[] vectorArray = toFloatArray(queryVector); + + String distanceOp = getDistanceOperator(distanceType); + String sql = String.format( + "SELECT id, text_content, metadata, embedding %s ? AS distance FROM %s ORDER BY embedding %s ? LIMIT ?", + distanceOp, sanitizeIdentifier(tableName), distanceOp); + + try (Connection conn = getEndpoint().getDataSource().getConnection()) { + PGvector.addVectorType(conn); + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + PGvector pgVector = new PGvector(vectorArray); + pstmt.setObject(1, pgVector); + pstmt.setObject(2, pgVector); + pstmt.setInt(3, topK); + + try (ResultSet rs = pstmt.executeQuery()) { + List<Map<String, Object>> results = new ArrayList<>(); + while (rs.next()) { + Map<String, Object> row = new HashMap<>(); + row.put("id", rs.getString("id")); + row.put("text_content", rs.getString("text_content")); + row.put("metadata", rs.getString("metadata")); + row.put("distance", rs.getDouble("distance")); + results.add(row); + } + exchange.getMessage().setBody(results); + } + } + } + } + + // *************************************** + // + // Helpers + // + // *************************************** + + private static float[] toFloatArray(List<Float> vector) { + float[] result = new float[vector.size()]; + for (int i = 0; i < vector.size(); i++) { + result[i] = vector.get(i); + } + return result; + } + + private static String getDistanceOperator(String distanceType) { + return switch (distanceType) { + case "euclidean" -> "<->"; + case "innerProduct" -> "<#>"; + case "cosine" -> "<=>"; + default -> throw new IllegalArgumentException("Unknown distance type: " + distanceType); + }; + } + + private static String sanitizeIdentifier(String identifier) { + if (!identifier.matches("[a-zA-Z_][a-zA-Z0-9_]*")) { + throw new IllegalArgumentException("Invalid SQL identifier: " + identifier); + } + return identifier; + } +} diff --git a/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/transform/PgVectorEmbeddingsDataTypeTransformer.java b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/transform/PgVectorEmbeddingsDataTypeTransformer.java new file mode 100644 index 000000000000..166d8c64f958 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/transform/PgVectorEmbeddingsDataTypeTransformer.java @@ -0,0 +1,76 @@ +/* + * 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.component.pgvector.transform; + +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import org.apache.camel.Message; +import org.apache.camel.ai.CamelLangchain4jAttributes; +import org.apache.camel.component.pgvector.PgVectorHeaders; +import org.apache.camel.spi.DataType; +import org.apache.camel.spi.DataTypeTransformer; +import org.apache.camel.spi.Transformer; + +/** + * Maps a LangChain4j Embeddings to a PgVector upsert message to write an embeddings vector on a PostgreSQL pgvector + * database. + */ +@DataTypeTransformer(name = "pgvector:embeddings", + description = "Prepares the message to become an object writable by PgVector component") +public class PgVectorEmbeddingsDataTypeTransformer extends Transformer { + + @Override + public void transform(Message message, DataType fromType, DataType toType) { + Embedding embedding + = message.getHeader(CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR, Embedding.class); + TextSegment text = message.getBody(TextSegment.class); + + String id = message.getHeader(PgVectorHeaders.RECORD_ID, () -> UUID.randomUUID().toString(), String.class); + message.setHeader(PgVectorHeaders.RECORD_ID, id); + + message.setBody(embedding.vectorAsList()); + + if (text != null) { + message.setHeader(PgVectorHeaders.TEXT_CONTENT, text.text()); + Metadata metadata = text.metadata(); + if (metadata != null && !metadata.toMap().isEmpty()) { + message.setHeader(PgVectorHeaders.METADATA, toJson(metadata.toMap())); + } + } + } + + private static String toJson(Map<String, Object> map) { + return "{" + map.entrySet().stream() + .map(e -> "\"" + escapeJson(String.valueOf(e.getKey())) + "\":\"" + + escapeJson(String.valueOf(e.getValue())) + "\"") + .collect(Collectors.joining(",")) + + "}"; + } + + private static String escapeJson(String value) { + return value.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } +} diff --git a/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/transform/PgVectorReverseEmbeddingsDataTypeTransformer.java b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/transform/PgVectorReverseEmbeddingsDataTypeTransformer.java new file mode 100644 index 000000000000..593b8f0f0c8a --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/main/java/org/apache/camel/component/pgvector/transform/PgVectorReverseEmbeddingsDataTypeTransformer.java @@ -0,0 +1,46 @@ +/* + * 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.component.pgvector.transform; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.camel.Message; +import org.apache.camel.spi.DataType; +import org.apache.camel.spi.DataTypeTransformer; +import org.apache.camel.spi.Transformer; + +/** + * Maps a List of PgVector similarity search results to a List of String for LangChain4j RAG. + */ +@DataTypeTransformer(name = "pgvector:rag", + description = "Prepares the PgVector similarity search results to become a List of String for LangChain4j RAG") +public class PgVectorReverseEmbeddingsDataTypeTransformer extends Transformer { + + @Override + @SuppressWarnings("unchecked") + public void transform(Message message, DataType from, DataType to) throws Exception { + List<Map<String, Object>> results = message.getBody(List.class); + + List<String> textContents = results.stream() + .map(row -> (String) row.getOrDefault("text_content", "")) + .collect(Collectors.toList()); + + message.setBody(textContents); + } +} diff --git a/components/camel-ai/camel-pgvector/src/test/java/org/apache/camel/component/pgvector/PgVectorComponentIT.java b/components/camel-ai/camel-pgvector/src/test/java/org/apache/camel/component/pgvector/PgVectorComponentIT.java new file mode 100644 index 000000000000..a157875e5fe3 --- /dev/null +++ b/components/camel-ai/camel-pgvector/src/test/java/org/apache/camel/component/pgvector/PgVectorComponentIT.java @@ -0,0 +1,191 @@ +/* + * 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.component.pgvector; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.infra.postgres.services.PostgresService; +import org.apache.camel.test.infra.postgres.services.PostgresVectorServiceFactory; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.postgresql.ds.PGSimpleDataSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PgVectorComponentIT extends CamelTestSupport { + + public static final String PGVECTOR_URI = "pgvector:test_embeddings"; + private static final int DIMENSION = 3; + + @RegisterExtension + static PostgresService POSTGRES = PostgresVectorServiceFactory.createService(); + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext context = super.createCamelContext(); + + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(POSTGRES.jdbcUrl()); + dataSource.setUser(POSTGRES.userName()); + dataSource.setPassword(POSTGRES.password()); + + PgVectorComponent component = context.getComponent(PgVector.SCHEME, PgVectorComponent.class); + component.getConfiguration().setDataSource(dataSource); + component.getConfiguration().setDimension(DIMENSION); + + return context; + } + + @Test + @Order(1) + public void createTable() { + Exchange result = fluentTemplate.to(PGVECTOR_URI) + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.CREATE_TABLE) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(Boolean.class)).isTrue(); + } + + @Test + @Order(2) + public void upsert() { + List<Float> vector = Arrays.asList(0.1f, 0.2f, 0.3f); + + Exchange result = fluentTemplate.to(PGVECTOR_URI) + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.UPSERT) + .withHeader(PgVectorHeaders.RECORD_ID, "test-1") + .withHeader(PgVectorHeaders.TEXT_CONTENT, "Hello World") + .withHeader(PgVectorHeaders.METADATA, "source=test") + .withBody(vector) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(String.class)).isEqualTo("test-1"); + } + + @Test + @Order(3) + public void upsertSecond() { + List<Float> vector = Arrays.asList(0.4f, 0.5f, 0.6f); + + Exchange result = fluentTemplate.to(PGVECTOR_URI) + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.UPSERT) + .withHeader(PgVectorHeaders.RECORD_ID, "test-2") + .withHeader(PgVectorHeaders.TEXT_CONTENT, "Goodbye World") + .withBody(vector) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(String.class)).isEqualTo("test-2"); + } + + @Test + @Order(4) + @SuppressWarnings("unchecked") + public void similaritySearch() { + List<Float> queryVector = Arrays.asList(0.1f, 0.2f, 0.3f); + + Exchange result = fluentTemplate.to(PGVECTOR_URI) + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.SIMILARITY_SEARCH) + .withHeader(PgVectorHeaders.QUERY_TOP_K, 2) + .withBody(queryVector) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + + List<Map<String, Object>> results = result.getMessage().getBody(List.class); + assertThat(results).isNotEmpty(); + assertThat(results).hasSizeLessThanOrEqualTo(2); + + // The closest vector should be test-1 (exact match) + assertThat(results.get(0).get("id")).isEqualTo("test-1"); + assertThat(results.get(0).get("text_content")).isEqualTo("Hello World"); + } + + @Test + @Order(5) + public void delete() { + Exchange result = fluentTemplate.to(PGVECTOR_URI) + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.DELETE) + .withHeader(PgVectorHeaders.RECORD_ID, "test-1") + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(Boolean.class)).isTrue(); + } + + @Test + @Order(6) + @SuppressWarnings("unchecked") + public void searchAfterDelete() { + List<Float> queryVector = Arrays.asList(0.1f, 0.2f, 0.3f); + + Exchange result = fluentTemplate.to(PGVECTOR_URI) + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.SIMILARITY_SEARCH) + .withHeader(PgVectorHeaders.QUERY_TOP_K, 2) + .withBody(queryVector) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + + List<Map<String, Object>> results = result.getMessage().getBody(List.class); + assertThat(results).hasSize(1); + assertThat(results.get(0).get("id")).isEqualTo("test-2"); + } + + @Test + @Order(7) + public void dropTable() { + Exchange result = fluentTemplate.to(PGVECTOR_URI) + .withHeader(PgVectorHeaders.ACTION, PgVectorAction.DROP_TABLE) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(Boolean.class)).isTrue(); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + from("direct:in") + .to(PGVECTOR_URI); + } + }; + } +} diff --git a/components/camel-ai/pom.xml b/components/camel-ai/pom.xml index b3cc35116113..d0d2ed952d81 100644 --- a/components/camel-ai/pom.xml +++ b/components/camel-ai/pom.xml @@ -52,6 +52,7 @@ <module>camel-milvus</module> <module>camel-neo4j</module> <module>camel-openai</module> + <module>camel-pgvector</module> <module>camel-pinecone</module> <module>camel-qdrant</module> <module>camel-tensorflow-serving</module> diff --git a/parent/pom.xml b/parent/pom.xml index 73fe2178b18e..fc8e4583a315 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -458,6 +458,7 @@ <pdfbox-version>3.0.7</pdfbox-version> <pgjdbc-driver-version>42.7.10</pgjdbc-driver-version> <pgjdbc-ng-driver-version>0.8.9</pgjdbc-ng-driver-version> + <pgvector-version>0.1.6</pgvector-version> <picocli-version>4.7.7</picocli-version> <pinecone-client-version>3.1.0</pinecone-client-version> <plc4x-version>0.13.1</plc4x-version> @@ -2373,6 +2374,11 @@ <artifactId>camel-pgevent</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-pgvector</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-pinecone</artifactId> diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java index c4ebaa99c5c3..2a51e647d6fb 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java +++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java @@ -46,7 +46,7 @@ public final class MojoHelper { dir.resolve("camel-langchain4j-web-search"), dir.resolve("camel-qdrant"), dir.resolve("camel-milvus"), dir.resolve("camel-neo4j"), dir.resolve("camel-openai"), - dir.resolve("camel-pinecone"), dir.resolve("camel-kserve"), + dir.resolve("camel-pgvector"), dir.resolve("camel-pinecone"), dir.resolve("camel-kserve"), dir.resolve("camel-tensorflow-serving"), dir.resolve("camel-weaviate"), dir.resolve("camel-docling")); case "camel-as2":
