diff --git a/changelog/unreleased/SOLR-14687-parentPath-param.yml b/changelog/unreleased/SOLR-14687-parentPath-param.yml new file mode 100644 index 000000000000..24e4eadf9340 --- /dev/null +++ b/changelog/unreleased/SOLR-14687-parentPath-param.yml @@ -0,0 +1,9 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc +title: The {!parent} and {!child} query parsers now support a parentPath local param that automatically derives the correct parent filter using the _nest_path_ field, making nested document queries easier to write correctly. +type: added # added, changed, fixed, deprecated, removed, dependency_update, security, other +authors: + - name: David Smiley + - name: hossman +links: + - name: SOLR-14687 + url: https://issues.apache.org/jira/browse/SOLR-14687 diff --git a/solr/core/src/java/org/apache/solr/search/join/BlockJoinChildQParser.java b/solr/core/src/java/org/apache/solr/search/join/BlockJoinChildQParser.java index bb6c80db07a8..637bddd9cd9b 100644 --- a/solr/core/src/java/org/apache/solr/search/join/BlockJoinChildQParser.java +++ b/solr/core/src/java/org/apache/solr/search/join/BlockJoinChildQParser.java @@ -25,6 +25,7 @@ import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.search.SyntaxError; +/** Matches child documents based on parent doc criteria. */ public class BlockJoinChildQParser extends BlockJoinParentQParser { public BlockJoinChildQParser( @@ -33,8 +34,8 @@ public BlockJoinChildQParser( } @Override - protected Query createQuery(Query parentListQuery, Query query, String scoreMode) { - return new ToChildBlockJoinQuery(query, getBitSetProducer(parentListQuery)); + protected Query createQuery(Query parentListQuery, Query fromQuery, String scoreMode) { + return new ToChildBlockJoinQuery(fromQuery, getBitSetProducer(parentListQuery)); } @Override @@ -52,4 +53,58 @@ protected Query noClausesQuery() throws SyntaxError { .build(); return new BitSetProducerQuery(getBitSetProducer(notParents)); } + + /** + * Parses the query using the {@code parentPath} local-param for the child parser. + * + *

For the {@code child} parser with {@code parentPath="/a/b/c"}: + * + *

NEW: q={!child parentPath="/a/b/c"}p_title:dad
+   *
+   * OLD: q={!child of=$ff v=$vv}
+   *      ff=(*:* -{!prefix f="_nest_path_" v="/a/b/c/"})
+   *      vv=(+p_title:dad +{!field f="_nest_path_" v="/a/b/c"})
+ * + *

For {@code parentPath="/"}: + * + *

NEW: q={!child parentPath="/"}p_title:dad
+   *
+   * OLD: q={!child of=$ff v=$vv}
+   *      ff=(*:* -_nest_path_:*)
+   *      vv=(+p_title:dad -_nest_path_:*)
+ * + *

