43

I'm trying to implement a very basic keyword search in an application using linq-to-sql. My search terms are in an array of strings, each array item being one word, and I would like to find the rows that contain the search terms. I don't mind if they contain more than just the search terms (most likely, they will), but all the search terms do have to be present.

Ideally, I would like something similar to the snippet below, but I know that this won't work. Also, I have looked at this question here, but the author of that question seems content to do things the other way round ( query.Contains(part.partName) ), which doesn't work for me.

public IQueryable<Part> SearchForParts(string[] query)
{
    return from part in db.Parts
           where part.partName.Contains(query)
           select part;
}

How can I rewrite this query so that it will do what I need?

Community
  • 1
  • 1
a_m0d
  • 12,034
  • 15
  • 57
  • 79
  • Try to think what you ask trying to do. It does not make sense really. How can you match an array with a string? You need to tell us what partName will look like at least. – leppie Mar 03 '10 at 06:17

7 Answers7

45

Looking at the other attempts saddens me :(

public IQueryable<Part> SearchForParts(string[] query)
{
  var q = db.Parts.AsQueryable(); 

  foreach (var qs in query)
  { 
    var likestr = string.Format("%{0}%", qs);
    q = q.Where(x => SqlMethods.Like(x.partName, likestr));
  }

  return q;
}

Assumptions:

  • partName looks like: "ABC 123 XYZ"

  • query is { "ABC", "123", "XY" }

leppie
  • 115,091
  • 17
  • 196
  • 297
  • This is equivalent to my answer, except you are not escaping the wild card characters ('_', '%', '['). – Trisped Feb 29 '12 at 03:59
23

A simpler and more correct solution (then leppie's):

public IQueryable<Part> SearchForParts(string[] query)
{
    var q = db.Parts.AsQueryable(); 

    foreach (string qs in query)
    {
        q = q.Where(x => x.partName.Contains(qs));
    }

    return q;
}

This will work as long as partName is a string (or an SQL equivalent of a string).

The important thing to note is partName.Contains(qs) is different than query.Contains(partName).
With partName.Contains(qs), partName is searched for any occurrence of qs. The resulting SQL would be equivalent (where <qs> is the value of qs):

select * from Parts where partName like '%<qs>%';

Also of note are StartsWith and EndsWith which are similar to Contains but look for the string in the specific location.

query.Contains(partName) is the same as a SQL in command. The resulting SQL would be equivalent to (where <query0> is the value of query[0], <query1> is the value of query[1], and <queryN> is the last value in the query array):

select * from Parts where partName in ( <query0>, <query1>, ..., <queryN> );

Update:
It is also important to note that leppie's answer does not escape the wildcard characters before adding them to the like statement. This is not an issue with the Contains solution since Linq will escape the query before sending it. An escaped version of the SqlMethods.Like solution would be:

public IQueryable<Part> SearchForParts(string[] query)
{
    var q = db.Parts.AsQueryable(); 

    foreach (var qs in query)
    {
        string escaped_bs = qs.Replace("/", "//"),
            escaped_us = escaped_bs.Replace("_", "/_"),
            escaped_p = escaped_us.Replace("%", "/%"),
            escaped_br = escaped_p.Replace("[", "/["),
            likestr = string.Format("%{0}%", escaped_br);

        q = q.Where(x => SqlMethods.Like(x.partName, likestr, '/'));
    }

    return q;
}

You don't have to worry about ' since Linq will escape that for you.

Trisped
  • 5,705
  • 2
  • 45
  • 58
  • You assign escaped_p but never use it. Typo? – Patrick M Oct 28 '13 at 22:05
  • @PatrickM Yeah, I forgot to change some of the code after copy/paste. It is fixed now. – Trisped Jan 06 '14 at 22:44
  • In my testing I was returning an `IQueryable` that I then applied the `Contains` to, so I will have to see if that impacted the code (i.e. turned it into LINQ to entities accidentally). I will delete my previous comment for now. – iCollect.it Ltd Mar 11 '15 at 09:38
  • @TrueBlueAussie I have had issues where getting the order wrong on the LINQ statment would cause the query to be run on the local machine. For example, if you add an order by clause and then add a where filter it seemed to always bring down the ordered data, then apply your where filter. If the contains is converted to SQL then it will use `LIKE` and '%``%'. If you cannot guarantee the LINQ will be run in SQL, then only your solution with `ToLower` will work. – Trisped Mar 11 '15 at 19:24
1

Using the NinjaNye.SearchExtension nuget package allows you to perform this search with ease:

string[] terms = new[]{"search", "term", "collection"};
var result = db.Parts.Search(terms, p => p.PartName);

You could also search multiple string properties

var result = db.Parts.Search(terms, p => p.PartName, p.PartDescription);

Or perform a RankedSearch which returns IQueryable<IRanked<T>> which simply includes a property which shows how many times the search terms appeared:

//Perform search and rank results by the most hits
var result = db.Parts.RankedSearch(terms, p => p.PartName, p.PartDescription)
                     .OrderByDescending(r = r.Hits);

There is a more extensive guide on the projects GitHub page: https://github.com/ninjanye/SearchExtensions

Hope this helps future visitors

NinjaNye
  • 7,046
  • 1
  • 32
  • 46
1

You could try:

public IQueryable<Part> SearchForParts(string[] query)
{
    return from part in db.Parts
           where query.All(term => part.partName.Contains(term))
           select part;
}

However, I'm not sure if LINQ to SQL will be able to transform it into T-SQL. Another option would be:

public IQueryable<Part> SearchForParts(string[] query)
{
    var result = from part in db.Parts
                 select part;

    foreach(var term in query)
    {
        result = from part in result
                 where part.partName.Contains(term)
                 select part;
    }

    return result;
}

It's not as pretty, but it should work. You'll get a query with a lot of ANDs in the where clause.

Rory
  • 2,722
  • 1
  • 20
  • 11
  • Doing this results in the error _"Local sequence cannot be used in LINQ to SQL implementation of query operators except the Contains() operator."_ – a_m0d Mar 03 '10 at 06:00
  • @casperOne: The expression passed to `All` uses the `Contains` method, not the equals operator, meaning the part name would be tested to see if it **contains** the query term instead of being exactly equal to it. As for using `Any`, the question states that the results must match all of the search terms, not just one of them (which is what any would do). – Rory Mar 03 '10 at 06:07
  • @a_m0d: I did say I wasn't sure if it would transform. Try the second suggestion, you should have better results. – Rory Mar 03 '10 at 06:08
  • To be honest, I don't really care too much whether they are all there or not, but I feel that the search would be much better if they are all there, and probably easier to implement as well. – a_m0d Mar 03 '10 at 06:12
  • Yeah, the second suggestion does work, but I don't really like the idea of loading it all into memory like that and then searching through it. Ideally, I would let the database search for me, but maybe that is just premature optimisation on my part. – a_m0d Mar 03 '10 at 06:19
  • @a_m0d: Well if that's the case, you should put it in your question. The way it's currently worded makes it seem like its a requirement. That aside, try running the profiler and checking the query that gets sent to the DB: it shouldn't be loading everything into memory. One of the benefits of deferred execution is that the query is only executed when the results are enumerated (which should only happen in your calling method). – Rory Mar 03 '10 at 06:24
1

You can write it as this

var result = db.Parts.Where(p => query.All(q => p.partName.Contains(q)));
Fadrian Sudaman
  • 6,405
  • 21
  • 29
  • Doing this results in the error _"Local sequence cannot be used in LINQ to SQL implementation of query operators except the Contains() operator."_ - same as below. – a_m0d Mar 03 '10 at 06:11
  • This works fine for me in a similar query, MVC 2/.NET 4. The accepted answer for the question uses SqlMethods which I can't find the namespace for in my project - deprecated? – Andy Copley Jan 26 '11 at 10:29
  • SqlMethods is in System.Data.Linq.SqlClient namespace. Use SqlMethods only in Linq to SQL implementation and you will need reference to System.Data.Linq assembly – Fadrian Sudaman Feb 21 '11 at 10:35
0

I feel this is somewhat simple and working for me:

string[] product = products.Split(','); 
using (var context = new ProjectTrackerEntities()) 
{ var result = from part in context.DBAudits where product.Contains(part.TableName) select part; }
Bo Persson
  • 90,663
  • 31
  • 146
  • 203
murali
  • 11
  • 2
-3

please try this:

public IQueryable<Part> SearchForParts(string[] query)
{
    return from part in db.Parts
           where Search(part.Name,query)
           select part;
}

public bool Search(string partName,string[] query)
{
    for (int i = 0; i < query.Length; i++)
    {
        if(partName.Contains(query[i]))
           return true;
    }

    return false;
}
masoud ramezani
  • 22,228
  • 29
  • 98
  • 151
  • 2
    Your Search method isn't available to SQL - unless you fetched EVERY part by adding, say, .ToList() after db.Parts it's not going to work – Ian Mercer Mar 03 '10 at 08:00