Nadel im Heuhaufen (Linux-Magazin, November 97)

Wie arbeiten AltaVista, Hotbot, Yahoo und Konsorten? Sie hetzen Herden von Robots, Spiders und ähnlichem Getier 24 Stunden am Tag quer durchs Internet und schnappen HTML-Seiten. Daß ein Search-Engine jedoch auf die Frage Welche Dokumente enthalten die Daten, die ich suche? kluge Antworten geben kann, erfordert mehr: Ausgefeilte Software muß die Dokumente analysieren und eine Schlagwort-Datenbank aufbauen. Auf eine Suchanfrage (Query) hin spuckt ein derartiges System die Namen der Dokumente aus, die die verlangten Schlagworte möglichst oft enthalten und - so jedenfalls die Theorie - inhaltlich am ehesten dem Gesuchten entsprechen.

Privater Search-Engine

Auch wenn man nicht gerade den Ehrgeiz hat, das gesamte Internet zu durchstöbern, sondern nur eine größere Website mit einer intelligenten Suchfunktion ausstatten will, steht man früher oder später vor dem Problem der Indizierung. Zum Glück gibt's hierfür bereits funktionierende Software! Und sogar kostenlos!

freeWAIS-sf basiert auf einem Produkt der (mittlerweile nicht mehr existierenden) Firma WAIS zur Indizierung von frei formatierten Texten bzw. der Stichwortsuche in denselben. freeWAIS-sf läuft unter Linux und anderen gängigen Unix-Plattformen und ist ruck-zuck installiert (siehe Kasten: freeWAIS-sf: Installation und Indizierung).

Lauscht der neue WAIS-Server einmal auf dem eingestellten Port, dürfen Clients wie das nachfolgend vorgestellte CGI-Skript search.pl Suchanfragen stellen und bekommen passende Dokumente als Antwort.

In Aktion

Abbildung 1 zeigt search.pl in Aktion. Es liegt im cgi-bin des Webservers und gibt gerade darüber Auskunft, welche Dokumente auf meiner lokalen Test-Website die Begriffe muen* (könnte auf die bayrische Landeshauptstadt passen) und san francisco enthalten, und, siehe da, das einzig passende ist - mein Lebenslauf, der, relativ zum Webserver im Unterverzeichnis webpage hängt. Selbstverständlich fördert ein Mausklick auf den angezeigten Namen das entsprechende Dokument zutage.

Abb.1: Stichwortsuche mit dem CGI-Skript

Der WAIS-Engine versteht - und das ist der Vorteil gegenüber einfachen Suchprogrammen wie grep - auch komplexere Suchbegriffe wie

    a AND b    # Dokumente, die 'a' und 'b' enthalten
    a OR b     # Dokumente, die 'a' oder 'b' enthalten
    a NOT b    # Dokumente, die 'a', aber nicht 'b' enthalten
    a b        # Dokumente, die 'a' oder 'b' enthalten
    "a b"      # Dokumente, die die Zeichenkette "a b" enthalten
    a*         # Dokumente, die Wörter enthalten, die mit 'a' beginnen

und liefert wirklich nur die Dokumente, deren Inhalt allen Bedingungen genügt.

Die Perl-Schnittstelle Wais.pm von Ulrich Pfeifer, vom CPAN geholt und installiert (siehe Kasten Wais.pm installieren), bietet derart komfortablen High-Level Zugriff auf den WAIS-Server, daß search.pl mit nur 77 Zeilen Code auskommt. Es nimmt einen Query-String vom Benutzer entgegen, sendet ihn an den WAIS-Server, erhält von dort die Namen passender Dokumente und zeigt diese anschließend als HTML-Hyperlinks an.

So wird's gemacht

Wie aus Listing 1 hervorgeht, greift sich search.pl in Zeile 6 das für CGI-Skripts unverzichtbare CGI.pm (kommt mit perl5.004) und importiert auch gleich - entgegen meiner sonstigen Maxime - die wichtigsten Funktionen. Dies hat zur Folge, dass auch ohne ein explizit erzeugtes CGI-Objekt Funktionen wie header() (gibt den HTTP-Header aus) oder h1() (schreibt eine HTML-Überschrift) zur Verfügung stehen.

