You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ds...@apache.org on 2018/08/29 14:05:13 UTC

lucene-solr:branch_7x: SOLR-12519: child doc transformer can now produce a nested structure. Fixed SolrDocument's confusion of field-attached child documents in addField() Fixed AtomicUpdateDocumentMerger's confusion of field-attached child documents in

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_7x fec2bf5d1 -> 171cfc8e8


SOLR-12519: child doc transformer can now produce a nested structure.
Fixed SolrDocument's confusion of field-attached child documents in addField()
Fixed AtomicUpdateDocumentMerger's confusion of field-attached child documents in isAtomicUpdate()

(cherry picked from commit 5a0e7a615a9b1e7ac97c6b0f9e5604dcc1aeb03f)


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/171cfc8e
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/171cfc8e
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/171cfc8e

Branch: refs/heads/branch_7x
Commit: 171cfc8e8e4d4e3f0061aa181c28c14e967a350f
Parents: fec2bf5
Author: David Smiley <ds...@apache.org>
Authored: Wed Aug 29 10:02:09 2018 -0400
Committer: David Smiley <ds...@apache.org>
Committed: Wed Aug 29 10:05:01 2018 -0400

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   4 +
 .../response/transform/ChildDocTransformer.java | 245 ++++++++++++
 .../transform/ChildDocTransformerFactory.java   | 187 ++++-----
 .../processor/AtomicUpdateDocumentMerger.java   |   4 +-
 .../solr/collection1/conf/schema-nest.xml       |  65 ++++
 .../solr/collection1/conf/schema15.xml          |   3 -
 .../transform/TestChildDocTransformer.java      |  13 +-
 .../TestChildDocTransformerHierarchy.java       | 386 +++++++++++++++++++
 .../solr/update/TestNestedUpdateProcessor.java  |  14 +-
 .../org/apache/solr/common/SolrDocument.java    |   4 +-
 10 files changed, 794 insertions(+), 131 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 62e70d9..43c4e60 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -115,6 +115,10 @@ New Features
 * SOLR-12655: Add Korean morphological analyzer ("nori") to default distribution. This also adds examples
   for configuration in Solr's schema.  (Uwe Schindler)
 
