Reputation:
I have a List<Locations>
that will be filtered to yield a set of results relevant to a search term.
At the moment, I tried these 'search results' by filtering with the following:
return locations.Where(o => o.Name.Contains(term)).Take(10).ToList();
Problem
If I were to enter 'Chester' as the search term, I will never see the item "Chester" despite it existing in the locations
list. The reason for this is that there are 10 or more other items in the list that contain the String "Chester" in their name (Manchester, Dorchester etc.).
How can I use LINQ to first of all take the results that start with the search term?
What I've Got So Far
var startsWithList = list.Where(o => o.Name.StartsWith(term)).Take(10).ToList();
var containsList = list.Where(o => o.Name.StartsWith(term) && !startsWithList.Contains(o)).Take(10 - startsWithList.Count).ToList();
return startsWithList.AddRange(containsList);
I don't like the above code at all. I feel like this should be achieved in one Where
as opposed to performing two Where and Take's and combining the two lists.
Upvotes: 2
Views: 3869
Reputation: 5500
Raphaël's Solution will work but if you were say searching for Warwick you could find that it might not put Warwick the top of the list if Warwickshire is also a possible location,using the scores you can also extend this infinitely with more matching methods, as well as tweaking the score values to refine your search order
return locations.Select(l => New {SearchResult=l,
Score=(L.Name == Term ?
100 :
l.Name.StartsWith(term) ?
10 :
l.Name.Contains(term) ?
1 :
0
)})
.OrderByDescending(r=>r.Score)
.Take(10)
.Select(r => r.SearchResult);
note i would probably do this by making a Match method and do the logic in there rather than in the linq like i did above so it would just be
return locations.OrderByDescending(Match).Take(10);
Upvotes: 2
Reputation: 2978
What about this?
return locations.Where(o => o.Name.Contains(term))
.OrderBy(m => m.Length)
.Take(10)
.ToList();
Upvotes: 0
Reputation: 230
All Solutions will work but the better score can gain more easier as below
return locations.Where(o => o.Name.Contains(term))
.OrderBy(m => m.Name.IndexOf(term))
.Take(10)
.ToList();
as a result each name that contain term at nearest to start, show first.
Upvotes: 1
Reputation: 7126
I have created a new github project that uses expression trees to search for text in any number of properties
It also has a RankedSearch()
method which returns the matching items with the number of hits for each record meaning you can do the following:
return locations.RankedSearch(term, l => l.Name)
.OrderByDescending(x => x.Hits)
.Take(10)
.ToList();
If you wanted you could search accross multiple properties
return locations.RankedSearch(term, l => l.Name, l => l.City)
... or for multiple terms,
return locations.RankedSearch(l => l.Name, term, "AnotherTerm" )
... or for both multiple properties and multiple terms
return locations.RankedSearch(new[]{term, "AnotherTerm"},
l => l.Name,
l => l.City)
Checkout this post for more information on the SQL generated and other usages: http://jnye.co/Posts/27/searchextensions-multiple-property-search-support-with-ranking-in-c-sharp
You can download this as a nuget package to: https://www.nuget.org/packages/NinjaNye.SearchExtensions/
Upvotes: 4
Reputation: 60503
just order ascending before Take, putting a lower value for items starting with term.
return locations.Where(o => o.Name.Contains(term))
.OrderBy(m => m.Name.StartsWith(term) ? 0 : 1)
//or OrderByDescending(m => m.Name.StartsWith(term))
.Take(10)
.ToList();
adapted with the improvement of MikeT (exact match before StartsWith), you could just do
return locations.Where(o => o.Name.Contains(term))
.OrderBy(m => m.Name.StartsWith(term)
? (m.Name == term ? 0 : 1)
: 2)
.Take(10)
.ToList();
Upvotes: 8