The optional {@code childPath} local-param narrows the returned children to docs at exactly + * {@code parentPath/childPath}. + * + * @param parentPath the normalized parent path (starts with "/", no trailing slash except for + * root "/") + * @param childPath optional path constraining the children relative to parentPath + */ + @Override + protected Query parseUsingParentPath(String parentPath, String childPath) throws SyntaxError { + + final BooleanQuery parsedParentQuery = parseImpl(); + + if (parsedParentQuery.clauses().isEmpty()) { // i.e. match all parents + // no block-join needed; just filter to certain children + return wrapWithChildPathConstraint(parentPath, childPath, new MatchAllDocsQuery()); + } + + // allParents filter: (*:* -{!prefix f="_nest_path_" v="/"}) + // For root: (*:* -_nest_path_:*) + final Query allParentsFilter = buildAllParentsFilterFromPath(parentPath); + + // constrain the parent query to only match docs at exactly parentPath + // (+ +{!field f="_nest_path_" v=""}) + // For root: (+ -_nest_path_:*) + Query constrainedParentQuery = wrapWithParentPathConstraint(parentPath, parsedParentQuery); + + Query joinQuery = createQuery(allParentsFilter, constrainedParentQuery, null); + // matches all children of matching parents + if (childPath == null) { + return joinQuery; + } + // need to constrain to certain children + return wrapWithChildPathConstraint(parentPath, childPath, joinQuery); + } } diff --git a/solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java b/solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java index 1d73bbd78aa7..7df1257b9154 100644 --- a/solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java +++ b/solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java @@ -20,13 +20,21 @@ import java.io.UncheckedIOException; import java.util.Objects; import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.ConstantScoreScorer; import org.apache.lucene.search.ConstantScoreWeight; import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.FieldExistsQuery; import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.QueryVisitor; import org.apache.lucene.search.ScorerSupplier; +import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.Weight; import org.apache.lucene.search.join.BitSetProducer; import org.apache.lucene.search.join.QueryBitSetProducer; @@ -34,18 +42,42 @@ import org.apache.lucene.search.join.ToParentBlockJoinQuery; import org.apache.lucene.util.BitSet; import org.apache.lucene.util.BitSetIterator; +import org.apache.solr.common.SolrException; import org.apache.solr.common.params.SolrParams; import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.schema.IndexSchema; import org.apache.solr.search.ExtendedQueryBase; import org.apache.solr.search.QParser; import org.apache.solr.search.SolrCache; import org.apache.solr.search.SyntaxError; import org.apache.solr.util.SolrDefaultScorerSupplier; +/** Matches parent documents based on child doc criteria. */ public class BlockJoinParentQParser extends FiltersQParser { /** implementation detail subject to change */ public static final String CACHE_NAME = "perSegFilter"; + /** + * Optional local-param that, when specified, makes this parser natively aware of the {@link + * IndexSchema#NEST_PATH_FIELD_NAME} field to automatically derive the parent filter (the {@code + * which} param). The value must be an absolute path starting with {@code /} using {@code /} as + * separator, e.g. {@code /} for root-level parents or {@code /skus} for parents nested at that + * path. When specified, the {@code which} param must not also be specified. + * + * @see SOLR-14687 + */ + public static final String PARENT_PATH_PARAM = "parentPath"; + + /** + * Optional local-param, only valid together with {@link #PARENT_PATH_PARAM} on the {@code parent} + * parser. When specified, the subordinate (child) query is constrained to docs at exactly the + * path formed by concatenating {@code parentPath + "/" + childPath}, instead of the default + * behavior of matching all descendants. For example, {@code parentPath="/skus" + * childPath="manuals"} constrains children to docs whose {@code _nest_path_} is exactly {@code + * /skus/manuals}. + */ + public static final String CHILD_PATH_PARAM = "childPath"; + protected String getParentFilterLocalParamName() { return "which"; } @@ -60,6 +92,175 @@ protected String getFiltersParamName() { super(qstr, localParams, params, req); } + @Override + public Query parse() throws SyntaxError { + String parentPath = localParams.get(PARENT_PATH_PARAM); + if (parentPath != null) { + if (localParams.get(getParentFilterLocalParamName()) != null) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, + PARENT_PATH_PARAM + + " and " + + getParentFilterLocalParamName() + + " local params are mutually exclusive"); + } + if (!parentPath.startsWith("/")) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, PARENT_PATH_PARAM + " must start with '/'"); + } + // strip trailing slash (except for root "/") + if (parentPath.length() > 1 && parentPath.endsWith("/")) { + parentPath = parentPath.substring(0, parentPath.length() - 1); + } + + String childPath = localParams.get(CHILD_PATH_PARAM); + if (childPath != null) { + if (childPath.startsWith("/")) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, CHILD_PATH_PARAM + " must not start with '/'"); + } + if (childPath.isEmpty()) { + childPath = null; // treat empty as not specified + } + } + return parseUsingParentPath(parentPath, childPath); + } + + // NO parentPath; use classic/advanced/DIY code path: + + if (localParams.get(CHILD_PATH_PARAM) != null) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, CHILD_PATH_PARAM + " requires " + PARENT_PATH_PARAM); + } + return super.parse(); + } + + /** + * Parses the query using the {@code parentPath} localparam to automatically derive the parent + * filter and child query constraints from {@link IndexSchema#NEST_PATH_FIELD_NAME}. + * + *

For the {@code parent} parser with {@code parentPath="/a/b/c"}: + * + *

NEW: q={!parent parentPath="/a/b/c"}c_title:son
+   *
+   * OLD: q=(+{!field f="_nest_path_" v="/a/b/c"} +{!parent which=$ff v=$vv})
+   *      ff=(*:* -{!prefix f="_nest_path_" v="/a/b/c/"})
+   *      vv=(+c_title:son +{!prefix f="_nest_path_" v="/a/b/c/"})
+ * + *

For {@code parentPath="/"}: + * + *

NEW: q={!parent parentPath="/"}c_title:son
+   *
+   * OLD: q=(+(*:* -_nest_path_:*) +{!parent which=$ff v=$vv})
+   *      ff=(*:* -_nest_path_:*)
+   *      vv=(+c_title:son +_nest_path_:*)
+ * + * @param parentPath the normalized parent path (starts with "/", no trailing slash except for + * root "/") + * @param childPath optional path constraining the children relative to parentPath + */ + protected Query parseUsingParentPath(String parentPath, String childPath) throws SyntaxError { + final BooleanQuery parsedChildQuery = parseImpl(); + + if (parsedChildQuery.clauses().isEmpty()) { // i.e. all children + // no block-join needed; just return all "parent" docs at this level + return wrapWithParentPathConstraint(parentPath, new MatchAllDocsQuery()); + } + + // allParents filter: (*:* -{!prefix f="_nest_path_" v="/"}) + // For root: (*:* -_nest_path_:*) + final Query allParentsFilter = buildAllParentsFilterFromPath(parentPath); + + // constrain child query: (+ +{!prefix f="_nest_path_" v="/"}) + // For root: (+ +_nest_path_:*) + // If childPath specified: (+ +{!term f="_nest_path_" + // v="/"}) + final Query constrainedChildQuery = + wrapWithChildPathConstraint(parentPath, childPath, parsedChildQuery); + + final String scoreMode = localParams.get("score", ScoreMode.None.name()); + final Query parentJoinQuery = createQuery(allParentsFilter, constrainedChildQuery, scoreMode); + + // wrap result: (+ +{!field f="_nest_path_" v=""}) + // For root: (+ -_nest_path_:*) + return wrapWithParentPathConstraint(parentPath, parentJoinQuery); + } + + /** + * Builds the "all parents" filter query from the given {@code parentPath}. This query matches all + * documents that are NOT strictly below (nested inside) the given path. This includes: + * + *
    + *
  • documents without any {@code _nest_path_} (root-level, non-nested docs) + *
  • documents at the same level as {@code parentPath} (i.e. with exactly that path) + *
  • documents at levels above {@code parentPath} + *
  • documents at completely orthogonal paths (e.g. {@code /x/y/z} when parentPath is {@code + * /a/b/c}) + *
+ * + *

Equivalent to: {@code (*:* -{!prefix f="_nest_path_" v="/"})} For root ({@code + * /}): {@code (*:* -_nest_path_:*)} + */ + protected static Query buildAllParentsFilterFromPath(String parentPath) { + final Query excludeQuery; + if (parentPath.equals("/")) { + excludeQuery = new FieldExistsQuery(IndexSchema.NEST_PATH_FIELD_NAME); + } else { + excludeQuery = new PrefixQuery(new Term(IndexSchema.NEST_PATH_FIELD_NAME, parentPath + "/")); + } + return new BooleanQuery.Builder() + .add(new MatchAllDocsQuery(), Occur.MUST) + .add(excludeQuery, Occur.MUST_NOT) + .build(); + } + + /** + * Wraps the given query with a constraint ensuring only docs at exactly {@code parentPath} are + * matched. + */ + protected static Query wrapWithParentPathConstraint(String parentPath, Query query) { + final BooleanQuery.Builder builder = new BooleanQuery.Builder().add(query, Occur.MUST); + if (parentPath.equals("/")) { + builder.add(new FieldExistsQuery(IndexSchema.NEST_PATH_FIELD_NAME), Occur.MUST_NOT); + } else { + final Query constraint = + new TermQuery(new Term(IndexSchema.NEST_PATH_FIELD_NAME, parentPath)); + if (query instanceof MatchAllDocsQuery) { + return new ConstantScoreQuery(constraint); + } + builder.add(constraint, Occur.FILTER); + } + return builder.build(); + } + + /** + * Wraps the sub-query with a constraint ensuring only docs that are descendants of {@code + * parentPath} are matched. If {@code childPath} is non-null, further narrows to docs at exactly + * {@code parentPath/childPath}. + */ + protected static Query wrapWithChildPathConstraint( + String parentPath, String childPath, Query subQuery) { + final Query nestPathConstraint; + if (childPath != null) { + String effectiveChildPath = + parentPath.equals("/") ? "/" + childPath : parentPath + "/" + childPath; + nestPathConstraint = + new TermQuery(new Term(IndexSchema.NEST_PATH_FIELD_NAME, effectiveChildPath)); + } else if (parentPath.equals("/")) { + nestPathConstraint = new FieldExistsQuery(IndexSchema.NEST_PATH_FIELD_NAME); + } else { + nestPathConstraint = + new PrefixQuery(new Term(IndexSchema.NEST_PATH_FIELD_NAME, parentPath + "/")); + } + if (subQuery instanceof MatchAllDocsQuery) { + return new ConstantScoreQuery(nestPathConstraint); + } + return new BooleanQuery.Builder() + .add(subQuery, Occur.MUST) + .add(nestPathConstraint, Occur.FILTER) + .build(); + } + protected Query parseParentFilter() throws SyntaxError { String filter = localParams.get(getParentFilterLocalParamName()); QParser parentParser = subQuery(filter, null); @@ -76,19 +277,33 @@ protected Query wrapSubordinateClause(Query subordinate) throws SyntaxError { @Override protected Query noClausesQuery() throws SyntaxError { + assert false : "dead code"; return new BitSetProducerQuery(getBitSetProducer(parseParentFilter())); } - protected Query createQuery(final Query parentList, Query query, String scoreMode) + /** + * Create the block-join query, the core Query of the QParser. + * + * @param parentList the "parent" query. The result will internally be cached. + * @param fromQuery source/from query. For {!parent}, this is a child, otherwise it's a parent + * @param scoreMode see {@link ScoreMode} + * @return non-null + * @throws SyntaxError Only if scoreMode doesn't parse + */ + protected Query createQuery(final Query parentList, Query fromQuery, String scoreMode) throws SyntaxError { return new AllParentsAware( - query, getBitSetProducer(parentList), ScoreModeParser.parse(scoreMode), parentList); + fromQuery, getBitSetProducer(parentList), ScoreModeParser.parse(scoreMode), parentList); } BitSetProducer getBitSetProducer(Query query) { return getCachedBitSetProducer(req, query); } + /** + * Returns a Lucene {@link BitSetProducer}, typically cached by query. Note that BSP itself + * internally caches a per-segment {@link BitSet}. + */ public static BitSetProducer getCachedBitSetProducer( final SolrQueryRequest request, Query query) { @SuppressWarnings("unchecked") @@ -105,6 +320,7 @@ public static BitSetProducer getCachedBitSetProducer( } } + /** A {@link ToParentBlockJoinQuery} exposing the query underlying the {@link BitSetProducer}. */ static final class AllParentsAware extends ToParentBlockJoinQuery { private final Query parentQuery; diff --git a/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java b/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java index c91cdf272578..b89a7c2974f4 100644 --- a/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java +++ b/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java @@ -439,6 +439,97 @@ public void checkParentAndChildQueriesOfEachDocument() { "//result/@numFound=1", "//doc/str[@name='id'][.='" + ancestorId + "']"); + // additionally test childPath: find the immediate parent of descendentId and use + // childPath to constrain to that exact child path level + final String directParentPath = + doc_path.contains("/") + ? (doc_path.lastIndexOf("/") == 0 + ? "/" + : doc_path.substring(0, doc_path.lastIndexOf("/"))) + : "/"; + final String childSegment = doc_path.substring(doc_path.lastIndexOf("/") + 1); + // find the ancestor ID whose path is directParentPath + for (Object candAncestorId : allAncestorIds) { + final String candPath = + allDocs.get(candAncestorId.toString()).getFieldValue("test_path_s").toString(); + if (candPath.equals(directParentPath)) { + // childPath constrains the child query to exactly doc_path, so we should find + // the direct parent + assertQ( + req( + params( + "q", + "{!parent parentPath='" + + directParentPath + + "' childPath='" + + childSegment + + "'}id:" + + descendentId), + "_trace_childPath_tested", + directParentPath + "/" + childSegment, + "fl", + "id", + "indent", + "true"), + "//result/@numFound=1", + "//doc/str[@name='id'][.='" + candAncestorId + "']"); + // a childPath that doesn't match descendentId's path should return 0 results + assertQ( + req( + params( + "q", + "{!parent parentPath='" + + directParentPath + + "' childPath='xxx_yyy'}id:" + + descendentId), + "_trace_childPath_tested", + directParentPath + "/xxx_yyy", + "fl", + "id", + "indent", + "true"), + "//result/@numFound=0"); + // childPath for {!child}: constrain returned children to exactly doc_path + assertQ( + req( + params( + "q", + "{!child parentPath='" + + directParentPath + + "' childPath='" + + childSegment + + "'}id:" + + candAncestorId), + "_trace_child_childPath_tested", + directParentPath + "/" + childSegment, + "rows", + "9999", + "fl", + "id", + "indent", + "true"), + "count(//doc)>=1", + "//doc/str[@name='id'][.='" + descendentId + "']"); + // a childPath that doesn't match should return 0 results + assertQ( + req( + params( + "q", + "{!child parentPath='" + + directParentPath + + "' childPath='xxx_yyy'}id:" + + candAncestorId), + "_trace_child_childPath_tested", + directParentPath + "/xxx_yyy", + "fl", + "id", + "indent", + "true"), + "//result/@numFound=0"); + break; + } + } + // meanwhile, a 'child' query wrapped around a query for the ancestorId, using the // ancestor_path, should match all of its descendents (for simplicity we'll check just // the numFound and the 'descendentId' we started with) @@ -545,7 +636,14 @@ public int recursiveCheckParentQueryOfAllChildren(List parent_path) { */ private SolrParams parentQueryMaker(String parent_path, String inner_child_query) { assertValidPathSyntax(parent_path); - final boolean verbose = random().nextBoolean(); + final int variant = random().nextInt(3); + + if (variant == 2) { + // new parentPath sugar + return params("q", "{!parent parentPath='" + parent_path + "'}" + inner_child_query); + } // else old-style with explicit which/of... + + final boolean verbose = variant == 1; if (parent_path.equals("/")) { if (verbose) { @@ -633,7 +731,14 @@ public int recursiveCheckChildQueryOfAllParents(List parent_path) { */ private SolrParams childQueryMaker(String parent_path, String inner_parent_query) { assertValidPathSyntax(parent_path); - final boolean verbose = random().nextBoolean(); + final int variant = random().nextInt(3); + + if (variant == 2) { + // new parentPath sugar + return params("q", "{!child parentPath='" + parent_path + "'}" + inner_parent_query); + } // else old-style with explicit which/of... + + final boolean verbose = variant == 1; if (parent_path.equals("/")) { if (verbose) { diff --git a/solr/solr-ref-guide/modules/query-guide/pages/block-join-query-parser.adoc b/solr/solr-ref-guide/modules/query-guide/pages/block-join-query-parser.adoc index 2eed97216392..0eb7bcc39c7f 100644 --- a/solr/solr-ref-guide/modules/query-guide/pages/block-join-query-parser.adoc +++ b/solr/solr-ref-guide/modules/query-guide/pages/block-join-query-parser.adoc @@ -51,10 +51,40 @@ The example usage of the query parsers below assumes the following documents hav This parser wraps a query that matches some parent documents and returns the children of those documents. -The syntax for this parser is: `q={!child of=}`. +=== Using `parentPath` -* The inner subordinate query string (`someParents`) must be a query that will match some parent documents -* The `of` parameter must be a query string to use as a <> -- typically a query that matches the set of all possible parent documents +If your schema supports xref:indexing-guide:indexing-nested-documents.adoc[nested documents], you _should_ specify `parentPath`. +Specify the path at which the parent documents live: + +[source,text] +q={!child parentPath=} + +Key points about `parentPath`: + +* Must start with `/`. +* Use `parentPath="/"` to treat root-level documents as the parents. +* A trailing `/` is stripped automatically (e.g., `"/skus/"` is treated as `"/skus"`). +* `parentPath` and `of` are mutually exclusive; specifying both returns a `400 Bad Request` error. +* Optionally, use `childPath` to narrow the returned children to docs at exactly `parentPath/childPath`. Without `childPath`, all descendants of parents at `parentPath` are returned. + +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: + +[source,text] +q={!child parentPath="/"}description_t:staplers + +To return only `skus` children of root documents matching a description query (excluding other child types): + +[source,text] +q={!child parentPath="/" childPath="skus"}description_t:staplers + +=== Using the `of` Parameter + +This approach is used with anonymous child documents (schemas without `_nest_path_`). +It is more verbose and has some <>. +The syntax is: `q={!child of=}`. + +* The inner subordinate query string (`someParents`) must be a query that will match some parent documents. +* The `of` parameter must be a query string to use as a <> -- typically a query that matches the set of all possible parent documents. The resulting query will match all documents which do _not_ match the `` query and are children (or descendents) of the documents matched by ``. @@ -111,10 +141,40 @@ More precisely, `q={!child of=}` is equivalent to `q=\*:* -}`. +=== Using `parentPath` + +If your schema supports xref:indexing-guide:indexing-nested-documents.adoc[nested documents], you _should_ specify `parentPath`. +Specify the path at which the parent documents live: + +[source,text] +q={!parent parentPath=} + +Key points about `parentPath`: + +* Must start with `/`. +* Use `parentPath="/"` to treat root-level documents as the parents. +* A trailing `/` is stripped automatically (e.g., `"/skus/"` is treated as `"/skus"`). +* `parentPath` and `which` are mutually exclusive; specifying both returns a `400 Bad Request` error. +* Optionally, use `childPath` to constrain the child query to docs at exactly `parentPath/childPath`. Without `childPath`, all descendants of `parentPath` are eligible as children. + +For example, using the deeply nested documents described in xref:searching-nested-documents.adoc[], the following query returns the root-level product documents that are ancestors of manuals with exactly one page: + +[source,text] +q={!parent parentPath="/"}pages_i:1 + +To instead return the `skus` that are ancestors of one-page _manuals_ (only manuals, not other sku children): + +[source,text] +q={!parent parentPath="/skus" childPath="manuals"}pages_i:1 + +=== Using the `which` Parameter + +This approach is used with anonymous child documents (schemas without `_nest_path_`). +It is more verbose and has some <>. +The syntax is: `q={!parent which=}`. -* The inner subordinate query string (`someChildren`) must be a query that will match some child documents -* The `which` parameter must be a query string to use as a <> -- typically a query that matches the set of all possible parent documents +* The inner subordinate query string (`someChildren`) must be a query that will match some child documents. +* The `which` parameter must be a query string to use as a <> -- typically a query that matches the set of all possible parent documents. The resulting query will match all documents which _do_ match the `` query and are parents (or ancestors) of the documents matched by ``. 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 83b2e35f54cd..15979f8ac320 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 @@ -108,11 +108,11 @@ $ curl 'http://localhost:8983/solr/gettingstarted/select?omitHeader=true&q=descr The `{!child}` query parser can be used to search for the _descendent_ documents of parent documents matching a wrapped query. For a detailed explanation of this parser, see the section xref:block-join-query-parser.adoc#block-join-children-query-parser[Block Join Children Query Parser]. -Let's consider again the `description_t:staplers` query used above -- if we wrap that query in a `{!child}` query parser then instead of "matching" & returning the product level documents, we instead match all of the _descendent_ child documents of the original query: +Let's consider again the `description_t:staplers` query used above -- if we wrap that query in a `{!child}` query parser with `parentPath="/"` then instead of "matching" & returning the product level documents, we instead match all of the _descendent_ child documents of the original query: [source,text] ---- -$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' -d 'q={!child of="*:* -_nest_path_:*"}description_t:staplers' +$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' --data-urlencode 'q={!child parentPath="/"}description_t:staplers' { "response":{"numFound":5,"start":0,"maxScore":0.30136836,"numFoundExact":true,"docs":[ { @@ -146,14 +146,14 @@ $ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' - }} ---- -In this example we've used `\*:* -\_nest_path_:*` as our xref:block-join-query-parser.adoc#block-mask[`of` parameter] to indicate we want to consider all documents which don't have a nest path -- i.e., all "root" level document -- as the set of possible parents. +In this example `parentPath="/"` indicates we want to consider all root-level documents as the set of possible parents. -By changing the `of` parameter to match ancestors at specific `\_nest_path_` levels, we can narrow down the list of children we return. -In the query below, we search for all descendants of `skus` (using an `of` parameter that identifies all documents that do _not_ have a `\_nest_path_` with the prefix `/skus/*`) with a `price_i` less than `50`: +By changing the `parentPath` to a specific `_nest_path_` level, we can narrow down the list of children we return. +In the query below, we search for all children of `skus` with a `price_i` less than `50`: [source,text] ---- -$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' --data-urlencode 'q={!child of="*:* -_nest_path_:\\/skus\\/*"}(+price_i:[* TO 50] +_nest_path_:\/skus)' +$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' --data-urlencode 'q={!child parentPath="/skus"}price_i:[* TO 50]' { "response":{"numFound":1,"start":0,"maxScore":1.0,"numFoundExact":true,"docs":[ { @@ -165,25 +165,6 @@ $ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' - }} ---- -[#double-escaping-nest-path-slashes] -[CAUTION] -.Double Escaping `\_nest_path_` slashes in `of` -==== -Note that in the above example, the `/` characters in the `\_nest_path_` were "double escaped" in the `of` parameter: - -* One level of `\` escaping is necessary to prevent the `/` from being interpreted as a {lucene-javadocs}/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Regexp_Searches[Regex Query] -* An additional level of "escaping the escape character" is necessary because the `of` local parameter is a quoted string; so we need a second `\` to ensure the first `\` is preserved and passed as is to the query parser. - -(You can see that only a single level of `\` escaping is needed in the body of the query string -- to prevent the Regex syntax -- because it's not a quoted string local param). - -You may find it more convenient to use xref:local-params.adoc#parameter-dereferencing[parameter references] in conjunction with xref:other-parsers.adoc[other parsers] that do not treat `/` as a special character to express the same query in a more verbose form: - -[source,text] ----- -$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' --data-urlencode 'q={!child of=$block_mask}(+price_i:[* TO 50] +{!field f="_nest_path_" v="/skus"})' --data-urlencode 'block_mask=(*:* -{!prefix f="_nest_path_" v="/skus/"})' ----- -==== - === Parent Query Parser The inverse of the `{!child}` query parser is the `{!parent}` query parser, which lets you search for the _ancestor_ documents of some child documents matching a wrapped query. @@ -217,11 +198,11 @@ $ curl 'http://localhost:8983/solr/gettingstarted/select?omitHeader=true&q=pages }} ---- -We can wrap that query in a `{!parent}` query to return the details of all products that are ancestors of these manuals: +We can wrap that query in a `{!parent}` query with `parentPath="/"` to return the details of all root-level products that are ancestors of these manuals: [source,text] ---- -$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="*:* -_nest_path_:*"}(+_nest_path_:\/skus\/manuals +pages_i:1)' +$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' --data-urlencode 'q={!parent parentPath="/"}pages_i:1' { "response":{"numFound":2,"start":0,"maxScore":1.4E-45,"numFoundExact":true,"docs":[ { @@ -237,14 +218,15 @@ $ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' - }} ---- -In this example we've used `\*:* -\_nest_path_:*` as our xref:block-join-query-parser.adoc#block-mask[`which` parameter] to indicate we want to consider all documents which don't have a nest path -- i.e., all "root" level document -- as the set of possible parents. +In this example `parentPath="/"` indicates we want root-level documents to be the parents. -By changing the `which` parameter to match ancestors at specific `\_nest_path_` levels, we can change the type of ancestors we return. -In the query below, we search for `skus` (using an `which` parameter that identifies all documents that do _not_ have a `\_nest_path_` with the prefix `/skus/*`) that are the ancestors of `manuals` with exactly `1` page: +By changing `parentPath` to a specific path, we can change the type of ancestors we return. +In the query below, we search for the `skus` that are the ancestors of `manuals` with exactly `1` page. +Adding `childPath="manuals"` constrains the child query to only docs nested at `/skus/manuals`, preventing pages from other child types from matching: [source,text] ---- -$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="*:* -_nest_path_:\\/skus\\/*"}(+_nest_path_:\/skus\/manuals +pages_i:1)' +$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' --data-urlencode 'q={!parent parentPath="/skus" childPath="manuals"}pages_i:1' { "response":{"numFound":2,"start":0,"maxScore":1.4E-45,"numFoundExact":true,"docs":[ { @@ -260,11 +242,6 @@ $ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' - }} ---- -[CAUTION] -==== -Note that in the above example, the `/` characters in the `\_nest_path_` were "double escaped" in the `which` parameter, for the <> regarding the `{!child} pasers `of` parameter. -==== - === Combining Block Join Query Parsers with Child Doc Transformer The combination of these two parsers with the `[child]` transformer enables seamless creation of very powerful queries. @@ -279,7 +256,7 @@ Here for example is a query where: [source,text] ---- -$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' -d 'fq=color_s:RED' --data-urlencode 'q={!child of="*:* -_nest_path_:*" filters=$parent_fq}' --data-urlencode 'parent_fq={!parent which="*:* -_nest_path_:*"}(+_nest_path_:"/manuals" +content_t:"lifetime guarantee")' -d 'fl=*,[child]' +$ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' -d 'fq=color_s:RED' --data-urlencode 'q={!child parentPath="/" filters=$parent_fq}' --data-urlencode 'parent_fq={!parent parentPath="/"}content_t:"lifetime guarantee"' -d 'fl=*,[child]' { "response":{"numFound":1,"start":0,"maxScore":1.4E-45,"numFoundExact":true,"docs":[ {