This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch backport/26192-to-camel-4.22.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit a49e8489da1936d15bab7d0245e78504eca89e0c Author: Guillaume Nodet <[email protected]> AuthorDate: Fri Sep 11 08:53:07 2026 +0000 [backport camel-4.22.x] CAMEL-24542: camel-qdrant - honour the PayloadSelector header --- .../camel/component/qdrant/QdrantProducer.java | 30 +++-- .../qdrant/it/QdrantPayloadSelectorIT.java | 132 +++++++++++++++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 11 ++ 3 files changed, 164 insertions(+), 9 deletions(-) diff --git a/components/camel-ai/camel-qdrant/src/main/java/org/apache/camel/component/qdrant/QdrantProducer.java b/components/camel-ai/camel-qdrant/src/main/java/org/apache/camel/component/qdrant/QdrantProducer.java index f3520ed50108..8288d8e36d7f 100644 --- a/components/camel-ai/camel-qdrant/src/main/java/org/apache/camel/component/qdrant/QdrantProducer.java +++ b/components/camel-ai/camel-qdrant/src/main/java/org/apache/camel/component/qdrant/QdrantProducer.java @@ -26,7 +26,6 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import io.qdrant.client.QdrantClient; -import io.qdrant.client.WithPayloadSelectorFactory; import io.qdrant.client.WithVectorsSelectorFactory; import io.qdrant.client.grpc.Collections.VectorParams; import io.qdrant.client.grpc.Common; @@ -106,6 +105,25 @@ public class QdrantProducer extends DefaultAsyncProducer { } } + /** + * Resolves the payload selector to send with a read. An explicit {@link QdrantHeaders#PAYLOAD_SELECTOR} wins, so a + * route can ask for a subset of the payload fields; otherwise the {@link QdrantHeaders#INCLUDE_PAYLOAD} boolean + * selects all or nothing as before. + */ + private static Points.WithPayloadSelector payloadSelector(Message in) { + Points.WithPayloadSelector selector = in.getHeader( + QdrantHeaders.PAYLOAD_SELECTOR, + Points.WithPayloadSelector.class); + if (selector != null) { + return selector; + } + + return enable(in.getHeader( + QdrantHeaders.INCLUDE_PAYLOAD, + QdrantHeaders.DEFAULT_INCLUDE_PAYLOAD, + boolean.class)); + } + // *************************************** // // Actions @@ -151,10 +169,7 @@ public class QdrantProducer extends DefaultAsyncProducer { this.client.retrieveAsync( collection, ids, - WithPayloadSelectorFactory.enable(in.getHeader( - QdrantHeaders.INCLUDE_PAYLOAD, - QdrantHeaders.DEFAULT_INCLUDE_PAYLOAD, - boolean.class)), + payloadSelector(in), WithVectorsSelectorFactory.enable(in.getHeader( QdrantHeaders.INCLUDE_VECTORS, QdrantHeaders.DEFAULT_INCLUDE_VECTORS, @@ -288,10 +303,7 @@ public class QdrantProducer extends DefaultAsyncProducer { QdrantHeaders.INCLUDE_VECTORS, QdrantHeaders.DEFAULT_INCLUDE_VECTORS, boolean.class))) - .setWithPayload(enable(in.getHeader( - QdrantHeaders.INCLUDE_PAYLOAD, - QdrantHeaders.DEFAULT_INCLUDE_PAYLOAD, - boolean.class))); + .setWithPayload(payloadSelector(in)); if (filter != null) { queryRequestBuilder.setFilter(filter); diff --git a/components/camel-ai/camel-qdrant/src/test/java/org/apache/camel/component/qdrant/it/QdrantPayloadSelectorIT.java b/components/camel-ai/camel-qdrant/src/test/java/org/apache/camel/component/qdrant/it/QdrantPayloadSelectorIT.java new file mode 100644 index 000000000000..720018da41e5 --- /dev/null +++ b/components/camel-ai/camel-qdrant/src/test/java/org/apache/camel/component/qdrant/it/QdrantPayloadSelectorIT.java @@ -0,0 +1,132 @@ +/* + * 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.qdrant.it; + +import java.util.Collection; +import java.util.List; + +import io.qdrant.client.PointIdFactory; +import io.qdrant.client.ValueFactory; +import io.qdrant.client.VectorsFactory; +import io.qdrant.client.WithPayloadSelectorFactory; +import io.qdrant.client.grpc.Collections; +import io.qdrant.client.grpc.Points; +import org.apache.camel.Exchange; +import org.apache.camel.component.qdrant.QdrantAction; +import org.apache.camel.component.qdrant.QdrantHeaders; +import org.apache.camel.component.qdrant.QdrantTestSupport; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The {@link QdrantHeaders#PAYLOAD_SELECTOR} header is advertised by the component, so a route must be able to ask for + * a subset of the payload fields instead of the all-or-nothing {@link QdrantHeaders#INCLUDE_PAYLOAD} boolean. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class QdrantPayloadSelectorIT extends QdrantTestSupport { + + private static final String COLLECTION = "qdrant:payloadSelector"; + + @Test + @Order(0) + void createCollection() { + Exchange result = fluentTemplate.to(COLLECTION) + .withHeader(QdrantHeaders.ACTION, QdrantAction.CREATE_COLLECTION) + .withBody(Collections.VectorParams.newBuilder() + .setSize(2) + .setDistance(Collections.Distance.Cosine) + .build()) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + } + + @Test + @Order(1) + void upsert() { + Exchange result = fluentTemplate.to(COLLECTION) + .withHeader(QdrantHeaders.ACTION, QdrantAction.UPSERT) + .withBody(Points.PointStruct.newBuilder() + .setId(PointIdFactory.id(1)) + .putPayload("keep", ValueFactory.value("kept")) + .putPayload("drop", ValueFactory.value("dropped")) + .setVectors(VectorsFactory.vectors(List.of(0.5f, 0.5f))) + .build()) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + } + + @Test + @Order(2) + void retrieveHonoursThePayloadSelector() { + Exchange result = fluentTemplate.to(COLLECTION) + .withHeader(QdrantHeaders.ACTION, QdrantAction.RETRIEVE) + .withHeader(QdrantHeaders.PAYLOAD_SELECTOR, WithPayloadSelectorFactory.include(List.of("keep"))) + .withBody(PointIdFactory.id(1)) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + assertThat(result.getIn().getBody()).isInstanceOfSatisfying(Collection.class, c -> { + assertThat(c).hasSize(1); + Points.RetrievedPoint point = (Points.RetrievedPoint) c.iterator().next(); + assertThat(point.getPayloadMap()).containsOnlyKeys("keep"); + }); + } + + @Test + @Order(3) + void retrieveFallsBackToTheIncludePayloadFlag() { + Exchange result = fluentTemplate.to(COLLECTION) + .withHeader(QdrantHeaders.ACTION, QdrantAction.RETRIEVE) + .withHeader(QdrantHeaders.INCLUDE_PAYLOAD, true) + .withBody(PointIdFactory.id(1)) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + assertThat(result.getIn().getBody()).isInstanceOfSatisfying(Collection.class, c -> { + Points.RetrievedPoint point = (Points.RetrievedPoint) c.iterator().next(); + assertThat(point.getPayloadMap()).containsOnlyKeys("keep", "drop"); + }); + } + + @Test + @Order(4) + void similaritySearchHonoursThePayloadSelector() { + Exchange result = fluentTemplate.to(COLLECTION) + .withHeader(QdrantHeaders.ACTION, QdrantAction.SIMILARITY_SEARCH) + .withHeader(QdrantHeaders.PAYLOAD_SELECTOR, WithPayloadSelectorFactory.include(List.of("keep"))) + .withBody(List.of(0.5f, 0.5f)) + .request(Exchange.class); + + assertThat(result).isNotNull(); + assertThat(result.getException()).isNull(); + assertThat(result.getIn().getBody()).isInstanceOfSatisfying(Collection.class, c -> { + assertThat(c).hasSize(1); + Points.ScoredPoint point = (Points.ScoredPoint) c.iterator().next(); + assertThat(point.getPayloadMap()).containsOnlyKeys("keep"); + }); + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index ba334d38fbbd..425e64ffcf44 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -1865,3 +1865,14 @@ Routes that set `useRecovery=false` are unaffected. Routes that left recovery en default, stop seeing in-progress aggregations re-delivered, and start seeing genuine recovery. The cache now also holds one entry per completed and not yet confirmed exchange; those entries are removed on confirmation. + +=== camel-qdrant - the PayloadSelector header is now honoured + +`QdrantHeaders.PAYLOAD_SELECTOR` (`CamelQdrantPointsPayloadSelector`) was declared and advertised in +the component metadata but never read. The `RETRIEVE` and `SIMILARITY_SEARCH` operations only honoured +the `CamelQdrantWithPayload` boolean, so a route that set a `Points.WithPayloadSelector` to request +specific payload fields was silently given the whole payload. + +Both operations now use the header when it is present, and fall back to the `CamelQdrantWithPayload` +boolean otherwise. A route that already sets the header starts receiving only the payload fields it +selected; a route that does not set it is unaffected.
