1

i am starting to build a web page to refine some search results. The code below works pretty well, and if i add ONE of the query strings (ie, ?beds=4, it returns the correct results. If, however, i specify both query strings (ie, ?beds=4&sleeps=8, it returns results matching either (all propertys with 4 beds (regardless of sleeps) and all propertys with 8 sleeps (regardless of beds), and not BOTH. i need some sort of AND statement, so that the results match beds AND sleeps?

@{
Layout = "~/_SiteLayout.cshtml";
Page.Title = "Search";

string searchText = Request.Unvalidated["searchText"];

var searchTerms = searchText.Split('"').Select((element, index) => index % 2 == 0 ? element.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) : new string[] { element }).SelectMany(element => element).ToList();

for (int i=0; i<searchTerms.Count; i++)
{
    if (searchTerms[i].ToUpper() == "THE" || searchTerms[i].ToUpper() == "AND" || searchTerms[i].ToUpper() == "AS" || searchTerms[i].ToUpper() == "AN" || searchTerms[i].ToUpper() == "BUT" || searchTerms[i].ToUpper() == "OR" || searchTerms[i].ToUpper() == "OF" || searchTerms[i].ToUpper() == "IF" || searchTerms[i].ToUpper() == "IS" || searchTerms[i].ToUpper() == "IN" || searchTerms[i].ToUpper() == "IT" || searchTerms[i].ToUpper() == "BY" || searchTerms[i].ToUpper() == "TO" || searchTerms[i].ToUpper() == "FOR" || searchTerms[i].Length <= 1 || String.IsNullOrWhiteSpace(searchTerms[i]))
        {
        searchTerms.RemoveAt(i);
        i--;  //decrements 'i' if an element is removed because all indexes after this one will drop by one. This ensures that no indexes get skipped.
        }
}



var db = Database.Open("StayInFlorida");
string searchQueryString = "";
int termCount = searchTerms.Count;
string[] searchTermsArray = searchTerms.ToArray();

searchQueryString = "SELECT * FROM PropertyInfo WHERE numBedrooms = ";

for (int i=0; i<termCount; i++)
{
    if (i != 0)
        {
        searchQueryString += "OR numBedrooms = "; //Ensures that this is not appended for the first term. Alternatively, of course, you can use "AND", depending on how you want the results returned, but you probably want "OR".
        }

searchQueryString += "@" + i + " ";
}

searchQueryString += "UNION ";
searchQueryString += "SELECT * FROM PropertyInfo WHERE numSleeps = ";

for (int i=0; i<termCount; i++)
{
    if (i != 0)
        {
        searchQueryString += "OR numSleeps = ";
        }

searchQueryString += "@" + i + " ";
}

searchQueryString += "UNION ";
searchQueryString += "SELECT * FROM PropertyInfo WHERE numBathrooms = ";

for (int i=0; i<termCount; i++)
{
    if (i != 0)
        {
        searchQueryString += "OR numBathrooms = ";
        }

searchQueryString += "@" + i + " ";
}

searchQueryString += "ORDER BY anyTermYouWishToOrderBy DESC";

if (searchTermsArray.Length > 0) //Prevents a server-side error if the searchTerm list was empty when converted to the searchTermsArray
    {
    var queryResults = db.Query(searchQueryString, searchTermsArray);
    }
}
Gavin5511
  • 791
  • 5
  • 31
  • I am not sure I fully understand what you require, but I do know that there isn't any way to involve a logical `AND` in a querystring. The approach I think you should be taking is to collect both querystring values and handle the logistics you require on the server side (with C# by the look of your code). – VoidKing Jul 24 '13 at 19:20
  • I'm happy to give a more specific answer when/if I feel like I fully understand what your required results are. – VoidKing Jul 24 '13 at 19:22
  • Hi, thanks for getting back to me VoidKing. So I essentially need to pull 2 parameters from te query string, and make them return results that match them both in my SQL select statement. Does that sound a bit clearer? – Gavin5511 Jul 25 '13 at 00:15
  • i'm guessing the 2 IF statements above need merging together somehow, but i can't figure out the right syntax – Gavin5511 Jul 25 '13 at 01:27
  • Well I just saw your additional stipulations in the comments for Mike's answer. I must say that any requirements to your needed solution should be addressed in your question so that the answers can be more suitably written the first time. That having been said, I realize that you may not have known at the time that you needed this extra functionality. I know that happens to me all the time. I am trying to think of how to best logically handle your problem. – VoidKing Jul 25 '13 at 13:36

2 Answers2

2

You have a number of alternative scenarios that need to be handled:

  1. If number of beds is specified but not number of sleeps

  2. If number of sleeps is specified but not number of beds

  3. If both are specified

  4. If neither are specified

(Here's a line of text to counter the bug in this editor that doesn't allow code to be posted directly under an ordered list.)

var selectCommand = "SELECT * FROM PropertyInfo ";
var beds = Request["beds"].AsInt();
var sleeps = Request["sleeps"].AsInt();
IEnumerable<dynamic> selectedData = null;

if(beds > 0 && sleeps == 0) { 
    selectCommand = "SELECT * FROM PropertyInfo WHERE NumBedrooms = @0";
    selectedData = db.Query(selectCommand, beds);
}

if(sleeps > 0 && beds = 0) { 
    selectCommand = "SELECT * FROM PropertyInfo WHERE NumSleeps = @0";
    selectedData = db.Query(selectCommand, sleeps)
}

if(beds > 0 && sleeps > 0){
    selectCommand = "SELECT * FROM PropertyInfo WHERE NumBedrooms = @0 AND NumSleeps = @1";
    selectedData = db.Query(selectCommand, beds, sleeps)
}

if(beds == 0 && sleeps == 0){
    //no meaningful numbers where specified
}
Mike Brind
  • 28,238
  • 6
  • 56
  • 88
  • Thanks Mike, that completely makes sense. So if I were to extend my search criteria to 3 variables (for example, number of bathrooms), would I have to add if statements that checks all possible scenarios for all 3 variables? I potentially want users to be able to do advanced searches on many things, so I can see it becoming unmanageable very quickly? Is this the same logic used by all search websites? – Gavin5511 Jul 25 '13 at 12:38
  • Hey, I finally randomly found an answer to a question I've had for a while in your answer here Mike :) Now I know that the List created from using the `var` keyword to assign SQL query results to a variable is of the `IEnumerable` data type :) Thanks, Mike! – VoidKing Jul 25 '13 at 13:33
  • @Gavin5511 If by "I potentially want users to be able to do advanced searches on many things" you mean that you don't know how many variables you will have until runtime, then you will probably have to create a list that stores all of the variables, then you can do all sorts of things with it, since you can always do `List.Count` to determine how many items you have. You can even (I just got through doing this, myself) create SQL query strings in loops that make the number of placeholders you need (i), convert the list to an array, and use that to fill in the parameters. – VoidKing Jul 25 '13 at 13:45
  • That sounds like exactly what I need to do. Do you have a link to some sample code so that I can start building out my own lists and creating these SQL loops? That's for the input, it's been so helpful – Gavin5511 Jul 25 '13 at 14:30
  • @Gavin5511 I am working on an additional answer for you now, and it may be a little bit bloated but you will see the logic therein and hopefully be able to apply it to your situation. – VoidKing Jul 25 '13 at 15:29
2

Caveat:

I hope this answer helps you approach, logically, a solution to extend the functionality to include a number of variables that you do not know until runtime, but must use to query a database. I understand that you are not searching on strings but instead numbers, or possibly other kinds of data, probably gathered from a form on a previous page, but once you get all of the data you need to the target page (using query strings may not be suitable), the logistics should be same, so we'll start there. If, in the end, you find that you are having trouble getting the unknown number of variables from the form to the target page, we can address that, also, but for now I will assume that you have gathered the necessary data on the page that queries the database.

For this example I will assume that the user types a string of text that I must split on spaces, extract the words from, and store each in a list.

Firstly, (for this example) retrieve the value in a single query string from a <form> using GET on a previous page. This, you already know how to do.

string searchText = Request.Unvalidated["searchText"];

Note that I have to retrieve this value unvalidated, because if the user were to type angle brackets in their search term, it would throw a server-side error. This is okay, though, because we are going to be careful with the untrusted data.

Next you can simply check their whole search text to make sure it isn't empty or white space like so:

if (!String.IsNullOrWhiteSpace(searchText))

Within that branch you can begin to execute all of the functionality you need to make this work.

You can replace any simple characters you want to ignore (we'll use the apostrophe and the comma).

searchText = searchText.Replace("'", "").Replace(",", "");

Now, this next line is a little complex and I'm not even sure I will explain it correctly (I'm still really new to LINQ), so, suffice it to say that it effectively splits the string on spaces (taking care of things like double-spaces and treating text wrapped in quotes as a single item) and storing them in a list (This part may be outside the scope of your question and if so, I apologize, but just in case you need to use something like this...).

var searchTerms = searchText.Split('"').Select((element, index) => index % 2 == 0 ? element.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) : new string[] { element }).SelectMany(element => element).ToList();

Now, if you so choose, you can omit terms from this list that you want to ignore (I will use some simple common words in this example, as well as, any single character items, and of course, empty, null, or white space items):

for (int i=0; i<searchTerms.Count; i++)
{
    if (searchTerms[i].ToUpper() == "THE" || searchTerms[i].ToUpper() == "AND" || searchTerms[i].ToUpper() == "AS" || searchTerms[i].ToUpper() == "AN" || searchTerms[i].ToUpper() == "BUT" || searchTerms[i].ToUpper() == "OR" || searchTerms[i].ToUpper() == "OF" || searchTerms[i].ToUpper() == "IF" || searchTerms[i].ToUpper() == "IS" || searchTerms[i].ToUpper() == "IN" || searchTerms[i].ToUpper() == "IT" || searchTerms[i].ToUpper() == "BY" || searchTerms[i].ToUpper() == "TO" || searchTerms[i].ToUpper() == "FOR" || searchTerms[i].Length <= 1 || String.IsNullOrWhiteSpace(searchTerms[i]))
    {
        searchTerms.RemoveAt(i);
        i--;  //decrements 'i' if an element is removed because all indexes after this one will drop by one. This ensures that no indexes get skipped.
    }
}

Next, declare some basic variables that you will need:

var db = Database.Open("StayInFlorida");
string searchQueryString = "";
int termCount = searchTerms.Count;
string[] searchTermsArray = searchTerms.ToArray();

Now here is where some of the logic comes into play that will help you handle compiling the sql query string when you don't know how many variables to test your columns against (for this example I will assume three database columns exist: numBedrooms, numSleeps, and numBathrooms).

searchQueryString = "SELECT * FROM PropertyInfo WHERE numBedrooms = ";

for (int i=0; i<termCount; i++)
{
    if (i != 0)
    {
        searchQueryString += "OR numBedrooms = "; //Ensures that this is not appended for the first term. Alternatively, of course, you can use "AND", depending on how you want the results returned, but you probably want "OR".
    }

    searchQueryString += "@" + i + " ";
}

searchQueryString += "UNION ";
searchQueryString += "SELECT * FROM PropertyInfo WHERE numSleeps = ";

for (int i=0; i<termCount; i++)
{
    if (i != 0)
    {
        searchQueryString += "OR numSleeps = ";
    }

    searchQueryString += "@" + i + " ";
}

searchQueryString += "UNION ";
searchQueryString += "SELECT * FROM PropertyInfo WHERE numBathrooms = ";

for (int i=0; i<termCount; i++)
{
    if (i != 0)
    {
        searchQueryString += "OR numBathrooms = ";
    }

    searchQueryString += "@" + i + " ";
}

searchQueryString += "ORDER BY anyTermYouWishToOrderBy DESC"; //You can order by whatever term you need, and, as always, can use "ASC" instead of "DESC"

Now that that part is over with all you have left is to make sure that you pass the appropriate amount of arguments to the db.Query() method, and since we already have an array copy of our list, we can just use it.

if (searchTermsArray.Length > 0) //Prevents a server-side error if the searchTerm list was empty when converted to the searchTermsArray
{
    var queryResults = db.Query(searchQueryString, searchTermsArray);

Thankfully the db.Query() method makes that simple by accepting an array of values as the second argument, which simply fills in the parameters in the query, as if you were adding multiple arguments after the first.

Lastly, to simply display the results (you probably already know this part, but for the sake of completion, I will show an example anyway)

foreach (var row in queryResults)
{
    <div>Number of Bedrooms: @row.numBedrooms</div><br/>
    <div>Number of Sleeps: @row.numSleeps</div><br/>
    <div>Number of Bathrooms: @row.numBathrooms</div><br/><hr/><br/>
}
} // <-- Don't forget to close the `if (searchTermsArray.Length > 0)` branch.
} // <-- Don't forget to close the `if (!String.IsNullOrWhiteSpace(searchText))` branch.

