http://www.sphinxsearch.com Search Engine, die – wie sie sich selber nennt – „Free open-source SQL full-text search engine“ ist ein gutes tool, um die Volltextsuche auf dem eigenen zu beschleunigen. Der Ansatz ist gut, die Ergebnisse noch besser, aber die Installation ein langer Leidensweg… Da dies öfters bei Open Source Projekten der Fall ist, muss man wohl oder über nach Anleitungen Ausschau halten, um genau das nicht selber durchmachen zu müssen.
Eine solche Anleitung soll dieser Artikel sein.

In Max‘ Tagebuch fand ich eine gute Hilfe, um das Grundproblem zu knacken: Wie komme ich an die binaries (fertig kompillierte Programme)…

Leider währte meine Freude nur kurz: die Beschreibung stimmt mittlerweile nicht mehr ganz. Und eine Erklärung, wie man die Konfig-Datei einrichtet war sowieso nicht dabei! Deshalb fangen wir mal bei 0 (NULL) an!

Von der Download-Seite von besorgen wir aus der Abteilung
„Source Downloads“ „Compressed GNU TAR archive (tar.gz)“
z.B. per:

wget ftp://sunsite.informatik.rwth-achen.de/pub/mirror/www.mysql.com/Downloads/MySQL-5.0/mysql-5.0.41.tar.gz
und natürlich sphinx:
wget http://www.sphinxsearch.com/downloads/sphinx-0.9.7.tar.gz

Natürlich können die Versionen variieren und die Patches von sphinx können für die neuste Version von MySQL nicht anwendbar sein – hier hilft nur die Festlegung auf die hier beschriebene Versionen – die funktionieren zumindest…

Mit

tar xzf sphinx-0.9.7.tar.gz && tar xzf mysql-5.0.41.tar.gz
entpacken wir die beiden und schreiten zur Tat:

Wir installieren notwendige development Packete:

apt-get install libmysqlclient15-dev libtool debhelper
libncurses5-dev libwrap0-dev zlib1g-dev libreadline-dev
chrpath automake1.8 automake1.9 doxygen dpatch procps
file perl psmisc po-debconf tetex-bin tetex-base tetex-extra gs gawk bison

bei Debian und seinen Kindern
bzw.

„yum install mysqlclient14-devel mysql-devel doxygen gcc automake readline-devel byacc libtool bison“

bei den roten Hüten und Ähnlichen…

Jetzt müssten wir alles nötige für eine erfolgreiche Kompilation haben. Wenn nicht, kann nur die Suche nach fehlenden libraries in /usr/include/ helfen… Vorsicht ist bei einigen älteren Distributionen geboten: g++ und gcc wird in Version 4 (bzw 4.1) verlangt. Sollte es auch dann nicht funktionieren und ein „–preserve-dup-deps: command not found“ erscheint, muss man den Pfad zu libtool setzten (mysql hat libtool im eigenen Quellen-Verzeichnis drin, es wird bei ./configfure-Aufruf erstellt), es reicht in configure im MySQL-Quellen-Verzeichnis die Zeile „LIBTOOL=“ –preserve-dup-deps“ “ mit „LIBTOOL=“/pfad/zu/mysql-quelle-5.0.xx/libtool –preserve-dup-deps“ “ zu ersetzten oder noch einfacher: „LIBTOOL=“/pfad/zu/mysql-quelle-5.0.xxx/libtool“ in der Shell abzusetzten.)

Jetzt kompilieren wir Sphinx

cd ../sphinx-0.9.7 (da wären wir im Sphinx Quellen-Verzeichnis)

./configure --prefix=/usr --with-mysql wird Alles für die Nutzung von sphinx (mit MySQL-Anbindung) vorbereiten. Die Binaries landen nach der Installation in /usr/bin/ /usr/lib statt in /usr/local/bin und /usr/local/lib.

