diff options
-rw-r--r-- | jstests/fts_querylang.js | 88 | ||||
-rw-r--r-- | jstests/slowNightly/sharding_passthrough.js | 3 | ||||
-rw-r--r-- | src/mongo/db/fts/fts_command.h | 3 | ||||
-rw-r--r-- | src/mongo/db/fts/fts_search.h | 3 | ||||
-rw-r--r-- | src/mongo/db/query/indexability.h | 3 | ||||
-rw-r--r-- | src/mongo/db/query/new_find.cpp | 7 | ||||
-rw-r--r-- | src/mongo/db/query/plan_enumerator.cpp | 5 | ||||
-rw-r--r-- | src/mongo/db/query/query_planner.cpp | 33 | ||||
-rw-r--r-- | src/mongo/db/query/query_solution.cpp | 23 | ||||
-rw-r--r-- | src/mongo/db/query/query_solution.h | 23 | ||||
-rw-r--r-- | src/mongo/db/query/stage_builder.cpp | 31 | ||||
-rw-r--r-- | src/mongo/db/query/stage_types.h | 1 |
12 files changed, 214 insertions, 9 deletions
diff --git a/jstests/fts_querylang.js b/jstests/fts_querylang.js new file mode 100644 index 00000000000..ac2285d25b2 --- /dev/null +++ b/jstests/fts_querylang.js @@ -0,0 +1,88 @@ +// Test $text query operator. + +var t = db.getSiblingDB("test").getCollection("fts_querylang"); +var cursor; +var results; + +db.adminCommand({setParameter:1, textSearchEnabled:true}); +db.adminCommand({setParameter:1, newQueryFrameworkEnabled:true}); + +t.drop(); + +t.insert({_id:0, _idCopy: 0, a:"textual content"}); +t.insert({_id:1, _idCopy: 1, a:"additional content"}); +t.insert({_id:2, _idCopy: 2, a:"irrelevant content"}); +t.ensureIndex({a:"text"}); + +// Test implicit sort for basic text query. +results = t.find({$text: {$search: "textual content -irrelevant"}}).toArray(); +assert.eq(results.length, 2); +assert.eq(results[0]._id, 0); +assert.eq(results[1]._id, 1); + +// Test implicit limit for basic text query. +for (var i=0; i<200; i++) { + t.insert({a: "temporary content"}) +} +results = t.find({$text: {$search: "textual content -irrelevant"}}).toArray(); +assert.eq(results.length, 100); +t.remove({a: "temporary content"}); + +// Test skip for basic text query. +results = t.find({$text: {$search: "textual content -irrelevant"}}).skip(1).toArray(); +assert.eq(results.length, 1); +assert.eq(results[0]._id, 1); + +// Test explicit limit for basic text query. +results = t.find({$text: {$search: "textual content -irrelevant"}}).limit(1).toArray(); +assert.eq(results.length, 1); +assert.eq(results[0]._id, 0); + +// TODO Test basic text query with explicit sort, once sort is enabled in the new query framework. + +// TODO Test basic text query with projection, once projection is enabled in the new query +// framework. + +// Test $and of basic text query with indexed expression. +results = t.find({$text: {$search: "content -irrelevant"}, + _id: 1}).toArray(); +assert.eq(results.length, 1); +assert.eq(results[0]._id, 1); + +// Test $and of basic text query with unindexed expression. +results = t.find({$text: {$search: "content -irrelevant"}, + _idCopy: 1}).toArray(); +assert.eq(results.length, 1); +assert.eq(results[0]._id, 1); + +// TODO Test that $or of basic text query with indexed expression is disallowed. + +// Test that $or of basic text query with unindexed expression is disallowed. +assert.throws(function() { t.find({$or: [{$text: {$search: "content -irrelevant"}}, + {_idCopy: 2}]}).itcount(); }); + +// TODO Test invalid inputs for $text, $search, $language. + +// TODO Test $language. + +// TODO Test $and of basic text query with geo expression. + +// TODO Test update with $text, once it is enabled with the new query framework. + +// TODO Test remove with $text, once it is enabled with the new query framework. + +// TODO Test count with $text, once it is enabled with the new query framework. + +// TODO Test findAndModify with $text, once it is enabled with the new query framework. + +// TODO Test aggregate with $text, once it is enabled with the new query framework. + +// TODO Test that old query framework rejects $text queries. + +// TODO Test that $text fails without a text index. + +// TODO Test that $text accepts a hint of the text index. + +// TODO Test that $text fails if a different index is hinted. + +// TODO Test $text with {$natural:1} sort, {$natural:1} hint. diff --git a/jstests/slowNightly/sharding_passthrough.js b/jstests/slowNightly/sharding_passthrough.js index 2be95201544..6963717bea0 100644 --- a/jstests/slowNightly/sharding_passthrough.js +++ b/jstests/slowNightly/sharding_passthrough.js @@ -92,7 +92,8 @@ files.forEach(function(x) { 'update4|' + 'update_setOnInsert|' + 'profile\\d*|' + - 'max_time_ms' + // Will be fixed when SERVER-2212 is resolved. + 'max_time_ms|' + // Will be fixed when SERVER-2212 is resolved. + 'fts_querylang' + // Will be fixed when SERVER-9063 is resolved. ')\.js$'); // These aren't supposed to get run under sharding: diff --git a/src/mongo/db/fts/fts_command.h b/src/mongo/db/fts/fts_command.h index 990a9a64998..81a4a3d6043 100644 --- a/src/mongo/db/fts/fts_command.h +++ b/src/mongo/db/fts/fts_command.h @@ -35,6 +35,9 @@ #include "mongo/db/commands.h" +// mongo::fts::FTSCommand is deprecated: the "text" command is deprecated in favor of the $text +// query operator. + namespace mongo { namespace fts { diff --git a/src/mongo/db/fts/fts_search.h b/src/mongo/db/fts/fts_search.h index 46264e20d43..17070cec903 100644 --- a/src/mongo/db/fts/fts_search.h +++ b/src/mongo/db/fts/fts_search.h @@ -42,6 +42,9 @@ #include "mongo/db/index/index_descriptor.h" #include "mongo/db/matcher.h" +// mongo::fts::FTSSearch is deprecated: the "text" command is deprecated in favor of the $text +// query operator. + namespace mongo { class BtreeCursor; diff --git a/src/mongo/db/query/indexability.h b/src/mongo/db/query/indexability.h index 626a4cdd1a0..b72eb2a6573 100644 --- a/src/mongo/db/query/indexability.h +++ b/src/mongo/db/query/indexability.h @@ -48,7 +48,8 @@ namespace mongo { || me->matchType() == MatchExpression::MATCH_IN || me->matchType() == MatchExpression::TYPE_OPERATOR || me->matchType() == MatchExpression::GEO - || me->matchType() == MatchExpression::GEO_NEAR; + || me->matchType() == MatchExpression::GEO_NEAR + || me->matchType() == MatchExpression::TEXT; } /** diff --git a/src/mongo/db/query/new_find.cpp b/src/mongo/db/query/new_find.cpp index 5854a196216..dfac2b653fc 100644 --- a/src/mongo/db/query/new_find.cpp +++ b/src/mongo/db/query/new_find.cpp @@ -531,6 +531,10 @@ namespace mongo { // We use this a lot below. const LiteParsedQuery& pq = cq->getParsed(); + // Need to call cq->toString() now, since upon error getRunner doesn't guarantee + // cq is in a consistent state. + string cqStr = cq->toString(); + Status status = Status::OK(); if (pq.hasOption(QueryOption_OplogReplay)) { status = getOplogStartHack(cq, &rawRunner); @@ -541,8 +545,7 @@ namespace mongo { } if (!status.isOK()) { - uasserted(17007, "Couldn't process query " + cq->toString() - + " why: " + status.reason()); + uasserted(17007, "Couldn't process query " + cqStr + " why: " + status.reason()); } verify(NULL != rawRunner); diff --git a/src/mongo/db/query/plan_enumerator.cpp b/src/mongo/db/query/plan_enumerator.cpp index 6c806cfa624..1af135c4bdc 100644 --- a/src/mongo/db/query/plan_enumerator.cpp +++ b/src/mongo/db/query/plan_enumerator.cpp @@ -304,11 +304,14 @@ namespace mongo { for (size_t j = 0; j < oie.preds.size(); ++j) { MatchExpression* expr = oie.preds[j]; - // TODO: Text goes here. if (MatchExpression::GEO_NEAR == expr->matchType()) { hasPredThatRequiresIndex = true; break; } + if (MatchExpression::TEXT == expr->matchType()) { + hasPredThatRequiresIndex = true; + break; + } } if (hasPredThatRequiresIndex) { diff --git a/src/mongo/db/query/query_planner.cpp b/src/mongo/db/query/query_planner.cpp index f8c3a77ad3d..d67a29d666f 100644 --- a/src/mongo/db/query/query_planner.cpp +++ b/src/mongo/db/query/query_planner.cpp @@ -37,6 +37,7 @@ #include "mongo/client/dbclientinterface.h" #include "mongo/db/matcher/expression_array.h" #include "mongo/db/matcher/expression_geo.h" +#include "mongo/db/matcher/expression_text.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/query/canonical_query.h" #include "mongo/db/query/index_bounds_builder.h" @@ -153,7 +154,10 @@ namespace mongo { } return false; } - else if ("text" == ixtype || "_fts" == ixtype || "geoHaystack" == ixtype) { + else if ("text" == ixtype || "fts" == ixtype) { + return (exprtype == MatchExpression::TEXT); + } + else if ("geoHaystack" == ixtype) { return false; } else { @@ -286,6 +290,16 @@ namespace mongo { ret->seek = nearme->getRawObj(); return ret; } + else if (MatchExpression::TEXT == expr->matchType()) { + // We must not keep the expression node around. + *exact = true; + TextMatchExpression* textExpr = static_cast<TextMatchExpression*>(expr); + TextNode* ret = new TextNode(); + ret->_indexKeyPattern = index.keyPattern; + ret->_query = textExpr->getQuery(); + ret->_language = textExpr->getLanguage(); + return ret; + } else { // QLOG() << "making ixscan for " << expr->toString() << endl; @@ -398,7 +412,7 @@ namespace mongo { void QueryPlanner::finishLeafNode(QuerySolutionNode* node, const IndexEntry& index) { const StageType type = node->getType(); - if (STAGE_GEO_2D == type || STAGE_GEO_NEAR_2D == type) { + if (STAGE_GEO_2D == type || STAGE_GEO_NEAR_2D == type || STAGE_TEXT == type) { // XXX: do we do anything here? return; } @@ -1153,6 +1167,15 @@ namespace mongo { } } + // Likewise, if there is a TEXT it must have an index it can use directly. + MatchExpression* textNode; + if (QueryPlannerCommon::hasNode(query.root(), MatchExpression::TEXT, &textNode)) { + RelevantTag* tag = static_cast<RelevantTag*>(textNode->getTag()); + if (0 == tag->first.size() && 0 == tag->notFirst.size()) { + return; + } + } + // If we have any relevant indices, we try to create indexed plans. if (0 < relevantIndices.size()) { for (size_t i = 0; i < relevantIndices.size(); ++i) { @@ -1279,8 +1302,10 @@ namespace mongo { // TODO: Do we always want to offer a collscan solution? // XXX: currently disabling the always-use-a-collscan in order to find more planner bugs. - if (!QueryPlannerCommon::hasNode(query.root(), MatchExpression::GEO_NEAR) - && ((options & QueryPlanner::INCLUDE_COLLSCAN) || (0 == out->size() && canTableScan))) { + if ( !QueryPlannerCommon::hasNode(query.root(), MatchExpression::GEO_NEAR) + && !QueryPlannerCommon::hasNode(query.root(), MatchExpression::TEXT) + && ((options & QueryPlanner::INCLUDE_COLLSCAN) || (0 == out->size() && canTableScan))) + { QuerySolution* collscan = makeCollectionScan(query, false); out->push_back(collscan); QLOG() << "Planner: outputting a collscan\n"; diff --git a/src/mongo/db/query/query_solution.cpp b/src/mongo/db/query/query_solution.cpp index 66f4c756228..4647eb592de 100644 --- a/src/mongo/db/query/query_solution.cpp +++ b/src/mongo/db/query/query_solution.cpp @@ -31,6 +31,29 @@ namespace mongo { // + // TextNode + // + + void TextNode::appendToString(stringstream* ss, int indent) const { + addIndent(ss, indent); + *ss << "TEXT\n"; + addIndent(ss, indent + 1); + *ss << "numWanted = " << _numWanted << endl; + addIndent(ss, indent + 1); + *ss << "keyPattern = " << _indexKeyPattern.toString() << endl; + addIndent(ss, indent + 1); + *ss << "fetched = " << fetched() << endl; + addIndent(ss, indent + 1); + *ss << "sortedByDiskLoc = " << sortedByDiskLoc() << endl; + addIndent(ss, indent + 1); + *ss << "getSort = " << getSort().toString() << endl; + addIndent(ss, indent + 1); + *ss << "query = " << _query << endl; + addIndent(ss, indent + 1); + *ss << "language = " << _language << endl; + } + + // // CollectionScanNode // diff --git a/src/mongo/db/query/query_solution.h b/src/mongo/db/query/query_solution.h index c8a0e933247..2ccbcfa4ffe 100644 --- a/src/mongo/db/query/query_solution.h +++ b/src/mongo/db/query/query_solution.h @@ -30,12 +30,15 @@ #include "mongo/db/matcher/expression.h" #include "mongo/db/geo/geoquery.h" +#include "mongo/db/fts/fts_query.h" #include "mongo/db/query/index_bounds.h" #include "mongo/db/query/projection_parser.h" #include "mongo/db/query/stage_types.h" namespace mongo { + using mongo::fts::FTSQuery; + /** * This is an abstract representation of a query plan. It can be transcribed into a tree of * PlanStages, which can then be handed to a PlanRunner for execution. @@ -155,6 +158,26 @@ namespace mongo { MONGO_DISALLOW_COPYING(QuerySolution); }; + struct TextNode : public QuerySolutionNode { + TextNode() : _numWanted(100) { } + virtual ~TextNode() { } + + virtual StageType getType() const { return STAGE_TEXT; } + + virtual void appendToString(stringstream* ss, int indent) const; + + bool fetched() const { return false; } + bool hasField(const string& field) const { return false; } + bool sortedByDiskLoc() const { return false; } + BSONObj getSort() const { return _indexKeyPattern; } + + uint32_t _numWanted; + BSONObj _indexKeyPattern; + std::string _query; + std::string _language; + scoped_ptr<MatchExpression> _filter; + }; + struct CollectionScanNode : public QuerySolutionNode { CollectionScanNode(); virtual ~CollectionScanNode() { } diff --git a/src/mongo/db/query/stage_builder.cpp b/src/mongo/db/query/stage_builder.cpp index 1859329f26b..8de28f45464 100644 --- a/src/mongo/db/query/stage_builder.cpp +++ b/src/mongo/db/query/stage_builder.cpp @@ -40,6 +40,7 @@ #include "mongo/db/exec/s2near.h" #include "mongo/db/exec/sort.h" #include "mongo/db/exec/skip.h" +#include "mongo/db/exec/text.h" #include "mongo/db/index/catalog_hack.h" #include "mongo/db/namespace_details.h" @@ -189,6 +190,36 @@ namespace mongo { return new S2NearStage(ns, node->indexKeyPattern, node->nq, node->baseBounds, node->filter.get(), ws); } + else if (STAGE_TEXT == root->getType()) { + const TextNode* node = static_cast<const TextNode*>(root); + + NamespaceDetails* nsd = nsdetails(ns.c_str()); + if (NULL == nsd) { return NULL; } + vector<int> idxMatches; + nsd->findIndexByType("text", idxMatches); + if (0 == idxMatches.size()) { return NULL; } + IndexDescriptor* index = CatalogHack::getDescriptor(nsd, idxMatches[0]); + auto_ptr<FTSAccessMethod> fam(new FTSAccessMethod(index)); + TextStageParams params(fam->getSpec()); + + params.ns = ns; + params.index = index; + params.spec = fam->getSpec(); + params.limit = node->_numWanted; + Status s = fam->getSpec().getIndexPrefix(BSONObj(), ¶ms.indexPrefix); + if (!s.isOK()) { return NULL; } + + string language = ("" == node->_language + ? fam->getSpec().defaultLanguage() + : node->_language); + + FTSQuery ftsq; + Status parseStatus = ftsq.parse(node->_query, language); + if (!parseStatus.isOK()) { return NULL; } + params.query = ftsq; + + return new TextStage(params, ws, node->_filter.get()); + } else { stringstream ss; root->appendToString(&ss, 0); diff --git a/src/mongo/db/query/stage_types.h b/src/mongo/db/query/stage_types.h index 5db655f8e81..c59c20c2cf1 100644 --- a/src/mongo/db/query/stage_types.h +++ b/src/mongo/db/query/stage_types.h @@ -53,6 +53,7 @@ namespace mongo { STAGE_SKIP, STAGE_SORT, STAGE_SORT_MERGE, + STAGE_TEXT, STAGE_UNKNOWN, }; |