/** * Copyright 2011 (c) 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 . * * 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. */ #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery #include "mongo/platform/basic.h" #include "mongo/db/pipeline/document_source.h" #include "mongo/db/pipeline/document.h" #include "mongo/util/log.h" namespace mongo { using boost::intrusive_ptr; using std::min; REGISTER_DOCUMENT_SOURCE(geoNear, DocumentSourceGeoNear::createFromBson); const char* DocumentSourceGeoNear::getSourceName() const { return "$geoNear"; } boost::optional DocumentSourceGeoNear::getNext() { pExpCtx->checkForInterrupt(); if (!resultsIterator) runCommand(); if (!resultsIterator->more()) return boost::none; // each result from the geoNear command is wrapped in a wrapper object with "obj", // "dis" and maybe "loc" fields. We want to take the object from "obj" and inject the // other fields into it. Document result(resultsIterator->next().embeddedObject()); MutableDocument output(result["obj"].getDocument()); output.setNestedField(*distanceField, result["dis"]); if (includeLocs) output.setNestedField(*includeLocs, result["loc"]); return output.freeze(); } void DocumentSourceGeoNear::setSource(DocumentSource*) { uasserted(16602, "$geoNear is only allowed as the first pipeline stage"); } bool DocumentSourceGeoNear::coalesce(const intrusive_ptr& pNextSource) { DocumentSourceLimit* limitSrc = dynamic_cast(pNextSource.get()); if (limitSrc) { limit = min(limit, limitSrc->getLimit()); return true; } return false; } // This command is sent as-is to the shards. // On router this becomes a sort by distance (nearest-first) with limit. intrusive_ptr DocumentSourceGeoNear::getShardSource() { return this; } intrusive_ptr DocumentSourceGeoNear::getMergeSource() { return DocumentSourceSort::create(pExpCtx, BSON(distanceField->getPath(false) << 1), limit); } Value DocumentSourceGeoNear::serialize(bool explain) const { MutableDocument result; if (coordsIsArray) { result.setField("near", Value(BSONArray(coords))); } else { result.setField("near", Value(coords)); } // not in buildGeoNearCmd result.setField("distanceField", Value(distanceField->getPath(false))); result.setField("limit", Value(limit)); if (maxDistance > 0) result.setField("maxDistance", Value(maxDistance)); if (minDistance > 0) result.setField("minDistance", Value(minDistance)); result.setField("query", Value(query)); result.setField("spherical", Value(spherical)); result.setField("distanceMultiplier", Value(distanceMultiplier)); if (includeLocs) result.setField("includeLocs", Value(includeLocs->getPath(false))); return Value(DOC(getSourceName() << result.freeze())); } BSONObj DocumentSourceGeoNear::buildGeoNearCmd() const { // this is very similar to sourceToBson, but slightly different. // differences will be noted. BSONObjBuilder geoNear; // not building a subField geoNear.append("geoNear", pExpCtx->ns.coll()); // not in toBson if (coordsIsArray) { geoNear.appendArray("near", coords); } else { geoNear.append("near", coords); } geoNear.append("num", limit); // called limit in toBson if (maxDistance > 0) geoNear.append("maxDistance", maxDistance); if (minDistance > 0) geoNear.append("minDistance", minDistance); geoNear.append("query", query); geoNear.append("spherical", spherical); geoNear.append("distanceMultiplier", distanceMultiplier); if (includeLocs) geoNear.append("includeLocs", true); // String in toBson return geoNear.obj(); } void DocumentSourceGeoNear::runCommand() { massert(16603, "Already ran geoNearCommand", !resultsIterator); bool ok = _mongod->directClient()->runCommand( pExpCtx->ns.db().toString(), buildGeoNearCmd(), cmdOutput); uassert(16604, "geoNear command failed: " + cmdOutput.toString(), ok); resultsIterator.reset(new BSONObjIterator(cmdOutput["results"].embeddedObject())); } intrusive_ptr DocumentSourceGeoNear::create( const intrusive_ptr& pCtx) { return new DocumentSourceGeoNear(pCtx); } intrusive_ptr DocumentSourceGeoNear::createFromBson( BSONElement elem, const intrusive_ptr& pCtx) { intrusive_ptr out = new DocumentSourceGeoNear(pCtx); out->parseOptions(elem.embeddedObjectUserCheck()); return out; } void DocumentSourceGeoNear::parseOptions(BSONObj options) { // near and distanceField are required uassert(16605, "$geoNear requires a 'near' option as an Array", options["near"].isABSONObj()); // Array or Object (Object is deprecated) coordsIsArray = options["near"].type() == Array; coords = options["near"].embeddedObject().getOwned(); uassert(16606, "$geoNear requires a 'distanceField' option as a String", options["distanceField"].type() == String); distanceField.reset(new FieldPath(options["distanceField"].str())); // remaining fields are optional // num and limit are synonyms if (options["limit"].isNumber()) limit = options["limit"].numberLong(); if (options["num"].isNumber()) limit = options["num"].numberLong(); if (options["maxDistance"].isNumber()) maxDistance = options["maxDistance"].numberDouble(); if (options["minDistance"].isNumber()) minDistance = options["minDistance"].numberDouble(); if (options["query"].type() == Object) query = options["query"].embeddedObject().getOwned(); spherical = options["spherical"].trueValue(); if (options["distanceMultiplier"].isNumber()) distanceMultiplier = options["distanceMultiplier"].numberDouble(); if (options.hasField("includeLocs")) { uassert(16607, "$geoNear requires that 'includeLocs' option is a String", options["includeLocs"].type() == String); includeLocs.reset(new FieldPath(options["includeLocs"].str())); } if (options.hasField("uniqueDocs")) warning() << "ignoring deprecated uniqueDocs option in $geoNear aggregation stage"; } DocumentSourceGeoNear::DocumentSourceGeoNear(const intrusive_ptr& pExpCtx) : DocumentSource(pExpCtx), coordsIsArray(false), limit(100), maxDistance(-1.0), minDistance(-1.0), spherical(false), distanceMultiplier(1.0) {} }