summaryrefslogtreecommitdiff
path: root/src/mongo/db/exec/geo_near.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/exec/geo_near.cpp')
-rw-r--r--src/mongo/db/exec/geo_near.cpp2034
1 files changed, 973 insertions, 1061 deletions
diff --git a/src/mongo/db/exec/geo_near.cpp b/src/mongo/db/exec/geo_near.cpp
index 1776fd95a26..b07113f21fd 100644
--- a/src/mongo/db/exec/geo_near.cpp
+++ b/src/mongo/db/exec/geo_near.cpp
@@ -51,1247 +51,1159 @@
namespace mongo {
- using std::abs;
- using std::unique_ptr;
+using std::abs;
+using std::unique_ptr;
- //
- // Shared GeoNear search functionality
- //
-
- static const double kCircOfEarthInMeters = 2 * M_PI * kRadiusOfEarthInMeters;
- static const double kMaxEarthDistanceInMeters = kCircOfEarthInMeters / 2;
- static const double kMetersPerDegreeAtEquator = kCircOfEarthInMeters / 360;
-
- namespace {
-
- /**
- * Structure that holds BSON addresses (BSONElements) and the corresponding geometry parsed
- * at those locations.
- * Used to separate the parsing of geometries from a BSONObj (which must stay in scope) from
- * the computation over those geometries.
- * TODO: Merge with 2D/2DSphere key extraction?
- */
- struct StoredGeometry {
-
- static StoredGeometry* parseFrom(const BSONElement& element) {
- if (!element.isABSONObj())
- return NULL;
-
- unique_ptr<StoredGeometry> stored(new StoredGeometry);
- if (!stored->geometry.parseFromStorage(element).isOK())
- return NULL;
- stored->element = element;
- return stored.release();
- }
+//
+// Shared GeoNear search functionality
+//
- BSONElement element;
- GeometryContainer geometry;
- };
- }
-
- /**
- * Find and parse all geometry elements on the appropriate field path from the document.
- */
- static void extractGeometries(const BSONObj& doc,
- const string& path,
- vector<StoredGeometry*>* geometries) {
+static const double kCircOfEarthInMeters = 2 * M_PI * kRadiusOfEarthInMeters;
+static const double kMaxEarthDistanceInMeters = kCircOfEarthInMeters / 2;
+static const double kMetersPerDegreeAtEquator = kCircOfEarthInMeters / 360;
- BSONElementSet geomElements;
- // NOTE: Annoyingly, we cannot just expand arrays b/c single 2d points are arrays, we need
- // to manually expand all results to check if they are geometries
- doc.getFieldsDotted(path, geomElements, false /* expand arrays */);
+namespace {
- for (BSONElementSet::iterator it = geomElements.begin(); it != geomElements.end(); ++it) {
+/**
+ * Structure that holds BSON addresses (BSONElements) and the corresponding geometry parsed
+ * at those locations.
+ * Used to separate the parsing of geometries from a BSONObj (which must stay in scope) from
+ * the computation over those geometries.
+ * TODO: Merge with 2D/2DSphere key extraction?
+ */
+struct StoredGeometry {
+ static StoredGeometry* parseFrom(const BSONElement& element) {
+ if (!element.isABSONObj())
+ return NULL;
+
+ unique_ptr<StoredGeometry> stored(new StoredGeometry);
+ if (!stored->geometry.parseFromStorage(element).isOK())
+ return NULL;
+ stored->element = element;
+ return stored.release();
+ }
- const BSONElement& el = *it;
- unique_ptr<StoredGeometry> stored(StoredGeometry::parseFrom(el));
+ BSONElement element;
+ GeometryContainer geometry;
+};
+}
- if (stored.get()) {
- // Valid geometry element
- geometries->push_back(stored.release());
- }
- else if (el.type() == Array) {
-
- // Many geometries may be in an array
- BSONObjIterator arrIt(el.Obj());
- while (arrIt.more()) {
-
- const BSONElement nextEl = arrIt.next();
- stored.reset(StoredGeometry::parseFrom(nextEl));
-
- if (stored.get()) {
- // Valid geometry element
- geometries->push_back(stored.release());
- }
- else {
- warning() << "geoNear stage read non-geometry element " << nextEl.toString()
- << " in array " << el.toString();
- }
+/**
+ * Find and parse all geometry elements on the appropriate field path from the document.
+ */
+static void extractGeometries(const BSONObj& doc,
+ const string& path,
+ vector<StoredGeometry*>* geometries) {
+ BSONElementSet geomElements;
+ // NOTE: Annoyingly, we cannot just expand arrays b/c single 2d points are arrays, we need
+ // to manually expand all results to check if they are geometries
+ doc.getFieldsDotted(path, geomElements, false /* expand arrays */);
+
+ for (BSONElementSet::iterator it = geomElements.begin(); it != geomElements.end(); ++it) {
+ const BSONElement& el = *it;
+ unique_ptr<StoredGeometry> stored(StoredGeometry::parseFrom(el));
+
+ if (stored.get()) {
+ // Valid geometry element
+ geometries->push_back(stored.release());
+ } else if (el.type() == Array) {
+ // Many geometries may be in an array
+ BSONObjIterator arrIt(el.Obj());
+ while (arrIt.more()) {
+ const BSONElement nextEl = arrIt.next();
+ stored.reset(StoredGeometry::parseFrom(nextEl));
+
+ if (stored.get()) {
+ // Valid geometry element
+ geometries->push_back(stored.release());
+ } else {
+ warning() << "geoNear stage read non-geometry element " << nextEl.toString()
+ << " in array " << el.toString();
}
}
- else {
- warning() << "geoNear stage read non-geometry element " << el.toString();
- }
+ } else {
+ warning() << "geoNear stage read non-geometry element " << el.toString();
}
}
+}
- static StatusWith<double> computeGeoNearDistance(const GeoNearParams& nearParams,
- WorkingSetMember* member) {
-
- //
- // Generic GeoNear distance computation
- // Distances are computed by projecting the stored geometry into the query CRS, and
- // computing distance in that CRS.
- //
-
- // Must have an object in order to get geometry out of it.
- invariant(member->hasObj());
-
- CRS queryCRS = nearParams.nearQuery->centroid->crs;
-
- // Extract all the geometries out of this document for the near query
- OwnedPointerVector<StoredGeometry> geometriesOwned;
- vector<StoredGeometry*>& geometries = geometriesOwned.mutableVector();
- extractGeometries(member->obj.value(), nearParams.nearQuery->field, &geometries);
-
- // Compute the minimum distance of all the geometries in the document
- double minDistance = -1;
- BSONObj minDistanceObj;
- for (vector<StoredGeometry*>::iterator it = geometries.begin(); it != geometries.end();
- ++it) {
+static StatusWith<double> computeGeoNearDistance(const GeoNearParams& nearParams,
+ WorkingSetMember* member) {
+ //
+ // Generic GeoNear distance computation
+ // Distances are computed by projecting the stored geometry into the query CRS, and
+ // computing distance in that CRS.
+ //
- StoredGeometry& stored = **it;
+ // Must have an object in order to get geometry out of it.
+ invariant(member->hasObj());
- // NOTE: A stored document with STRICT_SPHERE CRS is treated as a malformed document
- // and ignored. Since GeoNear requires an index, there's no stored STRICT_SPHERE shape.
- // So we don't check it here.
+ CRS queryCRS = nearParams.nearQuery->centroid->crs;
- // NOTE: For now, we're sure that if we get this far in the query we'll have an
- // appropriate index which validates the type of geometry we're pulling back here.
- // TODO: It may make sense to change our semantics and, by default, only return
- // shapes in the same CRS from $geoNear.
- if (!stored.geometry.supportsProject(queryCRS))
- continue;
- stored.geometry.projectInto(queryCRS);
+ // Extract all the geometries out of this document for the near query
+ OwnedPointerVector<StoredGeometry> geometriesOwned;
+ vector<StoredGeometry*>& geometries = geometriesOwned.mutableVector();
+ extractGeometries(member->obj.value(), nearParams.nearQuery->field, &geometries);
- double nextDistance = stored.geometry.minDistance(*nearParams.nearQuery->centroid);
+ // Compute the minimum distance of all the geometries in the document
+ double minDistance = -1;
+ BSONObj minDistanceObj;
+ for (vector<StoredGeometry*>::iterator it = geometries.begin(); it != geometries.end(); ++it) {
+ StoredGeometry& stored = **it;
- if (minDistance < 0 || nextDistance < minDistance) {
- minDistance = nextDistance;
- minDistanceObj = stored.element.Obj();
- }
- }
+ // NOTE: A stored document with STRICT_SPHERE CRS is treated as a malformed document
+ // and ignored. Since GeoNear requires an index, there's no stored STRICT_SPHERE shape.
+ // So we don't check it here.
- if (minDistance < 0) {
- // No distance to report
- return StatusWith<double>(-1);
- }
+ // NOTE: For now, we're sure that if we get this far in the query we'll have an
+ // appropriate index which validates the type of geometry we're pulling back here.
+ // TODO: It may make sense to change our semantics and, by default, only return
+ // shapes in the same CRS from $geoNear.
+ if (!stored.geometry.supportsProject(queryCRS))
+ continue;
+ stored.geometry.projectInto(queryCRS);
- if (nearParams.addDistMeta) {
- if (nearParams.nearQuery->unitsAreRadians) {
- // Hack for nearSphere
- // TODO: Remove nearSphere?
- invariant(SPHERE == queryCRS);
- member->addComputed(new GeoDistanceComputedData(minDistance
- / kRadiusOfEarthInMeters));
- }
- else {
- member->addComputed(new GeoDistanceComputedData(minDistance));
- }
- }
+ double nextDistance = stored.geometry.minDistance(*nearParams.nearQuery->centroid);
- if (nearParams.addPointMeta) {
- member->addComputed(new GeoNearPointComputedData(minDistanceObj));
+ if (minDistance < 0 || nextDistance < minDistance) {
+ minDistance = nextDistance;
+ minDistanceObj = stored.element.Obj();
}
-
- return StatusWith<double>(minDistance);
}
- static R2Annulus geoNearDistanceBounds(const GeoNearExpression& query) {
-
- const CRS queryCRS = query.centroid->crs;
-
- if (FLAT == queryCRS) {
- return R2Annulus(query.centroid->oldPoint, query.minDistance, query.maxDistance);
- }
-
- invariant(SPHERE == queryCRS);
-
- // TODO: Tighten this up a bit by making a CRS for "sphere with radians"
- double minDistance = query.minDistance;
- double maxDistance = query.maxDistance;
+ if (minDistance < 0) {
+ // No distance to report
+ return StatusWith<double>(-1);
+ }
- if (query.unitsAreRadians) {
- // Our input bounds are in radians, convert to meters since the query CRS is actually
- // SPHERE. We'll convert back to radians on outputting distances.
- minDistance *= kRadiusOfEarthInMeters;
- maxDistance *= kRadiusOfEarthInMeters;
+ if (nearParams.addDistMeta) {
+ if (nearParams.nearQuery->unitsAreRadians) {
+ // Hack for nearSphere
+ // TODO: Remove nearSphere?
+ invariant(SPHERE == queryCRS);
+ member->addComputed(new GeoDistanceComputedData(minDistance / kRadiusOfEarthInMeters));
+ } else {
+ member->addComputed(new GeoDistanceComputedData(minDistance));
}
-
- // GOTCHA: oldPoint is a misnomer - it is the original point data and is in the correct
- // CRS. We must not try to derive the original point from the spherical S2Point generated
- // as an optimization - the mapping is not 1->1 - [-180, 0] and [180, 0] map to the same
- // place.
- // TODO: Wrapping behavior should not depend on the index, which would make $near code
- // insensitive to which direction we explore the index in.
- return R2Annulus(query.centroid->oldPoint,
- min(minDistance, kMaxEarthDistanceInMeters),
- min(maxDistance, kMaxEarthDistanceInMeters));
}
- //
- // GeoNear2DStage
- //
-
- static R2Annulus twoDDistanceBounds(const GeoNearParams& nearParams,
- const IndexDescriptor* twoDIndex) {
+ if (nearParams.addPointMeta) {
+ member->addComputed(new GeoNearPointComputedData(minDistanceObj));
+ }
- R2Annulus fullBounds = geoNearDistanceBounds(*nearParams.nearQuery);
- const CRS queryCRS = nearParams.nearQuery->centroid->crs;
+ return StatusWith<double>(minDistance);
+}
- if (FLAT == queryCRS) {
+static R2Annulus geoNearDistanceBounds(const GeoNearExpression& query) {
+ const CRS queryCRS = query.centroid->crs;
- // Reset the full bounds based on our index bounds
- GeoHashConverter::Parameters hashParams;
- Status status = GeoHashConverter::parseParameters(twoDIndex->infoObj(), &hashParams);
- invariant(status.isOK()); // The index status should always be valid
+ if (FLAT == queryCRS) {
+ return R2Annulus(query.centroid->oldPoint, query.minDistance, query.maxDistance);
+ }
- // The biggest distance possible in this indexed collection is the diagonal of the
- // square indexed region.
- const double sqrt2Approx = 1.5;
- const double diagonalDist = sqrt2Approx * (hashParams.max - hashParams.min);
+ invariant(SPHERE == queryCRS);
- fullBounds = R2Annulus(fullBounds.center(),
- fullBounds.getInner(),
- min(fullBounds.getOuter(), diagonalDist));
- }
- else {
- // Spherical queries have upper bounds set by the earth - no-op
- // TODO: Wrapping errors would creep in here if nearSphere wasn't defined to not wrap
- invariant(SPHERE == queryCRS);
- invariant(!nearParams.nearQuery->isWrappingQuery);
- }
+ // TODO: Tighten this up a bit by making a CRS for "sphere with radians"
+ double minDistance = query.minDistance;
+ double maxDistance = query.maxDistance;
- return fullBounds;
+ if (query.unitsAreRadians) {
+ // Our input bounds are in radians, convert to meters since the query CRS is actually
+ // SPHERE. We'll convert back to radians on outputting distances.
+ minDistance *= kRadiusOfEarthInMeters;
+ maxDistance *= kRadiusOfEarthInMeters;
}
- class GeoNear2DStage::DensityEstimator {
- public:
- DensityEstimator(const IndexDescriptor* twoDindex, const GeoNearParams* nearParams) :
- _twoDIndex(twoDindex), _nearParams(nearParams), _currentLevel(0)
- {
- GeoHashConverter::Parameters hashParams;
- Status status = GeoHashConverter::parseParameters(_twoDIndex->infoObj(),
- &hashParams);
- // The index status should always be valid.
- invariant(status.isOK());
-
- _converter.reset(new GeoHashConverter(hashParams));
- _centroidCell = _converter->hash(_nearParams->nearQuery->centroid->oldPoint);
-
- // Since appendVertexNeighbors(level, output) requires level < hash.getBits(),
- // we have to start to find documents at most GeoHash::kMaxBits - 1. Thus the finest
- // search area is 16 * finest cell area at GeoHash::kMaxBits.
- _currentLevel = std::max(0u, hashParams.bits - 1u);
- }
+ // GOTCHA: oldPoint is a misnomer - it is the original point data and is in the correct
+ // CRS. We must not try to derive the original point from the spherical S2Point generated
+ // as an optimization - the mapping is not 1->1 - [-180, 0] and [180, 0] map to the same
+ // place.
+ // TODO: Wrapping behavior should not depend on the index, which would make $near code
+ // insensitive to which direction we explore the index in.
+ return R2Annulus(query.centroid->oldPoint,
+ min(minDistance, kMaxEarthDistanceInMeters),
+ min(maxDistance, kMaxEarthDistanceInMeters));
+}
+
+//
+// GeoNear2DStage
+//
+
+static R2Annulus twoDDistanceBounds(const GeoNearParams& nearParams,
+ const IndexDescriptor* twoDIndex) {
+ R2Annulus fullBounds = geoNearDistanceBounds(*nearParams.nearQuery);
+ const CRS queryCRS = nearParams.nearQuery->centroid->crs;
+
+ if (FLAT == queryCRS) {
+ // Reset the full bounds based on our index bounds
+ GeoHashConverter::Parameters hashParams;
+ Status status = GeoHashConverter::parseParameters(twoDIndex->infoObj(), &hashParams);
+ invariant(status.isOK()); // The index status should always be valid
+
+ // The biggest distance possible in this indexed collection is the diagonal of the
+ // square indexed region.
+ const double sqrt2Approx = 1.5;
+ const double diagonalDist = sqrt2Approx * (hashParams.max - hashParams.min);
+
+ fullBounds = R2Annulus(
+ fullBounds.center(), fullBounds.getInner(), min(fullBounds.getOuter(), diagonalDist));
+ } else {
+ // Spherical queries have upper bounds set by the earth - no-op
+ // TODO: Wrapping errors would creep in here if nearSphere wasn't defined to not wrap
+ invariant(SPHERE == queryCRS);
+ invariant(!nearParams.nearQuery->isWrappingQuery);
+ }
- PlanStage::StageState work(OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection,
- WorkingSetID* out,
- double* estimatedDistance);
-
- void saveState();
- void restoreState(OperationContext* txn);
- void invalidate(OperationContext* txn, const RecordId& dl, InvalidationType type);
-
- private:
- void buildIndexScan(OperationContext* txn, WorkingSet* workingSet, Collection* collection);
-
- const IndexDescriptor* _twoDIndex; // Not owned here.
- const GeoNearParams* _nearParams; // Not owned here.
- unique_ptr<IndexScan> _indexScan;
- unique_ptr<GeoHashConverter> _converter;
- GeoHash _centroidCell;
- unsigned _currentLevel;
- };
-
- // Initialize the internal states
- void GeoNear2DStage::DensityEstimator::buildIndexScan(OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection)
- {
- IndexScanParams scanParams;
- scanParams.descriptor = _twoDIndex;
- scanParams.direction = 1;
- scanParams.doNotDedup = true;
-
- // Scan bounds on 2D indexes are only over the 2D field - other bounds aren't applicable.
- // This is handled in query planning.
- scanParams.bounds = _nearParams->baseBounds;
-
- // The "2d" field is always the first in the index
- const string twoDFieldName = _nearParams->nearQuery->field;
- const int twoDFieldPosition = 0;
-
- // Construct index intervals used by this stage
- OrderedIntervalList oil;
- oil.name = scanParams.bounds.fields[twoDFieldPosition].name;
-
- vector<GeoHash> neighbors;
- // Return the neighbors of closest vertex to this cell at the given level.
- _centroidCell.appendVertexNeighbors(_currentLevel, &neighbors);
- std::sort(neighbors.begin(), neighbors.end());
-
- for (vector<GeoHash>::const_iterator it = neighbors.begin(); it != neighbors.end(); it++) {
- mongo::BSONObjBuilder builder;
- it->appendHashMin(&builder, "");
- it->appendHashMax(&builder, "");
- oil.intervals.push_back(IndexBoundsBuilder::makeRangeInterval(builder.obj(),
- true,
- true));
- }
+ return fullBounds;
+}
- invariant(oil.isValidFor(1));
+class GeoNear2DStage::DensityEstimator {
+public:
+ DensityEstimator(const IndexDescriptor* twoDindex, const GeoNearParams* nearParams)
+ : _twoDIndex(twoDindex), _nearParams(nearParams), _currentLevel(0) {
+ GeoHashConverter::Parameters hashParams;
+ Status status = GeoHashConverter::parseParameters(_twoDIndex->infoObj(), &hashParams);
+ // The index status should always be valid.
+ invariant(status.isOK());
- // Intersect the $near bounds we just generated into the bounds we have for anything else
- // in the scan (i.e. $within)
- IndexBoundsBuilder::intersectize(oil,
- &scanParams.bounds.fields[twoDFieldPosition]);
+ _converter.reset(new GeoHashConverter(hashParams));
+ _centroidCell = _converter->hash(_nearParams->nearQuery->centroid->oldPoint);
- _indexScan.reset(new IndexScan(txn, scanParams, workingSet, NULL));
+ // Since appendVertexNeighbors(level, output) requires level < hash.getBits(),
+ // we have to start to find documents at most GeoHash::kMaxBits - 1. Thus the finest
+ // search area is 16 * finest cell area at GeoHash::kMaxBits.
+ _currentLevel = std::max(0u, hashParams.bits - 1u);
}
- // Return IS_EOF is we find a document in it's ancestor cells and set estimated distance
- // from the nearest document.
- PlanStage::StageState GeoNear2DStage::DensityEstimator::work(OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection,
- WorkingSetID* out,
- double* estimatedDistance)
- {
- if (!_indexScan) {
- // Setup index scan stage for current level.
- buildIndexScan(txn, workingSet, collection);
- }
+ PlanStage::StageState work(OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection,
+ WorkingSetID* out,
+ double* estimatedDistance);
+
+ void saveState();
+ void restoreState(OperationContext* txn);
+ void invalidate(OperationContext* txn, const RecordId& dl, InvalidationType type);
+
+private:
+ void buildIndexScan(OperationContext* txn, WorkingSet* workingSet, Collection* collection);
+
+ const IndexDescriptor* _twoDIndex; // Not owned here.
+ const GeoNearParams* _nearParams; // Not owned here.
+ unique_ptr<IndexScan> _indexScan;
+ unique_ptr<GeoHashConverter> _converter;
+ GeoHash _centroidCell;
+ unsigned _currentLevel;
+};
+
+// Initialize the internal states
+void GeoNear2DStage::DensityEstimator::buildIndexScan(OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection) {
+ IndexScanParams scanParams;
+ scanParams.descriptor = _twoDIndex;
+ scanParams.direction = 1;
+ scanParams.doNotDedup = true;
+
+ // Scan bounds on 2D indexes are only over the 2D field - other bounds aren't applicable.
+ // This is handled in query planning.
+ scanParams.bounds = _nearParams->baseBounds;
+
+ // The "2d" field is always the first in the index
+ const string twoDFieldName = _nearParams->nearQuery->field;
+ const int twoDFieldPosition = 0;
+
+ // Construct index intervals used by this stage
+ OrderedIntervalList oil;
+ oil.name = scanParams.bounds.fields[twoDFieldPosition].name;
+
+ vector<GeoHash> neighbors;
+ // Return the neighbors of closest vertex to this cell at the given level.
+ _centroidCell.appendVertexNeighbors(_currentLevel, &neighbors);
+ std::sort(neighbors.begin(), neighbors.end());
+
+ for (vector<GeoHash>::const_iterator it = neighbors.begin(); it != neighbors.end(); it++) {
+ mongo::BSONObjBuilder builder;
+ it->appendHashMin(&builder, "");
+ it->appendHashMax(&builder, "");
+ oil.intervals.push_back(IndexBoundsBuilder::makeRangeInterval(builder.obj(), true, true));
+ }
- WorkingSetID workingSetID;
- PlanStage::StageState state = _indexScan->work(&workingSetID);
-
- if (state == PlanStage::IS_EOF) {
- // We ran through the neighbors but found nothing.
- if (_currentLevel > 0u) {
- // Advance to the next level and search again.
- _currentLevel--;
- // Reset index scan for the next level.
- _indexScan.reset(NULL);
- return PlanStage::NEED_TIME;
- }
+ invariant(oil.isValidFor(1));
+
+ // Intersect the $near bounds we just generated into the bounds we have for anything else
+ // in the scan (i.e. $within)
+ IndexBoundsBuilder::intersectize(oil, &scanParams.bounds.fields[twoDFieldPosition]);
+
+ _indexScan.reset(new IndexScan(txn, scanParams, workingSet, NULL));
+}
+
+// Return IS_EOF is we find a document in it's ancestor cells and set estimated distance
+// from the nearest document.
+PlanStage::StageState GeoNear2DStage::DensityEstimator::work(OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection,
+ WorkingSetID* out,
+ double* estimatedDistance) {
+ if (!_indexScan) {
+ // Setup index scan stage for current level.
+ buildIndexScan(txn, workingSet, collection);
+ }
- // We are already at the top level.
- *estimatedDistance = _converter->sizeEdge(_currentLevel);
- return PlanStage::IS_EOF;
- } else if (state == PlanStage::ADVANCED) {
- // Found a document at current level.
- *estimatedDistance = _converter->sizeEdge(_currentLevel);
- // Clean up working set.
- workingSet->free(workingSetID);
- return PlanStage::IS_EOF;
- } else if (state == PlanStage::NEED_YIELD) {
- *out = workingSetID;
+ WorkingSetID workingSetID;
+ PlanStage::StageState state = _indexScan->work(&workingSetID);
+
+ if (state == PlanStage::IS_EOF) {
+ // We ran through the neighbors but found nothing.
+ if (_currentLevel > 0u) {
+ // Advance to the next level and search again.
+ _currentLevel--;
+ // Reset index scan for the next level.
+ _indexScan.reset(NULL);
+ return PlanStage::NEED_TIME;
}
- // Propagate NEED_TIME or errors
- return state;
+ // We are already at the top level.
+ *estimatedDistance = _converter->sizeEdge(_currentLevel);
+ return PlanStage::IS_EOF;
+ } else if (state == PlanStage::ADVANCED) {
+ // Found a document at current level.
+ *estimatedDistance = _converter->sizeEdge(_currentLevel);
+ // Clean up working set.
+ workingSet->free(workingSetID);
+ return PlanStage::IS_EOF;
+ } else if (state == PlanStage::NEED_YIELD) {
+ *out = workingSetID;
}
- void GeoNear2DStage::DensityEstimator::saveState() {
- if (_indexScan) {
- _indexScan->saveState();
- }
+ // Propagate NEED_TIME or errors
+ return state;
+}
+
+void GeoNear2DStage::DensityEstimator::saveState() {
+ if (_indexScan) {
+ _indexScan->saveState();
}
+}
- void GeoNear2DStage::DensityEstimator::restoreState(OperationContext* txn) {
- if (_indexScan) {
- _indexScan->restoreState(txn);
- }
+void GeoNear2DStage::DensityEstimator::restoreState(OperationContext* txn) {
+ if (_indexScan) {
+ _indexScan->restoreState(txn);
}
+}
- void GeoNear2DStage::DensityEstimator::invalidate(OperationContext* txn,
- const RecordId& dl,
- InvalidationType type) {
- if (_indexScan) {
- _indexScan->invalidate(txn, dl, type);
- }
+void GeoNear2DStage::DensityEstimator::invalidate(OperationContext* txn,
+ const RecordId& dl,
+ InvalidationType type) {
+ if (_indexScan) {
+ _indexScan->invalidate(txn, dl, type);
}
+}
- PlanStage::StageState GeoNear2DStage::initialize(OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection,
- WorkingSetID* out)
- {
- if (!_densityEstimator) {
- _densityEstimator.reset(new DensityEstimator(_twoDIndex, &_nearParams));
- }
+PlanStage::StageState GeoNear2DStage::initialize(OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection,
+ WorkingSetID* out) {
+ if (!_densityEstimator) {
+ _densityEstimator.reset(new DensityEstimator(_twoDIndex, &_nearParams));
+ }
- double estimatedDistance;
- PlanStage::StageState state = _densityEstimator->work(txn, workingSet, collection, out,
- &estimatedDistance);
-
- if (state == PlanStage::IS_EOF) {
- // 2d index only works with legacy points as centroid. $nearSphere will project
- // the point into SPHERE CRS and calculate distance based on that.
- // STRICT_SPHERE is impossible here, as GeoJSON centroid is not allowed for 2d index.
-
- // Estimator finished its work, we need to finish initialization too.
- if (SPHERE == _nearParams.nearQuery->centroid->crs) {
- // Estimated distance is in degrees, convert it to meters.
- _boundsIncrement = deg2rad(estimatedDistance) * kRadiusOfEarthInMeters * 3;
- // Limit boundsIncrement to ~20KM, so that the first circle won't be too aggressive.
- _boundsIncrement = std::min(_boundsIncrement, kMaxEarthDistanceInMeters / 1000.0);
- }
- else {
- // We expand the radius by 3 times to give a reasonable starting search area.
- // Assume points are distributed evenly. X is the edge size of cells at whose
- // level we found a document in 4 neighbors. Thus the closest point is at least
- // X/2 far from the centroid. The distance between two points is at least X.
- // The area of Pi * (3X)^2 ~= 28 * X^2 will cover dozens of points at most.
- // We'll explore the space with exponentially increasing radius if this guess is
- // too small, so starting from a conservative initial radius doesn't hurt.
-
- _boundsIncrement = 3 * estimatedDistance;
- }
- invariant(_boundsIncrement > 0.0);
+ double estimatedDistance;
+ PlanStage::StageState state =
+ _densityEstimator->work(txn, workingSet, collection, out, &estimatedDistance);
+
+ if (state == PlanStage::IS_EOF) {
+ // 2d index only works with legacy points as centroid. $nearSphere will project
+ // the point into SPHERE CRS and calculate distance based on that.
+ // STRICT_SPHERE is impossible here, as GeoJSON centroid is not allowed for 2d index.
+
+ // Estimator finished its work, we need to finish initialization too.
+ if (SPHERE == _nearParams.nearQuery->centroid->crs) {
+ // Estimated distance is in degrees, convert it to meters.
+ _boundsIncrement = deg2rad(estimatedDistance) * kRadiusOfEarthInMeters * 3;
+ // Limit boundsIncrement to ~20KM, so that the first circle won't be too aggressive.
+ _boundsIncrement = std::min(_boundsIncrement, kMaxEarthDistanceInMeters / 1000.0);
+ } else {
+ // We expand the radius by 3 times to give a reasonable starting search area.
+ // Assume points are distributed evenly. X is the edge size of cells at whose
+ // level we found a document in 4 neighbors. Thus the closest point is at least
+ // X/2 far from the centroid. The distance between two points is at least X.
+ // The area of Pi * (3X)^2 ~= 28 * X^2 will cover dozens of points at most.
+ // We'll explore the space with exponentially increasing radius if this guess is
+ // too small, so starting from a conservative initial radius doesn't hurt.
- // Clean up
- _densityEstimator.reset(NULL);
+ _boundsIncrement = 3 * estimatedDistance;
}
+ invariant(_boundsIncrement > 0.0);
- return state;
+ // Clean up
+ _densityEstimator.reset(NULL);
}
- static const string kTwoDIndexNearStage("GEO_NEAR_2D");
-
- GeoNear2DStage::GeoNear2DStage(const GeoNearParams& nearParams,
- OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection,
- IndexDescriptor* twoDIndex)
- : NearStage(txn,
- workingSet,
- collection,
- new PlanStageStats(CommonStats(kTwoDIndexNearStage.c_str()),
- STAGE_GEO_NEAR_2D)),
- _nearParams(nearParams),
- _twoDIndex(twoDIndex),
- _fullBounds(twoDDistanceBounds(nearParams, twoDIndex)),
- _currBounds(_fullBounds.center(), -1, _fullBounds.getInner()),
- _boundsIncrement(0.0) {
-
- getNearStats()->keyPattern = twoDIndex->keyPattern();
- getNearStats()->indexName = twoDIndex->indexName();
- }
+ return state;
+}
- GeoNear2DStage::~GeoNear2DStage() {
- }
+static const string kTwoDIndexNearStage("GEO_NEAR_2D");
- void GeoNear2DStage::finishSaveState() {
- if (_densityEstimator) {
- _densityEstimator->saveState();
- }
+GeoNear2DStage::GeoNear2DStage(const GeoNearParams& nearParams,
+ OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection,
+ IndexDescriptor* twoDIndex)
+ : NearStage(txn,
+ workingSet,
+ collection,
+ new PlanStageStats(CommonStats(kTwoDIndexNearStage.c_str()), STAGE_GEO_NEAR_2D)),
+ _nearParams(nearParams),
+ _twoDIndex(twoDIndex),
+ _fullBounds(twoDDistanceBounds(nearParams, twoDIndex)),
+ _currBounds(_fullBounds.center(), -1, _fullBounds.getInner()),
+ _boundsIncrement(0.0) {
+ getNearStats()->keyPattern = twoDIndex->keyPattern();
+ getNearStats()->indexName = twoDIndex->indexName();
+}
+
+GeoNear2DStage::~GeoNear2DStage() {}
+
+void GeoNear2DStage::finishSaveState() {
+ if (_densityEstimator) {
+ _densityEstimator->saveState();
}
+}
- void GeoNear2DStage::finishRestoreState(OperationContext* txn) {
- if (_densityEstimator) {
- _densityEstimator->restoreState(txn);
- }
+void GeoNear2DStage::finishRestoreState(OperationContext* txn) {
+ if (_densityEstimator) {
+ _densityEstimator->restoreState(txn);
}
+}
- void GeoNear2DStage::finishInvalidate(OperationContext* txn,
- const RecordId& dl,
- InvalidationType type) {
- if (_densityEstimator) {
- _densityEstimator->invalidate(txn, dl, type);
- }
+void GeoNear2DStage::finishInvalidate(OperationContext* txn,
+ const RecordId& dl,
+ InvalidationType type) {
+ if (_densityEstimator) {
+ _densityEstimator->invalidate(txn, dl, type);
}
+}
- namespace {
-
- /**
- * Expression which checks whether a legacy 2D index point is contained within our near
- * search annulus. See nextInterval() below for more discussion.
- * TODO: Make this a standard type of GEO match expression
- */
- class TwoDPtInAnnulusExpression : public LeafMatchExpression {
- public:
-
- TwoDPtInAnnulusExpression(const R2Annulus& annulus, StringData twoDPath)
- : LeafMatchExpression(INTERNAL_2D_POINT_IN_ANNULUS), _annulus(annulus) {
-
- initPath(twoDPath);
- }
-
- virtual ~TwoDPtInAnnulusExpression() {
- }
-
- virtual void toBSON(BSONObjBuilder* out) const {
- out->append("TwoDPtInAnnulusExpression", true);
- }
-
- virtual bool matchesSingleElement(const BSONElement& e) const {
- if (!e.isABSONObj())
- return false;
-
- PointWithCRS point;
- if (!GeoParser::parseStoredPoint(e, &point).isOK()) return false;
-
- return _annulus.contains(point.oldPoint);
- }
-
- //
- // These won't be called.
- //
-
- virtual void debugString(StringBuilder& debug, int level = 0) const {
- invariant(false);
- }
-
- virtual bool equivalent(const MatchExpression* other) const {
- invariant(false);
- return false;
- }
-
- virtual LeafMatchExpression* shallowClone() const {
- invariant(false);
- return NULL;
- }
-
- private:
+namespace {
- R2Annulus _annulus;
- };
-
- /**
- * Expression which checks whether a 2D key for a point (2D hash) intersects our search
- * region. The search region may have been formed by more granular hashes.
- */
- class TwoDKeyInRegionExpression : public LeafMatchExpression {
- public:
-
- TwoDKeyInRegionExpression(R2Region* region,
- const GeoHashConverter::Parameters& hashParams,
- StringData twoDKeyPath)
- : LeafMatchExpression(INTERNAL_2D_KEY_IN_REGION),
- _region(region),
- _unhasher(hashParams) {
-
- initPath(twoDKeyPath);
- }
-
- virtual ~TwoDKeyInRegionExpression() {
- }
-
- virtual void toBSON(BSONObjBuilder* out) const {
- out->append("TwoDKeyInRegionExpression", true);
- }
-
- virtual bool matchesSingleElement(const BSONElement& e) const {
- // Something has gone terribly wrong if this doesn't hold.
- invariant(BinData == e.type());
- return !_region->fastDisjoint(_unhasher.unhashToBoxCovering(_unhasher.hash(e)));
- }
-
- //
- // These won't be called.
- //
-
- virtual void debugString(StringBuilder& debug, int level = 0) const {
- invariant(false);
- }
-
- virtual bool equivalent(const MatchExpression* other) const {
- invariant(false);
- return true;
- }
-
- virtual MatchExpression* shallowClone() const {
- invariant(false);
- return NULL;
- }
+/**
+ * Expression which checks whether a legacy 2D index point is contained within our near
+ * search annulus. See nextInterval() below for more discussion.
+ * TODO: Make this a standard type of GEO match expression
+ */
+class TwoDPtInAnnulusExpression : public LeafMatchExpression {
+public:
+ TwoDPtInAnnulusExpression(const R2Annulus& annulus, StringData twoDPath)
+ : LeafMatchExpression(INTERNAL_2D_POINT_IN_ANNULUS), _annulus(annulus) {
+ initPath(twoDPath);
+ }
- private:
+ virtual ~TwoDPtInAnnulusExpression() {}
- const unique_ptr<R2Region> _region;
- const GeoHashConverter _unhasher;
- };
+ virtual void toBSON(BSONObjBuilder* out) const {
+ out->append("TwoDPtInAnnulusExpression", true);
+ }
- // Helper class to maintain ownership of a match expression alongside an index scan
- class IndexScanWithMatch : public IndexScan {
- public:
+ virtual bool matchesSingleElement(const BSONElement& e) const {
+ if (!e.isABSONObj())
+ return false;
- IndexScanWithMatch(OperationContext* txn,
- const IndexScanParams& params,
- WorkingSet* workingSet,
- MatchExpression* filter)
- : IndexScan(txn, params, workingSet, filter), _matcher(filter) {
- }
+ PointWithCRS point;
+ if (!GeoParser::parseStoredPoint(e, &point).isOK())
+ return false;
- virtual ~IndexScanWithMatch() {
- }
-
- private:
+ return _annulus.contains(point.oldPoint);
+ }
- // Owns matcher
- const unique_ptr<MatchExpression> _matcher;
- };
+ //
+ // These won't be called.
+ //
- // Helper class to maintain ownership of a match expression alongside an index scan
- class FetchStageWithMatch : public FetchStage {
- public:
+ virtual void debugString(StringBuilder& debug, int level = 0) const {
+ invariant(false);
+ }
- FetchStageWithMatch(OperationContext* txn,
- WorkingSet* ws,
- PlanStage* child,
- MatchExpression* filter,
- const Collection* collection)
- : FetchStage(txn, ws, child, filter, collection), _matcher(filter) {
- }
+ virtual bool equivalent(const MatchExpression* other) const {
+ invariant(false);
+ return false;
+ }
- virtual ~FetchStageWithMatch() {
- }
+ virtual LeafMatchExpression* shallowClone() const {
+ invariant(false);
+ return NULL;
+ }
- private:
+private:
+ R2Annulus _annulus;
+};
- // Owns matcher
- const unique_ptr<MatchExpression> _matcher;
- };
+/**
+ * Expression which checks whether a 2D key for a point (2D hash) intersects our search
+ * region. The search region may have been formed by more granular hashes.
+ */
+class TwoDKeyInRegionExpression : public LeafMatchExpression {
+public:
+ TwoDKeyInRegionExpression(R2Region* region,
+ const GeoHashConverter::Parameters& hashParams,
+ StringData twoDKeyPath)
+ : LeafMatchExpression(INTERNAL_2D_KEY_IN_REGION), _region(region), _unhasher(hashParams) {
+ initPath(twoDKeyPath);
}
- static double min2DBoundsIncrement(const GeoNearExpression& query, IndexDescriptor* twoDIndex) {
- GeoHashConverter::Parameters hashParams;
- Status status = GeoHashConverter::parseParameters(twoDIndex->infoObj(), &hashParams);
- invariant(status.isOK()); // The index status should always be valid
- GeoHashConverter hasher(hashParams);
+ virtual ~TwoDKeyInRegionExpression() {}
- // The hasher error is the diagonal of a 2D hash region - it's generally not helpful
- // to change region size such that a search radius is smaller than the 2D hash region
- // max radius. This is slightly conservative for now (box diagonal vs circle radius).
- double minBoundsIncrement = hasher.getError() / 2;
+ virtual void toBSON(BSONObjBuilder* out) const {
+ out->append("TwoDKeyInRegionExpression", true);
+ }
- const CRS queryCRS = query.centroid->crs;
- if (FLAT == queryCRS)
- return minBoundsIncrement;
+ virtual bool matchesSingleElement(const BSONElement& e) const {
+ // Something has gone terribly wrong if this doesn't hold.
+ invariant(BinData == e.type());
+ return !_region->fastDisjoint(_unhasher.unhashToBoxCovering(_unhasher.hash(e)));
+ }
- invariant(SPHERE == queryCRS);
+ //
+ // These won't be called.
+ //
- // If this is a spherical query, units are in meters - this is just a heuristic
- return minBoundsIncrement * kMetersPerDegreeAtEquator;
+ virtual void debugString(StringBuilder& debug, int level = 0) const {
+ invariant(false);
}
- static R2Annulus projectBoundsToTwoDDegrees(R2Annulus sphereBounds) {
-
- const double outerDegrees = rad2deg(sphereBounds.getOuter() / kRadiusOfEarthInMeters);
- const double innerDegrees = rad2deg(sphereBounds.getInner() / kRadiusOfEarthInMeters);
- const double maxErrorDegrees = computeXScanDistance(sphereBounds.center().y, outerDegrees);
+ virtual bool equivalent(const MatchExpression* other) const {
+ invariant(false);
+ return true;
+ }
- return R2Annulus(sphereBounds.center(),
- max(0.0, innerDegrees - maxErrorDegrees),
- outerDegrees + maxErrorDegrees);
+ virtual MatchExpression* shallowClone() const {
+ invariant(false);
+ return NULL;
}
- StatusWith<NearStage::CoveredInterval*> //
+private:
+ const unique_ptr<R2Region> _region;
+ const GeoHashConverter _unhasher;
+};
+
+// Helper class to maintain ownership of a match expression alongside an index scan
+class IndexScanWithMatch : public IndexScan {
+public:
+ IndexScanWithMatch(OperationContext* txn,
+ const IndexScanParams& params,
+ WorkingSet* workingSet,
+ MatchExpression* filter)
+ : IndexScan(txn, params, workingSet, filter), _matcher(filter) {}
+
+ virtual ~IndexScanWithMatch() {}
+
+private:
+ // Owns matcher
+ const unique_ptr<MatchExpression> _matcher;
+};
+
+// Helper class to maintain ownership of a match expression alongside an index scan
+class FetchStageWithMatch : public FetchStage {
+public:
+ FetchStageWithMatch(OperationContext* txn,
+ WorkingSet* ws,
+ PlanStage* child,
+ MatchExpression* filter,
+ const Collection* collection)
+ : FetchStage(txn, ws, child, filter, collection), _matcher(filter) {}
+
+ virtual ~FetchStageWithMatch() {}
+
+private:
+ // Owns matcher
+ const unique_ptr<MatchExpression> _matcher;
+};
+}
+
+static double min2DBoundsIncrement(const GeoNearExpression& query, IndexDescriptor* twoDIndex) {
+ GeoHashConverter::Parameters hashParams;
+ Status status = GeoHashConverter::parseParameters(twoDIndex->infoObj(), &hashParams);
+ invariant(status.isOK()); // The index status should always be valid
+ GeoHashConverter hasher(hashParams);
+
+ // The hasher error is the diagonal of a 2D hash region - it's generally not helpful
+ // to change region size such that a search radius is smaller than the 2D hash region
+ // max radius. This is slightly conservative for now (box diagonal vs circle radius).
+ double minBoundsIncrement = hasher.getError() / 2;
+
+ const CRS queryCRS = query.centroid->crs;
+ if (FLAT == queryCRS)
+ return minBoundsIncrement;
+
+ invariant(SPHERE == queryCRS);
+
+ // If this is a spherical query, units are in meters - this is just a heuristic
+ return minBoundsIncrement * kMetersPerDegreeAtEquator;
+}
+
+static R2Annulus projectBoundsToTwoDDegrees(R2Annulus sphereBounds) {
+ const double outerDegrees = rad2deg(sphereBounds.getOuter() / kRadiusOfEarthInMeters);
+ const double innerDegrees = rad2deg(sphereBounds.getInner() / kRadiusOfEarthInMeters);
+ const double maxErrorDegrees = computeXScanDistance(sphereBounds.center().y, outerDegrees);
+
+ return R2Annulus(sphereBounds.center(),
+ max(0.0, innerDegrees - maxErrorDegrees),
+ outerDegrees + maxErrorDegrees);
+}
+
+StatusWith<NearStage::CoveredInterval*> //
GeoNear2DStage::nextInterval(OperationContext* txn,
WorkingSet* workingSet,
Collection* collection) {
+ // The search is finished if we searched at least once and all the way to the edge
+ if (_currBounds.getInner() >= 0 && _currBounds.getOuter() == _fullBounds.getOuter()) {
+ return StatusWith<CoveredInterval*>(NULL);
+ }
- // The search is finished if we searched at least once and all the way to the edge
- if (_currBounds.getInner() >= 0 && _currBounds.getOuter() == _fullBounds.getOuter()) {
- return StatusWith<CoveredInterval*>(NULL);
- }
-
- //
- // Setup the next interval
- //
-
- const NearStats* stats = getNearStats();
-
- if (!stats->intervalStats.empty()) {
-
- const IntervalStats& lastIntervalStats = stats->intervalStats.back();
+ //
+ // Setup the next interval
+ //
- // TODO: Generally we want small numbers of results fast, then larger numbers later
- if (lastIntervalStats.numResultsBuffered < 300)
- _boundsIncrement *= 2;
- else if (lastIntervalStats.numResultsBuffered > 600)
- _boundsIncrement /= 2;
- }
+ const NearStats* stats = getNearStats();
- _boundsIncrement = max(_boundsIncrement,
- min2DBoundsIncrement(*_nearParams.nearQuery, _twoDIndex));
+ if (!stats->intervalStats.empty()) {
+ const IntervalStats& lastIntervalStats = stats->intervalStats.back();
- R2Annulus nextBounds(_currBounds.center(),
- _currBounds.getOuter(),
- min(_currBounds.getOuter() + _boundsIncrement,
- _fullBounds.getOuter()));
+ // TODO: Generally we want small numbers of results fast, then larger numbers later
+ if (lastIntervalStats.numResultsBuffered < 300)
+ _boundsIncrement *= 2;
+ else if (lastIntervalStats.numResultsBuffered > 600)
+ _boundsIncrement /= 2;
+ }
- const bool isLastInterval = (nextBounds.getOuter() == _fullBounds.getOuter());
- _currBounds = nextBounds;
+ _boundsIncrement =
+ max(_boundsIncrement, min2DBoundsIncrement(*_nearParams.nearQuery, _twoDIndex));
- //
- // Get a covering region for this interval
- //
+ R2Annulus nextBounds(_currBounds.center(),
+ _currBounds.getOuter(),
+ min(_currBounds.getOuter() + _boundsIncrement, _fullBounds.getOuter()));
- const CRS queryCRS = _nearParams.nearQuery->centroid->crs;
-
- unique_ptr<R2Region> coverRegion;
-
- if (FLAT == queryCRS) {
-
- // NOTE: Due to floating point math issues, FLAT searches of a 2D index need to treat
- // containment and distance separately.
- // Ex: (distance) 54.001 - 54 > 0.001, but (containment) 54 + 0.001 <= 54.001
- // The idea is that a $near search with bounds is really a $within search, sorted by
- // distance. We attach a custom $within : annulus matcher to do the $within search,
- // and adjust max/min bounds slightly since, as above, containment does not mean the
- // distance calculation won't slightly overflow the boundary.
- //
- // The code below adjusts:
- // 1) Overall min/max bounds of the generated distance intervals to be more inclusive
- // 2) Bounds of the interval covering to be more inclusive
- // ... and later on we add the custom $within : annulus matcher.
- //
- // IMPORTANT: The *internal* interval distance bounds are *exact thresholds* - these
- // should not be adjusted.
- // TODO: Maybe integrate annuluses as a standard shape, and literally transform $near
- // internally into a $within query with $near just as sort.
-
- // Compute the maximum axis-aligned distance error
- const double epsilon = std::numeric_limits<double>::epsilon()
- * (max(abs(_fullBounds.center().x), abs(_fullBounds.center().y))
- + _fullBounds.getOuter());
-
- if (nextBounds.getInner() > 0 && nextBounds.getInner() == _fullBounds.getInner()) {
- nextBounds = R2Annulus(nextBounds.center(),
- max(0.0, nextBounds.getInner() - epsilon),
- nextBounds.getOuter());
- }
+ const bool isLastInterval = (nextBounds.getOuter() == _fullBounds.getOuter());
+ _currBounds = nextBounds;
- if (nextBounds.getOuter() > 0 && nextBounds.getOuter() == _fullBounds.getOuter()) {
- // We're at the max bound of the search, adjust interval maximum
- nextBounds = R2Annulus(nextBounds.center(),
- nextBounds.getInner(),
- nextBounds.getOuter() + epsilon);
- }
+ //
+ // Get a covering region for this interval
+ //
- // *Always* adjust the covering bounds to be more inclusive
- coverRegion.reset(new R2Annulus(nextBounds.center(),
- max(0.0, nextBounds.getInner() - epsilon),
- nextBounds.getOuter() + epsilon));
- }
- else {
- invariant(SPHERE == queryCRS);
- // TODO: As above, make this consistent with $within : $centerSphere
+ const CRS queryCRS = _nearParams.nearQuery->centroid->crs;
- // Our intervals aren't in the same CRS as our index, so we need to adjust them
- coverRegion.reset(new R2Annulus(projectBoundsToTwoDDegrees(nextBounds)));
- }
+ unique_ptr<R2Region> coverRegion;
+ if (FLAT == queryCRS) {
+ // NOTE: Due to floating point math issues, FLAT searches of a 2D index need to treat
+ // containment and distance separately.
+ // Ex: (distance) 54.001 - 54 > 0.001, but (containment) 54 + 0.001 <= 54.001
+ // The idea is that a $near search with bounds is really a $within search, sorted by
+ // distance. We attach a custom $within : annulus matcher to do the $within search,
+ // and adjust max/min bounds slightly since, as above, containment does not mean the
+ // distance calculation won't slightly overflow the boundary.
//
- // Setup the stages for this interval
- //
-
- IndexScanParams scanParams;
- scanParams.descriptor = _twoDIndex;
- scanParams.direction = 1;
- // We use a filter on the key. The filter rejects keys that don't intersect with the
- // annulus. An object that is in the annulus might have a key that's not in it and a key
- // that's in it. As such we can't just look at one key per object.
+ // The code below adjusts:
+ // 1) Overall min/max bounds of the generated distance intervals to be more inclusive
+ // 2) Bounds of the interval covering to be more inclusive
+ // ... and later on we add the custom $within : annulus matcher.
//
- // This does force us to do our own deduping of results, though.
- scanParams.doNotDedup = true;
-
- // Scan bounds on 2D indexes are only over the 2D field - other bounds aren't applicable.
- // This is handled in query planning.
- scanParams.bounds = _nearParams.baseBounds;
-
- // The "2d" field is always the first in the index
- const string twoDFieldName = _nearParams.nearQuery->field;
- const int twoDFieldPosition = 0;
-
- OrderedIntervalList coveredIntervals;
- coveredIntervals.name = scanParams.bounds.fields[twoDFieldPosition].name;
-
- ExpressionMapping::cover2d(*coverRegion,
- _twoDIndex->infoObj(),
- internalGeoNearQuery2DMaxCoveringCells,
- &coveredIntervals);
-
- // Intersect the $near bounds we just generated into the bounds we have for anything else
- // in the scan (i.e. $within)
- IndexBoundsBuilder::intersectize(coveredIntervals,
- &scanParams.bounds.fields[twoDFieldPosition]);
-
- // These parameters are stored by the index, and so must be ok
- GeoHashConverter::Parameters hashParams;
- GeoHashConverter::parseParameters(_twoDIndex->infoObj(), &hashParams);
-
- MatchExpression* keyMatcher =
- new TwoDKeyInRegionExpression(coverRegion.release(),
- hashParams,
- twoDFieldName);
-
- // 2D indexes support covered search over additional fields they contain
- // TODO: Don't need to clone, can just attach to custom matcher above
- if (_nearParams.filter) {
- AndMatchExpression* andMatcher = new AndMatchExpression();
- andMatcher->add(keyMatcher);
- andMatcher->add(_nearParams.filter->shallowClone());
- keyMatcher = andMatcher;
+ // IMPORTANT: The *internal* interval distance bounds are *exact thresholds* - these
+ // should not be adjusted.
+ // TODO: Maybe integrate annuluses as a standard shape, and literally transform $near
+ // internally into a $within query with $near just as sort.
+
+ // Compute the maximum axis-aligned distance error
+ const double epsilon = std::numeric_limits<double>::epsilon() *
+ (max(abs(_fullBounds.center().x), abs(_fullBounds.center().y)) +
+ _fullBounds.getOuter());
+
+ if (nextBounds.getInner() > 0 && nextBounds.getInner() == _fullBounds.getInner()) {
+ nextBounds = R2Annulus(nextBounds.center(),
+ max(0.0, nextBounds.getInner() - epsilon),
+ nextBounds.getOuter());
}
- // IndexScanWithMatch owns the matcher
- IndexScan* scan = new IndexScanWithMatch(txn, scanParams, workingSet, keyMatcher);
-
- MatchExpression* docMatcher = NULL;
-
- // FLAT searches need to add an additional annulus $within matcher, see above
- if (FLAT == queryCRS) {
- docMatcher = new TwoDPtInAnnulusExpression(_fullBounds, twoDFieldName);
+ if (nextBounds.getOuter() > 0 && nextBounds.getOuter() == _fullBounds.getOuter()) {
+ // We're at the max bound of the search, adjust interval maximum
+ nextBounds = R2Annulus(
+ nextBounds.center(), nextBounds.getInner(), nextBounds.getOuter() + epsilon);
}
- // FetchStage owns index scan
- FetchStage* fetcher(new FetchStageWithMatch(txn,
- workingSet,
- scan,
- docMatcher,
- collection));
-
- return StatusWith<CoveredInterval*>(new CoveredInterval(fetcher,
- true,
- nextBounds.getInner(),
- nextBounds.getOuter(),
- isLastInterval));
- }
+ // *Always* adjust the covering bounds to be more inclusive
+ coverRegion.reset(new R2Annulus(nextBounds.center(),
+ max(0.0, nextBounds.getInner() - epsilon),
+ nextBounds.getOuter() + epsilon));
+ } else {
+ invariant(SPHERE == queryCRS);
+ // TODO: As above, make this consistent with $within : $centerSphere
- StatusWith<double> GeoNear2DStage::computeDistance(WorkingSetMember* member) {
- return computeGeoNearDistance(_nearParams, member);
+ // Our intervals aren't in the same CRS as our index, so we need to adjust them
+ coverRegion.reset(new R2Annulus(projectBoundsToTwoDDegrees(nextBounds)));
}
//
- // GeoNear2DSphereStage
+ // Setup the stages for this interval
//
- static int getFieldPosition(const IndexDescriptor* index, const string& fieldName) {
-
- int fieldPosition = 0;
-
- BSONObjIterator specIt(index->keyPattern());
- while (specIt.more()) {
- if (specIt.next().fieldName() == fieldName) {
- break;
- }
- ++fieldPosition;
- }
-
- if (fieldPosition == index->keyPattern().nFields())
- return -1;
-
- return fieldPosition;
+ IndexScanParams scanParams;
+ scanParams.descriptor = _twoDIndex;
+ scanParams.direction = 1;
+ // We use a filter on the key. The filter rejects keys that don't intersect with the
+ // annulus. An object that is in the annulus might have a key that's not in it and a key
+ // that's in it. As such we can't just look at one key per object.
+ //
+ // This does force us to do our own deduping of results, though.
+ scanParams.doNotDedup = true;
+
+ // Scan bounds on 2D indexes are only over the 2D field - other bounds aren't applicable.
+ // This is handled in query planning.
+ scanParams.bounds = _nearParams.baseBounds;
+
+ // The "2d" field is always the first in the index
+ const string twoDFieldName = _nearParams.nearQuery->field;
+ const int twoDFieldPosition = 0;
+
+ OrderedIntervalList coveredIntervals;
+ coveredIntervals.name = scanParams.bounds.fields[twoDFieldPosition].name;
+
+ ExpressionMapping::cover2d(*coverRegion,
+ _twoDIndex->infoObj(),
+ internalGeoNearQuery2DMaxCoveringCells,
+ &coveredIntervals);
+
+ // Intersect the $near bounds we just generated into the bounds we have for anything else
+ // in the scan (i.e. $within)
+ IndexBoundsBuilder::intersectize(coveredIntervals,
+ &scanParams.bounds.fields[twoDFieldPosition]);
+
+ // These parameters are stored by the index, and so must be ok
+ GeoHashConverter::Parameters hashParams;
+ GeoHashConverter::parseParameters(_twoDIndex->infoObj(), &hashParams);
+
+ MatchExpression* keyMatcher =
+ new TwoDKeyInRegionExpression(coverRegion.release(), hashParams, twoDFieldName);
+
+ // 2D indexes support covered search over additional fields they contain
+ // TODO: Don't need to clone, can just attach to custom matcher above
+ if (_nearParams.filter) {
+ AndMatchExpression* andMatcher = new AndMatchExpression();
+ andMatcher->add(keyMatcher);
+ andMatcher->add(_nearParams.filter->shallowClone());
+ keyMatcher = andMatcher;
}
- static const string kS2IndexNearStage("GEO_NEAR_2DSPHERE");
-
- GeoNear2DSphereStage::GeoNear2DSphereStage(const GeoNearParams& nearParams,
- OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection,
- IndexDescriptor* s2Index)
- : NearStage(txn,
- workingSet,
- collection,
- new PlanStageStats(CommonStats(kS2IndexNearStage.c_str()),
- STAGE_GEO_NEAR_2DSPHERE)),
- _nearParams(nearParams),
- _s2Index(s2Index),
- _fullBounds(geoNearDistanceBounds(*nearParams.nearQuery)),
- _currBounds(_fullBounds.center(), -1, _fullBounds.getInner()),
- _boundsIncrement(0.0) {
-
- getNearStats()->keyPattern = s2Index->keyPattern();
- getNearStats()->indexName = s2Index->indexName();
- }
+ // IndexScanWithMatch owns the matcher
+ IndexScan* scan = new IndexScanWithMatch(txn, scanParams, workingSet, keyMatcher);
- GeoNear2DSphereStage::~GeoNear2DSphereStage() {
+ MatchExpression* docMatcher = NULL;
+
+ // FLAT searches need to add an additional annulus $within matcher, see above
+ if (FLAT == queryCRS) {
+ docMatcher = new TwoDPtInAnnulusExpression(_fullBounds, twoDFieldName);
}
- namespace {
-
- S2Region* buildS2Region(const R2Annulus& sphereBounds) {
- // Internal bounds come in SPHERE CRS units
- // i.e. center is lon/lat, inner/outer are in meters
- S2LatLng latLng = S2LatLng::FromDegrees(sphereBounds.center().y,
- sphereBounds.center().x);
-
- vector<S2Region*> regions;
-
- S2Cap innerCap = S2Cap::FromAxisAngle(latLng.ToPoint(),
- S1Angle::Radians(sphereBounds.getInner()
- / kRadiusOfEarthInMeters));
- innerCap = innerCap.Complement();
- regions.push_back(new S2Cap(innerCap));
-
- // We only need to max bound if this is not a full search of the Earth
- // Using the constant here is important since we use the min of kMaxEarthDistance
- // and the actual bounds passed in to set up the search area.
- if (sphereBounds.getOuter() < kMaxEarthDistanceInMeters) {
- S2Cap outerCap = S2Cap::FromAxisAngle(latLng.ToPoint(),
- S1Angle::Radians(sphereBounds.getOuter()
- / kRadiusOfEarthInMeters));
- regions.push_back(new S2Cap(outerCap));
- }
+ // FetchStage owns index scan
+ FetchStage* fetcher(new FetchStageWithMatch(txn, workingSet, scan, docMatcher, collection));
- // Takes ownership of caps
- return new S2RegionIntersection(&regions);
- }
+ return StatusWith<CoveredInterval*>(new CoveredInterval(
+ fetcher, true, nextBounds.getInner(), nextBounds.getOuter(), isLastInterval));
+}
- /**
- * Expression which checks whether a 2DSphere key for a point (S2 hash) intersects our
- * search region. The search region may have been formed by more granular hashes.
- */
- class TwoDSphereKeyInRegionExpression : public LeafMatchExpression {
- public:
+StatusWith<double> GeoNear2DStage::computeDistance(WorkingSetMember* member) {
+ return computeGeoNearDistance(_nearParams, member);
+}
- TwoDSphereKeyInRegionExpression(const R2Annulus& bounds, StringData twoDSpherePath)
- : LeafMatchExpression(INTERNAL_2DSPHERE_KEY_IN_REGION),
- _region(buildS2Region(bounds)) {
+//
+// GeoNear2DSphereStage
+//
- initPath(twoDSpherePath);
- }
+static int getFieldPosition(const IndexDescriptor* index, const string& fieldName) {
+ int fieldPosition = 0;
- virtual ~TwoDSphereKeyInRegionExpression() {
- }
+ BSONObjIterator specIt(index->keyPattern());
+ while (specIt.more()) {
+ if (specIt.next().fieldName() == fieldName) {
+ break;
+ }
+ ++fieldPosition;
+ }
- virtual void toBSON(BSONObjBuilder* out) const {
- out->append("TwoDSphereKeyInRegionExpression", true);
- }
+ if (fieldPosition == index->keyPattern().nFields())
+ return -1;
+
+ return fieldPosition;
+}
+
+static const string kS2IndexNearStage("GEO_NEAR_2DSPHERE");
+
+GeoNear2DSphereStage::GeoNear2DSphereStage(const GeoNearParams& nearParams,
+ OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection,
+ IndexDescriptor* s2Index)
+ : NearStage(
+ txn,
+ workingSet,
+ collection,
+ new PlanStageStats(CommonStats(kS2IndexNearStage.c_str()), STAGE_GEO_NEAR_2DSPHERE)),
+ _nearParams(nearParams),
+ _s2Index(s2Index),
+ _fullBounds(geoNearDistanceBounds(*nearParams.nearQuery)),
+ _currBounds(_fullBounds.center(), -1, _fullBounds.getInner()),
+ _boundsIncrement(0.0) {
+ getNearStats()->keyPattern = s2Index->keyPattern();
+ getNearStats()->indexName = s2Index->indexName();
+}
+
+GeoNear2DSphereStage::~GeoNear2DSphereStage() {}
+
+namespace {
+
+S2Region* buildS2Region(const R2Annulus& sphereBounds) {
+ // Internal bounds come in SPHERE CRS units
+ // i.e. center is lon/lat, inner/outer are in meters
+ S2LatLng latLng = S2LatLng::FromDegrees(sphereBounds.center().y, sphereBounds.center().x);
+
+ vector<S2Region*> regions;
+
+ S2Cap innerCap = S2Cap::FromAxisAngle(
+ latLng.ToPoint(), S1Angle::Radians(sphereBounds.getInner() / kRadiusOfEarthInMeters));
+ innerCap = innerCap.Complement();
+ regions.push_back(new S2Cap(innerCap));
+
+ // We only need to max bound if this is not a full search of the Earth
+ // Using the constant here is important since we use the min of kMaxEarthDistance
+ // and the actual bounds passed in to set up the search area.
+ if (sphereBounds.getOuter() < kMaxEarthDistanceInMeters) {
+ S2Cap outerCap = S2Cap::FromAxisAngle(
+ latLng.ToPoint(), S1Angle::Radians(sphereBounds.getOuter() / kRadiusOfEarthInMeters));
+ regions.push_back(new S2Cap(outerCap));
+ }
- virtual bool matchesSingleElement(const BSONElement& e) const {
- // Something has gone terribly wrong if this doesn't hold.
- invariant(String == e.type());
- S2Cell keyCell = S2Cell(S2CellId::FromString(e.str()));
- return _region->MayIntersect(keyCell);
- }
+ // Takes ownership of caps
+ return new S2RegionIntersection(&regions);
+}
- const S2Region& getRegion() {
- return *_region;
- }
+/**
+ * Expression which checks whether a 2DSphere key for a point (S2 hash) intersects our
+ * search region. The search region may have been formed by more granular hashes.
+ */
+class TwoDSphereKeyInRegionExpression : public LeafMatchExpression {
+public:
+ TwoDSphereKeyInRegionExpression(const R2Annulus& bounds, StringData twoDSpherePath)
+ : LeafMatchExpression(INTERNAL_2DSPHERE_KEY_IN_REGION), _region(buildS2Region(bounds)) {
+ initPath(twoDSpherePath);
+ }
- //
- // These won't be called.
- //
+ virtual ~TwoDSphereKeyInRegionExpression() {}
- virtual void debugString(StringBuilder& debug, int level = 0) const {
- invariant(false);
- }
+ virtual void toBSON(BSONObjBuilder* out) const {
+ out->append("TwoDSphereKeyInRegionExpression", true);
+ }
- virtual bool equivalent(const MatchExpression* other) const {
- invariant(false);
- return true;
- }
+ virtual bool matchesSingleElement(const BSONElement& e) const {
+ // Something has gone terribly wrong if this doesn't hold.
+ invariant(String == e.type());
+ S2Cell keyCell = S2Cell(S2CellId::FromString(e.str()));
+ return _region->MayIntersect(keyCell);
+ }
- virtual MatchExpression* shallowClone() const {
- invariant(false);
- return NULL;
- }
+ const S2Region& getRegion() {
+ return *_region;
+ }
- private:
+ //
+ // These won't be called.
+ //
- const unique_ptr<S2Region> _region;
- };
+ virtual void debugString(StringBuilder& debug, int level = 0) const {
+ invariant(false);
}
- // Estimate the density of data by search the nearest cells level by level around center.
- class GeoNear2DSphereStage::DensityEstimator {
- public:
- DensityEstimator(const IndexDescriptor* s2Index, const GeoNearParams* nearParams) :
- _s2Index(s2Index), _nearParams(nearParams), _currentLevel(0)
- {
- S2IndexingParams params;
- ExpressionParams::parse2dsphereParams(_s2Index->infoObj(), &params);
- // Since cellId.AppendVertexNeighbors(level, output) requires level < cellId.level(),
- // we have to start to find documents at most S2::kMaxCellLevel - 1. Thus the finest
- // search area is 16 * finest cell area at S2::kMaxCellLevel, which is less than
- // (1.4 inch X 1.4 inch) on the earth.
- _currentLevel = std::max(0, params.finestIndexedLevel - 1);
- }
+ virtual bool equivalent(const MatchExpression* other) const {
+ invariant(false);
+ return true;
+ }
- // Search for a document in neighbors at current level.
- // Return IS_EOF is such document exists and set the estimated distance to the nearest doc.
- PlanStage::StageState work(OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection,
- WorkingSetID* out,
- double* estimatedDistance);
-
- void saveState();
- void restoreState(OperationContext* txn);
- void invalidate(OperationContext* txn, const RecordId& dl, InvalidationType type);
-
- private:
- void buildIndexScan(OperationContext* txn, WorkingSet* workingSet, Collection* collection);
-
- const IndexDescriptor* _s2Index; // Not owned here.
- const GeoNearParams* _nearParams; // Not owned here.
- int _currentLevel;
- unique_ptr<IndexScan> _indexScan;
- };
-
- // Setup the index scan stage for neighbors at this level.
- void GeoNear2DSphereStage::DensityEstimator::buildIndexScan(OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection)
- {
- IndexScanParams scanParams;
- scanParams.descriptor = _s2Index;
- scanParams.direction = 1;
- scanParams.doNotDedup = true;
- scanParams.bounds = _nearParams->baseBounds;
-
- // Because the planner doesn't yet set up 2D index bounds, do it ourselves here
- const string s2Field = _nearParams->nearQuery->field;
- const int s2FieldPosition = getFieldPosition(_s2Index, s2Field);
- fassert(28677, s2FieldPosition >= 0);
- OrderedIntervalList* coveredIntervals = &scanParams.bounds.fields[s2FieldPosition];
- coveredIntervals->intervals.clear();
-
- // Find 4 neighbors (3 neighbors at face vertex) at current level.
- const S2CellId& centerId = _nearParams->nearQuery->centroid->cell.id();
- vector<S2CellId> neighbors;
-
- // The search area expands 4X each time.
- // Return the neighbors of closest vertex to this cell at the given level.
- invariant(_currentLevel < centerId.level());
- centerId.AppendVertexNeighbors(_currentLevel, &neighbors);
-
- // Convert S2CellId to string and sort
- vector<string> neighborKeys;
- for (vector<S2CellId>::const_iterator it = neighbors.begin(); it != neighbors.end(); it++) {
- neighborKeys.push_back(it->toString());
- }
- std::sort(neighborKeys.begin(), neighborKeys.end());
-
- for (vector<string>::const_iterator it = neighborKeys.begin(); it != neighborKeys.end();
- it++)
- {
- // construct interval [*it, end) for this cell.
- std::string end = *it;
- end[end.size() - 1]++;
- coveredIntervals->intervals.push_back(
- IndexBoundsBuilder::makeRangeInterval(*it, end, true, false));
- }
+ virtual MatchExpression* shallowClone() const {
+ invariant(false);
+ return NULL;
+ }
- invariant(coveredIntervals->isValidFor(1));
+private:
+ const unique_ptr<S2Region> _region;
+};
+}
+
+// Estimate the density of data by search the nearest cells level by level around center.
+class GeoNear2DSphereStage::DensityEstimator {
+public:
+ DensityEstimator(const IndexDescriptor* s2Index, const GeoNearParams* nearParams)
+ : _s2Index(s2Index), _nearParams(nearParams), _currentLevel(0) {
+ S2IndexingParams params;
+ ExpressionParams::parse2dsphereParams(_s2Index->infoObj(), &params);
+ // Since cellId.AppendVertexNeighbors(level, output) requires level < cellId.level(),
+ // we have to start to find documents at most S2::kMaxCellLevel - 1. Thus the finest
+ // search area is 16 * finest cell area at S2::kMaxCellLevel, which is less than
+ // (1.4 inch X 1.4 inch) on the earth.
+ _currentLevel = std::max(0, params.finestIndexedLevel - 1);
+ }
- // Index scan
- _indexScan.reset(new IndexScan(txn, scanParams, workingSet, NULL));
+ // Search for a document in neighbors at current level.
+ // Return IS_EOF is such document exists and set the estimated distance to the nearest doc.
+ PlanStage::StageState work(OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection,
+ WorkingSetID* out,
+ double* estimatedDistance);
+
+ void saveState();
+ void restoreState(OperationContext* txn);
+ void invalidate(OperationContext* txn, const RecordId& dl, InvalidationType type);
+
+private:
+ void buildIndexScan(OperationContext* txn, WorkingSet* workingSet, Collection* collection);
+
+ const IndexDescriptor* _s2Index; // Not owned here.
+ const GeoNearParams* _nearParams; // Not owned here.
+ int _currentLevel;
+ unique_ptr<IndexScan> _indexScan;
+};
+
+// Setup the index scan stage for neighbors at this level.
+void GeoNear2DSphereStage::DensityEstimator::buildIndexScan(OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection) {
+ IndexScanParams scanParams;
+ scanParams.descriptor = _s2Index;
+ scanParams.direction = 1;
+ scanParams.doNotDedup = true;
+ scanParams.bounds = _nearParams->baseBounds;
+
+ // Because the planner doesn't yet set up 2D index bounds, do it ourselves here
+ const string s2Field = _nearParams->nearQuery->field;
+ const int s2FieldPosition = getFieldPosition(_s2Index, s2Field);
+ fassert(28677, s2FieldPosition >= 0);
+ OrderedIntervalList* coveredIntervals = &scanParams.bounds.fields[s2FieldPosition];
+ coveredIntervals->intervals.clear();
+
+ // Find 4 neighbors (3 neighbors at face vertex) at current level.
+ const S2CellId& centerId = _nearParams->nearQuery->centroid->cell.id();
+ vector<S2CellId> neighbors;
+
+ // The search area expands 4X each time.
+ // Return the neighbors of closest vertex to this cell at the given level.
+ invariant(_currentLevel < centerId.level());
+ centerId.AppendVertexNeighbors(_currentLevel, &neighbors);
+
+ // Convert S2CellId to string and sort
+ vector<string> neighborKeys;
+ for (vector<S2CellId>::const_iterator it = neighbors.begin(); it != neighbors.end(); it++) {
+ neighborKeys.push_back(it->toString());
+ }
+ std::sort(neighborKeys.begin(), neighborKeys.end());
+
+ for (vector<string>::const_iterator it = neighborKeys.begin(); it != neighborKeys.end(); it++) {
+ // construct interval [*it, end) for this cell.
+ std::string end = *it;
+ end[end.size() - 1]++;
+ coveredIntervals->intervals.push_back(
+ IndexBoundsBuilder::makeRangeInterval(*it, end, true, false));
}
- PlanStage::StageState GeoNear2DSphereStage::DensityEstimator::work(OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection,
- WorkingSetID* out,
- double* estimatedDistance)
- {
- if (!_indexScan) {
- // Setup index scan stage for current level.
- buildIndexScan(txn, workingSet, collection);
- }
+ invariant(coveredIntervals->isValidFor(1));
- WorkingSetID workingSetID;
- PlanStage::StageState state = _indexScan->work(&workingSetID);
-
- if (state == PlanStage::IS_EOF) {
- // We ran through the neighbors but found nothing.
- if (_currentLevel > 0) {
- // Advance to the next level and search again.
- _currentLevel--;
- // Reset index scan for the next level.
- _indexScan.reset(NULL);
- return PlanStage::NEED_TIME;
- }
+ // Index scan
+ _indexScan.reset(new IndexScan(txn, scanParams, workingSet, NULL));
+}
+
+PlanStage::StageState GeoNear2DSphereStage::DensityEstimator::work(OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection,
+ WorkingSetID* out,
+ double* estimatedDistance) {
+ if (!_indexScan) {
+ // Setup index scan stage for current level.
+ buildIndexScan(txn, workingSet, collection);
+ }
- // We are already at the top level.
- *estimatedDistance = S2::kAvgEdge.GetValue(_currentLevel) * kRadiusOfEarthInMeters;
- return PlanStage::IS_EOF;
- } else if (state == PlanStage::ADVANCED) {
- // We found something!
- *estimatedDistance = S2::kAvgEdge.GetValue(_currentLevel) * kRadiusOfEarthInMeters;
- // Clean up working set.
- workingSet->free(workingSetID);
- return PlanStage::IS_EOF;
- } else if (state == PlanStage::NEED_YIELD) {
- *out = workingSetID;
+ WorkingSetID workingSetID;
+ PlanStage::StageState state = _indexScan->work(&workingSetID);
+
+ if (state == PlanStage::IS_EOF) {
+ // We ran through the neighbors but found nothing.
+ if (_currentLevel > 0) {
+ // Advance to the next level and search again.
+ _currentLevel--;
+ // Reset index scan for the next level.
+ _indexScan.reset(NULL);
+ return PlanStage::NEED_TIME;
}
- // Propagate NEED_TIME or errors
- return state;
+ // We are already at the top level.
+ *estimatedDistance = S2::kAvgEdge.GetValue(_currentLevel) * kRadiusOfEarthInMeters;
+ return PlanStage::IS_EOF;
+ } else if (state == PlanStage::ADVANCED) {
+ // We found something!
+ *estimatedDistance = S2::kAvgEdge.GetValue(_currentLevel) * kRadiusOfEarthInMeters;
+ // Clean up working set.
+ workingSet->free(workingSetID);
+ return PlanStage::IS_EOF;
+ } else if (state == PlanStage::NEED_YIELD) {
+ *out = workingSetID;
}
- void GeoNear2DSphereStage::DensityEstimator::saveState() {
- if (_indexScan) {
- _indexScan->saveState();
- }
+ // Propagate NEED_TIME or errors
+ return state;
+}
+
+void GeoNear2DSphereStage::DensityEstimator::saveState() {
+ if (_indexScan) {
+ _indexScan->saveState();
}
+}
- void GeoNear2DSphereStage::DensityEstimator::restoreState(OperationContext* txn) {
- if (_indexScan) {
- _indexScan->restoreState(txn);
- }
+void GeoNear2DSphereStage::DensityEstimator::restoreState(OperationContext* txn) {
+ if (_indexScan) {
+ _indexScan->restoreState(txn);
}
+}
- void GeoNear2DSphereStage::DensityEstimator::invalidate(OperationContext* txn,
- const RecordId& dl,
- InvalidationType type) {
- if (_indexScan) {
- _indexScan->invalidate(txn, dl, type);
- }
+void GeoNear2DSphereStage::DensityEstimator::invalidate(OperationContext* txn,
+ const RecordId& dl,
+ InvalidationType type) {
+ if (_indexScan) {
+ _indexScan->invalidate(txn, dl, type);
}
+}
- PlanStage::StageState GeoNear2DSphereStage::initialize(OperationContext* txn,
- WorkingSet* workingSet,
- Collection* collection,
- WorkingSetID* out)
- {
- if (!_densityEstimator) {
- _densityEstimator.reset(new DensityEstimator(_s2Index, &_nearParams));
- }
+PlanStage::StageState GeoNear2DSphereStage::initialize(OperationContext* txn,
+ WorkingSet* workingSet,
+ Collection* collection,
+ WorkingSetID* out) {
+ if (!_densityEstimator) {
+ _densityEstimator.reset(new DensityEstimator(_s2Index, &_nearParams));
+ }
- double estimatedDistance;
- PlanStage::StageState state = _densityEstimator->work(txn, workingSet, collection, out,
- &estimatedDistance);
-
- if (state == IS_EOF) {
- // We find a document in 4 neighbors at current level, but didn't at previous level.
- //
- // Assuming cell size at current level is d and data is even distributed, the distance
- // between two nearest points are at least d. The following circle with radius of 3 * d
- // covers PI * 9 * d^2, giving at most 30 documents.
- //
- // At the coarsest level, the search area is the whole earth.
- _boundsIncrement = 3 * estimatedDistance;
- invariant(_boundsIncrement > 0.0);
+ double estimatedDistance;
+ PlanStage::StageState state =
+ _densityEstimator->work(txn, workingSet, collection, out, &estimatedDistance);
- // Clean up
- _densityEstimator.reset(NULL);
- }
+ if (state == IS_EOF) {
+ // We find a document in 4 neighbors at current level, but didn't at previous level.
+ //
+ // Assuming cell size at current level is d and data is even distributed, the distance
+ // between two nearest points are at least d. The following circle with radius of 3 * d
+ // covers PI * 9 * d^2, giving at most 30 documents.
+ //
+ // At the coarsest level, the search area is the whole earth.
+ _boundsIncrement = 3 * estimatedDistance;
+ invariant(_boundsIncrement > 0.0);
- return state;
+ // Clean up
+ _densityEstimator.reset(NULL);
}
- void GeoNear2DSphereStage::finishSaveState() {
- if (_densityEstimator) {
- _densityEstimator->saveState();
- }
+ return state;
+}
+
+void GeoNear2DSphereStage::finishSaveState() {
+ if (_densityEstimator) {
+ _densityEstimator->saveState();
}
+}
- void GeoNear2DSphereStage::finishRestoreState(OperationContext* txn) {
- if (_densityEstimator) {
- _densityEstimator->restoreState(txn);
- }
+void GeoNear2DSphereStage::finishRestoreState(OperationContext* txn) {
+ if (_densityEstimator) {
+ _densityEstimator->restoreState(txn);
}
+}
- void GeoNear2DSphereStage::finishInvalidate(OperationContext* txn,
- const RecordId& dl,
- InvalidationType type) {
- if (_densityEstimator) {
- _densityEstimator->invalidate(txn, dl, type);
- }
+void GeoNear2DSphereStage::finishInvalidate(OperationContext* txn,
+ const RecordId& dl,
+ InvalidationType type) {
+ if (_densityEstimator) {
+ _densityEstimator->invalidate(txn, dl, type);
}
+}
- StatusWith<NearStage::CoveredInterval*> //
+StatusWith<NearStage::CoveredInterval*> //
GeoNear2DSphereStage::nextInterval(OperationContext* txn,
WorkingSet* workingSet,
Collection* collection) {
+ // The search is finished if we searched at least once and all the way to the edge
+ if (_currBounds.getInner() >= 0 && _currBounds.getOuter() == _fullBounds.getOuter()) {
+ return StatusWith<CoveredInterval*>(NULL);
+ }
- // The search is finished if we searched at least once and all the way to the edge
- if (_currBounds.getInner() >= 0 && _currBounds.getOuter() == _fullBounds.getOuter()) {
- return StatusWith<CoveredInterval*>(NULL);
- }
+ //
+ // Setup the next interval
+ //
- //
- // Setup the next interval
- //
+ const NearStats* stats = getNearStats();
- const NearStats* stats = getNearStats();
+ if (!stats->intervalStats.empty()) {
+ const IntervalStats& lastIntervalStats = stats->intervalStats.back();
- if (!stats->intervalStats.empty()) {
+ // TODO: Generally we want small numbers of results fast, then larger numbers later
+ if (lastIntervalStats.numResultsBuffered < 300)
+ _boundsIncrement *= 2;
+ else if (lastIntervalStats.numResultsBuffered > 600)
+ _boundsIncrement /= 2;
+ }
- const IntervalStats& lastIntervalStats = stats->intervalStats.back();
+ invariant(_boundsIncrement > 0.0);
- // TODO: Generally we want small numbers of results fast, then larger numbers later
- if (lastIntervalStats.numResultsBuffered < 300)
- _boundsIncrement *= 2;
- else if (lastIntervalStats.numResultsBuffered > 600)
- _boundsIncrement /= 2;
- }
+ R2Annulus nextBounds(_currBounds.center(),
+ _currBounds.getOuter(),
+ min(_currBounds.getOuter() + _boundsIncrement, _fullBounds.getOuter()));
- invariant(_boundsIncrement > 0.0);
+ bool isLastInterval = (nextBounds.getOuter() == _fullBounds.getOuter());
+ _currBounds = nextBounds;
- R2Annulus nextBounds(_currBounds.center(),
- _currBounds.getOuter(),
- min(_currBounds.getOuter() + _boundsIncrement,
- _fullBounds.getOuter()));
+ //
+ // Setup the covering region and stages for this interval
+ //
- bool isLastInterval = (nextBounds.getOuter() == _fullBounds.getOuter());
- _currBounds = nextBounds;
+ IndexScanParams scanParams;
+ scanParams.descriptor = _s2Index;
+ scanParams.direction = 1;
+ // We use a filter on the key. The filter rejects keys that don't intersect with the
+ // annulus. An object that is in the annulus might have a key that's not in it and a key
+ // that's in it. As such we can't just look at one key per object.
+ //
+ // This does force us to do our own deduping of results, though.
+ scanParams.doNotDedup = true;
+ scanParams.bounds = _nearParams.baseBounds;
- //
- // Setup the covering region and stages for this interval
- //
+ // Because the planner doesn't yet set up 2D index bounds, do it ourselves here
+ const string s2Field = _nearParams.nearQuery->field;
+ const int s2FieldPosition = getFieldPosition(_s2Index, s2Field);
+ fassert(28678, s2FieldPosition >= 0);
+ scanParams.bounds.fields[s2FieldPosition].intervals.clear();
+ OrderedIntervalList* coveredIntervals = &scanParams.bounds.fields[s2FieldPosition];
- IndexScanParams scanParams;
- scanParams.descriptor = _s2Index;
- scanParams.direction = 1;
- // We use a filter on the key. The filter rejects keys that don't intersect with the
- // annulus. An object that is in the annulus might have a key that's not in it and a key
- // that's in it. As such we can't just look at one key per object.
- //
- // This does force us to do our own deduping of results, though.
- scanParams.doNotDedup = true;
- scanParams.bounds = _nearParams.baseBounds;
-
- // Because the planner doesn't yet set up 2D index bounds, do it ourselves here
- const string s2Field = _nearParams.nearQuery->field;
- const int s2FieldPosition = getFieldPosition(_s2Index, s2Field);
- fassert(28678, s2FieldPosition >= 0);
- scanParams.bounds.fields[s2FieldPosition].intervals.clear();
- OrderedIntervalList* coveredIntervals = &scanParams.bounds.fields[s2FieldPosition];
-
- TwoDSphereKeyInRegionExpression* keyMatcher =
- new TwoDSphereKeyInRegionExpression(_currBounds, s2Field);
-
- ExpressionMapping::cover2dsphere(keyMatcher->getRegion(),
- _s2Index->infoObj(),
- coveredIntervals);
-
- // IndexScan owns the hash matcher
- IndexScan* scan = new IndexScanWithMatch(txn, scanParams, workingSet, keyMatcher);
-
- // FetchStage owns index scan
- FetchStage* fetcher(new FetchStage(txn, workingSet, scan, _nearParams.filter, collection));
-
- return StatusWith<CoveredInterval*>(new CoveredInterval(fetcher,
- true,
- nextBounds.getInner(),
- nextBounds.getOuter(),
- isLastInterval));
- }
+ TwoDSphereKeyInRegionExpression* keyMatcher =
+ new TwoDSphereKeyInRegionExpression(_currBounds, s2Field);
- StatusWith<double> GeoNear2DSphereStage::computeDistance(WorkingSetMember* member) {
- return computeGeoNearDistance(_nearParams, member);
- }
+ ExpressionMapping::cover2dsphere(
+ keyMatcher->getRegion(), _s2Index->infoObj(), coveredIntervals);
+
+ // IndexScan owns the hash matcher
+ IndexScan* scan = new IndexScanWithMatch(txn, scanParams, workingSet, keyMatcher);
+
+ // FetchStage owns index scan
+ FetchStage* fetcher(new FetchStage(txn, workingSet, scan, _nearParams.filter, collection));
+
+ return StatusWith<CoveredInterval*>(new CoveredInterval(
+ fetcher, true, nextBounds.getInner(), nextBounds.getOuter(), isLastInterval));
+}
-} // namespace mongo
+StatusWith<double> GeoNear2DSphereStage::computeDistance(WorkingSetMember* member) {
+ return computeGeoNearDistance(_nearParams, member);
+}
+} // namespace mongo