Súlyozott keresés, avagy a helyettünk gondolkodó kereső
Az előző bejegyzésemben részletesen bemutattam, hogy hogyan lehet létrehozni egy olyan keresőt, ami jól reagál a visszalépésre, ami alkalmazza az előkeresési lista megjelenítését, és ami képes több adatbázis oszlopban egyszerre keresni a kereső kifejezés szétbontása mellett. Mivel az a bejegyzésem igen hosszúra sikerült, így jobbnak láttam ezeket a továbbfejlesztési lehetőségeket egy új bejegyzésben leírni. Ha még nem tetted meg, akkor érdemes elolvasnod előbb azt a leírást, hiszen ez annak a forráskódjára épül.
A súlyozott keresés
Ennek az a lényege, hogy a keresési találatok kilistázásakor előre vegye a sorban azokat a találatokat, amik egy megadott kritériumnak megfelelnek. Ilyen kritérium lehet például, hogy az kerüljön előrébb, ahol a keresési kifejezés benne van a "felhasználónév"-ben és az "e-mail cím"-ben is. A következő kritérium lehet, hogy csak a "felhasználónév"-ben van benne. Majd az ezt követő utolsó kritérium, hogy csak az "e-mail cím"-ben van benne.
Az előző példából kiragadom az SQL utasítást és megmutatom, hogy ennek hogyan is kell kinéznie, a fent leírt kritériumok betartása mellett.
Eredmény a "web mester" keresésre
SELECT u_id, username, email,
IF (username = 'web', 4, 0) +
IF (username LIKE '%web%', 2, 0) +
IF (username = 'mester', 4, 0) +
IF (username LIKE '%mester%', 2, 0) +
IF (email = 'web', 2, 0) +
IF (email LIKE '%web%', 1, 0) +
IF (email = 'mester', 2, 0) +
IF (email LIKE '%mester%', 1, 0) AS search_weight
FROM users
WHERE (username LIKE '%web%' AND username LIKE '%mester%') OR (email LIKE '%web%' AND email LIKE '%mester%') ORDER BY search_weight DESC, username ASC
Az ok, amiért nem "MATCH AGAINST" megoldásban gondolkodtam, hogy igényli a "FULL TEXT" indexelést, meghatározza az adatbázist. További problémám vele, hogy ez a keresési módszer, ha a sorok 50%-ánál több egyezőséget talál, eldobja a keresést. Magyarán ez csak akkor használható igazán, ha nagyméretű adatbázissal dolgozunk és pontosak a keresési kifejezések. Maga az eljárás nagyon jó, de bizonyos esetekben pont ugyan annyira zavaró, mint hasznos. Sőt, akár még lassú is lehet. A másik probléma, hogy bár sok beállítási lehetőséggel bír a "MATCH AGAINST", de ez okozza a bonyolultságát is. A most bemutatott példában ezeket a beállításokat fogjuk megvalósítani, csak más megoldással. Később majd ezzel kényelmesebb lesz dolgozni.
Tehát a megoldásom bár elsőre bonyolultabbnak tűnhet, mégis könnyebben alkalmazható adatbázis átalakítás nélkül is. Illetve kisebb méretű adatbázis esetén is kiválóan üzemeltethető. A megoldás lényege, hogy az előbbi példában megismert módon végrehajtjuk a lekérdezést, majd ezt követően egy "IF" segítségével megvizsgáljuk a talált sorokat. Ha keresőszó, vagy kifejezés részlete szerepel az adott találat oszlopában, akkor növeljük a súlyozásra szolgáló beszúrt oszlopunk értékét. Jelen esetben ez a "search_weight". Tehát, ha például a "web" szó szerepel a "username"-ben, akkor ennek a "search_weight"-nek növelje meg az értékét, ha nincs benne, akkor pedig ne növelje. Az "IF"-ek értékét pedig "+" jellel egyszerűen összeadjuk. Az "AS search_weight" pedig létrehozza a tárolásra alkalmas oszlopot, és belehelyezi a summázás után kapott összeget. Egyetlen hátránya a "MATCH AGAINST"-el szemben, hogy ez nem képes összegezni, egy mezőben hányszor volt megtalálható a keresőszó. Itt azt a kérdést kell feltenni magunknak, hogy erre valóban szükségünk van-e, vagy sem. Ha igen, akkor vagy a lekérdezés végén PHP segítségével lehet megvizsgálni az eredmény tömböt, vagy ebben az esetben alkalmazni a "MATCH AGAINST" megoldást. 100%-ban tökéletes megoldás nincs, csak megfelelő van.
Végül ezt az oszlopot használjuk fel a sorrend felállítására. Ami a legtöbb pontot kapta, feltehetően a legrelevánsabb találat a keresésünkre, tehát csökkenő sorrendben listázzuk ki a találatokat. Ez után még beállíthatjuk opcionálisan, hogy azonos értékek esetén a felhasználónevet ABC sorrendbe rendezze. Mutatom, ezt hogyan is érhetjük el:
PHP
<?php
if (filter_input(INPUT_POST, 'search'))
{
$_POST = array_map('clear_tags', $_POST);
$_POST = array_map('clear_sql_injection', $_POST);
if (filter_input(INPUT_POST, 'search_text'))
{
$_SESSION['search_text'] = $_POST['search_text'];
header('HTTP/1.1 301 Moved Permanently');
header('Location: '.get_url_root().'/result.php');
exit;
}
}
elseif (filter_var($_SESSION['search_text']))
{
$_SESSION['search_text'] = clear_tags($_SESSION['search_text']);
$_SESSION['search_text'] = clear_sql_injection($_SESSION['search_text']);
$search_weight = '';
$search_where = '';
$search_column_array = array('username' => 2, 'email' => 1);
$search_column_count = count($search_column_array);
$stop_words_array = array('a', 'az', 'egy', 'és', 'de', 'vagy', 'avagy', 'habár', 'ezzel', 'azzal', 'erre', 'arra', 'innen', 'onnan');
$replace_array = array('.', ',', '/', ':', ';', '-', '+', '--', '=', '>', '<', '<=', '>=');
$search_text = str_replace($replace_array, '', $_SESSION['search_text']);
$search_text_array = explode(' ', $search_text);
$search_text_count = count($search_text_array);
if ($search_text_count == 1)
{
if (!in_array($search_text, $stop_words_array))
{
foreach ($search_column_array as $column => $order_point)
{
if (filter_var($search_where))
{
$search_weight .= " + ";
$search_where .= " OR ";
}
$search_weight .= "IF (".$column." = '".$search_text."', ".($order_point * 2).", 0) + IF (".$column." LIKE '%".$search_text."%', ".$order_point.", 0)";
$search_where .= $column." LIKE '%".$search_text."%'";
}
if (filter_var($search_weight))
{
$search_weight .= " AS search_weight";
}
}
}
else
{
$i = 1;
foreach ($search_column_array as $column => $order_point)
{
$y = 1;
$x = $search_text_count;
foreach ($search_text_array as $search_slug)
{
if (!in_array($search_slug, $stop_words_array))
{
if ($y == 1)
{
$search_where .= "(";
}
$search_weight .= "IF (".$column." = '".$search_slug."', ".($order_point * 2).", 0) + IF (".$column." LIKE '%".$search_slug."%', ".$order_point.", 0)";
$search_where .= $column." LIKE '%".$search_slug."%'";
if ($y < $x)
{
$search_weight .= " + ";
$search_where .= " AND ";
}
else
{
$search_where .= ")";
}
$y++;
}
else
{
$x--;
}
}
if (($i < $search_column_count) && ($x > 0))
{
$search_weight .= " + ";
$search_where .= " OR ";
}
$i++;
}
if (filter_var($search_weight))
{
$search_weight .= " AS search_weight";
}
}
if ((filter_var($search_weight)) && (filter_var($search_where)))
{
$select = mysqli_query($GLOBALS['connect'], "SELECT u_id, username, email, ".$search_weight." FROM users WHERE ".$search_where." ORDER BY search_weight DESC, username ASC");
if (mysqli_num_rows($select) > 0)
{
while ($data = mysqli_fetch_assoc($select))
{
echo '<a href="'.get_url_root().'/detail.php?user='.$data['u_id'].'" title="'.$data['username'].'">'.$data['username'].'</a><br/>';
}
}
else
{
echo '<p class="alert alert-danger">Nincs találat...</p>';
}
}
}
?>
Bekerült a "$search_column_array" tömbbe egy újabb dimenzió, ami minden keresett oszlophoz beállít találat esetén egy súlyozási értéket. Ezzel a számmal fog növekedni a "search_weight" értéke minden egyes találat esetén. Ahol nagyobb számot írsz, ott nagyobb lesz a súlyozás mértéke. Pontos találat esetén ez a rangsorolási érték a kétszeresét éri. A "$stop_words_array" tömb azokat a töltelék szavakat tárolja, melyek megléte nem feltétlen fogja segíteni a keresés végkimenetelét. Magyarán nem nevezhetőek értékes keresőszavaknak, így ezeket a tömbbe felsoroljuk, majd ezeket a program automatikusan kiszűri, hogy ne vegyenek részt a keresésben. A "$replace_array" tömbben pedig azokat az elemeket soroljuk fel, melyek valamilyen írásjelek, vagy matematikai operátorok. Ezekre szintén nem lesz szükségünk a keresés során.
Mivel igen sokat bonyolódott a lekérdezésünk, így több lett az erőforrás igénye. Több memóriát foglal le és a lekérdezés számításigénye a keresőszavak számától függően megnőhet. Éppen ezért, hogy ne terheljük le túlságosan a szervert és, hogy ne adjunk lehetőséget arra, hogy botokkal leterheljék a weboldalt limitáljuk a lekérdezések számát egy meghatározott időintervallumon belül. Én ezt úgy valósítottam meg, hogy fél percenként maximum 1 lekérdezést tudjon futtatni a felhasználó. Ezt úgy érhetjük el, hogy létrehozunk egy munkamenet változót, és abban tároljuk az első lekérdezés idejét, majd a következő lekérdezés idejével összehasonlítjuk. Ha több idő telt el, mint 30 másodperc engedjük a lekérdezést, ha nem, akkor pedig kiírjuk a felhasználónak, hogy még hány másodpercet kell várnia a következő keresésig.
PHP
<?php
if (filter_input(INPUT_POST, 'search'))
{
$_POST = array_map('clear_tags', $_POST);
$_POST = array_map('clear_sql_injection', $_POST);
if (filter_input(INPUT_POST, 'search_text'))
{
$_SESSION['search_text'] = $_POST['search_text'];
header('HTTP/1.1 301 Moved Permanently');
header('Location: '.get_url_root().'/result.php');
exit;
}
}
elseif (filter_var($_SESSION['search_text']))
{
$time_now = time();
$time_secure = $time_now - 30;
$search_weight = '';
$search_where = '';
if (!filter_var($_SESSION['search_last_date']))
{
$_SESSION['search_last_date'] = $time_secure;
}
if ($_SESSION['search_last_date'] <= $time_secure)
{
$_SESSION['search_last_date'] = $time_now;
$_SESSION['search_text'] = clear_tags($_SESSION['search_text']);
$_SESSION['search_text'] = clear_sql_injection($_SESSION['search_text']);
$search_column_array = array('username' => 2, 'email' => 1);
$search_column_count = count($search_column_array);
$stop_words_array = array('a', 'az', 'egy', 'és', 'de', 'vagy', 'avagy', 'habár', 'ezzel', 'azzal', 'erre', 'arra', 'innen', 'onnan');
$replace_array = array('.', ',', '/', ':', ';', '-', '+', '--', '=', '>', '<', '<=', '>=');
$search_text = str_replace($replace_array, '', $_SESSION['search_text']);
$search_text_array = explode(' ', $search_text);
$search_text_count = count($search_text_array);
if ($search_text_count == 1)
{
if (!in_array($search_text, $stop_words_array))
{
foreach ($search_column_array as $column => $order_point)
{
if (filter_var($search_where))
{
$search_weight .= " + ";
$search_where .= " OR ";
}
$search_weight .= "IF (".$column." = '".$search_text."', ".($order_point * 2).", 0) + IF (".$column." LIKE '%".$search_text."%', ".$order_point.", 0)";
$search_where .= $column." LIKE '%".$search_text."%'";
}
if (filter_var($search_weight))
{
$search_weight .= " AS search_weight";
}
}
}
else
{
$i = 1;
foreach ($search_column_array as $column => $order_point)
{
$y = 1;
$x = $search_text_count;
foreach ($search_text_array as $search_slug)
{
if (!in_array($search_slug, $stop_words_array))
{
if ($y == 1)
{
$search_where .= "(";
}
$search_weight .= "IF (".$column." = '".$search_slug."', ".($order_point * 2).", 0) + IF (".$column." LIKE '%".$search_slug."%', ".$order_point.", 0)";
$search_where .= $column." LIKE '%".$search_slug."%'";
if ($y < $x)
{
$search_weight .= " + ";
$search_where .= " AND ";
}
else
{
$search_where .= ")";
}
$y++;
}
else
{
$x--;
}
}
if (($i < $search_column_count) && ($x > 0))
{
$search_weight .= " + ";
$search_where .= " OR ";
}
$i++;
}
if (filter_var($search_weight))
{
$search_weight .= " AS search_weight";
}
}
}
if ((filter_var($search_weight)) && (filter_var($search_where)))
{
$select = mysqli_query($GLOBALS['connect'], "SELECT u_id, username, email, ".$search_weight." FROM users WHERE ".$search_where." ORDER BY search_weight DESC, username ASC");
if (mysqli_num_rows($select) > 0)
{
while ($data = mysqli_fetch_assoc($select))
{
echo '<a href="'.get_url_root().'/detail.php?user='.$data['u_id'].'" title="'.$data['username'].'">'.$data['username'].'</a><br/>';
}
}
else
{
if ($_SESSION['search_last_date'] > $time_secure)
{
echo '<p class="alert alert-danger">'.($_SESSION['search_last_date'] - $time_secure).' másodpercet kell még várni a következő kereséshez...</p>';
}
else
{
echo '<p class="alert alert-danger">Nincs találat...</p>';
}
}
}
}
?>
A keresőnk most már elég intelligens releváns találatok listázásához, de a bonyolultságát ezen felül még számtalan módon lehet fokozni. Nyilván nem egy Google kistestvért írtunk meg pár óra alatt, viszont egy weboldalon, vagy webáruházban normális kereséshez jó alapot szolgáló kód készült. Következő bejegyzésemben ezt még azzal fogjuk fokozni, hogy listát készítünk a felhasználók keresési szokásairól. Visszatérő keresések esetén, ahol nem elég eredményes a keresés, vagy egyáltalán nincs találat, ott meg tudjuk támogatni egy kis trükkel a keresés eredményességét.
Leírásaink azon kezdő és haladó programozóknak nyújtanak segítséget, akik már minimális szinten foglalkoztak weboldalkészítéssel. Ha szeretnél jobban elmélyülni a témában, vagy elsajátítani alapokat, még tovább fejlődni, akkor nézz körbe tanfolyam kínálatunkban, ahol a kezdőtől a profi szintig nyújtunk képzéseket a számodra.