Etwas sinnlos landet die Beispiel-Konfigurationsdatei sphinx.conf.dist in /usr/etc. Nicht weiter schlimm! Wir verschieben sie einfach:
„mv /usr/etc/sphinx.conf.dist /etc“

Jetzt noch ein startscript für den searchd (search daemon). Er bedient den start-, restart- und stop-Aufruf in redhat und debian Systemen (überarbeitete Version von Max‘ Tagebuch Blog):

/etc/init.d/searchd

#! /bin/sh
### BEGIN INIT INFO
# Provides: searchd
# Required-Start: $local_fs $remote_fs
# Required-Stop: $local_fs $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: S 0 1 6
# Short-Description: Example initscript
# Description: This file should be used to construct scripts to be
# placed in /etc/init.d.
### END INIT INFO

#
# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/usr/sbin:/usr/bin:/sbin:/bin
DESC="Sphinx Index Query Daemon"
NAME=searchd
DAEMON=/usr/bin/$NAME
INDEXER=/usr/bin/indexer
DAEMON_ARGS="-- /etc/sphinx.conf"
PIDFILE=/var/run/searchd.pid
SCRIPTNAME=/etc/init.d/$NAME
initdir=/etc/init.d
system=unknown

if [ -f /etc/debian_version ]; then
system=debian
elif [ -f /etc/redhat-release ]; then
system=redhat
else
echo "$0: Unknown system, please port" 1>&2
exit 1
fi

if [ $system = redhat ]; then
. $initdir/functions
fi

if [ $system = suse ]; then
. /etc/rc.status
fi

[ -x "$DAEMON" ] || exit 0

[ -r /etc/default/$NAME ] && . /etc/default/$NAME

[ -f /etc/default/rcS ] && . /etc/default/rcS

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions

#
# Function that starts the daemon/service
#
do_start()
{
if [ $system = debian ]; then
# Return
# 0 if daemon has been started
# 1 if daemon was already running
# 2 if daemon could not be started
start-stop-daemon --start --quiet --pidfile $PIDFILE
--exec $DAEMON --test > /dev/null
|| return 1
#Run Indexer before rest
$INDEXER $DAEMON_ARGS --all
#Start daemon
start-stop-daemon --start --quiet --pidfile $PIDFILE
--exec $DAEMON -- $DAEMON_ARGS
|| return 2
# Add code here, if necessary, that waits for the process to be ready
# to handle requests from services started subsequently which depend
# on this one. As a last resort, sleep for some time.
fi
if [ $system = redhat ]; then
echo -n "Starting searchd"
daemon $DAEMON $DAEMON_ARGS
fi
}

#
# Function that stops the daemon/service
#
do_stop()
{
if [ $system = debian ]; then
# Return
# 0 if daemon has been stopped
# 1 if daemon was already stopped
# 2 if daemon could not be stopped
# other if a failure occurred
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5
--pidfile $PIDFILE --name $NAME
RETVAL="$?"
[ "$RETVAL" = 2 ] && return 2
# Wait for children to finish too if this is a daemon that forks
# and if the daemon is only ever run from this initscript.
# If the above conditions are not satisfied then add some other code
# that waits for the process to drop all resources that could be
# needed by services started subsequently. A last resort is to
# sleep for some time.
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5
--exec $DAEMON
[ "$?" = 2 ] && return 2
# Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE
return "$RETVAL"
fi
if [ $system = redhat ]; then
echo -n "Stopping searchd"
#daemon $DAEMON $DAEMON_ARGS
#PID=
#PID=$(cat $PIDFILE)
#kill $PID
killproc $NAME
echo
rm -f $PIDFILE
return $RETVAL
fi
}

case "$1" in
start)
do_start
;;
stop)
do_stop
;;
restart|force-reload)

if [ $system = debian ]; then
do_stop
do_start
fi

if [ $system = debian ]; then
log_daemon_msg "Restarting $DESC" "$NAME"
do_stop
case "$?" in
0|1)
do_start
case "$?" in
0) log_end_msg 0 ;;
1) log_end_msg 1 ;; # Old process is still running
esac
;;
*)
# Failed to stop
log_end_msg 1
;;
esac
fi
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
exit 3
;;
esac