+* SOLR-12519: The [child] transformer now returns a nested child doc structure (attached as fields if provided this way)
+  provided the schema is enabled for nested documents.  This is part of a broader enhancement of nested docs.
+  (Moshe Bla, David Smiley)
+
 Bug Fixes
 ----------------------
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/core/src/java/org/apache/solr/response/transform/ChildDocTransformer.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/ChildDocTransformer.java b/solr/core/src/java/org/apache/solr/response/transform/ChildDocTransformer.java
new file mode 100644
index 0000000..bffbaf2
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/transform/ChildDocTransformer.java
@@ -0,0 +1,245 @@
+/*
+ * 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.response.transform;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import org.apache.lucene.index.DocValues;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.ReaderUtil;
+import org.apache.lucene.index.SortedDocValues;
+import org.apache.lucene.search.join.BitSetProducer;
+import org.apache.lucene.util.BitSet;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.search.DocSet;
+import org.apache.solr.search.SolrIndexSearcher;
+import org.apache.solr.search.SolrReturnFields;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.solr.response.transform.ChildDocTransformerFactory.NUM_SEP_CHAR;
+import static org.apache.solr.response.transform.ChildDocTransformerFactory.PATH_SEP_CHAR;
+import static org.apache.solr.schema.IndexSchema.NEST_PATH_FIELD_NAME;
+
+class ChildDocTransformer extends DocTransformer {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static final String ANON_CHILD_KEY = "_childDocuments_";
+
+  private final String name;
+  private final BitSetProducer parentsFilter;
+  private final DocSet childDocSet;
+  private final int limit;
+  private final boolean isNestedSchema;
+
+  //TODO ought to be provided/configurable
+  private final SolrReturnFields childReturnFields = new SolrReturnFields();
+
+  ChildDocTransformer(String name, BitSetProducer parentsFilter,
+                      DocSet childDocSet, boolean isNestedSchema, int limit) {
+    this.name = name;
+    this.parentsFilter = parentsFilter;
+    this.childDocSet = childDocSet;
+    this.limit = limit;
+    this.isNestedSchema = isNestedSchema;
+  }
+
+  @Override
+  public String getName()  {
+    return name;
+  }
+
+  @Override
+  public void transform(SolrDocument rootDoc, int rootDocId) {
+    // note: this algorithm works if both if we have have _nest_path_  and also if we don't!
+
+    try {
+
+      // lookup what the *previous* rootDocId is, and figure which segment this is
+      final SolrIndexSearcher searcher = context.getSearcher();
+      final List<LeafReaderContext> leaves = searcher.getIndexReader().leaves();
+      final int seg = ReaderUtil.subIndex(rootDocId, leaves);
+      final LeafReaderContext leafReaderContext = leaves.get(seg);
+      final int segBaseId = leafReaderContext.docBase;
+      final int segRootId = rootDocId - segBaseId;
+      final BitSet segParentsBitSet = parentsFilter.getBitSet(leafReaderContext);
+
+      final int segPrevRootId = segRootId==0? -1: segParentsBitSet.prevSetBit(segRootId - 1); // can return -1 and that's okay
+
+      if (segPrevRootId == (segRootId - 1)) {
+        // doc has no children, return fast
+        return;
+      }
+
+      // we'll need this soon...
+      final SortedDocValues segPathDocValues = DocValues.getSorted(leafReaderContext.reader(), NEST_PATH_FIELD_NAME);
+      // passing a different SortedDocValues obj since the child documents which come after are of smaller docIDs,
+      // and the iterator can not be reversed.
+      // The root doc is the input document to be transformed, and is not necessarily the root doc of the block of docs.
+      final String rootDocPath = getPathByDocId(segRootId, DocValues.getSorted(leafReaderContext.reader(), NEST_PATH_FIELD_NAME));
+
+      // the key in the Map is the document's ancestors key (one above the parent), while the key in the intermediate
+      // MultiMap is the direct child document's key(of the parent document)
+      final Map<String, Multimap<String, SolrDocument>> pendingParentPathsToChildren = new HashMap<>();
+
+      final int firstChildId = segBaseId + segPrevRootId + 1;
+      int matches = 0;
+      // Loop each child ID up to the parent (exclusive).
+      for (int docId = firstChildId; docId < rootDocId; ++docId) {
+
+        // get the path.  (note will default to ANON_CHILD_KEY if schema is not nested or empty string if blank)
+        final String fullDocPath = getPathByDocId(docId - segBaseId, segPathDocValues);
+
+        if (isNestedSchema && !fullDocPath.startsWith(rootDocPath)) {
+          // is not a descendant of the transformed doc; return fast.
+          continue;
+        }
+
+        // Is this doc a direct ancestor of another doc we've seen?
+        boolean isAncestor = pendingParentPathsToChildren.containsKey(fullDocPath);
+
+        // Do we need to do anything with this doc (either ancestor or matched the child query)
+        if (isAncestor || childDocSet == null || childDocSet.exists(docId)) {
+
+          // If we reached the limit, only add if it's an ancestor
+          if (limit != -1 && matches >= limit && !isAncestor) {
+            continue;
+          }
+          ++matches; // note: includes ancestors that are not necessarily in childDocSet
+
+          // load the doc
+          SolrDocument doc = searcher.getDocFetcher().solrDoc(docId, childReturnFields);
+
+          if (isAncestor) {
+            // if this path has pending child docs, add them.
+            addChildrenToParent(doc, pendingParentPathsToChildren.remove(fullDocPath)); // no longer pending
+          }
+
+          // get parent path
+          String parentDocPath = getParentPath(fullDocPath);
+          String lastPath = getLastPath(fullDocPath);
+          // put into pending:
+          // trim path if the doc was inside array, see trimPathIfArrayDoc()
+          // e.g. toppings#1/ingredients#1 -> outer map key toppings#1
+          // -> inner MultiMap key ingredients
+          // or lonely#/lonelyGrandChild# -> outer map key lonely#
+          // -> inner MultiMap key lonelyGrandChild#
+          pendingParentPathsToChildren.computeIfAbsent(parentDocPath, x -> ArrayListMultimap.create())
+              .put(trimLastPoundIfArray(lastPath), doc); // multimap add (won't replace)
+        }
+      }
+
+      if (pendingParentPathsToChildren.isEmpty()) {
+        // no child docs matched the child filter; return fast.
+        return;
+      }
+
+      // only children of parent remain
+      assert pendingParentPathsToChildren.keySet().size() == 1;
+
+      // size == 1, so get the last remaining entry
+      addChildrenToParent(rootDoc, pendingParentPathsToChildren.values().iterator().next());
+
+    } catch (IOException e) {
+      //TODO DWS: reconsider this unusual error handling approach; shouldn't we rethrow?
+      log.warn("Could not fetch child documents", e);
+      rootDoc.put(getName(), "Could not fetch child documents");
+    }
+  }
+
+  private static void addChildrenToParent(SolrDocument parent, Multimap<String, SolrDocument> children) {
+    for (String childLabel : children.keySet()) {
+      addChildrenToParent(parent, children.get(childLabel), childLabel);
+    }
+  }
+
+  private static void addChildrenToParent(SolrDocument parent, Collection<SolrDocument> children, String cDocsPath) {
+    // if no paths; we do not need to add the child document's relation to its parent document.
+    if (cDocsPath.equals(ANON_CHILD_KEY)) {
+      parent.addChildDocuments(children);
+      return;
+    }
+    // lookup leaf key for these children using path
+    // depending on the label, add to the parent at the right key/label
+    String trimmedPath = trimLastPound(cDocsPath);
+    // if the child doc's path does not end with #, it is an array(same string is returned by ChildDocTransformer#trimLastPound)
+    if (!parent.containsKey(trimmedPath) && (trimmedPath == cDocsPath)) {
+      List<SolrDocument> list = new ArrayList<>(children);
+      parent.setField(trimmedPath, list);
+      return;
+    }
+    // is single value
+    parent.setField(trimmedPath, ((List)children).get(0));
+  }
+
+  private static String getLastPath(String path) {
+    int lastIndexOfPathSepChar = path.lastIndexOf(PATH_SEP_CHAR);
+    if(lastIndexOfPathSepChar == -1) {
+      return path;
+    }
+    return path.substring(lastIndexOfPathSepChar + 1);
+  }
+
+  private static String trimLastPoundIfArray(String path) {
+    // remove index after last pound sign and if there is an array index e.g. toppings#1 -> toppings
+    // or return original string if child doc is not in an array ingredients# -> ingredients#
+    final int indexOfSepChar = path.lastIndexOf(NUM_SEP_CHAR);
+    if (indexOfSepChar == -1) {
+      return path;
+    }
+    int lastIndex = path.length() - 1;
+    boolean singleDocVal = indexOfSepChar == lastIndex;
+    return singleDocVal ? path: path.substring(0, indexOfSepChar);
+  }
+
+  private static String trimLastPound(String path) {
+    // remove index after last pound sign and index from e.g. toppings#1 -> toppings
+    int lastIndex = path.lastIndexOf('#');
+    return lastIndex == -1 ? path : path.substring(0, lastIndex);
+  }
+
+  /**
+   * Returns the *parent* path for this document.
+   * Children of the root will yield null.
+   */
+  private static String getParentPath(String currDocPath) {
+    // chop off leaf (after last '/')
+    // if child of leaf then return null (special value)
+    int lastPathIndex = currDocPath.lastIndexOf(PATH_SEP_CHAR);
+    return lastPathIndex == -1 ? null : currDocPath.substring(0, lastPathIndex);
+  }
+
+  /** Looks up the nest path.  If there is none, returns {@link #ANON_CHILD_KEY}. */
+  private String getPathByDocId(int segDocId, SortedDocValues segPathDocValues) throws IOException {
+    if (!isNestedSchema) {
+      return ANON_CHILD_KEY;
+    }
+    int numToAdvance = segPathDocValues.docID() == -1 ? segDocId : segDocId - (segPathDocValues.docID());
+    assert numToAdvance >= 0;
+    boolean advanced = segPathDocValues.advanceExact(segDocId);
+    return advanced ? segPathDocValues.binaryValue().utf8ToString(): "";
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/core/src/java/org/apache/solr/response/transform/ChildDocTransformerFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/ChildDocTransformerFactory.java b/solr/core/src/java/org/apache/solr/response/transform/ChildDocTransformerFactory.java
index a414dc9..2478c48 100644
--- a/solr/core/src/java/org/apache/solr/response/transform/ChildDocTransformerFactory.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/ChildDocTransformerFactory.java
@@ -17,39 +17,31 @@
 package org.apache.solr.response.transform;
 
 import java.io.IOException;
-import java.util.Set;
 
-import org.apache.lucene.document.Document;
-import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.DocValuesFieldExistsQuery;
+import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.search.Sort;
 import org.apache.lucene.search.join.BitSetProducer;
 import org.apache.lucene.search.join.QueryBitSetProducer;
-import org.apache.lucene.search.join.ToChildBlockJoinQuery;
-import org.apache.solr.common.SolrDocument;
+import org.apache.solr.client.solrj.util.ClientUtils;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.DocsStreamer;
-import org.apache.solr.schema.FieldType;
-import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.schema.SchemaField;
-import org.apache.solr.search.DocIterator;
-import org.apache.solr.search.DocList;
+import org.apache.solr.search.DocSet;
 import org.apache.solr.search.QParser;
-import org.apache.solr.search.SolrDocumentFetcher;
-import org.apache.solr.search.SolrReturnFields;
 import org.apache.solr.search.SyntaxError;
 
+import static org.apache.solr.schema.IndexSchema.NEST_PATH_FIELD_NAME;
+
 /**
+ * Attaches all descendants (child documents) to each parent document.
  *
- * @since solr 4.9
- *
- * This transformer returns all descendants of each parent document in a flat list nested inside the parent document.
+ * The "parentFilter" parameter is mandatory if the schema is not of nest/hierarchy.
  *
- *
- * The "parentFilter" parameter is mandatory.
  * Optionally you can provide a "childFilter" param to filter out which child documents should be returned and a
  * "limit" param which provides an option to specify the number of child documents
  * to be returned per parent document. By default it's set to 10.
@@ -58,121 +50,92 @@ import org.apache.solr.search.SyntaxError;
  * [child parentFilter="fieldName:fieldValue"]
  * [child parentFilter="fieldName:fieldValue" childFilter="fieldName:fieldValue"]
  * [child parentFilter="fieldName:fieldValue" childFilter="fieldName:fieldValue" limit=20]
+ *
+ * @since solr 4.9
  */
 public class ChildDocTransformerFactory extends TransformerFactory {
 
+  static final char PATH_SEP_CHAR = '/';
+  static final char NUM_SEP_CHAR = '#';
+  private static final BooleanQuery rootFilter = new BooleanQuery.Builder()
+      .add(new BooleanClause(new MatchAllDocsQuery(), BooleanClause.Occur.MUST))
+      .add(new BooleanClause(new DocValuesFieldExistsQuery(NEST_PATH_FIELD_NAME), BooleanClause.Occur.MUST_NOT)).build();
+
   @Override
   public DocTransformer create(String field, SolrParams params, SolrQueryRequest req) {
     SchemaField uniqueKeyField = req.getSchema().getUniqueKeyField();
-    if(uniqueKeyField == null) {
+    if (uniqueKeyField == null) {
       throw new SolrException( ErrorCode.BAD_REQUEST,
           " ChildDocTransformer requires the schema to have a uniqueKeyField." );
     }
-
-    String parentFilter = params.get( "parentFilter" );
-    if( parentFilter == null ) {
-      throw new SolrException( ErrorCode.BAD_REQUEST, "Parent filter should be sent as parentFilter=filterCondition" );
-    }
-
-    String childFilter = params.get( "childFilter" );
-    int limit = params.getInt( "limit", 10 );
-
-    BitSetProducer parentsFilter = null;
-    try {
-      Query parentFilterQuery = QParser.getParser( parentFilter, req).getQuery();
-      //TODO shouldn't we try to use the Solr filter cache, and then ideally implement
-      //  BitSetProducer over that?
-      // DocSet parentDocSet = req.getSearcher().getDocSet(parentFilterQuery);
-      // then return BitSetProducer with custom BitSet impl accessing the docSet
-      parentsFilter = new QueryBitSetProducer(parentFilterQuery);
-    } catch (SyntaxError syntaxError) {
-      throw new SolrException( ErrorCode.BAD_REQUEST, "Failed to create correct parent filter query" );
+    // Do we build a hierarchy or flat list of child docs (attached anonymously)?
+    boolean buildHierarchy = req.getSchema().hasExplicitField(NEST_PATH_FIELD_NAME);
+
+    String parentFilterStr = params.get( "parentFilter" );
+    BitSetProducer parentsFilter;
+    // TODO reuse org.apache.solr.search.join.BlockJoinParentQParser.getCachedFilter (uses a cache)
+    // TODO shouldn't we try to use the Solr filter cache, and then ideally implement
+    //  BitSetProducer over that?
+    // DocSet parentDocSet = req.getSearcher().getDocSet(parentFilterQuery);
+    // then return BitSetProducer with custom BitSet impl accessing the docSet
+    if (parentFilterStr == null) {
+      if (!buildHierarchy) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "Parent filter should be sent as parentFilter=filterCondition");
+      }
+      parentsFilter = new QueryBitSetProducer(rootFilter);
+    } else {
+      if(buildHierarchy) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "Parent filter should not be sent when the schema is nested");
+      }
+      parentsFilter = new QueryBitSetProducer(parseQuery(parentFilterStr, req,  "parentFilter"));
     }
 
-    Query childFilterQuery = null;
-    if(childFilter != null) {
+    String childFilterStr = params.get( "childFilter" );
+    DocSet childDocSet;
+    if (childFilterStr == null) {
+      childDocSet = null;
+    } else {
+      if (buildHierarchy) {
+        childFilterStr = processPathHierarchyQueryString(childFilterStr);
+      }
+      Query childFilter = parseQuery(childFilterStr, req, "childFilter");
       try {
-        childFilterQuery = QParser.getParser( childFilter, req).getQuery();
-      } catch (SyntaxError syntaxError) {
-        throw new SolrException( ErrorCode.BAD_REQUEST, "Failed to create correct child filter query" );
+        childDocSet = req.getSearcher().getDocSet(childFilter);
+      } catch (IOException e) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, e);
       }
     }
 