Zeile 7 holt das frisch installierte Wais.pm-Modul herein. Die Zeilen 12 bis 15 definieren Parameterwerte, die an lokale Gegebenheiten anzupassen sind. Da der WAIS-Server Zugriffspfade von Dokumenten entsprechend dem File-System spezifiziert, muß das Skript zur Umwandlung von lokalen File-System-Pfaden in globale URLs wissen, wo das Dokumentenverzeichnis des HTTP-Servers relativ zur Unix-Wurzel liegt ($htdocs_path). $hostname ist der Name des Rechners, auf dem der WAIS-Server läuft, $port der zugehörige Port, $dbname der Name der Datenbank - alles Parameter, die bei der Installation bzw. der Indizierung festgelegt werden.

Zeile 18 ruft die Funktion print_form() auf, die ab Zeile 59 den HTTP-Header sendet und das Eingabeformular in den Browser zaubert. Liegt, wie beim ersten Aufruf von search.pl, keine Query-Eingabe vor, beendet Zeile 20 das Skript ab. Trägt der Benutzer dann den Suchstring in das angezeigte Textfeld ein und drückt die Eingabetaste, startet search.pl erneut - und diesmal liefert param("query") den Query entsprechend dem Inhalt des Textfeldes.

Die Funktion Wais::Search in Zeile 23 nimmt Kontakt zum WAIS-Server auf, der wiederum seine Datenbank durchsucht und das Ergebnis wieder über die aufgebaute TCP-Verbindung zurückschickt. search.pl sieht nichts von alledem - nur $result, ein Objekt der Klasse Wais::Result. Die Methode header() gibt ein Array von Referenzen zurück, deren jede wiederum auf ein Array mit den Elementen

    $tag, $score, $lines, $bytes, $headline

verweist. $tag ist hierbei der Name der WAIS-Datenbank, die für das Ergebnis verantwortlich zeichnet, $score die Trefferquote, $lines und $bytes die Länge des gefundenen Dokuments in Zeilen bzw. Bytes. In $headline liegen der Dateiname des Dokuments und, durch Leerzeichen getrennt, der Zugriffspfad relativ zum Dateisystem.

Zeile 33 ermittelt die Länge des Ergebnis-Arrays - die Anzahl der Treffer. Doch, halt, eine Ausnahme gibt's: Für den Fall, daß nichts gefunden wurde oder ein Fehler aufgetreten ist, kommt ein Eintrag zurück, dessen Score-Feld auf Null gesetzt ist.

