Th. Berger
(ThB.com@t-online.de)
8.10.1997
| dieses Dokument im PDF-Format | Beispielscripte/Parameterdateien |
Anstatt eine auch in der Praxis einsetzbare oder bereits eingesetzte phonetische Suche mit Avanti vorzuführen, hatte diese kurze Demonstration eher folgende Ziele:
Unter diesen Zielen und zusammen mit dem Zufall(?), daß ein Modul Soundex.pm mit einer Implementierung des zwar extrem trivialen, aber verblüffend funktionsfähigen Soundex-Algorithmus von Knuth zur Standardbibliothek von perl gehört, kam es dann zu obigem Titel als selbstgestecktem Ziel der Demonstration.
Die im folgenden angeführten Codebeispiele stammen aus einer etwas elaborierteren Version der jeweiligen Programme, die ich zur Vorbereitung des Vortrags angefertigt hatte.
Zunächst soll ausgehend von tcp-ip.pl, dem mit Avanti ausgelieferten Demonstrationsskript, welches, wie nicht genug zu betonen ist, auch zum Testen von Teilen einer Avanti-CGI-WWW-Installationen nicht zu unterschätzen ist, ein interaktives Benutzerinterface für Avanti geschaffen werden:
Der Benutzer gibt auf einer Zeile mehrere durch Spatien getrennte Suchbegriffe an, die dann durch implizites Und verknüpft im Titelregister der Datenbank recherchiert werden. Das Resultat wird dann mit einer Standardparameterdatei formatiert (hier: gefeldertes Format) dem Benutzer zurückgeliefert und ihm sofort der Prompt für die nächste Recherche angezeigt. Eingabe einer vollständig leeren Suchbegriffszeile beendet das Programm.
#!perl -w require Socket;Hier einige vorgezogene Setzungen, um das Skript etwas flexibler als die Vorlage tcp-ip.pl zu halten:
$PfadParam="c:\\tmp"; $Datenbank="avdemo"; $user="opac"; $passwd="opac";Gewisser Standard-Code aus tcp-ip.pl ist einfach in das Unterprogramm verlagert: Initialisierungen und das Herstellen der Socket-Verbindung.
&TCPConnect;Es beginnt eine Endlosschleife
endlessly:
{
print "Suchbegriff: ";
chop($query = <STDIN>);
die durch leere Eingabe beendet werden kann:
last unless $query;Der virtuelle Aufrufpfad wird später in einem Unterprogramm übermittelt, hier wird jetzt nur der eigentliche Job zunächst in der Textvariablen $jobtext zusammengebaut. \n bedeutet Zeilenvorschub.
$jobtext = "xport param E-W\n";
Die Eingabe wird an den Leerzeichen zerlegt und dann mit ` AND '
verbunden. D.h. aus abc def wird der Find-Befehl
find TIT abc AND def für Avanti.
@patterns = (split (/ /, $query));
$jobtext .= "find TIT ".join (" AND ", @patterns)."\n";
Der Rest des Auftrags ist statisch und wird an $jobtext angehängt.
Wir benutzen das vereinfachende Kommando download set, ich weiß
aber nicht, ob der Sprung zu noset tatsächlich im Falle einer
leeren Ergebnismenge ausgeführt wird.
$jobtext .= <<"XxX";
download set
if error jump noset
jump ende
:noset
write "Leider Kein Resultat!" newline
:ende
XxX
Ein Unterprogramm, das den Job abschickt
&launchjob();
warn "\nwaiting for result...\n";
und ein Unterprogramm, das das Ergebnis abwartet: Jede
Zeile des Ergebnisses ist dann ein Element des Arrays @result
@result = &waitjob();
Wieder nur ein Demo: Damit der Schirm nicht überläuft, leiten wir
das Suchergebnis in eine Datei, hinter der sich das MS-DOS
Systemkommando more verbirgt:
open (PIPE, "|more");
print PIPE @result;
close (PIPE);
Trennen und Wiederholen der Schleife...
print "\n-------------------\n\n";
redo endlessly;
}
Jetzt das Schließen des Sockets: Wir haben (um Overhead zu vermeiden)
die gesamte Zeit nur einen Socket benutzt. Das schafft uns
maximal schnelle Beantwortung aufeinanderfolgender Jobs, hält aber
natürlich auf dem Zielsystem eine Instanz des Avanti-Prozessmoduls
procav am leben. (1)
close (S); die "Bye\n"; #+# ;-)Hier das Unterprogramm zum Verbindungsaufbau: 4949 ist der (Standard-)Port von Avanti, localhost der Name dieser Maschine (immer!). Die Einbindung des Bibliotheksmoduls Socket.pm via use Socket ist hierhin geraten, sie ermöglicht vor allem den Zugriff auf die symbolischen Konstanten AFINET etc.
sub TCPConnect {
#TCP-IP Verbindungen herstellen:
$port = 4949;
$them = 'localhost';
use Socket;
$sockaddr = 'S n a4 x8';
$hostname = 'localhost';
($name, $aliases, $proto) = getprotobyname('tcp');
# ($name, $aliases, $port) = getservbyname($port, 'tcp')
# unless $port = /^\d+$/;
($name, $aliases, $type, $len, $thisaddr) =
gethostbyname($hostname);
Hier könnte eine geringfügige Verbesserung nicht schaden, damit auch
bereits numerisch angegebene Internetadressen korrekt verarbeitet
werden, d.h. der Aufruf der Funktion gethostbyname ist zu umgehen
und mittels split und pack wäre der vierbytige Wert für $thataddr
anders zu konstruieren.
($name, $aliases, $type, $len, $thataddr) =
gethostbyname($them);
$this = pack($sockaddr, AF_INET, 0, $thisaddr);
$that = pack($sockaddr, AF_INET, $port, $thataddr);
Jetzt haben wir die Initialisierungsdaten beisammen und erzeugen den
Socket (oder eine Fehlermeldung)
socket(S, PF_INET, SOCK_STREAM, $proto)
|| die "socket: $!";
Nach erfolgreicher Erzeugung wird sofort die Verbindung mit dem
Server aufgenommen:
if(! connect(S, $that)){
&ServerMustBeDown;
exit;
}
Diese Anweisungen stellen sicher, daß maximal große Datenpakete an
den Server übertragen werden (sonst erfolgt etwa nach jeder Zeile eine
Übermittlung, was nicht besonders ökonomisch ist).
select(S); $| = 0; select(STDOUT);
Dies ist derzeit für den Windows-Server Avanti-W ein muß, für die
UNIX-Variante aber verboten. Herr Hoeppner versprach baldige
Vereinheitlichung.
print S "SendEndOfReply";
}
### Auftrag abschicken
sub launchjob {
local ($_);
Es wird der Text mit dem Job aus $jobtext in seine Zeilen zerlegt,
diese um Kommentare bereinigt, der Virtuelle Aufrufpfad davorgesetzt
und Datenbankname, User und Password dahinter. Alles zusammen wird
uf den Socket geschrieben d.h. an den Server übermittelt.
local (@jobtext) = split (/\n/, $jobtext);
print S "& $PfadParam\n";
foreach (@jobtext) {
($_) = split (/\s*\/\//, $_);
print S "$_\n";
};
print S "\@ DB=$Datenbank ID=$user/$passwd\n";
print S "AVANTI:EOJ\n";
Hiermit wird kurzzeitig die oben eingestellte maximale Bufferung der
Übertragung aufgehoben, man könnte die fünf Kommandos auch in ein
Unterprogramm schreiben, das sollte dann flush heissen!
select(S); $| = 1; print S ""; $| = 0; select(STDOUT);
}
### Auftragsergebnis einsammeln
sub waitjob {
Solange eine Zeile über den Socket einlesbar ist, wird diese an
das Array @Input angehängt.
local(@Input);
while (<S>) {
# bis das Ende-Signal der Antwort empfangen wird:
# EOR (=End of Reply)
next if /^$/;
Hier unterscheiden sich Sockets etwas von normalen'Dateien: Wenn
zu einem Zeitpunkt keine Daten anliegen, wissen wir nicht, ob nicht
später weitere nachfolgen, da durch dieses Programm und Avanti
zumindest theoretisch simultan gelesen und geschrieben werden kann.
Würde Avanti von sich aus bereits ein Close oder Shutdown machen,
würde dieses Programm das natürlich merken, Avanti tut dies aber nicht
und erhält die Verbindung aufrecht, was wir in der allgemeinen
Schleife im Hauptprogramm ja auch ausnutzen. Also teilt uns Avanti
über den festen Text AVANTI:EOR (d.h. End of Result) mit, daß es
das Übertragen abgeschlossen hat und nichts mehr kommt, wir also mit
der abschließenden Verarbeitung der empfangenen Daten beginnen dürfen.
last if /AVANTI:EOR/;
push (@Input, $_);
};
return(@Input);
}
# Meldung, falls der Server unten ist
sub ServerMustBeDown {
print "Der Server fuer die Datenbank $Datenbank scheint"
. " nicht aktiv zu sein.\n",
"Versuchen Sie es bitte später noch einmal\n";
}
Dies war jetzt also das kleine Programm, das Avanti ständig abfragt. Das Register für die Suchbegriffe ist natürlich fest eingestellt, zum Abfragen einer Datenbank ist es aber fast brauchbar (im Lesesaal würde man es aber vielleicht nicht einsetzen wollen;-)
Was man wissen muß, ist eigentlich nur, daß ein Modul dafür Bestandteil der Standard-Perl-Bibliothek ist. Mittels
use Text::Soundex;am Anfang unseres Perlprogramms und dem Wissen, daß dieses Modul eine Funktion soundex() zur Verfügung stellt, die gleichermaßen für einzelne Worte wie für Arrays funktioniert. Es liefert also soundex("Heilbronn") den Wert H416.
Bevor wir nun im nächsten Abschnitt daran gehen, auch auf der Seite der Datebank das Register PHO zu erzeugen, einige Worte zum Soundex-Algorithmus.
Der Kern des Moduls besteht aus folgenden wenigen Zeilen Code (Autor und Quelle bitte in soundex.pm nachsehen). Diese operieren auf $_, das ein einziges, bereits in Großbuchstaben umgewandeltes Wort enthält, zum Beispiel Wolfenbuettel
Zunächst den ersten Buchstaben merken, hier also W.
($f) = /^(.)/;
Jedem Buchstaben ist ein (amerikanischer!) Lautwert zugeordnet, Vokale
(und ähnliches) bekommen den Wert 0, die anderen Buchstaben einen
Wert zwischen 1 und 6, im Beispiel erhalten wir nun
0041051003304
tr/AEHIOUWYBFPVCGJKQSXZDTLMNR/00000000111122222222334556/;Der Lautwert des ersten Buchstabens kommt nach $fc, also 0
($fc) = /^(.)/;
Der Lautwert des ersten Buchstabens wird vom Anfang der in Codes
umgewandelten Kette (evtl. mehrfach) entfernt, das ergibt
im Beispiel dann 41051003304
s/^$fc+//;
Jede Folge gleicher Lautwerte wird auf ein Vorkommen reduziert!
Jetzt haben wir nur noch 410510304.
tr///cs;
Jetzt werden noch alle Vokale ganz ausgeblendet, in unserem Beispiel bleibt
415134
tr/0//d;
der erste Buchstabe im Klartext vorangestellt und sichergestellt,
daß das Resultat mindestens vier Zeichen hat, hier nun W415134000
$_ = $f . $_ . '000';
und schließlich das Ergebnis auf die ersten vier Zeichen (d.i. ein
Buchstabe gefolgt von drei Ziffern) reduziert, das Ergebnis im
Beispiel ist dann W415
s/^(.{4}).*/$1/;
Wir sehen also, daß dieser Algorithmus die gesamte Sprache auf 26 * 6 * 6 * 6 = 5616 mögliche Soundex-Codes abbildet und daher nicht besonders gut sein wird. Durch eine bessere (aus Deutsche zugeschnittene?) Lauttabelle und eine Verlängerung der Codes kann er durchaus noch verbessert werden, es handelt sich aber prinzipiell eigentlich nur um ein instruktives Beispiel aus Knuths Art of computer programming, viele bessere Algorithmen sind seitdem entwickelt worden!
Für die geplante phonetische Suche mittels Soundex-Codes müssen wir nun diese Codes auf irgendeine Weise in die Register der Datenbank bringen, um sie dort finden zu können. Prinzipiell haben wir nun viele Möglichkeiten:
Die ersten beiden Möglichkeiten passen nicht auf unseren oben programmierten Client, die letzten zwei, obwohl die einzig realistischen meiner Meinung nach, haben den Nachteil, daß die produzierten Soundex-Äquivalente bei einer Änderung der zugrundeliegenden Allegro-Datensätze nicht mit aktualisert werden. Die Vorgehensweise ist also ersteinmal nur für statische Datenbanken geeignet, die nicht interaktiv weiterbearbeitet werden können. Die mittleren (also bis auf die ersten und letzten beiden) haben nicht nur den Nachteil der geringen Verbreitung der C-Schnittstelle, sondern sie fallen auch stärker unter die Einschränkung, daß ein Allegro-Datensatz nur 500 Schlüssel produzieren darf.
Nur aus Gründen der Beispielhaftigkeit und vor allem der etwas unüblichen Nutzung der neuen Schiller-Räuber-Funktionalität verfolgen wir den letzten der aufgezählten Wege:
Zunächst also die Exportdatei, wir nennen sie E-PHO.APR, sie kann ihre Herknunf von den Standardexporten I-1.APR oder E-1.APR nicht verleugnen:
zl=0 Zeilenlänge unbegrenzt (kein Umbruch)
ks=4 Beginn des Ausgabetextes nach der Kategorienummer
ke=" " Kategorie-Ende = Code 0
as="" Aufnahme-Start: Hierarchiekennung + 0
Hauptaufnahme : Code 01 als Startzeichen
Unteraufnahme Stufe 1: 02 ...
ae=13 10 Aufnahme-Ende: Carriage Return / Line Feed
bei hierarchischer Aufnahme: nur am Ende
---- Anweisungsteil -----------------------------------
i4=1 Stammsatzersetzungen machen!
/0.. Viel (codiertes, Nummern, Signaturen) ausblenden
/30.
/32.
/71
/76
/77
/87.
/88.
/89.
/90.
/91.
Stammsätze ausblenden
#3n +# Z #zz 0
#4n +# Z #zz 0
#6n +# Z #zz 0
#8n +# Z #zz 0
#hi +#98z Z #zz 0 % hierarchieuebergreifend!
#t{h0}
#00 y0 p"00 y" #zz 0 % Originalidentnummer mit 'y' davor
% nach #00
#t{0 "96 "} % Alle Worte (auch von hierarchisch
% gespeicherten Unterbänden)
% in *eine* #96 bringen
#98z
## Pauschalexport: alle Kategorien hintereinander
ausgeben, jeweils mit ke abschließen
#+#
Ein paar (viel zu grobe) Zeichenumsetzungen.
p !/@ =32 fast alle Sonderzeichen (und Ziffern!)
als Worttrenner nehmen!
p _ 32
p [ 1
p ] 1
p ¬ 1
p ä "ae"
p ö "oe"
p ü "ue"
p ß "ss"
p Ä "Ae"
p Ö "Oe"
p Ü "Ue"
Export mit diesen Parametern wird uns also eine Menge von Sätzen liefern, die aus Kategorien #00 mit y-Identnummern und #96 mit Stichworten bestehen.
Dies können wir nun hervorragend mit folgendem Perlskript nachbearbeiten:
#!perl use Text::Soundex; #$soundex_nocode = 'Z000';Für jeden Datensatz
while ( <> ) {
Zeilenende (Satzende) abschneiden
chop;
Mehrfachleerzeichen (davon hat uns der Export Massen erzeugt!) zusamenfassen
s/\s+/ /g; # mehrfachleerzeichen
Leerzeilen kann es (Stammsätze etc.) auch geben, diese müssen ausgeblendet
werden:
next unless $_;
Das Einfügen der vier Zeichen x gibt uns Daten im .ALD-Format,
der split() liefert uns einen Teildatensatz vor #96 und
danach.
s/^\x01/\x01xxxx/; # mache ald!
($id, $words) = split (/\x0096 /);
Der Anfang des Datensatzes wird ausgegeben:
print $id;
Zerlegen in Worte
@words = split(/ /, $words);
umwandeln in Soundex-Äquivalente
@new = soundex (@words);
und wiederzusammensetzen zu einem Text.
$new = join (" ", @new);
vorige drei Zeilen kürzer:
# $new = join (" ", soundex(split(/ /, $words)));
Ausgabe des restlichen Teils des Datensatzes:
print "\x0096 $new\x00\n";
}
Fertig.
Was nun noch fehlt, ist ein bischen Code für die Indexparameter.
Zum einen müssen die in die neue Kategorie #96 gebrachten Sound-Äquivalente indexiert werden, weil wir nur mit AVANTI über symbolische Registernamen zugreifen, nehmen wir den Bereich `PH ' im Register 7. Die abschließende I-Zeile manifestiert den symbolischen Registernamen PHO.
ak=96" "+Ç #-Ç #u1 y0 p"|7PH " Phonetik-Eintrag #+# I PHO 7PH.
Zum anderen müssen ja auch die neuen Datensätze mit den phonetischen Äquivalenten in Beziehung zu den Originalsätzen gesetzt werden! Wenn wir uns an die Allegro-Lösung für das Schiller-Räuber-Problem erinnern, so fällt uns auf, daß diese Lösung asymetrisch ist: Schiller ist der in der Hauptaufnahme enthaltene Verfasser, Räuber ein in einer Unteraufnahme enthaltenes Titelstichwort. Die sogenannte Plus-Suche erweitert nun den Treffer zu Schiller um alle Unteraufnahmen, so daß Kombination mit den Treffern zu Räuber zu einem gemeinsamen Treffer, nämlich dem Band, führt.
Über Avanti werden wir bei unserer Suche nach den phonetischen Codes zunächst einmal nur die Datensätze mit diesen finden, die Ausweitung & im Sinne der Plus-Suche muß uns dann die eigentlichen Aufnahmen liefern. Insofern ist klar, daß wir die frisch generierten Sätze mit der phonetischen Information in #96 als Hauptsätze der schon längst vorhandenen Titelsätze auffassen müssen. Für die technische Realisierung müssen wir die Formulierung umkehren: Die Titelsätze deklarieren sich als Untersätze der zukünftigen phonetischen Sätze, diese werden aber dieselbe Identnummer nur mit vorangestelltem `y' bekommen, wir haben also kein Problem, folgende Zeilen in die .API zu integrieren (y-Sätze selbst sollen dabei natürlich nicht Untersätze von nie existierenden yy-Sätzen werden):
ak=00+ü i7=9 #-ü SR-Zusatzeintrag (falsch herum!) #u1 y0 I4,y p"y" X9 #+#
Anders als im Beispiel von Schiller und Räuber werden unsere phonetische Suchen immer nur Hauptsatz-begriffe suchen und miteinander verknüpfen; die Ausweitung wird uns also stets, da nie durch Untersatz-begriffe eingeschränkt, als Resultat phonetische Sätze und die Titelsätze liefern. Hier wird also noch gefiltert werden müssen. Aus Gründen der Ökonomie des Beispiels verlegen wir dieses Filtern nicht in die Exportparameterdatei, sondern modifizieren gleich den avanti-Job.
Zunächst eine kleine Stapeldatei, um die Datenbank zu generieren:
: phony.bat: Produktion der "phonetischen" Zusatzdateien :1. allgemeine Setzungen set -k=A set -L=ger set -d=d:\avanti-w\avdemo set -P=d:\coop\a-prg set -b=cat :2. Isolation der Worte mit Allegro %-P%\srch -f6 -ee-pho/%-b%_99.raw -d%-d%\%-b%_1 -m0 :3. Ersetzen der Worte durch die phonetischen Aequivalente perl phony.pl<%-b%_99.raw>%-D%\%-b%_99.ald rem del %-b%_99.raw :4. Neuaufbau der Datenbank %-P%\index -f70 -@1 -e%-b%/%-d% -d*%-d%\%-b%_ -n0 -m0 -v0 -h0 %-P%\index -fi1 -@2 -e%-b%/%-d% -d*%-d%\%-b%_ -n0 -m0 -v0 -h0
Jetzt fehlt als Abschluß noch der Umbau des ursprünglichen Perlskripts in folgenden Punkten:
Dazu gehen wir wie folgt vor:
use Text::Soundex;
@patterns = soundex (split (/ /, $query));
und einige Zeilen weiter die Konstruktion des Avanti-Find-Befehls durch:
$jobtext .= "f PHO &".join (" AND &", @patterns)."\n";
Zu beachten ist hierbei nicht nur das geänderte Register (TIT zu
PHO) sondern auch die an zwei Stellen ergänzten &, die Avanti
mitteilen, daß die Suche im Sinne der Schiller-Räuber-Problematik
automatisch auszuweiten ist. Die zu suchenden Begriffe A123 und
B456 erfordern somit die Suchanweisung f PHO &A123 AND &B456.
$jobtext .= <<"XxX";
get first
if error jump noset
:loop
if #96 jump next
download
:next
get next
if ok jump loop
Damit sind die Modifikationen am Client beendet.
© 1997 Thomas Berger
|
Thomas Berger Adolfstr. 100 53 111 Bonn |
E-Mail: thb.com@t-online.de Tel. 0228/63 38 16 Fax. 0228/63 38 20 |