Fulltextové vyhledávání v MySQL
Pro prohledávání textů v MySQL se dá často použít jednoduchý operátor LIKE, pro složitější dotazy se ale může hodit fulltextové vyhledávání, kterým MySQL disponuje. To má dvě hlavní varianty: vyhledávání v přirozeném jazyce, které výsledkům zároveň přiřazuje míru relevance, a boolean vyhledávání, které dovoluje používat operátory zpřesňující výsledky. Obě varianty mají několik omezení: ignorují se běžná anglická slova (lze změnit proměnnou ft_stopword_file), minimální délka indexovaného slova je 4 znaky (lze změnit proměnnou ft_min_word_len) a při vyhledávání v přirozeném jazyce se navíc poněkud alibisticky ignorují slova obsažená alespoň v polovině dokumentů (lze změnit pouze překompilováním MySQL). Dobrá zpráva je, že od MySQL 4.1.1 lze fulltextové vyhledávání používat i s kódováním UTF-8.
Hlavně kvůli 50% hranici ignorovaných slov, ale také kvůli dostupným operátorům mi užitečné přijde především boolean vyhledávání. Nic nám ale nebrání výsledky seřadit podle výsledků vyhledávání v přirozeném jazyce:
<?php
$search = mysql_real_escape_string($_GET["search"]);
$result = mysql_query("
SELECT *
FROM clanky
WHERE MATCH(nadpis, clanek) AGAINST ('$search' IN BOOLEAN MODE)
ORDER BY 5 * MATCH(nadpis) AGAINST ('$search') + MATCH(clanek) AGAINST ('$search') DESC
");
?>
Dotaz předpokládá, že v tabulce existují indexy FULLTEXT (nadpis)
, FULLTEXT (clanek)
a FULLTEXT (nadpis, clanek)
. Dotaz by fungoval i s podmínkou MATCH(nadpis, clanek) AGAINST ('$search' IN BOOLEAN MODE)
, v tom případě by se ale index vůbec nepoužil a dotaz by tak byl výrazně pomalejší. Při řazení se na výskyty v nadpisu bere pětkrát větší váha než na výskyty v těle článku – při jediném indexu FULLTEXT (nadpis, clanek)
by tento rozdíl nebylo možné zachytit.
Viz také Nastavení fulltextového vyhledávání MySQL.
Přijďte si o tomto tématu popovídat na školení Návrh a používání MySQL databáze.
Diskuse
Hds:
Fulltextové vyhledávání v MySQL je podle mého názoru na sdíleném hostingu téměr nevyužitelné právě kvůli ft_min_word_len, která je většinou napevno nastavena na 4 a administrátoři hostingu s ní většinou nejsou ochotní hýbat. Příklad za všechny - e-shop. Zákazník bude hledat značku "LG", "AEG" atp., ale nenajde mu to nic, protože je to kratší než 4 znaky. Tam nezbývá než nasadit jednoduché LIKE. Anebo mi něco uniká? :)
13.9.2006 11:50:17
Slušný hosting by to měl být ochotný přenastavit. Pokud to nejde, tak lze pro vyhledávání kratších slov doplnit třeba takovouto podmínku:
<?php
$where = "";
$ft_min_word_len = mysql_result(mysql_query("SHOW VARIABLES LIKE 'ft_min_word_len'"), 0, 1);
preg_match_all("~[\\pL\\pN_]+('[\\pL\\pN_]+)*~u", stripslashes($_GET["search"]), $matches);
foreach ($matches[0] as $part) {
if (iconv_strlen($part, "utf-8") < $ft_min_word_len) {
$regexp = "REGEXP '[[:<:]]" . addslashes($part) . "[[:>:]]'";
$where .= " AND (nadpis $regexp OR clanek $regexp)";
}
}
?>
Pracuje se v kódování UTF-8 a ignorují se vyhledávací operátory.
13.9.2006 14:02:20
Hds:
Díky za tip, tohle jsem ještě nezkoušel.
13.9.2006 18:57:37
Venca Černík:
Hele, asi jsem trochu natvrdlej... Potřebuju aby se tam dalo vyhledávat i se 3 znakama (kdyby 2, ještě líp.) A pokud možno, tak s podobnou metodou. Prosím poraď..
16.11.2006 14:36:17
Nieje v tom RegExpe chyba?
Mne to na serveri vracia chybovu hlasku "Stránku nelze zobrazit".
Ked ten riadok vykomentujem, tak mi to zbehne, ale samozerejme bez toho riadku.
Absolutne tomu RegExpu nerozumiem. Neda sa nahradit niecim inym?
14.4.2008 15:21:46
Tak nakoniec to bola blba verzia php (tusim 5.1.25).
Po upgrade servera to ide normalne.
16.4.2008 15:15:55
Domnívám se, že místo $where .= " AND... by tam mělo být $where .= " OR...
22.8.2008 15:24:51
To záleží na tom, jestli chci vyhledat texty se všemi zadanými slovy (AND) nebo jen některými (OR).
23.8.2008 02:41:01
Jiri Cizek:
Odkaz je nefunkční
19.7.2011 00:24:36
Mike:
Zdravím, myslím že fulltext (index) funguje jen u MyISAM tabulek a nelze ho provést u transakčních INNODb .. není to dost špatné nebo je to u nové verze jinak ?
13.9.2006 15:00:14
pojízdná kočka:
cituju z CHM nápovědy:
11.8.5. Full-Text Restrictions
Full-text searches are supported for MyISAM tables only.
3.8.2009 19:21:17
pojízdná kočka:
ale s verzí 6.0 se prý chystají transakce i pro MyISAM, takže to může znamenat konec kompromisu mezi možností mít transakce nebo fulltextové vyhledávání..
4.8.2009 15:21:43
Transakce se pro Maria (následník MyISAM) chystají, ale cizí klíče bohužel ne.
4.8.2009 15:28:08
PHX:
Zdravim. Z textu mi neni 100% jasne zda BOOLEAN MOD ma taky tu 50% hranici o vyskytu slov. Ma nebo ne? DIKY
1.12.2006 12:18:03
Režim BOOLEAN tuto hranici nemá.
1.12.2006 12:29:06
PetrX:
Můžete mi někdo poradit jak vypsat tu část textu kde se nachází hledané slovo, které bylo vyhledáno v databázi pomocí fulltextového indexu. Pořád si s tím lámu hlavu a nevím jak na to.
16.1.2007 14:16:33
MySQL ti v tom nijak nepomůže, musíš si to vyřešit sám v PHP. Nebo použít vyhledávací nástroj, který úryvky nalezeného textu generovat umí, např.
http://www.sphinxsearch.com/.
16.1.2007 14:40:28
PetrX:
sphinxsearch se mi zdá až moc složitý, spíše by mě zajímal verze v PHP, ale nemohu najít žádný článek, který by se tomu věnoval, nevíš o nějakém?
16.1.2007 18:34:11
Nevím, ale napsal jsem si to do témat, o kterých bych časem mohl napsat.
17.1.2007 10:45:19
PetrX:
V diskuzi mi bylo poradeno pouzit funkci strpos pomocí níž najdu výskyt hledaného, pak vezmu 50 znaků před a za a pomocí funkcí strpos a strrpos najít první mezeru v takovém to úrivku a poté již vybrat z úrivku přesně vybrat od začátku slova do konce posledního celého slova.
Ovšem nevím, zda-li to není až příliž kostrbaté a zbytečně složité na zatížení serveru.
17.1.2007 15:55:19
Zdravím - tak co napsal jste to co jste přislíbil? ;o)
21.9.2007 00:47:37
majo:
neviem vyriesit problem ako zahrnut do vyhladavania aj ciastocne vyrazy. nachadza mi iba presne vyrazy.
npr. zadam vyhladavat slovo auto a chcem aby mi vo vysledkoch vyhladavania zahrnulo aj vyraz autolak zapisany v db.
25.10.2007 03:02:31
Dá se k tomu použít operátor * v BOOLEAN vyhledávání.
25.10.2007 22:01:11
AndyBrandy:
Zdravim, ako zistim relevanciu tych vysledkov? Radi to podla relevancie alebo podla coho to radi vysledky?
Dik
18.11.2007 18:11:59
GrizzlyNetch:
Relevance se dá zobrazit takto:
<?php
$result = mysql_query("SELECT (5 * MATCH(nadpis) AGAINST ('$_GET[search]') + MATCH(clanek) AGAINST ('$_GET[search]')) as relevance
FROM cla...
");
?>
...k druhé otázce je asi zbytečné něco psát, když sql dotaz obsahuje přímo ORDER BY, nebo ne?:-D
19.2.2008 17:43:18
Super clanek, pomohl. Diky moc autorovi
23.2.2008 03:12:10
lolek:
Zdravím, chtěl bych se zeptat jak ted vypisu ty položky vyhledane? Jsem asi natvrdlej nebo teda spis nezkusenej :). Ale zkouším následně a stále pořád to hází chybu (mysql_fetch_array(): supplied argument is not a valid).
<?php
while($row = mysql_fetch_array($result)) {
echo $row["nadpis"];
}
?>
A rovnou bych se chtěl zeptat jestli je lepší psát kód takto bez mezery a s druhym slovem velke pismeno $nasPes nebo takto $nas_pes. Jak to normálně píšete vy Jakube?
Děkuji předem za odpověď.
23.3.2008 09:56:07
Po zavolání mysql_query() si nech vypsat chybu, ke které došlo -
<?php echo mysql_error(); ?>.
Já používám $nas_pes, v objektovém programování se obvykle používá $nasPes. Stejně to je i ve většině PHP.
23.3.2008 22:42:07
lolek:
Děkuju moc za radu, už vše funguje jak má. Jen bych se chtěl zeptat na poslední otázku, myslel jsem že tohle vyhledava tak, že když napíšu do vyhledavani např. slovo "dota", a v nadpise u některeho članku je slovo "dotaz", tak se to takhle najde ale ono ne.
24.3.2008 17:33:21
Dotaz by fungoval i s podmínkou MATCH(nadpis, clanek) AGAINST ('$_GET[search]' IN BOOLEAN MODE), v tom případě by se ale index vůbec nepoužil a dotaz by tak byl výrazně pomalejší.
Jak to myslíš? Proč by se neměl použít index?
23.4.2008 12:29:43
Protože jsou vytvořeny dva indexy (nadpis) a (clanek), aby se dala nadpisu přiřadit vyšší váha.
23.4.2008 13:19:36
No jestli to chápu dobře, tak pro váhu je tu řádek ORDER BY 5 * MATCH(nadpi ...
Ale pořád mi není jasné proč by se při použití zápisu MATCH(nadpis, clanek) neměl použít index oproti MATCH(nadpis) ... MATCH (clanek)
Díval jsem se i do knihy Luke Wellinga, ale nic jsem tam v tomto smyslu nenašel. Tuto informaci máš z manuálu?
24.4.2008 12:33:42
Máš na mysli, že je pořeba exitence společného indexu pro oba sloupce součastně a nestačí pouze indexovat každý sloupec zvlášť?
24.4.2008 13:31:17
Rhuin:
Dobrý den, mám otázku.
Proč mi dotaz:
SELECT firma,
MATCH (firma) AGAINST ('M.K.QUATRO') AS Relevance
FROM `search_fulltext3`
WHERE
MATCH (firma) AGAINST ('M.K.QUATRO'IN BOOLEAN MODE)
HAVING Relevance > 0.2
ORDER BY Relevance DESC
hodí výsledek
M.S. Quatro s.r.o. 10.204277038574
QUATRO CH, s.r.o. 10.204277038574
QUATRO,s.r.o. 10.204277038574
QUATRO - R.I.S.K., s.r.o. 10.204277038574
HB Quatro s.r.o. 10.204277038574
QUATRO-CL s.r.o. 10.204277038574
Quatro - M, spol. s r. o. 10.089566230774
Elektro - Quatro, s.r.o. 10.089566230774
Quatro Dřevostyl, s.r.o. 10.089566230774
Quatro D Čelákovice, s.r.o. 10.089566230774
Quatro Izol s.r.o. 10.089566230774
Quatro - production, s.r.o. 10.089566230774
quatro solutions a.s. 10.089566230774
QUATRO FINANCE a.s. 10.089566230774
OPTIK-QUATRO s.r.o. 10.089566230774
METALVIS Quatro, s.r.o. 10.089566230774
QUATRO - P , SPOL.S R.O. 10.089566230774
QUATRO PLUS s.r.o., v likvidaci 10.089566230774
PIZZA - QUATRO s.r.o. 10.089566230774
L - QUATRO, spol. s r.o. 10.089566230774
Quatro Brno a.s. 10.089566230774
M.K.QUATRO, spol.s r.o. 10.089566230774
QUATRO CS,spol.s r.o. 10.089566230774
přičemž je jasné že předposlední M.K.QUATRO by mělo mít nejvyšší relevanci.
můžete mi to objasnit?
Děkuji
7.6.2008 05:59:17
Je na to odpověď přímo v článku – slova do délky ft_min_word_len se ignorují.
8.6.2008 11:14:15
methew:
Nevíte někdo, jak udělat to aby když vyhledávám třeba "A6 Audi" aby mi to našlo řádky jen kde jsou obsažené obě slova a ne že mi to bude hledat ještě všechny audiny v databázi. Jak se prostě dá definovat aby vypisoval jenom řetězec se všema hledanýma slovama. Díkes
7.7.2008 08:52:10
('+A6 +Audi' IN BOOLEAN MODE). Navíc musí být nastavené ft_min_word_len nejvíce na 2.
7.7.2008 10:38:39
Jan Bláha:
Zkusil jsem příklad popsaný v článku aplikovat na svoji databázi, ale bohužel varianta s OR (match .. OR match ...) nefunguje, indexy se nepoužijí. Mám nadefinované separátní fulltext indexy na obou sloupcích, ale pokud je v dotazu použit OR, ani jeden z indexů se nepoužije (podle EXPLAIN). Pokud je ve WHERE jen jedna podmínka, index se použije správně.
Zjistil jsem že to souvisí s tím že MySQL umí použít pouze 1 fulltext index v dotazu (viz např.
http://capttofu.livejournal.com/8881.html).
Řešením je mít v dotazu podmínku "MATCH(nadpis, clanek) AGAINST ..." a mít indexy FULLTEXT(nadpis, clanek) pro WHERE část a FULLTEXT(nadpis) a FULLTEXT(clanek) pro výpočet relevance. Při velkých tabulkách tyto indexy ovšem značně bobtnají.
Rád bych zde proto zkonzultoval, zda není ještě jiné řešení nebo zda se dá vymyslet jiný způsob, při kterém by se dala použít podmínka OR tak jak je v příkladu.
Vše testováno na MySQL 5.0.51a
9.8.2008 13:17:20
godder:
Dekuji nescetnekrat, dva dny jsem stravil resenim a prepisovanim dotazu, proc se mi nepouzivaji indexy, kdyz pouzivam OR. Myslim, ze by na to melo byt v clanku vyrazne upozorneni. Kazdopadne jeste jednou diky!
11.9.2009 19:28:33
Díky za upozornění, opravil jsem to.
11.9.2009 23:15:29
Perry:
Neporadil by mi někdo, jak "obejít" stopwords ? Bohužel je nijak nemůžu na hostingu vypnout. Něco podobného jako s min. délkou slova, jako psal Jakub Vrána ve druhém příspěvku by se mi hodilo
24.8.2008 06:13:38
Logik:
Jednoduše - připojit podtržítko za každý slovo...
3.12.2008 11:56:25
Suto:
to ale vobec nefunguje
ked chcem hladat ceske mesto Most
tak to berie ako stopword most a podtrzitko s tym nic neurobi
4.12.2009 12:35:22
davEsim:
Dotaz funguje bezvadně, jen by podle mne mělo byt v ORDER BY descendentní řazení...
14.10.2008 07:47:42
Díky za upozornění, opravil jsem to.
15.10.2008 04:29:08
miki:
při hledání mi to najde pouze slova stejné velikosti písmen dá se to nějak obejít aby byl stejný výsledek pro "TEST" i "test" ?
16.3.2009 15:36:27
Zdravím, velmi bych uvítal radu.
Mám dva různé vyhledávací řetězce, oba by měly najít slovo "černobílá", avšak druhý toto slovo při vší snaze nenajde (jde tradičně o znaky ž, š, č, ř):
1) SELECT * FROM `sklad` WHERE `popis1` LIKE '%cernobílá%' COLLATE utf8_general_ci
2) SELECT * FROM `sklad` WHERE MATCH (`popis1`) AGAINST ('cernobílá' COLLATE utf8_general_ci IN BOOLEAN MODE)
Mají pro to nějaký pádný důvod? COLLATE evidentně nereaguje. Avšak pak je fulltext v podstatě k ničemu...
Podotázka - najít "automobil" pomocí MATCH AGAINST ze slova "auto" jde snadno, stačí použít 'auto*'. Ale dá se najít takto "superauto"? Řetězec '*auto' nereaguje...
25.4.2009 13:18:31
Pokud chceš fulltextově vyhledávat bez diakritiky a nastavení COLLATE utf8_general_ci rovnou v definici sloupce se nedá použít, tak nezbývá nic jiného, než si data kopírovat do nového sloupce určeného pro vyhledávání (nejlépe automaticky pomocí triggeru).
Hvězdička slouží jen pro pravostranné rozšíření.
Viz také
http://php.vrana.cz/vyhledani-textu-bez-diakritiky.php.
28.4.2009 06:21:14
Řeším problém...
potřebuje aby když napíšu „xander“, aby mi to v mysql fulltext našlo i „Alexander“
(když napíšu Alex* tak to Alexander najde, to je v pohodě)
píšete, že: Hvězdička slouží jen pro pravostranné rozšíření.
Existuje i nějaké levostranné řešení??
Díky
7.5.2009 05:27:07
Operátor pro levostranné rozšíření ve fulltextovém vyhledávání MySQL neexistuje.
7.5.2009 05:28:44
jj, to vím, že neexistuje :-) ale potřebuji to nějak obejít a to ve fulltextu, aby to bylo rychlé. Máme obchod, kde je například foťák:
SONY DSCW130B.CEE9 Cyber-Shot 8,1MPix, 4x zoom...
no a zákazník si pamatuje, že to je třeba sony a 130B jenže ono to je před tou stotřicítkou ještě dscw takže mu to najde tisíce jinýh výrobků sony :-(
7.5.2009 05:49:55
1. Nějakým způsobem doplnit vhodné oddělovače při vkládání
2. Při čtení je odstranit
7.5.2009 06:39:19
Vincenzo:
Prosím poraďte, co dělám špatně.
tabulka je celá v UTF-8general-ci
zkoušel jsem mnoho způsobů např. tohle
SELECT * FROM `wd_zapisy` WHERE MATCH(nadpis) AGAINST ('neznámé' IN BOOLEAN MODE)
ale diakritiku to prostě nenajde :-(
25.7.2009 00:38:04
Martin:
celý problém, aby to hledalo i malými písmeny a i části řetězce jsem vyřešil takto:
$hledej=strtolower($hledej);
$query ="Select * from tabulka where lower(`nazev`) like '%$hledej%' or lower(`vyrobce`) like '%$hledej%' ";
30.7.2009 15:24:24
MP:
Mam trochu iny problem s "fulltextovym" hladanim, potrebujem hladat v zaciatkoch slov, bez rozlisovania diakritiky teda s collate utf8_general_ci LIKE nieje riesenim a REGEX collate ignoruje.
'Koněvova' RLIKE '[[:<:]]Kon' == true
'Koněvova' RLIKE '[[:<:]]Kone' == false
Fulltext pouzit nemozem. Pomoze dakto?
22.8.2011 00:22:59
Dělat to na úrovni MySQL je dost chůze po tenkém ledě, viz varování na
http://dev.mysql.com/doc/refman/5.1/en/regexp.html#operator_regexp (operátor funguje na úrovni bajtů).
Dalo by se to vyřešit tak, že by se pomocí LIKE vybrali kandidáti na shodu a ta by se pak potvrdila v PHP. Případně pokud jsou začátky slov nějak omezeny (např. víme, že začínají mezerou), tak použít:
s LIKE 'Kone%' OR s LIKE '% Kone%'
22.8.2011 15:01:14
MP:
Vdaka o like som vedel bohuzial tych oddelovacov tam moze byt viac a dopredu presne neviem ake budu. Rozmyslam ako by sa to vyriesilo na strane php, aby to vedelo overit bez ohladu na diakritiku, ako ano oddiakritikovat a porovnat, ale to sa mi zda take nepekne zlozite. Asi tam hodim tych par like a uvidim ako budu postupne pribudat oddelovace. Este raz diki za odpoved
22.8.2011 18:36:27
Pavel:
Zdravím,
mám jeden problém. Mám to napsané správně? Snažím se vyhledat článek, ale píše to: Nebylo nic nalezeno.
Zde je kód:
<?php
public function hledejClanky() {
$hledani = mysqli_real_escape_string($this->con, $_GET['search']);
$query = "SELECT * FROM clanky WHERE MATCH (nazev, text) AGAINST ('$hledani' IN BOOLEAN MODE)";
$resu = mysqli_query($this->con, $query);
$radku = $resu ? mysqli_affected_rows($this->con) : 0;
if ($radku)
{
while ($vys = mysqli_fetch_assoc($resu))
{
echo('<h2>' . $vys['nazev'] . '</h2>');
}
}
else
{
echo('<h2>Nebylo nic nalezeno.</h2>');
}
}
?>
22.2.2017 18:53:21
mysqli_affected_rows vrací počet řádků, které byly posledním dotazem změněné. Počet nalezených řádků vrací mysqli_num_rows.
$vys['nazev'] je dobré zabalit do htmlspecialchars().
23.2.2017 10:45:58