:

Konfiguration von Sphinx

Wir editieren: /etc/sphinx.conf.dist und speichern als /etc/sphinx.conf !!!

/etc/sphinx.conf

# die Datenquelle
source source_CONTENT
{
# Typ der Datenquelle: 'mysql', 'pgsql' oder 'xmlpipe'. Wir nehmen mysql
type = mysql

# HTML-Tags entfernen? Ja
strip_html = 1

# Welche HTML-Attribute beim entfernen von Tags indexieren?
# ALT ,TITLE bei IMG und TITLE bei A, kann aber auch leer bleiben
index_html_attrs = img=alt,title; a=title;

# Einstellungen für die Datenbank-Verbindung
sql_host = localhost
sql_user = root
sql_pass =
sql_db = myDB
sql_port = 3306 # optional, default is 3306

# Die Tabelle, die wir lesen wollen heißt CONTENT und enthält als BEISPIEL Inhalt eines
# CMS-Systems mit ID, Titel, Inhalt, Datum und Kategorie
# Wenn unsere zu indexierende Datenquelle sehr groß ist (>1000), empfielt es sich,
# die Daten sequenziell zu lesen. Dazu legt man in der angegebenen DB eine Tabelle namens
# counters mit:
# CREATE TABLE `counters` (
# `id` int(11) NOT NULL,
# `val` int(11) NOT NULL,
# PRIMARY KEY (`id`)
# );
# Es legt die Start-ID und End-ID der zu lesenden Datensätze (dabei wird einfach aus der
# Tabelle CONTENT die kleinste und die höchste ID kopiert)
# Brauchst Du kein sequenzielles Lesen gehe weiter zu " sql_query"
sql_query_pre = REPLACE INTO counters SELECT 1, MAX(ID) FROM CONTENT

# main document fetch query
#
# die ID muss >0, max 32 Bit und eindeutig (unique) sein
#
# (man kann zusätzlich sog. Attribute anlegen, die gleichen Anforderungen bis auf unique
# unterliegen dazu später)
#
# Jetzt wird die Länge einer Sequenz festgelegt: 1 Schritt = 1000 Sätze
sql_query_range = SELECT MIN(ID),MAX(ID) FROM CONTENT_DE
sql_range_step = 1000

# Jetzt lesen wir die zu indexierenden Datensätze
sql_query =
SELECT
ID
AUTOR,
TITLE,
ARTICLE,
UNIX_TIMESTAMP(LAST_UPDATE) as LAST_UPDATE,
CATEGORY
FROM CONTENT
WHERE ID >=$start AND ID siehe unten "docinfo")
# Jetzt kommt die SQL-Anfrage, die zum Lesen einzelner Artikel dient.
# Dieser muss anhand von ID (siehe oben) gefunden werden können!
sql_query_info = SELECT * FROM CONTENT WHERE ID=$id
}

