I am working with PHP and MySQL. I'll provide a simple table below:
------------------------------------------------------
| id | text |
------------------------------------------------------
| 1 | The quick brown fox jumped over the lazy dog |
------------------------------------------------------
I'd like users to be able to search using keywords separated by spaces. I've got a simple SQL query for the above which is SELECT * FROM table WHERE text LIKE %[$keyword]% which means if I search for "The quick fox", I won't get any results. Is there a way to separate the keywords by spaces so if I search for "The quick fox", it will run 3 searches. One for "The", one for "quick", and one for "fox" - removing all white spaces. Though it should only display one result since it all belongs to the same row instead of 3 since all 3 keywords matched the data in the row.
What's the best way to do this? All suggestions to do this better are always welcome. Thanks!
[EDIT]
Just thought of this now, would separating the keywords by comma (,) be a better option?
You might consider a regular expression via REGEXP to separate the words into an or group.
SELECT *
FROM tbl
WHERE
LOWER(`text`) REGEXP '\b(the|quick|fox)\b'
Matches:
The quick brown fox jumps over the lazy dog
Quick, get the fox!
I ate the cake
Doesn't match
Brown dogs
Inside PHP, you can construct this expression by splitting your search string on the spaces and imploding it back on | after escaping each component.
$str = "the quick brown fox";
$kwds = explode(" ", $str);
$kwds = array_map("mysql_real_escape_string", $kwds);
$regexp = "\b(" . implode("|", $kwds) . ")\b";
Then use REGEXP '$regexp' in your statement.
Addendum:
Since you didn't mention it in the OP, I want to be sure you aware of MySQL's full text searching capabilities on MyISAM tables, in case it can meet your need. From your description, full text doesn't sound like exactly your requirement, but you should review it as a possibility: http://dev.mysql.com/doc/refman/5.0/en/fulltext-search.html
I did it by a different approach of concatenating the query string.
//this line contains four words which are being used for a search
$str = "the quick brown fox";
//the query string is written in little pieces, stored in variable "$uquery"
$uquery = "select * from tablename where";
//a new variable "$keywords" is going to hold the values as an array
$keywords = explode(" ", $str);
$keywords = array_map("mysql_real_escape_string", $keywords);
//"$k" will be storing the total no. of elements in the array, should be holding "4" for this example
$k = count($keywords);
//defining variable "$i" equals to zero
$i = 0;
//initiating a while loop which will be continued 4 times as we have only 4 keywords in this example
while($i <= $k){
//here comes another new variable "$uquery_", following part will be concatenated with the previous piece of "$uquery" later
$uquery_ .= " topic like convert(_utf8 '%$keywords[$i]%' USING latin1)";
//for more explanation let me tell you that how "$keywords[$i]" will help in above piece of code, for every value of "$i" it will be fetching a keyword from the array, see below:
//$keywords[0]= "the" for $i =0
//$keywords[1]= "quick" for $i =1
//$keywords[2]= "brown" for $i =2
//$keywords[3]= "fox" for $i =3
//now concatenating logical && operator after every round of the loop in "$uquery_" but not for the last round as the query needs to be ended after last keyword
if($i != $k){ $uquery_ .= " &&"; }
//adding 1 to $i after every round
$i++;
}
//now our main variable "$uquery" is being concatenated with "$uquery_"
$uquery .= "$uquery_";
The above piece of code will be generating following query:
select * from tablename where topic like convert(_utf8 '%the%' USING latin1) && topic like convert(_utf8 '%quick%' USING latin1) && topic like convert(_utf8 '%brown%' USING latin1) && topic like convert(_utf8 '%fox%' USING latin1)
Note: "topic" is supposed to be the column name in mysql table, you can replace it with the column name as defined in your table.
I hope it will help a few of the people. If you have any questions feel free to ask me via Live Chat feature on my website http://www.79xperts.com
Regards
Adnan Saeed
Related
Not sure if this is possible in MySQL, but I have a column that has business names like:
AT&T Store
O'Reilly's Auto Parts
Burger King
which I import into Sphinx Search with a MySQL query. I have MariaDB, so there is a REGEXP_REPLACE(col, regexp, replace) function, but I'm having trouble figuring out the rest.
What I need is to repeat words with non-alphanumeric characters replaced with and without a space. So the above examples would become:
ATT AT T Store
OReillys O Reilly s Auto Parts
Burger King
Is this possible in a MySQL query? Thanks!
This can be done all at once, but maybe not by SQL primitive regex.
I don't know REGEXP_REPLACE, nor modern day SQL.
Typically its done by three regex.
Pseudo code:
$column_val = "O'Reilly's Auto Parts";
$new_column_val = Replace_Globally(
$column_val,
'\b\w+[[:punct:]](?:[[:punct:]]*\w)+\b',
function( $match ) {
$val = $match.value;
$text1 = Replace_Globally( $val, '[[:punct:]]+', "" );
$text2 = Replace_Globally( $val, '[[:punct:]]+', " " );
return $text1 + " " + $text2;
}
);
So, this might not look like something sql can do, so you might have to get creative.
REGEXP_REPLACE is in MariaDB only, MySQL doesn't have it.
select regexp_replace(regexp_replace(
"AT&T Store
O'Reilly's Auto Parts
Burger King",
'([[:alnum:]]+)[[:punct:]]+([[:alnum:]]+)[[:punct:]]+([[:alnum:]]+)',
'\\1\\2\\3 \\1 \\2 \\3'),
'([[:alnum:]]+)[[:punct:]]+([[:alnum:]]+)',
'\\1\\2 \\1 \\2')
Can someone help me figure out the correct syntax of
for (my $i = 0; $i <=3; $i++)
{
$store = qq(INSERT INTO main (creator_name,relationship)
VALUES("$data{creatorname}",$data{"relationship$i"}) );
The problem lies with $data{"relationship$1"}. I'm looping because I have 'relationship1', 'relationship2', and 'relationship3' in my data hash. I didn't want to go through 3 separate mysql queries to get the job done so I'm trying to loop over it.
Any pointers?
EDIT:
Thanks for your help with pointing me towards placeholders. It's not working as placeholders and it looks like it's because of
$sth->execute($data{creatorname},$data{relationship},"DATE_ADD(NOW(), INTERVAL $interval)"
I have a DATE_ADD now that I'm using, it doesn't look like it likes to be used as a placeholder.
As pointed out by mob and Bill, if possible it is best to use place holders, but that's not the reason your code is not working.
It is not working because you are trying to do two levels of variable interpolation in one string: first interpolate $i into "relationship$i", then interpolate $data{"relationship$i"} into the larger string quoted with qq. They will not nest like that.
This would work:
for (my $i = 0; $i <=3; $i++)
{
my $relationship = $data{"relationship$i"}
$store = qq(INSERT INTO main (creator_name,relationship)
VALUES("$data{creatorname}",$relationship ) );
As #mob says, you should use query parameters instead of fighting with how to interpolate variables directly into strings.
$store = qq(INSERT INTO main (creator_name, relationship) VALUES (?, ?));
$st = $dbi->prepare($store);
for (my $i = 0; $i < 3; $i++)
{
$st->execute($data{creatorname}, $data{"relationship$i"});
}
Advantages of using parameters:
Easier to code, without worrying about awkward string interpolation.
Slightly better for performance, because the SQL statement is parsed once, instead of repeatedly during each loop iteration.
Safer with respect to application security; good defense against SQL injection.
Re your comment:
An SQL parameter can be used only in place of a single scalar value. Not an expression, or a table name or column name, or a list of values, or SQL keywords. Basically, any value you pass for the parameter value will be treated as though you had put quotes around it (there are some nuances to that, but it gives you the approximate idea).
Given the expression you described, I'd write the code like this:
$store = qq(INSERT INTO main (creator_name, relationship, complicated_column)
VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? HOUR)));
$st = $dbi->prepare($store);
for (my $i = 0; $i < 3; $i++)
{
$st->execute($data{creatorname}, $data{"relationship$i"}, $interval);
}
Re answer from #harmic:
This is awkward to reply to another answer by adding to my own answer, but I wanted to share a code test that demonstrates the "double-interpolation" does in fact work.
$ cat test.pl
$i = 1;
$data{"key$i"} = "word";
$s = qq(string with parentheses ($data{"key$i"}));
print $s, "\n";
$ perl test.pl
string with parentheses (word)
The output of running this Perl script shows that the interpolation worked.
It's a bit hard on the eyes, but if you always have three rows to enter you could do it all in one execute():
my $sth = $dbh->prepare(<<'__eosql');
INSERT INTO main (time_column, creator_name, relationship)
SELECT NOW() + INTERVAL ? HOUR, -- placeholder for $interval
?, -- $data{creatorname}
relation
FROM (SELECT ? AS relation -- $data{relationship1}
UNION ALL
SELECT ? -- $data{relationship2}
UNION ALL
SELECT ?) d -- $data{relationship3}
__eosql
$sth->execute($interval, #data{qw(creatorname relationship1 relationship2 relationship3)});
That uses a hash slice to pull the values out of %data.
I've been out of the mysql and perl game for quite a few years and can't seem to get this right. I have a table with just 3 columns. 'cnt' is one of them. All I want to do is query the table on 'name' and see if name exists. If it does, I want to capture the value of 'cnt'. The table has a record of testName with a value of 2 I added manually. When this script is run it returns empty.
my $count;
my $pop = qq(SELECT cnt FROM popular WHERE name="testName");
my $sth = $dbh->prepare($pop);
$sth->execute() or die $dbh->errstr;
my #return;
while (#return = $sth->fetchrow_array()) {
$count = $return[1];
}
print "our return count is $count";
Is it obvious to anyone what I did wrong?
You probably mean
$count = $return[0];
According to perl doc on mysql
An alternative to fetchrow_arrayref. Fetches the next row of data and returns it as a list containing the field values.
Since you select cnt as the return value ,so , the size of #return is 1,but you misunderstand it as the number of results which meets your query condition.No, it is not so!Please have a more careful reading of perl doc.
So I use the PDO for a DB connection like this:
$this->dsn[$key] = array('mysql:host=' . $creds['SRVR'] . ';dbname=' . $db, $creds['USER'], $creds['PWD']);
$this->db[$key] = new PDO($this->dsn[$key]);
Using PDO I can then execute a MySQL SELECT using something like this:
$sql = "SELECT * FROM table WHERE id = ?";
$st = $db->prepare($sql);
$st->execute($id);
$result = $st->fetchAll();
The $result variable will then return an array of arrays where each row is given a incremental key - the first row having the array key 0. And then that data will have an array the DB data like this:
$result (array(2)
[0]=>[0=>1, "id"=>1, 1=>"stuff", "field1"=>"stuff", 2=>"more stuff", "field2"=>"more stuff" ...],
[1]=>[0=>2, "id"=>2, 1=>"yet more stuff", "field1"=>"yet more stuff", 2=>"even more stuff", "field2"=>"even more stuff"]);
In this example the DB table's field names would be id, field1 and field2. And the result allows you to spin through the array of data rows and then access the data using either a index (0, 1, 2) or the field name ("id", "field1", "field2"). Most of the time I prefer to access the data via the field names but access via both means is useful.
So I'm learning the ruby-mysql gem right now and I can retrieve the data from the DB. However, I cannot get the field names. I could probably extract it from the SQL statement given but that requires a fair bit of coding for error trapping and only works so long as I'm not using SELECT * FROM ... as my SELECT statement.
So I'm using a table full of State names and their abbreviations for my testing. When I use "SELECT State, Abbr FROM states" with the following code
st = #db.prepare(sql)
if empty(where)
st.execute()
else
st.execute(where)
end
rows = []
while row = st.fetch do
rows << row
end
st.close
return rows
I get a result like this:
[["Alabama", "AL"], ["Alaska", "AK"], ...]
And I'm wanting a result like this:
[[0=>"Alabama", "State"=>"Alabama", 1=>"AL", "Abbr"=>"AL"], ...]
I'm guessing I don't have the way inspect would display it quite right but I'm hoping you get the idea by now.
Anyway to do this? I've seen some reference to doing this type of thing but it appears to require the DBI module. I guess that isn't the end of the world but is that the only way? Or can I do it with ruby-mysql alone?
I've been digging into all the methods I can find without success. Hopefully you guys can help.
Thanks
Gabe
You can do this yourself without too much effort:
expanded_rows = rows.map do |r|
{ 0 => r[0], 'State' => r[0], 1 => r[1], 'Abbr' => r[1] }
end
Or a more general approach that you could wrap up in a method:
columns = ['State', 'Abbr']
expanded_rows = rows.map do |r|
0.upto(names.length - 1).each_with_object({}) do |i, h|
h[names[i]] = h[i] = r[i]
end
end
So you could collect up the rows as you are now and then pump that array of arrays through something like what's above and you should get the sort of data structure you're looking for out the other side.
There are other methods on the row you get from st.fetch as well:
http://rubydoc.info/gems/mysql/2.8.1/Mysql/Result
But you'll have to experiment a little to see what exactly they return as the documentation is, um, a little thin.
You should be able to get the column names out of row or st:
http://rubydoc.info/gems/mysql/2.8.1/Mysql/Stmt
but again, you'll have to experiment to figure out the API. Sorry, I don't have anything set up to play around with the MySQL API that you're using so I can't be more specific.
I realize that php programmers are all cowboys who think using a db layer is cheating, but you should really consider activerecord.
This is an addition to my solved question here:
how to get array of zip codes within x miles in perl
OK, I have the array #zips. Now I am trying to use it in a query like this:
SELECT `club_name`,`city` FROM `table` WHERE `public_gig` = 'y' AND `zip` IN (#zips)
#I also tried syntax "IN ("#zips"), IN #zips and IN ('#zips')"
But, I cannot get it to work. (I am using placeholders and such as you see in my link above.)
I was able to get this to work:
$fzip=shift(#Zips);
$lzip=pop(#Zips);
SELECT `club_name`,`city` FROM `table` WHERE `public_gig` = 'y' AND `zip` BETWEEN $fzip AND $lzip
ZIP | public_gig | start_time | fin_time | city | club_name | and so on
33416 | y | 9pm | 2am | clearwater | beach bar | yada
But, for obvious reasons and some resemblance of accuracy, that is not really what I want. Just wanted to see if I could get SOMETHING working on my own.
Why can't I get the query to work with the zips in the array using IN?? Nothing is returned and there is no error.
There is actually a lot more in that query but, I left it all out to keep it short here.
I tried to figure it out by myself. Obviously, my learning capacity for the day is near peak.
Thanks for any help.
All of the examples posted here will screw up if any of your values contain single-quotes, don't use them.
Instead (assuming $dbh is the database handle for your mysql connection):
my $zip_string = join q{,}, map $dbh->quote($_), #zips;
and interpolate that.
Or, for something nice, but not half as outlandish as DBIx::Perlish: SQL::Abstract.
my $sqla = SQL::Abstract->new;
my ($sql, #bind) = $sqla->select(
'table',
['club_name', 'city'],
{
public_gig => y',
zip => { -in => \#zips },
}
);
$dbh->prepare($sql);
$dbh->execute(#bind);
# fetchrow etc.
This can be done using placeholders, you just have to work around the limitation that each placeholder can only accept a single value. WHERE zip IN (?) won't work because you're (presumably) looking for more than one value (otherwise, why use IN?).
You can, however, easily build a statement on the fly with the correct number of placeholders:
#!/usr/bin/env perl
use strict;
use warnings;
my #zips = (12345, 54321, 90210);
my $stmt = "SELECT `club_name`,`city`
FROM `table`
WHERE `public_gig` = 'y' AND `zip` IN ("
. join(', ', ('?') x #zips) . ')';
print "$stmt\n";
# Now just:
# my $sth = $dbh->prepare($stmt);
# $sth->execute(#zips);
Alternatively, if you don't mind using weird CPAN modules,
with DBIx::Perlish you can just say:
my #results = db_fetch {
my $t: table;
$t->public_gig eq "y";
$t->zip <- #zips;
};
and it will do the right thing.
Full disclosure: I am the author of DBIx::Perlish.
I don't know perl too much, but this looks like a simple SQL problem: why don't you just build the SQL IN clause from your array? You should get something like
AND zip IN ('zip 1', 'zip 2', '...')
I doubt just adding an array in perl will create the right strings for the SQL string ...
You need to turn the array into a string of values seperated by commas.
Try this :
my $zipcodes = join('\',\'',#zips);
SELECT `club_name`,`city` FROM `table` WHERE `public_gig` = 'y' AND `zip` IN ('".$zipcodes."');