Bevezetés a Sphinx keresőmotor használatába
A jól használható kereső gyakran a legfontosabb része a honlapnak; gyakran önmagában meghatározza annak sikerességét, avagy sikertelenségét. A szöveges keresések megtámogatására ideális eszköz lehet a Sphinx.
Mi a Sphinx keresőmotor?
A Sphinx egy úgynevezett full-text keresőmotor, ami rendkívül alkalmas nagyobb mennyiségű szövegben történő keresésre. A puszta teljesítményadatok igen impozánsak:
- 50-100x gyorsabb indexelés mint a MySQL full-text esetében, és 4-10x gyorsabb, mint más külső keresőmotorok esetében;
- a keresési sebesség a keresési módtól függően akár 500x gyorsabb lehet a MySQL full-textnél (különösen nagy mennyiségű adat és
GROUP BY
esetén), de más külső keresőmotoroktól is 2x gyorsabb;
A kitűnő teljesítmény mellett mind vertikálisan, mind horizontálisan jól skálázható.
Mikor, és mire érdemes használni a Sphinxet?
Ha nagyobb mennyiségű szövegben keresünk, akkor mindenképpen, de jól jöhet, akkor is, ha címke szerint szeretnénk megjeleníteni a keresési eredményeket, továbbá jó lehet azokban az esetekben is, amikor az adott szövegkörnyezet alapján a minél relevánsabb találatok megjelenítése a cél, ugyanis a nyers teljesítmény mellett ez a Sphinx másik erőssége; számtalan keresési lehetőséget, és többféle súlyozási algoritmust kínál.
Mire nem jó a Sphinx?
A Sphinx nem egy teljes adatbázis-kezelő, funkciói a szöveges keresésekre vannak kihegyezve, a nagyon speciális esetektől eltekintve a meglévő adatbázis-kezelő mellett szokás alkalmazni. Néhány egyszerű példán keresztül igyekszem bemutatni, miként kezdhetjük el a Sphinx használatát. A Sphinxről főleg a PHP–MySQL viszonyában fogok szólni, ám az itt leírtak nagyrészt általános érvényűek.
Környezet kialakítása
Első körben azt nézzük meg, miként lehet felállítani a windowsos tesztkörnyezetet. Megjegyzem, nincsen túl sok különbség a Windowson és Linuxon való futtatás között. Ha már tisztában vagyunk az alapvető működéssel, percek alatt belőhető a Sphinx az éles (Linux) rendszerben is.
Az első lépés a windowsos állományok letöltése. Ezután az állományokat egyszerűen helyezzük el a C:\Sphinx könyvtárban.
A tényleges keresések előtt a következő fő lépéseket kell megtennünk.
- Létrehozunk néhány minta táblát az adatbázisban.
- Létre kell hozni egy konfigurációs fájlt, ami az indexelő alkalmazás és a kereső démon is használni fog. (A config fájlok részére létrehoztam egy külön conf/ könyvtárat.)
- Létre kell hozni egy index állományt az indexer segítségével.
- El kell indítani a searchd-t, ami majd a kereséseket végzi.
- Létrehozunk egy php fájlt a teszt keresésékre.
A Sphinx indexet létrehozhatjuk egy XML állományból vagy mint a példánkban is, közvetlenül adatbázisból. Az index forrásaként ezeket a leegyszerűsített táblákat és a hozzájuk tartozó adatokat használjuk:
CREATE TABLE IF NOT EXISTS `product` (
`id` int(11) unsigned NOT NULL,
`name` varchar(255) NOT NULL,
`category_id` int(11) unsigned NOT NULL,
`score` float DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `tag` (
`id` int(11) unsigned NOT NULL,
`name` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `_product_tag` (
`product_id` int(11) unsigned NOT NULL,
`tag_id` int(11) unsigned NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO product (id, name, category_id, score) VALUE
(1, 'Marhahúsos kutyatáp', 1, 5.2),
(2, 'Marhahúsos macskatáp', 2, 7.1),
(3, 'Vitamin tabletta kutyának', 1, 5.1),
(4, 'Dog Stop távoltartó spray', 1, 3.2),
(5, 'Gyógysampon kutyának', 1, 5),
(6, 'Csonterősítő tabletta kutyának', 1, 4.9),
(7, 'Alomlapát', 2, 6.6),
(8, 'Vitamin tabletta macskának', 2, 7.2),
(9, 'Játék labda', 2, 7.7),
(10, 'Őrölt száraztáp', 1, 9.4);
INSERT INTO tag (id, name) VALUES
(1, 'táp'),
(2, 'vitamin'),
(3, 'játék'),
(4, 'kutya'),
(5, 'macska');
INSERT INTO _product_tag (product_id, tag_id) VALUES
(1,1),
(1,4),
(2,1),
(2,5),
(3,2),
(3,4),
(4,4),
(5,4),
(6,4),
(7,5),
(8,2),
(8,5),
(10,1),
(10,4);
Ezek a táblák a könnyebb érthetőség végett a lehető legegyszerűbb szerkezetűek, nincsenek indexek, idegen kulcsok stb. Ahogy a táblákból is látszik, egy webáruházas példán keresztül mutatom be a Sphinx használatát.
Így néz ki a konfigurációs állomány:
source sphinx_test
{
type = mysql
sql_host = localhost
sql_user =
sql_pass =
sql_db = sphinx_test
sql_query_pre = SET NAMES utf8
sql_query = \
SELECT pr.id, MIN(pr.name) AS name, MIN(pr.name) AS name_f, \
MIN(pr.category_id) AS category_id, MIN(pr.score) AS score, \
GROUP_CONCAT(tag.name) AS tags, \
UNIX_TIMESTAMP(MIN(pr.created_at)) AS created_at \
FROM product AS pr \
LEFT JOIN _product_tag AS pt ON pr.id = pt.product_id \
LEFT JOIN tag ON pt.tag_id = tag.id \
GROUP BY pr.id
sql_attr_string = name
sql_attr_uint = category_id
sql_attr_float = score
sql_attr_timestamp = created_at
}
index providers
{
source = sphinx_test
path = C:\Sphinx\data\sphinx_test
docinfo = extern
charset_type = utf-8
charset_table = 0..9, A..Z->a..z, a..z, -, \
U+00C1->U+00E1, U+00C9->U+00E9, U+00CD->U+00ED, \
U+00D3->U+00F3, U+00D6->U+00F6, U+0150->U+0151, \
U+00DA->U+00FA, U+00DC->U+00FC, U+0170->U+0171, \
U+00E1, U+00E9, U+00ED, U+00F3, U+00F6, U+0151, \
U+00FA, U+00FC, U+0171
min_word_len = 2
enable_star = 1
min_prefix_len = 2
}
indexer
{
mem_limit = 32M
}
# searchd options (used by search daemon)
searchd
{
listen = 9310
log = C:\Sphinx\data\searchd.log
query_log = C:\Sphinx\data\query.log
max_children = 30
pid_file = C:\Sphinx\data\searchd.pid
}
Mit is tartalmaz a konfiguráció?
Az első részben állítjuk be a forrás típusát (MySQL), valamint megadjuk a lekérdezést, ami előállítja az indexet. Definiáljuk az indexben szereplő mezőket, és azok típusát. A második szekcióban megadjuk az index nevét és karakterkódolását. A karekterkódolás mellett a karaktertáblát is felül kell definiálnunk, ha magyar ékezetes szövegben is szeretnénk keresni. Ezt követően beállítjuk az indexelő alkalmazás maximális memóriafoglalását. A nagyobb érték gyorsabb indexeléssel járhat nagyobb adatmennyiség esetén. Itt meg kell jegyeznem, hogy néhány tízezer rekord indexelésekor nem láttam különbséget az indexelés sebességében, ha jóval több memóriát adtam, mint az alapértelmezett 32 MB, tehát csak valóban sok rekord esetén érdemes ezen változtatni.
Nagyon fontos, hogy a lekérdezésben szereplő első érték (példánkban a pr.id
) egyedi azonosító legyen, ugyanis ez lesz a Sphinx által visszadott document ID! Ez az azonosító kizárólag pozitív egész szám lehet.
A config fájl utolsó részében tudjuk beállítani a portot, amin a démon fut, és a napló fájlok helyét. Üresen hagytam az SQL felhasználót és jelszót, azokat természetesen meg kell adni.
Nagyobb rekordszám esetén érdemes elkerülni a GROUP BY
-t az SQL lekérdezésben. Erre két megoldás is van: az sql_attr_multi és az sql_joined_field. Az előbbi a tag azonosítók tárolására hasznos, amik a SetFilter() használatával szűrhetőek, míg az utóbbi megoldás jóval rugalmasabb, gyakorlatilag ugyanazt az indexet tudjuk előállítani. Ezt választva, azonban szükségünk van egy view-ra, ami tárolja dokumentum ID-kat és a címkéket:
CREATE VIEW tags_view AS
SELECT pr.id, tag.name
FROM product AS pr
LEFT JOIN _product_tag AS pt ON pr.id = pt.product_id
LEFT JOIN tag ON pt.tag_id = tag.id
ORDER BY id
Ezután eképpen tudjuk módosítani a konfigurációs állományt:
sql_query = \
SELECT pr.id, pr.name, pr.name AS name_f, pr.category_id, pr.score,
UNIX_TIMESTAMP(pr.created_at) AS created_at \
FROM product AS pr \
ORDER BY id
sql_joined_field = tags from query; \
SELECT id, name FROM tags_view ORDER BY id
Az indexelés
Ha eddig megvagyunk, jöhet az index létrehozása:
C:\Sphinx\bin>indexer --config C:\Sphinx\conf\sphinx_test.conf –all
A példa magáért beszél: a config kapcsolóval megadhatjuk a konfigurációs állomány elérési útját, az all kapcsolóval pedig megadhatjuk, hogy config fájlban szereplő indexek közül melyik legyen újra létrehozva. Az all opcióval mindegyik index újra lesz építve.
A kereső démon futtatása
C:\Sphinx\bin>searchd --config C:\Sphinx\conf\sphinx_test.conf
Most már fut a kereső démon, jöhetnek a keresések! Több programnyelvhez is rendelkezésre áll Sphinx API; PHP, Ruby, Python és Java nyelvekhez egyaránt vannak példák. A Sphinx használatához PHP-n keresztül mindössze include-olni kell a sphinxapi.php-t, majd példányosítani kell a Sphinx klienst, és már jöhetnek is a keresések.
A konfigurációban definiált mezők közül mindegyikre full-text index kerül, kivéve azokat, amiket egyedileg integerként, stringként, floatként stb. definiáltunk az sql_attr segítségével. Jól látszik, hogy a termék nevét stringként is használjuk, és name_f-ként full-text indexszel is bekerül az indexbe. A string változatot rendezéskor tudjuk használni, míg a full-text változatot kereséskor, a kettő együtt nem megy, ezért kell ez a kis trükközés.
A keresések előtt még nézzük tovább, mit is jelentenek az egyes mezők a konfigurációs állományban. A name a termék neve, a category_id a termék kategória, a score a termék értékelése, a tags a címkéket jelöli, a „created_at” a rekord létrehozási dátuma.
A Sphinx keresések némileg eltérnek a MySQL keresésekről, főként az alábbiakban.
- A Sphinx csak a dokumentum azonosítókat adja vissza a teljes sorok helyett, így a keresések megjelenítéshez még külön lekérdezést kell futtatni.
- Nem kell külön
COUNT
lekérdezést futtatni, mert a lekérdezés eredményében szerepel az összes talált elem száma is. - A Sphinx saját lekérdező nyelvvel rendelkezik (SphinxQL), ami némileg hasonlít az SQL-hez.
- Alap esetben a Sphinx csak az első 1000 találatot adja vissza (a jobb teljesítmény érdekében), ami a max_matches megadásával felüldefiniálható a konfigurációban.
Jöjjenek végre a keresések!
<?php
header('Content-Type: text/html; charset=utf-8');
include('sphinxapi.php');
function display($result, $query) {
echo '<h2>'.$query.'</h2>';
echo '<h3>Találatok: '.$result['total_found'].'</h3>';
echo '<h3> Összes: '.$result['total'].'</h3>';
echo '<h3>Lekérdezés ideje: '.$result['time'].'s</h3>';
echo '<p>DOC id-k: ';
foreach ($result['matches'] as $item) {
echo $item['id'].' ';
}
echo '</p><hr />';
}
$client = new SphinxClient();
// kapcsolodas
$client->SetServer('127.0.0.1', 9310);
$client->SetConnectTimeout(1);
$client->SetArrayResult(true);
// eles kereseskor tobbnyire beallitjuk a limitet
$client->SetLimits(0, 10);
// a leheto legegyszerubb kereses
$result = $client->Query('');
display($result, "''");
// nev szerinti rendezes
$client->SetSortMode(SPH_SORT_ATTR_ASC, 'name');
$result = $client->Query('');
display($result, "''");
// a default match mode: SPH_MATCH_ALL
// hasznalataval az osszes full-text indexelt mezon lefut a kereses
$client->SetMatchMode(SPH_MATCH_ALL);
$result = $client->Query('macska');
display($result, 'macska');
// ha csak a cimkek kozott szeretnenk keresni, akkor mar a SPH_MATCH_EXTENDED egyezesi modra van szuksegunk
$client->SetMatchMode(SPH_MATCH_EXTENDED);
// a keresesi eredemenyeket a pontszam szerint rendezzuk (a nagyobb van elol)
$client->SetSortMode(SPH_SORT_ATTR_DESC, 'score');
// a cimkek kozott keresunk a macska szora
$result = $client->Query('@tags macska');
display($result, '@tags macska');
// osszetett kereseseket is futtathatunk, hasznalhatjuk a szokasos AND, OR es egyeb operatorokat
$result = $client->Query('@name_f macskatáp | @tags kutya');
display($result, '@name_f macskatáp | @tags kutya');
// ha nincs meghatarozva az operator akkor AND kapcsolat van kozottuk
$result = $client->Query('@name_f dog @tags kutya');
display($result, '@name_f dog @tags kutya');
$result = $client->Query('@tags táp');
// lehetoseg van egyszerre novekvo es csokkeno rendezest hasznalni
$client->SetSortMode(SPH_SORT_EXTENDED, 'score DESC, name ASC');
display($result, '@tags táp');
// lehetoseg van a relevancia(suly) szerinti rendezesre is
$result = $client->Query('@tags táp');
// az SPH_SORT_RELEVANCE gyakorlatilag egyenlo a "@weight DESC, @id ASC"-el
// a @relevance és a @rank a @weight szinonimai
$client->SetSortMode(SPH_SORT_RELEVANCE);
display($result, '@tags táp');
// lehetoseg van pontos idezetre is keresni, ehhez csak
// idezojelbe kell tenni a keresoszot
$result = $client->Query('@tags "kutya"');
display($result, '@tags "kutya"');
// kereshetunk szovegreszletre is, ehhez a config fajlban
// meg kell adnunk az enable_star es a min_prefix_len parametereket
$result = $client->Query('@tags "ku*"');
display($result, '@tags "ku*"');
// csak azokat listazza, amelyek az 1-es kategoriaba tartoznak
// a filter mindenkeppen tombot var masodik parameterkent
$client->SetFilter ('category_id', array(1));
$result = $client->Query('');
$client->SetSortMode(SPH_SORT_EXTENDED, 'score DESC, name ASC');
display($result, "''");
Remélem, sikerült megadnom a kezdőimpulzust a Sphinx használatához azok számára is, akik túl bonyolultnak gondolták az első lépéseket. Innentől már nincs más, mint használni a Sphinxet, és próbálgatni a lehetőségeit.
Ezúton köszönöm CoL-nak, hogy átnézte és lektorálta a cikket.
■
két kérdés
De két röpke kérdésem felmerült bennem:
-ez a 4-10-50-100-500x gyorsulás tapasztalható really? saját mérés vagy holnapon szereplő PR fogás?
-a keresődémon mennyire eszi a gépet?
A következőek tapasztalataim:
A terhelés terén igen pozitívak a tapasztalataim, látványosan visszaesett a load, mióta Sphinx-et használunk. A MySQL még úgy is több erőforrást használ (mind memóriában, mind CPU-ban), hogy a lekérdezések kb. 90%-át a Sphinx viszi (az oldalam a keresésekre van kihegyezve).
500x biztos nem, de 5-10x
indexer és searchd
rotate