diff --git a/src/mongo/db/catalog/collection_impl.cpp b/src/mongo/db/catalog/collection_impl.cpp
index 4389b6b764..f019cd5879 100644
--- a/src/mongo/db/catalog/collection_impl.cpp
+++ b/src/mongo/db/catalog/collection_impl.cpp
@@ -240,16 +240,25 @@ Status CollectionImpl::checkValidation(OperationContext* opCtx, const BSONObj& d
     if (documentValidationDisabled(opCtx))
         return Status::OK();
 
-    if (_validator->matchesBSON(document))
+    std::deque<std::string> explain;
+    if (_validator->matchesBSON(document, nullptr, &explain))
         return Status::OK();
 
+    StringBuilder builder;
+    for (auto&& str : explain) {
+        builder << std::move(str);
+    }
+    const auto reason = builder.str();
+
     if (_validationAction == ValidationAction::WARN) {
         warning() << "Document would fail validation"
-                  << " collection: " << ns() << " doc: " << redact(document);
+                  << " collection: " << ns() << " doc: " << redact(document)
+                  << " reason: " << reason;
         return Status::OK();
     }
 
-    return {ErrorCodes::DocumentValidationFailure, "Document failed validation"};
+    return {ErrorCodes::DocumentValidationFailure,
+            str::stream() << "Document failed validation: " << reason};
 }
 
 StatusWithMatchExpression CollectionImpl::parseValidator(
diff --git a/src/mongo/db/catalog/collection_impl.h b/src/mongo/db/catalog/collection_impl.h
index 9c146f8b55..663f028b4b 100644
--- a/src/mongo/db/catalog/collection_impl.h
+++ b/src/mongo/db/catalog/collection_impl.h
@@ -118,7 +118,7 @@ public:
                  Snapshotted<BSONObj>* out) const final;
 
     std::unique_ptr<SeekableRecordCursor> getCursor(OperationContext* opCtx,
-                                                    bool forward = true) const final;
+                                                    bool forward) const final;
 
     /**
      * Deletes the document with the given RecordId from the collection.
diff --git a/src/mongo/db/exec/geo_near.cpp b/src/mongo/db/exec/geo_near.cpp
index 80923c4f55..248e0f970e 100644
--- a/src/mongo/db/exec/geo_near.cpp
+++ b/src/mongo/db/exec/geo_near.cpp
@@ -508,7 +508,9 @@ public:
         out->append("TwoDPtInAnnulusExpression", true);
     }
 
-    bool matchesSingleElement(const BSONElement& e, MatchDetails* details = nullptr) const final {
+    bool matchesSingleElement(const BSONElement& e,
+                              ArrayPositionalMatch* = nullptr,
+                              std::deque<std::string>* = nullptr) const final {
         if (!e.isABSONObj())
             return false;
 
diff --git a/src/mongo/db/exec/projection_exec.cpp b/src/mongo/db/exec/projection_exec.cpp
index ae17257c9e..acdeac4485 100644
--- a/src/mongo/db/exec/projection_exec.cpp
+++ b/src/mongo/db/exec/projection_exec.cpp
@@ -266,11 +266,11 @@ Status ProjectionExec::transform(WorkingSetMember* member) const {
 
     BSONObjBuilder bob;
     if (member->hasObj()) {
-        MatchDetails matchDetails;
+        ArrayPositionalMatch matchDetails;
 
-        // If it's a positional projection we need a MatchDetails.
+        // If it's a positional projection we need a ArrayPositionalMatch.
         if (transformRequiresDetails()) {
-            matchDetails.requestElemMatchKey();
+            matchDetails.requestArrayPosition();
             verify(NULL != _queryExpression);
             verify(_queryExpression->matchesBSON(member->obj.value(), &matchDetails));
         }
@@ -378,7 +378,7 @@ Status ProjectionExec::transform(WorkingSetMember* member) const {
 
 Status ProjectionExec::transform(const BSONObj& in,
                                  BSONObjBuilder* bob,
-                                 const MatchDetails* details) const {
+                                 const ArrayPositionalMatch* details) const {
     const ArrayOpType& arrayOpType = _arrayOpType;
 
     BSONObjIterator it(in);
@@ -408,8 +408,8 @@ Status ProjectionExec::transform(const BSONObj& in,
             return Status(ErrorCodes::BadValue, "Matchers are only supported for $elemMatch");
         }
 
-        MatchDetails arrayDetails;
-        arrayDetails.requestElemMatchKey();
+        ArrayPositionalMatch arrayDetails;
+        arrayDetails.requestArrayPosition();
 
         if (matcher->second->matchesBSON(in, &arrayDetails)) {
             FieldMap::const_iterator fieldIt = _fields.find(elt.fieldName());
@@ -426,13 +426,14 @@ Status ProjectionExec::transform(const BSONObj& in,
                               "$elemMatch called on document element with eoo");
             }
 
-            if (in.getField(elt.fieldName()).Obj().getField(arrayDetails.elemMatchKey()).eoo()) {
+            invariant(arrayDetails.arrayPosition());
+            const auto& pos = *arrayDetails.arrayPosition();
+            if (in.getField(elt.fieldName()).Obj().getField(pos).eoo()) {
                 return Status(ErrorCodes::InternalError,
                               "$elemMatch called on array element with eoo");
             }
 
-            arrBuilder.append(
-                in.getField(elt.fieldName()).Obj().getField(arrayDetails.elemMatchKey()));
+            arrBuilder.append(in.getField(elt.fieldName()).Obj().getField(pos));
             subBob.appendArray(matcher->first, arrBuilder.arr());
             Status status = append(bob, subBob.done().firstElement(), details, arrayOpType);
             if (!status.isOK()) {
@@ -492,7 +493,7 @@ void ProjectionExec::appendArray(BSONObjBuilder* bob, const BSONObj& array, bool
 
 Status ProjectionExec::append(BSONObjBuilder* bob,
                               const BSONElement& elt,
-                              const MatchDetails* details,
+                              const ArrayPositionalMatch* details,
                               const ArrayOpType arrayOpType) const {
     // Skip if the field name matches a computed $meta field.
     // $meta projection fields can exist at the top level of
@@ -528,7 +529,7 @@ Status ProjectionExec::append(BSONObjBuilder* bob,
         BSONObjBuilder matchedBuilder;
         if (details && arrayOpType == ARRAY_OP_POSITIONAL) {
             // $ positional operator specified
-            if (!details->hasElemMatchKey()) {
+            if (!details->arrayPosition()) {
                 mongoutils::str::stream error;
                 error << "positional operator (" << elt.fieldName()
                       << ".$) requires corresponding field"
@@ -536,12 +537,13 @@ Status ProjectionExec::append(BSONObjBuilder* bob,
                 return Status(ErrorCodes::BadValue, error);
             }
 
-            if (elt.embeddedObject()[details->elemMatchKey()].eoo()) {
+            const auto& pos = *details->arrayPosition();
+            if (elt.embeddedObject()[pos].eoo()) {
                 return Status(ErrorCodes::BadValue, "positional operator element mismatch");
             }
 
             // append as the first and only element in the projected array
-            matchedBuilder.appendAs(elt.embeddedObject()[details->elemMatchKey()], "0");
+            matchedBuilder.appendAs(elt.embeddedObject()[pos], "0");
         } else {
             // append exact array; no subarray matcher specified
             subfm.appendArray(&matchedBuilder, elt.embeddedObject());
diff --git a/src/mongo/db/exec/projection_exec.h b/src/mongo/db/exec/projection_exec.h
index 829cdf0d43..b3f2577de2 100644
--- a/src/mongo/db/exec/projection_exec.h
+++ b/src/mongo/db/exec/projection_exec.h
@@ -110,7 +110,7 @@ private:
      */
     Status transform(const BSONObj& in,
                      BSONObjBuilder* bob,
-                     const MatchDetails* details = NULL) const;
+                     const ArrayPositionalMatch* details = NULL) const;
 
     /**
      * See transform(...) above.
@@ -125,7 +125,7 @@ private:
      */
     Status append(BSONObjBuilder* bob,
                   const BSONElement& elt,
-                  const MatchDetails* details = NULL,
+                  const ArrayPositionalMatch* details = NULL,
                   const ArrayOpType arrayOpType = ARRAY_OP_NORMAL) const;
 
     /**
@@ -166,7 +166,7 @@ private:
 
     ArrayOpType _arrayOpType;
 
-    // The full query expression.  Used when we need MatchDetails.
+    // The full query expression.  Used when we need ArrayPositionalMatch.
     const MatchExpression* _queryExpression;
 
     // Projections that aren't sourced from the document or index keys.
diff --git a/src/mongo/db/exec/update.cpp b/src/mongo/db/exec/update.cpp
index cac39a2972..ab51af6b0c 100644
--- a/src/mongo/db/exec/update.cpp
+++ b/src/mongo/db/exec/update.cpp
@@ -231,15 +231,16 @@ BSONObj UpdateStage::transformAndUpdate(const Snapshotted<BSONObj>& oldObj, Reco
             StringData(), &_doc, validateForStorage, immutablePaths, &logObj, &docWasModified);
     } else {
         // If there was a matched field, obtain it.
-        MatchDetails matchDetails;
-        matchDetails.requestElemMatchKey();
+        ArrayPositionalMatch matchDetails;
+        matchDetails.requestArrayPosition();
 
         dassert(cq);
         verify(cq->root()->matchesBSON(oldObj.value(), &matchDetails));
 
         string matchedField;
-        if (matchDetails.hasElemMatchKey())
-            matchedField = matchDetails.elemMatchKey();
+        if (matchDetails.arrayPosition()) {
+            matchedField = *matchDetails.arrayPosition();
+        }
 
         status = driver->update(
             matchedField, &_doc, validateForStorage, immutablePaths, &logObj, &docWasModified);
diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript
index 4ed8aa31be..ee8aa3232a 100644
--- a/src/mongo/db/matcher/SConscript
+++ b/src/mongo/db/matcher/SConscript
@@ -29,6 +29,7 @@ env.CppUnitTest(
 env.Library(
     target='expressions',
     source=[
+        'array_positional_match.cpp',
         'expression.cpp',
         'expression_algo.cpp',
         'expression_array.cpp',
@@ -46,7 +47,6 @@ env.Library(
         'expression_with_placeholder.cpp',
         'extensions_callback.cpp',
         'extensions_callback_noop.cpp',
-        'match_details.cpp',
         'matchable.cpp',
         'matcher.cpp',
         'matcher_type_set.cpp',
diff --git a/src/mongo/db/matcher/array_positional_match.cpp b/src/mongo/db/matcher/array_positional_match.cpp
new file mode 100644
index 0000000000..5e6c20d5a3
--- /dev/null
+++ b/src/mongo/db/matcher/array_positional_match.cpp
@@ -0,0 +1,49 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or  modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/matcher/array_positional_match.h"
+
+#include "mongo/base/string_data.h"
+
+namespace mongo {
+void ArrayPositionalMatch::reset() {
+    _arrayPosition = boost::none;
+}
+
+void ArrayPositionalMatch::requestArrayPosition() {
+    _arrayPositionRequested = true;
+}
+
+void ArrayPositionalMatch::setArrayPosition(StringData pos) {
+    if (_arrayPositionRequested) {
+        _arrayPosition = pos.toString();
+    }
+}
+}  // namespace mongo
diff --git a/src/mongo/db/matcher/array_positional_match.h b/src/mongo/db/matcher/array_positional_match.h
new file mode 100644
index 0000000000..895130c9dc
--- /dev/null
+++ b/src/mongo/db/matcher/array_positional_match.h
@@ -0,0 +1,84 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or  modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#include <boost/optional.hpp>
+#include <memory>
+#include <string>
+
+#include "mongo/base/string_data.h"
+
+namespace mongo {
+/**
+ * Stores the offset into an array for which an element satisfies a match predicate. Used for
+ * $elemMatch and $-positional projections.
+ */
+class ArrayPositionalMatch {
+public:
+    ArrayPositionalMatch() = default;
+
+    /**
+     * Returns true if a request has been made to store the array offset.
+     */
+    bool arrayPositionRequested() const {
+        return _arrayPositionRequested;
+    }
+
+    /**
+     * The array offset that satisfies the match predicate, if set.
+     */
+    boost::optional<std::string> arrayPosition() const {
+        return _arrayPosition;
+    }
+
+    /**
+     * Requests that the array match position be recorded.
+     */
+    void requestArrayPosition();
+
+    /**
+     * Sets the array match position to 'pos'. No effect if the array position has not been
+     * requested.
+     */
+    void setArrayPosition(StringData pos);
+
+    /**
+     * Resets any recorded array position. (If the position has been requested, though, it remains
+     * requested.)
+     */
+    void reset();
+
+private:
+    bool _arrayPositionRequested;
+
+    // If set, this contains the array index for which a match predicate was satisfied. The index is
+    // stored as a field name string (e.g. "1").
+    boost::optional<std::string> _arrayPosition;
+};
+}  // namespace mongo
diff --git a/src/mongo/db/matcher/expression.cpp b/src/mongo/db/matcher/expression.cpp
index 0c0efc660c..fb8f00192c 100644
--- a/src/mongo/db/matcher/expression.cpp
+++ b/src/mongo/db/matcher/expression.cpp
@@ -56,14 +56,18 @@ void MatchExpression::_debugAddSpace(StringBuilder& debug, int level) const {
         debug << "    ";
 }
 