# Datenquelle haben wir, jetzt kommt die Definition des Indexes
index index_CONTENT
{
# Aus welcher Quelle? Aus source_CONTENT (oben definiert)
source = source_CONTENT

# tWohin mit den Dateien? Nach /var/lib/sphinx/INDEXNAME (Verzeichnis muss existieren!)
path = /var/lib/sphinx/index_CONTENT

# docinfo
# Drei Möglichkeiten: are "none", "inline" and "extern"
#
# "none" keine Informationen zum Artikel werden gespeichert
#
# "inline" Infos landen in doclist Datei (nur über 50 Mil. Atikel sinnvoll)
#
# "extern" Infos landen in getrennter Datei
#
# Wohin mit DocInfos? "extern", weil unter 50 000 000 Artikel!
docinfo = extern

# Morphologie: "none", "stem_en", "stem_ru", "stem_enru", "soundex"
# (Ähnlichkeit anhand von Endungen: leider nur englisch und russisch)
# Beis soundex wird die Aussprache beachtet: google und googl sind gleich
# none -> Keine Ähnlichkeiten suchen
# Für Wissenschaftler emphele ich Artikel über Indexierung von Prof. Zimmermann
morphology = none

# Pfad zur Stoppwort-Datei (datei mit Wörtern,
# die nicht indexiert werden sollen wie: "und", "der", "die", "das", "ein" )
#
stopwords = /var/lib/sphinx/stopwords.txt

# Länge des kürzesten Wortes? 2 Buchstaben (Wegen möglicher Abkürzungen "AU", "TÜV")
min_word_len = 2

# charset encoding ? Bei uns Latin1 also Single Byte -> SBCS
charset_type = sbcs

# 'sbcs' default value is
# charset_table = 0..9, A..Z->a..z, _, a..z, U+A8->U+B8, U+B8, U+C0..U+DF->U+E0..U+FF, U+E0..U+FF
#
# 'utf-8' default value is
# charset_table = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F

# Wie lang sind die zu indexierenden Prefixe? Bei und 0 -> gar nicht indexieren
min_prefix_len = 0

# Länge der Infixe? (1 für deutsche Komposita mir "s" als Verbindung)
min_infix_len = 1
}

# Verteilte Indexierung (auf mehreren Maschinen) - brauchen wir nicht
#index dist1
# {
# type = distributed
# lokal haben wir index-CONTENT
# local = index_CONTENT
# Auf anderen Maschinen
# Syntax: 'hostname:port:index1,[index2[,...]]
# BEISPIEL:
# agent = localhost:3313:index_CONTENT
# agent_connect_timeout = 1000
# agent_query_timeout = 3000
# }

#Einstell. für das Programm indexer

indexer
{
# Speicherlimit: hier 64 MB
mem_limit = 64M
}

#Einstell. für den search-daemon

searchd
{
# Adresse IP, auf der er lauschen soll? hier nur lokal
address = 127.0.0.1
# Port zum lauschen? standard -> 3312
port = 3312
# Pfad zur Log-Datei? (Verzeichnis muss existieren!)
log = /var/log/searchd.log
# Pfad zur Log-Datei für Anfragen (Verzeichnis muss existieren!)
query_log = /var/log/query.log
# Timeout in Sekunden 5
read_timeout = 5
# Maximale Zahl an Kinder-Prozessen? 30 ist OK
max_children = 30
# Pfad zur Datei mit PID (Verzeichnis muss existieren!)
pid_file = /var/run/searchd.pid
# Wie viel Antworten maximal können ansgeliefert werden? 5000
max_matches = 5000
}

Den Betrieb aufnehmen…

Wir gehen ins /etc (blöder Weise muss das sein) und starten „indexer INDEXNAME“ (unser Beispiel: indexer index_CONTENT). Das Programm indexer kam mit sphinx und ist für das Erstellen des Indexes zuständig. Sollte es hier Probleme geben, sind 2 Fragen zu Beantworten: Bin ich in /etc? (indexer versucht sphinx.conf im aktuellen Verzeichnis zu lesen d.h. dort wo man grade ist -> „pwd“ hilft weiter) Stimmt die Konfiguration? (Stimmen die Pfade? Existieren die Verzeichnisse? Ist MySQL erreichbar? Sind Zeilenumbrüche geschützt mit „“? Sind die SQL-Statements OK?)

Jetzt nur noch des Seach-Daemon mit /etc/init.d/searchd start starten und wir müssten jetzt suchen können: search -c /etc/sphinx.conf -i INDEXNAME Suchwort oder search -i INDEXNAME Suchwort – wenn man in /etc ist (da sphinx.conf im aktuellen Verzeichnis gesucht wird).

Für das Reindexieren (Erneuern von Indexes) braucht man ein Paar Buchstaben mehr: indexer --rotate INDEXNAME. Das teilt dem Search-Daemon anschließend mit, dass der Index sich verändert hat und neu eingelesen werden sollte…

