42

I am using MongoDB and the C# driver for MongoDB.

I recently discovered that all queries in MongoDB are case-sensitive. How can I make a case-insensitive search?

I found one way to do this:

Query.Matches(
    "FirstName", 
    BsonRegularExpression.Create(new Regex(searchKey,RegexOptions.IgnoreCase)));
i3arnon
  • 101,022
  • 27
  • 291
  • 322
Andrew Orsich
  • 49,434
  • 15
  • 132
  • 132

11 Answers11

68

The simplest and safest way to do that is using Linq:

var names = namesCollection.AsQueryable().Where(name =>
    name.FirstName.ToLower().Contains("hamster"));

As explained in the tutorial ToLower, ToLowerInvariant, ToUpper and ToUpperInvariant all perform matches in a case insensitive way. After that you can use all the supported string methods like Contains or StartsWith.

This example will generate:

{
    "FirstName" : /hamster/is
}

The i option makes it case insensitive.

i3arnon
  • 101,022
  • 27
  • 291
  • 322
  • 3
    This should be the top answer! – Meta-Knight May 20 '14 at 20:51
  • how can you know whether the `query` using `Linq` has found something or not, I just tried to use it, but I don't know whether my condition is met or not – Kyojimaru Sep 09 '14 at 11:14
  • @Kyojimaru where is a filter. It returns only what passes through the filter and meets the condition. If you want to get the items themselves use a `foreach` loop. If you only want to know **whether any** document meets the condition you can use the Any extension method. – i3arnon Sep 09 '14 at 11:20
  • what is the result if I use the `any`,I used it before like this `col.AsQueryable().Where(x => x._id.ToLower().Contains(input.ToLower));` I tried using `foreach` and put `int i = 0;` inside it, but it never hit it when debugging. The `id` inside my collection is `sushi`, and when the input is `Sushi` or any variant of lower or upper case, it never hit the `int i = 0;` inside the `foreach` loop. I also tried using `.Count()` but it always says `0` for the results – Kyojimaru Sep 09 '14 at 13:06
  • 1
    @Kyojimaru then you don't have any documents that satisfy that condition – i3arnon Sep 09 '14 at 13:18
  • what does `s` do in `/is`? – waitforit Mar 05 '18 at 18:12
  • @legen---waitforit---dary "Allows the dot character (i.e. .) to match all characters *including* newline characters." https://docs.mongodb.com/manual/reference/operator/query/regex/#regex-dot-new-line – i3arnon Mar 05 '18 at 19:08
  • @i3arnon Is there a deterministic way of knowing whether this query is being performed against the database (as expected) OR if actually, the whole collection is being pulled into memory and filtered in place? – Leonardo Dec 05 '20 at 15:45
  • @Leonardo How hard do you want to prove it? :) https://stackoverflow.com/a/2876655/885318 – i3arnon Dec 06 '20 at 16:05
  • @i3arnon the things is that a couple of days ago, I was working with a similar, apparently trivial query like this one, and the performance was bad. Writing what I thought was the underlying query in robo3T, the results came in very very fast... I quickly deducted that the operation in question, which was similar to `ToLower` was not supported by the driver... – Leonardo Dec 06 '20 at 16:38
  • @Leonardo Did you use `AsQueryable`? I guess you didn't. If you would have I would expect that query to fail, not be executed in the client-side instead.. – i3arnon Dec 06 '20 at 18:02
  • 1
    There is one VERY important detail that took me hours to figure out: when using Linq you can't use ToLower and Equals, you must use "==" instead. I.e: .Where(x => x.Name.ToLower().Equals("fernando")) throws exception while .Where(x => x.Name.ToLower() == "fernando") works just fine! – Auresco82 Feb 10 '21 at 01:07
42

I've just implemented this much simpler than any of the other suggestions. However I realise due to the age of this question, this functionality may not have been available at the time.

Use the options of the Bson Regular Expression constructor to pass in case insensitivity. I just had a look at the source code and found that 'i' is all you need. For example.

var regexFilter = Regex.Escape(filter);
var bsonRegex = new BsonRegularExpression(regexFilter, "i");

Query.Matches("MyField", bsonRegex);

You shouldn't have to keep records twice for searching.

Matt Canty
  • 2,206
  • 5
  • 33
  • 50
  • 4
    WARNING: This will fail if the filter string passed in contains regex symbols. For example, if you pass in an email address "john+smith@gmail.com", it will return 0 records back due to the "+" symbol. – Justin Nov 04 '14 at 20:55
  • I'd be happy to update this answer if anyone can recommend a way around the comment @Justin has made. I'm not working with MongoDb at the moment so it isn't practical to investigate. – Matt Canty Apr 01 '15 at 08:47
  • 10
    @bassbytesbikes Your best bet would be escape the characters in `filter` with `Regex.Escape(filter)` then pass it. – ymerej Apr 01 '15 at 20:45
  • shouldn't be it ? `var bsonRegex = new BsonRegularExpression(regexFilter, "i");` – balron Aug 25 '17 at 12:27
17

try to use something like this:

Query.Matches("FieldName", BsonRegularExpression.Create(new Regex(searchKey, RegexOptions.IgnoreCase)))
Andrei Andrushkevich
  • 9,729
  • 4
  • 28
  • 42