-bool MatchExpression::matchesBSON(const BSONObj& doc, MatchDetails* details) const {
+bool MatchExpression::matchesBSON(const BSONObj& doc,
+                                  ArrayPositionalMatch* details,
+                                  std::deque<std::string>* explain) const {
     BSONMatchableDocument mydoc(doc);
-    return matches(&mydoc, details);
+    return matches(&mydoc, details, explain);
 }
 
-bool MatchExpression::matchesBSONElement(BSONElement elem, MatchDetails* details) const {
+bool MatchExpression::matchesBSONElement(BSONElement elem,
+                                         ArrayPositionalMatch* details,
+                                         std::deque<std::string>* explain) const {
     BSONElementViewMatchableDocument matchableDoc(elem);
-    return matches(&matchableDoc, details);
+    return matches(&matchableDoc, details, explain);
 }
 
 void MatchExpression::setCollator(const CollatorInterface* collator) {
diff --git a/src/mongo/db/matcher/expression.h b/src/mongo/db/matcher/expression.h
index bc0796dc1a..a1508c9792 100644
--- a/src/mongo/db/matcher/expression.h
+++ b/src/mongo/db/matcher/expression.h
@@ -35,7 +35,7 @@
 #include "mongo/base/status.h"
 #include "mongo/bson/bsonobj.h"
 #include "mongo/bson/bsonobjbuilder.h"
-#include "mongo/db/matcher/match_details.h"
+#include "mongo/db/matcher/array_positional_match.h"
 #include "mongo/db/matcher/matchable.h"
 #include "mongo/db/pipeline/dependencies.h"
 #include "mongo/stdx/functional.h"
@@ -214,12 +214,23 @@ public:
     virtual bool equivalent(const MatchExpression* other) const = 0;
 
     //
-    // Determine if a document satisfies the tree-predicate.
+    // Matching API.
     //
 
-    virtual bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const = 0;
+    /**
+     * Returns true if 'doc' satisfies the query predicate. If 'details' is set, it is filled with
+     * the array offset of the match if requested.
+     */
+    virtual bool matches(const MatchableDocument* doc,
+                         ArrayPositionalMatch* details = nullptr,
+                         std::deque<std::string>* explain = nullptr) const = 0;
 
-    virtual bool matchesBSON(const BSONObj& doc, MatchDetails* details = nullptr) const;
+    /**
+     * Returns true if the BSONObj satisfies the query predicate.
+     */
+    virtual bool matchesBSON(const BSONObj& doc,
+                             ArrayPositionalMatch* details = nullptr,
+                             std::deque<std::string>* = nullptr) const;
 
     /**
      * Determines if 'elem' would satisfy the predicate if wrapped with the top-level field name of
@@ -228,14 +239,17 @@ public:
      * obj["a"]["0"] because it performs the match as if the element at "a.0" were the BSONObj {i:
      * 5}.
      */
-    virtual bool matchesBSONElement(BSONElement elem, MatchDetails* details = nullptr) const;
+    virtual bool matchesBSONElement(BSONElement elem,
+                                    ArrayPositionalMatch* details = nullptr,
+                                    std::deque<std::string>* = nullptr) const;
 
     /**
      * Determines if the element satisfies the tree-predicate.
      * Not valid for all expressions (e.g. $where); in those cases, returns false.
      */
     virtual bool matchesSingleElement(const BSONElement& e,
-                                      MatchDetails* details = nullptr) const = 0;
+                                      ArrayPositionalMatch* details = nullptr,
+                                      std::deque<std::string>* = nullptr) const = 0;
 
     //
     // Tagging mechanism: Hang data off of the tree for retrieval later.
diff --git a/src/mongo/db/matcher/expression_always_boolean.h b/src/mongo/db/matcher/expression_always_boolean.h
index f7186d7028..6a87c84b7c 100644
--- a/src/mongo/db/matcher/expression_always_boolean.h
+++ b/src/mongo/db/matcher/expression_always_boolean.h
@@ -44,11 +44,15 @@ public:
      */
     virtual StringData name() const = 0;
 
-    bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const final {
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details = nullptr,
+                 std::deque<std::string>* explain = nullptr) const final {
         return _value;
     }
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final {
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
         return _value;
     }
 
diff --git a/src/mongo/db/matcher/expression_array.cpp b/src/mongo/db/matcher/expression_array.cpp
index e4c9af9a02..4c01fc7560 100644
--- a/src/mongo/db/matcher/expression_array.cpp
+++ b/src/mongo/db/matcher/expression_array.cpp
@@ -36,12 +36,16 @@
 namespace mongo {
 
 bool ArrayMatchingMatchExpression::matchesSingleElement(const BSONElement& elt,
-                                                        MatchDetails* details) const {
+                                                        ArrayPositionalMatch* details,
+                                                        std::deque<std::string>* explain) const {
     if (elt.type() != BSONType::Array) {
+        if (explain) {
+            explain->push_front(str::stream() << elt.fieldNameStringData() << " is not an array");
+        }
         return false;
     }
 
-    return matchesArray(elt.embeddedObject(), details);
+    return matchesArray(elt.embeddedObject(), details, explain);
 }
 
 
@@ -72,19 +76,24 @@ ElemMatchObjectMatchExpression::ElemMatchObjectMatchExpression(StringData path,
     : ArrayMatchingMatchExpression(ELEM_MATCH_OBJECT, path), _sub(sub) {}
 
 bool ElemMatchObjectMatchExpression::matchesArray(const BSONObj& anArray,
-                                                  MatchDetails* details) const {
+                                                  ArrayPositionalMatch* details,
+                                                  std::deque<std::string>* explain) const {
     BSONObjIterator i(anArray);
     while (i.more()) {
         BSONElement inner = i.next();
         if (!inner.isABSONObj())
             continue;
-        if (_sub->matchesBSON(inner.Obj(), NULL)) {
-            if (details && details->needRecord()) {
-                details->setElemMatchKey(inner.fieldName());
+        if (_sub->matchesBSON(inner.Obj(), nullptr, explain)) {
+            if (details && details->arrayPositionRequested()) {
+                details->setArrayPosition(inner.fieldNameStringData());
             }
             return true;
         }
     }
+    if (explain) {
+        explain->push_front(str::stream() << "no elements of array '" << path()
+                                          << "' match the predicate");
+    }
     return false;
 }
 
@@ -138,14 +147,15 @@ void ElemMatchValueMatchExpression::add(MatchExpression* sub) {
 }
 
 bool ElemMatchValueMatchExpression::matchesArray(const BSONObj& anArray,
-                                                 MatchDetails* details) const {
+                                                 ArrayPositionalMatch* details,
+                                                 std::deque<std::string>* explain) const {
     BSONObjIterator i(anArray);
     while (i.more()) {
         BSONElement inner = i.next();
 
         if (_arrayElementMatchesAll(inner)) {
-            if (details && details->needRecord()) {
-                details->setElemMatchKey(inner.fieldName());
+            if (details && details->arrayPositionRequested()) {
+                details->setArrayPosition(inner.fieldNameStringData());
             }
             return true;
         }
@@ -207,7 +217,9 @@ MatchExpression::ExpressionOptimizerFunc ElemMatchValueMatchExpression::getOptim
 SizeMatchExpression::SizeMatchExpression(StringData path, int size)
     : ArrayMatchingMatchExpression(SIZE, path), _size(size) {}
 
-bool SizeMatchExpression::matchesArray(const BSONObj& anArray, MatchDetails* details) const {
+bool SizeMatchExpression::matchesArray(const BSONObj& anArray,
+                                       ArrayPositionalMatch* details,
+                                       std::deque<std::string>* explain) const {
     if (_size < 0)
         return false;
     return anArray.nFields() == _size;
diff --git a/src/mongo/db/matcher/expression_array.h b/src/mongo/db/matcher/expression_array.h
index 454f6a4a33..2505279a1a 100644
--- a/src/mongo/db/matcher/expression_array.h
+++ b/src/mongo/db/matcher/expression_array.h
@@ -58,9 +58,13 @@ public:
      *
      * 'anArray' must be the nested array at this expression's path.
      */
-    virtual bool matchesArray(const BSONObj& anArray, MatchDetails* details) const = 0;
+    virtual bool matchesArray(const BSONObj& anArray,
+                              ArrayPositionalMatch* details,
+                              std::deque<std::string>* explain) const = 0;
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     bool equivalent(const MatchExpression* other) const override;
 
@@ -73,7 +77,9 @@ class ElemMatchObjectMatchExpression : public ArrayMatchingMatchExpression {
 public:
     ElemMatchObjectMatchExpression(StringData path, MatchExpression* sub);
 
-    bool matchesArray(const BSONObj& anArray, MatchDetails* details) const;
+    bool matchesArray(const BSONObj& anArray,
+                      ArrayPositionalMatch* details,
+                      std::deque<std::string>* explain) const;
 
     virtual std::unique_ptr<MatchExpression> shallowClone() const {
         std::unique_ptr<ElemMatchObjectMatchExpression> e =
@@ -126,7 +132,9 @@ public:
 
     void add(MatchExpression* sub);
 
-    bool matchesArray(const BSONObj& anArray, MatchDetails* details) const;
+    bool matchesArray(const BSONObj& anArray,
+                      ArrayPositionalMatch* details,
+                      std::deque<std::string>* explain) const;
 
     virtual std::unique_ptr<MatchExpression> shallowClone() const {
         std::unique_ptr<ElemMatchValueMatchExpression> e =
@@ -189,7 +197,9 @@ public:
         return nullptr;
     }
 
-    virtual bool matchesArray(const BSONObj& anArray, MatchDetails* details) const;
+    virtual bool matchesArray(const BSONObj& anArray,
+                              ArrayPositionalMatch* details,
+                              std::deque<std::string>* explain) const;
 
     virtual void debugString(StringBuilder& debug, int level) const;
 
diff --git a/src/mongo/db/matcher/expression_array_test.cpp b/src/mongo/db/matcher/expression_array_test.cpp
index 4d014818c2..892b5d6f93 100644
--- a/src/mongo/db/matcher/expression_array_test.cpp
+++ b/src/mongo/db/matcher/expression_array_test.cpp
@@ -120,22 +120,22 @@ TEST(ElemMatchObjectMatchExpression, ElemMatchKey) {
     BSONObj baseOperand = BSON("c" << 6);
     unique_ptr<ComparisonMatchExpression> eq(new EqualityMatchExpression("c", baseOperand["c"]));
     ElemMatchObjectMatchExpression op("a.b", eq.release());
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!op.matchesBSON(BSONObj(), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(!op.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY(BSON("c" << 7)))), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(op.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY(3 << BSON("c" << 6)))), &details));
-    ASSERT(details.hasElemMatchKey());
+    ASSERT(details.arrayPosition());
     // The entry within the $elemMatch array is reported.
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT_EQUALS("1", *details.arrayPosition());
     ASSERT(op.matchesBSON(
         BSON("a" << BSON_ARRAY(1 << 2 << BSON("b" << BSON_ARRAY(3 << 5 << BSON("c" << 6))))),
         &details));
-    ASSERT(details.hasElemMatchKey());
+    ASSERT(details.arrayPosition());
     // The entry within a parent of the $elemMatch array is reported.
-    ASSERT_EQUALS("2", details.elemMatchKey());
+    ASSERT_EQUALS("2", *details.arrayPosition());
 }
 
 TEST(ElemMatchObjectMatchExpression, Collation) {
@@ -228,21 +228,21 @@ TEST(ElemMatchValueMatchExpression, ElemMatchKey) {
     BSONObj baseOperand = BSON("$gt" << 6);
     unique_ptr<ComparisonMatchExpression> gt(new GTMatchExpression("", baseOperand["$gt"]));
     ElemMatchValueMatchExpression op("a.b", gt.release());
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!op.matchesBSON(BSONObj(), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(!op.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY(2))), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(op.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY(3 << 7))), &details));
-    ASSERT(details.hasElemMatchKey());
+    ASSERT(details.arrayPosition());
     // The entry within the $elemMatch array is reported.
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT_EQUALS("1", *details.arrayPosition());
     ASSERT(op.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2 << BSON("b" << BSON_ARRAY(3 << 7)))),
                           &details));
-    ASSERT(details.hasElemMatchKey());
+    ASSERT(details.arrayPosition());
     // The entry within a parent of the $elemMatch array is reported.
-    ASSERT_EQUALS("2", details.elemMatchKey());
+    ASSERT_EQUALS("2", *details.arrayPosition());
 }
 
 /**
@@ -394,16 +394,16 @@ TEST(SizeMatchExpression, MatchesNestedArray) {
 
 TEST(SizeMatchExpression, ElemMatchKey) {
     SizeMatchExpression size("a.b", 3);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!size.matchesBSON(BSON("a" << 1), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(size.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY(1 << 2 << 3))), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(size.matchesBSON(BSON("a" << BSON_ARRAY(2 << BSON("b" << BSON_ARRAY(1 << 2 << 3)))),
                             &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(SizeMatchExpression, Equivalent) {
diff --git a/src/mongo/db/matcher/expression_expr.cpp b/src/mongo/db/matcher/expression_expr.cpp
index 18883040f9..a182c830bf 100644
--- a/src/mongo/db/matcher/expression_expr.cpp
+++ b/src/mongo/db/matcher/expression_expr.cpp
@@ -40,7 +40,9 @@ ExprMatchExpression::ExprMatchExpression(BSONElement elem,
     : ExprMatchExpression(Expression::parseOperand(expCtx, elem, expCtx->variablesParseState),
                           expCtx) {}
 
-bool ExprMatchExpression::matches(const MatchableDocument* doc, MatchDetails* details) const {
+bool ExprMatchExpression::matches(const MatchableDocument* doc,
+                                  ArrayPositionalMatch* details,
+                                  std::deque<std::string>* explain) const {
     if (_rewriteResult && _rewriteResult->matchExpression() &&
         !_rewriteResult->matchExpression()->matches(doc, details)) {
         return false;
diff --git a/src/mongo/db/matcher/expression_expr.h b/src/mongo/db/matcher/expression_expr.h
index bd50d2e94f..060c57d5e1 100644
--- a/src/mongo/db/matcher/expression_expr.h
+++ b/src/mongo/db/matcher/expression_expr.h
@@ -49,11 +49,15 @@ public:
     ExprMatchExpression(boost::intrusive_ptr<Expression> expr,
                         const boost::intrusive_ptr<ExpressionContext>& expCtx);
 
-    bool matchesSingleElement(const BSONElement& e, MatchDetails* details = nullptr) const final {
+    bool matchesSingleElement(const BSONElement& e,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
         MONGO_UNREACHABLE;
     }
 
-    bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const final;
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details = nullptr,
+                 std::deque<std::string>* explain = nullptr) const final;
 
     std::unique_ptr<MatchExpression> shallowClone() const final;
 
diff --git a/src/mongo/db/matcher/expression_geo.cpp b/src/mongo/db/matcher/expression_geo.cpp
index 5cab016b9b..837f4febe9 100644
--- a/src/mongo/db/matcher/expression_geo.cpp
+++ b/src/mongo/db/matcher/expression_geo.cpp
@@ -345,7 +345,9 @@ GeoMatchExpression::GeoMatchExpression(StringData path,
                                        const BSONObj& rawObj)
     : LeafMatchExpression(GEO, path), _rawObj(rawObj), _query(query), _canSkipValidation(false) {}
 
-bool GeoMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetails* details) const {
+bool GeoMatchExpression::matchesSingleElement(const BSONElement& e,
+                                              ArrayPositionalMatch* details,
+                                              std::deque<std::string>* explain) const {
     if (!e.isABSONObj())
         return false;
 
@@ -430,7 +432,8 @@ GeoNearMatchExpression::GeoNearMatchExpression(StringData path,
     : LeafMatchExpression(GEO_NEAR, path), _rawObj(rawObj), _query(query) {}
 
 bool GeoNearMatchExpression::matchesSingleElement(const BSONElement& e,
-                                                  MatchDetails* details) const {
+                                                  ArrayPositionalMatch* details,
+                                                  std::deque<std::string>* explain) const {
     return true;
 }
 
diff --git a/src/mongo/db/matcher/expression_geo.h b/src/mongo/db/matcher/expression_geo.h
index 34b015484e..a22ed37bcd 100644
--- a/src/mongo/db/matcher/expression_geo.h
+++ b/src/mongo/db/matcher/expression_geo.h
@@ -85,7 +85,9 @@ public:
 
     virtual ~GeoMatchExpression() {}
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual void debugString(StringBuilder& debug, int level = 0) const;
 
@@ -176,7 +178,9 @@ public:
      * Stub implementation that should never be called, since geoNear execution requires an
      * appropriate geo index.
      */
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual void debugString(StringBuilder& debug, int level = 0) const;
 
diff --git a/src/mongo/db/matcher/expression_internal_expr_eq.cpp b/src/mongo/db/matcher/expression_internal_expr_eq.cpp
index 61306a13fd..61f22bca55 100644
--- a/src/mongo/db/matcher/expression_internal_expr_eq.cpp
+++ b/src/mongo/db/matcher/expression_internal_expr_eq.cpp
@@ -38,7 +38,8 @@ namespace mongo {
 constexpr StringData InternalExprEqMatchExpression::kName;
 
 bool InternalExprEqMatchExpression::matchesSingleElement(const BSONElement& elem,
-                                                         MatchDetails* details) const {
+                                                         ArrayPositionalMatch* details,
+                                                         std::deque<std::string>* explain) const {
     // We use NonLeafArrayBehavior::kMatchSubpath traversal in InternalExprEqMatchExpression. This
     // means matchesSinglElement() will be called when an array is found anywhere along the patch we
     // are matching against. When this occurs, we return 'true' and depend on the corresponding
diff --git a/src/mongo/db/matcher/expression_internal_expr_eq.h b/src/mongo/db/matcher/expression_internal_expr_eq.h
index 3a890eaa7d..458203b1d5 100644
--- a/src/mongo/db/matcher/expression_internal_expr_eq.h
+++ b/src/mongo/db/matcher/expression_internal_expr_eq.h
@@ -65,7 +65,9 @@ public:
         return kName;
     }
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails*) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch*,
+                              std::deque<std::string>* explain) const final;
 
     std::unique_ptr<MatchExpression> shallowClone() const final;
 };
diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp
index b2c92ed892..5a082a9814 100644
--- a/src/mongo/db/matcher/expression_leaf.cpp
+++ b/src/mongo/db/matcher/expression_leaf.cpp
@@ -113,7 +113,8 @@ ComparisonMatchExpression::ComparisonMatchExpression(MatchType type,
 }
 
 bool ComparisonMatchExpression::matchesSingleElement(const BSONElement& e,
-                                                     MatchDetails* details) const {
+                                                     ArrayPositionalMatch* details,
+                                                     std::deque<std::string>* explain) const {
     if (e.canonicalType() != _rhs.canonicalType()) {
         // We can't call 'compareElements' on elements of different canonical types.  Usually
         // elements with different canonical types should never match any comparison, but there are
@@ -153,40 +154,54 @@ bool ComparisonMatchExpression::matchesSingleElement(const BSONElement& e,
         bool bothNaN = std::isnan(e.numberDouble()) && std::isnan(_rhs.numberDouble());
         switch (matchType()) {
             case LT:
+            case GT:
+                if (explain) {
+                    explain->push_front(str::stream() << e.toString(false) << " is not " << name()
+                                                      << " "
+                                                      << _rhs.toString(false));
+                }
                 return false;
             case LTE:
-                return bothNaN;
             case EQ:
-                return bothNaN;
-            case GT:
-                return false;
             case GTE:
+                if (!bothNaN && explain) {
+                    explain->push_front(str::stream() << e.toString(false) << " is not " << name()
+                                                      << " "
+                                                      << _rhs.toString(false));
+                }
                 return bothNaN;
             default:
-                // This is a comparison match expression, so it must be either
-                // a $lt, $lte, $gt, $gte, or equality expression.
-                fassertFailed(17448);
+                MONGO_UNREACHABLE;
         }
     }
 
     int x = BSONElement::compareElements(
         e, _rhs, BSONElement::ComparisonRules::kConsiderFieldName, _collator);
+    bool res;
     switch (matchType()) {
         case LT:
-            return x < 0;
+            res = x < 0;
+            break;
         case LTE:
-            return x <= 0;
+            res = x <= 0;
+            break;
         case EQ:
-            return x == 0;
+            res = x == 0;
+            break;
         case GT:
-            return x > 0;
+            res = x > 0;
+            break;
         case GTE:
-            return x >= 0;
+            res = x >= 0;
+            break;
         default:
-            // This is a comparison match expression, so it must be either
-            // a $lt, $lte, $gt, $gte, or equality expression.
-            fassertFailed(16828);
+            MONGO_UNREACHABLE;
+    }
+    if (!res && explain) {
+        explain->push_front(str::stream() << e.toString(false) << " is not " << name() << " "
+                                          << _rhs.toString(false));
     }
+    return res;
 }
 
 constexpr StringData EqualityMatchExpression::kName;
@@ -258,7 +273,9 @@ bool RegexMatchExpression::equivalent(const MatchExpression* other) const {
         _flags == realOther->_flags;
 }
 
-bool RegexMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetails* details) const {
+bool RegexMatchExpression::matchesSingleElement(const BSONElement& e,
+                                                ArrayPositionalMatch* details,
+                                                std::deque<std::string>* explain) const {
     switch (e.type()) {
         case String:
         case Symbol: {
@@ -266,11 +283,35 @@ bool RegexMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetai
             // pcrecpp::StringPiece instance using the full length of the string to avoid truncating
             // 'data' early.
             pcrecpp::StringPiece data(e.valuestr(), e.valuestrsize() - 1);
-            return _re->PartialMatch(data);
+            bool res = _re->PartialMatch(data);
+            if (!res && explain) {
+                explain->push_front(
+                    str::stream() << e.valueStringData() << " does not match {regex: " << _regex
+                                  << ", flags: "
+                                  << _flags
+                                  << "}");
+            }
+            return res;
+        }
+        case RegEx: {
+            bool res = (_regex == e.regex() && _flags == e.regexFlags());
+            if (!res && explain) {
+                explain->push_front(str::stream() << "{regex: " << _regex << ", flags: " << _flags
+                                                  << "} is not equal to "
+                                                  << "{regex: "
+                                                  << e.regex()
+                                                  << ", flags: "
+                                                  << e.regexFlags()
+                                                  << "}");
+            }
+            return res;
         }
-        case RegEx:
-            return _regex == e.regex() && _flags == e.regexFlags();
         default:
+            if (explain) {
+                explain->push_front(str::stream() << "regex " << _regex
+                                                  << " won't match element of type "
+                                                  << e.type());
+            }
             return false;
     }
 }
@@ -313,7 +354,9 @@ ModMatchExpression::ModMatchExpression(StringData path, int divisor, int remaind
     uassert(ErrorCodes::BadValue, "divisor cannot be 0", divisor != 0);
 }
 
-bool ModMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetails* details) const {
+bool ModMatchExpression::matchesSingleElement(const BSONElement& e,
+                                              ArrayPositionalMatch* details,
+                                              std::deque<std::string>* explain) const {
     if (!e.isNumber())
         return false;
     return e.numberLong() % _divisor == _remainder;
@@ -349,7 +392,8 @@ bool ModMatchExpression::equivalent(const MatchExpression* other) const {
 ExistsMatchExpression::ExistsMatchExpression(StringData path) : LeafMatchExpression(EXISTS, path) {}
 
 bool ExistsMatchExpression::matchesSingleElement(const BSONElement& e,
-                                                 MatchDetails* details) const {
+                                                 ArrayPositionalMatch* details,
+                                                 std::deque<std::string>* explain) const {
     return !e.eoo();
 }
 
@@ -402,7 +446,9 @@ std::unique_ptr<MatchExpression> InMatchExpression::shallowClone() const {
     return std::move(next);
 }
 
-bool InMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetails* details) const {
+bool InMatchExpression::matchesSingleElement(const BSONElement& e,
+                                             ArrayPositionalMatch* details,
+                                             std::deque<std::string>* explain) const {
     if (_hasNull && e.eoo()) {
         return true;
     }
@@ -691,7 +737,8 @@ bool BitTestMatchExpression::performBitTest(const char* eBinary, uint32_t eBinar
 }
 
 bool BitTestMatchExpression::matchesSingleElement(const BSONElement& e,
-                                                  MatchDetails* details) const {
+                                                  ArrayPositionalMatch* details,
+                                                  std::deque<std::string>* explain) const {
     // Validate 'e' is a number or a BinData.
     if (!e.isNumber() && e.type() != BSONType::BinData) {
         return false;
diff --git a/src/mongo/db/matcher/expression_leaf.h b/src/mongo/db/matcher/expression_leaf.h
index b45b1da16a..db303a32a6 100644
--- a/src/mongo/db/matcher/expression_leaf.h
+++ b/src/mongo/db/matcher/expression_leaf.h
@@ -166,7 +166,9 @@ public:
 
     virtual ~ComparisonMatchExpression() = default;
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 };
 
 class EqualityMatchExpression final : public ComparisonMatchExpression {
@@ -300,7 +302,9 @@ public:
         return std::move(e);
     }
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual void debugString(StringBuilder& debug, int level) const;
 
@@ -344,7 +348,9 @@ public:
         return std::move(m);
     }
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual void debugString(StringBuilder& debug, int level) const;
 
@@ -380,7 +386,9 @@ public:
         return std::move(e);
     }
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual void debugString(StringBuilder& debug, int level) const;
 
@@ -403,7 +411,9 @@ public:
 
     virtual std::unique_ptr<MatchExpression> shallowClone() const;
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual void debugString(StringBuilder& debug, int level) const;
 
@@ -492,7 +502,9 @@ public:
                                     uint32_t bitMaskLen);
     virtual ~BitTestMatchExpression() {}
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual void debugString(StringBuilder& debug, int level) const;
 
diff --git a/src/mongo/db/matcher/expression_leaf_test.cpp b/src/mongo/db/matcher/expression_leaf_test.cpp
index 235e2af641..25df1255f2 100644
--- a/src/mongo/db/matcher/expression_leaf_test.cpp
+++ b/src/mongo/db/matcher/expression_leaf_test.cpp
@@ -213,15 +213,15 @@ TEST(EqOp, MatchesThroughNestedArray) {
 TEST(EqOp, ElemMatchKey) {
     BSONObj operand = BSON("a" << 5);
     EqualityMatchExpression eq("a", operand["a"]);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!eq.matchesBSON(BSON("a" << 4), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(eq.matchesBSON(BSON("a" << 5), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(eq.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2 << 5)), &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("2", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("2", *details.arrayPosition());
 }
 
 // SERVER-14886: when an array is being traversed explictly at the same time that a nested array
@@ -231,12 +231,12 @@ TEST(EqOp, ElemMatchKeyWithImplicitAndExplicitTraversal) {
     BSONObj operand = BSON("a.0.b" << 3);
     BSONElement operandFirstElt = operand.firstElement();
     EqualityMatchExpression eq(operandFirstElt.fieldName(), operandFirstElt);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     BSONObj obj = fromjson("{a: [{b: [2, 3]}, {b: [4, 5]}]}");
     ASSERT(eq.matchesBSON(obj, &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(EqOp, Equality1) {
@@ -360,15 +360,15 @@ TEST(LtOp, MatchesMaxKey) {
 TEST(LtOp, ElemMatchKey) {
     BSONObj operand = BSON("$lt" << 5);
     LTMatchExpression lt("a", operand["$lt"]);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!lt.matchesBSON(BSON("a" << 6), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(lt.matchesBSON(BSON("a" << 4), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(lt.matchesBSON(BSON("a" << BSON_ARRAY(6 << 2 << 5)), &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(LteOp, MatchesElement) {
@@ -475,15 +475,15 @@ TEST(LteOp, MatchesMaxKey) {
 TEST(LteOp, ElemMatchKey) {
     BSONObj operand = BSON("$lte" << 5);
     LTEMatchExpression lte("a", operand["$lte"]);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!lte.matchesBSON(BSON("a" << 6), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(lte.matchesBSON(BSON("a" << 4), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(lte.matchesBSON(BSON("a" << BSON_ARRAY(6 << 2 << 5)), &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 DEATH_TEST(GtOp, InvalidEooOperand, "Invariant failure _rhs") {
@@ -577,15 +577,15 @@ TEST(GtOp, MatchesMaxKey) {
 TEST(GtOp, ElemMatchKey) {
     BSONObj operand = BSON("$gt" << 5);
     GTMatchExpression gt("a", operand["$gt"]);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!gt.matchesBSON(BSON("a" << 4), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(gt.matchesBSON(BSON("a" << 6), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(gt.matchesBSON(BSON("a" << BSON_ARRAY(2 << 6 << 5)), &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(GteOp, MatchesElement) {
@@ -692,15 +692,15 @@ TEST(GteOp, MatchesMaxKey) {
 TEST(GteOp, ElemMatchKey) {
     BSONObj operand = BSON("$gte" << 5);
     GTEMatchExpression gte("a", operand["$gte"]);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!gte.matchesBSON(BSON("a" << 4), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(gte.matchesBSON(BSON("a" << 6), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(gte.matchesBSON(BSON("a" << BSON_ARRAY(2 << 6 << 5)), &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(RegexMatchExpression, MatchesElementExact) {
@@ -887,21 +887,21 @@ TEST(RegexMatchExpression, MatchesNull) {
 
 TEST(RegexMatchExpression, ElemMatchKey) {
     RegexMatchExpression regex("a", "b", "");
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!regex.matchesBSON(BSON("a"
                                    << "c"),
                               &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(regex.matchesBSON(BSON("a"
                                   << "b"),
                              &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(regex.matchesBSON(BSON("a" << BSON_ARRAY("c"
                                                     << "b")),
                              &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(RegexMatchExpression, Equality1) {
@@ -1032,15 +1032,15 @@ TEST(ModMatchExpression, MatchesNull) {
 
 TEST(ModMatchExpression, ElemMatchKey) {
     ModMatchExpression mod("a", 5, 2);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!mod.matchesBSON(BSON("a" << 4), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(mod.matchesBSON(BSON("a" << 2), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(mod.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2 << 5)), &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(ModMatchExpression, Equality1) {
@@ -1087,15 +1087,15 @@ TEST(ExistsMatchExpression, MatchesArray) {
 
 TEST(ExistsMatchExpression, ElemMatchKey) {
     ExistsMatchExpression exists("a.b");
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!exists.matchesBSON(BSON("a" << 1), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(exists.matchesBSON(BSON("a" << BSON("b" << 6)), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(exists.matchesBSON(BSON("a" << BSON_ARRAY(2 << BSON("b" << 7))), &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(ExistsMatchExpression, Equivalent) {
@@ -1227,15 +1227,15 @@ TEST(InMatchExpression, ElemMatchKey) {
     std::vector<BSONElement> equalities{operand[0], operand[1]};
     ASSERT_OK(in.setEqualities(std::move(equalities)));
 
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!in.matchesBSON(BSON("a" << 4), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(in.matchesBSON(BSON("a" << 5), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(in.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2 << 5)), &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(InMatchExpression, InMatchExpressionsWithDifferentNumbersOfElementsAreUnequal) {
diff --git a/src/mongo/db/matcher/expression_path.h b/src/mongo/db/matcher/expression_path.h
index 89cf953fd3..c05f1e04b7 100644
--- a/src/mongo/db/matcher/expression_path.h
+++ b/src/mongo/db/matcher/expression_path.h
@@ -53,15 +53,17 @@ public:
 
     virtual ~PathMatchExpression() {}
 
-    bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const final {
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details = nullptr,
+                 std::deque<std::string>* explain = nullptr) const final {
         MatchableDocument::IteratorHolder cursor(doc, &_elementPath);
         while (cursor->more()) {
             ElementIterator::Context e = cursor->next();
-            if (!matchesSingleElement(e.element(), details)) {
+            if (!matchesSingleElement(e.element(), details, explain)) {
                 continue;
             }
-            if (details && details->needRecord() && !e.arrayOffset().eoo()) {
-                details->setElemMatchKey(e.arrayOffset().fieldName());
+            if (details && details->arrayPositionRequested() && !e.arrayOffset().eoo()) {
+                details->setArrayPosition(e.arrayOffset().fieldNameStringData());
             }
             return true;
         }
diff --git a/src/mongo/db/matcher/expression_text.h b/src/mongo/db/matcher/expression_text.h
index 1bf453906b..6e172f3730 100644
--- a/src/mongo/db/matcher/expression_text.h
+++ b/src/mongo/db/matcher/expression_text.h
@@ -48,7 +48,9 @@ public:
         return _ftsQuery;
     }
 
-    bool matchesSingleElement(const BSONElement& e, MatchDetails* details = nullptr) const final {
+    bool matchesSingleElement(const BSONElement& e,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
         // Text match expressions force the selection of the text index and always generate EXACT
         // index bounds (which causes the MatchExpression node to be trimmed), so we don't currently
         // implement any explicit text matching logic here. SERVER-17648 tracks the work to
diff --git a/src/mongo/db/matcher/expression_text_noop.h b/src/mongo/db/matcher/expression_text_noop.h
index 3c2316595a..49abe6a7be 100644
--- a/src/mongo/db/matcher/expression_text_noop.h
+++ b/src/mongo/db/matcher/expression_text_noop.h
@@ -41,7 +41,9 @@ public:
         return _ftsQuery;
     }
 
-    bool matchesSingleElement(const BSONElement& e, MatchDetails* details = nullptr) const final {
+    bool matchesSingleElement(const BSONElement& e,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
         MONGO_UNREACHABLE;
     }
 
diff --git a/src/mongo/db/matcher/expression_tree.cpp b/src/mongo/db/matcher/expression_tree.cpp
index 9c3d79c073..a23b4f9f8d 100644
--- a/src/mongo/db/matcher/expression_tree.cpp
+++ b/src/mongo/db/matcher/expression_tree.cpp
@@ -193,20 +193,30 @@ bool ListOfMatchExpression::equivalent(const MatchExpression* other) const {
 
 // -----
 
-bool AndMatchExpression::matches(const MatchableDocument* doc, MatchDetails* details) const {
+bool AndMatchExpression::matches(const MatchableDocument* doc,
+                                 ArrayPositionalMatch* details,
+                                 std::deque<std::string>* explain) const {
     for (size_t i = 0; i < numChildren(); i++) {
-        if (!getChild(i)->matches(doc, details)) {
+        if (!getChild(i)->matches(doc, details, explain)) {
             if (details)
-                details->resetOutput();
+                details->reset();
+            if (explain) {
+                explain->push_front(str::stream() << "clause " << i << " of $and failed: ");
+            }
             return false;
         }
     }
     return true;
 }
 
-bool AndMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetails* details) const {
+bool AndMatchExpression::matchesSingleElement(const BSONElement& e,
+                                              ArrayPositionalMatch* details,
+                                              std::deque<std::string>* explain) const {
     for (size_t i = 0; i < numChildren(); i++) {
-        if (!getChild(i)->matchesSingleElement(e, details)) {
+        if (!getChild(i)->matchesSingleElement(e, details, explain)) {
+            if (explain) {
+                explain->push_front(str::stream() << "clause " << i << " of $and failed: ");
+            }
             return false;
         }
     }
@@ -238,18 +248,32 @@ bool AndMatchExpression::isTriviallyTrue() const {
 
 // -----
 
-bool OrMatchExpression::matches(const MatchableDocument* doc, MatchDetails* details) const {
+bool OrMatchExpression::matches(const MatchableDocument* doc,
+                                ArrayPositionalMatch*,
+                                std::deque<std::string>* explain) const {
     for (size_t i = 0; i < numChildren(); i++) {
-        if (getChild(i)->matches(doc, NULL)) {
+        // We purposefully do not set the ArrayPositionalMatch. Users can use the array filters
+        // feature to find the array position of a non-matching element.
+        constexpr auto positionalMatch = nullptr;
+        if (getChild(i)->matches(doc, positionalMatch, explain)) {
+            if (explain) {
+                explain->clear();
+            }
             return true;
         }
     }
+
+    if (explain) {
+        explain->push_front("no clauses of $or were satisfied: ");
+    }
     return false;
 }
 
-bool OrMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetails* details) const {
+bool OrMatchExpression::matchesSingleElement(const BSONElement& e,
+                                             ArrayPositionalMatch* details,
+                                             std::deque<std::string>* explain) const {
     for (size_t i = 0; i < numChildren(); i++) {
-        if (getChild(i)->matchesSingleElement(e, details)) {
+        if (getChild(i)->matchesSingleElement(e, details, explain)) {
             return true;
         }
     }
@@ -281,18 +305,25 @@ bool OrMatchExpression::isTriviallyFalse() const {
 
 // ----
 
-bool NorMatchExpression::matches(const MatchableDocument* doc, MatchDetails* details) const {
+bool NorMatchExpression::matches(const MatchableDocument* doc,
+                                 ArrayPositionalMatch* details,
+                                 std::deque<std::string>* explain) const {
     for (size_t i = 0; i < numChildren(); i++) {
-        if (getChild(i)->matches(doc, NULL)) {
+        // We purposefully do not set the ArrayPositionalMatch for negative match expressions. Users
+        // can use the array filters feature to find the array position of a non-matching element.
+        constexpr auto positionalMatch = nullptr;
+        if (getChild(i)->matches(doc, positionalMatch, explain)) {
             return false;
         }
     }
     return true;
 }
 
-bool NorMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetails* details) const {
+bool NorMatchExpression::matchesSingleElement(const BSONElement& e,
+                                              ArrayPositionalMatch* details,
+                                              std::deque<std::string>* explain) const {
     for (size_t i = 0; i < numChildren(); i++) {
-        if (getChild(i)->matchesSingleElement(e, details)) {
+        if (getChild(i)->matchesSingleElement(e, details, explain)) {
             return false;
         }
     }
diff --git a/src/mongo/db/matcher/expression_tree.h b/src/mongo/db/matcher/expression_tree.h
index d92a9118e7..708c48050e 100644
--- a/src/mongo/db/matcher/expression_tree.h
+++ b/src/mongo/db/matcher/expression_tree.h
@@ -111,9 +111,13 @@ public:
     AndMatchExpression() : ListOfMatchExpression(AND) {}
     virtual ~AndMatchExpression() {}
 
-    virtual bool matches(const MatchableDocument* doc, MatchDetails* details = 0) const;
+    virtual bool matches(const MatchableDocument* doc,
+                         ArrayPositionalMatch* = nullptr,
+                         std::deque<std::string>* = nullptr) const;
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual std::unique_ptr<MatchExpression> shallowClone() const {
         std::unique_ptr<AndMatchExpression> self = stdx::make_unique<AndMatchExpression>();
@@ -140,9 +144,13 @@ public:
     OrMatchExpression() : ListOfMatchExpression(OR) {}
     virtual ~OrMatchExpression() {}
 
-    virtual bool matches(const MatchableDocument* doc, MatchDetails* details = 0) const;
+    virtual bool matches(const MatchableDocument* doc,
+                         ArrayPositionalMatch* = nullptr,
+                         std::deque<std::string>* = nullptr) const;
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual std::unique_ptr<MatchExpression> shallowClone() const {
         std::unique_ptr<OrMatchExpression> self = stdx::make_unique<OrMatchExpression>();
@@ -169,9 +177,13 @@ public:
     NorMatchExpression() : ListOfMatchExpression(NOR) {}
     virtual ~NorMatchExpression() {}
 
-    virtual bool matches(const MatchableDocument* doc, MatchDetails* details = 0) const;
+    virtual bool matches(const MatchableDocument* doc,
+                         ArrayPositionalMatch* = nullptr,
+                         std::deque<std::string>* = nullptr) const;
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual std::unique_ptr<MatchExpression> shallowClone() const {
         std::unique_ptr<NorMatchExpression> self = stdx::make_unique<NorMatchExpression>();
@@ -202,12 +214,27 @@ public:
         return std::move(self);
     }
 
-    virtual bool matches(const MatchableDocument* doc, MatchDetails* details = 0) const {
-        return !_exp->matches(doc, NULL);
+    virtual bool matches(const MatchableDocument* doc,
+                         ArrayPositionalMatch* = nullptr,
+                         std::deque<std::string>* explain = nullptr) const {
+        // We purposefully do not set the ArrayPositionalMatch for negative match expressions. Users
+        // can use the array filters feature to find the array position of a non-matching element.
+        constexpr auto positionalMatch = nullptr;
+        bool res = !_exp->matches(doc, positionalMatch, explain);
+        if (!res && explain) {
+            explain->push_front(str::stream() << "subclause evaluated true: " << _exp->toString());
+        }
+        return res;
     }
 
-    bool matchesSingleElement(const BSONElement& elt, MatchDetails* details = nullptr) const final {
-        return !_exp->matchesSingleElement(elt, details);
+    bool matchesSingleElement(const BSONElement& elt,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
+        bool res = !_exp->matchesSingleElement(elt, details, explain);
+        if (!res && explain) {
+            explain->push_front(str::stream() << "subclause evaluated true: " << _exp->toString());
+        }
+        return res;
     }
 
     virtual void debugString(StringBuilder& debug, int level = 0) const;
diff --git a/src/mongo/db/matcher/expression_tree_test.cpp b/src/mongo/db/matcher/expression_tree_test.cpp
index 54e85206d7..a575cb0a2e 100644
--- a/src/mongo/db/matcher/expression_tree_test.cpp
+++ b/src/mongo/db/matcher/expression_tree_test.cpp
@@ -63,15 +63,15 @@ TEST(NotMatchExpression, ElemMatchKey) {
     BSONObj baseOperand = BSON("$lt" << 5);
     unique_ptr<ComparisonMatchExpression> lt(new LTMatchExpression("a", baseOperand["$lt"]));
     NotMatchExpression notOp(lt.release());
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!notOp.matchesBSON(BSON("a" << BSON_ARRAY(1)), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(notOp.matchesBSON(BSON("a" << 6), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(notOp.matchesBSON(BSON("a" << BSON_ARRAY(6)), &details));
-    // elemMatchKey is not implemented for negative match operators.
-    ASSERT(!details.hasElemMatchKey());
+    // arrayPosition is not implemented for negative match operators.
+    ASSERT(!details.arrayPosition());
 }
 
 TEST(NotMatchExpression, SetCollatorPropagatesToChild) {
@@ -166,16 +166,16 @@ TEST(AndOp, ElemMatchKey) {
     andOp.add(sub1.release());
     andOp.add(sub2.release());
 
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!andOp.matchesBSON(BSON("a" << BSON_ARRAY(1)), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(!andOp.matchesBSON(BSON("b" << BSON_ARRAY(2)), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(andOp.matchesBSON(BSON("a" << BSON_ARRAY(1) << "b" << BSON_ARRAY(1 << 2)), &details));
-    ASSERT(details.hasElemMatchKey());
+    ASSERT(details.arrayPosition());
     // The elem match key for the second $and clause is recorded.
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(OrOp, NoClauses) {
@@ -229,15 +229,15 @@ TEST(OrOp, ElemMatchKey) {
     orOp.add(sub1.release());
     orOp.add(sub2.release());
 
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!orOp.matchesBSON(BSONObj(), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(!orOp.matchesBSON(BSON("a" << BSON_ARRAY(10) << "b" << BSON_ARRAY(10)), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(orOp.matchesBSON(BSON("a" << BSON_ARRAY(1) << "b" << BSON_ARRAY(1 << 2)), &details));
     // The elem match key feature is not implemented for $or.
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
 }
 
 TEST(NorOp, NoClauses) {
@@ -292,15 +292,15 @@ TEST(NorOp, ElemMatchKey) {
     norOp.add(sub1.release());
     norOp.add(sub2.release());
 
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!norOp.matchesBSON(BSON("a" << 1), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(!norOp.matchesBSON(BSON("a" << BSON_ARRAY(1) << "b" << BSON_ARRAY(10)), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(norOp.matchesBSON(BSON("a" << BSON_ARRAY(3) << "b" << BSON_ARRAY(4)), &details));
     // The elem match key feature is not implemented for $nor.
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
 }
 
 
diff --git a/src/mongo/db/matcher/expression_type.h b/src/mongo/db/matcher/expression_type.h
index d38ce36514..9dd99d5a59 100644
--- a/src/mongo/db/matcher/expression_type.h
+++ b/src/mongo/db/matcher/expression_type.h
@@ -60,8 +60,21 @@ public:
     }
 
     bool matchesSingleElement(const BSONElement& elem,
-                              MatchDetails* details = nullptr) const final {
-        return _typeSet.hasType(elem.type());
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
+        bool match = _typeSet.hasType(elem.type());
+        if (!match && explain) {
+            StringBuilder builder;
+            builder << "[";
+            for (auto&& type : _typeSet.bsonTypes) {
+                builder << typeName(type) << ", ";
+            }
+            builder << "]";
+            explain->push_front(str::stream() << "element of type " << elem.type()
+                                              << " does not match $type "
+                                              << builder.str());
+        }
+        return match;
     }
 
     void debugString(StringBuilder& debug, int level) const final {
diff --git a/src/mongo/db/matcher/expression_type_test.cpp b/src/mongo/db/matcher/expression_type_test.cpp
index e50f30b9b4..fea848ca73 100644
--- a/src/mongo/db/matcher/expression_type_test.cpp
+++ b/src/mongo/db/matcher/expression_type_test.cpp
@@ -136,21 +136,21 @@ TEST(ExpressionTypeTest, MatchesNull) {
 
 TEST(ExpressionTypeTest, ElemMatchKey) {
     TypeMatchExpression type("a.b", String);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT(!type.matchesBSON(BSON("a" << 1), &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(type.matchesBSON(BSON("a" << BSON("b"
                                              << "string")),
                             &details));
-    ASSERT(!details.hasElemMatchKey());
+    ASSERT(!details.arrayPosition());
     ASSERT(type.matchesBSON(BSON("a" << BSON("b" << BSON_ARRAY("string"))), &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("0", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("0", *details.arrayPosition());
     ASSERT(type.matchesBSON(BSON("a" << BSON_ARRAY(2 << BSON("b" << BSON_ARRAY("string")))),
                             &details));
-    ASSERT(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(ExpressionTypeTest, Equivalent) {
diff --git a/src/mongo/db/matcher/expression_where.cpp b/src/mongo/db/matcher/expression_where.cpp
index dea7756f92..f360c04e15 100644
--- a/src/mongo/db/matcher/expression_where.cpp
+++ b/src/mongo/db/matcher/expression_where.cpp
@@ -70,7 +70,9 @@ WhereMatchExpression::WhereMatchExpression(OperationContext* opCtx,
     uassert(ErrorCodes::BadValue, "$where compile error", _func);
 }
 
-bool WhereMatchExpression::matches(const MatchableDocument* doc, MatchDetails* details) const {
+bool WhereMatchExpression::matches(const MatchableDocument* doc,
+                                   ArrayPositionalMatch* details,
+                                   std::deque<std::string>* explain) const {
     uassert(28692, "$where compile error", _func);
     BSONObj obj = doc->toBSON();
 
diff --git a/src/mongo/db/matcher/expression_where.h b/src/mongo/db/matcher/expression_where.h
index 50062d6583..0f5b66a4f8 100644
--- a/src/mongo/db/matcher/expression_where.h
+++ b/src/mongo/db/matcher/expression_where.h
@@ -39,7 +39,9 @@ class WhereMatchExpression final : public WhereMatchExpressionBase {
 public:
     WhereMatchExpression(OperationContext* opCtx, WhereParams params, StringData dbName);
 
-    bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const final;
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details = nullptr,
+                 std::deque<std::string>* explain = nullptr) const final;
 
     std::unique_ptr<MatchExpression> shallowClone() const final;
 
diff --git a/src/mongo/db/matcher/expression_where_base.h b/src/mongo/db/matcher/expression_where_base.h
index 12e7fd8632..adb6afc0b7 100644
--- a/src/mongo/db/matcher/expression_where_base.h
+++ b/src/mongo/db/matcher/expression_where_base.h
@@ -56,7 +56,9 @@ public:
         return nullptr;
     }
 
-    bool matchesSingleElement(const BSONElement& e, MatchDetails* details = nullptr) const final {
+    bool matchesSingleElement(const BSONElement& e,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
         return false;
     }
 
diff --git a/src/mongo/db/matcher/expression_where_noop.cpp b/src/mongo/db/matcher/expression_where_noop.cpp
index c65bf70019..592bc42cb4 100644
--- a/src/mongo/db/matcher/expression_where_noop.cpp
+++ b/src/mongo/db/matcher/expression_where_noop.cpp
@@ -39,7 +39,9 @@ namespace mongo {
 WhereNoOpMatchExpression::WhereNoOpMatchExpression(WhereParams params)
     : WhereMatchExpressionBase(std::move(params)) {}
 
-bool WhereNoOpMatchExpression::matches(const MatchableDocument* doc, MatchDetails* details) const {
+bool WhereNoOpMatchExpression::matches(const MatchableDocument* doc,
+                                       ArrayPositionalMatch* details,
+                                       std::deque<std::string>* explain) const {
     MONGO_UNREACHABLE;
 }
 
diff --git a/src/mongo/db/matcher/expression_where_noop.h b/src/mongo/db/matcher/expression_where_noop.h
index 0ee8a835c0..b0eda8ff0e 100644
--- a/src/mongo/db/matcher/expression_where_noop.h
+++ b/src/mongo/db/matcher/expression_where_noop.h
@@ -42,7 +42,9 @@ class WhereNoOpMatchExpression final : public WhereMatchExpressionBase {
 public:
     explicit WhereNoOpMatchExpression(WhereParams params);
 
-    bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const final;
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details = nullptr,
+                 std::deque<std::string>* explain = nullptr) const final;
 
     std::unique_ptr<MatchExpression> shallowClone() const final;
 };
diff --git a/src/mongo/db/matcher/expression_with_placeholder.h b/src/mongo/db/matcher/expression_with_placeholder.h
index 6bea7cc2a8..9dabe22b28 100644
--- a/src/mongo/db/matcher/expression_with_placeholder.h
+++ b/src/mongo/db/matcher/expression_with_placeholder.h
@@ -72,7 +72,9 @@ public:
      * field whose name is given by getPlaceholder(). If the placeholder name does not exist, then
      * the filter expression does not refer to any specific paths.
      */
-    bool matchesBSONElement(BSONElement elem, MatchDetails* details = nullptr) const {
+    bool matchesBSONElement(BSONElement elem,
+                            ArrayPositionalMatch* details = nullptr,
+                            std::deque<std::string>* explain = nullptr) const {
         return _filter->matchesBSONElement(elem, details);
     }
 
diff --git a/src/mongo/db/matcher/match_details.cpp b/src/mongo/db/matcher/match_details.cpp
deleted file mode 100644
index 6675bf08e1..0000000000
--- a/src/mongo/db/matcher/match_details.cpp
+++ /dev/null
@@ -1,72 +0,0 @@
-// match_details.cpp
-
-/**
-*    Copyright (C) 2013 10gen Inc.
-*
-*    This program is free software: you can redistribute it and/or  modify
-*    it under the terms of the GNU Affero General Public License, version 3,
-*    as published by the Free Software Foundation.
-*
-*    This program is distributed in the hope that it will be useful,
-*    but WITHOUT ANY WARRANTY; without even the implied warranty of
-*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-*    GNU Affero General Public License for more details.
-*
-*    You should have received a copy of the GNU Affero General Public License
-*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*
-*    As a special exception, the copyright holders give permission to link the
-*    code of portions of this program with the OpenSSL library under certain
-*    conditions as described in each individual source file and distribute
-*    linked combinations including the program with the OpenSSL library. You
-*    must comply with the GNU Affero General Public License in all respects for
-*    all of the code used other than as permitted herein. If you modify file(s)
-*    with this exception, you may extend this exception to your version of the
-*    file(s), but you are not obligated to do so. If you do not wish to do so,
-*    delete this exception statement from your version. If you delete this
-*    exception statement from all source files in the program, then also delete
-*    it in the license file.
-*/
-
-#include "mongo/db/matcher/match_details.h"
-
-#include <sstream>
-
-#include "mongo/util/assert_util.h"
-
-namespace mongo {
-
-using std::string;
-
-MatchDetails::MatchDetails() : _elemMatchKeyRequested() {
-    resetOutput();
-}
-
-void MatchDetails::resetOutput() {
-    _loadedRecord = false;
-    _elemMatchKey.reset();
-}
-
-bool MatchDetails::hasElemMatchKey() const {
-    return _elemMatchKey.get();
-}
-
-std::string MatchDetails::elemMatchKey() const {
-    verify(hasElemMatchKey());
-    return *(_elemMatchKey.get());
-}
-
-void MatchDetails::setElemMatchKey(const std::string& elemMatchKey) {
-    if (_elemMatchKeyRequested) {
-        _elemMatchKey.reset(new std::string(elemMatchKey));
-    }
-}
-
-string MatchDetails::toString() const {
-    std::stringstream ss;
-    ss << "loadedRecord: " << _loadedRecord << " ";
-    ss << "elemMatchKeyRequested: " << _elemMatchKeyRequested << " ";
-    ss << "elemMatchKey: " << (_elemMatchKey ? _elemMatchKey->c_str() : "NONE") << " ";
-    return ss.str();
-}
-}
diff --git a/src/mongo/db/matcher/match_details.h b/src/mongo/db/matcher/match_details.h
deleted file mode 100644
index 15a0a60667..0000000000
--- a/src/mongo/db/matcher/match_details.h
+++ /dev/null
@@ -1,81 +0,0 @@
-// match_details.h
-
-/**
-*    Copyright (C) 2013 10gen Inc.
-*
-*    This program is free software: you can redistribute it and/or  modify
-*    it under the terms of the GNU Affero General Public License, version 3,
-*    as published by the Free Software Foundation.
-*
-*    This program is distributed in the hope that it will be useful,
-*    but WITHOUT ANY WARRANTY; without even the implied warranty of
-*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-*    GNU Affero General Public License for more details.
-*
-*    You should have received a copy of the GNU Affero General Public License
-*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*
-*    As a special exception, the copyright holders give permission to link the
-*    code of portions of this program with the OpenSSL library under certain
-*    conditions as described in each individual source file and distribute
-*    linked combinations including the program with the OpenSSL library. You
-*    must comply with the GNU Affero General Public License in all respects for
-*    all of the code used other than as permitted herein. If you modify file(s)
-*    with this exception, you may extend this exception to your version of the
-*    file(s), but you are not obligated to do so. If you do not wish to do so,
-*    delete this exception statement from your version. If you delete this
-*    exception statement from all source files in the program, then also delete
-*    it in the license file.
-*/
-
-#pragma once
-
-#include <memory>
-#include <string>
-
-namespace mongo {
-
-/** Reports information about a match request. */
-class MatchDetails {
-public:
-    MatchDetails();
-
-    void resetOutput();
-
-    // for debugging only
-    std::string toString() const;
-
-    // relating to whether or not we had to load the full record
-
-    void setLoadedRecord(bool loadedRecord) {
-        _loadedRecord = loadedRecord;
-    }
-
-    bool hasLoadedRecord() const {
-        return _loadedRecord;
-    }
-
-    // this name is wrong
-
-    bool needRecord() const {
-        return _elemMatchKeyRequested;
-    }
-
-    // if we need to store the offset into an array where we found the match
-
-    /** Request that an elemMatchKey be recorded. */
-    void requestElemMatchKey() {
-        _elemMatchKeyRequested = true;
-    }
-
-    bool hasElemMatchKey() const;
-    std::string elemMatchKey() const;
-
-    void setElemMatchKey(const std::string& elemMatchKey);
-
-private:
-    bool _loadedRecord;
-    bool _elemMatchKeyRequested;
-    std::unique_ptr<std::string> _elemMatchKey;
-};
-}
diff --git a/src/mongo/db/matcher/matcher.cpp b/src/mongo/db/matcher/matcher.cpp
index bea280d4d0..1a668a61cf 100644
--- a/src/mongo/db/matcher/matcher.cpp
+++ b/src/mongo/db/matcher/matcher.cpp
@@ -55,7 +55,9 @@ Matcher::Matcher(const BSONObj& pattern,
     _expression = std::move(statusWithMatcher.getValue());
 }
 
-bool Matcher::matches(const BSONObj& doc, MatchDetails* details) const {
+bool Matcher::matches(const BSONObj& doc,
+                      ArrayPositionalMatch* details,
+                      std::deque<std::string>* explain) const {
     if (!_expression)
         return true;
 
diff --git a/src/mongo/db/matcher/matcher.h b/src/mongo/db/matcher/matcher.h
index 1e4fbd999c..39f537e971 100644
--- a/src/mongo/db/matcher/matcher.h
+++ b/src/mongo/db/matcher/matcher.h
@@ -34,10 +34,10 @@
 #include "mongo/base/disallow_copying.h"
 #include "mongo/base/status.h"
 #include "mongo/bson/bsonobj.h"
+#include "mongo/db/matcher/array_positional_match.h"
 #include "mongo/db/matcher/expression.h"
 #include "mongo/db/matcher/expression_parser.h"
 #include "mongo/db/matcher/extensions_callback_noop.h"
-#include "mongo/db/matcher/match_details.h"
 
 
 namespace mongo {
@@ -60,7 +60,9 @@ public:
             MatchExpressionParser::AllowedFeatureSet allowedFeatures =
                 MatchExpressionParser::kDefaultSpecialFeatures);
 
-    bool matches(const BSONObj& doc, MatchDetails* details = NULL) const;
+    bool matches(const BSONObj& doc,
+                 ArrayPositionalMatch* details = nullptr,
+                 std::deque<std::string>* = nullptr) const;
 
     const BSONObj* getQuery() const {
         return &_pattern;
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_all_elem_match_from_index.h b/src/mongo/db/matcher/schema/expression_internal_schema_all_elem_match_from_index.h
index dfa64a171d..793a975f5c 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_all_elem_match_from_index.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_all_elem_match_from_index.h
@@ -47,15 +47,24 @@ public:
 
     std::unique_ptr<MatchExpression> shallowClone() const final;
 
-    bool matchesArray(const BSONObj& array, MatchDetails* details) const final {
+    bool matchesArray(const BSONObj& array,
+                      ArrayPositionalMatch* details,
+                      std::deque<std::string>* explain) const final {
         auto iter = BSONObjIterator(array);
-        for (int i = 0; iter.more() && i < _index; i++) {
+        int index = 0;
+        for (; iter.more() && index < _index; ++index) {
             iter.next();
         }
         while (iter.more()) {
             if (!_expression->matchesBSONElement(iter.next(), details)) {
+                if (explain) {
+                    explain->push_front(str::stream() << "element " << index << " of array '"
+                                                      << path()
+                                                      << "' does not satisfy subquery: ");
+                }
                 return false;
             }
+            ++index;
         }
         return true;
     }
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.cpp
index 5fa61fd23e..645d983fb5 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.cpp
@@ -87,13 +87,15 @@ bool InternalSchemaAllowedPropertiesMatchExpression::equivalent(const MatchExpre
                             });
 }
 
-bool InternalSchemaAllowedPropertiesMatchExpression::matches(const MatchableDocument* doc,
-                                                             MatchDetails* details) const {
+bool InternalSchemaAllowedPropertiesMatchExpression::matches(
+    const MatchableDocument* doc,
+    ArrayPositionalMatch* details,
+    std::deque<std::string>* explain) const {
     return _matchesBSONObj(doc->toBSON());
 }
 
-bool InternalSchemaAllowedPropertiesMatchExpression::matchesSingleElement(const BSONElement& elem,
-                                                                          MatchDetails*) const {
+bool InternalSchemaAllowedPropertiesMatchExpression::matchesSingleElement(
+    const BSONElement& elem, ArrayPositionalMatch*, std::deque<std::string>* explain) const {
     if (elem.type() != BSONType::Object) {
         return false;
     }
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h
index 89c7448ec7..aa51128f10 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_allowed_properties.h
@@ -131,8 +131,12 @@ public:
      *  - any field not contained in '_properties' nor matching a pattern in '_patternProperties'
      *    matches the '_otherwise' match expression.
      */
-    bool matches(const MatchableDocument* doc, MatchDetails* details) const final;
-    bool matchesSingleElement(const BSONElement& element, MatchDetails* details) const final;
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details,
+                 std::deque<std::string>* explain) const final;
+    bool matchesSingleElement(const BSONElement& element,
+                              ArrayPositionalMatch* details,
+                              std::deque<std::string>* explain) const final;
 
     void serialize(BSONObjBuilder* builder) const final;
 
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_cond.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_cond.cpp
index b05de7d8d6..80d4cefaf7 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_cond.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_cond.cpp
@@ -34,13 +34,16 @@ namespace mongo {
 constexpr StringData InternalSchemaCondMatchExpression::kName;
 
 bool InternalSchemaCondMatchExpression::matches(const MatchableDocument* doc,
-                                                MatchDetails* details) const {
+                                                ArrayPositionalMatch* details,
+                                                std::deque<std::string>* explain) const {
     return condition()->matches(doc, details) ? thenBranch()->matches(doc, details)
                                               : elseBranch()->matches(doc, details);
 }
 
-bool InternalSchemaCondMatchExpression::matchesSingleElement(const BSONElement& elem,
-                                                             MatchDetails* details) const {
+bool InternalSchemaCondMatchExpression::matchesSingleElement(
+    const BSONElement& elem,
+    ArrayPositionalMatch* details,
+    std::deque<std::string>* explain) const {
     return condition()->matchesSingleElement(elem, details)
         ? thenBranch()->matchesSingleElement(elem, details)
         : elseBranch()->matchesSingleElement(elem, details);
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_cond.h b/src/mongo/db/matcher/schema/expression_internal_schema_cond.h
index ab5f38b4d0..ef45210d4b 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_cond.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_cond.h
@@ -69,8 +69,12 @@ public:
      * If the input object matches 'condition', returns the result of matching it against
      * 'thenBranch'. Otherwise, returns the result of matching it against 'elseBranch'.
      */
-    bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const final;
-    bool matchesSingleElement(const BSONElement& elem, MatchDetails* details = nullptr) const final;
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details = nullptr,
+                 std::deque<std::string>* explain = nullptr) const final;
+    bool matchesSingleElement(const BSONElement& elem,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 };
 
 }  // namespace mongo
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp
index b28de000a3..fb7581bb23 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_eq.cpp
@@ -48,7 +48,8 @@ InternalSchemaEqMatchExpression::InternalSchemaEqMatchExpression(StringData path
 }
 
 bool InternalSchemaEqMatchExpression::matchesSingleElement(const BSONElement& elem,
-                                                           MatchDetails* details) const {
+                                                           ArrayPositionalMatch* details,
+                                                           std::deque<std::string>* explain) const {
     return _eltCmp.evaluate(_rhsElem == elem);
 }
 
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_eq.h b/src/mongo/db/matcher/schema/expression_internal_schema_eq.h
index 5afa686009..6469f3a300 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_eq.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_eq.h
@@ -49,7 +49,9 @@ public:
 
     std::unique_ptr<MatchExpression> shallowClone() const final;
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails*) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch*,
+                              std::deque<std::string>* explain) const final;
 
     void debugString(StringBuilder& debug, int level) const final;
 
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp
index 3cc617d94b..b6bc2cef63 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp
@@ -47,8 +47,8 @@ InternalSchemaFmodMatchExpression::InternalSchemaFmodMatchExpression(StringData
     uassert(ErrorCodes::BadValue, "divisor cannot be infinite", !divisor.isInfinite());
 }
 
-bool InternalSchemaFmodMatchExpression::matchesSingleElement(const BSONElement& e,
-                                                             MatchDetails* details) const {
+bool InternalSchemaFmodMatchExpression::matchesSingleElement(
+    const BSONElement& e, ArrayPositionalMatch* details, std::deque<std::string>* explain) const {
     if (!e.isNumber()) {
         return false;
     }
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h
index 6847913187..67a1bdf23d 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h
@@ -49,7 +49,9 @@ public:
         return std::move(m);
     }
 
-    bool matchesSingleElement(const BSONElement& e, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement& e,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     void debugString(StringBuilder& debug, int level) const final;
 
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_fmod_test.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_fmod_test.cpp
index 1701f0b8e2..9b1c256436 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_fmod_test.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_fmod_test.cpp
@@ -87,15 +87,15 @@ TEST(InternalSchemaFmodMatchExpression, NegativeRemainders) {
 
 TEST(InternalSchemaFmodMatchExpression, ElemMatchKey) {
     InternalSchemaFmodMatchExpression fmod("a", Decimal128(5), Decimal128(2));
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT_FALSE(fmod.matchesBSON(BSON("a" << 4), &details));
-    ASSERT_FALSE(details.hasElemMatchKey());
+    ASSERT_FALSE(details.arrayPosition());
     ASSERT_TRUE(fmod.matchesBSON(BSON("a" << 2), &details));
-    ASSERT_FALSE(details.hasElemMatchKey());
+    ASSERT_FALSE(details.arrayPosition());
     ASSERT_TRUE(fmod.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2 << 5)), &details));
-    ASSERT_TRUE(details.hasElemMatchKey());
-    ASSERT_EQUALS("1", details.elemMatchKey());
+    ASSERT_TRUE(details.arrayPosition());
+    ASSERT_EQUALS("1", *details.arrayPosition());
 }
 
 TEST(InternalSchemaFmodMatchExpression, Equality) {
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_match_array_index.h b/src/mongo/db/matcher/schema/expression_internal_schema_match_array_index.h
index dea8895c86..e8d31018b2 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_match_array_index.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_match_array_index.h
@@ -52,7 +52,9 @@ public:
      * Matches 'array' if the element at '_index' matches '_expression', or if its size is less than
      * '_index'.
      */
-    bool matchesArray(const BSONObj& array, MatchDetails* details) const final {
+    bool matchesArray(const BSONObj& array,
+                      ArrayPositionalMatch* details,
+                      std::deque<std::string>* explain) const final {
         BSONElement element;
         auto iterator = BSONObjIterator(array);
 
@@ -64,7 +66,12 @@ public:
             element = iterator.next();
         }
 
-        return _expression->matchesBSONElement(element, details);
+        bool match = _expression->matchesBSONElement(element, details);
+        if (!match && explain) {
+            explain->push_front(str::stream() << "element " << _index << " of array '" << path()
+                                              << "' does not match: ");
+        }
+        return match;
     }
 
     void serialize(BSONObjBuilder* builder) const final;
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h b/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h
index 814aba1da8..977205fa67 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h
@@ -43,8 +43,17 @@ public:
         : InternalSchemaNumArrayItemsMatchExpression(
               INTERNAL_SCHEMA_MAX_ITEMS, path, numItems, "$_internalSchemaMaxItems"_sd) {}
 
-    bool matchesArray(const BSONObj& anArray, MatchDetails* details) const final {
-        return (anArray.nFields() <= numItems());
+    bool matchesArray(const BSONObj& anArray,
+                      ArrayPositionalMatch* details,
+                      std::deque<std::string>* explain) const final {
+        bool match = (anArray.nFields() <= numItems());
+        if (!match && explain) {
+            explain->push_front(str::stream() << "array '" << path() << "' has "
+                                              << anArray.nFields()
+                                              << " elements, over the limit of "
+                                              << numItems());
+        }
+        return match;
     }
 
     std::unique_ptr<MatchExpression> shallowClone() const final {
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_max_properties.h b/src/mongo/db/matcher/schema/expression_internal_schema_max_properties.h
index 82bc65dabe..967db0f0f8 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_max_properties.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_max_properties.h
@@ -44,13 +44,16 @@ public:
                                                      numProperties,
                                                      "$_internalSchemaMaxProperties") {}
 
-    bool matches(const MatchableDocument* doc, MatchDetails* details) const final {
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details,
+                 std::deque<std::string>* explain) const final {
         BSONObj obj = doc->toBSON();
         return (obj.nFields() <= numProperties());
     }
 
     bool matchesSingleElement(const BSONElement& elem,
-                              MatchDetails* details = nullptr) const final {
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
         if (elem.type() != BSONType::Object) {
             return false;
         }
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h b/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h
index 3aaffb6234..2ae014058d 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h
@@ -43,7 +43,9 @@ public:
         : InternalSchemaNumArrayItemsMatchExpression(
               INTERNAL_SCHEMA_MIN_ITEMS, path, numItems, "$_internalSchemaMinItems"_sd) {}
 
-    bool matchesArray(const BSONObj& anArray, MatchDetails* details) const final {
+    bool matchesArray(const BSONObj& anArray,
+                      ArrayPositionalMatch* details,
+                      std::deque<std::string>* explain) const final {
         return (anArray.nFields() >= numItems());
     }
 
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_min_properties.h b/src/mongo/db/matcher/schema/expression_internal_schema_min_properties.h
index 9ac68da48a..8c48b0e279 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_min_properties.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_min_properties.h
@@ -44,13 +44,16 @@ public:
                                                      numProperties,
                                                      "$_internalSchemaMinProperties") {}
 
-    bool matches(const MatchableDocument* doc, MatchDetails* details) const final {
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details,
+                 std::deque<std::string>* explain) const final {
         BSONObj obj = doc->toBSON();
         return (obj.nFields() >= numProperties());
     }
 
     bool matchesSingleElement(const BSONElement& elem,
-                              MatchDetails* details = nullptr) const final {
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
         if (elem.type() != BSONType::Object) {
             return false;
         }
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_object_match.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_object_match.cpp
index b1a3301ff7..72bb85bc7b 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_object_match.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_object_match.cpp
@@ -42,12 +42,17 @@ InternalSchemaObjectMatchExpression::InternalSchemaObjectMatchExpression(
                           ElementPath::NonLeafArrayBehavior::kTraverse),
       _sub(std::move(expr)) {}
 
-bool InternalSchemaObjectMatchExpression::matchesSingleElement(const BSONElement& elem,
-                                                               MatchDetails* details) const {
+bool InternalSchemaObjectMatchExpression::matchesSingleElement(
+    const BSONElement& elem,
+    ArrayPositionalMatch* details,
+    std::deque<std::string>* explain) const {
     if (elem.type() != BSONType::Object) {
+        if (explain) {
+            explain->push_front(str::stream() << "'" << path() << "' is not an object");
+        }
         return false;
     }
-    return _sub->matchesBSON(elem.Obj());
+    return _sub->matchesBSON(elem.embeddedObject(), details, explain);
 }
 
 void InternalSchemaObjectMatchExpression::debugString(StringBuilder& debug, int level) const {
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_object_match.h b/src/mongo/db/matcher/schema/expression_internal_schema_object_match.h
index 8f7b389de8..558d6d9d36 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_object_match.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_object_match.h
@@ -38,7 +38,9 @@ public:
 
     InternalSchemaObjectMatchExpression(StringData path, std::unique_ptr<MatchExpression> expr);
 
-    bool matchesSingleElement(const BSONElement& elem, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement& elem,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     std::unique_ptr<MatchExpression> shallowClone() const final;
 
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp
index 2684533c46..26a6ebb971 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.cpp
@@ -35,7 +35,8 @@ namespace mongo {
 constexpr StringData InternalSchemaRootDocEqMatchExpression::kName;
 
 bool InternalSchemaRootDocEqMatchExpression::matches(const MatchableDocument* doc,
-                                                     MatchDetails* details) const {
+                                                     ArrayPositionalMatch* details,
+                                                     std::deque<std::string>* explain) const {
     return _objCmp.evaluate(doc->toBSON() == _rhsObj);
 }
 
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h
index 7ca66e89ca..3710fc225c 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h
@@ -51,14 +51,17 @@ public:
     explicit InternalSchemaRootDocEqMatchExpression(BSONObj rhs)
         : MatchExpression(MatchExpression::INTERNAL_SCHEMA_ROOT_DOC_EQ), _rhsObj(std::move(rhs)) {}
 
-    bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const final;
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details = nullptr,
+                 std::deque<std::string>* explain = nullptr) const final;
 
     /**
      * This expression should only be used to match full documents, not objects within an array
      * in the case of $elemMatch.
      */
     bool matchesSingleElement(const BSONElement& elem,
-                              MatchDetails* details = nullptr) const final {
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
         MONGO_UNREACHABLE;
     }
 
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_str_length.h b/src/mongo/db/matcher/schema/expression_internal_schema_str_length.h
index 2a00de506b..5dca9ca632 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_str_length.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_str_length.h
@@ -48,7 +48,8 @@ public:
     virtual Validator getComparator() const = 0;
 
     bool matchesSingleElement(const BSONElement& elem,
-                              MatchDetails* details = nullptr) const final {
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final {
         if (elem.type() != BSONType::String) {
             return false;
         }
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_unique_items.h b/src/mongo/db/matcher/schema/expression_internal_schema_unique_items.h
index 5338d3fbcc..073272b3af 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_unique_items.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_unique_items.h
@@ -60,10 +60,19 @@ public:
         return nullptr;
     }
 
-    bool matchesArray(const BSONObj& array, MatchDetails*) const final {
+    bool matchesArray(const BSONObj& array,
+                      ArrayPositionalMatch*,
+                      std::deque<std::string>* explain) const final {
         auto set = _comparator.makeBSONEltSet();
-        for (auto&& elem : array) {
+        for (const auto& elem : array) {
             if (!std::get<bool>(set.insert(elem))) {
+                if (explain) {
+                    explain->push_front(
+                        str::stream()
+                        << "array does not have unique contents; duplicates of element with value "
+                        << elem.toString(false)
+                        << " exist");
+                }
                 return false;
             }
         }
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_xor.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_xor.cpp
index 8a0d80c275..e719caab05 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_xor.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_xor.cpp
@@ -38,7 +38,8 @@ namespace mongo {
 constexpr StringData InternalSchemaXorMatchExpression::kName;
 
 bool InternalSchemaXorMatchExpression::matches(const MatchableDocument* doc,
-                                               MatchDetails* details) const {
+                                               ArrayPositionalMatch* details,
+                                               std::deque<std::string>* explain) const {
     bool found = false;
     for (size_t i = 0; i < numChildren(); i++) {
         if (getChild(i)->matches(doc, nullptr)) {
@@ -51,8 +52,10 @@ bool InternalSchemaXorMatchExpression::matches(const MatchableDocument* doc,
     return found;
 }
 
-bool InternalSchemaXorMatchExpression::matchesSingleElement(const BSONElement& element,
-                                                            MatchDetails* details) const {
+bool InternalSchemaXorMatchExpression::matchesSingleElement(
+    const BSONElement& element,
+    ArrayPositionalMatch* details,
+    std::deque<std::string>* explain) const {
     bool found = false;
     for (size_t i = 0; i < numChildren(); i++) {
         if (getChild(i)->matchesSingleElement(element, details)) {
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_xor.h b/src/mongo/db/matcher/schema/expression_internal_schema_xor.h
index e10ad9f315..c505235993 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_xor.h
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_xor.h
@@ -42,9 +42,13 @@ public:
 
     InternalSchemaXorMatchExpression() : ListOfMatchExpression(INTERNAL_SCHEMA_XOR) {}
 
-    bool matches(const MatchableDocument* doc, MatchDetails* details = nullptr) const final;
+    bool matches(const MatchableDocument* doc,
+                 ArrayPositionalMatch* details = nullptr,
+                 std::deque<std::string>* explain = nullptr) const final;
 
-    bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+    bool matchesSingleElement(const BSONElement&,
+                              ArrayPositionalMatch* details = nullptr,
+                              std::deque<std::string>* explain = nullptr) const final;
 
     virtual std::unique_ptr<MatchExpression> shallowClone() const {
         auto xorCopy = stdx::make_unique<InternalSchemaXorMatchExpression>();
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_xor_test.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_xor_test.cpp
index 0a982bfdba..f5d1055f34 100644
--- a/src/mongo/db/matcher/schema/expression_internal_schema_xor_test.cpp
+++ b/src/mongo/db/matcher/schema/expression_internal_schema_xor_test.cpp
@@ -83,17 +83,17 @@ TEST(InternalSchemaXorOp, DoesNotUseElemMatchKey) {
     BSONObj matchPredicate = fromjson("{$_internalSchemaXor: [{a: 1}, {b: 2}]}");
 
     auto expr = MatchExpressionParser::parse(matchPredicate, expCtx);
-    MatchDetails details;
-    details.requestElemMatchKey();
+    ArrayPositionalMatch details;
+    details.requestArrayPosition();
     ASSERT_OK(expr.getStatus());
     ASSERT_TRUE(expr.getValue()->matchesBSON(BSON("a" << 1), &details));
-    ASSERT_FALSE(details.hasElemMatchKey());
+    ASSERT_FALSE(details.arrayPosition());
     ASSERT_TRUE(expr.getValue()->matchesBSON(BSON("a" << BSON_ARRAY(1) << "b" << BSON_ARRAY(10)),
                                              &details));
-    ASSERT_FALSE(details.hasElemMatchKey());
+    ASSERT_FALSE(details.arrayPosition());
     ASSERT_FALSE(
         expr.getValue()->matchesBSON(BSON("a" << BSON_ARRAY(3) << "b" << BSON_ARRAY(4)), &details));
-    ASSERT_FALSE(details.hasElemMatchKey());
+    ASSERT_FALSE(details.arrayPosition());
 }
 
 TEST(InternalSchemaXorOp, Equivalent) {
diff --git a/src/mongo/db/query/get_executor.cpp b/src/mongo/db/query/get_executor.cpp
index 7238b4c2a4..eea8dada3b 100644
--- a/src/mongo/db/query/get_executor.cpp
+++ b/src/mongo/db/query/get_executor.cpp
@@ -718,9 +718,9 @@ StatusWith<unique_ptr<PlanStage>> applyProjection(OperationContext* opCtx,
     }
     unique_ptr<ParsedProjection> pp(rawParsedProj);
 
-    // ProjectionExec requires the MatchDetails from the query expression when the projection
-    // uses the positional operator. Since the query may no longer match the newly-updated
-    // document, we forbid this case.
+    // ProjectionExec requires the ArrayPositionalMatch from the query expression when the
+    // projection uses the positional operator. Since the query may no longer match the
+    // newly-updated document, we forbid this case.
     if (!allowPositional && pp->requiresMatchDetails()) {
         return {ErrorCodes::BadValue,
                 "cannot use a positional projection and return the new document"};
diff --git a/src/mongo/db/query/parsed_projection.cpp b/src/mongo/db/query/parsed_projection.cpp
index fa2b10f4f8..83622a4300 100644
--- a/src/mongo/db/query/parsed_projection.cpp
+++ b/src/mongo/db/query/parsed_projection.cpp
@@ -260,7 +260,7 @@ Status ParsedProjection::make(OperationContext* opCtx,
 
     pp->_isInclusionProjection = (includeExclude == IncludeExclude::kInclude);
 
-    // The positional operator uses the MatchDetails from the query
+    // The positional operator uses the ArrayPositionalMatch from the query
     // expression to know which array element was matched.
     pp->_requiresMatchDetails = arrayOpType == ARRAY_OP_POSITIONAL;
 
diff --git a/src/mongo/dbtests/matchertests.cpp b/src/mongo/dbtests/matchertests.cpp
index ee02b4b24d..1c8f8f9e36 100644
--- a/src/mongo/dbtests/matchertests.cpp
+++ b/src/mongo/dbtests/matchertests.cpp
@@ -203,20 +203,20 @@ public:
     }
 };
 
-/** Test that MatchDetails::elemMatchKey() is set correctly after a match. */
+/** Test that ArrayPositionalMatch::arrayPosition() is set correctly after a match. */
 template <typename M>
 class ElemMatchKey {
 public:
     void run() {
         boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
         M matcher(BSON("a.b" << 1), expCtx);
-        MatchDetails details;
-        details.requestElemMatchKey();
-        ASSERT(!details.hasElemMatchKey());
+        ArrayPositionalMatch details;
+        details.requestArrayPosition();
+        ASSERT(!details.arrayPosition());
         ASSERT(matcher.matches(fromjson("{ a:[ { b:1 } ] }"), &details));
         // The '0' entry of the 'a' array is matched.
-        ASSERT(details.hasElemMatchKey());
-        ASSERT_EQUALS(string("0"), details.elemMatchKey());
+        ASSERT(details.arrayPosition());
+        ASSERT_EQUALS(string("0"), *details.arrayPosition());
     }
 };
 
