简体   繁体   中英

EF filtering/searching with multiple words

I have a simple custom table with a search/filter field. I leave the implementation of the search up to each use of the table.

So let's say I have users in my table and I want to search for them. I want to search both in users firstname, lastname and also any role they are in.

This would probably do the trick

            searchString = searchString.ToLower();

            query = query.Where(
                x =>
                    x.FirstName.ToLower().Contains(searchString)
                    ||
                    x.LastName.ToLower().Contains(searchString)
                    ||
                    x.Roles.Any(
                        role =>
                            role.Name.ToLower().Contains(searchString)
                        )
                );

But now I want to search/filter on multiple words. First I get an array of all separate words.

            var searchStrings = searchString.ToLower().Split(null);

I tried the following but it does not fulfill my requirements listed further down as it returns any user where any word is matched in any field. I need that all words are matched (but possibly in different fields). Se below for more details.

            query = query.Where(
                x =>
                    searchStrings.Any(word => x.FirstName.ToLower().Contains(word))
                    ||
                    searchStrings.Any(word => x.LastName.ToLower().Contains(word))
                    //snipped away roles search for brevity
                );

First let me produce some data

Users (data)

Billy-James Carter is admin and manager

James Carter is manager

Billy Carter has no role

Cases

If my search string is "billy car" I want Billy-James and Billy returned but not James Carter (so all words must match but not on same field).

If my search string is "bil jam" or even "bil jam car" I only want Billy-James returned as he is the only one matching all terms/words. So in this the words bil and jam were both found in the FirstName field while the car term was found in the LastName field. Only getting the "car" part correct is not enough and James is not returned.

If I search for "car man" Billy-James and James are both managers (man) and named Carter and should show up. should I search for "car man admi" then only Billy-James should show up.

I am happy to abandon my current approach if better is suggested.

I cannot think of a way to wrap what you're looking for up into a single LINQ statement. There may be a way, but I know with EF the options are more limited than LINQ on an object collection. With that said, why not grab a result set from the database with the first word in the split, then filter the resulting collection further?

var searchWords = searchString.ToLower().split(' ');

var results = dataBase.Where(i => i.FirstName.ToLower().Contains(searchWords[0])
                  || i.LastName.ToLower().Contains(searchWords[0])
                  || i.Role.ToLower().Contains(searchWords[0]));

if(searchWords.Length > 1) {
    for(int x = 1; x < searchWords.Length; x++) {
        results = results.Where(i => i.FirstName.ToLower().Contains(searchWords[x])
                  || i.LastName.ToLower().Contains(searchWords[x])
                  || i.Role.ToLower().Contains(searchWords[x]));
    }
}

Your final content of the results collection will be what you're looking for.

Disclaimer: I didn't have a setup at the ready to test this, so there may be something like a .ToList() needed to make this work, but it's basically functional.

Update: More information about EF and deferred execution, and string collection search

Given we have the schema:

Employee:
    FirstName - String
    Last Name - String
    Roles - One to Many
        Role:
            Name - String

The following will build a query for everything you want to find

var searchTerms = SearchString.ToLower().Split(null);

var term = searchTerms[0];
var results = from e in entities.Employees
              where (e.FirstName.Contains(term) 
                  || e.LastName.Contains(term) 
                  || e.Roles.Select(r => r.Name).Any(n => n.Contains(term)))
              select e;

if (searchTerms.Length > 1)
{
    for (int i = 1; i < searchTerms.Length; i++)
    {
        var tempTerm = searchTerms[i];
        results = from e in results
                  where (e.FirstName.Contains(tempTerm) 
                      || e.LastName.Contains(tempTerm) 
                      || e.Roles.Select(r => r.Name).Any(n => n.Contains(tempTerm)))
                  select e;
    }
}

At this point the query still has not been executed. As you filter the result set in the loop, this is actually adding additional AND clauses to the search criteria. The query doesn't execute until you run a command that does something with the result set like ToList(), iterating over the collection, etc. Put a break point after everything that builds the query and take a look at it. LINQ to SQL is both interesting and powerful.

More on deferred execution

The one thing which needs explanation is the variable tempTerm . We need a variable which is scoped within the loop so that we don't end up with one value for all the parameters in the query referencing the variable term .

I simplified it a bit

        //we want to search/filter
        if (!string.IsNullOrEmpty(request.SearchText))
        {
            var searchTerms = request.SearchText.ToLower().Split(null);

            foreach (var term in searchTerms)
            {
                string tmpTerm = term;
                query = query.Where(
                    x =>
                        x.Name.ToLower().Contains(tmpTerm)
                    );
            }

        }

I build a much bigger query where searching is just a part, starting like this

        var query = _context.RentSpaces.Where(x => x.Property.PropertyId == request.PropertyId).AsQueryable();

above search only uses one field but should work just fine with more complex fields. like in my user example.

I usually take the apporach to sort of queue the queries. They are all executed in one step at the database if you look with the diagnostic tools:

IQueryable<YourEntity> entityQuery = context.YourEntity.AsQueryable();
foreach (string term in serchTerms)
{
  entityQuery = entityQuery.Where(a => a.YourProperty.Contains(term));
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM