2. Architektur von Guugelhupf

Guugelhupf besteht, grob gesehen, aus zwei Teilen:

2.1. Der Indizierer

Aus einer beliebigen Anzahl von Dateien erstellt der Indizierer einen schnell zu durchsuchenden Index.

Der Indizierer unterteilt sich selbst wieder in die folgenden Teile:

2.1.1. Tokenizer

In der vorliegenden Version von Guugelhupf ist nur eine Art von Tokenizer implementiert, den wir CharTableTransform-Tokenizer nennen, da er aus einer Lookup-Tabelle besteht, die jedes ASCII Zeichen auf ein anderes ASCII Zeichen abbildet/transformiert, mit der Besonderheit, daß der ASCII-Wert 0 eine spezielle Bedeutung hat, und zwar ist in diesem Fall das Zeichen ein Non-Word Zeichen und kann "weggeschmissen" werden. Ein Wort ist somit definiert als alle Zeichen die von zwei ASCII 0 Zeichen umgrenzt werden. Auch wird mit dieser Art von Tokenizer eine anschließende Konvertierung zu Kleinbuchstaben gespart, indem man eine entsprechende Tabelle verwendet.

Der Code hierfür findet sich im Verzeichnis tokenizer/char-table-transform.

Drei Unterschiedliche Ausprägungen des CharTableTransform-Tokenizers sind vorhanden:

  • Memory Mapped (MmapTokenizer)

  • Stream (StreamTokenizer)

  • String (StringTokenizer)

wobei der Memory Mapped Tokenizer der schnellste ist, nicht zuletzt weil ein großer Teil in C geschrieben ist. Der StreamTokenizer operiert auf Streams (d.h. auch StdIn oder Sockets möglich die nicht unbedingt eine feste Länge besitzen, wie dies für den Einsatz des MmapTokenizer erforderlich ist) und ist vollständig in SML geschrieben. Aufgrund des Fehlens der Funktion TextIO.openString in MLton wird der StringTokenizer benötigt, der auf Strings operiert, wie der Name schon sagt.

Zur Erzeugung von Character-Tables kann das Ruby-Script createCharTable.rb im tools Verzeichnis verwendet werden. Es erzeugt dann entweder SML-Sourcecode, der in ein Programm einkompiliert werden kann, oder eine Binär-Datei, die erst zur Laufzeit geladen wird. Das Eingabe-File für eine Englische Character-Table könnte z.B. wie folgt aussehen:

    add LOWERCASE
    add UPPERCASE, LOWERCASE
    add DIGITS
Dies gibt an, daß alle kleinen Buchstaben (LOWERCASE) auf sich selbst abgebildet werden sollen, ebenso wie alle Ziffern (DIGITS). Großbuchstaben (UPPERCASE) jedoch werden auf die entsprechenden Kleinbuchstaben abgebildet. Alle anderen Zeichen besitzen den Wert 0 und werden somit nicht zur Bildung eines Wortes verwendet.

Alle Tokenizer haben ein Subset von Funktionen mit gleichem Typ (z.B. nextToken), verdeutlicht wird das durch die Signatur TOKEN_STREAM (definiert in Datei gh-defs.sml). Dies trifft auch für alle Filter zu.

2.1.2. Filter

Filter haben als Eingabe einen TokenStream und liefern einen veränderten TokenStream als Ausgabe. Die vorliegende Version implementiert nur einen Stopword-Filter, der haufige Wörter einer Sprache eleminiert. Die zu eleminierenden Wörter können z.B. aus einer Datei gelesen werden, die in jeder Zeile ein Wort enthält. Diese Wörter werden dann einmalig in eine Hashtabelle eingefügt, um später eine schnelle Abfrage zu gewähleisten, ob das Token ein Stopwort ist oder nicht.

Der Sourcecode des Stopword-Filters befindet sich im Verzeichnis filter in der Datei stopword-filter.sml.

2.1.3. Die Index Datenstruktur

Die gebräuchlichste Datenstruktur für einen Wort-Index ist wohl eine Invertierte Wort Liste. Diesen Weg haben auch wir gewählt.

Zwei Ausprägungen sind denkbar:

  • FrequencyInvertedList

  • PositionInvertedList

wobei die FrequencyInvertedList nur die Häufigkeiten des Auftretens eines Wortes in einem Dokument speichert, die PositionInvertedList dagegen alle Positionen eines Worten in einem Dokument. Letzteres benötigt sehr viel mehr Speicherplatz, und ist in der Vorliegenden Version noch nicht implementiert. Vorteilhaft dagegen wäre die Möglichkeit der Abfrage ob Wörter in einem bestimmten Abstand zu anderen Wörtern stehen oder ob diese benachbar sind.

Unsere Invertierte Liste verwendet eine Hashtabelle (Datei ds/hash-table.sml), und bildet damit die Wörter auf eine Splay-Tree Map ab. Letztere Datenstruktur bildet die Dokument-ID auf die Häufigkeit des Auftretens ab. Es findet also folgende Abbildung statt:

token -> docid -> frequency
Bzw. für die PositionInvertedList:
token -> docid -> position list

Der Sourcecode für die Invertierte Liste ist in Datei ds/inv-list.sml.

2.1.3.1. Dateiformat des Index

Im Folgenden ist die Struktur eines abgespeicherten Index beschrieben.

Der Header ist eine 28-Byte große Struktur, die wie folgt aufgebaut ist:

Byte-PositionDaten-TypBeschreibung
0 - 15stringKennung (z.B. "FREQ_INV_LIST ")
16 - 19dwordVersion (z.B. 0x00010000 für 1.0)
20 - 23dwordFlags (z.Z. unbenutzt)
24 - 27dwordGröße des Index (Hashtabelle)

Daraufhin folgt die Hashtabelle (4*Größe des Index Bytes). Jedes Bucket der Hashtabelle enthält entweder den Offset (relativ zum Ende der Hashtabelle) auf die Token mit demselben Hashwert, oder bei einem leeren Bucket den Wert -1 (0xFFFFFFFF).

Die Daten (d.h. Token und Häufigkeiten) folgen direkt der Hashtabelle. Jedes nicht-leere Bucket wird in der Daten-Sektion wie folgt abgebildet:

Byte-PositionDaten-TypBeschreibung
x - (x+3)dwordAnzahl der folgenden Tokens

Daraufhin folgt für jedes Token folgende Struktur:

Byte-PositionDaten-TypBeschreibung
y - (y+3)dwordAnzahl der Dokumente (in denen das Token vorkommt)
y+4byteLänge des Token-Textes
(y+5) - (y+5+n)stringToken-Text

Hierauf folgt Anzahl der Dokumente-mal:

Byte-PositionDaten-TypBeschreibung
z - (z+3)dwordDokument-ID
(z+4) - (z+7)dwordHäufigkeit des Auftretens in diesem Dokument

2.1.4. Das Indizier-Programm gh_index

Das Programm gh_index ist die Schnittstelle für den Benutzer zum Anlegen eines Indexes. Der Sourcecode befindet sich in Datei test/index.sml. Man ruft es wie folgt von der Kommandozeile auf:

% gh_index hashSize charTableFile stopwordFile indexFile docDB

Die verschiedenen Parameter sind in der folgenden Tabelle erklärt:

ParameterBeschreibung
hashSizeHiermit kann man die Größe der Hashtabelle angeben.
charTableFileDie zu verwendente Char-Table Datei.
stopwordFileDatei, die zu verwendende Stopwörter enthält.
indexFileIndex-Datei, die erzeugt werden soll.
docDBName der Dokument-Datenbank (ohne Endung .db).

gh_index  erwartet auf der Standard-Eingabe die Namen der Dateien, die es indizieren soll. Es können auch zwei Dateinamen, getrennt durch ein TAB-Zeichen angegeben werden, woraufhin gh_index  die erste verwendet um die Datei zu tokenizen, die zweite jedoch in der Dokument-Datenbank abspeichert. Dies hat denn Sinn und Zweck, daß z.B. auch Webseiten indiziert werden können (müssen zuvor als lokale Datei gespeichert werden), ohne daß dabei die URL verloren geht.

