Universelle Volltextsuche

Erst nach Fertigstellung eines Datenbankprojekts fällt Ihrem Auftraggeber plötzlich noch ein, dass er noch ein unentbehrliches Feature benötigt: Er will alle Tabellen der Datenbank global nach bestimmten Stichwörtern durchsuchen können, etwa indem er den Namen “Meyer” in ein Suchfeld eingibt und schnell sämtliche Vorkommen in einer Ergebnisliste erhält. Hieraus will er die entsprechenden Stellen anschließend aufrufen. Einfach, denken Sie, und signalisieren Ihrem Kunden: “O.K., alles kein Problem!” Doch bei näherer Betrachtung fällt Ihnen auf, dass es da mehr zu bedenken gibt, als zunächst vermutet …

Schwache Leistung

Die Idee, die Sie vielleicht haben, ist, einfach alle Tabellen der Datenbank anhand der TableDefs-Auflistung zu ermitteln, jede mit einem Recordset zu öffnen und in allen Feldern nach dem Begriff zu suchen. Welche Felder der Tabellen in die Suche mit einbezogen werden sollen, entscheidet der Datentyp: Nur Textfelder müssen nach den Wünschen des Auftraggebers durchsucht werden und diese ergeben sich aus den Type-Eigenschaften der Field-Objekte des Recordsets. Sie dürfen den DAO-Typ dbText oder dbMemo aufweisen. Der Inhalt der so gefilterten Felder wird dann Datensatz für Datensatz ausgelesen. Dabei wird mit der VBA-Funktion InStr() ermittelt, ob darin der Suchbegriff vorkommt.

Das würde sicherlich funktionieren, doch dann kommen Ihnen berechtigte Zweifel, ob diese Art der Suche nicht mit Performance-Schwierigkeiten zu kämpfen hätte. Schließlich müsste die Anwendung alle Datensätze aller Textfelder aller Tabellen durchlaufen, was bei dem erwarteten Datenaufkommen und der Tatsache, dass viele Benutzer auf die Datenbank zugreifen werden, nicht zu einem Resultat führt, dem man das Prädikat “schnell” anheften könnte. Ein Produkt mit schlechter Performance möchten Sie aber nicht abliefern und deshalb muss eine andere Lösung her.

Suchmaschine

Suchmaschinen wie Google suchen ja auch nicht jeweils im gesamten Terabyte-schweren Bestand von Webseiten, die ihre Robots aus dem Netz gefischt haben, wenn Sie einen Suchbegriff eingeben.

Stattdessen besitzen sie einen eindeutigen Index aller Begriffe, die irgendwo auf den Seiten vorkommen. Etwas ähnliches sollte auch unsere Volltextsuche enthalten. In Bild 1 sehen Sie einen Vorschlag für den Aufbau der zum Volltextindex gehörenden Tabellen.

relations.tif

Bild 1: Tabellenaufbau des Volltextsuchindex

Die Angelegenheit teilt sich in zwei Vorgänge auf: Zuerst werden alle Tabellen der Datenbank ermittelt und iher Namen sowie Primärschlüsselnamen in der Tabelle tblFulltextTables abgelegt.

Außerdem wird die Tabelle tblFulltextFields mit den zu den Tabellen gehörenden Feldnamen gefüllt – aber nur für jene Felder, die Texte enthalten. Diese beiden Tabellen stellen die Grundlage für die anschließende Indizierung dar. Die Tabellen enthalten zusätzlich je ein Boolean-Feld namens bScan, das mitteilt, ob die Tabelle oder das Feld überhaupt für die Indizierung relevant sind.

Nur wenn bScan den Wert True hat, wird das Feld oder die Tabelle in die Indizierung mit einbezogen.

Im zweiten Schritt werden dann die Feldinhalte der nun ermittelten Textfelder ausgelesen, in Worte gesplittet und diese werden in die Tabelle tblFulltextWords aufgenommen.

Dabei ist das Feld Word eindeutig indiziert, sodass jeder Begriff nur einmal in der Tabelle vorkommen kann. ID ist ein Autowert. Die wichtigste Tabelle des Systems ist nun tblFulltextFind. Dort werden die folgenden Informationen zusammengefasst:

  • IDWord: enthält einen Verweis auf ein Wort der Tabelle tblFulltextWords
  • IDTable: enthält einen Verweis auf einen der in tblFulltextTables enthaltenen Tabellennamen
  • IDField: enthält einen Verweis auf ein Feld der in IDTable gespeicherten Tabelle
  • IDRecord: der Wert des Primärschlüssels, der zu dem Datensatz gehört, in dem der Text mit dem Suchbegriff steht

Das macht auch schon eines deutlich: Um einen Feldinhalt wiederzufinden, ist es praktisch unumgänglich, dass die Tabellen einen eindeutigen Primärschlüssel aufweisen. Ohne diesen kann ein Datensatz später nicht identifiziert werden. Sie sollten es sich daher zur Angewohnheit machen, jede auch noch so kleine Tabelle mit einem Primärschlüssel zu versehen. Das lässt sich auch noch nachträglich erledigen, indem Sie Tabellen ohne Schlüssel ein zusätzliches Autowert-Feld spendieren.

Suchen

Wie erfolgt nun die Suche nach einem Begriff Zunächst schaut die Anwendung im Feld Word der Tabelle tblFulltextWords nach, ob der Begriff vorhanden ist. Falls nicht, kommt also das Wort nirgendwo vor. Falls ja, wird die Tabelle tblFulltextFind zu Rate gezogen und nach Datensätzen gefiltert, die den richtigen Wert für IDWord enthalten. Und schon haben wir die Ergebnisliste mit Datensätzen, die auf Tabellen, Felder und Primärschlüssel verweisen.

