Posts mit dem Label preference werden angezeigt. Alle Posts anzeigen
Posts mit dem Label preference werden angezeigt. Alle Posts anzeigen

Montag, 16. März 2009

Einstellungen für einen Textindex: Komposita, Printjoins, Skipjoins, Mixed Case und mehr ...

Beim Erstellen eines Oracle TEXT-Index kann man eine ganze Menge Einstellungen vornehmen. Im letzten Posting ging es um das Thema Stopwörter, heute schauen wir uns ein paar andere Einstellungen an.
Wir beginnen ganz einfach und erzeugen zunächst eine Tabelle mit ein paar Textzeilen.
drop table texte
/

create table texte (
  id number,
  text varchar2(4000)
)
/

insert into texte values (1, 'Das Treffen am Bahnhofsplatz heute abend war schön');
insert into texte values (2, 'Dem Chat trat der Nutzer "user_7642" bei');

commit
/
Erstellen wir nun einen Index - zunächst mal ohne jede Parametrisierung ...
drop index idx_texte
/

create index idx_texte on texte (text)
indextype is ctxsys.context
/
Anschließend kann man sich "den Index" mit einem Blick auf die Token-Tabelle ansehen ...
SQL> select token_text from dr$idx_texte$i;

TOKEN_TEXT
--------------------------------------------------
7642
Bahnhof
Bahnhofsplatz
Chat
Das
Dem
Nutzer
Platz
Treffen
abend
heute
schön
trat
user
Die erste Auffälligkeit ist die Tatsache, dass die Wörter (Tokens) im Mixed Case in der Token-Tabelle stehen. Das ist für die meisten Fälle ungeeignet, da eine Suche nach "chat" (alles kleingeschrieben) zu keinem Ergebnis führen würde. Dies gälte es also durch Einstellung von Parametern zu ändern. An anderer Stelle es gut erkennbar, dass der Index die deutsche Sprache erkannt hat; das Token "Bahnhofsplatz" wurde korrekt in die zusätzlichen Tokens "Bahnhof" und "Platz" zerlegt. Experimentieren wir nun ein wenig mit den Parametern: Als erstes soll der Index nicht mehr Case-Sensitiv sein ...
drop index idx_texte
/

begin
  ctx_ddl.drop_preference( 
    preference_name => 'MY_LEXER'
  );
end;
/

begin
  ctx_ddl.create_preference(
    preference_name => 'MY_LEXER',
    object_name     => 'BASIC_LEXER'
  );
  -- Mixed Case abschalten
  ctx_ddl.set_attribute(
    preference_name => 'MY_LEXER',
    attribute_name  => 'MIXED_CASE',
    attribute_value => 'NO'
  );
end;
/

create index idx_texte on texte (text)
indextype is ctxsys.context
parameters ('LEXER MY_LEXER')
/
Die Parameter werden in die sog. Preference MY_LEXER eingestellt. Anschließend wird der Index neu erstellt - die Token-Tabelle sieht dann so aus:
SQL> select token_text from dr$idx_texte$i;