-    return new ChildDocTransformer( field, parentsFilter, uniqueKeyField, req.getSchema(), childFilterQuery, limit);
-  }
-}
-
-class ChildDocTransformer extends DocTransformer {
-  private final String name;
-  private final SchemaField idField;
-  private final IndexSchema schema;
-  private BitSetProducer parentsFilter;
-  private Query childFilterQuery;
-  private int limit;
+    int limit = params.getInt( "limit", 10 );
 
-  public ChildDocTransformer( String name, final BitSetProducer parentsFilter, 
-                              final SchemaField idField, IndexSchema schema,
-                              final Query childFilterQuery, int limit) {
-    this.name = name;
-    this.idField = idField;
-    this.schema = schema;
-    this.parentsFilter = parentsFilter;
-    this.childFilterQuery = childFilterQuery;
-    this.limit = limit;
+    return new ChildDocTransformer(field, parentsFilter, childDocSet, buildHierarchy, limit);
   }
 
-  @Override
-  public String getName()  {
-    return name;
-  }
-  
-  @Override
-  public String[] getExtraRequestFields() {
-    // we always need the idField (of the parent) in order to fill out it's children
-    return new String[] { idField.getName() };
-  }
-
-  @Override
-  public void transform(SolrDocument doc, int docid) {
-
-    FieldType idFt = idField.getType();
-    Object parentIdField = doc.getFirstValue(idField.getName());
-    
-    String parentIdExt = parentIdField instanceof IndexableField
-      ? idFt.toExternal((IndexableField)parentIdField)
-      : parentIdField.toString();
-
+  private static Query parseQuery(String qstr, SolrQueryRequest req, String param) {
     try {
-      Query parentQuery = idFt.getFieldQuery(null, idField, parentIdExt);
-      Query query = new ToChildBlockJoinQuery(parentQuery, parentsFilter);
-      DocList children = context.getSearcher().getDocList(query, childFilterQuery, new Sort(), 0, limit);
-      if(children.matches() > 0) {
-        SolrDocumentFetcher docFetcher = context.getSearcher().getDocFetcher();
-
-        Set<String> dvFieldsToReturn = docFetcher.getNonStoredDVs(true);
-        boolean shouldDecorateWithDVs = dvFieldsToReturn.size() > 0;
-        DocIterator i = children.iterator();
-
-        while(i.hasNext()) {
-          Integer childDocNum = i.next();
-          Document childDoc = context.getSearcher().doc(childDocNum);
-          // TODO: future enhancement...
-          // support an fl local param in the transformer, which is used to build
-          // a private ReturnFields instance that we use to prune unwanted field 
-          // names from solrChildDoc
-          SolrDocument solrChildDoc = DocsStreamer.convertLuceneDocToSolrDoc(childDoc, schema,
-                                                                             new SolrReturnFields());
+      return QParser.getParser(qstr, req).getQuery();
+    } catch (SyntaxError syntaxError) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "Failed to parse '" + param + "' param.");
+    }
+  }
 
-          if (shouldDecorateWithDVs) {
-            docFetcher.decorateDocValueFields(solrChildDoc, childDocNum, dvFieldsToReturn);
-          }
-          doc.addChildDocument(solrChildDoc);
-        }
-      }
-      
-    } catch (IOException e) {
-      doc.put(name, "Could not fetch child Documents");
+  // NOTE: THIS FEATURE IS PRESENTLY EXPERIMENTAL; WAIT TO SEE IT IN THE REF GUIDE.  FINAL SYNTAX IS TBD.
+  protected static String processPathHierarchyQueryString(String queryString) {
+    // if the filter includes a path string, build a lucene query string to match those specific child documents.
+    // e.g. toppings/ingredients/name_s:cocoa -> +_nest_path_:"toppings/ingredients/" +(name_s:cocoa)
+    int indexOfFirstColon = queryString.indexOf(':');
+    if (indexOfFirstColon <= 0) {
+      return queryString;// give up
+    }
+    int indexOfLastPathSepChar = queryString.lastIndexOf(PATH_SEP_CHAR, indexOfFirstColon);
+    if (indexOfLastPathSepChar < 0) {
+      return queryString;
     }
+    String path = queryString.substring(0, indexOfLastPathSepChar + 1);
+    String remaining = queryString.substring(indexOfLastPathSepChar + 1);
+    return
+        "+" + NEST_PATH_FIELD_NAME + ":" + ClientUtils.escapeQueryChars(path)
+        + " +(" + remaining + ")";
   }
 }
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/core/src/java/org/apache/solr/update/processor/AtomicUpdateDocumentMerger.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/update/processor/AtomicUpdateDocumentMerger.java b/solr/core/src/java/org/apache/solr/update/processor/AtomicUpdateDocumentMerger.java
index 1198bc9..1069c33 100644
--- a/solr/core/src/java/org/apache/solr/update/processor/AtomicUpdateDocumentMerger.java
+++ b/solr/core/src/java/org/apache/solr/update/processor/AtomicUpdateDocumentMerger.java
@@ -32,6 +32,7 @@ import java.util.regex.Pattern;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.BytesRefBuilder;
+import org.apache.solr.common.SolrDocumentBase;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.SolrInputDocument;
@@ -73,7 +74,8 @@ public class AtomicUpdateDocumentMerger {
   public static boolean isAtomicUpdate(final AddUpdateCommand cmd) {
     SolrInputDocument sdoc = cmd.getSolrInputDocument();
     for (SolrInputField sif : sdoc.values()) {
-      if (sif.getValue() instanceof Map) {
+      Object val = sif.getValue();
+      if (val instanceof Map && !(val instanceof SolrDocumentBase)) {
         return true;
       }
     }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/core/src/test-files/solr/collection1/conf/schema-nest.xml
----------------------------------------------------------------------
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema-nest.xml b/solr/core/src/test-files/solr/collection1/conf/schema-nest.xml
new file mode 100644
index 0000000..313e586
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/schema-nest.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" ?>
+<!--
+  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.
+  -->
+
+
+<schema name="nested-docs" version="1.6">
+
+  <field name="id" type="string" indexed="true" stored="true" multiValued="false" required="true"/>
+  <field name="id_i" type="int" indexed="true" multiValued="false" docValues="true" stored="false" useDocValuesAsStored="false" />
+  <!-- copy id field as int -->
+  <copyField source="id" dest="id_i"/>
+
+  <!-- for versioning -->
+  <field name="_version_" type="long" indexed="false" stored="false" docValues="true"/>
+  <!-- points to the root document of a block of nested documents -->
+  <field name="_root_" type="string" indexed="true" stored="true"/>
+
+  <!-- required for NestedUpdateProcessor -->
+  <field name="_nest_parent_" type="string" indexed="true" stored="true"/>
+  <field name="_nest_path_" type="descendants_path" indexed="true" multiValued="false" docValues="true" stored="false" useDocValuesAsStored="false"/>
+
+  <dynamicField name="*_s" type="string" indexed="true" stored="true"/>
+  <dynamicField name="*_ss" type="string" indexed="true" stored="true" multiValued="true"/>
+
+
+  <fieldType name="string" class="solr.StrField" sortMissingLast="true"/>
+
+  <!-- Point Fields -->
+  <fieldType name="int" class="solr.IntPointField" docValues="true"/>
+  <fieldType name="long" class="solr.LongPointField" docValues="true"/>
+  <fieldType name="double" class="solr.DoublePointField" docValues="true"/>
+  <fieldType name="float" class="solr.FloatPointField" docValues="true"/>
+  <fieldType name="date" class="solr.DatePointField" docValues="true"/>
+
+  <fieldType name="descendants_path" class="solr.SortableTextField">
+    <analyzer type="index">
+      <!--char filter to append / to path in the indexed form e.g. toppings/ingredients turns to toppings/ingredients/ -->
+      <charFilter class="solr.PatternReplaceCharFilterFactory" pattern="(^.*.*$)" replacement="$0/"/>
+      <!--tokenize the path so path queries are optimized -->
+      <tokenizer class="solr.PathHierarchyTokenizerFactory" delimiter="/"/>
+      <!--remove the # and digit index of array from path toppings#1/ingredients#/ turns to toppings/ingredients/ -->
+      <filter class="solr.PatternReplaceFilterFactory" pattern="[#*\d]*" replace="all"/>
+    </analyzer>
+    <analyzer type="query">
+      <tokenizer class="solr.KeywordTokenizerFactory"/>
+    </analyzer>
+  </fieldType>
+
+  <uniqueKey>id</uniqueKey>
+
+</schema>

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/core/src/test-files/solr/collection1/conf/schema15.xml
----------------------------------------------------------------------
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema15.xml b/solr/core/src/test-files/solr/collection1/conf/schema15.xml
index e65cbfd..80d19e9 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema15.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema15.xml
@@ -565,9 +565,6 @@
   <field name="_version_" type="long" indexed="false" stored="false" docValues="true"/>
   <!-- points to the root document of a block of nested documents -->
   <field name="_root_" type="string" indexed="true" stored="true"/>
-  <!-- required for NestedUpdateProcessor -->
-  <field name="_nest_parent_" type="string" indexed="true" stored="true"/>
-  <field name="_nest_path_" type="string" indexed="true" stored="true"/>
 
   <field name="multi_int_with_docvals" type="tint" multiValued="true" docValues="true" indexed="false"/>
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/core/src/test/org/apache/solr/response/transform/TestChildDocTransformer.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/response/transform/TestChildDocTransformer.java b/solr/core/src/test/org/apache/solr/response/transform/TestChildDocTransformer.java
index 71b77f4..385ca4f 100644
--- a/solr/core/src/test/org/apache/solr/response/transform/TestChildDocTransformer.java
+++ b/solr/core/src/test/org/apache/solr/response/transform/TestChildDocTransformer.java
@@ -30,7 +30,7 @@ public class TestChildDocTransformer extends SolrTestCaseJ4 {
 
   @BeforeClass
   public static void beforeClass() throws Exception {
-    initCore("solrconfig.xml","schema.xml");
+    initCore("solrconfig.xml","schema.xml"); // *not* the "nest" schema version
   }
 
   @After
@@ -63,7 +63,7 @@ public class TestChildDocTransformer extends SolrTestCaseJ4 {
     testChildDocNonStoredDVFields();
   }
 
-  private void testChildDoctransformerXML() {
+  private void testChildDoctransformerXML() throws Exception {
     String test1[] = new String[] {
         "//*[@numFound='1']",
         "/response/result/doc[1]/doc[1]/str[@name='id']='2'" ,
@@ -81,8 +81,9 @@ public class TestChildDocTransformer extends SolrTestCaseJ4 {
 
     String test3[] = new String[] {
         "//*[@numFound='1']",
+        "count(/response/result/doc[1]/doc)=2",
         "/response/result/doc[1]/doc[1]/str[@name='id']='3'" ,
-        "/response/result/doc[1]/doc[2]/str[@name='id']='5'" };
+        "/response/result/doc[1]/doc[2]/str[@name='id']='5'"};
 
 
 
@@ -214,7 +215,7 @@ public class TestChildDocTransformer extends SolrTestCaseJ4 {
         "fl", "subject,[child parentFilter=\"subject:parentDocument\" childFilter=\"title:foo\"]"), test2);
 
     assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
-        "fl", "subject,[child parentFilter=\"subject:parentDocument\" childFilter=\"title:bar\" limit=2]"), test3);
+        "fl", "subject,[child parentFilter=\"subject:parentDocument\" childFilter=\"title:bar\" limit=3]"), test3);
   }
 
   private void testChildDocNonStoredDVFields() throws Exception {
@@ -338,7 +339,7 @@ public class TestChildDocTransformer extends SolrTestCaseJ4 {
     assertJQ(req("q", "*:*", 
                  "sort", "id asc",
                  "fq", "subject:\"parentDocument\" ",
-                 "fl", "id,[child childFilter='cat:childDocument' parentFilter=\"subject:parentDocument\"]"), 
+                 "fl", "id,[child childFilter='cat:childDocument' parentFilter=\"subject:parentDocument\"]"),
              tests);
 
   }
@@ -397,7 +398,7 @@ public class TestChildDocTransformer extends SolrTestCaseJ4 {
     assertQ(req("q", "*:*", 
                 "sort", "id asc",
                 "fq", "subject:\"parentDocument\" ",
-                "fl", "id,[child childFilter='cat:childDocument' parentFilter=\"subject:parentDocument\"]"), 
+                "fl", "id,[child childFilter='cat:childDocument' parentFilter=\"subject:parentDocument\"]"),
             tests);
   }
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/core/src/test/org/apache/solr/response/transform/TestChildDocTransformerHierarchy.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/response/transform/TestChildDocTransformerHierarchy.java b/solr/core/src/test/org/apache/solr/response/transform/TestChildDocTransformerHierarchy.java
new file mode 100644
index 0000000..f207166
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/response/transform/TestChildDocTransformerHierarchy.java
@@ -0,0 +1,386 @@
+/*
+ * 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.response.transform;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.Iterables;
+import org.apache.lucene.index.IndexableField;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.BasicResultContext;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestChildDocTransformerHierarchy extends SolrTestCaseJ4 {
+
+  private static AtomicInteger idCounter = new AtomicInteger();
+  private static final String[] types = {"donut", "cake"};
+  private static final String[] ingredients = {"flour", "cocoa", "vanilla"};
+  private static final Iterator<String> ingredientsCycler = Iterables.cycle(ingredients).iterator();
+  private static final String[] names = {"Yaz", "Jazz", "Costa"};
+  private static final String[] fieldsToRemove = {"_nest_parent_", "_nest_path_", "_root_"};
+  private static final int sumOfDocsPerNestedDocument = 8;
+  private static final int numberOfDocsPerNestedTest = 10;
+  private static int firstTestedDocId = 0;
+  private static String fqToExcludeNonTestedDocs; // filter documents that were created for random segments to ensure the transformer works with multiple segments.
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig-update-processor-chains.xml", "schema-nest.xml"); // use "nest" schema
+    final boolean useSegments = random().nextBoolean();
+    if(useSegments) {
+      // create random segments
+      final int numOfDocs = 10;
+      for(int i = 0; i < numOfDocs; ++i) {
+        updateJ(generateDocHierarchy(i), params("update.chain", "nested"));
+        if(random().nextBoolean()) {
+          assertU(commit());
+        }
+      }
+      assertU(commit());
+      fqToExcludeNonTestedDocs = "{!frange l=" + firstTestedDocId + " incl=false}id_i";
+    } else {
+      fqToExcludeNonTestedDocs = "*:*";
+    }
+    firstTestedDocId = idCounter.get();
+  }
+
+  @After
+  public void after() throws Exception {
+    assertU(delQ(fqToExcludeNonTestedDocs));
+    assertU(commit());
+    idCounter.set(firstTestedDocId); // reset idCounter
+  }
+
+  @Test
+  public void testParentFilterJSON() throws Exception {
+    indexSampleData(numberOfDocsPerNestedTest);
+    String[] tests = new String[] {
+        "/response/docs/[0]/type_s==donut",
+        "/response/docs/[0]/toppings/[0]/type_s==Regular",
+        "/response/docs/[0]/toppings/[1]/type_s==Chocolate",
+        "/response/docs/[0]/toppings/[0]/ingredients/[0]/name_s==cocoa",
+        "/response/docs/[0]/toppings/[1]/ingredients/[1]/name_s==cocoa",
+        "/response/docs/[0]/lonely/test_s==testing",
+        "/response/docs/[0]/lonely/lonelyGrandChild/test2_s==secondTest",
+    };
+
+    try(SolrQueryRequest req = req("q", "type_s:donut", "sort", "id asc",
+        "fl", "*, _nest_path_, [child]", "fq", fqToExcludeNonTestedDocs)) {
+      BasicResultContext res = (BasicResultContext) h.queryAndResponse("/select", req).getResponse();
+      Iterator<SolrDocument> docsStreamer = res.getProcessedDocuments();
+      while (docsStreamer.hasNext()) {
+        SolrDocument doc = docsStreamer.next();
+        cleanSolrDocumentFields(doc);
+        int currDocId = Integer.parseInt((doc.getFirstValue("id")).toString());
+        assertEquals("queried docs are not equal to expected output for id: " + currDocId, fullNestedDocTemplate(currDocId), doc.toString());
+      }
+    }
+
+    assertJQ(req("q", "type_s:donut",
+        "sort", "id asc",
+        "fl", "*, _nest_path_, [child]",
+        "fq", fqToExcludeNonTestedDocs),
+        tests);
+  }
+
+  @Test
+  public void testParentFilterLimitJSON() throws Exception {
+    indexSampleData(numberOfDocsPerNestedTest);
+
+    try(SolrQueryRequest req = req("q", "type_s:donut", "sort", "id asc", "fl", "id, type_s, toppings, _nest_path_, [child childFilter='_nest_path_:\"toppings/\"' limit=1]",
+        "fq", fqToExcludeNonTestedDocs)) {
+      BasicResultContext res = (BasicResultContext) h.queryAndResponse("/select", req).getResponse();
+      Iterator<SolrDocument> docsStreamer = res.getProcessedDocuments();
+      while (docsStreamer.hasNext()) {
+        SolrDocument doc = docsStreamer.next();
+        cleanSolrDocumentFields(doc);
+        assertFalse("root doc should not have anonymous child docs", doc.hasChildDocuments());
+        assertEquals("should only have 1 child doc", 1, doc.getFieldValues("toppings").size());
+      }
+    }
+
+    assertJQ(req("q", "type_s:donut",
+        "sort", "id asc",
+        "fl", "*, [child limit=1]",
+        "fq", fqToExcludeNonTestedDocs),
+        "/response/docs/[0]/type_s==donut",
+        "/response/docs/[0]/lonely/test_s==testing",
+        "/response/docs/[0]/lonely/lonelyGrandChild/test2_s==secondTest",
+        // "!" (negate): don't find toppings.  The "limit" kept us from reaching these, which follow lonely.
+        "!/response/docs/[0]/toppings/[0]/type_s==Regular"
+    );
+  }
+
+  @Test
+  public void testChildFilterLimitJSON() throws Exception {
+    indexSampleData(numberOfDocsPerNestedTest);
+
+    try(SolrQueryRequest req = req("q", "type_s:donut", "sort", "id asc", "fl", "*, _nest_path_, " +
+        "[child limit='1' childFilter='toppings/type_s:Regular']", "fq", fqToExcludeNonTestedDocs)) {
+      BasicResultContext res = (BasicResultContext) h.queryAndResponse("/select", req).getResponse();
+      Iterator<SolrDocument> docsStreamer = res.getProcessedDocuments();
+      while (docsStreamer.hasNext()) {
+        SolrDocument doc = docsStreamer.next();
+        cleanSolrDocumentFields(doc);
+        assertFalse("root doc should not have anonymous child docs", doc.hasChildDocuments());
+        assertEquals("should only have 1 child doc", 1, doc.getFieldValues("toppings").size());
+        assertEquals("should be of type_s:Regular", "Regular", ((SolrDocument) doc.getFirstValue("toppings")).getFieldValue("type_s"));
+      }
+    }
+
+    assertJQ(req("q", "type_s:donut",
+        "sort", "id asc",
+        "fl", "id, type_s, toppings, _nest_path_, [child limit='10' childFilter='toppings/type_s:Regular']",
+        "fq", fqToExcludeNonTestedDocs),
+        "/response/docs/[0]/type_s==donut",
+        "/response/docs/[0]/toppings/[0]/type_s==Regular");
+  }
+
+  @Test
+  public void testExactPath() throws Exception {
+    indexSampleData(2);
+    String[] tests = {
+        "/response/numFound==4",
+        "/response/docs/[0]/_nest_path_=='toppings#0'",
+        "/response/docs/[1]/_nest_path_=='toppings#0'",
+        "/response/docs/[2]/_nest_path_=='toppings#1'",
+        "/response/docs/[3]/_nest_path_=='toppings#1'",
+    };
+
+    assertJQ(req("q", "_nest_path_:*toppings/",
+        "sort", "_nest_path_ asc",
+        "fl", "*, id_i, _nest_path_",
+        "fq", fqToExcludeNonTestedDocs),
+        tests);
+
+    assertJQ(req("q", "+_nest_path_:\"toppings/\"",
+        "sort", "_nest_path_ asc",
+        "fl", "*, _nest_path_",
+        "fq", fqToExcludeNonTestedDocs),
+        tests);
+  }
+
+  @Test
+  public void testChildFilterJSON() throws Exception {
+    indexSampleData(numberOfDocsPerNestedTest);
+    String[] tests = new String[] {
+        "/response/docs/[0]/type_s==donut",
+        "/response/docs/[0]/toppings/[0]/type_s==Regular",
+    };
+
+    assertJQ(req("q", "type_s:donut",
+        "sort", "id asc",
+        "fl", "*,[child childFilter='toppings/type_s:Regular']",
+        "fq", fqToExcludeNonTestedDocs),
+        tests);
+  }
+
+  @Test
+  public void testGrandChildFilterJSON() throws Exception {
+    indexSampleData(numberOfDocsPerNestedTest);
+    String[] tests = new String[] {
+        "/response/docs/[0]/type_s==donut",
+        "/response/docs/[0]/toppings/[0]/ingredients/[0]/name_s==cocoa"
+    };
+
+    try(SolrQueryRequest req = req("q", "type_s:donut", "sort", "id asc",
+        "fl", "*,[child childFilter='toppings/ingredients/name_s:cocoa'],", "fq", fqToExcludeNonTestedDocs)) {
+      BasicResultContext res = (BasicResultContext) h.queryAndResponse("/select", req).getResponse();
+      Iterator<SolrDocument> docsStreamer = res.getProcessedDocuments();
+      while (docsStreamer.hasNext()) {
+        SolrDocument doc = docsStreamer.next();
+        cleanSolrDocumentFields(doc);
+        int currDocId = Integer.parseInt((doc.getFirstValue("id")).toString());
+        assertEquals("queried docs are not equal to expected output for id: " + currDocId, grandChildDocTemplate(currDocId), doc.toString());
+      }
+    }
+
+
+
+    assertJQ(req("q", "type_s:donut",
+        "sort", "id asc",
+        "fl", "*,[child childFilter='toppings/ingredients/name_s:cocoa']",
+        "fq", fqToExcludeNonTestedDocs),
+        tests);
+  }
+
+  @Test
+  public void testSingularChildFilterJSON() throws Exception {
+    indexSampleData(numberOfDocsPerNestedTest);
+    String[] tests = new String[] {
+        "/response/docs/[0]/type_s==cake",
+        "/response/docs/[0]/lonely/test_s==testing",
+        "/response/docs/[0]/lonely/lonelyGrandChild/test2_s==secondTest"
+    };
+
+    assertJQ(req("q", "type_s:cake",
+        "sort", "id asc",
+        "fl", "*,[child childFilter='lonely/lonelyGrandChild/test2_s:secondTest']",
+        "fq", fqToExcludeNonTestedDocs),
+        tests);
+  }
+
+  @Test
+  public void testNonRootChildren() throws Exception {
+    indexSampleData(numberOfDocsPerNestedTest);
+    assertJQ(req("q", "test_s:testing",
+        "sort", "id asc",
+        "fl", "*,[child childFilter='lonely/lonelyGrandChild/test2_s:secondTest']",
+        "fq", fqToExcludeNonTestedDocs),
+        "/response/docs/[0]/test_s==testing",
+        "/response/docs/[0]/lonelyGrandChild/test2_s==secondTest");
+
+    assertJQ(req("q", "type_s:Chocolate",
+        "sort", "id asc",
+        "fl", "*,[child]",
+        "fq", fqToExcludeNonTestedDocs),
+        "/response/docs/[0]/type_s==Chocolate",
+        "/response/docs/[0]/ingredients/[0]/name_s==cocoa",
+        "/response/docs/[0]/ingredients/[1]/name_s==cocoa");
+  }
+
+  @Test
+  public void testExceptionThrownWParentFilter() throws Exception {
+    expectThrows(SolrException.class,
+        "Exception was not thrown when parentFilter param was passed to ChildDocTransformer using a nested schema",
+        () -> assertJQ(req("q", "test_s:testing",
+            "sort", "id asc",
+            "fl", "*,[child childFilter='lonely/lonelyGrandChild/test2_s:secondTest' parentFilter='_nest_path_:\"lonely/\"']",
+            "fq", fqToExcludeNonTestedDocs),
+            "/response/docs/[0]/test_s==testing",
+            "/response/docs/[0]/lonelyGrandChild/test2_s==secondTest")
+    );
+  }
+
+  @Test
+  public void testNoChildren() throws Exception {
+    final String addDocWoChildren =
+        "{\n" +
+          "\"add\": {\n" +
+            "\"doc\": {\n" +
+                "\"id\": " + id() + ", \n" +
+                "\"type_s\": \"cake\", \n" +
+            "}\n" +
+          "}\n" +
+        "}";
+    updateJ(addDocWoChildren, params("update.chain", "nested"));
+    assertU(commit());
+
+    assertJQ(req("q", "type_s:cake",
+        "sort", "id asc",
+        "fl", "*,[child childFilter='lonely/lonelyGrandChild/test2_s:secondTest']",
+        "fq", fqToExcludeNonTestedDocs),
+        "/response/docs/[0]/type_s==cake");
+  }
+
+  private void indexSampleData(int numDocs) throws Exception {
+    for(int i = 0; i < numDocs; ++i) {
+      updateJ(generateDocHierarchy(i), params("update.chain", "nested"));
+    }
+    assertU(commit());
+  }
+
+  private static int id() {
+    return idCounter.incrementAndGet();
+  }
+
+  private static void cleanSolrDocumentFields(SolrDocument input) {
+    for(String fieldName: fieldsToRemove) {
+      input.removeFields(fieldName);
+    }
+    for(Map.Entry<String, Object> field: input) {
+      Object val = field.getValue();
+      if(val instanceof Collection) {
+        Object newVals = ((Collection) val).stream().map((item) -> (cleanIndexableField(item)))
+            .collect(Collectors.toList());
+        input.setField(field.getKey(), newVals);
+        continue;
+      }
+      input.setField(field.getKey(), cleanIndexableField(field.getValue()));
+    }
+  }
+
+  private static Object cleanIndexableField(Object field) {
+    if(field instanceof IndexableField) {
+      return ((IndexableField) field).stringValue();
+    } else if(field instanceof SolrDocument) {
+      cleanSolrDocumentFields((SolrDocument) field);
+    }
+    return field;
+  }
+
+  private static String grandChildDocTemplate(int id) {
+    final int docNum = (id - firstTestedDocId) / sumOfDocsPerNestedDocument; // the index of docs sent to solr in the AddUpdateCommand. e.g. first doc is 0
+    return
+        "SolrDocument{id="+ id + ", type_s=" + types[docNum % types.length] + ", name_s=" + names[docNum % names.length] + ", " +
+          "toppings=[" +
+            "SolrDocument{id=" + (id + 3) + ", type_s=Regular, " +
+              "ingredients=[SolrDocument{id=" + (id + 4) + ", name_s=cocoa}]}, " +
+            "SolrDocument{id=" + (id + 5) + ", type_s=Chocolate, " +
+              "ingredients=[SolrDocument{id=" + (id + 6) + ", name_s=cocoa}, SolrDocument{id=" + (id + 7) + ", name_s=cocoa}]}]}";
+  }
+
+  private static String fullNestedDocTemplate(int id) {
+    final int docNum = (id - firstTestedDocId) / sumOfDocsPerNestedDocument; // the index of docs sent to solr in the AddUpdateCommand. e.g. first doc is 0
+    boolean doubleIngredient = docNum % 2 == 0;
+    String currIngredient = doubleIngredient ? ingredients[1]: ingredientsCycler.next();
+    return
+        "SolrDocument{id=" + id + ", type_s=" + types[docNum % types.length] + ", name_s=" + names[docNum % names.length] + ", " +
+          "lonely=SolrDocument{id=" + (id + 1) + ", test_s=testing, " +
+            "lonelyGrandChild=SolrDocument{id=" + (id + 2) + ", test2_s=secondTest}}, " +
+          "toppings=[" +
+            "SolrDocument{id=" + (id + 3) + ", type_s=Regular, " +
+              "ingredients=[SolrDocument{id=" + (id + 4) + ", name_s=" + currIngredient + "}]}, " +
+            "SolrDocument{id=" + (id + 5) + ", type_s=Chocolate, " +
+              "ingredients=[SolrDocument{id=" + (id + 6) + ", name_s=cocoa}, SolrDocument{id=" + (id + 7) + ", name_s=cocoa}]}]}";
+  }
+
+  private static String generateDocHierarchy(int i) {
+    boolean doubleIngredient = i % 2 == 0;
+    String currIngredient = doubleIngredient ? ingredients[1]: ingredientsCycler.next();
+    return "{\n" +
+              "\"add\": {\n" +
+                "\"doc\": {\n" +
+                  "\"id\": " + id() + ", \n" +
+                  "\"type_s\": \"" + types[i % types.length] + "\", \n" +
+                  "\"lonely\": {\"id\": " + id() + ", \"test_s\": \"testing\", \"lonelyGrandChild\": {\"id\": " + id() + ", \"test2_s\": \"secondTest\"}}, \n" +
+                  "\"name_s\": " + names[i % names.length] +
+                  "\"toppings\": [ \n" +
+                    "{\"id\": " + id() + ", \"type_s\":\"Regular\"," +
+                      "\"ingredients\": [{\"id\": " + id() + "," +
+                        "\"name_s\": \"" + currIngredient + "\"}]" +
+                    "},\n" +
+                    "{\"id\": " + id() + ", \"type_s\":\"Chocolate\"," +
+                      "\"ingredients\": [{\"id\": " + id() + "," +
+                        "\"name_s\": \"" + ingredients[1] + "\"}," +
+                        "{\"id\": " + id() + ",\n" + "\"name_s\": \"" + ingredients[1] +"\"" +
+                        "}]" +
+                  "}]\n" +
+                "}\n" +
+              "}\n" +
+            "}";
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java
----------------------------------------------------------------------
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 a09a476..b386485 100644
--- a/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java
+++ b/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java
@@ -90,7 +90,7 @@ public class TestNestedUpdateProcessor extends SolrTestCaseJ4 {
 
   @BeforeClass
   public static void beforeClass() throws Exception {
-    initCore("solrconfig-update-processor-chains.xml", "schema15.xml");
+    initCore("solrconfig-update-processor-chains.xml", "schema-nest.xml");
   }
 
   @Before
@@ -107,8 +107,8 @@ public class TestNestedUpdateProcessor extends SolrTestCaseJ4 {
     };
     indexSampleData(jDoc);
 
-    assertJQ(req("q", IndexSchema.NEST_PATH_FIELD_NAME + ":*/grandChild#*",
-        "fl","*",
+    assertJQ(req("q", IndexSchema.NEST_PATH_FIELD_NAME + ":*/grandChild",
+        "fl","*, _nest_path_",
         "sort","id desc",
         "wt","json"),
         tests);
