MySql Search Query for Two Combined Fields - php

I am working on a site that allows users to list boats and yachts for sale. There is a mysql database that has a table "yachts" and among other fields ther are "make" and "model".
When people come to the site to look for boats for sale there is a search form, one of the options is to enter the make and/or model into a text field. The relevant where clause on the results page is the following
WHERE ( make LIKE '%$yacht_make%' OR model LIKE '%$yacht_make%')
This is working if someone enters either the make or model but not if they enter both.
For example, if someone enters "Jeanneau", the make, it finds the boat with that make, or if they enter "Sun Odyssey", the model, it finds the boat of that model, but if they enter "Jeanneau Sun Odyssey" it comes up empty.
Is there is a way to write a query where all three ways of entering the above search criteria would find the boat?
Here is the site http://yachtsoffered.com/
Thanks,
Rob Fenwick
Edit:
The query is built with a php script here is the script
if(!empty($yacht_id)) {
$where = " WHERE yacht_id = $yacht_id ";
} else {
$where = " WHERE ( make LIKE '%$yacht_make%' OR model LIKE '%$yacht_make%') ";
if(!empty($year_from) && !empty($year_to)){
$where .= "AND ( year BETWEEN $year_from AND $year_to ) ";
}
if(!empty($length_from) && !empty($length_to)){
$where .= "AND ( length_ft BETWEEN $length_from AND $length_to ) ";
}
if(!empty($price_from) && !empty($price_to)){
$where .= "AND ( price BETWEEN $price_from AND $price_to ) ";
}
if ($sail_power != 2){
$where .= "AND ( sail_power = $sail_power ) ";
}
if (count($material_arr) > 0){
$material = 'AND (';
foreach ($material_arr as $value) {
$material .= ' material LIKE \'%' . $value . '%\' OR';
}
$material = substr_replace ( $material , ') ' , -2 );
$where .= $material;
}
if (count($engine_arr) > 0){
$engine = 'AND (';
foreach ($engine_arr as $value) {
$engine .= ' engine LIKE \'%' . $value . '%\' OR';
}
$engine = substr_replace ( $engine , ') ' , -2 );
$where .= $engine;
}
if (count($type_arr) > 0){
$type = 'AND (';
foreach ($type_arr as $value) {
$type .= ' type LIKE \'' . $value . '\' OR';
}
$type = substr_replace ( $type , ') ' , -2 );
$where .= $type;
}
if (count($region_arr) > 0){
$region = 'AND (';
foreach ($region_arr as $value) {
$region .= ' region LIKE \'' . $value . '\' OR';
}
$region = substr_replace ( $region , ') ' , -2 );
$where .= $region;
}
$where .= 'AND ( active = 1 ) ORDER BY yacht_id DESC';
}
$sql = "SELECT * FROM $tbl_name $where LIMIT $start, $limit";
$result = mysql_query($sql);

There are many ways to do it, the easiest one in my opinion is:
$search = preg_replace('/\s+/','|', $yacht_make);
$sql = "select * from yacht where concat_ws(' ',make,model) rlike '$search'";
This replaces all whitespace with |, that is used as OR in regexp-powered-like query on concatenation of all searchable fields. The speed of it may be questionable in heavy trafic sites but is quite compact and easy to add more fields.

Your problem is that in your query you are searching for the exact substring "Jeanneau Sun Odyssey", which is neither a make nor a model.
The easiest solution is to use to separate input boxes for make and model. But if you really need to use a single input box, your best bet would be to split on spaces and add clauses for each separate word, so your query will end up looking something like
WHERE make like '%sun%' OR model like '%sun%'
OR make like '%odyssey%' OR model like '%odyssey%'
OR make like '%Jeanneau%' OR model like '%Jeanneau%'