Die Dokument-Datenbank ist eine DBM Hash-Datenbank, in der zu jeder Dokument-ID der zugehörige Dateiname steht. Eine DocDB kann auch für mehrere Indexe verwendet werden. Der Sourcecode des DBM-Interface für SML ist im Verzeichnis dbm zu finden (Dateien dbm.c und dbm.sml).

2.1.4.1. Das Hilfprogramm gh_conv

Da gh_index eine Liste von Dateinamen von StdIn ließt, muß man diese irgenwie generieren. Unter UNIX bietet sich hierfür das Programm find an:

% find . -type f -print

liefert rekusiv eine Liste aller Dateien beginnend vom aktuellen Verzeichnis. Mit Hilfe von Pipes kann man dies nun gh_index  auf StdIn umleiten:

% find . -type f -print | gh_index ...

Das Ruby-Skript gh_conv  kann dies ebenso, dient aber vorwiegend zur Konvertierung sowie Filterung verschiedener Dateitypen. So ermittelt gh_conv  den Dateityp durch das UNIX-Hilfsprogramm file  und reagiert entsprechend dem Dateityp. Es kann:

  • Dateien ignorieren (:REJECT); sinnvoll bei Videos, Bildern oder Audio-Files

  • Dateien as is passieren lassen (:ASIS)

  • ein externes Datei-Konvertierung Programm aufrufen (z.B. lynx, ps2ascii oder catdoc)

Aktionen für weitere Dateitypen können hinzugefügt werden, indem man im Programm (Datei test/gh_conv) das Array FILE_TYPE_LIST entsprechend erweitert.

Aufrufen kann man es auf zwei Arten:

% find . -print | gh_conv | gh_index ....

% gh_conv /start/dir | gh_index ....

2.2. Das Abfrage/Query Programm

Das Query-Programm besteht aus einem RankingParser (Files query/ranking-parser.sml und query/ranking.lex), der Struktur RankedQuery (File query/ranked-query.sml) sowie dem Sourcecode für das Programm selbst (test/query.sml). Der eigentliche Zugriff auf die Index-Datei findet in der InvertedList Struktur in Datei ds/inv-list.sml statt, oder falls eine extrem schnelle und Resourcen-schonende Version benötigt wird, kann ds/query-inv-list.c verwendet werden.

Der RankingParser transformiert eine Eingabe, wie etwa folgende:

(+10)IR -diode +information retrieval
in eine Liste von RankedTerm's:
[RankedTerm(IR, 10), RankedTerm(diode, -1), RankedTerm(information, 1), RankedTerm(retrieval, 1)]

RankedQuery führt eine Abfrage auf den Index durch und berechnet für jedes auftretende Dokument einen Score. Die Dokumente sortiert nach absteigendem Score werden dann schließlich vom Hauptprogramm gh_query ausgegeben.

2.2.1. Das Query-Programm gh_query

Das Programm gh_query ist die Schnittstelle für den Benutzer zum Abfragen eines Indexes. Der Sourcecode befindet sich in Datei test/query.sml. Man ruft es wie folgt von der Kommandozeile auf:

% gh_query charTableFile stopwordFile indexFile docDB

Die verschiedenen Parameter sind in der folgenden Tabelle erklärt:

ParameterBeschreibung
charTableFileDie zu verwendente Char-Table Datei.
stopwordFileDatei, die zu verwendende Stopwörter enthält.
indexFileIndex-Datei, die abgefragt werden soll.
docDBName der Dokument-Datenbank (ohne Endung .db).

gh_query  erwartet auf der Standard-Eingabe eine Abfrage (siehe weiter oben). Daraufhin gibt das Programm auf der Standard-Ausgabe die passenden Dokumente geordnet nach höchstem Score aus.