Ist, wie Zeile 28 prüft, die Anzahl der Ergebnisse gleich Null ($#results liefert Eins weniger als die Länge von @results), muß ein Fehler aufgetreten sein, wahrscheinlich läuft der WAIS-Server nicht. search.pl zeigt daraufhin eine Fehlermeldung in Rot an und bricht ab.

Sonst gibt Zeile 40 die (korrigierte) Anzahl der Treffer aus und ab Zeile 42 beginnt eine for-Schleife, die über alle Treffer iteriert, die Pfad-Datei-Informationen vom WAIS-Server in URLs umwandelt und als HTML-Links in einer Tabelle anzeigt. Die td(), TR(), a() und table()-Funktionen des CGI.pm-Moduls sehen zwar kryptisch aus, verkürzen jedoch den Code beträchtlich, und verleihen einem, hat man sie mal verstanden - grenzenlooose Maaaacht, huaaah ...

Kasten: freeWAIS-sf: Installation und Indizierung

freeWAIS-sf von Ulrich Pfeifer zeichnet sich gegenüber dem originalen WAIS-Engine durch die Einbindung sogenannter structured fields aus: in Dokumenten eingebettete Felder, die dem Indizierer ein wenig von der Dokument-Struktur vermitteln sollen, statt nur auf den Inhalt loszugehen. Die im Artikel besprochene Anwendung macht von diesem Feature keinen Gebrauch, schaden tut's jedoch nicht.

Die neueste Version von freeWAIS-sf ist freeWAIS-sf-2.1.2.tar.gz und liegt auf

    ftp://ftp.wsct.wsc.com/pub/freeWAIS-sf/freeWAIS-sf-2.1/

oder einem der deutschen Spiegel, z.B.

    ftp://ftp.leo.org/pub/comp/infosys/wais/freeWAIS/freeWAIS-sf-2.1/

zur Abholung bereit. Ausgepackt, konfiguriert, ge-maked und installiert wird folgendermaßen:

    tar zxfv freeWAIS-sf-2.1.2.tar.gz
    cd freeWAIS-sf-2.1.2
    ./configure
    make
    make install

./configure wirft eine Reihe von Fragen auf, die jedoch durch stetes Hämmern auf die Return-Taste schnell verschwinden. make install erfordert - im allgemeinen - Root-Rechte. Der fertige Build wird später noch benötigt.

Dafür, daß der noch zu startende WAIS-Server die Dokumente der Website in seinem Index findet, sorgt das folgendes Shell-Script, welches das mit der freeWAIS-sf-Installation kommende Programm waisindex aufruft und ihm den Pfad zur Website, sowie das Verzeichnis, indem der Index abgelegt wird, angibt:

    
    WAISDIR=/usr/local/etc/httpd/wais
    WEBSITE=/usr/local/etc/httpd/htdocs
    WAISINDEX=/usr/local/bin/waisindex
    
    # Verzeichnis für WAIS-Datenbank anlegen
    if [ ! -d $WAISDIR ] 
    then                
        mkdir $WAISDIR 
    fi
    cd $WAISDIR
    
    # Indizierung starten
    $WAISINDEX -r -d website $WEBSITE

Die WEBSITE-Variable gibt den Pfad an, unter dem das Dokumentenverzeichnis der zu indizierenen Website liegt. Soll die Datenbank, wie in WAISDIR angegeben, in einem Nebenverzeichnis des Webservers liegen, benötigt das Skript natürlich die entsprechenden Rechte, sonst tut's auch jedes andere Verzeichnis. waisindex ackert dabei rekursiv die gesamte Website durch, dieser Vorgang kann, entsprechend der zu bearbeiteten Dokumentenzahl, etwas dauern. Der eigentliche Server-Prozeß läßt sich über folgende Kommandozeile starten:

    /usr/local/bin/waisserver -d /usr/local/etc/httpd/wais -p 4711

Die angegebene Port-Nummer muß - klarerweise - mit der im CGI-Script search.pl angegebenen übereinstimmen, das über die -d-Option spezifizierte Verzeichnis entspricht dem waisindex vorher mitgeteilten. Damit der WAIS-Server zukünftig gleich beim Booten des Rechners startet, empfiehlt es sich, folgende Zeilen in /etc/rc.d/rc.local zu packen:

    echo "Starting WAIS Server"
    /usr/local/bin/waisserver -d /usr/local/etc/httpd/wais \
        -p 4711 -e /usr/var/log.wais &

Kasten: Wais.pm installieren

Die Perl-Schnittstelle Wais.pm zu installieren, ist etwas haarig, aber machbar. Zunächst geht alles wie gewohnt: Ans CPAN, wahlweise

    ftp://ftp.leo.org/pub/comp/programming/languages/perl/CPAN/
    ftp://ftp.rz.ruhr-uni-bochum.de/pub/CPAN/
    ftp://ftp.uni-hamburg.de/pub/soft/lang/perl/CPAN/

angedockt, findet sich unter modules/by-module/Wais die Distribution

    Wais-2.304.tar.gz

Diese entpackt sich wie gewohnt - und zwar am geschicktesten im selben Verzeichnis wie vorher die freeWAIS-sf-Distribution - mit

    tar zxfv Wais-2.304.tar.gz

Doch vor dem 'make' sind einige wilde Aktionen mit der freeWAIS-sf-Distribution notwendig: So benötigt Wais.pm die Include-Datei wais.h sowie die Bibliothek libwais.a. Beide Dateien entstehen unter Mitwirkung des freeWAIS-sf-Source-Codes.

Stehen, wie vorgeschlagen, freeWAIS-sf-2.1.2/ und Wais-2.304/ im gleichen Verzeichnis, geht das so:

    cd freeWAIS-sf-2.1.2/ir
    perl ../../Wais-2.304/mkinc -I../ctype ui.h cutil.h irext.h \
      irfiles.h irsearch.h irtfiles.h weight.h \
      docid.h >/tmp/wais.h
    
    cd ../../Wais-2.304
    mkdir tmp
    cd tmp
    ar x ../../freeWAIS-sf-2.1.2/ir/libwais.a
    ar x ../../freeWAIS-sf-2.1.2/regexp/libregexp.a
    ar x ../../freeWAIS-sf-2.1.2/lib/libftw.a
    cp ../../freeWAIS-sf-2.1.2/ctype/ctype.o .
    ar rc libwais.a *.o
    ranlib libwais.a
    cp libwais.a /tmp/libwais.a
    cd ..

Dann müssen wais.h und libwais.a, die noch unter /tmp liegen, dorthin, wo der Compiler sie findet, also z.B. (als root)

    cp /tmp/wais.h /usr/local/include
    cp /tmp/libwais.a /usr/local/lib

Dann ist Wais-2.304 endlich startbereit:

    cd Wais-2.304
    perl Makefile.PL
    make

Der make wirft eine Reihe von unkritischen Warnings auf, läuft aber erfolgreich. Schließlich plaziert ein unter root aufgerufenes

    make install

das Modul Wais.pm in den Perl-Modul-Pfad. Es gab schon einfachere Installationen.

Listing search.pl

    01 #!/usr/bin/perl -wT
    02 ##########################################################################
    03 # search.pl - Stichwortsuche in einer WAIS Datenbank
    04 #
    05 # Michael Schilli, 1998 (mschilli@perlmeister.com)
    06 ##########################################################################
    07 
    08 use CGI qw/:form :html param header/;
    09 use Wais;
    10 
    11 ##########################################################################
    12 # Konfiguration
    13 ##########################################################################
    14 $htdocs_path = "/usr/local/etc/httpd/htdocs";
    15 $dbname      = "website";
    16 $hostname    = "m1";
    17 $port        = 4711;
    18 ##########################################################################
    19 
    20 print_form();                 # Überschrift und Suchformular ausgeben
    21 
    22 exit 0 unless param("query"); # Ohne spezifizierten Query ist hier Schluß
    23 
    24                               # Query ist angegeben, WAIS befragen
    25 $result = Wais::Search({'query' => param("query"), 'database' => $dbname,
    26                         'host'  => $hostname,      'port'     => $port});
    27 
    28 @results = $result->header(); # Ergebnisse aufbereiten
    29 
    30 if($#results < 0) {           # Fehler aufgetreten?
    31     print pre(font({color=>"red"}, "Error - WAIS server down?\n"));
    32     return;
    33 }
    34 
    35 $nof_hits = $#results + 1;    # Anzahl der Ergebnisse
    36                               
    37 if($nof_hits == 1) {          # Ein Ergebnis mit Score 0 => Kein Treffer
    38     my ($tag, $score) = @{$results[0]};
    39     $nof_hits = 0 unless $score;
    40 }
    41 
    42 print hr, i($nof_hits, " documents found\n"); # Anzahl Treffer
    43 
    44 for(@results) {       # Über Ergebnisse iterieren
    45 
    46     my ($tag, $score, $lines, $bytes, $headline, $types, $docid) = @$_;
    47 
    48     next unless $score;       # Bogus-Ergebnis vergessen
    49 
    50                               # WAIS-Pfad in URL-Pfad umwandeln
    51     my ($file, $path) = split(' ', $headline);
    52     $path =~ s,^$htdocs_path/,,g;        # File-System-prefix wegwerfen
    53                               # URL in Tabelle hängen
    54     $rows .= TR(td( a({href=>"http://$hostname/$path$file"}, 
    55                       "$path$file")));
    56 }
    57 
    58 print table($rows);           # Tabelle ausgeben
    59 
    60 ##########################################################################
    61 sub print_form {
    62 ##########################################################################
    63 
    64     print header, 
    65           start_html(-title   => 'Website Search',
    66                      -BGCOLOR => 'white'),
    67           center(h1("Search the Website")),
    68           center(table(TR(
    69               td(
    70                   p("Enter Query:")),
    71               td(
    72                   start_form,
    73                   textfield(-name  => 'query', 
    74                             -value => (param('query') || ""),
    75                             -size  => 40),
    76                   end_form,
    77           )))),
    78           end_html;
    79 }

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.