12

You will probably have to store the field twice, once with its real value, and again in all lowercase. You can then query the lowercased version for case-insensitive search (don't forget to also lowercase the query string).

This approach works (or is necessary) for many database systems, and it should perform better than regular expression based techniques (at least for prefix or exact matching).

Thilo
  • 241,635
  • 91
  • 474
  • 626
  • That is not a practical solution IMO. The correct answer is here. http://stackoverflow.com/a/8246621/309644 – afollestad Mar 25 '17 at 04:22
  • @afollestad Depends. Using full text search comes with its own downsides. A regular B-Tree indexed field can do nice things like prefix searches or be more easily combined with other fields for example. – Thilo Mar 25 '17 at 06:06
5

As i3arnon answered, you can use Queryable to do a case insensitive comparison/search. What i found out was, that i could not use string.Equals() method, because is it not supported. If you need to do a comparison, Contains() will unfortunately not be suitable which kept me struggling for a solution, for quite some time.

For anyone wanting to do a string comparison, simply use == instead of .Equals().

Code:

var names = namesCollection.AsQueryable().Where(name =>
    name.FirstName.ToLower() == name.ToLower());
Thomas Teilmann
  • 1,878
  • 6
  • 25
  • 52
  • 1
    Thanks alot for this answer. I tried to follow i3arnon's answer and kept running in to an error {document}{key}.ToLower() is not supported. Turned out to be as you said: Equals is not supported. – jimmy Jan 02 '17 at 09:58
  • .AsQueryable() always convert your query to Aggregate pipeline, that more slower, than basic query – ZOXEXIVO Feb 01 '17 at 19:38
3

You can also use MongoDB's built in filters. It may make it easier for using some of mongo's methods.

var filter = Builders<Model>.Filter.Where(p => p.PropertyName.ToLower().Contains(s.ToLower()));
var list = collection.Find(filter).Sort(mySort).ToList();
Daniel
  • 2,604
  • 1
  • 29
  • 41
A_Arnold
  • 2,081
  • 17
  • 36
3

For MongoDB 3.4+ the recommended way is to use indexes. See https://jira.mongodb.org/browse/DOCS-11105?focusedCommentId=1859745&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-1859745

I am successfully searching with case insensitive by: 1. Creating an index with Collation for a locale (e.g: "en") and with a strength of 1 or 2. See https://docs.mongodb.com/manual/core/index-case-insensitive/ for further details

  1. Using the same Collation when performing searches on the MongoDb collection.

As an example:

Create a collation with strength 1 or 2 for case insensitive

private readonly Collation _caseInsensitiveCollation = new Collation("en", strength: CollationStrength.Primary);

Create an index. In my case I index several fields:

private void CreateIndex()
{
    var indexOptions = new CreateIndexOptions {Collation = _caseInsensitiveCollation};
    var indexDefinition
        = Builders<MyDto>.IndexKeys.Combine(
            Builders<MyDto>.IndexKeys.Ascending(x => x.Foo),
            Builders<MyDto>.IndexKeys.Ascending(x => x.Bar));
    _myCollection.Indexes.CreateOne(indexDefinition, indexOptions);
}

When querying make sure you use the same Collation:

public IEnumerable<MyDto> GetItems()
{
    var anyFilter = GetQueryFilter();
    var anySort = sortBuilder.Descending(x => x.StartsOn);  
    var findOptions = new FindOptions {Collation = _caseInsensitiveCollation};

    var result = _salesFeeRules
        .Find(anyFilter, findOptions)
        .Sort(anySort)
        .ToList();

    return result;
}
diegosasw
  • 8,724
  • 7
  • 66
  • 106
1

The easiest way for MongoDB 3.4+ is to use one of ICU Comparison Levels

return await Collection()
.Find(filter, new FindOptions { Collation = new Collation("en", strength: CollationStrength.Primary) })
.ToListAsync();

More info https://docs.mongodb.com/manual/reference/method/cursor.collation/index.html

1

In case anyone else wondering, using fluent-mongo add-on, you can use Linq to query like that:

public User FindByEmail(Email email)
{
    return session.GetCollection<User>().AsQueryable()
           .Where(u => u.EmailAddress.ToLower() == email.Address.ToLower()).FirstOrDefault();
}

Which results in correct JS-query. Unfortunately, String.Equals() isn't supported yet.

Kostassoid
  • 1,754
  • 2
  • 19
  • 28
  • 1
    I wonder how this works internally? I was under the impression that the only ways of doing it were via a regex (which can't use the index) and duplicating the field. – UpTheCreek Sep 03 '11 at 17:03
  • Looking at the sources, It uses .toLowerCase() method, nothing special. But you've got me worried about indexes. – Kostassoid Sep 05 '11 at 05:23
0

A way to do it is to use the MongoDB.Bson.BsonJavaScript class as shown below

 store.FindAs<Property>(Query.Where(BsonJavaScript.Create(string.Format("this.City.toLowerCase().indexOf('{0}') >= 0", filter.City.ToLower()))));
Rachel Gallen
  • 25,819
  • 19
  • 69
  • 75
-1

this is exact text search and case insensitive (see this link).

{ “FieldName” : /^keywordHere$/i }
Aljohn Yamaro
  • 1,627
  • 17
  • 21