Da geschieht also nicht viel und das genau ist der Vorteil der indizierten Volltextsuche. Sie läuft schnell ab. Das Suchen ist einfach, das Indizieren allerdings relativ aufwändig und Zeit raubend.

Modul zur Volltextindizierung

Ich erspare Ihnen, die Routinen zur Indizierung selbst zu programmieren. Im Modul mdlVolltextindex der Beispieldatenbank finden Sie alles Notwendige. Die Erläuterung einzelner Code-Stellen folgt weiter unten. Die Datenbank enthält neben den notwendigen Tabellen zur Indizierung (siehe Bild 1) als Beispiel noch die Tabellen aus der Südsturm-Datenbank [1] sowie ein Suchformular frmVolltextsuche (siehe Bild 3), das die Funktionalität demonstrieren soll.

Das Formular enthält eine Schaltfläche Suchindex neu aufbauen, deren Betätigung die Routinen zur Neuindizierung aller Tabellen der Datenbank anstößt. Dabei wird in einem Zwischenschritt die Tabelle tblFulltextTables geöffnet, die mit der als Unterdatenblatt eingeblendeten Tabelle tblFulltextFields verknüpft ist. Das gibt Ihnen die Gelegenheit, für jede Tabelle und jedes Feld festzulegen, ob es bei der Indizierung berücksichtigt werden soll.

Dazu müssen Häkchen in die Verwenden-Checkboxen gesetzt werden (siehe Bild 2).

datenblatt.tif

Bild 2: Tabellen und Felder mit Textinhalten

Ich würde hier anregen, das Feld PLZ zu deaktivieren, denn es macht wenig Sinn, Postleitzahlen in eine Volltextsuche zu integrieren.

Nach dem Schließen der Tabelle fährt die Routine automatisch fort und indiziert den gesamten Datenbestand.

Sie meldet sich nach Vollzug schließlich mit einer Ausgabe der benötigten Zeit. Das wird ungefähr eine Sekunde dauern.In Datenbanken mit Zigtausenden von Datensätzen wird die Sache allerdings anders aussehen. Hier kann die Indizierung ohne Weiteres mehrere Minuten beanspruchen.

Nach der Indizierung können Sie einen Suchbegriff in das Formular eingeben, auf Suchen! klicken und Sie erhalten eine Ergebnisliste wie in Bild 3.

formular.tif

Bild 3: Suchformular der Beispieldatenbank

Ein Doppelklick auf einen Eintrag öffnet dann das entsprechende Formular und markiert außerdem das Textfeld, in dem der Begriff enthalten ist.

Die Spalte “Inhalt” der Ergebnisliste enthält übrigens nur einen Ausschnitt des Textinhalts, falls dieser länger als 50 Zeichen sein sollte.

Behind the curtain

Schauen wir uns auszugsweise an, wie der Code des Moduls mdlVolltextindex gestrickt ist.

ScanMyTables

Diese Prozedur listet die Tabellen und Felder auf, die in die Volltextsuche einbezogen werden können, und speichert das Ergebnis in den Tabellen tblFulltextTables und tblFulltextFields. Dazu wird die TableDefs-Auflistung der Datenbank enumeriert:

Dim dbs As Database
Dim tdf As TableDef
Set dbs = CurrentDb
For Each tdf In dbs.TableDefs
     tdf.MacheIrgendWas
Next tdf

Welche Tabellen und Felder für die Indizierung in Frage kommen, hängt von drei Bedingungen ab:

  • Wenn Systemtabellen wie MSysAccessObjects ausgeschlossen werden sollen, kann die Prozedur bei Aufruf der Parameter OmitSysTables auf True gesetzt werden.
  • Zulässige Tabellen müssen einen Primärschlüssel aufweisen.
  • Die Felder dürfen vom Datentyp Text oder Memo sein.

Ob eine Tabelle eine Systemtabelle ist, lässt sich anhand ihrer Attributes-Eigenschaft feststellen:

If (tdf.Attributes AND dbSystemObject) = 0 Then 
 ...

Hier wird der Wert von Attributes mit dem Bit-Operator AND und der Konstanten dbSystemObject verknüpft. Heraus kommt also die Kombination beider Bit-Werte, die 0 beträgt, wenn die Werte nicht übereinstimmen – es handelt sich dann nicht um eine Systemtabelle.

Nicht ganz so einfach ist zu ermitteln, wie der Primärschlüssel einer Tabelle lautet. Es gibt keine entsprechende Eigenschaft eines TableDefs-Objekts. Stattdessen enthält es eine Indexes-Auflistung, die durchlaufen werden muss, wobei Sie jeden einzelnen enthaltenen Index auf die Eigenschaft Primary überprüfen müssen:

Dim Index As DAO.Index
Dim bIndex As Boolean
For Each idx In tdf.Indexes
     If idx.Primary = True Then
         bIndex = True
         Exit For
     End If
Next idx
If bIndex Then
    ...

Wenn die Primary-Eigenschaft den Wert True hat, dann handelt es sich bei dem Index um den Primärschlüssel und die Schleife wird verlassen. Davor wird noch das Flag bIndex auf True gesetzt, damit der weitere Code weiß, dass überhaupt ein Primarschlüssel gefunden wurde. Nur, wenn das der Fall ist, darf die Tabelle in die Indizierung mit aufgenommen werden.

Den Namen des Index erhält man übrigens über seine Fields-Auflistung. Es ist ihr erstes Feld:

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

den kompletten Artikel im PDF-Format mit Beispieldatenbank

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar