summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/core/fts_casesensitive.js27
-rw-r--r--src/mongo/db/matcher/expression_parser_text.cpp25
-rw-r--r--src/mongo/db/matcher/expression_parser_text_test.cpp48
-rw-r--r--src/mongo/db/matcher/expression_text.cpp18
-rw-r--r--src/mongo/db/matcher/expression_text.h4
-rw-r--r--src/mongo/db/query/planner_access.cpp1
-rw-r--r--src/mongo/db/query/query_planner_test_lib.cpp7
-rw-r--r--src/mongo/db/query/query_planner_text_test.cpp8
-rw-r--r--src/mongo/db/query/query_solution.cpp3
-rw-r--r--src/mongo/db/query/query_solution.h1
-rw-r--r--src/mongo/db/query/stage_builder.cpp2
11 files changed, 124 insertions, 20 deletions
diff --git a/jstests/core/fts_casesensitive.js b/jstests/core/fts_casesensitive.js
new file mode 100644
index 00000000000..824bcf52ada
--- /dev/null
+++ b/jstests/core/fts_casesensitive.js
@@ -0,0 +1,27 @@
+// Integration tests for {$caseSensitive: true} option to $text query operator.
+
+load('jstests/libs/fts.js')
+var coll = db.fts_casesensitive;
+
+coll.drop();
+
+assert.writeOK(coll.insert({_id: 0, a: "The Quick Brown Fox Jumps Over The Lazy Dog"}));
+assert.commandWorked(coll.ensureIndex({a: "text"}));
+
+assert.throws(function() { queryIDS(coll, "hello", null, {$caseSensitive: "invalid"}); });
+
+assert.eq([0], queryIDS(coll, "The quick Brown", null, {$caseSensitive: true}));
+assert.eq([0], queryIDS(coll, "Jumped", null, {$caseSensitive: true}));
+assert.eq([0], queryIDS(coll, "\"Quick\"", null, {$caseSensitive: true}));
+assert.eq([0], queryIDS(coll, "\"Fox\" Jumped", null, {$caseSensitive: true}));
+assert.eq([0], queryIDS(coll, "\"Fox Jumps\" \"Over The\"", null, {$caseSensitive: true}));
+assert.eq([0], queryIDS(coll, "\"Fox Jumps\" -\"over the\"", null, {$caseSensitive: true}));
+
+assert.eq([], queryIDS(coll, "The", null, {$caseSensitive: true}));
+assert.eq([], queryIDS(coll, "quick", null, {$caseSensitive: true}));
+assert.eq([], queryIDS(coll, "The quick brown", null, {$caseSensitive: true}));
+assert.eq([], queryIDS(coll, "The -quick -brown", null, {$caseSensitive: true}));
+assert.eq([], queryIDS(coll, "The quick -brown", null, {$caseSensitive: true}));
+assert.eq([], queryIDS(coll, "he Quic", null, {$caseSensitive: true}));
+assert.eq([], queryIDS(coll, "\"over the\"", null, {$caseSensitive: true}));
+assert.eq([], queryIDS(coll, "\"Fox Jumps\" -\"Over The\"", null, {$caseSensitive: true}));
diff --git a/src/mongo/db/matcher/expression_parser_text.cpp b/src/mongo/db/matcher/expression_parser_text.cpp
index ad8114c88e8..0a7cb0b01ab 100644
--- a/src/mongo/db/matcher/expression_parser_text.cpp
+++ b/src/mongo/db/matcher/expression_parser_text.cpp
@@ -44,16 +44,20 @@ namespace mongo {
// Validate queryObj, but defer construction of FTSQuery (which requires access to the
// target namespace) until stage building time.
+ int expectedFieldCount = 1;
+
if ( mongo::String != queryObj["$search"].type() ) {
- return StatusWithMatchExpression( ErrorCodes::BadValue, "$search needs a String" );
+ return StatusWithMatchExpression( ErrorCodes::TypeMismatch,
+ "$search requires a string value" );
}
string language = "";
BSONElement languageElt = queryObj["$language"];
if ( !languageElt.eoo() ) {
+ expectedFieldCount++;
if ( mongo::String != languageElt.type() ) {
- return StatusWithMatchExpression( ErrorCodes::BadValue,
- "$language needs a String" );
+ return StatusWithMatchExpression( ErrorCodes::TypeMismatch,
+ "$language requires a string value" );
}
language = languageElt.String();
Status status =
@@ -65,12 +69,23 @@ namespace mongo {
}
string query = queryObj["$search"].String();
- if ( queryObj.nFields() != ( languageElt.eoo() ? 1 : 2 ) ) {
+ BSONElement caseSensitiveElt = queryObj["$caseSensitive"];
+ bool caseSensitive = fts::FTSQuery::caseSensitiveDefault;
+ if ( !caseSensitiveElt.eoo() ) {
+ expectedFieldCount++;
+ if ( mongo::Bool != caseSensitiveElt.type() ) {
+ return StatusWithMatchExpression( ErrorCodes::TypeMismatch,
+ "$caseSensitive requires a boolean value" );
+ }
+ caseSensitive = caseSensitiveElt.trueValue();
+ }
+
+ if ( queryObj.nFields() != expectedFieldCount ) {
return StatusWithMatchExpression( ErrorCodes::BadValue, "extra fields in $text" );
}
auto_ptr<TextMatchExpression> e( new TextMatchExpression() );
- Status s = e->init( query, language );
+ Status s = e->init( query, language, caseSensitive );
if ( !s.isOK() ) {
return StatusWithMatchExpression( s );
}
diff --git a/src/mongo/db/matcher/expression_parser_text_test.cpp b/src/mongo/db/matcher/expression_parser_text_test.cpp
index b2f42018178..42eb9ce2464 100644
--- a/src/mongo/db/matcher/expression_parser_text_test.cpp
+++ b/src/mongo/db/matcher/expression_parser_text_test.cpp
@@ -39,23 +39,53 @@
namespace mongo {
- TEST( MatchExpressionParserText, Parse1 ) {
- BSONObj query = fromjson( "{$text:{$search:\"awesome\", $language:\"english\"}}" );
+ TEST( MatchExpressionParserText, Basic ) {
+ BSONObj query = fromjson( "{$text: {$search:\"awesome\", $language:\"english\"}}" );
StatusWithMatchExpression result = MatchExpressionParser::parse( query );
ASSERT_TRUE( result.isOK() );
- MatchExpression* exp = result.getValue();
- ASSERT_EQUALS( MatchExpression::TEXT, exp->matchType() );
-
- TextMatchExpression* textExp = static_cast<TextMatchExpression*>( exp );
+ ASSERT_EQUALS( MatchExpression::TEXT, result.getValue()->matchType() );
+ boost::scoped_ptr<TextMatchExpression> textExp(
+ static_cast<TextMatchExpression*>( result.getValue() ) );
ASSERT_EQUALS( textExp->getQuery(), "awesome" );
ASSERT_EQUALS( textExp->getLanguage(), "english" );
- delete exp;
+ ASSERT_EQUALS( textExp->getCaseSensitive(), fts::FTSQuery::caseSensitiveDefault );
+ }
+
+ TEST( MatchExpressionParserText, LanguageError ) {
+ BSONObj query = fromjson( "{$text: {$search:\"awesome\", $language:\"spanglish\"}}" );
+
+ StatusWithMatchExpression result = MatchExpressionParser::parse( query );
+ ASSERT_FALSE( result.isOK() );
+ }
+
+ TEST( MatchExpressionParserText, CaseSensitiveTrue ) {
+ BSONObj query = fromjson( "{$text: {$search:\"awesome\", $caseSensitive: true}}" );
+
+ StatusWithMatchExpression result = MatchExpressionParser::parse( query );
+ ASSERT_TRUE( result.isOK() );
+
+ ASSERT_EQUALS( MatchExpression::TEXT, result.getValue()->matchType() );
+ boost::scoped_ptr<TextMatchExpression> textExp(
+ static_cast<TextMatchExpression*>( result.getValue() ) );
+ ASSERT_EQUALS( textExp->getCaseSensitive(), true );
+ }
+
+ TEST( MatchExpressionParserText, CaseSensitiveFalse ) {
+ BSONObj query = fromjson( "{$text: {$search:\"awesome\", $caseSensitive: false}}" );
+
+ StatusWithMatchExpression result = MatchExpressionParser::parse( query );
+ ASSERT_TRUE( result.isOK() );
+
+ ASSERT_EQUALS( MatchExpression::TEXT, result.getValue()->matchType() );
+ boost::scoped_ptr<TextMatchExpression> textExp(
+ static_cast<TextMatchExpression*>( result.getValue() ) );
+ ASSERT_EQUALS( textExp->getCaseSensitive(), false );
}
- TEST( MatchExpressionParserText, Parse2 ) {
- BSONObj query = fromjson( "{$text:{$search:\"awesome\", $language:\"spanglish\"}}" );
+ TEST( MatchExpressionParserText, CaseSensitiveError ) {
+ BSONObj query = fromjson( "{$text:{$search:\"awesome\", $caseSensitive: 0}}" );
StatusWithMatchExpression result = MatchExpressionParser::parse( query );
ASSERT_FALSE( result.isOK() );
diff --git a/src/mongo/db/matcher/expression_text.cpp b/src/mongo/db/matcher/expression_text.cpp
index 3754d431f57..320136c8751 100644
--- a/src/mongo/db/matcher/expression_text.cpp
+++ b/src/mongo/db/matcher/expression_text.cpp
@@ -35,9 +35,12 @@ namespace mongo {
using std::string;
- Status TextMatchExpression::init( const string& query, const string& language ) {
+ Status TextMatchExpression::init( const string& query,
+ const string& language,
+ bool caseSensitive ) {
_query = query;
_language = language;
+ _caseSensitive = caseSensitive;
return initPath( "_fts" );
}
@@ -50,7 +53,9 @@ namespace mongo {
void TextMatchExpression::debugString( StringBuilder& debug, int level ) const {
_debugAddSpace(debug, level);
- debug << "TEXT : query=" << _query << ", language=" << _language << ", tag=";
+ debug << "TEXT : query=" << _query << ", language="
+ << _language << ", caseSensitive="
+ << _caseSensitive << ", tag=";
MatchExpression::TagData* td = getTag();
if ( NULL != td ) {
td->debugString( &debug );
@@ -62,7 +67,9 @@ namespace mongo {
}
void TextMatchExpression::toBSON(BSONObjBuilder* out) const {
- out->append("$text", BSON("$search" << _query << "$language" << _language));
+ out->append("$text", BSON("$search" << _query <<
+ "$language" << _language <<
+ "$caseSensitive" << _caseSensitive));
}
bool TextMatchExpression::equivalent( const MatchExpression* other ) const {
@@ -79,12 +86,15 @@ namespace mongo {
if ( realOther->getLanguage() != _language ) {
return false;
}
+ if ( realOther->getCaseSensitive() != _caseSensitive ) {
+ return false;
+ }
return true;
}
LeafMatchExpression* TextMatchExpression::shallowClone() const {
TextMatchExpression* next = new TextMatchExpression();
- next->init( _query, _language );
+ next->init( _query, _language, _caseSensitive );
if ( getTag() ) {
next->setTag( getTag()->clone() );
}
diff --git a/src/mongo/db/matcher/expression_text.h b/src/mongo/db/matcher/expression_text.h
index 6ebea78ba3d..8d853de5621 100644
--- a/src/mongo/db/matcher/expression_text.h
+++ b/src/mongo/db/matcher/expression_text.h
@@ -41,7 +41,7 @@ namespace mongo {
TextMatchExpression() : LeafMatchExpression( TEXT ) {}
virtual ~TextMatchExpression() {}
- Status init( const std::string& query, const std::string& language );
+ Status init( const std::string& query, const std::string& language, bool caseSensitive );
virtual bool matchesSingleElement( const BSONElement& e ) const;
@@ -55,9 +55,11 @@ namespace mongo {
const std::string& getQuery() const { return _query; }
const std::string& getLanguage() const { return _language; }
+ bool getCaseSensitive() const { return _caseSensitive; }
private:
std::string _query;
std::string _language;
+ bool _caseSensitive;
};
} // namespace mongo
diff --git a/src/mongo/db/query/planner_access.cpp b/src/mongo/db/query/planner_access.cpp
index aa107cd906d..e41b3559bb9 100644
--- a/src/mongo/db/query/planner_access.cpp
+++ b/src/mongo/db/query/planner_access.cpp
@@ -151,6 +151,7 @@ namespace mongo {
ret->indexKeyPattern = index.keyPattern;
ret->query = textExpr->getQuery();
ret->language = textExpr->getLanguage();
+ ret->caseSensitive = textExpr->getCaseSensitive();
return ret;
}
else {
diff --git a/src/mongo/db/query/query_planner_test_lib.cpp b/src/mongo/db/query/query_planner_test_lib.cpp
index 72784d711f2..570d5da2e81 100644
--- a/src/mongo/db/query/query_planner_test_lib.cpp
+++ b/src/mongo/db/query/query_planner_test_lib.cpp
@@ -287,6 +287,13 @@ namespace mongo {
}
}
+ BSONElement caseSensitiveElt = textObj["caseSensitive"];
+ if (!caseSensitiveElt.eoo()) {
+ if (caseSensitiveElt.trueValue() != node->caseSensitive) {
+ return false;
+ }
+ }
+
BSONElement indexPrefix = textObj["prefix"];
if (!indexPrefix.eoo()) {
if (!indexPrefix.isABSONObj()) {
diff --git a/src/mongo/db/query/query_planner_text_test.cpp b/src/mongo/db/query/query_planner_text_test.cpp
index 376cf8fd809..672af7ddc41 100644
--- a/src/mongo/db/query/query_planner_text_test.cpp
+++ b/src/mongo/db/query/query_planner_text_test.cpp
@@ -624,4 +624,12 @@ namespace {
"pattern: {other: 1}}}]}}}}");
}
+ TEST_F(QueryPlannerTest, TextCaseSensitive) {
+ addIndex(BSON("_fts" << "text" << "_ftsx" << 1));
+ runQuery(fromjson("{$text: {$search: 'blah', $caseSensitive: true}}"));
+
+ assertNumSolutions(1);
+ assertSolutionExists("{text: {search: 'blah', caseSensitive: true}}");
+ }
+
} // namespace
diff --git a/src/mongo/db/query/query_solution.cpp b/src/mongo/db/query/query_solution.cpp
index 877e58612c3..91785b2d218 100644
--- a/src/mongo/db/query/query_solution.cpp
+++ b/src/mongo/db/query/query_solution.cpp
@@ -77,6 +77,8 @@ namespace mongo {
addIndent(ss, indent + 1);
*ss << "language = " << language << '\n';
addIndent(ss, indent + 1);
+ *ss << "caseSensitive= " << caseSensitive << '\n';
+ addIndent(ss, indent + 1);
*ss << "indexPrefix = " << indexPrefix.toString() << '\n';
if (NULL != filter) {
addIndent(ss, indent + 1);
@@ -93,6 +95,7 @@ namespace mongo {
copy->indexKeyPattern = this->indexKeyPattern;
copy->query = this->query;
copy->language = this->language;
+ copy->caseSensitive = this->caseSensitive;
copy->indexPrefix = this->indexPrefix;
return copy;
diff --git a/src/mongo/db/query/query_solution.h b/src/mongo/db/query/query_solution.h
index 544d911c9eb..95afa910864 100644
--- a/src/mongo/db/query/query_solution.h
+++ b/src/mongo/db/query/query_solution.h
@@ -239,6 +239,7 @@ namespace mongo {
BSONObj indexKeyPattern;
std::string query;
std::string language;
+ bool caseSensitive;
// "Prefix" fields of a text index can handle equality predicates. We group them with the
// text node while creating the text leaf node and convert them into a BSONObj index prefix
diff --git a/src/mongo/db/query/stage_builder.cpp b/src/mongo/db/query/stage_builder.cpp
index eeabcca2bff..f6c1b93da09 100644
--- a/src/mongo/db/query/stage_builder.cpp
+++ b/src/mongo/db/query/stage_builder.cpp
@@ -266,7 +266,7 @@ namespace mongo {
Status parseStatus = params.query.parse(node->query,
language,
- fts::FTSQuery::caseSensitiveDefault,
+ node->caseSensitive,
fam->getSpec().getTextIndexVersion());
if (!parseStatus.isOK()) {
warning() << "Can't parse text search query";