AngularJS: Order search results by similarity to search text

I have built countless angular-powered, find-as-you-type-style searches over the past year or so, and I love how easy they are to implement. However, one problem I've never found a good solution for is how to properly sort those search results. I would really appreciate anyone who has ideas on how to do this.

Here's my problem:

Please see This Plunker for reference

If you look at this super-simple example plunker above, I have a list of sample pets that I want to be able to search through. If I type any text into the field, the list of pets gets narrowed down. This is all great. My problem, is that the search results aren't sorted in relation to how similar they are to the text that was entered.

For example:

There are a total of 8 pets in the list, sorted by name using the orderBy:'name' filter on the list's ng-repeat directive. If I type just the character T into the input, I get only 5 remaining results. But the pet at the top of the list is "blackie" (because blackie is the first pet in the list that has a t in it, because blackie's type is "cat", which contains a t).

Sample search results

What I would like to see is a way to sort the remaining results by their similarity to the search text. So, in the example above, instead of seeing "blackie" at the top of the list, I would see "tom" (which is at the bottom of the filtered list) because tom's name actually starts with the letter t, which is what I started searching for and is more likely to be what I am trying to find.

Any ideas on how to implement this sort of search algorithm? I haven't been able to find anything in all of my googling and all of my attempts to write such an algorithm.

Thanks in advance for the ideas!

Define your own filter to narrow down the list and for each item calculate a rank and assign the calculated rank to that item. Then order by rank.

Change HTML like this:

<pre ng-repeat="pet in pets | ranked:inputText | orderBy:'rank':true">{{pet | json}}</pre>

Add the following filter:

app.filter('ranked', function() {
return function(obj, searchKey)
{
  if (!obj)
    return obj;
  for(var i =0; i< obj.length; i++)
  {
     var currentObj = obj[i];
      var numOfName = currentObj.name.indexOf(searchKey) + 1;
      var numOfType = currentObj.type.indexOf(searchKey) + 1;
      var numOfColor = currentObj.color.indexOf(searchKey) + 1;

      currentObj.rank = numOfName * 10 + numOfType * 5 + numOfColor;
  }
  return obj;
}
});

I'm really surprised that such awesome answers came so quickly! Thanks guys!

Stealing @Aidin's answer, I decided to go with his solution but with a slightly different filter:

app.filter('rankSearch', function() {
   return function(array, searchKey, props){
      if (!array || !searchKey || !props) return array;

      for(var i =0; i<array.length; i++){
          var obj = array[i];
          obj.rankSearch = 0;
          for(var j=0; j<props.length; j++){
              var index = obj[props[j]].indexOf(searchKey);
              // i should probably spend time tweaking these arbitrary numbers
              // to find good values that produce the best results, but
              // here's what I have so far...
              obj.rankSearch += (index === -1 ? 15 : index) * ((j+1)*8);
          }
      }

      return array;
   }
});

which allows me to do this in the markup:

<pre ng-repeat="pet in pets | rankSearch:inputText:['name','type','color'] | orderBy:'rankSearch'">{{pet | json}}</pre>

One potential issue I see with this solution is that, if any object in the array already has a rankSearch property (as unlikely as that is), it would be cleared out by the filter. So I changed it from @Aidin's rank to rankSearch to try to make it a little more unique. Also, this makes higher priority items have smaller numbers, so you don't have to add the reverse flag to the end of the orderBy filter.

But this also allows you to reuse this filter for any array of objects, because you can specify which object properties you want to rank, straight from the markup.

You can view the resulting Plunker here.

Anyway, thanks to everyone for their ideas and suggestions!

An easy way to do this would be to JSON.stringify the whole object, run a regex on it and hand out points for each occurence (DEMO):

app.filter('search', function() {
  return function(items, str) {
    if(str == '') return items;

    var filtered = [];
    var rgx = new RegExp(str, 'gi');

    angular.forEach(items, function(item) {
      item.points = (JSON.stringify(item).match(rgx) || []).length;

      if(item.points > 0) filtered.push(item);
    });

    return filtered;
  }
});

And use the filter in the ng-repeat with an orderBy for points:

<pre ng-repeat="pet in pets | search : inputText | orderBy : 'points' : true">

To be honest, I don't know how good this scales and if it would be better to loop trough the properties instead of the stringify. But it works pretty good as much as I can see.

Here is a slightly modified version which hands out an additional point if a word starts with the given search string (DEMO):

//..
var rgxstart = new RegExp(': ?"'+str);

angular.forEach(items, function(item) {
  var stringified = JSON.stringify(item); 
  item.points = (stringified.match(rgx) || []).length;

  if(rgxstart.test(stringified)) item.points++;

  if(item.points > 0) filtered.push(item);
});
//...

Of course this solution is very general and works in a way that it doesn't need to know anything about the object itself which also means that it can't provide the best possible results.