@@ -124,14 +124,14 @@ public class TestNestedUpdateProcessor extends SolrTestCaseJ4 {
     };
     indexSampleData(jDoc);
 
-    assertJQ(req("q", IndexSchema.NEST_PATH_FIELD_NAME + ":children#?",
-        "fl","*",
+    assertJQ(req("q", IndexSchema.NEST_PATH_FIELD_NAME + ":children/",
+        "fl","*, _nest_path_",
         "sort","id asc",
         "wt","json"),
         childrenTests);
 
-    assertJQ(req("q", IndexSchema.NEST_PATH_FIELD_NAME + ":anotherChildList#?",
-        "fl","*",
+    assertJQ(req("q", IndexSchema.NEST_PATH_FIELD_NAME + ":anotherChildList/",
+        "fl","*, _nest_path_",
         "sort","id asc",
         "wt","json"),
         "/response/docs/[0]/id=='4'",

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/171cfc8e/solr/solrj/src/java/org/apache/solr/common/SolrDocument.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/common/SolrDocument.java b/solr/solrj/src/java/org/apache/solr/common/SolrDocument.java
index 43a7983..910ec51 100644
--- a/solr/solrj/src/java/org/apache/solr/common/SolrDocument.java
+++ b/solr/solrj/src/java/org/apache/solr/common/SolrDocument.java
@@ -105,7 +105,7 @@ public class SolrDocument extends SolrDocumentBase<Object, SolrDocument> impleme
     else if( value instanceof NamedList ) {
       // nothing
     }
-    else if( value instanceof Iterable ) {
+    else if( value instanceof Iterable && !(value instanceof SolrDocumentBase)) {
       ArrayList<Object> lst = new ArrayList<>();
       for( Object o : (Iterable)value ) {
         lst.add( o );
@@ -154,7 +154,7 @@ public class SolrDocument extends SolrDocumentBase<Object, SolrDocument> impleme
     }
     
     // Add the values to the collection
-    if( value instanceof Iterable ) {
+    if( value instanceof Iterable && !(value instanceof SolrDocumentBase)) {
       for( Object o : (Iterable<Object>)value ) {
         vals.add( o );
       }