This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new d20702f9101e CAMEL-26215: camel-pgvector - take the vector as a List
of any numbers, such as the List of Double a JSON array gives, or a float or
double array (#29062)
d20702f9101e is described below
commit d20702f9101e3283f2fc2cacac8f9322ef958965
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Nov 3 12:13:06 2026 +0100
CAMEL-26215: camel-pgvector - take the vector as a List of any numbers,
such as the List of Double a JSON array gives, or a float or double array
(#29062)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../apache/camel/catalog/components/pgvector.json | 2 +-
.../camel/catalog/docs/pgvector-component.adoc | 4 +-
.../apache/camel/component/pgvector/pgvector.json | 2 +-
.../src/main/docs/pgvector-component.adoc | 4 +-
.../camel/component/pgvector/PgVectorEndpoint.java | 11 +-
.../camel/component/pgvector/PgVectorProducer.java | 45 ++++--
.../component/pgvector/PgVectorVectorBodyTest.java | 159 +++++++++++++++++++++
7 files changed, 203 insertions(+), 24 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pgvector.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pgvector.json
index 9e1e646de8e2..02e825a759f2 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pgvector.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pgvector.json
@@ -42,7 +42,7 @@
"CamelPgVectorFilterParams": { "index": 6, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "java.util.List", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Parameter values for
parameterized filter queries. Use with placeholders in the CamelPgVectorFilter
header. Example: filter = 'text_content LIKE AND metadata::jsonb-'category' = '
with filterParams = List.of(%hello%, science).", [...]
},
"bodyTypes": {
- "producerIn": { "carriers": [ "list", "null" ], "byOption": [ { "option":
"action", "value": "UPSERT", "carriers": [ "list" ] }, { "option": "action",
"value": "SIMILARITY_SEARCH", "carriers": [ "list" ] }, { "option": "action",
"value": "CREATE_TABLE", "carriers": [ "any" ] }, { "option": "action",
"value": "CREATE_INDEX", "carriers": [ "any" ] }, { "option": "action",
"value": "DROP_TABLE", "carriers": [ "any" ] }, { "option": "action", "value":
"DELETE", "carriers": [ "any" ] } ], [...]
+ "producerIn": { "carriers": [ "list", "null" ], "byOption": [ { "option":
"action", "value": "UPSERT", "carriers": [ "list" ] }, { "option": "action",
"value": "SIMILARITY_SEARCH", "carriers": [ "list" ] }, { "option": "action",
"value": "CREATE_TABLE", "carriers": [ "any" ] }, { "option": "action",
"value": "CREATE_INDEX", "carriers": [ "any" ] }, { "option": "action",
"value": "DROP_TABLE", "carriers": [ "any" ] }, { "option": "action", "value":
"DELETE", "carriers": [ "any" ] } ], [...]
"producerOut": { "carriers": [ "value", "text", "list" ], "byOption": [ {
"option": "action", "value": "CREATE_TABLE", "carriers": [ "value" ],
"javaType": "java.lang.Boolean" }, { "option": "action", "value":
"CREATE_INDEX", "carriers": [ "value" ], "javaType": "java.lang.Boolean" }, {
"option": "action", "value": "DROP_TABLE", "carriers": [ "value" ], "javaType":
"java.lang.Boolean" }, { "option": "action", "value": "DELETE", "carriers": [
"value" ], "javaType": "java.lang.Boolean" [...]
},
"properties": {
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pgvector-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pgvector-component.adoc
index bf760999eac1..f3805a5546d6 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pgvector-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pgvector-component.adoc
@@ -38,9 +38,9 @@ The `action` option of the endpoint, such as
`pgvector:myTable?action=SIMILARITY
- `CREATE_TABLE` - Creates the pgvector extension and a table with columns:
id, text_content, metadata, embedding
- `CREATE_INDEX` - Creates an HNSW index on the embedding column for faster
approximate nearest neighbor search. The index uses the distance type
configured on the endpoint (default: COSINE)
- `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
+- `UPSERT` - Inserts or updates a vector record. The body is the vector: a
List of numbers, such as a `List<Float>` or the `List<Double>` a JSON array
unmarshals to, or a `float[]` or `double[]`; each number is converted to a
float, and another body fails. 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). Optionally set `CamelPgVectorFilter` with a SQL WHERE clause to
filter results (e.g., `text_content LIKE '%hello%'`). Returns a
`List<Map<String, Object>>` with keys: id, text_content, metadata, distance
+- `SIMILARITY_SEARCH` - Searches for similar vectors. The body is the query
vector, of the same types as for `UPSERT`. Set `CamelPgVectorQueryTopK` for max
results (default 3). Optionally set `CamelPgVectorFilter` with a SQL WHERE
clause to filter results (e.g., `text_content LIKE '%hello%'`). Returns a
`List<Map<String, Object>>` with keys: id, text_content, metadata, distance
== Defaults
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
index 9e1e646de8e2..02e825a759f2 100644
---
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
@@ -42,7 +42,7 @@
"CamelPgVectorFilterParams": { "index": 6, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "java.util.List", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Parameter values for
parameterized filter queries. Use with placeholders in the CamelPgVectorFilter
header. Example: filter = 'text_content LIKE AND metadata::jsonb-'category' = '
with filterParams = List.of(%hello%, science).", [...]
},
"bodyTypes": {
- "producerIn": { "carriers": [ "list", "null" ], "byOption": [ { "option":
"action", "value": "UPSERT", "carriers": [ "list" ] }, { "option": "action",
"value": "SIMILARITY_SEARCH", "carriers": [ "list" ] }, { "option": "action",
"value": "CREATE_TABLE", "carriers": [ "any" ] }, { "option": "action",
"value": "CREATE_INDEX", "carriers": [ "any" ] }, { "option": "action",
"value": "DROP_TABLE", "carriers": [ "any" ] }, { "option": "action", "value":
"DELETE", "carriers": [ "any" ] } ], [...]
+ "producerIn": { "carriers": [ "list", "null" ], "byOption": [ { "option":
"action", "value": "UPSERT", "carriers": [ "list" ] }, { "option": "action",
"value": "SIMILARITY_SEARCH", "carriers": [ "list" ] }, { "option": "action",
"value": "CREATE_TABLE", "carriers": [ "any" ] }, { "option": "action",
"value": "CREATE_INDEX", "carriers": [ "any" ] }, { "option": "action",
"value": "DROP_TABLE", "carriers": [ "any" ] }, { "option": "action", "value":
"DELETE", "carriers": [ "any" ] } ], [...]
"producerOut": { "carriers": [ "value", "text", "list" ], "byOption": [ {
"option": "action", "value": "CREATE_TABLE", "carriers": [ "value" ],
"javaType": "java.lang.Boolean" }, { "option": "action", "value":
"CREATE_INDEX", "carriers": [ "value" ], "javaType": "java.lang.Boolean" }, {
"option": "action", "value": "DROP_TABLE", "carriers": [ "value" ], "javaType":
"java.lang.Boolean" }, { "option": "action", "value": "DELETE", "carriers": [
"value" ], "javaType": "java.lang.Boolean" [...]
},
"properties": {
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
index bf760999eac1..f3805a5546d6 100644
--- a/components/camel-ai/camel-pgvector/src/main/docs/pgvector-component.adoc
+++ b/components/camel-ai/camel-pgvector/src/main/docs/pgvector-component.adoc
@@ -38,9 +38,9 @@ The `action` option of the endpoint, such as
`pgvector:myTable?action=SIMILARITY
- `CREATE_TABLE` - Creates the pgvector extension and a table with columns:
id, text_content, metadata, embedding
- `CREATE_INDEX` - Creates an HNSW index on the embedding column for faster
approximate nearest neighbor search. The index uses the distance type
configured on the endpoint (default: COSINE)
- `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
+- `UPSERT` - Inserts or updates a vector record. The body is the vector: a
List of numbers, such as a `List<Float>` or the `List<Double>` a JSON array
unmarshals to, or a `float[]` or `double[]`; each number is converted to a
float, and another body fails. 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). Optionally set `CamelPgVectorFilter` with a SQL WHERE clause to
filter results (e.g., `text_content LIKE '%hello%'`). Returns a
`List<Map<String, Object>>` with keys: id, text_content, metadata, distance
+- `SIMILARITY_SEARCH` - Searches for similar vectors. The body is the query
vector, of the same types as for `UPSERT`. Set `CamelPgVectorQueryTopK` for max
results (default 3). Optionally set `CamelPgVectorFilter` with a SQL WHERE
clause to filter results (e.g., `text_content LIKE '%hello%'`). Returns a
`List<Map<String, Object>>` with keys: id, text_content, metadata, distance
== Defaults
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
index 9f7cf907958e..aceac50c4720 100644
---
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
@@ -56,11 +56,12 @@ import org.apache.camel.support.DefaultEndpoint;
@BodyType.When(option = "action",
value = "DROP_TABLE", carriers = Carrier.ANY),
@BodyType.When(option = "action",
value = "DELETE", carriers = Carrier.ANY) },
description = "By the action in the action
option or the CamelPgVectorAction header: the vector as a"
- + " List of Float or a float
array for UPSERT and SIMILARITY_SEARCH, with the id, text and"
- + " metadata of UPSERT in
headers. CREATE_TABLE, CREATE_INDEX, DROP_TABLE and DELETE do not"
- + " read the body, DELETE
takes the id from the CamelPgVectorRecordId header. Another body"
- + " fails, a List of Double
too: the pgvector:embeddings data type transformer builds the"
- + " vector from the output of
langchain4j-embeddings"),
+ + " List of numbers, such as
the List of Double a JSON array unmarshals to, or a float or"
+ + " double array for UPSERT
and SIMILARITY_SEARCH, with the id, text and metadata of UPSERT"
+ + " in headers. CREATE_TABLE,
CREATE_INDEX, DROP_TABLE and DELETE do not read the body,"
+ + " DELETE takes the id from
the CamelPgVectorRecordId header. Another body fails: the"
+ + " pgvector:embeddings data
type transformer builds the vector from the output of"
+ + " langchain4j-embeddings"),
producerOut = @BodyType(value = { Carrier.VALUE, Carrier.TEXT,
Carrier.LIST },
byOption = {
@BodyType.When(option = "action",
value = "CREATE_TABLE", carriers = Carrier.VALUE,
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
index 75625df6e841..62fa3af9ad46 100644
---
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
@@ -29,6 +29,7 @@ import java.util.UUID;
import com.pgvector.PGvector;
import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
import org.apache.camel.Message;
import org.apache.camel.NoSuchHeaderException;
import org.apache.camel.support.DefaultProducer;
@@ -36,6 +37,8 @@ import org.apache.camel.support.ExchangeHelper;
public class PgVectorProducer extends DefaultProducer {
+ private static final String VECTOR_TYPES = "a List of numbers, such as
Float or Double, a float[] or a double[]";
+
public PgVectorProducer(PgVectorEndpoint endpoint) {
super(endpoint);
}
@@ -146,14 +149,12 @@ public class PgVectorProducer extends DefaultProducer {
}
}
- @SuppressWarnings("unchecked")
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);
+ float[] vectorArray = toVector(exchange);
String textContent = in.getHeader(PgVectorHeaders.TEXT_CONTENT,
String.class);
String metadata = in.getHeader(PgVectorHeaders.METADATA, String.class);
@@ -193,7 +194,6 @@ public class PgVectorProducer extends DefaultProducer {
}
}
- @SuppressWarnings("unchecked")
private void similaritySearch(Exchange exchange) throws Exception {
final Message in = exchange.getMessage();
String tableName = getEndpoint().getCollection();
@@ -202,8 +202,7 @@ public class PgVectorProducer extends DefaultProducer {
String filter = in.getHeader(PgVectorHeaders.FILTER, String.class);
List<?> filterParams = in.getHeader(PgVectorHeaders.FILTER_PARAMS,
List.class);
- List<Float> queryVector = in.getMandatoryBody(List.class);
- float[] vectorArray = toFloatArray(queryVector);
+ float[] vectorArray = toVector(exchange);
String distanceOp = distanceType.getOperator();
StringBuilder sqlBuilder = new StringBuilder();
@@ -256,14 +255,34 @@ public class PgVectorProducer extends DefaultProducer {
//
// ***************************************
- private static float[] toFloatArray(List<Float> vector) {
- float[] result = new float[vector.size()];
- for (int i = 0; i < vector.size(); i++) {
- Float value = vector.get(i);
- if (value == null) {
- throw new IllegalArgumentException("Vector contains null value
at index " + i);
+ /**
+ * The vector in the body of UPSERT and SIMILARITY_SEARCH: a float[], or a
List of numbers, such as the List of
+ * Double a JSON array unmarshals to, or another body Camel converts to a
List, such as a double[] or a Float[].
+ */
+ private static float[] toVector(Exchange exchange) throws
InvalidPayloadException {
+ final Message in = exchange.getMessage();
+ if (in.getBody() instanceof float[] vector) {
+ return vector;
+ }
+ List<?> elements = in.getBody(List.class);
+ if (elements == null) {
+ throw new InvalidPayloadException(exchange, List.class, in, "the
vector must be " + VECTOR_TYPES);
+ }
+ return toFloatArray(elements);
+ }
+
+ private static float[] toFloatArray(List<?> elements) {
+ float[] result = new float[elements.size()];
+ int i = 0;
+ for (Object element : elements) {
+ if (!(element instanceof Number number)) {
+ // the type only, not the value, which comes from the message
+ throw new IllegalArgumentException(
+ "Vector element at index " + i + " is "
+ + (element == null ? "null"
: "a " + element.getClass().getName())
+ + ": the vector must be " +
VECTOR_TYPES);
}
- result[i] = value;
+ result[i++] = number.floatValue();
}
return result;
}
diff --git
a/components/camel-ai/camel-pgvector/src/test/java/org/apache/camel/component/pgvector/PgVectorVectorBodyTest.java
b/components/camel-ai/camel-pgvector/src/test/java/org/apache/camel/component/pgvector/PgVectorVectorBodyTest.java
new file mode 100644
index 000000000000..ce0fbf08de8d
--- /dev/null
+++
b/components/camel-ai/camel-pgvector/src/test/java/org/apache/camel/component/pgvector/PgVectorVectorBodyTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Stream;
+
+import javax.sql.DataSource;
+
+import com.pgvector.PGvector;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Named;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.postgresql.PGConnection;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26215: the vector of UPSERT and SIMILARITY_SEARCH is a List of
numbers, such as the List of Double a JSON array
+ * unmarshals to, or a float or double array. A stub DataSource records the
vectors the statements are given, so no
+ * database is needed.
+ */
+class PgVectorVectorBodyTest extends CamelTestSupport {
+
+ private static final float[] VECTOR = { 0.1f, 0.2f, 0.3f };
+
+ private final List<float[]> vectors = new CopyOnWriteArrayList<>();
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ ResultSet noRows = stub(ResultSet.class, (proxy, method, args) ->
switch (method.getName()) {
+ case "next" -> false;
+ case "close" -> null;
+ default -> throw new
UnsupportedOperationException(method.getName());
+ });
+ PreparedStatement statement = stub(PreparedStatement.class, (proxy,
method, args) -> switch (method.getName()) {
+ case "setObject" -> {
+ if (args[1] instanceof PGvector vector) {
+ vectors.add(vector.toArray());
+ }
+ yield null;
+ }
+ case "setString", "setInt", "close" -> null;
+ case "executeUpdate" -> 1;
+ case "executeQuery" -> noRows;
+ default -> throw new
UnsupportedOperationException(method.getName());
+ });
+ PGConnection pgConnection = stub(PGConnection.class, (proxy, method,
args) -> null);
+ Connection connection = stub(Connection.class, (proxy, method, args)
-> switch (method.getName()) {
+ case "unwrap" -> pgConnection;
+ case "prepareStatement" -> statement;
+ case "close" -> null;
+ default -> throw new
UnsupportedOperationException(method.getName());
+ });
+ DataSource dataSource = stub(DataSource.class, (proxy, method, args)
-> switch (method.getName()) {
+ case "getConnection" -> connection;
+ default -> throw new
UnsupportedOperationException(method.getName());
+ });
+ context.getComponent(PgVector.SCHEME,
PgVectorComponent.class).getConfiguration().setDataSource(dataSource);
+ return context;
+ }
+
+ static Stream<Named<Object>> vectorBodies() {
+ return Stream.of(
+ Named.of("List of Float", List.of(0.1f, 0.2f, 0.3f)),
+ Named.of("List of Double", List.of(0.1d, 0.2d, 0.3d)),
+ Named.of("List of BigDecimal", List.of(new BigDecimal("0.1"),
new BigDecimal("0.2"), new BigDecimal("0.3"))),
+ Named.of("float[]", new float[] { 0.1f, 0.2f, 0.3f }),
+ Named.of("double[]", new double[] { 0.1d, 0.2d, 0.3d }),
+ Named.of("Double[]", new Double[] { 0.1d, 0.2d, 0.3d }));
+ }
+
+ @ParameterizedTest
+ @MethodSource("vectorBodies")
+ void upsertTakesTheVector(Object body) {
+ Exchange result = fluentTemplate.to("pgvector:items?action=UPSERT")
+ .withHeader(PgVectorHeaders.RECORD_ID, "item-1")
+ .withBody(body)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ assertThat(result.getMessage().getBody()).isEqualTo("item-1");
+ assertThat(vectors).hasSize(1);
+ assertThat(vectors.get(0)).containsExactly(VECTOR);
+ }
+
+ @ParameterizedTest
+ @MethodSource("vectorBodies")
+ void similaritySearchTakesTheVector(Object body) {
+ Exchange result =
fluentTemplate.to("pgvector:items?action=SIMILARITY_SEARCH")
+ .withBody(body)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ assertThat(result.getMessage().getBody(List.class)).isEmpty();
+ // the vector is bound twice: for the distance and for the order
+ assertThat(vectors).hasSize(2);
+ assertThat(vectors).allSatisfy(vector ->
assertThat(vector).containsExactly(VECTOR));
+ }
+
+ @Test
+ void aBodyThatIsNotAVectorFails() {
+ Exchange text = fluentTemplate.to("pgvector:items?action=UPSERT")
+ .withBody("0.1, 0.2, 0.3")
+ .request(Exchange.class);
+
assertThat(text.getException()).isInstanceOfSatisfying(InvalidPayloadException.class,
+ e -> assertThat(e.getType()).isEqualTo(List.class))
+ .hasMessageContaining("the vector must be a List of numbers,
such as Float or Double, a float[] or a double[]");
+
+ // the message names the type of the element, not its value
+ Exchange listOfText =
fluentTemplate.to("pgvector:items?action=SIMILARITY_SEARCH")
+ .withBody(List.of("secret-0.1", "0.2"))
+ .request(Exchange.class);
+
assertThat(listOfText.getException()).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageStartingWith("Vector element at index 0 is a
java.lang.String")
+ .hasMessageNotContaining("secret");
+
+ Exchange nullElement =
fluentTemplate.to("pgvector:items?action=UPSERT")
+ .withBody(Arrays.asList(0.1d, null, 0.3d))
+ .request(Exchange.class);
+
assertThat(nullElement.getException()).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageStartingWith("Vector element at index 1 is null");
+
+ assertThat(vectors).isEmpty();
+ }
+
+ private static <T> T stub(Class<T> type, InvocationHandler handler) {
+ return type
+
.cast(Proxy.newProxyInstance(PgVectorVectorBodyTest.class.getClassLoader(), new
Class<?>[] { type }, handler));
+ }
+}