Custom site search by category - mysql

I want to create a simple search for a site that I'm working on. I have items in my db that all hold a specific category id and can optionally be linked to multiple tags.
I would like to take whatever search terms come in and query the category.name and tag.name fields to find the items that match those terms.
I'm looking for advice on how to create an efficient/quick query that does this AND orders the results by the items that match closest(most matches)
Here's a quick version of my relevant tables:
item
id | category | title | description
category
id | name | parentId
tag
id | name | uses
item_tag
itemId | tagId

I still didn't entirely understand what you want.
Well, here's a first version for us to discuss.
I suggest you to create a view as the following:
CREATE OR REPLACE VIEW `search_view` AS
SELECT
i.id AS `item_id`,
i.title AS `item_title`,
c.id AS `cat_id`,
c.name AS `cat_name`,
t.id AS `tag_id`,
t.name AS `tag_name`
FROM item AS i
LEFT OUTER JOIN item_tag AS it
ON (i.id = it.itemId)
LEFT OUTER JOIN tag AS t
ON (it.tagId = t.id)
LEFT OUTER JOIN category AS c
ON (i.category = c.id)
WHERE ((t.id IS NOT NULL) OR (c.id IS NOT NULL));
And then you query from the view using something near
SELECT
*,
(
IF(tag_name like ?, 2, 0)
+ IF(cat_name like ?, 4, 0)
+ IF(item_title like ?, 1, 0)
) AS `priority`
FROM search_view
GROUP BY item_id
ORDER BY SUM(priority);
No tests in the code above. Report any problems you have
[first edition] You use PHP, don't you?
Well, you can normalize your queries using PHP string functions; one way is to replace every occurrence of ',' by '|', remove extra spaces and perform the following query: [I'll give an example using #VAR (actually you'll replace it with your input string)]
SET #VAR = 'notebook|samsung';
SELECT
*,
(
IF (tag_name REGEXP CONCAT('.*(', #VAR, ').*'), 2, 0)
+ IF (cat_name REGEXP CONCAT('.*(', #VAR, ').*'), 4, 0)
+ IF (item_title REGEXP CONCAT('.*(', #VAR, ').*'), 1, 0)
) AS `priority`
FROM search_view
ORDER BY priority DESC;
This time I tested. Yes, you can use MySQL functions, something about REPLACE(REPLACE(#VAR,' ',''), ',', '|'). But I recommend you to do it in PHP (or java, python etc).

SELECT item.*
FROM items
LEFT JOIN categories
ON categories.name = '< input name >'
AND categories.id = items.category
LEFT JOIN item_tags
AND item_tags.itemId = items.id
LEFT JOIN tags
ON tags.name = '< input name >'
AND tags.id = item_tags.tagId
WHERE categories.id IS NOT NULL
OR tags.id IS NOT NULL
ORDER BY COUNT(items.id) DESC;
This may not be the fastest way. Basically you'll left join categories and tags to items while making sure the category and tag have the correct name. Filter out all items that don't match a category or an item_tag in the where clause.
Another alternative would be to create temporary tables. Create one for all categories with the correct name, and one with all tags with the correct name. You could then SELECT items WHERE items.id IN categories_table OR items.id IN tags_table

Related

Select rows with same id that have other data ( search )

I'm trying to build a search function but the current table structure troubles me.
So each row identifies a 'tag' association with an audio ID.
TABLE audio_tag_assoc example
I get the name of each tag id by joining the table tag_association.
TABLE tag_association example
Therefore I need to get all the audio ids that match two or even three tag names.
What I tried is the following but you can probably tell that it doesn't work. I would have later added a join in between those parentheses to change the IDs of the IN with strings.
SELECT *
FROM audio_tag_assoc a
JOIN tag_association b ON a.tag = b.id
WHERE a.audio = (SELECT *
FROM audio_tag_assoc
WHERE tag IN (2,3)
)
Initially I tried having b.name = 'Male' AND b.name = 'Film' but of course, that's not how mysql WHERE clause works.
Do :
SELECT
*
FROM
audio_tag_assoc
INNER JOIN
tag_association ON audio_tag_assoc.tag = tag_association.id
WHERE
audio_tag_assoc.id IN (SELECT
a.id
FROM audio_tag_assoc a
WHERE
a.tag IN (2,3)
GROUP BY
a.audio
HAVING COUNT(a.id) >= 2
);

How to exclude rows from a SQL fetch that have a related keyword through a many to many relationship

I have a MySQL database with a media table, and a keywords table, and a many-to-many relationship between media and keywords via a media_keywords join table.
I want to fetch all records from the media table where the following set of conditions match:
'description' is like 'dog' OR
'media.keywords' includes the id for the 'dog' keyword [100]
And exclude from the found set any records where:
'description' is like 'cat' OR
'media.keywords' includes the id for the 'cat' keyword [400]
And also exclude any row where:
'media.keywords' includes the id for the 'monochrome' keyword [500]
I also want to return only distinct rows, so I'm using GROUP By 'media.id'
The SQL statement I have at the moment is as follows:
SELECT DISTINCT
`media`.`id`,`media`.`description`,
`keywords`.`id` AS `keywords.id`,
`keywords->media_keywords`.`id` AS `keywords.media_keywords.id`,
`keywords->media_keywords`.`media_id` AS `keywords.media_keywords.media_id`,
`keywords->media_keywords`.`keyword_id` AS `keywords.media_keywords.keyword_id`
FROM database.media
LEFT OUTER JOIN
(
`media_keywords` AS `keywords->media_keywords`
INNER JOIN `keywords` AS `keywords`
ON `keywords`.`id` = `keywords->media_keywords`.`keyword_id`
)
ON `media`.`id` = `keywords->media_keywords`.`media_id`
WHERE
(
(`media`.`description` LIKE '%dog%' )
OR `keywords`.`id` IN (100)
)
AND NOT
(
(`media`.`description` LIKE '%cat%' )
OR `keywords`.`id` IN (400,500)
)
GROUP BY `media`.`id` ;
This correctly fetches records where 'dog' is in the description or is a keyword, but ignores the exclusions completely.
Can anyone see what I'm doing wrong here?
I would use a where clause:
select m.*
from media m
where (m.description like '%dog%' or
exists (select 1
from keywords k
where k.media_id = m.id and
k.keyword_id = 100
)
) and
(m.description not like '%cat%' or
exists (select 1
from keywords k
where k.media_id = m.id and
k.keyword_id in (400, 500)
)
);
This is pretty much a direct translation of your conditions.
I don't work with mySQL much, but I would suggestion a different approach
Move the "DOG" condition inside the LEFT JOIN (so only get keywords
matching "DOG") and make it a JOIN. Now you'll have a list of all
matches.
add a subquery in the WHERE clause
WHERE id not in (SELECT id FROM ... WHERE LIKE '%cat%')

MySQL Query show results based on multiple filters/tags

This has been asked in different ways before, but I can't seem to get something that works for what I need exactly.
The goal here is to make a search query that returns Photos based on tags that are selected. Many tags can be applied to the filter simultaneously, which would need to make it so that the query only returns photos that have ALL of the tags selected. Think of any major web shop where you are narrowing down results after performing a basic keyword search.
Table1: Photos
ID|Title|Description|URL|Created
Table2: PhotosTagsXref
ID|PhotoId|TagId
Table3: PhotosTags
ID|Title|Category
What I have:
SELECT p.* FROM `PhotosTagsXref` AS pt
LEFT JOIN `Photos` AS p ON p.`ID` = pt.`PhotoId`
LEFT JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
WHERE p.`Description` LIKE "%test%" AND
????
GROUP BY p.`ID`
ORDER BY p.`Created` DESC LIMIT 20
The ???? is where I've tried a bunch of things, but stumped. Problem is I can easily find a result set that contains photos with one tag or another, but if applying 2, 3, or 4 tags we'd need to only return photos that have entries for all of those tags in the database. I think this will involve combining result sets but not 100% sure.
Example:
Photo 1 Tags: Blue, White, Red
Photo 2 Tags: Blue
Searching for a photo with tags of 'blue' returns both photos, searching for a photo with tags of 'blue' and 'white' returns only Photo 1.
Supposing the requested set of tags is (red,blue) you can do:
SELECT * FROM `Photos`
WHERE `Description` LIKE "%test%"
AND `ID` IN (
SELECT pt.`PhotoId` FROM `PhotosTagsXref` AS pt
JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
WHERE t.Title in ('red','blue') /* your set here */
GROUP BY pt.`PhotoId` HAVING COUNT(DISTINCT t.`TagId`)=2 /* # of tags */
)
ORDER BY `Created` DESC LIMIT 20
Apparently, the tag set needs to be created dynamically, as well as its count.
Note: I'm counting DISTINCT TagIDs because I don't know your table's constraints. If PhotosTagsXRef had a PK/UNIQUE (PhotoId,TagId) and PhotosTags had a PK/UNIQUE (TagId), then COUNT(*) would suffice.
Admittedly a bit ugly. But assuming that PhotosTags.Category has the 'Blue', 'White', etc, try something along this line.
SELECT p.*
From `Photos` AS p
WHERE p.`Description` LIKE "%test%" AND
AND Exists
( Select 1 FROM `PhotosTagsXref` AS pt
Inner JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
Where pt.`PhotoId` = p.`ID`
And t.Category = 'FirstCatToSearch'
)
AND Exists
( Select 1 FROM `PhotosTagsXref` AS pt
Inner JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
Where pt.`PhotoId` = p.`ID`
And t.Category = 'SecondCatToSearch'
)
AND Exists
( ...
)
...
SELECT p.* FROM `PhotosTagsXref` AS pt
LEFT JOIN `Photos` AS p ON p.`ID` = pt.`PhotoId`
LEFT JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
inner join (select PhotoId from PhotosTagsXref
LEFT JOIN `PhotosTags` AS t
ON pt.`TagId` = t.`ID`
where (t.title = 'cond 1' or t.title = 'cond 2' ...)
--where t.title in (list condition) **this works as well**
having count(1) = (count of conditions) ) filter
on filter.photoID = pt.PhotoID
WHERE p.`Description` LIKE "%test%"
GROUP BY p.`ID`
ORDER BY p.`Created` DESC LIMIT 20
That should work, I made some assumptions on what column to use for the filter and joins, you may need to retool...the inner join functions as a filter and should pull out only records that have the number of matches equal to the total of the number of matches submitted. Now you just need a language to plug in those conditions and condition count values.

Select parents based on all children statisfying condition

This is such a simple problem but for some reason I cannot get my head round it today.
I have two entities:- title and product each respectively named tbl_title and tbl_product. Each title can have many products.
The product table has a field called unwanted which can be either null, 0 or 1.
I wish to select all titles based on where all products (ALL) have unwanted set to 1. So in other words I wish to select the parent based upon all children filling a certain condition. So if a title has one product that is unwanted but another that is not I do not wish for this title to enter the result set.
When I try this the most I get out of my head is:
SELECT * FROM `tbl_title`
left join tbl_product on tbl_product.title_id = tbl_title.id
where tbl_product.unwanted = 1
group by tbl_title.id
Which obviously does not work.
So how do I code such a query?
select * from tbl_title
where id not in (select title_id from tbl_product where unwanted = 0)
In English, this query eliminates all titles that have a wanted product.
From a style point of view, it would be better to call your column wanted, because unwanted = 0 is a double-negative of wanted = 1. It's always easier to get your head around positives.
SELECT t.id
FROM `tbl_title` t
left join tbl_product p on p.title_id = t.id
group by t.id
having sum(p.unwanted = 0 or p.unwanted is null) = 0
Try using a subquery like this:
SELECT * FROM `tbl_title` AS t
WHERE EXISTS (SELECT 1 FROM products WHERE title_id = t.id AND unwanted = 1)
AND NOT EXISTS (SELECT 1 FROM products WHERE title_id = t.id AND (unwanted = 0 OR unwanted IS NULL))
Just for the fields in title table
SELECT *
FROM `tbl_title` AS t
JOIN tbl_product AS v ON t.id = v.title_id
WHERE NOT EXISTS(
SELECT *
FROM tbl_product
WHERE (t.id = title_id)
AND (unwanted = 0 OR unwanted IS NULL)
GROUP BY t.id

Select from table1 WHERE table2 contains ALL search parameters

I have two tables (notes and tags). Tags has a foreign key to notes. There may be several tag records to a single note record.
I'm trying to select only the notes that contain all of the desired tags.
SELECT notes.*, tags.* FROM notes LEFT JOIN tags ON notes.id = tags.note_id
WHERE {my note contains all three tags I would like to search on}
Using WHERE tag.name IN ('fruit','meat','vegetable') will bring back all the notes that have a "fruit", "meat", OR "vegetable" tag. I only want to return notes that have all three "fruit", "meat", AND "vegetable" tags.
I'm ok to bring back multiple records (the query above would yield a record for each tag).
I need help with my where clause. Is it possible to do this without a sub-select?
Assuming tags(note_id, tag) is declared UNIQUE or PK, then you can use:
SELECT note_id, COUNT(tag) FROM tags
WHERE tag IN ('fruit', 'vegetable', 'meat')
GROUP BY note_id
HAVING COUNT(tag) >= 3
Further answer based on OP's comment below. To get all tags for the records that match:
SELECT * FROM tags
INNER JOIN
(
SELECT note_id, COUNT(tag) FROM tags
WHERE tag IN ('fruit', 'vegetable', 'meat')
GROUP BY note_id
HAVING COUNT(tag) >= 3
) search_results
ON search_results.note_id = tags.note_id
Without a subselect, as per request:
SELECT notes.*
FROM notes
JOIN tags
ON tag.note = notes.id
AND tag.name IN ('fruit','meat','vegetable')
GROUP BY
notes.id
HAVING COUNT(*) = 3
More efficient method would be:
SELECT notes.*
FROM (
SELECT to.note
FROM tags to
WHERE to.name = 'meat'
AND EXISTS
(
SELECT NULL
FROM tags ti
WHERE ti.note = to.note
AND to.name IN ('fruit', 'vegetable')
LIMIT 1, 1
)
) t
JOIN notes
ON note.id = t.note
The trick here is to put the search on the most selective tag ('meat' in my example) on the first place.
If it is not too late, wouldn't it be better to have a NoteTag table - so you will have notes, tags, notetag tables and you can use simple queries and AND operator to find what you want ?)