Thanks everyone I ended up using dev-null-dweller's solution above
I revised my code as following,
to get the value and escape here is the code,
if (isset($_GET['yacht_make'])) {
$yacht_make = cleanString($_GET['yacht_make']);
$yacht_make = preg_replace('/\s+/','|', $yacht_make);
} else {
$yacht_make = NULL;
}
And I revised the first few lines of the php query building code I posted above to read,
if(!empty($yacht_id)) {
$where = " WHERE yacht_id = $yacht_id ";
} else {
$where = " WHERE ( yacht_id LIKE '%' )";
if(!empty($yacht_make)){
$where .= "AND ( CONCAT_WS(' ',make,model) RLIKE '$yacht_make') ";
}
It is working nicely, although it brings up more results than I would like for "Jeanneau Sun Odyssey" as it brings up "Bayliner Sunbridge", I assume because it is matching "Sun".
But it is a big improvement from what I had.
Thanks All
Rob Fenwick

You could also use:
WHERE make LIKE '%$yacht_make%'
OR model LIKE '%$yacht_make%'
OR '$yacht_make' LIKE CONCAT('%', make, '%', model, '%')
OR '$yacht_make' LIKE CONCAT('%', model, '%', make, '%')
It will not be very efficient and still not catch all possibilities, e.g. if the user provide the make and a part of the model name, like 'Jeanneau Odyssey' or in wrong order: Sun Jeanneau Odyssey.

After checking out your site it appears you are only taking input from one text field and searching for that string in each field.
So, you can either use full text searches (linkie) or you could split your string by spaces and generate a WHERE cause on the fly. Here is a rough example:
$searchterms = explode (' ', $input);
$sql .= "... WHERE"; /* obviously need the rest of your query */
foreach ($searchterms as $term) {
if ((substr ($string, -5) != 'WHERE') &&
(substr ($string, -3) == ' ||') { $sql .= " ||"; }
$sql .= " make LIKE '%$term%' || model LIKE '%$term%'";
}

Related

Generated MySQL 'AND' select behaving like 'OR' select

I'm trying to build a MySQL search to match keywords occurring in any order in the column being searched (not just whole phrases as would normally be the case). My class function is:
public function keywords_author($keywords, $author) {
$keywords = explode(" ", trim($keywords));
$keywords = array_filter($keywords);
$count_keywords = count($keywords);
if ($count_keywords != 0) {
$query = "SELECT * FROM `table` WHERE ";
$query_echo = $query;
$a = 0;
while ($a < $count_keywords) {
$query .= "`column` LIKE :keyword ";
$query_echo .= "`column` LIKE '%" . $keywords[$a] . "%' ";
$a++;
if ($a < $count_keywords) {
$query .= " && ";
$query_echo .= " && ";
}
}
$stmt = $this->db->prepare($query);
for ($a=0; $a<$count_keywords; $a++) {
$keyword = "%" . $keywords[$a] . "%";
$stmt->bindParam(':keyword', $keyword);
}
$stmt->execute();
$output = '';
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// build $output
}
echo $output;
echo $query_echo;
}
}
I have just added $query_echo to check the query being built, which is:
SELECT * FROM `table`
WHERE `column` LIKE '%php%'
&& title LIKE '%mysql%'
&& title LIKE '%jquery%'
&& title LIKE '%ajax%'
This works fine when I copy that into the SQL command line in phpMyAdmin, returning only those records where ALL keywords are matched, but when I run the class file in my site it behaves like an OR select and returns results where ANY of the keywords occurs in the column.
I'm confused! Any ideas what's going on would be a huge help!
David -
Thanks, David Kmenta - that's certainly a step in the right direction and now I'm getting the correct query:
SELECT * FROM table WHERE column LIKE :keyword0 AND column LIKE :keyword1 AND column LIKE :keyword2 AND column LIKE :keyword3 AND column LIKE :keyword4
But it is still returning the result for the last value only. I am sure it is a basic, probably obvious error in the loop enclosing the new bindParam statement:
for ($a=0; $a<$count_keywords; $a++) {
$keyword = "%" . $keywords[$a] . "%";
$stmt->bindParam(':keyword'.$a, $keyword);
}
I'm very tired - can you spot the problem?
But
Problem is probably here:
for ($a=0; $a<$count_keywords; $a++) {
$keyword = "%" . $keywords[$a] . "%";
$stmt->bindParam(':keyword', $keyword);
}
Every occurrence of :keyword is replaced with last item in $keywords array.

Search by criteria not returning any records

I am trying to check if the POST or the GET has my search variables and then add the variables to my query. I then want to pass the array name of those variables into the URL for paginating my search results. With someone's help, this is how far I have gone.
$criteria = array('ctitle', 'csubject', 'creference', 'cat_id', 'cmaterial', 'ctechnic', 'cartist', 'csource', 'stolen');
$likes = "";
$url_criteria = '';
foreach ( $criteria AS $criterion ) {
if ( ! empty($_POST[$criterion]) ) {
$value = ($_POST[$criterion]);
$likes .= " AND `$criterion` = '%$value%'";
$url_criteria .= '&'.$criterion.'='.htmlentities($_POST[$criterion]);
} elseif ( ! empty($_GET[$criterion]) ) {
$value = mysql_real_escape_string($_GET[$criterion]);
$likes .= " AND `$criterion` = '%$value%'";
$url_criteria .= '&'.$criterion.'='.htmlentities($_GET[$criterion]);
}
}
$sql = "SELECT * FROM collections WHERE c_id>0" . $likes . " ORDER BY c_id ASC";
echo $sql;
The problem I have here is that after modifying the query I had before, any criteria I use to search does not return any records even when those records exist. I also echoed thequery and it printed the following line:
SELECT * FROM collections WHERE c_id>0 AND `cmaterial` = '%wood%' ORDER BY c_id ASC
Please, what am I missing here?
You should use LIKE keyword instead of = when concatenating parts of criteria. Your condition means searching for exact match including % symbols, while LIKE means searching by pattern.
$likes .= " AND `$criterion` LIKE '%$value%'";

PHP SQL: Way to skip over section of a query if variable is blank

I'm writing a query that uses input from a search form where Brand, Type and Price are optional input fields:
SELECT * FROM `database` WHERE `brand` LIKE "%' . $brand . '%" AND `type` LIKE "%' . $type. '%" AND `price` LIKE "%' . $price . '%"
I am wondering if there is a way to say 'all' if nothing is entered into one of the fields. For example if they do not enter a value in the price field is there a way to tell SQL to just say ignore that section, eg:
AND `price` LIKE "*";
So the reuslts are still filtered by Brand and Type but can have any Price.
Any advice on this is appreciated! Thanks
As Ariel mentioned, it would be better to have PHP do the filtering as you build the query. Here's a code sample for doing it that way:
<?php
$sql = 'SELECT * FROM `database`';
$where = array();
if ($brand !== '') $where[] = '`brand` LIKE "%'.$brand.'%"';
if ($type !== '') $where[] = '`type` LIKE "%'.$type.'%"';
if ($price !== '') $where[] = '`price` LIKE "%'.$price.'%"';
if (count($where) > 0) {
$sql .= ' WHERE '.implode(' AND ', $where);
} else {
// Error out; must specify at least one!
}
// Run $sql
NOTE: Please, please, please make sure that the $brand, $type, and $price variable contents are sanitized before you use them this way or you make yourself vulnerable to SQL injection attacks (ideally you should be using the PHP PDO database connector with prepared statements to sanitize the input).
Normally you do that in the front end language, not SQL.
But price LIKE '%' does, in fact, mean all (except for NULLs). So you are probably fine.
If you have your form fields organized, you can do something like:
<?php
$fields = array(
// Form // SQL
'brand' => 'brand',
'type' => 'type',
'price' => 'price',
);
$sql = 'SELECT * FROM `database`';
$comb = ' WHERE ';
foreach($fields as $form => $sqlfield)
{
if (!isset($_POST[$form]))
continue;
if (empty($_POST[$form]))
continue;
// You can complicate your $fields structure and e.g. use an array
// with both sql field name and "acceptable regexp" to check input
// ...
// This uses the obsolete form for mysql_*
$sql .= $comb . $sqlfield . ' LIKE "%'
. mysql_real_escape_string($_POST[$form])
. '"';
/* To use PDO, you would do something like
$sql .= $comb . $sqlfield . 'LIKE ?';
$par[] = $_POST[$form];
*/
$comb = ' AND ';
}
// Other SQL to go here
$sql .= " ORDER BY brand;";
/* In PDO, after preparing query, you would bind parameters
- $par[0] is value for parameter 1 and so on.
foreach($par as $n => $value)
bindParam($n+1, '%'.$value.'%');
*/

select table filter by querystring php

ok so I've been trying for a while now to get this to work but there has to be a better solution than what im thinking about. I'm fairly new to php/mysql so not sure how to do the following:
I have a search box that contains dropdowns for country, state, city
Now if the user only selects country and clicks on search it needs to filter the select by just country and show everything else.
if(!empty($_REQUEST['city']))
$city = $_REQUEST['city'];
else
$city= "%";
if(!empty($_REQUEST['state']))
$state= $_REQUEST['state'];
else
$state= "%";
if(!empty($_REQUEST['country']))
$country= $_REQUEST['country'];
select * from table where country = $country and state = $state and city = $city
problem with this is that those columns are ints so I can't use the "%" to filter it. I hope I was able to explain it any help is more than welcome. Thanks in advance
If you don't want to constrain a column, simply omit it from your query
never insert a string from $_REQUEST directly into a query string -- classic SQL injection flaw.
you probably want to enforce some sort of limit, lest the query return every single result in your database.
example:
<?php
$conditions = array();
if(!empty($_REQUEST['city']))
$conditions[] = "city = " . mysql_real_escape_string($_REQUEST['city']);
if(!empty($_REQUEST['state']))
$conditions[] = "state = " . mysql_real_escape_string($_REQUEST['state']);
if(!empty($_REQUEST['country']))
$conditions[] = "country = " . mysql_real_escape_string($_REQUEST['country']);
$sql = 'select * from table ';
if(!empty($conditions))
$sql .= ' where '. implode(' AND ', $conditions);
$sql .= ' LIMIT 1000';
$where = array();
if(!empty($_REQUEST['city'])) $where[] = "city = '".(int)$_REQUEST['city']."'";
if(!empty($_REQUEST['state'])) $where[] = "state = '".(int)$_REQUEST['state']."'";
if(!empty($_REQUEST['country'])) $where[] = "country = '".(int)$_REQUEST['country']."'";
$wherestring = if(count($where) != 0) ? " WHERE ".implode(' AND ', $where) : "" ;
$query = "SELECT * FROM table".$wherestring;
You may want to consider writing several query strings, one for just country, one for state and country and one for city, state and country. Alternatively you can assemble the query string based upon the different parameters you have to work with.
Example:
if(isset() || isset() || isset() ) //make sure at least one is set
{
$query_string = "SELECT * FROM table WHERE ";
if(isset($_REQUEST['country']))
{
$country = $_REQUEST['country'];
$query_string .= " country = $country";
}
if(isset($_REQUEST['state']))
{
$state = $_REQUEST['state'];
$query_string .= " state = $state";
}
if(isset($_REQUEST['city']))
{
$city = $_REQUEST['city'];
$query_string .= " city = $city";
}
}
else
{
//Else, if none are set, just select all the entries if no specifications were made
$query_string = "SELECT * FROM table";
}
//Then run your query...
So in english, the first thing you do is check your parameters, making sure you have something to work with before you try and concatenate empty variables together.
Then you make the base query string (as long as we have parameters) and leave it open ended so that we can add whatever parameters you need.
Next check each parameter, and if it is set, then concatenate that parameter onto the end of the query string.
Finally process the query by sending it to the SQL server.
Good luck!
h
Here're my suggestions.
I'm giving you an answer, even though you have three already. I'm thinking mine may be easier on the code-eyes.
Do not use the raw $_REQUEST value, as it's likely that the user can poison your database by feeding it fake $_REQUEST data. Though there may be better ways to do it, keep in mind the command "mysql_real_escape_string($string)".
A common method I've seen for solving this problem is written below. (The implode idea, basically. Frank Farmer does it as well in his.)
-
$__searchWheres = array(); //Where we'll store each requirement used later
foreach( array('city','state','country') as $_searchOption) {
if ( ! empty( $_REQUEST[$_searchOption] ) ) {
$__searchWheres[] = $_searchOption . '= "' . mysql_real_escape_string( $_REQUEST[$_searchOption] ) . '"';
}
}
$__query = 'select * from table' . (count($__searchWheres) > 0 ? ' WHERE ' . implode(' AND ',$__searchWheres) : ''); //Implode idea also used by Frank Farmer
//Select from the table, but only add the 'WHERE' key and where data if we have it.
mysql_query($__query);

Building a long query and have a lot of if statements - is there a more elegant way?

I have to build a query based on certain conditions. Is there a better way of doing it than the way I have done below? It works fine but I can see it getting out of hand fairly quickly if there were more conditions since I check if any previous conditions had been met every time I check a new one.
$sql = "SELECT DISTINCT fkRespondentID FROM tblRespondentDayTime";
if (!empty($day) || !empty($time) || !empty($sportID)) {
$sql .= " WHERE";
if (!empty($day)) {
$sql .= " fldDay='$day'";
}
if (!empty($time)) {
if (!empty($day)) {
$sql .= " AND";
}
$sql .= " fldTime='$time'";
}
if (!empty($sportID)) {
if (!empty($day) || !empty($time)) {
$sql .= " AND";
}
$sql .= " fkRespondentID IN (SELECT fkRespondentID FROM tblRespondentSport WHERE fkSportID='$sportID')";
}
}
I would use the old "WHERE 1=1" trick; add this as the first condition, and then you can assume the "AND" condition on each statement that follows.
$sql = "SELECT DISTINCT fkRespondentID FROM tblRespondentDayTime WHERE 1=1";
if (!empty($day))
$sql .= "AND fldDay='$day'";
if (!empty($time)) {
$sql .= "AND fldTime='$time'";
if (!empty($sportID))
$sql .= "AND fkRespondentID IN (SELECT fkRespondentID FROM tblRespondentSport WHERE fkSportID='$sportID')";
Build a list/array of conditions, where each conditional is optional (i.e. if condition is valid, push it on the list).
If this list is > 0, add "where" and then add the list join'ed by "and".
Rather than doing checks like if (!empty($day) || !empty($time)) you can create a $whereClause variable and check it like this:
$sql = "SELECT DISTINCT fkRespondentID
FROM tblRespondentDayTime";
$whereClause = '';
// fldDay
if (!empty($day)) {
$whereClause .= " fldDay='$day'";
}
// fldTime
if (!empty($time)) {
if (!empty($whereClause)) {
$whereClause .= ' AND ';
}
$whereClause .= " fldTime='$time'";
}
// fkRespondentID
if (!empty($sportID)) {
if (!empty($whereClause)) {
$whereClause .= ' AND ';
}
$whereClause .= " fkRespondentID IN (SELECT fkRespondentID
FROM tblRespondentSport
WHERE fkSportID='$sportID')";
}
if (!empty($whereClause)) {
$whereClause = ' WHERE '.$whereClause;
}
$sql .= $whereClause;
This will also work if you need to, say, change some to an OR (1=1 trick won't work in that case and could even prove quite hazardous).
you could try putting your variables in an array and having a boolean that tells if you need to add the "AND" in before your next phrase. This would shorten your control statements to a foreach and a nested if.
Here is my solution:
$sql = "SELECT * FROM table";
$conditions = array(
'fldDay' => $day,
'fldTime' => $time,
);
if (count(array_filter($conditions))) {
$sql .= ' WHERE ';
$sql .= implode(' AND ', array_map(function($field, $value) {
return $field . '=\'' . pg_escape_string($value) . '\'';
}, array_keys($conditions), $conditions));
}
Please note, that because of the closures, this won't work below PHP 5.3. If you are using an older PHP, make the closure as a separate function, or substitute it with a foreach.
Unfortunately, building dynamic SQL is a tedious experience and even if you can change a few things in your logic (which actually looks relatively clean), it's still going to be ugly.
Fortunately, Object-relational mapping exists. I'm not too familiar with PHP, but Perl has several CPAN modules such as SQL::Abstract, which will allow you to build fairly complex SQL statements using basic data structures.
If you use stored procedures, you can do something like this:
CREATE PROCEDURE `FindRespondents` (
IN `_day` varchar(255),
...
)
BEGIN
SELECT DISTINCT fkRespondentID
FROM tblRespondentDayTime
WHERE (_day Is Null OR fldDay = _day)
AND ...
END;
|
Passing in null for _day means any fldDay is OK. Any other value for _day, and it must be matched. I assumed fldDay is text, but of course you can type everything properly here.
I know some people aren't fans of stored procedures, but it can be handy encapsulate query logic this way.

Categories