TOKEN_TEXT
----------------------------------------------------------------
7642
ABEND
BAHNHOFSPLATZ
CHAT
HEUTE
NUTZER
SCHÖN
TRAT
TREFFEN
USER
OK ... damit ist das Mixed-Case-Problem behoben. Allerdings wurde der Bahnhofsplatz nun nicht mehr zerlegt - und das war ja eigentlich ganz gut so ... Das Erstellen der Preference MY_LEXER ändern wir also nochmals und schalten die Kompositazerlegung wieder ein (von nun an stelle ich nur noch die create_preference Aufrufe hier vor.
begin
  ctx_ddl.create_preference(
    preference_name => 'MY_LEXER',
    object_name     => 'BASIC_LEXER'
  );
  -- Mixed Case abschalten
  ctx_ddl.set_attribute(
    preference_name => 'MY_LEXER',
    attribute_name  => 'MIXED_CASE',
    attribute_value => 'NO'
  );
  -- Kompositazerlegung einschalten
  ctx_ddl.set_attribute(
    preference_name => 'MY_LEXER',
    attribute_name  => 'COMPOSITE',
    attribute_value => 'GERMAN'
  );
end;
/
Ergebnis ..
SQL>  select token_text from dr$idx_texte$i;

TOKEN_TEXT
---------------------------------------------------------
7642
ABEND
BAHNHOF
BAHNHOFSPLATZ
CHAT
HEUTE
NUTZER
PLATZ
SCHÖN
TRAT
TREFFEN
USER
Das Token user_7642 wurde offensichtlich ebenfalls zerlegt: Oracle TEXT behandelt den Unterstrich (_) als Trenner von Tokens. Auch dies kann man mit dem Parameter PRINTJOINS abschalten ...
begin
  ctx_ddl.create_preference(
    preference_name => 'MY_LEXER',
    object_name     => 'BASIC_LEXER'
  );
  -- Mixed Case abschalten
  ctx_ddl.set_attribute(
    preference_name => 'MY_LEXER',
    attribute_name  => 'MIXED_CASE',
    attribute_value => 'NO'
  );
  -- Kompositazerlegung einschalten
  ctx_ddl.set_attribute(
    preference_name => 'MY_LEXER',
    attribute_name  => 'COMPOSITE',
    attribute_value => 'GERMAN'
  );
  -- Den Unterstrich (_) als "Printjoin" deklarieren
  ctx_ddl.set_attribute(
    preference_name => 'MY_LEXER',
    attribute_name  => 'PRINTJOINS',
    attribute_value => '_'
  );
end;
/
Das Ergebnis ...
SQL>  select token_text from dr$idx_texte$i;

TOKEN_TEXT
---------------------------------------------------
ABEND
BAHNHOF
BAHNHOFSPLATZ
CHAT
HEUTE
NUTZER
PLATZ
SCHÖN
TRAT
TREFFEN
USER_7642
Doch hierbei Vorsicht: Der Unterstrich wirkt nun überhaupt nicht mehr als Trennzeichen für Tokens - die Aufnahme eines Zeichens zu den Printjoins sollte also nur dann erfolgen, wenn man sich sicher ist, dass dies auch für den gesamten Dokumentbestand in Ordnung geht. Weiterhin könnt Ihr nur einzelne Zeichen als Printjoins deklarieren, keine Zeichenketten. Hierbei muss man also ein wenig aufpassen ...
Eine andere Variante wäre, den Unterstrich als Skipjoin zu deklarieren ...

begin
  ctx_ddl.create_preference(
    preference_name => 'MY_LEXER',
    object_name     => 'BASIC_LEXER'
  );
  -- Mixed Case abschalten
  ctx_ddl.set_attribute(
    preference_name => 'MY_LEXER',
    attribute_name  => 'MIXED_CASE',
    attribute_value => 'NO'
  );
  -- Kompositazerlegung einschalten
  ctx_ddl.set_attribute(
    preference_name => 'MY_LEXER',
    attribute_name  => 'COMPOSITE',
    attribute_value => 'GERMAN'
  );
  -- Den Unterstrich (_) als "Skipjoin" deklarieren
  ctx_ddl.set_attribute(
    preference_name => 'MY_LEXER',
    attribute_name  => 'SKIPJOINS',
    attribute_value => '_'
  );
end;
/
... was dann so aussieht; der Unterstrich wäre dann verschwunden und würde bei Abfragen ignoriert.
SQL>  select token_text from dr$idx_texte$i;

TOKEN_TEXT
---------------------------------------------
ABEND
BAHNHOF
BAHNHOFSPLATZ
CHAT
HEUTE
NUTZER
PLATZ
SCHÖN
TRAT
TREFFEN
USER7642
Eine vollständige Übersicht über alle Parameter (es gibt noch ein paar mehr) findet Ihr im Handbuch Text Reference. So ... das war's für heute - mehr zu Textindex-Parametern und Einstellungsmöglichkeiten in den nächsten Postings ...

Dienstag, 11. November 2008

Mehrere Tabellenspalten indizieren: MULTICOLUMN_DATASTORE

Mitunter kommt es vor, dass die Dokumente in einer Tabelle in mehreren Spalten vorliegen. Eine Tabelle könnte wie folgt angelegt sein:
create table produktions_hinweise(
  fall_id               number(10),
  kommentar_werk        varchar2(4000),
  kommentar_controlling varchar2(4000),
  hinweise_produktion   clob
)
/
Wenn die Spalten KOMMENTAR_WERK, KOMMENTAR_CONTROLLING und HINWEISE_PRODUKTION nun volltextindiziert werden sollen, wird häufig (in Analogie zum normalen B-Baum-Index) der Weg gegangen, diese Spalten einzeln zu indizieren und später per AND bzw. OR kombinierte CONTAINS-Abfragen zu verwenden ...
/*
 * Abfragen auf mehrere Tabellenspalten NIEMALS so durchführen!
 */
select fall_id from produktions_hinweise
where
  contains(hinweise_produktion, 'Spezialmaschine') >0 and
  contains(kommentar_controlling, 'Abschreibungen') >0 and
  contains(kommentar_werk, 'Auslastung') >0
/
Dies führt allerdings immer zu sehr schlechter Performance. Denn es existieren nun drei Textindizes, die unabhängig voneinander ausgewertet werden. Die drei Zwischenergebnisse werden dann mit AND kombiniert. Insbesondere bei großen Datenmengen und wenn die Einzelabfragen nicht selektiv sind, führt dies zu sehr langen Antwortzeiten. Man kann als "Faustregel" festhalten, dass eine Volltextabfrage möglichst wenig CONTAINS()-Aufrufe (am besten nur einen) enthalten sollte. Das bedeutet aber auch, dass ein einziger Volltextindex auf alle drei Spalten erzeugt werden muss. Und das geht auch: Mit dem MULTI_COLUMN_DATASTORE.
Um den Multicolumn Datastore nutzen zu können, muss zunächst eine sog. Preference erzeugt werden; hier werden die Spalten, welche gemeinsam indiziert werden sollen, konfiguriert. Die folgenden SQL-Anweisungen erzeugen eine Preference vom Typ MULTICOLUMN_DATASTORE und legen die zu indizierenden Spalten der Tabelle als Inhalt fest.
begin
  ctx_ddl.create_preference(
    preference_name => 'kommentare_store'
   ,object_name     => 'MULTI_COLUMN_DATASTORE'
  );
  ctx_ddl.set_attribute(
    preference_name => 'kommentare_store'
   ,attribute_name  => 'columns'
   ,attribute_value => 'KOMMENTAR_WERK, KOMMENTAR_CONTROLLING, HINWEISE_PRODUKTION'
  );
end;
/
Um das Paket CTX_DDL nutzen zu können, benötigt man die Rolle CTXAPP (oder ein explizites EXECUTE-Privileg). Der Textindex wird im folgenden CREATE INDEX Kommando auf eine Tabellenspalte erzeugt (die tatsächlich angesprochenen Spalten sind in der Preference konfiguriert). Man kann nun eine der drei Dokumentspalten nehmen; aus Gründen der Übersichtlichkeit empfiehlt es sich jedoch, eine eigene "Dummy"-Spalte zu erzeugen.
alter table produktions_hinweise add (indexspalte char(1))
/
Nachtrag (danke für den Kommentar): Diese "Dummy-Spalte" muss bei einem Update der Tabellenzeile immer mit aktualisiert werden, damit der Index die Änderungen bemerkt. Das läßt sich z.B. durch einen Before-Update-Trigger realisieren.
create or replace trigger trg_produktions_hinweise
before update on produktions_hinweise
for each row
begin
  :new.indexspalte := :new.indexspalte;
end;
/
Nun kann der Index erzeugt werden.
create index idx_kommentare  on produktions_hinweise (indexspalte)
indextype is ctxsys.context
parameters ('
  datastore kommentare_store
  section group CTXSYS.AUTO_SECTION_GROUP'
)
/
Wie arbeitet der Multicolumn-Datastore nun? Obwohl es der Index formal für die Tabellenspalte INDEXSPALTE erzeugt wird, werden die tatsächlich indizierten Inhalte aus den drei in der Preference KOMMENTARE_STORE konfigurierten Spalten genommen. Die drei Dokumente pro Tabellenzeile werden zu einem einzigen zusammengefasst. Damit man jedoch noch in der Lage ist, auch gezielt in einer Spalte abzufragen, werden sie innerhalb des zusammengesetzten Dokuments durch XML Tags getrennt. Für das Beispiel wird (intern) also ein Dokument wie folgt generiert:
<KOMMENTAR_WERK>
  :
  Inhalt der Spalte KOMMENTAR_WERK hier  
  :
</KOMMENTAR_WERK>
<KOMMENTAR_CONTROLLING>
  :
  Inhalt der Spalte KOMMENTAR_CONTROLLING hier  
  :
</KOMMENTAR_CONTROLLING>
<HINWEISE_PRODUKTION>
  :
  Inhalt der Spalte HINWEISE_PRODUKTION hier  
  :
</HINWEISE_PRODUKTION>
Wie gesagt: Dieses Dokument wird nicht materialisiert - es wird nur während der Indexerstellung transient generiert. Nachdem es indiziert und alle Informationen in den Textindex aufgenommen wurden, wird es wieder zerstört. Für den Textindex gibt es pro Tabellenzeile jedoch nur noch ein Dokument, welches mehrere Abschnitte (Sections) hat. Damit diese Abschnitte separat abgefragt werden können, wurde dem CREATE INDEX Kommando der Parameter SECTION_GROUP CTXSYS.AUTO_SECTION_GROUP hinzugefügt (siehe oben). Eine kombinierte Abfrage (Beispiel oben) kann nun so aussehen:
select fall_id from produktions_hinweise
where
  contains(indexspalte, '(Spezialmaschine) and (Abschreibungen) and (Auslastung)') >0
/
Aus den drei CONTAINS-Aufrufen ist nun einer geworden; die AND bzw. OR-Verknüpfungen befinden sich nun innerhalb des CONTAINS(). Allerdings entspricht diese Abfrage der obigen nicht ganz genau. Denn das Token Spezialmaschine wurde oben gezielt in der Spalte HINWEISE_PRODUKTION gesucht; hier muss es nur in einer der Spalten vorkommen. Es wird also ein Zugriff auf die einzelnen Abschnitte des Dokuments benötigt ...
select fall_id from produktions_hinweise
where contains(
  indexspalte, 
  '(Spezialmaschine WITHIN (HINWEISE_PRODUKTION)) and 
   (Abschreibungen WITHIN (KOMMENTAR_CONTROLLING)) and 
   (Auslastung WITHIN (KOMMENTAR_WERK))'
) > 0
/
Mit der WITHIN-Klausel kann ein Token gezielt in einem der Abschnitt gesucht werden. Man kann die Begriffe nun also sehr flexibel innerhalb aller indizierten Spalten oder in bestimmten Spalten suchen. Alle anderen Operatoren der SQL-Funktion CONTAINS funktionieren wie gehabt.
Das entscheidende ist jedoch, dass diese Abfragen stets komplett in ein- und demselben Volltextindex ausgeführt wird. Es ergibt sich eine um Faktoren bessere Performance als mit einzelnen Textindizes ...
Übrigens: Man kann mit dem Multicolumn Datastore auch gewöhnliche relationale Tabellenspalten (bspw. Adressfelder) indizieren. Dann lassen sich die linguistischen Features (Fuzzy-Suche) auch für Abfragen in strukturierten Daten nutzen. Wie das geht und was man damit erreichen kann, ist in einem Tipp der deutschsprachigen Application Express-Community beschrieben.

Beliebte Postings