Sphinx

Sphinx to otwarty, w pełni tekstowy serwer wyszukiwania stworzony w C++ i udostępniony na licencji GPLv2. Najczęściej jest wykorzystywany do indeksowania danych z baz MySQL, PostgreSQL i specjalnie sformatowanych plików xml.

Elementy Sphinxa

Sphinx składa się z następujących elementów:

  • indexer - narzędzie do tworzenia pełnotekstowych indeksów (indices);
  • searchd - demon do przeszukiwania indeksów przez zewnętrzne aplikacji (np. skrypty www komunikujące się przez API, MySQ z SphinxSE itp.), jest uruchomiony na serwerze;
  • sphinxapi - zbiór bibliotek udostępniających API Sphinxa dla PHP, Pythona, Javy, Perla, czy Rubiego;
  • spelldump - proste narzędzie do wydobywania pozycji ze słownika ispell lub MySpell służące do dostosowania indexu;
  • indextool - program narzędziowy do zrzucania rozmaitych;
  • wordbreaker - program służący do rozdzielania połączonych wyrazów na oddzielne.

Przykład

Poniżej znajduje się przykład indeksowania wiadomości.

Indeksowanie

Konfiguracja cz. 1

Dla wygodniejszej pracy zalecane jest stworzenie paru katalogów, na przykład:

  • ~/wyszukiwanie/dane/wiadomosci - wszystkie artykuły;
  • ~/wyszukiwanie/index/wiadomosci - wszystkie pliki stworzone podczas indeksowania;
  • ~/wyszukiwanie/conf - pliki konfiguracyjne;
  • ~/wyszukiwanie/bin/wiadomosci - skrypty potrzebne do indeksowania.

Plik konfiguracyjny ~/wyszukiwanie/conf/sphinx.conf:

source wiadomosci
{
    type                    = xmlpipe
    xmlpipe_command         = ~/wyszukiwanie/bin/wiadomosci/xmlout.sh
    xmlpipe_field           = content
    xmlpipe_attr_string     = url
    xmlpipe_attr_uint       = date
    xmlpipe_fixup_utf8      = 1s
}
  • source wiadomosci - definicja źródła indeksowania identyfikowana jako wiadomosci.
  • type = xmlpipe - definicja typu źródła indeksowania, tutaj to dokument XML, który jest przekazywany za pomocą pipe do indexera.
  • xmlpipe_command = ~/wyszukiwanie/bin/news/xmlout.sh - jest to komenda, która zostanie uruchomiana aby wypisała na standardowe wyjście wszystkie dokumenty XML jako jeden dokument.
Pola i atrybuty

Zawartość dokumentu, która potrzebuje być zindeksowana nazywana jest polem (field). Intuicyjnie dla wiadomości (newsów) cała treść artykułu jest polem.

Atrybut jest informacją powiązaną z artykułem.

Gdy jedno z wyszukiwań dopasuje szukane frazy do fraz zindeksowanych we wszystkich dokumentach to dla każdego dopasowanego artykułu zwracany jest sam dokument wraz z jego atrybutami.

xmlpipe_field           = content
xmlpipe_attr_string     = url
xmlpipe_attr_uint       = date

Powyższa zawartość określa:

  • Tekst w tagu XML content będzie traktowany jako pole (field), więc będzie indeksowane
  • Tekst w tagu XML url będzie traktowany jak atrybut tekstowy (string).
  • Tekst w tagu XML date będzie traktowany jak atrybut numeryczny (numerical).

Numeryczny atrybut daty wydaje się nieintuicyjny. Natomiast podczas szukania można zdefiniować nie tylko konkretne wartości atrybutów, ale i ich zakresy, które nie działają najlepiej z atrybutem tekstowym.

xmlpipe_fixup_utf8      = 1

Powyższa linijka sprawi, że indexer da sobie radę ze streamem XML w UTF-8.

XML

Poniżej są 2 przykładowe artykuły z następującą treścią:

Gmina Ostrowice w Zachodniopomorskiem może zostać zlikwidowana za długi. Jej zadłużenie trzykrotnie przewyższa roczne dochody.

i

Miasto Słupsk popadło w kłopoty finansowe, po przegranym procesie, który miastu wytoczył wyrzucony z budowy słupskiego aquaparku wykonawca. Miasto ma zapłacić firmie Termochem 24 miliony złotych.

Indexer będzie się spodziewał takiego streamu:

<?xml version="1.0" encoding="utf-8"?>
<sphinx:docset>
    <sphinx:document id="1">
        <url>url</url>
        <date>data</date>
        <content>Gmina Ostrowice w Zachodniopomorskiem może zostać zlikwidowana za długi. Jej zadłużenie trzykrotnie przewyższa roczne dochody.</content>
    </sphinx:document>
    <sphinx:document id="2">
        <url>url</url>
        <date>data</date>
        <content>Miasto Słupsk popadło w kłopoty finansowe, po przegranym procesie, który miastu wytoczył wyrzucony z budowy słupskiego aquaparku wykonawca. Miasto ma zapłacić firmie Termochem 24 miliony złotych.</content>
    </sphinx:document>
</sphinx:docset>

Rzeczy warte zauważenia:

  • każdy dokument musi mieć numeryczne id,
  • wszystkie tagi niezaczynające się od sphinx: zostały zdefiniowane w pliku konfiguracyjnym.

Kolejnym etapem jest stworzenie skryptu ~/wyszukiwanie/bin/wiadomosci/xmlout.sh, który wygeneruje powyższy XML. Poniżej znajduje się przykładowy:

#!/bin/bash
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
echo "<sphinx:docset>"
counter=0
for filename in `find ~/wyszukiwanie/dane/wiadomosci/ -type f`
do
   url=`head -n 2 $filename | tail -n 1`
   url=`echo $url | sed 's/&/%26/g' `
   date=`head -n 1 $filename | sed 's/\-//g'`
   counter=`expr $counter + 1`
   lines=`wc -l $filename | awk '{print $1}'`
   lines=`expr $lines - 2`
   echo "<sphinx:document id=\"$counter\">"
   echo "<url>$url</url>"
   echo "<date>$date</date>"
   echo -n "<content>"
   tail -n $lines $filename | sed 's/&/and/g'
   echo "</content>"
   echo "</sphinx:document>"
done
echo "</sphinx:docset>"
Konfiguracja cz. 2

Druga część konfiguracji w pliku ~/wyszukiwanie/conf/sphinx.conf:

index wiadomosci
{
    source                  = wiadomosci
    path                    = ~/wyszukiwanie/index/wiadomosci/idx
    docinfo                 = extern
    mlock                   = 0
    morphology              = stem_en, soundex
    min_word_len            = 1
    charset_type            = utf-8
    html_strip              = 1
}

Ta część odpowiada za definicję indeksowania wiadomości. Określa w jaki sposób indeks ma być stworzony, gdzie przetrzymywany itd.

  • source = wiadomosci - definicja źródła indeksowania. Źródło wiadomosci zostało określone w pierwszej części konfiguracji.
  • path = ~/wyszukiwanie/index/wiadomosci/idx - definicja ścieżki w której zostaną zapisane wszystkie pliki stworzone przez indexera z dodanym prefixem idx.

Konfiguracja indexera z maksymalnym limitem na pamięć 128MB:

indexer
{
    mem_limit               = 128M
}


Uruchamianie indeksowania

Poniższa komenda uruchomi indeksowanie

indexer --config ~/wyszukiwanie/conf/sphinx.conf --verbose wiadomosci

Co każde 1000 zindeksowanych dokumentów indexer wypisze powiadomienie. Po zakończeniu indeksowania zostaną wyświetlone jego statystyki.

Wyszukiwanie

Za wyszukiwanie odpowiada daemon searcgd. Poniżej znajduje się przykład konfiguracji oraz sposób jego uruchomienia.

Konfiguracja

Konfiguracja tak jak we wcześniejszych częściach znajduje się w pliku ~/wyszukiwanie/conf/sphinx.conf:

searchd
{
    listen                  = /home/login/wyszukiwanie/searchd.sock
    log                     = /home/login/wyszukiwanie/log/searchd.log
    query_log               = /home/login/wyszukiwanie/log/query.log
    read_timeout            = 5
    client_timeout          = 300
    max_children            = 30
    pid_file                = /home/login/wyszukiwanie/log/searchd.pid
    max_matches             = 1000
    seamless_rotate         = 1
    preopen_indexes         = 1
    unlink_old              = 1
    mva_updates_pool        = 1M
    max_packet_size         = 8M
    max_filters             = 256
    max_filter_values       = 4096
    max_batch_queries       = 32
    workers                 = 4
    dist_threads            = 4
}

Większość z opcji jest ustawiona standardowo i nie wymaga zrozumienia.

Uruchamianie daemona

Aby uruchomić daemon searchd należy skorzystać z polecenia:

searchd -c ~/wyszukiwanie/conf/sphinx.conf
Wyszukiwanie za pomocą PHP

Sphinx search ma bardzo dużo klientów, jednym z nich jest PHP API. Poniżej znajduje się przykład:

<?php header('Content-Type: text/plain; charset=iso-8859-1');
include('sphinxapi.php');
$cl = new SphinxClient();
$cl->SetServer("/home/login/wyszukiwanie/searchd.sock");
$cl->SetSortMode(SPH_SORT_RELEVANCE);
$results = (int)$_GET['results'];
$offset = (int)$_GET['offset'];
$cl->SetLimits($offset, $results);
$min_date = (int)$_GET['mindate'];
$max_date = (int)$_GET['maxdate'];
$cl->SetFilterRange('date', $min_date, $max_date);
if ($_GET['mode'] == 'All') {
        $cl->SetMatchMode(SPH_MATCH_ALL);
}
else if ($_GET['mode'] == 'Any') {
        $cl->SetMatchMode(SPH_MATCH_ANY);
}
else if ($_GET['mode'] == 'Phrase') {
        $cl->SetMatchMode(SPH_MATCH_PHRASE);
}
else if ($_GET['mode'] == 'Extended') {
        $cl->SetMatchMode(SPH_MATCH_EXTENDED);
}
$keywords = preg_replace("/&quot;/", "\"", $_GET['keywords']);
$result = $cl->Query( $keywords, $_GET['index'] );
if ( $result === false ) {
    echo "ERROR|Query failed: " . $cl->GetLastError() . "\n";
}
else {
    if ( $cl->GetLastWarning() ) {
        echo "WARNING|" . $cl->GetLastWarning() . "\n";
    }
    if ( ! empty($result['matches']) ) {
        print_r($result['matches']);
    }
}
?>

i przykładowe użycie za pomocą curl:

curl "http://login.usermd.net/sphinx/search.php?keywords=Anna+Team&results=20&offset=0&mindate=20090101&maxdate=20140806&mode=Phrase&index=news"

Odnośniki Zewnętrzne