ugrás a tartalomhoz

Bevezetés a Sphinx keresőmotor használatába

_subi_ · 2011. Jún. 6. (H), 10.27

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.

  1. Létrehozunk néhány minta táblát az adatbázisban.
  2. 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.)
  3. Létre kell hozni egy index állományt az indexer segítségével.
  4. El kell indítani a searchd-t, ami majd a kereséseket végzi.
  5. 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:

  1. CREATE TABLE IF NOT EXISTS `product` (  
  2.     `id` int(11) unsigned NOT NULL,  
  3.     `namevarchar(255) NOT NULL,  
  4.     `category_id` int(11) unsigned NOT NULL,  
  5.     `score` float DEFAULT 0,  
  6.     `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
  8.    
  9. CREATE TABLE IF NOT EXISTS `tag` (  
  10.     `id` int(11) unsigned NOT NULL,  
  11.     `namevarchar(255) NOT NULL  
  12. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
  13.    
  14. CREATE TABLE IF NOT EXISTS `_product_tag` (  
  15.     `product_id` int(11) unsigned NOT NULL,  
  16.     `tag_id` int(11) unsigned NOT NULL  
  17. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
  18.   
  19. INSERT INTO product (id, name, category_id, score) VALUE  
  20. (1, 'Marhahúsos kutyatáp', 1, 5.2),  
  21. (2, 'Marhahúsos macskatáp', 2, 7.1),  
  22. (3, 'Vitamin tabletta kutyának', 1, 5.1),  
  23. (4, 'Dog Stop távoltartó spray', 1, 3.2),  
  24. (5, 'Gyógysampon kutyának', 1, 5),  
  25. (6, 'Csonterősítő tabletta kutyának', 1, 4.9),  
  26. (7, 'Alomlapát', 2, 6.6),  
  27. (8, 'Vitamin tabletta macskának', 2, 7.2),  
  28. (9, 'Játék labda', 2, 7.7),  
  29. (10, 'Őrölt száraztáp', 1, 9.4);  
  30.    
  31. INSERT INTO tag (id, nameVALUES  
  32. (1, 'táp'),  
  33. (2, 'vitamin'),  
  34. (3, 'játék'),  
  35. (4, 'kutya'),  
  36. (5, 'macska');  
  37.    
  38. INSERT INTO _product_tag (product_id, tag_id) VALUES  
  39. (1,1),  
  40. (1,4),  
  41. (2,1),  
  42. (2,5),  
  43. (3,2),  
  44. (3,4),  
  45. (4,4),  
  46. (5,4),  
  47. (6,4),  
  48. (7,5),  
  49. (8,2),  
  50. (8,5),  
  51. (10,1),  
  52. (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:

  1. CREATE VIEW tags_view AS  
  2. SELECT pr.id, tag.name  
  3. FROM product AS pr  
  4. LEFT JOIN _product_tag AS pt ON pr.id = pt.product_id  
  5. LEFT JOIN tag ON pt.tag_id = tag.id  
  6. ORDER BY id  

Ezután eképpen tudjuk módosítani a konfigurációs állományt:

  1. sql_query   = \  
  2.             SELECT pr.id, pr.name, pr.name AS name_f, pr.category_id, pr.score,   
  3.             UNIX_TIMESTAMP(pr.created_at) AS created_at \  
  4.             FROM product AS pr \  
  5.             ORDER BY id  
  6.    
  7. sql_joined_field = tags from query; \  
  8.     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!

  1. <?php  
  2. header('Content-Type: text/html; charset=utf-8');  
  3. include('sphinxapi.php');  
  4.    
  5. function display($result$query) {  
  6.     echo '<h2>'.$query.'</h2>';  
  7.     echo '<h3>Találatok: '.$result['total_found'].'</h3>';  
  8.     echo '<h3> Összes: '.$result['total'].'</h3>';  
  9.     echo '<h3>Lekérdezés ideje: '.$result['time'].'s</h3>';  
  10.        
  11.     echo '<p>DOC id-k: ';  
  12.     foreach ($result['matches'as $item) {  
  13.      echo $item['id'].' ';  
  14.     }  
  15.     echo '</p><hr />';  
  16. }  
  17.    
  18. $client = new SphinxClient();  
  19.    
  20. // kapcsolodas  
  21. $client->SetServer('127.0.0.1', 9310);  
  22. $client->SetConnectTimeout(1);  
  23. $client->SetArrayResult(true);  
  24.    
  25. // eles kereseskor tobbnyire beallitjuk a limitet  
  26. $client->SetLimits(0, 10);  
  27.    
  28. // a leheto legegyszerubb kereses  
  29. $result = $client->Query('');  
  30. display($result"''");  
  31.    
  32. // nev szerinti rendezes  
  33. $client->SetSortMode(SPH_SORT_ATTR_ASC, 'name');  
  34. $result = $client->Query('');  
  35. display($result"''");  
  36.     
  37. // a default match mode: SPH_MATCH_ALL  
  38. // hasznalataval az osszes full-text indexelt mezon lefut a kereses  
  39. $client->SetMatchMode(SPH_MATCH_ALL);  
  40. $result = $client->Query('macska');  
  41. display($result'macska');  
  42.    
  43. // ha csak a cimkek kozott szeretnenk keresni, akkor mar a SPH_MATCH_EXTENDED egyezesi modra van szuksegunk  
  44. $client->SetMatchMode(SPH_MATCH_EXTENDED);  
  45.   
  46. // a keresesi eredemenyeket a pontszam szerint rendezzuk (a nagyobb van elol)  
  47. $client->SetSortMode(SPH_SORT_ATTR_DESC, 'score');  
  48.    
  49. // a cimkek kozott keresunk a macska szora  
  50. $result = $client->Query('@tags macska');  
  51. display($result'@tags macska');  
  52.    
  53. // osszetett kereseseket is futtathatunk, hasznalhatjuk a szokasos AND, OR es egyeb operatorokat  
  54. $result = $client->Query('@name_f macskatáp | @tags kutya');  
  55. display($result'@name_f macskatáp | @tags kutya');  
  56.    
  57. // ha nincs meghatarozva az operator akkor AND kapcsolat van kozottuk  
  58. $result = $client->Query('@name_f dog @tags kutya');  
  59. display($result'@name_f dog @tags kutya');  
  60.    
  61. $result = $client->Query('@tags táp');  
  62. // lehetoseg van egyszerre novekvo es csokkeno rendezest hasznalni  
  63. $client->SetSortMode(SPH_SORT_EXTENDED, 'score DESC, name ASC');  
  64. display($result'@tags táp');  
  65.    
  66. // lehetoseg van a relevancia(suly) szerinti rendezesre is  
  67. $result = $client->Query('@tags táp');  
  68. // az SPH_SORT_RELEVANCE gyakorlatilag egyenlo a "@weight DESC, @id ASC"-el  
  69. // a @relevance és a @rank a @weight szinonimai  
  70. $client->SetSortMode(SPH_SORT_RELEVANCE);  
  71. display($result'@tags táp');  
  72.    
  73. // lehetoseg van pontos idezetre is keresni, ehhez csak  
  74. // idezojelbe kell tenni a keresoszot  
  75. $result = $client->Query('@tags "kutya"');  
  76. display($result'@tags "kutya"');  
  77.    
  78. // kereshetunk szovegreszletre is, ehhez a config fajlban  
  79. // meg kell adnunk az enable_star es a min_prefix_len parametereket  
  80. $result = $client->Query('@tags "ku*"');  
  81. display($result'@tags "ku*"');  
  82.    
  83. // csak azokat listazza, amelyek az 1-es kategoriaba tartoznak  
  84. // a filter mindenkeppen tombot var masodik parameterkent  
  85. $client->SetFilter ('category_id'array(1));  
  86. $result = $client->Query('');  
  87. $client->SetSortMode(SPH_SORT_EXTENDED, 'score DESC, name ASC');  
  88. 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.

 
1

két kérdés

solkprog · 2011. Jún. 6. (H), 20.50
Először is köszönöm(jük) a cikket!
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?
2

A következőek tapasztalataim:

_subi_ · 2011. Jún. 6. (H), 22.27
Vannak olyan lekérdezéseim, ahol 20x gyorsabb a Sphinx, máshol "csak" 5x, bár hozzá kell tennem, hogy a MyISAM full-text lekérdezéseim elég alaposan optimalizálva voltak, ha nem lett volna cache táblám, hanem közvetlenül az InnoDB táblákban kerestem volna join-okkal és egyebekkel, akkor sokkal durvább lehetne a különbség.

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).
3

500x biztos nem, de 5-10x

gphilip · 2011. Jún. 7. (K), 01.53
500x biztos nem, de 5-10x reálisan tapasztalható, persze sokmindentől függ :) Az biztos, hogy rengeteg terhet levesz az RDBMS-ed válláról.
4

indexer és searchd

Ifju · 2011. Jún. 10. (P), 11.37
Mi történik akkor, amikor futó searchd mellett újra elindítod az indexelést, hogy az adatbázisban történt változásokról a Sphinx is tudjon? Az érdekelne, hogy az újraindexelés közben a keresés mennyire marad használható?
5

rotate

_subi_ · 2011. Jún. 10. (P), 17.14
Ha használod a rotate kapcsolót indexelésnél, akkor a searchd futását nem kell megszakítani, igaz így nagyobb lesz az indexelés memóriafoglalása. Bár ennek csak tényleg nagy adatmennyiség esetén van jelentősége.