Sphinx als Engine in MySQL

cd mysql-5.0.41
mit:
patch -p1 < ../sphinx-0.9.7/mysqlse/sphinx.5.0.37.diff
patchen wir die mysql-Quellen. Sollten hier fehler eintreten, ist mächtig was schief gelaufen oder – was viel wahrscheinlicher ist – Du verwendest andere Versionen als ich.

Jetzt die configurieren wir mysql für die Verwendung mit SPHINX als Datenbanktyp:

./configure
--prefix=/usr
--exec-prefix=/usr
--libexecdir=/usr/sbin
--datadir=/usr/share
--localstatedir=/var/lib/mysql
--includedir=/usr/include
--infodir=/usr/share/info
--mandir=/usr/share/man
--enable-shared
--enable-static
--enable-thread-safe-client
--enable-local-infile
--with-big-tables
--with-raid
--with-unix-socket-path=/var/run/mysqld/mysqld.sock
--with-mysqld-user=mysql
--with-libwrap
--with-vio
--without-openssl
--with-yassl
--without-docs
--with-bench
--without-readline
--with-extra-charsets=all
--with-innodb
--with-sphinx-storage-engine
--with-isam
--with-archive-storage-engine
--with-csv-storage-engine
--with-federated-storage-engine
--without-embedded-server
--with-ndbcluster
--with-ndb-shm
--without-ndb-sci
--without-ndb-test
--with-embedded-server
--with-embedded-privilege-control
--with-ndb-docs

Wichtig ist die Zeile: --with-sphinx-storage-engine, die SPHINX als storage engine mitkompiliert. Die Binaries landen – wie gewünscht – in /usr/lib und /usr/bin. Manchmal – wie auf einem CentOS – steht auch das --without-readline im Wege. --with-readline behebt das Problem.

Bevor wir zum make schreiten nur noch eins:
cp -r ../sphinx-0.9.7/mysqlse sql/sphinx

Für gcc 4.1 muss man noch in sql/sphinx/ha_sphinx.cc folgendes ersetzen:
CSphSEFilter::CSphSEFilter ()" mit "CSphSEFilter ()
CSphSEFilter::~CSphSEFilter ()" mit "~CSphSEFilter ()

Jetzt kommen die Macher dran: make

(Sollte es Probleme mit sql_yacc.cc geben, muss man etwas tricksen: sql/Makefile öffnen und „-d –debug –verbose“ in „–debug –verbose“ ändern. Oder ist bison nicht installiert?)

Jetzt müssten die binaries fertig sein… Wir stoppen den mysql-Dienst – sofern vorhanden und läuft – mit /etc/init.d/mysql stop und rufen „make install“ auf. Jetzt können wir die neue Version von mysql starten (mysql-Dienst starten z.B mit /etc/init.d/mysql start). Wir loggen uns in mysql ein (mysql -u Benutzername -p) und setzen show engines; ab. In der Liste sollte SPHINX schon drin stehen.
| SPHINX | YES | Sphinx storage engine 0.9.7 |
Wir haben alles richtig gemacht und können uns schon mal darüber freuen… :-)

Jetzt können wir die Pseudo-Tabelle erstellen, die eine Verbindung zum Seach-Daemon erstellt. Sie heißt als Beispiel „t1“

CREATE TABLE t1 (
id INTEGER NOT NULL,
weight INTEGER NOT NULL,
query VARCHAR(3072) NOT NULL,
group_id INTEGER, INDEX(query)
)
ENGINE=SPHINX CONNECTION="sphinx://localhost:3312/INDEXNAME";

Mit:

SELECT * FROM t1 WHERE query='test it;mode=any';kann man des Search-Daemon über MySQL abfragen! Keine API mehr Notwendig! Nur Anbindung an MySQL muss funktionieren. Wie man die Anfragen Stellt, muss man jedoch selber in der Sphinx Dokumentation studieren… Ich sage nur: Alles kommt in den query=“….“-String!