This is an automated email from the ASF dual-hosted git repository.
abenedetti pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_10x by this push:
new d31d514f148 SOLR-17736: introducing support for KNN search on nested
vectors (block join) (#3316)
d31d514f148 is described below
commit d31d514f1483ab4526083ed753d37427f7d9604d
Author: Alessandro Benedetti <[email protected]>
AuthorDate: Thu Dec 18 18:00:02 2025 +0100
SOLR-17736: introducing support for KNN search on nested vectors (block
join) (#3316)
* first draft
* Only Nested Vectors changes
* first tests draft, parent filter and children filter missing as a test
* tests cleaned
* code cleanup
* draft documentation
* tidy
* tidy
* add best child per document transformer
* add best child per document transformer
* add best child per document transformer
* minor refinement to avoid some instructions
* minor refactor
* Update
solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java
Co-authored-by: Christine Poerschke <[email protected]>
* Update
solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java
Co-authored-by: Christine Poerschke <[email protected]>
* new approach following feedback
* tests fixed for the new approach
* tidy + documentation
* tidy + documentation
* tidy + documentation
* tidy + documentation
* minor
* renaming after feedback
---------
Co-authored-by: Christine Poerschke <[email protected]>
(cherry picked from commit 26457c3191182b2c8016d837723ebcc3688536a1)
---
changelog/unreleased/SOLR-17736.yml | 8 +
.../org/apache/solr/search/vector/KnnQParser.java | 81 ++++
.../solr/collection1/conf/schema-densevector.xml | 7 +-
.../join/BlockJoinNestedVectorsQParserTest.java | 437 +++++++++++++++++++++
.../solr/search/vector/KnnQParserChildTest.java | 217 ++++++++++
.../query-guide/pages/dense-vector-search.adoc | 42 +-
.../pages/searching-nested-documents.adoc | 40 ++
7 files changed, 829 insertions(+), 3 deletions(-)
diff --git a/changelog/unreleased/SOLR-17736.yml
b/changelog/unreleased/SOLR-17736.yml
new file mode 100644
index 00000000000..675d0a365c3
--- /dev/null
+++ b/changelog/unreleased/SOLR-17736.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Introducing support for nested vector search, enabling the retrieval of
nested documents diversified by parent. This enables multi valued vectors
scenarios and best child retrieval per parent.
+type: added # added, changed, fixed, deprecated, removed, dependency_update,
security, other
+authors:
+ - name: Alessandro Benedetti
+links:
+ - name: SOLR-17736
+ url: https://issues.apache.org/jira/browse/SOLR-17736
diff --git a/solr/core/src/java/org/apache/solr/search/vector/KnnQParser.java
b/solr/core/src/java/org/apache/solr/search/vector/KnnQParser.java
index 1a5beb83880..ee82d78909c 100644
--- a/solr/core/src/java/org/apache/solr/search/vector/KnnQParser.java
+++ b/solr/core/src/java/org/apache/solr/search/vector/KnnQParser.java
@@ -17,7 +17,14 @@
package org.apache.solr.search.vector;
import java.util.Optional;
+import org.apache.lucene.index.VectorEncoding;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
+import org.apache.lucene.search.join.BitSetProducer;
+import org.apache.lucene.search.join.DiversifyingChildrenByteKnnVectorQuery;
+import org.apache.lucene.search.join.DiversifyingChildrenFloatKnnVectorQuery;
+import org.apache.lucene.search.join.ToChildBlockJoinQuery;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
@@ -25,6 +32,8 @@ import org.apache.solr.schema.DenseVectorField;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.QParser;
import org.apache.solr.search.SyntaxError;
+import org.apache.solr.search.join.BlockJoinParentQParser;
+import org.apache.solr.util.vector.DenseVectorParser;
public class KnnQParser extends AbstractVectorQParserBase {
@@ -41,6 +50,9 @@ public class KnnQParser extends AbstractVectorQParserBase {
protected static final String SATURATION_THRESHOLD = "saturationThreshold";
protected static final String PATIENCE = "patience";
+ public static final String PARENTS_PRE_FILTER = "parents.preFilter";
+ public static final String CHILDREN_OF = "childrenOf";
+
public KnnQParser(String qstr, SolrParams localParams, SolrParams params,
SolrQueryRequest req) {
super(qstr, localParams, params, req);
}
@@ -104,6 +116,7 @@ public class KnnQParser extends AbstractVectorQParserBase {
@Override
public Query parse() throws SyntaxError {
+ final String vectorField = getFieldName();
final SchemaField schemaField =
req.getCore().getLatestSchema().getField(getFieldName());
final DenseVectorField denseVectorType = getCheckedFieldType(schemaField);
final String vectorToSearch = getVectorToSearch();
@@ -119,6 +132,46 @@ public class KnnQParser extends AbstractVectorQParserBase {
final Integer filteredSearchThreshold =
localParams.getInt(FILTERED_SEARCH_THRESHOLD);
+ // check for parent diversification logic...
+ final String parentsFilterQuery = localParams.get(PARENTS_PRE_FILTER);
+ final String allParentsQuery = localParams.get(CHILDREN_OF);
+
+ boolean isDiversifyingChildrenKnnQuery = null != parentsFilterQuery ||
null != allParentsQuery;
+ if (isDiversifyingChildrenKnnQuery) {
+ if (null == allParentsQuery) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "When running a diversifying children KNN query, 'allParents'
parameter is required");
+ }
+ final DenseVectorParser vectorBuilder =
+ denseVectorType.getVectorBuilder(vectorToSearch,
DenseVectorParser.BuilderPhase.QUERY);
+ final VectorEncoding vectorEncoding =
denseVectorType.getVectorEncoding();
+
+ final BitSetProducer allParentsBitSet =
+ BlockJoinParentQParser.getCachedBitSetProducer(
+ req, subQuery(allParentsQuery, null).getQuery());
+ final BooleanQuery acceptedParents =
getParentsFilter(parentsFilterQuery);
+
+ Query acceptedChildren =
+ getChildrenFilter(getFilterQuery(), acceptedParents,
allParentsBitSet);
+ switch (vectorEncoding) {
+ case FLOAT32:
+ return new DiversifyingChildrenFloatKnnVectorQuery(
+ vectorField,
+ vectorBuilder.getFloatVector(),
+ acceptedChildren,
+ topK,
+ allParentsBitSet);
+ case BYTE:
+ return new DiversifyingChildrenByteKnnVectorQuery(
+ vectorField, vectorBuilder.getByteVector(), acceptedChildren,
topK, allParentsBitSet);
+ default:
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Unexpected encoding. Vector Encoding: " + vectorEncoding);
+ }
+ }
+
return denseVectorType.getKnnVectorQuery(
schemaField.getName(),
vectorToSearch,
@@ -129,4 +182,32 @@ public class KnnQParser extends AbstractVectorQParserBase {
getEarlyTerminationParams(),
filteredSearchThreshold);
}
+
+ private BooleanQuery getParentsFilter(String parentsFilterQuery) throws
SyntaxError {
+ BooleanQuery.Builder acceptedParentsBuilder = new BooleanQuery.Builder();
+ if (parentsFilterQuery != null) {
+ final Query parentsFilter = subQuery(parentsFilterQuery,
null).getQuery();
+ acceptedParentsBuilder.add(parentsFilter, BooleanClause.Occur.FILTER);
+ }
+ BooleanQuery acceptedParents = acceptedParentsBuilder.build();
+ return acceptedParents;
+ }
+
+ private Query getChildrenFilter(
+ Query childrenKnnPreFilter, BooleanQuery parentsFilter, BitSetProducer
allParentsBitSet) {
+ Query childrenFilter = childrenKnnPreFilter;
+
+ if (!parentsFilter.clauses().isEmpty()) {
+ Query acceptedChildrenBasedOnParentsFilter =
+ new ToChildBlockJoinQuery(parentsFilter, allParentsBitSet); // no
scoring happens here
+ BooleanQuery.Builder acceptedChildrenBuilder = new
BooleanQuery.Builder();
+ if (childrenFilter != null) {
+ acceptedChildrenBuilder.add(childrenFilter,
BooleanClause.Occur.FILTER);
+ }
+ acceptedChildrenBuilder.add(acceptedChildrenBasedOnParentsFilter,
BooleanClause.Occur.FILTER);
+
+ childrenFilter = acceptedChildrenBuilder.build();
+ }
+ return childrenFilter;
+ }
}
diff --git
a/solr/core/src/test-files/solr/collection1/conf/schema-densevector.xml
b/solr/core/src/test-files/solr/collection1/conf/schema-densevector.xml
index 42db078a6e2..f3d663a4066 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema-densevector.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema-densevector.xml
@@ -19,13 +19,14 @@
<!-- Test schema file for DenseVectorField -->
<schema name="schema-densevector" version="1.0">
- <fieldType name="string" class="solr.StrField" multiValued="true"/>
+ <fieldType name="string" class="solr.StrField" multiValued="true"/>
<fieldType name="knn_vector" class="solr.DenseVectorField"
vectorDimension="4" similarityFunction="cosine" />
<fieldType name="knn_vector_byte_encoding" class="solr.DenseVectorField"
vectorDimension="4" similarityFunction="cosine" vectorEncoding="BYTE"/>
<fieldType name="high_dimensional_float_knn_vector"
class="solr.DenseVectorField" vectorDimension="2048"
similarityFunction="cosine" vectorEncoding="FLOAT32"/>
<fieldType name="high_dimensional_byte_knn_vector"
class="solr.DenseVectorField" vectorDimension="2048"
similarityFunction="cosine" vectorEncoding="BYTE"/>
<fieldType name="plong" class="solr.LongPointField"
useDocValuesAsStored="false"/>
-
+
+ <field name="_root_" type="string" indexed="true" stored="true"
multiValued="false" required="true"/>
<field name="id" type="string" indexed="true" stored="true"
multiValued="false" required="false"/>
<field name="vector" type="knn_vector" indexed="true" stored="true"/>
<field name="vector2" type="knn_vector" indexed="true" stored="true"/>
@@ -34,6 +35,8 @@
<field name="2048_float_vector" type="high_dimensional_float_knn_vector"
indexed="true" stored="true" />
<field name="string_field" type="string" indexed="true" stored="true"
multiValued="false" required="false"/>
+ <dynamicField name="*_s" type="string" indexed="true" stored="true"
multiValued="false" />
+
<field name="_version_" type="plong" indexed="true" stored="true"
multiValued="false" />
<field name="_text_" type="text_general" indexed="true" stored="false"
multiValued="true"/>
<copyField source="*" dest="_text_"/>
diff --git
a/solr/core/src/test/org/apache/solr/search/join/BlockJoinNestedVectorsQParserTest.java
b/solr/core/src/test/org/apache/solr/search/join/BlockJoinNestedVectorsQParserTest.java
new file mode 100644
index 00000000000..3dbf2f98037
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/search/join/BlockJoinNestedVectorsQParserTest.java
@@ -0,0 +1,437 @@
+/*
+ * 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.solr.search.join;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.util.RandomNoReverseMergePolicyFactory;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+public class BlockJoinNestedVectorsQParserTest extends SolrTestCaseJ4 {
+ private static final List<Float> FLOAT_QUERY_VECTOR = Arrays.asList(1.0f,
1.0f, 1.0f, 1.0f);
+ private static final List<Integer> BYTE_QUERY_VECTOR = Arrays.asList(1, 1,
1, 1);
+
+ @ClassRule
+ public static final TestRule noReverseMerge =
RandomNoReverseMergePolicyFactory.createRule();
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ initCore("solrconfig.xml", "schema15.xml");
+ prepareIndex();
+ }
+
+ public static void prepareIndex() throws Exception {
+ List<SolrInputDocument> docsToIndex = prepareDocs();
+ for (SolrInputDocument doc : docsToIndex) {
+ assertU(adoc(doc));
+ }
+ assertU(commit());
+ }
+
+ /**
+ * The documents in the index are 10 parents, with some parent level
metadata and 30 nested
+ * documents (with vectors and children level metadata) Each parent document
has 3 nested
+ * documents with vectors.
+ *
+ * <p>This allows to run knn queries both at parent/children level and using
various pre-filters
+ * both for parent metadata and children.
+ *
+ * @return a list of documents to index
+ */
+ private static List<SolrInputDocument> prepareDocs() {
+ int totalParentDocuments = 10;
+ int totalNestedVectors = 30;
+ int perParentChildren = totalNestedVectors / totalParentDocuments;
+
+ final String[] klm = new String[] {"k", "l", "m"};
+ final String[] abcdef = new String[] {"a", "b", "c", "d", "e", "f"};
+
+ List<SolrInputDocument> docs = new ArrayList<>(totalParentDocuments);
+ for (int i = 1; i < totalParentDocuments + 1; i++) {
+ SolrInputDocument doc = new SolrInputDocument();
+ doc.setField("id", i);
+ doc.setField("parent_b", true);
+
+ doc.setField("parent_s", abcdef[i % abcdef.length]);
+ List<SolrInputDocument> children = new ArrayList<>(perParentChildren);
+
+ // nested vector documents have a distance from the query vector
inversely proportional to
+ // their id
+ for (int j = 0; j < perParentChildren; j++) {
+ SolrInputDocument child = new SolrInputDocument();
+ child.setField("id", i + "" + j);
+ child.setField("child_s", klm[i % klm.length]);
+ child.setField("vector", outDistanceFloat(FLOAT_QUERY_VECTOR,
totalNestedVectors));
+ child.setField("vector_byte", outDistanceByte(BYTE_QUERY_VECTOR,
totalNestedVectors));
+ totalNestedVectors--; // the higher the id of the nested document,
lower the distance with
+ // the query vector
+ children.add(child);
+ }
+ doc.setField("vectors", children);
+ docs.add(doc);
+ }
+
+ return docs;
+ }
+
+ /**
+ * Generate a resulting float vector with a distance from the original
vector that is proportional
+ * to the value in input (higher the value, higher the distance from the
original vector)
+ *
+ * @param vector a numerical vector
+ * @param value a numerical value to be added to the first element of the
vector
+ * @return a numerical vector that has a distance from the input vector,
proportional to the value
+ */
+ private static List<Float> outDistanceFloat(List<Float> vector, int value) {
+ List<Float> result = new ArrayList<>(vector.size());
+ for (int i = 0; i < vector.size(); i++) {
+ if (i == 0) {
+ result.add(vector.get(i) + value);
+ } else {
+ result.add(vector.get(i));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Generate a resulting byte vector with a distance from the original vector
that is proportional
+ * to the value in input (higher the value, higher the distance from the
original vector)
+ *
+ * @param vector a numerical vector
+ * @param value a numerical value to be added to the first element of the
vector
+ * @return a numerical vector that has a distance from the input vector,
proportional to the value
+ */
+ private static List<Integer> outDistanceByte(List<Integer> vector, int
value) {
+ List<Integer> result = new ArrayList<>(vector.size());
+ for (int i = 0; i < vector.size(); i++) {
+ if (i == 0) {
+ result.add(vector.get(i) + value);
+ } else {
+ result.add(vector.get(i));
+ }
+ }
+ return result;
+ }
+
+ @Test
+ public void
parentRetrieval_knnChildrenDiversifyingWithNoAllParents_shouldThrowException() {
+ assertQEx(
+ "When running a diversifying children KNN query, 'childrenOf'
parameter is required",
+ req(
+ "q",
+ "{!parent which=$allParents score=max v=$children.q}",
+ "fl",
+ "id,score",
+ "children.q",
+ "{!knn f=vector topK=3 parents.preFilter=$someParents}" +
FLOAT_QUERY_VECTOR,
+ "allParents",
+ "parent_s:[* TO *]",
+ "someParents",
+ "parent_s:(a c)"),
+ 400);
+ }
+
+ @Test
+ public void
childrenRetrievalFloat_filteringByParentMetadata_shouldReturnKnnChildren() {
+ assertQ(
+ req(
+ "fq", "{!child of=$allParents filters=$parent.fq}",
+ "q", "{!knn f=vector topK=5}" + FLOAT_QUERY_VECTOR,
+ "fl", "id",
+ "parent.fq", "parent_s:(a c)",
+ "allParents", "parent_s:[* TO *]"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/str[@name='id'][.='82']",
+ "//result/doc[2]/str[@name='id'][.='81']",
+ "//result/doc[3]/str[@name='id'][.='80']",
+ "//result/doc[4]/str[@name='id'][.='62']",
+ "//result/doc[5]/str[@name='id'][.='61']");
+ }
+
+ @Test
+ public void
childrenRetrievalByte_filteringByParentMetadata_shouldReturnKnnChildren() {
+ assertQ(
+ req(
+ "fq", "{!child of=$allParents filters=$parent.fq}",
+ "q", "{!knn f=vector_byte topK=5}" + BYTE_QUERY_VECTOR,
+ "fl", "id",
+ "parent.fq", "parent_s:(a c)",
+ "allParents", "parent_s:[* TO *]"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/str[@name='id'][.='82']",
+ "//result/doc[2]/str[@name='id'][.='81']",
+ "//result/doc[3]/str[@name='id'][.='80']",
+ "//result/doc[4]/str[@name='id'][.='62']",
+ "//result/doc[5]/str[@name='id'][.='61']");
+ }
+
+ @Test
+ public void parentRetrievalFloat_knnChildren_shouldReturnKnnParents() {
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score",
+ "children.q", "{!knn f=vector topK=3 childrenOf=$allParents}" +
FLOAT_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]"),
+ "//*[@numFound='3']",
+ "//result/doc[1]/str[@name='id'][.='10']",
+ "//result/doc[2]/str[@name='id'][.='9']",
+ "//result/doc[3]/str[@name='id'][.='8']");
+ }
+
+ @Test
+ public void
parentRetrievalFloat_knnChildrenWithNoDiversifying_shouldReturnOneParent() {
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score",
+ "children.q", "{!knn f=vector topK=3}" + FLOAT_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]"),
+ "//*[@numFound='1']",
+ "//result/doc[1]/str[@name='id'][.='10']");
+ }
+
+ @Test
+ public void
parentRetrievalFloat_knnChildrenWithParentFilter_shouldReturnKnnParents() {
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score",
+ "children.q",
+ "{!knn f=vector topK=3 parents.preFilter=$someParents
childrenOf=$allParents}"
+ + FLOAT_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]",
+ "someParents", "parent_s:(a c)"),
+ "//*[@numFound='3']",
+ "//result/doc[1]/str[@name='id'][.='8']",
+ "//result/doc[2]/str[@name='id'][.='6']",
+ "//result/doc[3]/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void
+
parentRetrievalFloat_knnChildrenWithParentFilterAndChildrenFilter_shouldReturnKnnParents()
{
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score",
+ "children.q",
+ "{!knn f=vector topK=3 preFilter=child_s:m
parents.preFilter=$someParents childrenOf=$allParents}"
+ + FLOAT_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]",
+ "someParents", "parent_s:(a c)"),
+ "//*[@numFound='2']",
+ "//result/doc[1]/str[@name='id'][.='8']",
+ "//result/doc[2]/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void parentRetrievalByte_knnChildren_shouldReturnKnnParents() {
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score",
+ "children.q", "{!knn f=vector_byte topK=3 childrenOf=$allParents}"
+ BYTE_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]"),
+ "//*[@numFound='3']",
+ "//result/doc[1]/str[@name='id'][.='10']",
+ "//result/doc[2]/str[@name='id'][.='9']",
+ "//result/doc[3]/str[@name='id'][.='8']");
+ }
+
+ @Test
+ public void
parentRetrievalByte_knnChildrenWithParentFilter_shouldReturnKnnParents() {
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score",
+ "children.q",
+ "{!knn f=vector_byte topK=3 parents.preFilter=$someParents
childrenOf=$allParents}"
+ + BYTE_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]",
+ "someParents", "parent_s:(a c)"),
+ "//*[@numFound='3']",
+ "//result/doc[1]/str[@name='id'][.='8']",
+ "//result/doc[2]/str[@name='id'][.='6']",
+ "//result/doc[3]/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void
+
parentRetrievalByte_knnChildrenWithParentFilterAndChildrenFilter_shouldReturnKnnParents()
{
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score",
+ "children.q",
+ "{!knn f=vector_byte topK=3 preFilter=child_s:m
parents.preFilter=$someParents childrenOf=$allParents}"
+ + BYTE_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]",
+ "someParents", "parent_s:(a c)"),
+ "//*[@numFound='2']",
+ "//result/doc[1]/str[@name='id'][.='8']",
+ "//result/doc[2]/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void
+
parentRetrievalFloat_topKWithChildTransformerWithFilter_shouldUseOriginalChildTransformerFilter()
{
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score,vectors,vector,[child limit=2 fl=vector]",
+ "children.q",
+ "{!knn f=vector topK=3 parents.preFilter=$someParents
childrenOf=$allParents}"
+ + FLOAT_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]",
+ "someParents", "parent_s:(a c)"),
+ "//result[@numFound='3']",
+ "//result/doc[1]/str[@name='id'][.='8']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[1][.='10.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[2][.='1.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[3][.='1.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[4][.='1.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[1][.='9.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[2][.='1.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[3][.='1.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[4][.='1.0']",
+ "//result/doc[2]/str[@name='id'][.='6']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[1][.='16.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[2][.='1.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[3][.='1.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[4][.='1.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[1][.='15.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[2][.='1.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[3][.='1.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[4][.='1.0']",
+ "//result/doc[3]/str[@name='id'][.='2']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[1][.='28.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[2][.='1.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[3][.='1.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[4][.='1.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[1][.='27.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[2][.='1.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[3][.='1.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector']/float[4][.='1.0']");
+ }
+
+ @Test
+ public void
parentRetrievalFloat_topKWithChildTransformerWithFilter_shouldReturnBestChild()
{
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score,vectors,vector,[child fl=vector
childFilter=$children.q]",
+ "children.q",
+ "{!knn f=vector topK=3 parents.preFilter=$someParents
childrenOf=$allParents}"
+ + FLOAT_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]",
+ "someParents", "parent_s:(b c)"),
+ "//result[@numFound='3']",
+ "//result/doc[1]/str[@name='id'][.='8']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[1][.='8.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[2][.='1.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[3][.='1.0']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[4][.='1.0']",
+ "//result/doc[2]/str[@name='id'][.='7']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[1][.='11.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[2][.='1.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[3][.='1.0']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[4][.='1.0']",
+ "//result/doc[3]/str[@name='id'][.='2']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[1][.='26.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[2][.='1.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[3][.='1.0']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector']/float[4][.='1.0']");
+ }
+
+ @Test
+ public void
parentRetrievalByte_topKWithChildTransformer_shouldReturnAllChildren() {
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score,vectors,vector_byte,[child limit=2
fl=vector_byte]",
+ "children.q",
+ "{!knn f=vector_byte topK=3 parents.preFilter=$someParents
childrenOf=$allParents}"
+ + BYTE_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]",
+ "someParents", "parent_s:(b c)"),
+ "//result[@numFound='3']",
+ "//result/doc[1]/str[@name='id'][.='8']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[1][.='10']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[2][.='1']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[3][.='1']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[4][.='1']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[1][.='9']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[2][.='1']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[3][.='1']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[4][.='1']",
+ "//result/doc[2]/str[@name='id'][.='7']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[1][.='13']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[2][.='1']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[3][.='1']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[4][.='1']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[1][.='12']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[2][.='1']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[3][.='1']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[4][.='1']",
+ "//result/doc[3]/str[@name='id'][.='2']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[1][.='28']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[2][.='1']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[3][.='1']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[4][.='1']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[1][.='27']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[2][.='1']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[3][.='1']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[2]/arr[@name='vector_byte']/int[4][.='1']");
+ }
+
+ @Test
+ public void
parentRetrievalByte_topKWithChildTransformerWithFilter_shouldReturnBestChild() {
+ assertQ(
+ req(
+ "q", "{!parent which=$allParents score=max v=$children.q}",
+ "fl", "id,score,vectors,vector_byte,[child fl=vector_byte
childFilter=$children.q]",
+ "children.q",
+ "{!knn f=vector_byte topK=3 parents.preFilter=$someParents
childrenOf=$allParents}"
+ + BYTE_QUERY_VECTOR,
+ "allParents", "parent_s:[* TO *]",
+ "someParents", "parent_s:(b c)"),
+ "//result[@numFound='3']",
+ "//result/doc[1]/str[@name='id'][.='8']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[1][.='8']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[2][.='1']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[3][.='1']",
+
"//result/doc[1]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[4][.='1']",
+ "//result/doc[2]/str[@name='id'][.='7']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[1][.='11']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[2][.='1']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[3][.='1']",
+
"//result/doc[2]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[4][.='1']",
+ "//result/doc[3]/str[@name='id'][.='2']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[1][.='26']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[2][.='1']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[3][.='1']",
+
"//result/doc[3]/arr[@name='vectors'][1]/doc[1]/arr[@name='vector_byte']/int[4][.='1']");
+ }
+}
diff --git
a/solr/core/src/test/org/apache/solr/search/vector/KnnQParserChildTest.java
b/solr/core/src/test/org/apache/solr/search/vector/KnnQParserChildTest.java
new file mode 100644
index 00000000000..694db4c90b9
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/vector/KnnQParserChildTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.solr.search.vector;
+
+import java.util.List;
+import java.util.Random;
+import java.util.stream.Collectors;
+import org.apache.lucene.tests.util.TestUtil;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.SolrInputField;
+import org.junit.BeforeClass;
+
+public class KnnQParserChildTest extends SolrTestCaseJ4 {
+
+ private static final int MAX_TOP_K = 100;
+ private static final int MIN_NUM_PARENTS = MAX_TOP_K * 10;
+ private static final int MIN_NUM_KIDS_PER_PARENT = 5;
+
+ @BeforeClass
+ public static void prepareIndex() throws Exception {
+ /* vectorDimension="4" similarityFunction="cosine" */
+ initCore("solrconfig_codec.xml", "schema-densevector.xml");
+
+ final int numParents = atLeast(MIN_NUM_PARENTS);
+ for (int p = 0; p < numParents; p++) {
+ final String parentId = "parent-" + p;
+ final SolrInputDocument parent = doc(f("id", parentId), f("type_s",
"PARENT"));
+ final int numKids = atLeast(MIN_NUM_KIDS_PER_PARENT);
+ for (int k = 0; k < numKids; k++) {
+ final String kidId = parentId + "-kid-" + k;
+ final SolrInputDocument kid =
+ doc(f("id", kidId), f("parent_s", parentId), f("type_s", "KID"));
+
+ kid.addField("vector", randomFloatVector(random()));
+ kid.addField("vector_byte_encoding", randomByteVector(random()));
+
+ parent.addChildDocument(kid);
+ }
+ assertU(adoc(parent));
+ if (rarely(random())) {
+ assertU(commit());
+ }
+ }
+ assertU(commit());
+ }
+
+ /** Direct usage knn w/childOf to confim that a diverse set of child docs
are returned */
+ public void testDiverseKids() {
+ final int numIters = atLeast(100);
+ for (int iter = 0; iter < numIters; iter++) {
+ final String topK = "" + TestUtil.nextInt(random(), 2, MAX_TOP_K);
+
+ // check floats...
+ assertQ(
+ req(
+ "q",
+ "{!knn f=vector topK=$k childrenOf='type_s:PARENT'}"
+ + vecStr(randomFloatVector(random())),
+ "indent",
+ "true",
+ "fl",
+ "id,parent_s",
+ "_iter",
+ "" + iter,
+ "k",
+ topK,
+ "rows",
+ topK),
+ "*[count(//doc/str[@name='parent_s' and
not(following::str[@name='parent_s']/text() = text())])="
+ + topK
+ + "]",
+ "*[count(//doc/str[@name='id' and
not(following::str[@name='id']/text() = text())])="
+ + topK
+ + "]");
+
+ // check bytes...
+ assertQ(
+ req(
+ "q",
+ "{!knn f=vector_byte_encoding topK=$k
childrenOf='type_s:PARENT'}"
+ + vecStr(randomByteVector(random())),
+ "indent",
+ "true",
+ "fl",
+ "id,parent_s",
+ "_iter",
+ "" + iter,
+ "k",
+ topK,
+ "rows",
+ topK),
+ "*[count(//doc/str[@name='parent_s' and
not(following::str[@name='parent_s']/text() = text())])="
+ + topK
+ + "]",
+ "*[count(//doc/str[@name='id' and
not(following::str[@name='id']/text() = text())])="
+ + topK
+ + "]");
+ }
+ }
+
+ /** Sanity check that knn w/diversification works as expected when wrapped
in parent query */
+ public void testParentsOfDiverseKids() {
+
+ final int numIters = atLeast(100);
+ for (int iter = 0; iter < numIters; iter++) {
+ final String topK = "" + TestUtil.nextInt(random(), 2, MAX_TOP_K);
+
+ // check floats...
+ assertQ(
+ req(
+ "q",
+ "{!parent which='type_s:PARENT' score=max v=$knn}",
+ "knn",
+ "{!knn f=vector topK=$k childrenOf='type_s:PARENT'}"
+ + vecStr(randomFloatVector(random())),
+ "indent",
+ "true",
+ "fl",
+ "id",
+ "_iter",
+ "" + iter,
+ "k",
+ topK,
+ "rows",
+ topK),
+ "*[count(//doc/str[@name='id' and
not(following::str[@name='id']/text() = text())])="
+ + topK
+ + "]");
+
+ // check bytes...
+ assertQ(
+ req(
+ "q",
+ "{!parent which='type_s:PARENT' score=max v=$knn}",
+ "knn",
+ "{!knn f=vector_byte_encoding topK=$k
childrenOf='type_s:PARENT'}"
+ + vecStr(randomByteVector(random())),
+ "indent",
+ "true",
+ "fl",
+ "id",
+ "_iter",
+ "" + iter,
+ "k",
+ topK,
+ "rows",
+ topK),
+ "*[count(//doc/str[@name='id' and
not(following::str[@name='id']/text() = text())])="
+ + topK
+ + "]");
+ }
+ }
+
+ /** Format a vector as a string for use in queries */
+ protected static String vecStr(final List<? extends Number> vector) {
+ return "[" +
vector.stream().map(Object::toString).collect(Collectors.joining(",")) + "]";
+ }
+
+ /** Random vector of size 4 */
+ protected static List<Float> randomFloatVector(Random r) {
+ // we don't want nextFloat() because it's bound by -1:1
+ // but we also don't want NaN, or +/- Infinity (so we don't mess with
intBitsToFloat)
+ // we could be fancier to get *all* the possible "real" floats, but this
is good enough...
+
+ // Note: bias first vec entry to ensure we never have an all zero vector
(invalid w/cosine sim
+ // used in configs)
+ return List.of(
+ 1F + (r.nextFloat() * 10000F),
+ r.nextFloat() * 10000F,
+ r.nextFloat() * 10000F,
+ r.nextFloat() * 10000F);
+ }
+
+ /** Random vector of size 4 */
+ protected static List<Byte> randomByteVector(Random r) {
+ final byte[] byteBuff = new byte[4];
+ r.nextBytes(byteBuff);
+ // Note: bias first vec entry to ensure we never have an all zero vector
(invalid w/cosine sim
+ // used in configs)
+ return List.of(
+ (Byte) (byte) (byteBuff[0] + 1),
+ (Byte) byteBuff[1],
+ (Byte) byteBuff[1],
+ (Byte) byteBuff[1]);
+ }
+
+ /** Convenience method for building a SolrInputDocument */
+ protected static SolrInputDocument doc(SolrInputField... fields) {
+ SolrInputDocument d = new SolrInputDocument();
+ for (SolrInputField f : fields) {
+ d.put(f.getName(), f);
+ }
+ return d;
+ }
+
+ /** Convenience method for building a SolrInputField */
+ protected static SolrInputField f(String name, Object value) {
+ final SolrInputField f = new SolrInputField(name);
+ f.setValue(value);
+ return f;
+ }
+}
diff --git
a/solr/solr-ref-guide/modules/query-guide/pages/dense-vector-search.adoc
b/solr/solr-ref-guide/modules/query-guide/pages/dense-vector-search.adoc
index d05743879e8..7f9122fe8fa 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/dense-vector-search.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/dense-vector-search.adoc
@@ -350,7 +350,7 @@ Apache Solr provides three query parsers that work with
dense vector fields, tha
All parsers return scores for retrieved documents that are the approximate
distance to the target vector (defined by the similarityFunction configured at
indexing time) and both support "Pre-Filtering" the document graph to reduce
the number of candidate vectors evaluated (without needing to compute their
vector similarity distances).
-Common parameters for both query parsers are:
+Common parameters for all query parsers are:
`f`::
+
@@ -516,6 +516,46 @@ Here is an example of a `knn` search using a
`filteredSearchThreshold`:
[source,text]
?q={!knn f=vector topK=10 filteredSearchThreshold=60}[1.0, 2.0, 3.0, 4.0]
+`parents.preFilter`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
+This parameter is meant to be a filter query on parent document metadata.
+The knn search returns the top-k nearest children documents that satify the
filter on the parent.
++
+Only one child per distinct parent is returned.
+
+Here is an example of a `knn` search using a `parents.preFilter`:
+
+[source,text]
+?q={!knn f=vector topK=3 parents.preFilter=$someParents
childrenOf=$allParents}[1.0, 2.0, 3.0, 4.0]
+&allParents=*:* -_nest_path_:*
+&someParents=color_s:RED
+
+The search results retrieved are the k=3 nearest documents to the vector in
input `[1.0, 2.0, 3.0, 4.0]`, each of them with a different parent. Only the
documents with a parent that satisfy the 'color_s:RED' condition are considered
candidates for the ANN search.
+
+`childrenOf`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|Mandatory if using 'parents.preFilter' parameter|Default: none
+|===
++
+A query that matches the set of ALL possible parent documents.
+It's required to work with the 'parents.preFilter' parameter.
+
+
+Here is an example of a `knn` search using a `childrenOf`:
+
+[source,text]
+?q={!knn f=vector topK=3 childrenOf=$allParents}[1.0, 2.0, 3.0, 4.0]
+&allParents=*:* -_nest_path_:*
+
+The search results retrieved are the k=3 nearest documents to the vector in
input `[1.0, 2.0, 3.0, 4.0]`, each of them with a different parent. The
'childrenOf' parameter must return all valid parents to guarantee the correct
functioning of the query.
+
=== knn_text_to_vector Query Parser
The `knn_text_to_vector` query parser encode a textual query to a vector using
a dedicated Large Language Model(fine tuned for the task of encoding text to
vector for sentence similarity) and matches k-nearest neighbours documents to
such query vector.
diff --git
a/solr/solr-ref-guide/modules/query-guide/pages/searching-nested-documents.adoc
b/solr/solr-ref-guide/modules/query-guide/pages/searching-nested-documents.adoc
index 7f85d8330bf..83b2e35f54c 100644
---
a/solr/solr-ref-guide/modules/query-guide/pages/searching-nested-documents.adoc
+++
b/solr/solr-ref-guide/modules/query-guide/pages/searching-nested-documents.adoc
@@ -296,3 +296,43 @@ $ curl 'http://localhost:8983/solr/gettingstarted/select'
-d 'omitHeader=true' -
"_version_":1676585794196733952}]}]
}}
----
+
+=== Nested Vectors search through Block Join Query Parsers and Child Doc
Transformer
+
+
+
+When dealing with vector search a possible use case involves having multiple
vectors for a single document.
+
+This in Solr can be implemented with the block join and nested documents.
+Each nested document has a vector field (among other metadata)
+
+This may happen, among other use cases, because you chunked the original text
into paragraphs, each of them modeled as a nested document with the paragraph
text and the vector representation.
+
+You can run knn vector search on children documents (with potential
prefiltering on children and/or parents metadata) and retrieve top-K parents.
+
+N.B. Solr ensures that the knn search for children keeps track of parent
metadata filtering, guaranteeing top-k parents retrieval
+
+An example:
+[source,text]
+?q={!parent which=$allParents score=max v=$children.q}&
+children.q={!knn f=vector topK=3 parents.preFilter=$someParents
childrenOf=$allParents}[1.0, 2.0, 3.0, 4.0]&
+allParents=*:* -_nest_path_:*&
+someParents=color_s:RED&
+fl=id,score,vectors,vector,[child fl=vector childFilter=$children.q]
+
+The search results retrieved are the top k=3 parents of the nearest children
to the vector in input `[1.0, 2.0, 3.0, 4.0]`, ranked by the
`similarityFunction` configured at indexing time.
+
+Let's decompose the query to better explain it:
+[source,text]
+?q={!parent which=$allParents score=max v=$children.q}
+
+This query returns the parent solr documents using the block join parent query
parser on a query that filters on the children documents ('children.q'). For
each child retrieved, the parent is returned.
+
+[source,text]
+children.q={!knn f=vector topK=3 parents.preFilter=$someParents
childrenOf=$allParents}[1.0, 2.0, 3.0, 4.0]
+
+This query is a knn vector search on the children documents.
+Specifically it retrieves the top k=3 children documents, filtered by
'someParent' metadata.
+This query ensures only one child per parent is retrieved.
+
+----