Doctrine search a string in multiple columns - php

I hope you can help :)
This is how the table looks:
+------------+----------------+------------------+---------+
| firstName | lastName | email | etc... |
+------------+----------------+------------------+---------+
| John | Doe | john#doe.com | etc... |
+------------+----------------+------------------+---------+
| John | Michaels | john#michaels.es | etc... |
+------------+----------------+------------------+---------+
This is how the code looks:
if($_GET['search-customers'] != '') {
$busqueda = $_GET['search-customers'];
$query->andWhere("(c.firstName LIKE '%$busqueda%' OR c.lastName LIKE '%$busqueda%' OR c.email LIKE '%$busqueda%')");
}
With that QUERY:
If I input: John in the search box, it gives me the 2 results.
OK
If I input: John D in the search box, it doesn't give me any result. FAIL
All right, I understand, When I type "John D", it try to find first in firstName (doesn't match) and also it doesn't match lastName or email.
How can I combine them?
The idea its to find the complete string in all possibilities.
Thanks!

I will provide you a different alternative using MySQL's Full-Text Search Functions. Lets begin to prepare the table:
ALTER TABLE persons ADD FULLTEXT (`firstname`, `lastname`);
Now, firstname and lastname are columns to be used by full-text in order to search for matches:
SELECT * FROM persons
WHERE MATCH (firstname,lastname)
AGAINST ('John D' IN NATURAL LANGUAGE MODE);
The result will be:
+------------+----------------+------------------+---------+
| firstName | lastName | email | etc... |
+------------+----------------+------------------+---------+
| John | Doe | john#doe.com | etc... |
+------------+----------------+------------------+---------+
| John | Michaels | john#michaels.es | etc... |
+------------+----------------+------------------+---------+
Why both? Because John (as a word) was found, however John Doe is in the first row because has much similitude with the term of search.
Say, that lets apply this tool with Doctrine. I will assume that your model looks like this:
class Person{
/** #column(type="string", name="firstname")*/
protected $firstName;
/** #column(type="string", name="lastname")*/
protected $lastName;
/** #column(type="string")*/
protected $email;
}
Lets create the search function:
public function search($term){
$rsm = new ResultSetMapping();
// Specify the object type to be returned in results
$rsm->addEntityResult('Models\Person', 'p');
// references each attribute with table's columns
$rsm->addFieldResult('p', 'firstName', 'firstName');
$rsm->addFieldResult('p', 'lastName', 'lastname');
$rsm->addFieldResult('p', 'email', 'email');
// create a native query
$sql = 'select p.firstName, p.lastname, p.email from persons p
where match(p.firstname, p.lastname) against(?)';
// execute the query
$query = $em->createNativeQuery($sql, $rsm);
$query->setParameter(1, $term);
// getting the results
return $query->getResult();
}
Finnally, and example:
$term = 'John D';
$results = search($term);
// two results
echo count($results);
Additional notes:
Before mysql 5.7, Full-Text only can be added just to MyISAM tables.
Only can be indexed CHAR, VARCHAR, or TEXT columns.
When using IN NATURAL LANGUAGE MODE in a search, mysql returns an empty resultse when the results represent < 50% of the records.

Maybe you could use the explode function like this:
$busqueda = $_GET['search-customers'];
$names = explode(' ',$busqueda);
if(count($names)>1){
$query->andWhere("(c.firstName LIKE '%{$names[0]}%' AND c.lastName LIKE '%{$names[1]}%')");
}else{
$query->andWhere("(c.firstName LIKE '%$busqueda%' OR c.lastName LIKE '%$busqueda%' OR c.email LIKE '%$busqueda%')");
}
but, using like %word% is inefficient, because it can't use index.

Finally, I decided to concat firstName and lastName. I excluded the email then the query looks like that:
$busqueda = $_GET['search-customers'];
$names = explode(' ',$busqueda);
$hasemail = strpos('#', $busqueda);
if ( $hasemail ) {
$query->andWhere("c.email LIKE '%$busqueda%'");
} else {
$query->andWhere("( CONCAT(c.firstName,' ',c.lastName) LIKE '%$busqueda%' OR c.email LIKE '%$busqueda%')");
}

In your repository you could do something like this:
public function findByTerms($terms) : array
{
$alias = "d";
$qb = $this->createQueryBuilder($alias);
foreach (explode(" ", $terms) as $i => $term) {
$qb
->andWhere($qb->expr()->orX( // nested condition
$qb->expr()->like($alias . ".name", ":term" . $i),
$qb->expr()->like($alias . ".description", ":term" . $i)
))
->setParameter("term" . $i, "%" . $term . "%")
;
}
return $qb->getQuery()->getResult();
}
The Query Builder is able to generate complex queries that scale to your needs

Related

Fastest MySQL way of matching and returning true and false

I have an array of search words.
$unprocessed=array("language1 word1", "language1 word2", "language1 word3");
Each word in $unprocessed has an unique entry in a column called "language1"
If found, return row column "language2" else return false. It is important that if the word is not found, false is returned.
1. $unprocessed=array("language1 word1", "language1 word2", "language1 word3");
2. MySQL Query (what is best way?)
3. $processed=array(false,"language2 word2","language2 word3");
Is it possible to do this without looping a query?
What is best way: using WHERE="word", IN("word"), LIKE="word" or something else?
Each word in $unprocessed has an unique entry in a column called "language1"
This means that you do not want to use LIKE. You could run a single query like this and return a single tuple with as many columns as requested words:
SELECT MAX(IF(language1=':word1', 1, 0)) AS hasWord1,
MAX(IF(language1=':word2', 1, 0)) AS hasWord2,
MAX(IF(language1=':word3', 1, 0)) AS hasWord3
FROM table WHERE language1 IN (':word1',':word2',':word3');
This is slightly more performant that three one-word query, especially if you have an index on language1. If you need more columns from the language table (i.e. not only the word but, say, its weight or its SVO status or...), you'd better ask for a tuple for each word. In this case, though, missing words will not be returned (anyway, you would have no data for them). A judicious use of LEFT JOIN plus the first query can be used to avoid this.
SELECT * FROM table WHERE language1 IN (':word1',':word2',':word3');
or
SELECT table2.* FROM table LEFT JOIN table AS table2 USING(primaryKey)
WHERE table.language1 IN (':word1',':word2',':word3');
You can also get one column only to identify the word by using binary representation:
SELECT MAX(IF(language1=':word1', 1, 0))
+ MAX(IF(language1=':word2', 2, 0))
+ MAX(IF(language1=':word3', 4, 0))
AS wordMask, ...
or, in the case of a JOIN, it's enough to get an index:
SELECT MAX(IF(language1=':word1', 1, 0)
+ MAX(IF(language1=':word2', 2, 0)
+ MAX(IF(language1=':word3', 3, 0)
AS wordIndex, ...
To build the query, you can use a PHP foreach loop.
In case of a simple translate table:
SELECT MAX(IF(language1=':word1', language2, ':word1')) AS word1,
MAX(IF(language1=':word2', language2, ':word2')) AS word2,
MAX(IF(language1=':word3', language2, ':word3')) AS word3,
FROM table WHERE language1 IN (':word1',':word2',':word3');
will return wordn containing either the original word, or the "translated" word from the language2 column:
select MAX(IF(language1='computer', language2, '-')) AS hasWord1, MAX(IF(language1='ouijamaflip', language2, '-')) AS hasWord2 FROM tbl WHERE language1 IN ('computer', 'ouijmaflip');
+------------+----------+
| hasWord1 | hasWord2 |
+------------+----------+
| ordinateur | - |
+------------+----------+
You can also have language priority, again building this in PHP:
SELECT
MAX(IF (language1=':word1',
COALESCE(language2, language3, language4, ':default1')))
AS word1,
You can set $default equal to $word, or maybe "?{$word}?" or similar.
$words = array( 'computer', 'bytes', 'processor' /*, 'other'... */);
$langs = array( 'language2', 'language3' /*, 'language4', ... */ );
$rets = array();
$whrs = array();
$defs = array();
foreach ($words as $ndx => $word) {
$rets[] = "COALESCE(IF (language1 = ':word{$ndx}', COALESCE("
. implode(", ", $langs) . "), ':default{$ndx}')) AS word{$ndx}";
$whrs[] = "':word{$ndx}'";
$defs[] = "MISSING_{$word}";
}
$SQL = "SELECT "
. implode(", ", $rets) // All IFs
. " FROM table WHERE language1 IN("
. implode(", ", $whrs) . ");"; // All WHEREs
This will return a query such as:
SELECT
COALESCE(
IF (language1='computer',
COALESCE(language2, language3),
'MISSING_computer')
) AS word1,
COALESCE(
IF(language1='bytes',
COALESCE(language2, language3),
'MISSING_bytes')
) AS word2,
COALESCE(
IF(language1='processor',
COALESCE(language2, language3),
'MISSING_processor')
) AS word3
FROM tbl
WHERE language1 IN ('computer', 'bytes', 'processor');
and finally, with this tbl:
+-----------+------------+-----------+
| language1 | language2 | language3 |
+-----------+------------+-----------+
| computer | ordinateur | NULL |
| bytes | NULL | ottetti |
+-----------+------------+-----------+
it will return
+------------+---------+-------------------+
| word1 | word2 | word3 |
+------------+---------+-------------------+
| ordinateur | ottetti | MISSING_processor |
+------------+---------+-------------------+
ordinateur will be taken from the first language because it's there, ottetti comes from the second language since the first is NULL, and processor returns an error because it's completely missing or has all relevant columns NULL. You can distinguish these cases by adding "language1" (or a string such as 'EMPTY ROW') as the least priority language.
As far as I know, and according to the comments you can't do this with just MySQL, you'll need PHP to process it.
Assuming you're using pdo.. this pseudo code should do it for you
function whatever($unprocessed){
global $db;
$processed = array();
foreach($unprocessed as $u){
$q = $db->prepare("SELECT * FROM language1 WHERE word = :wrd");
$q->execute(array(":wrd"=>$u));
$processed[] = $q->rowCount() ? $q->fetchColumn() : false;
}
return $processed;
}

Search multiple fields MySQL

I have a database like this :
ID | Name | Model | Type
1 | Car | 4 | C
2 | Bar | 2 | B
3 | Car | 4 | D
4 | Car | 3 | D
And a form like this :
Name :
Model :
Type :
Now, I would like to search only the name, for example "Car" and it returns lines 1, 3, 4. (I left Model and Type empty)
If I search "Car" in Name and 4 in Model, it returns lines 1, 3. (I left Type empty)
And if I search "Car" in Name and "D" in Type, it returns line 3, 4 (I left Model empty)
Is it possible to do this in one query ?
This is what I had :
SELECT *
FROM items
WHERE (:name IS NOT NULL AND name = :name)
AND (:model IS NOT NULL AND model = :model)
AND (:type IS NOT NULL AND type = :type)
But it doesn't work.
I would like to fill only 2 on 3 fields and the the "WHERE" adapts and ignore the blank field.
EDIT 1 : It is a little hard to explain but I have a form. I want to have only one required field, the two others are optional but if I also fill the one other or two others fields, they act like a filter.
So the name field is required (in the form). If I fill only the name field, it will select only where name = :name.
If I fill name + model, it will select where name = :name AND model = :model.
and so on...
Thank you for your help.
I'm not sure what you mean by "blank", but assuming you mean NULL, you can do something like this:
SELECT *
FROM items
WHERE (:name IS NULL OR name = :name) AND
(:model IS NULL OR model = :model) AND
(:type IS NULL OR type = :type);
That problem with this query is that it is very hard for MySQL to use indexes for it, because of the or conditions. If you have a large amount of data, and want to use indexes, then you should construct the where clauses based on the parameters that actually have data.
Here's an alternative approach using PHP. You'll need to update the variables.
<?php
$query = 'SELECT *
FROM items
WHERE 1 = 1 ';
//below used for testing can be remove
//$_GET['name'] = 'test';
//$_GET['car'] = 'test2';
//$_GET['type'] = 'test3';
if(!empty($_GET['name'])) {
$query .= ' and name = ? ';
$params[] = $_GET['name'];
}
if(!empty($_GET['car'])) {
$query .= ' and car = ? ';
$params[] = $_GET['car'];
}
if(!empty($_GET['type'])) {
$query .= ' and type = ? ';
$params[] = $_GET['type'];
}
if(!empty($params)) {
$dbh->prepare($query);
$sth->execute($params);
//fetch
} else {
echo 'Missing Values';
}
The 1=1 is so you can append and search field for each field with a value otherwise you'd need to see if it'd already been set.

MySQL search all fileds with codeIgniter active query or alt. escaping (like + equals)

Logged in $Account = 2; $SearchString = "Sally Do";
$search_array = explode(' ',$SearchString);
People table example:
ID | Account | FirstName | LastName | Misc Other
---------------------------------------------------
1 | 1 | John | Doe | Testing
2 | 2 | John | Doe | Pick Me
3 | 2 | Jonh | Bob | Not Me
If possible I would like to search all table fields without the need to specify them and to escape the values for the LIKE or preferably using CodeIgniter's Active Query.
SELECT * FROM poeple WHERE Account = 2 AND
(
PHP
foreach $search_array
{
FirstName LIKE '%svalue%' OR LastName LIKE '%svalue%'
}
)
Escaping breaks the SQL query and I'm sure I'm probably just doing something wrong...
$this->db->escape($search_array[0]);
$this->db->escape($search_array[1]);
I would much prefer a pure active query but I'm not sure it's possible?
$query = $this->db->select('*')->from('people')->where('Account', $Account)->like(???)->get();
The issue mostly with active query is the lack of ( ) support so the Account doesn't become an OR.
Decided to use an existing escaping function (built into CI) to use with a non-Active Query
function escape_string($str, $like = FALSE)
{
if (is_array($str)){
foreach ($str as $key => $val){
$str[$key] = escape_string($val, $like);
}
return $str;
}
mysql_real_escape_string($str);
if ($like === TRUE){
$str = str_replace(array('%', '_'), array('\\%', '\\_'), $str);
}
return $str;
}
try this way
$this->db->select('*');
$this->db->from('people');
$this->db->where('acount',$account);
if($first_name!='')
{
$this->db->like('first_name',$first_name);
}
if($last_name!='')
{
$this->db->like('last_name',$last_name);
}
Try this one.
$where = "(firstname LIKE '%".$value."%' OR lastname LIKE '%".$value."%')";
$this->db->SELECT('*');
$this->db->from('people');
$this->db->where($where);
$this->db->where('account', $account);
$this->db->get();

Mysql joining 1 row with multiple rows - group concat or php?

Consider the following example:
+----------+--------+-------------+----------+
| Person_id| Person | Language_id | Language |
+----------+--------+-------------+----------+
| 1 | Bob | 5 | English |
| 1 | Bob | 3 | Italiano |
| 1 | Bob | 8 | Deutsch |
+----------+--------+-------------+----------+
and the query is (not that important, just scripting to show you the table structure):
SELECT pl.Person_id, Person, Language_id, Language FROM people as p
LEFT JOIN people_languages as pl ON p.Person_id = pl.Person_id
LEFT JOIN languages as l ON pl.language_id = l.language_id
WHERE pl.Person = 1;
So basically, if the tables are constructed in this way, is it better to retrieve all results as shown above and then create a php function that creates a Person Model with languages_id and languages in an array, or using group_concat to retrieve a single row and then explode the languages and languages_id into an array?
By the way, no matter what I do, at the end I'd like to have a Person Model as the following:
class Person {
public $person_id; // 1
public $person; // Bob
public $language_id; // Array(5, 3, 8)
public $language; // Array(English, Italiano, Deutsch);
.
. // Functions
.
}
I think you should separate the queries into their separate model
There should be a Language model and will keep this simple
class Language
{
function getId() { return $id; }
function getDescription { return $description; }
}
class Person {
public $person_id; // 1
public $person; // Bob
public $languages; //this will store array of Language object
}
//From DataAccess
function getPerson($person_id)
{
$person = new Person();
//select only from Person table
//fill $person properties from records
//$person.person_id = $row['person_id']; etc
//select from people_languages joined to language where person_id=$person_id
$person->languages = getLanguagesByPerson($person->person_id); //returns array of languages
return $person;
}
You can now have
$person = getPerson(123);
$person->langauges[0]->getId(); //language id
$person->langauges[0]->getDescription(); //language id
$person->langauges[1]->getId(); //language id
$person->langauges[1]->getDescription(); //language id
Or loop through the languages
foreach($person->languages as $lang)
{
//use($lang->getId());
//use($lang->getDescription();
}
Here is the answer. You can use both ways but in my opinion it is much much better to use group concat. The reason is that this will increase performance as well as reduce the php code. If you go on with the example you gave you will have to do much coding on the php end. And sometimes it becomes difficult to handle on php end. I had this experience a couple of months ago. Instead using group concat will fetch you single row having everything you need for each person. On the php end simple extract the Group Concated cell and make another loop or put it in array. That is easy to handle.
Consider using a Dictionary
class Person {
public $person_id; // 1
public $person; // Bob
//I don't know php but for your idea
public Dictionary<int,string> languageList; // KeyValuePairs {(5,English),(3,Italiano),(8,Deutsch)}
.
. // Functions
.
}

PHP search multiple fields issue

I'm working through this tutorial online: http://goo.gl/qnk6U
The database table: ajax_search
Firstname | Lastname | Age | Hometown | Job
-------------------------------------------
Joe | Smith | 35 | Boulder | CIA
Steve | Apple | 36 | Denver | FBI
(Types are all varchar except age is an int.)
My question. Is the sql select statement below written correctly to query "Joe 35"? For some reason, I can only query "Joe" and it works, but not combining search terms.
$sql = "select * from ajax_search where FirstName like '%$rec%' or LastName like '%$rec%' or Age like '%$rec%' or Hometown like '%$rec%'";
Assuming your query is "Joe 35", then no. Your query matches any row where any of the four fields contains "Joe 35" (in a single field). To query for a user with name Joe and age 35, you'd need to split the search query and do something like:
WHERE Firstname LIKE "$firstname" AND Age LIKE "$age"
You need to split that sting from search query:
$columns = array("Firstname", "Lastname", "Age", "Hometown", "Job");
$string = ""; // acquire query string
$split = explode(" ", $string);
$search_words = array();
foreach ($split as $word) {
$search_words[] = $word;
}
Create query to search all over the fields:
$first = true;
$sql = "select * from ajax_search where";
foreach ($search_words as $word) {
foreach ($columns as $col) {
$sql .= (($first) ? " " : " OR") . "`$col` LIKE \"%" . $word . "%\"";
$first = false;
}
}
Then run this query.
Another and also a better solution would be more complicated word(tag) based indexing, because this can generate morbid queries when used with more words.

Categories