I'm currently trying to implement a search for keywords/tags in my db.
In my db, I have lines with keywords like:
auto,cabrio,frischluft or
hose,jeans,blaue hose,kleidung
so always some keywords (that can basically also have a whitespace), seperated by a comma (,).
Now I want to be able to find a product in my db that has some keywords entered.
With LIKE I can find the two entries I mentioned with queries like auto,cabrio or also cabrio,frischluft or hose,jeans,blau or hose,kleidung. But what happens if I enter auto,frischluft or something like hose,blaue hose or jeans,kleidung?
Then LIKE wont work any more. Is there a way to do this?
I hope you know what I mean...
So just to make it clear: The code I currently use is:
$searchQuery = "%".$request->input('productSearch')."%";
and $products = Product::where('name', 'LIKE', $searchQuery)->paginate(15);
But as I said, this won't bring me back the article with the keyowrds auto,cabrio,frischluft if the input productSearch has the keywords auto,frischluft entered...
Any ideas?
Sorry, I know I'm late for the party but this is the first result in Google when I was looking for Eloquent keywords search. I had the same problem and I want to help with my solution.
$q = $request->input('productSearch');
$needles = explode(',', $q);
// In my case, I wanted to split the string when a comma or a whitespace is found:
// $needles = preg_split('/[\s,]+/', $q);
$products = Products::where('name', 'LIKE', "%{$q}%");
foreach ($needles as $needle) {
$products = $products->orWhere('name', 'LIKE', "%{$needle}%");
}
$products = $products->paginate(15);
If the user input has too many commas, the $needles array could be too large (and the query too huge), so you can limit the search, for example, for only the first 5 elements in the array:
$needles = array_slice($needles, 0, 5);
I hope this can help somebody.
On your reply just now:
If you want it simpler, read this MySQL documentation: https://dev.mysql.com/doc/refman/5.7/en/regexp.html.
Basically, in a file you could grep for [,]?blaue hose[,]? to find: an optional comma, the string 'blaue hose', and an optional comma.
The more solid solution would be my initial answer:
You could actually create a keyword table, depending on your products table, where each keyphrase/keyword
is in one column by itself, and even lay an index on the keyphrase/keyword. I explain the principle here:
Optimising LIKE expressions that start with wildcards
And, to take your example as input - here is how I do that in Vertica. Many databases offer a function that returns
the n-th part/token of a string delimited by a character of your choice. In Vertica, it's SPLIT_PART().
MySQL, unfortunately, does not offer any correspondence to that function, and you would have to convert the Common Table Expressions in the WITH clauses below to in-line SELECT-s (SELECT ... FROM (SELECT ... ) AS foo(col1,col2,col3) ..... And then, there is a suggestion here From Daniel Vassallo on how to tackle it:
Split value from one field to two
In Vertica, it would look like this:
WITH
-- input
products(prod_id,keywords) AS (
SELECT 1001,'auto,cabrio,frischluft'
UNION ALL SELECT 1002,'hose,jeans,blaue hose,kleidung'
)
,
-- index to get the n-th part of the comma delimited string
max_keyword_count(idx) AS (
SELECT 1
UNION ALL SELECT 2
UNION ALL SELECT 3
UNION ALL SELECT 4
UNION ALL SELECT 5
)
SELECT
prod_id
, idx
, TRIM(SPLIT_PART(keywords,',',idx)) AS keywords
FROM products
CROSS JOIN max_keyword_count
WHERE SPLIT_PART(keywords,',',idx) <> ''
ORDER BY
prod_id
, idx
;
prod_id|idx|keywords
1,001| 1|auto
1,001| 2|cabrio
1,001| 3|frischluft
1,002| 1|hose
1,002| 2|jeans
1,002| 3|blaue hose
1,002| 4|kleidung
Related
I have a column name in my table. Which have value Fresh Cream for example.
And input string have value like This product is made of fresh cream and it is fresh made.
I have tried using regular expression by splitting the string on base of string like
Select * From products where product_name REGEXP ('[[:<:]]This[[:>:]]|[[:<:]]product[[:>:]]|[[:<:]]made[[:>:]]|[[:<:]]of[[:>:]]|[[:<:]]fresh[[:>:]]|[[:<:]]cream[[:>:]]')
But in this case it is also getting the records with the value Chicken with cream. But I want exact match of Fresh cream.
Is there a way to use locate() function like that,
Select * from products where locate(poducts.product_name , 'This product is made of fresh cream and it is fresh made')>0
Or something like That
You need to get all the 'name' then check if your input contain one of those. Then you can use "LIKE % %" expression
list_of_name = SELECT product_name FROM products;
input = "This product is made of fresh cream and it is fresh made.";
foreach (list_of_name as name){
if (strpos(input, name) !== false){
matched_name = name;
break;
}
expr = "%" . matched_name . "%";
product = SELECT * FROM products WHERE product_name LIKE expr;
}
I think you are over thinking this. Or I'm missing something. But shouldn't this work (and be a lot faster than REGEXP):
WHERE product_name LIKE '%fresh cream%'
That will get all records that contain fresh cream exactly and any preceding or following string.
My current config of tables is as follows
PagesTable
$this->belongsToMany('Keywords', ['through' => 'PagesKeywords']);
PagesKeywordsTable
Schema: id, page_id, keyword_id, relevance
$this->belongsTo('Pages');
$this->belongsTo('Keywords');
KeywordsTable
$this->belongsToMany('Pages', ['through' => 'PagesKeywords']);
Now heres what i'm trying to do..
Find pages via keywords, using an array to be precise then order by PagesKeywords.relevance
(This is basically storing how many time that keyword is repeated per page, so no duplicate keywords in join table)
I've currently got this working fine except it groups the results of keywords by the keyword itself, where as I need them to be grouped by Pages.id
Here is what i have in my Pages controller, search action:
$keywords = explode(" ", $this->request->query['q']);
$query = $this->Pages->Keywords->find()
->where(['keyword IN' => $keywords])
->contain(['Pages' => [
'queryBuilder' => function ($q) {
return $q->order([
'PagesKeywords.relevance' =>'DESC'
])->group(['Pages.id']);
}
]]);
$pages = array();
foreach($query as $result) {
$pages[] = $result;
}
I know this seems like a backward way to do things but its the only way I seemed to be able to order by _joinTable (PagesKeywords.relevance)
This returns the results I need but now it needs to be grouped by Pages.id which is where this whole thing goes to pot..
Just to be clear the structure I want is:
Page data 1
Page data 2
Page data 3
Page data 4
Where as its currently returning:
Keyword "google"
------- Page data 1
------- Page data 2
------- Page data 3
------- Page data 4
Keyword "something"
------- Page data 1
------- Page data 2
------- Page data 3
------- Page data 4
If you are able to help me thats great!
Thanks
If you're having issues with complex queries with an ORM, I find it always easier to figure out the SQL I need to get the results I require, then adapt that to the ORM.
The query you're looking for would be like this (Using MySQL engine... MySQL Handles field selects more liberally in GROUP BY clauses than other SQL engines )
SELECT Pages.*, COUNT(DISTINCT(PagesKeywords.keyword_id)) AS KeywordCount
FROM pages Pages
INNER JOIN pages_keywords PagesKeywords ON (PagesKeywords.page_id = Pages.id)
INNER JOIN keywords Keywords ON (Keywords.id = PagesKeywords.keyword_id)
WHERE Keywords.name IN ('keyword1','keyword2')
GROUP BY Pages.id
This will give you all pages that contain the keyword and KeywordCount will contain the number of distinct attached Keyword.id's
So a finder method for this would look like ( going ad-hoc here so my syntax might be shaky )
** Inside your PagesTable model **
public function findPageKeywordRank(Query $q, array $options) {
$q->select(['Pages.*','KeywordCount'=>$q->func()->count('DISTINCT(PagesKeywords.keyword_id)'])
->join([
'PagesKeywords'=>[
'table'=>'pages_keywords',
'type'=>'inner',
'conditions'=>['PagesKeywords.page_id = Pages.id']
],
'Keywords'=>[
'table'=>'keywords',
'type'=>'inner',
'conditions'=>['Keywords.id = PagesKeywords.keyword_id']
]
])
->group(['Pages.id']);
return $q;
}
Then you can all your finder query
$pages = TableRegistry::get("Pages")->find('PageKeywordRank')
->where(['Keywords.name'=>['keyword1','keyword2']]);
I know that $things->lists('type') brings back distinct types in my collection, but how do I go about bringing back the distinct types with the number of times they appear in the collection?
Ideally ending up with something like this for 'types':
array(
'type A' => 4,
'type B' => 1
...
)
Since you are not filtering distinct rows directly with a db query but afterwards on the collection, you unfortunately can't use a SQL COUNT(*) for that.
I don't know of any Laravel way to do it. But it's actually pretty easy with plain PHP
$list = $things->lists('type');
$types = array();
foreach($list as $type){
if(!isset($types[$type])){
$types[$type] = 0;
}
$types[$type]++;
}
I have an itemid and a category id that are both conditional. If none is given then all items are shown newest fist. If itemid is given then only items with an id lower than given id are shown (for paging). If category id is given than only items in a certain category are shown and if both are given than only items from a certain category where item id smaller than itemid are shown (items by category next page).
Because the parameters are conditional you get a lot of if statements depending on the params when building a SQL string (pseudo code I'm wearing out my dollar sign with php stuff):
if itemid ' where i.iid < :itemid '
if catid
if itemid
' and c.id = :catid'
else
' where c.id = :catid'
end if
end if
If more optional parameters are given this will turn very messy so I thought I'd give the createQueryBuilder a try. Was hoping for something like this:
if($itemId!==false){
$qb->where("i.id < :id");
}
if($categoryId!==false){
$qb->where("c.id = :catID");
}
This is sadly not so and the last where will overwrite the first one
What I came up with is this (in Symfony2):
private function getItems($itemId,$categoryId){
$qb=$this->getDoctrine()->getRepository('mrBundle:Item')
->createQueryBuilder('i');
$arr=array();
$qb->innerJoin('i.categories', 'c', null, null);
$itemIdWhere=null;
$categoryIdWhere=null;
if($itemId!==false){
$itemIdWhere=("i.id < :id");
}
if($categoryId!==false){
$categoryIdWhere=("c.id = :catID");
}
if($itemId!==false||$categoryId!==false){
$qb->where($itemIdWhere,$categoryIdWhere);
}
if($itemId!==false){
$qb->setParameter(':id', $itemId);
}
if($categoryId!==false){
$arr[]=("c.id = :catID");
$qb->setParameter(':catID', $categoryId);
}
$qb->add("orderBy", "i.id DESC")
->setFirstResult( 0 )
->setMaxResults( 31 );
I'm not fully trusting the $qb->where(null,null) although it is currently not creating errors or unexpected results. It looks like these parameters are ignored. Could not find anything in the documentation but an empty string would generate an error $qb->where('','').
It also looks a bit clunky to me still, if I could use multiple $qb->where(condition) then only one if statement per optional would be needed $qb->where(condition)->setParameter(':name', $val);
So the question is: Is there a better way?
I guess if doctrine had a function to escape strings I can get rid of the second if statement round (not sure if malicious user could POST something in a different character set that would allow sql injection):
private function getItems($itemId,$categoryId){
$qb=$this->getDoctrine()->getRepository('mrBundle:Item')
->createQueryBuilder('i');
$arr=array();
$qb->innerJoin('i.categories', 'c', null, null);
$itemIdWhere=null;
$categoryIdWhere=null;
if($itemId!==false){
$itemIdWhere=("i.id < ".
someDoctrineEscapeFunction($id));
}
Thank you for reading this far and hope you can enlighten me.
[UPDATE]
I am currently using a dummy where statement so any additional conditional statements can be added with andWhere:
$qb->where('1=1');// adding a dummy where
if($itemId!==false){
$qb->andWhere("i.id < :id")
->setParameter(':id',$itemId);
}
if($categoryId!==false){
$qb->andWhere("c.id = :catID")
->setParameter(':catID',$categoryId);
}
You can create filters if you want to use more generic approach of handling this.
Doctrine 2.2 features a filter system that allows the developer to add SQL to the conditional clauses of queries
Read more about filters however, I'm handling this in similar manner as you showed
EDIT::
Maybe I should be asking what the proper way to get a result set from the database is. When you have 5 joins where there is a 1:M relationship, do you go to the database 5 different times for the data??
I asked this question about an hour ago but haven't been able to get an answer that was fitting. I went ahead and wrote some code that does exactly what I need but am looking for a better way to do it
This array gives me multiple rows of which only some are needed once and others are needed many times. I need to filter these as I have done below but want a better way of doing this if possible.
Array
(
[0] => Array
(
[cid] => one line
[model] => one line
[mfgr] => one line
[color] => one line
[orderid] => one line
[product] => many lines
[location] => many lines
)
[1] => Array
(
.. repeats for as many rows as were found
)
)
This code works perfectly but again, I think there is a more efficient way of doing this. Is there a PHP function that will allow me to clean this up a bit?
// these are the two columns that produce more than 1 result.
$product = '';
$orderid = '';
foreach($res as $key)
{
// these produce many results but I only need one.
$cid = $key['cid'];
$model = $key['model'];
$mfgr = $key['mfgr'];
$color = $key['color'];
$orderid = $key['orderid'];
// these are the two columns that produce more than 1 result.
if($key['flag'] == 'product')
{
$product .= $key['content'];
}
if($key['flag'] == 'orderid')
{
$orderid .= $key['content'];
}
}
// my variables from above in string format:
Here is the requested SQL
SELECT
cid,
model,
mfgr,
color,
orderid,
product,
flag
FROM products Inner Join bluas ON products.cid = bluas.cid
WHERE bluas.cid = 332
ORDER BY bluas.location ASC
Without seeing your database structure it's a bit hard to decipher how you actually want to manipulate your data.
Perhaps this is what you're looking for though?
SELECT p.cid, p.model, p.mfgr, p.color, p.orderid, p.product, p.flag, GROUP_CONCAT(p.content SEPARATOR ', ')
FROM products AS p
INNER JOIN bluas AS b ON p.cid = b.cid
WHERE b.cid = 332
GROUP BY p.cid, p.flag
ORDER BY b.location ASC
So now for each product cid each flag will have an entry consisting of a comma separated list instead of there being many repeating for each flag entry.
Then after you're done with the string you can quickly turn it into an array for further manipulation by doing something like:
explode(', ', $key['content']);
Again it's really hard to tell what information you're trying to pull without seeing your database structure. Your SQL query also doesn't really match up with your code, like I don't even see you grabbing content.
At any rate I'm pretty sure some combination of GROUP BY and GROUP_CONCAT (more info) is what you're looking for.
If you can share more of your database structure and go into more detail of what information exactly you're trying to pull and how you want it formatted I can probably help you with the SQL if you need.