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.
+
+----


Reply via email to