Skip to content

Commit a8478be

Browse files
Copilotdsmiley
andcommitted
SOLR-14687: Add childPath support to {!child} parser; narrows returned children to exact path
Co-authored-by: dsmiley <377295+dsmiley@users.noreply.github.com>
1 parent 58e660b commit a8478be

File tree

4 files changed

+63
-9
lines changed

4 files changed

+63
-9
lines changed

solr/core/src/java/org/apache/solr/search/join/BlockJoinChildQParser.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,23 @@ protected Query noClausesQuery() throws SyntaxError {
7474
* ff=(*:* -_nest_path_:*)
7575
* vv=(+p_title:dad -_nest_path_:*)</pre>
7676
*
77+
* <p>The optional {@code childPath} localparam narrows the returned children to docs at exactly
78+
* {@code parentPath/childPath}.
79+
*
7780
* @param parentPath the normalized parent path (starts with "/", no trailing slash except for
7881
* root "/")
7982
*/
8083
@Override
8184
protected Query parseUsingParentPath(String parentPath) throws SyntaxError {
82-
if (localParams.get(CHILD_PATH_PARAM) != null) {
83-
throw new SolrException(
84-
SolrException.ErrorCode.BAD_REQUEST,
85-
CHILD_PATH_PARAM + " is not supported by the {!child} parser");
85+
String childPath = localParams.get(CHILD_PATH_PARAM);
86+
if (childPath != null) {
87+
if (childPath.startsWith("/")) {
88+
throw new SolrException(
89+
SolrException.ErrorCode.BAD_REQUEST, CHILD_PATH_PARAM + " must not start with '/'");
90+
}
91+
if (childPath.isEmpty()) {
92+
childPath = null; // treat empty as not specified
93+
}
8694
}
8795
// allParents filter: (*:* -{!prefix f="_nest_path_" v="<parentPath>/"})
8896
// For root: (*:* -_nest_path_:*)
@@ -97,14 +105,22 @@ protected Query parseUsingParentPath(String parentPath) throws SyntaxError {
97105
.add(new MatchAllDocsQuery(), Occur.MUST)
98106
.add(allParentsFilter, Occur.MUST_NOT)
99107
.build();
100-
return new BitSetProducerQuery(getBitSetProducer(notParents));
108+
Query result = new BitSetProducerQuery(getBitSetProducer(notParents));
109+
if (childPath != null) {
110+
result = buildChildQueryWithPathConstraint(parentPath, childPath, result);
111+
}
112+
return result;
101113
}
102114

103115
// constrain the parent query to only match docs at exactly parentPath
104116
// (+<original_parent> +{!field f="_nest_path_" v="<parentPath>"})
105117
// For root: (+<original_parent> -_nest_path_:*)
106118
Query constrainedParentQuery = wrapWithParentPathConstraint(parentPath, parsedParentQuery);
107119

108-
return createQuery(allParentsFilter, constrainedParentQuery, null);
120+
Query joinQuery = createQuery(allParentsFilter, constrainedParentQuery, null);
121+
if (childPath != null) {
122+
return buildChildQueryWithPathConstraint(parentPath, childPath, joinQuery);
123+
}
124+
return joinQuery;
109125
}
110126
}

solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ protected static Query wrapWithParentPathConstraint(String parentPath, Query que
232232
* {@code _nest_path_} is a "child". If {@code childPath} is non-null, the constraint is an exact
233233
* term match on {@code parentPath/childPath} instead of a prefix query.
234234
*/
235-
private static Query buildChildQueryWithPathConstraint(
235+
protected static Query buildChildQueryWithPathConstraint(
236236
String parentPath, String childPath, Query childQuery) {
237237
final Query nestPathConstraint;
238238
if (childPath != null) {

solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,43 @@ public void checkParentAndChildQueriesOfEachDocument() {
489489
"indent",
490490
"true"),
491491
"//result/@numFound=0");
492+
// childPath for {!child}: constrain returned children to exactly doc_path
493+
assertQ(
494+
req(
495+
params(
496+
"q",
497+
"{!child parentPath='"
498+
+ directParentPath
499+
+ "' childPath='"
500+
+ childSegment
501+
+ "'}id:"
502+
+ candAncestorId),
503+
"_trace_child_childPath_tested",
504+
directParentPath + "/" + childSegment,
505+
"rows",
506+
"9999",
507+
"fl",
508+
"id",
509+
"indent",
510+
"true"),
511+
"count(//doc)>=1",
512+
"//doc/str[@name='id'][.='" + descendentId + "']");
513+
// a childPath that doesn't match should return 0 results
514+
assertQ(
515+
req(
516+
params(
517+
"q",
518+
"{!child parentPath='"
519+
+ directParentPath
520+
+ "' childPath='xxx_yyy'}id:"
521+
+ candAncestorId),
522+
"_trace_child_childPath_tested",
523+
directParentPath + "/xxx_yyy",
524+
"fl",
525+
"id",
526+
"indent",
527+
"true"),
528+
"//result/@numFound=0");
492529
break;
493530
}
494531
}

solr/solr-ref-guide/modules/query-guide/pages/block-join-query-parser.adoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,17 @@ Key points about `parentPath`:
6565
* Use `parentPath="/"` to treat root-level documents as the parents.
6666
* A trailing `/` is stripped automatically (e.g., `"/skus/"` is treated as `"/skus"`).
6767
* `parentPath` and `of` are mutually exclusive; specifying both returns a `400 Bad Request` error.
68+
* Optionally, use `childPath` to narrow the returned children to docs at exactly `parentPath/childPath`. Without `childPath`, all descendants of parents at `parentPath` are returned.
6869

6970
For example, using the deeply nested documents described in xref:searching-nested-documents.adoc[], the following query returns all children of root-level product documents that match a description query:
7071

7172
[source,text]
7273
q={!child parentPath="/"}description_t:staplers
7374

74-
To return only children of `skus` documents with a price under 50:
75+
To return only `skus` children of root documents matching a description query (excluding other child types):
7576

7677
[source,text]
77-
q={!child parentPath="/skus"}price_i:[* TO 50]
78+
q={!child parentPath="/" childPath="skus"}description_t:staplers
7879

7980
=== Using the `of` Parameter
8081

0 commit comments

Comments
 (0)