user1017882
user1017882

Reputation:

Returning Most Relevant Search Results Using LINQ Where/Take

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

Answers (5)

MikeT
MikeT

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

Mhd
Mhd

Reputation: 2978

What about this?

return locations.Where(o => o.Name.Contains(term))
                .OrderBy(m => m.Length)
                .Take(10)
                .ToList();

Upvotes: 0

Reza F.Rad
Reza F.Rad

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

NinjaNye
NinjaNye

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

Rapha&#235;l Althaus
Rapha&#235;l Althaus

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

Related Questions