Mongoose text-search with partial string
Asked Answered
S

3

23

Hi i'm using mongoose to search for persons in my collection.

/*Person model*/
{
    name: {
       first: String,
       last: String
    }
}

Now i want to search for persons with a query:

let regex = new RegExp(QUERY,'i');

Person.find({
   $or: [
      {'name.first': regex},
      {'name.last': regex}
   ]
}).exec(function(err,persons){
  console.log(persons);
});

If i search for John i get results (event if i search for Jo). But if i search for John Doe i am not getting any results obviously.

If i change QUERY to John|Doe i get results, but it returns all persons who either have John or Doe in their last-/firstname.

The next thing was to try with mongoose textsearch:

First add fields to index:

PersonSchema.index({
   name: {
      first: 'text',
      last: 'text'
   }
},{
   name: 'Personsearch index',
   weights: {
      name: {
          first : 10,
          last: 10
   }
}
});

Then modify the Person query:

Person.find({ 
    $text : { 
        $search : QUERY
    } 
},
{ score:{$meta:'textScore'} })
.sort({ score : { $meta : 'textScore' } })
.exec(function(err,persons){
    console.log(persons);
});

This works just fine! But now it is only returning persons that match with the whole first-/lastname:

-> John returns value

-> Jo returns no value

Is there a way to solve this?

Answers without external plugins are preferred but others are wished too.

Simla answered 2/2, 2016 at 11:33 Comment(9)
You might need to use Elasticsearch for this. I can provide you detailed code if you like. For getting full name search you should put query in \"query"\ format.Tmesis
Doing partial search with only mongoose doesn't work. I tried for 3 days before opting to go with elastic.Tmesis
Thanks for you hint but Elastiq seems to be a bit too much for this case. If there was a possibility to merge first and last name into one field (for one query only) and then search with regex would do it. But i'm not sure if mongoose can do that?Simla
So you need autocomplete(wildcard query like "phrase*") for both first name and last name, but want to store them separately?Jeaninejeanlouis
please take a look at: virtuals You can merge both the fields.Tmesis
yes. I have an input field client-side with autocomplete, so each time the user hits a key it should search for persons that match (first- and/or lastname)Simla
@AnubhavMishra virtuals can not be used in queriesSimla
did you try to match the words as phrases, like $search : "\"jo\" \"doe\""Feriga
That didn't work either :(Simla
T
10

You can do this with an aggregate pipeline that concatenates the first and last names together using $concat and then searches against that:

let regex = new RegExp(QUERY,'i');

Person.aggregate([
    // Project the concatenated full name along with the original doc
    {$project: {fullname: {$concat: ['$name.first', ' ', '$name.last']}, doc: '$$ROOT'}},
    {$match: {fullname: regex}}
], function(err, persons) {
    // Extract the original doc from each item
    persons = persons.map(function(item) { return item.doc; });
    console.log(persons);
});

Performance is a concern, however, as this can't use an index so it will require a full collection scan.

You can mitigate that by preceding the $project stage with a $match query that can use an index to reduce the set of docs the rest of the pipeline needs to look at.

So if you separately index name.first and name.last and then take the first word of your search string as an anchored query (e.g. /^John/i), you could prepend the following to the beginning of your pipeline:

{$match: $or: [
  {'name.first': /^John/i},
  {'name.last': /^John/i}
]}

Obviously you'd need to programmicatically generate that "first word" regex, but hopefully it gives you the idea.

Terle answered 2/2, 2016 at 13:35 Comment(1)
Will this Regex approach perform good enough on large scale ?Shushubert
L
39

regex can help you with this.

Person.find({ "name": { "$regex": "Alex", "$options": "i" } },
function(err,docs) { 
});
Legation answered 14/1, 2018 at 14:28 Comment(4)
Where did you find this documentation for this?Henandchickens
I went through some posts. I don't remember them exactly but its tried and tested. You can use it in your code.Legation
Works, but I can't find this documented anywhere but here. Thank you!!Cesaro
@Cesaro While this is a question about Mongoose, the $regex operator is a Mongo operator that Mongoose uses to query the collection. docs.mongodb.com/manual/reference/operator/query/regexWagonage
T
10

You can do this with an aggregate pipeline that concatenates the first and last names together using $concat and then searches against that:

let regex = new RegExp(QUERY,'i');

Person.aggregate([
    // Project the concatenated full name along with the original doc
    {$project: {fullname: {$concat: ['$name.first', ' ', '$name.last']}, doc: '$$ROOT'}},
    {$match: {fullname: regex}}
], function(err, persons) {
    // Extract the original doc from each item
    persons = persons.map(function(item) { return item.doc; });
    console.log(persons);
});

Performance is a concern, however, as this can't use an index so it will require a full collection scan.

You can mitigate that by preceding the $project stage with a $match query that can use an index to reduce the set of docs the rest of the pipeline needs to look at.

So if you separately index name.first and name.last and then take the first word of your search string as an anchored query (e.g. /^John/i), you could prepend the following to the beginning of your pipeline:

{$match: $or: [
  {'name.first': /^John/i},
  {'name.last': /^John/i}
]}

Obviously you'd need to programmicatically generate that "first word" regex, but hopefully it gives you the idea.

Terle answered 2/2, 2016 at 13:35 Comment(1)
Will this Regex approach perform good enough on large scale ?Shushubert
T
0

a). Partial Text Search in a single field in collection :

If we want to search in single field in collection we can use that code in aggregate

{
  $match: {
    name: {
      $regex: “String seraching”,
      ‘$options’: ‘i’
      }
   }
}

b). Partial Text Search by multiple fields in collection :

If we want to search by more than one field (Multiple fields)in a particular collection then we can use that code in the aggregate query

{
  $match: {
    $or: [
     { name: {
       $regex: “String to be searched”,
       ‘$options’: ‘i’
     }},
     { email: {
       $regex: String to be searched,
       ‘$options’: ‘i’
     }}
    ]
}

},

Trice answered 14/9, 2021 at 18:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.