I know that 100% of the logic here may not exactly fit your scenario, but hopefully gives you enough that you can modify it to your needs. Also, if you have any questions, I'll be happy to help further if I can.

VoidKing
  • 6,282
  • 9
  • 49
  • 81
  • Hi voidking. thanks for such an immensely detailed explanation. i'm trying to make sense of it all, and getting a bit confused. Is all this code intended to go on the same page? and is it intended to read the query string that i send to that page, for example ?beds=1&sleeps=2@bathrooms=3 ? – Gavin5511 Jul 26 '13 at 09:31
  • @Gavin5511 Yes, it is all intended to go on the same page, and yes, in my example it is intended to read the query string that I send to that page, *however*, this is intended for one long query string that is separated into parts. This was just the example I used and I fully expect that you will have to do a bit in your case to get all of your variables to this page. If you're able to get all of your variables into a list for this page, though, much of it would be the same. One question I have: Do you know the maximum number of variables you will need to send to this page from the user-input? – VoidKing Jul 26 '13 at 13:09
  • @Gavin5511 I'm sorry that my example used a query string that was separated into multiple variables, because I'm pretty sure that is not what your scenario calls for, but I wasn't really sure if you had a form that just posts (or "get"s) a number of variables from a user-input form or if you truly didn't know, at all, how many variables you were gonna receive on the target page. My example just showed how a real possible scenario involving a truly indeterminate amount of variables may occur. – VoidKing Jul 26 '13 at 13:18
  • @Gavin5511 If, however, you have a form on a page that points to this one, and say, 30 different "possible" variables a user might (or might not) filter on, then that, too, is pretty easily attainable. On the same page that this code would go on, instead of converting the necessary pieces of the query string into parts, tearing out the commas and unnecessary words, you could instead just read all of the *possible* variables (testing them for empty or null values) and adding the non-empty values to a list. If you are working with two kinds of data-types (e.g., int and string), just convert... – VoidKing Jul 26 '13 at 13:25
  • @Gavin5511 ...the `int`s to strings and store in the same string list (later this can be re-converted with `.AsInt` or `.ToInt32` and even checked with `IsInt(stringVariable)` if you need to determine whether a value is supposed to be just a number or not). Once you have the list, you can pretty much just use my example from the line `var db = Database.Open("StayInFlorida");` down. – VoidKing Jul 26 '13 at 13:29
  • @Gavin5511 In fact, if you need a list where you can refer to these values with a "key" check out the Dictionary List (I pretty recently just learned about this, and am very psyched about it!): http://www.dotnetperls.com/dictionary OR if you want to automatically eliminate duplicate values, use a HashSet: http://www.dotnetperls.com/hashset – VoidKing Jul 26 '13 at 13:31
  • @Gavin5511 Also, if you have any questions about any of the code, just paste the code and ask me to explain it and I will be happy to do my best! – VoidKing Jul 26 '13 at 13:34
  • Thanks, you have been so amazingly helpful and knowledgable. You have given me plenty to work with here, and a want to work out for myself how to tailor it for my own application, but I will post my code if I get stuck. It'll take me a few days to digest all this, but thanks again :) – Gavin5511 Jul 26 '13 at 20:45
  • @Gavin5511 No prob, Gavin, I'm glad I could help. I know it's kind of a lot to take in all at once, but I don't think this question is going anywhere soon, so, you can always refer back to it. Hopefully I'll catch any new questions you post, but if not, feel free to ask about the code here, and I'll do my best to help! Good Luck! – VoidKing Jul 29 '13 at 13:14
  • Hi @VoidKing, i have updated the code that i'm trying to use above, but i'm still stuck. When i try and run my page, it gives me the error "The name 'queryResults' does not exist in the current context" so im not sure how to change that since i wont be splitting the string out? are you able to help? – Gavin5511 Aug 04 '13 at 10:17
  • @Gavin5511 Yes, I can help, but I have many departments out of Internet right now, so I will have to get back with you a little later. – VoidKing Aug 05 '13 at 13:14
  • @Gavin5511 `queryResults` should be the list returned from the SQL query. In my example: `var queryResults = db.Query(searchQueryString, searchTermsArray);` I would say (since I don't *think* that there is a problem with scope) that `queryResults` was never assigned. All you have to do is use `db.Query` with the appropriate arguments to make it work (the first argument being the actual SQL query string you compiled). Then later you can simply do: `foreach (var row in queryResults){//do stuff for each row found in returned sql query results, using row.colomnName to access specific data}` – VoidKing Aug 05 '13 at 14:12
  • @Gavin5511 In many online examples that I have found, they simply "cut out the middle-man" so to speak, and simply do this: `foreach (var row in db.Query(searchQueryString, var1, var2, var3)){//do stuff for each row}` which is essentially the same thing. I just like storing the list in its own variable first, in case I need to access it again later. If you *have* assigned the queryResults variable and are still getting the "the name 'queryResults' does not exist in the current context" error, then surely you're problem is that queryResults is out of scope. – VoidKing Aug 05 '13 at 14:23
  • @Gavin5511 Just checking back with you. Was curious if you were ever able to get anything working, or if you needed any more help. I don't mind adding to my answer since these comments are getting a little long. – VoidKing Aug 06 '13 at 21:58
  • hey dude! yeah, i've still had no luck, it's all gettign a bit confusing for me, but i want to finish it so bad! lol. – Gavin5511 Aug 26 '13 at 10:27
  • i just want to be able to check the query string (or storing parameters in it's own variable like you suggested). Then, i want to refine a search dependant on whether a query string is present or not. To keep things simple, let's just start with NumBedrooms, NumBathrooms and NumSleeps first. I can make it more omplicated at a later date. What is the best way of doing this, shall we carry on in this thread? it's getting a bit too long now isn't it? – Gavin5511 Aug 26 '13 at 10:32
  • @Gavin5511 Yes, I suppose it is getting a little long. :) But don't worry, there is definitely a way to do what you're asking. I am getting a little confused, though, about what is still hanging you up. Also, I would like to take this conversation to chat (since both the question and the answer are getting bulky), but I can't seem to find the link anymore. Did they remove this feature or is it just not showing up for me? Either way, if you can tell me more about what it is that is still currently stumping you, I will be happy to try and help further. – VoidKing Aug 26 '13 at 13:37