Donnerstag, 29. Januar 2009

Viele Abfragen auf einmal: Query Relaxation

Wenn man in Dokumentbeständen sucht, ist es ja vielfach so, dass man zunächst mit recht vielen Suchbegriffen anfängt und dann (wenn man nichts findet), die Abfrage allgemeiner macht, eben bis etwas kommt ...
Für eine CONTAINS-Abfrage mit Oracle TEXT könnte das in etwa folgendes bedeuten:
  • 1. Abfrage (keine Treffer)
    select * from dokument_tabelle
    where contains (dokument, 'oracle and datenbank and text and contains and parameters and 11g') > 0
    /
      
  • 2. Abfrage (bspw. 1 Treffer, passt aber nicht)
    select * from dokument_tabelle
    where contains (dokument, 'oracle and datenbank and (text or contains)') > 0
    /
      
  • 3. Abfrage (ausreichend Treffer)
    select * from dokument_tabelle
    where contains (dokument, 'oracle or datenbank') > 0
    /
    
Das bedeutet nun allerdings, dass man drei Abfragen absetzt. Man kann nun darüber nachdenken, einen solchen Prozeß zu automatisieren - wenn ein Suchwort in eine Maske eingegeben wird, wird zunächst genau danach gesucht, wenn nicht genug Treffer gefunden werden, wird das Stemming ($-Operator) verwendet und wenn es dann immer noch nicht reicht, probiert man es mit Fuzzy (?).
Es ist allerdings recht aufwändig, das selbst zu tun - man müsste jedesmal die Treffer zählen und bei Bedarf eine neue Abfrage absetzen: Die Antwortzeiten wären damit sicherlich irgendwann nicht mehr akzeptabel ...
Es gibt hierfür allerdings seit Oracle10g eine Funktion: Query Relaxation. Man kann alle diese Abfragen auf einmal übergeben und auch festlegen, wieviele Treffer man gerne haben möchte. Oracle TEXT erledigt dann alles mit nur einem CONTAINS-Aufruf.
select * from dokument_tabelle
where contains (
  dokument, 
  '<query>
     <textquery lang="ENGLISH" grammar="CONTEXT">
       <progression>
         <seq>{oracle} and {datenbank} and {text} and {contains} and {parameters} and {11g}</seq>
         <seq>{oracle} and {datenbank} and ({text} or {contains})</seq>
         <seq>{oracle} or {datenbank}</seq>
       </progression>
     </textquery>
     <score datatype="INTEGER" algorithm="DEFAULT"/>
  </query>'
)>0
and rownum <= 10;
Die CONTAINS-Abfrage wird im XML-Format übergeben; die sog. Query Templates werden hier verwendet (mehr Informationen). Man sieht sehr schön, dass die Abfragen der Reihe nach aufgeführt werden. Oracle TEXT führt alle Abfragen der Reihe nach aus, bis entweder das Ende erreicht ist (letztes seq-Tag oder bis die gewünschte Zahl der Treffer gefunden wurde. Die gewünschte Anzahl Treffer steckt in der zusätzlichen Bedingung and rownum <= 10.
Da nun alle Abfragen in ein und demselben CONTAINS Aufruf stecken, ist diese Variante wesentlich schneller und effizienter als das manuelle "Nacheinander-Aufrufen" von CONTAINS-Abfragen. Die Funktionalität zum Zugriff auf den Index wird eben nur einmal anstatt mehrmals aufgerufen; alle evtl. Initialisierungen finden nur einmal statt.
Das im Text oben beschriebene Beispiel (zuerst "normal", dann mit Stemming, danach Fuzzy) würde als Aufruf dann so aussehen ...
select * from dokument_tabelle
where contains (
  dokument, 
  '<query>
     <textquery lang="GERMAN" grammar="CONTEXT">
       <progression>
         <seq>{[suchwort]}</seq>
         <seq>{$[suchwort]}</seq>
         <seq>{FUZZY([suchwort], 75, 100, weight)}</seq>
         <seq>{FUZZY([suchwort], 60, 200, weight)}</seq>
         <seq>{FUZZY([suchwort], 40, 300, weight)}</seq>
       </progression>
     </textquery>
     <score datatype="INTEGER" algorithm="DEFAULT"/>
  </query>'
)>0
where rownum < 10;
Hier habe ich mal den Operator FUZZY anstelle des einfachen Fragezeichens (?) verwendet - der Unterschied ist, dass FUZZY parametrisiert werden kann. Mehr Informationen zur Syntax des FUZZY-Operators finden sich in der Oracle Dokumentation: TEXT Reference.

Donnerstag, 8. Januar 2009

Fragmentierung feststellen mit INDEX_STATS

Wie fragmentiert ist mein Text Index? Wann soll ich den Index optimieren? Dies sind wichtige Fragestellungen, um eine optimale Zugriffsperformance gewährleisten zu können. Ein nicht optimaler Index zeigt sich in der Fragmentierung der $I-Tabelle z.B. bewirkt durch zu kleine SYNC Intervalle bzw. durch vorhandenen Garbage entstanden durch DELETE bzw. UPDATE Operationen.
Wie kann ich nun den Grad der Fragmentierung monitoren? Das Package CTX_REPORT liefert mit der Prozedur INDEX_STATS eine einfache Möglichlichkeit. Folgender Aufruf demonstriert die Nutzung. Die Hilfstabelle REPORT ist dabei notwendig, um die Inhalte zu speichern.
DROP TABLE ausgabe;
CREATE TABLE ausgabe (resultat CLOB);
 
  declare
    ergebnis clob := null;
  begin
    ctx_report.index_stats(index_name=>'IDX_TEXT',report=>ergebnis,stat_type=>null);
    insert into ausgabe values (ergebnis);
    commit;
    dbms_lob.freetemporary(ergebnis);
  end;
  /
 
set long 32000
set head off
set pagesize 10000
SELECT * FROM ausgabe; 
 ....
-- ein Ausschnitt aus dem Ergebnis
---------------------------------------------------------------------------
                         FRAGMENTATION STATISTICS
---------------------------------------------------------------------------

total size of $I data:                                                398

$I rows:                                                              105
estimated $I rows if optimal:                                         105
estimated row fragmentation:                                          0 %

garbage docids:                                                         0
estimated garbage size:                                                 0

most fragmented tokens:
  WIRTSCHAFT (0:TEXT)                                                 0 %
  WIRD (0:TEXT)                                                       0 %
  WI
Das Ergebnis zeigt, dass weder Garbage noch Fragmentierung des Index vorhanden ist.
Da die Durchführung dieser Prozedur bei grossen Indizes oder Partitionen sehr lange dauern kann, kann es sinnvoll sein, Logging einzuschalten. So ist es möglich den Fortschritt (d.h. die ge-scannten Zeilen der $I Tabelle) der jeweiligen Operation monitoren zu können.
Im nächsten Beispiel wird das ganze Prozedere mit zusätzlichem Logging an einem größeren Index demonstriert. Paralleles Monitoren der LOGGING-Tabelle würde den Stand der gescannten Zeilen anzeigen.
DROP TABLE ausgabe;
CREATE TABLE ausgabe (resultat CLOB);
  declare
    ergebnis clob := null;
  begin
        ctx_output.start_log('index_statistik'); 
    ctx_report.index_stats(index_name=>'TEXT_IND',report=>ergebnis,stat_type=>null);
    insert into ausgabe values (ergebnis);
        ctx_output.end_log; 
    commit;
    dbms_lob.freetemporary(ergebnis);
  end;
  /
Nachdem die Prozedur INDEX_STATS erfolgreich durchgeführt ist, kann man wie eben die ERGEBNIS Tabelle monitoren mit:
spool fragment.lst
set long 32000
set head off
set pagesize 10000
SELECT * FROM ausgabe;
... 
spool off
Im Abschnitt "FRAGMENTATION STATISTICS" ist nun folgende Information zu finden.
---------------------------------------------------------------------------
                         FRAGMENTATION STATISTICS
---------------------------------------------------------------------------

total size of $I data:                            105,598,019 (100.71 MB)

$I rows:                                                        2,325,672
estimated $I rows if optimal:                                     538,712
estimated row fragmentation:                                         77 %

garbage docids:                                                         0
estimated garbage size:                                                 0

most fragmented tokens:
  126 (0:TEXT)                                                       96 %
  124 (0:TEXT)                                                       96 %
  12

Montag, 8. Dezember 2008

Ergebnisse zählen: Nicht mit count(*), sondern mit COUNT_HITS!

Wenn man Oracle TEXT in einer Applikation nutzt und Abfragen mit der CONTAINS-Funktion durchführt, möchte man fast immer auch die Anzahl der Treffer auf der Ergebnisseite anzeigen. Und der erste, naheliegende Gedanke ist, hierfür ein SELECT COUNT(*) zu verwenden - die Abfrage findet ja auch mit einem SELECT statt.
select count(*) from dokument_tab where contains(dokument, 'Bundeskanzler') > 0
Gerade bei Oracle TEXT ist diese Variante jedoch sehr ineffektiv - denn die Datenbank muss hierfür sowohl auf den Volltextindex als auch auf die Tabelle zugreifen. Besser ist es, die dafür vorgesehene Funktion COUNT_HITS im PL/SQL-Paket CTX_QUERY zu nutzen.
set serveroutput on

declare
  v_number number;
begin
  v_number := ctx_query.count_hits(
    index_name => 'MY_FULLTEXT_IDX',
    text_query => 'oracle and text',
    exact =>      true
  );
  dbms_output.put_line('Anzahl Treffer: '||v_number);
end;
/
 
Anzahl Treffer: 2657
Man muss den Namen des Index kennen, um die Funktion nutzen zu können; mit der Dictionary View CTX_USER_INDEXES ist dieser aber leicht zu ermitteln. Interessant ist noch der Parameter exact - gibt man hier false an, so arbeitet die Funktion schneller, dafür ungenauer: Seit der letzten Indexoptimierung gelöschte und veränderte Dokumente werden dann nicht berücksichtigt; der Wert ist also zu hoch. Direkt nach einer Indexoptimierung liefern sowohl exact => true als auch exact => false gleiche Ergebnisse zurück. In allen Fällen ist CTX_QUERY.COUNT_HITS schneller als ein SELECT COUNT(*).
Mehr Information in der Oracle Dokumentation - TEXT Reference.

Montag, 24. November 2008

Suchverhalten der Anwender monitoren: Abfrage-Analyse mit CTX_REPORT

Was sind die häufigsten Begriffe, nach denen die Anwender suchen? Welche Abfragen laufen ständig ins Leere und liefern kein Resultat? Diese Fragen sind häufig wichtig, um "anwenderfreundliche" Applikationen gestalten zu können oder um die Suchanfragen optimal zur Verfügung zu stellen.
Ab Oracle 10g ist es möglich mit CTX_OUTPUT ein spezielles Logging für Suchanfragen anzustossen und mit CTX_REPORT eine Analyse dieser Loginformationen durchzuführen.
Im Folgenden demonstriert ein Beispiel die Anwendung.

Wie in einem unserer vorangegangenen
Blogs schon demonstriert, wird das Logging in den Sessions, in denen die Abfragen laufen, aktiviert. Dazu legen wir zuerst das Log Verzeichnis fest.

execute CTXSYS.CTX_ADM.SET_PARAMETER ('LOG_DIRECTORY','/tmp'); 

Danach starten wir das Query-Logging mit folgendem Aufruf:

exec CTX_OUTPUT.START_QUERY_LOG('querylog');

Die folgenden SELECT Statements werden nun in einer Logdatei aufgezeichnet. Dabei nehmen als Grundlage, die in Blog generierte Tabelle TEXTTABELLE und den dazugehörenden Textindex.

SELECT * FROM texttabelle WHERE contains(dokument, 'USA')>0;
SELECT * FROM texttabelle WHERE contains(dokument, 'USA')>0;
SELECT * FROM texttabelle WHERE contains(dokument, 'USA')>0;
SELECT * FROM texttabelle WHERE contains(dokument, 'USA')>0;
SELECT * FROM texttabelle WHERE contains(dokument, 'USA and Messe')>0; 
SELECT * FROM texttabelle WHERE contains(dokument, 'Software')>0;
SELECT * FROM texttabelle WHERE contains(dokument, 'Hamburg')>0;

Das Logging wird dann mit folgendem Aufruf beendet:

execute CTX_OUTPUT.END_QUERY_LOG;

Wie schon erwähnt, ist diese Art von Logging vergleichbar mit dem Logging mit CTX_OUTPUT.START_LOG (siehe Blog). Es wird eine Datei mit Namen QUERYLOG im Logverzeichnis /tmp erzeugt. Allerdings liegt diese Logdatei in einem speziellen XML Format vor, wie in unserem Beispiel zu sehen ist:


<QuerySet><TimeStamp>10:30:42 11/20/08 </TimeStamp><IndexName>IDX_TEXT</IndexName><Query>USA</Query><ReturnHit>Yes</ReturnHit><QueryTag></QueryTag></QuerySet>
<QuerySet><TimeStamp>10:30:42 11/20/08 </TimeStamp><IndexName>IDX_TEXT</IndexName><Query>USA</Query><ReturnHit>Yes</ReturnHit><QueryTag></QueryTag></QuerySet>
<QuerySet><TimeStamp>10:30:42 11/20/08 </TimeStamp><IndexName>IDX_TEXT</IndexName><Query>USA</Query><ReturnHit>Yes</ReturnHit><QueryTag></QueryTag></QuerySet>
<QuerySet><TimeStamp>10:30:42 11/20/08 </TimeStamp><IndexName>IDX_TEXT</IndexName><Query>USA</Query><ReturnHit>Yes</ReturnHit><QueryTag></QueryTag></QuerySet>
<QuerySet><TimeStamp>10:30:42 11/20/08 </TimeStamp><IndexName>IDX_TEXT</IndexName><Query>USA and Messe</Query><ReturnHit>Yes</ReturnHit><QueryTag></QueryTag></QuerySet>
<QuerySet><TimeStamp>10:30:42 11/20/08 </TimeStamp><IndexName>IDX_TEXT</IndexName><Query>Software</Query>><ReturnHit>Yes</ReturnHit><QueryTag></QueryTag></QuerySet>
<QuerySet><TimeStamp>10:30:42 11/20/08 </TimeStamp><IndexName>IDX_TEXT</IndexName><Query>Oracle</Query>><ReturnHit>No</ReturnHit><QueryTag></QueryTag></QuerySet>

Wie lässt sich nun diese Datei, besonders wenn sie grösser als in unserem Minibeispiel ist, bequem auslesen?
Das Package CTX_REPORT mit der Prozedur QUERY_LOG_SUMMARY liefert eine mögliche Vorgehensweise. Diese Prozedur, als CTXSYS User ausgeführt, liest die TOP N Abfragen und liefert die Resultate in einer PL/SQL Table vom Typ CTX_REPORT.QUERY_TABLE. Die Zahl N ist dabei definierbar über den Parameter ROW_NUM. Folgender Beispiel-Code liest unsere Log-Datei QUERYLOG aus und liefert das Ergebnis der TOP 10 Abfragen:

SQL> connect ctxsys
Enter password:
Connected.
SQL> set serveroutput on

SQL> declare
  2  logentry ctx_report.query_table;
  3  begin
  4  ctx_report.query_log_summary('querylog','idx_text', logentry, row_num=>10);
  5  for i in 1..logentry.count 
  6  loop
  7  dbms_output.put_line(logentry(i).query || ' kommt '||logentry(i).times||' Mal vor');
  8  end loop;
  9  end;
  10  /
USA kommt 4 Mal vor
USA and Messe kommt 1 Mal vor
Software kommt 1 Mal vor

PL/SQL procedure successfully completed.

Standardmässig werden dabei nur die häufigsten Treffer der erfolgreichen Abfragen ausgewertet, da der Parameter MOST_FREQ auf TRUE steht. Manchmal ist es allerdings interessanter, die fehlgeschlagenen Abfragen zu dokumentieren. Der Parameter HAS_HIT (Defaultwert: TRUE) kontrolliert das Verhalten der Auswertung bzgl. der erfolgreichen Treffer. Setzen wir diesen Wert auf FALSE, werden nur die Resultate angezeigt, die von fehlgeschlagenen Suchabfragen stammen.
Folgendes Beispiel demonstriert das Verhalten:

SQL> set serveroutput on
SQL> declare
  2  logentry ctx_report.query_table;
  3  begin
  4  ctx_report.query_log_summary('querylog','idx_text', logentry, row_num=>10, has_hit=>false);
  5  for i in 1..logentry.count 
  6  loop
  7  dbms_output.put_line(logentry(i).query || ' kommt '||logentry(i).times||' Mal vor');
  8  end loop;
  9  end;
 10  /
Hamburg kommt 1 Mal vor

PL/SQL procedure successfully completed.

Um solches Fehlschlagen von Suchabfragen zu verhindern, könnte man z.B. in den Applikationen bessere Hilfestellung für den User anbieten oder über Synonyme (Thesaurus) alternative Schreibweisen ermöglichen.
Mehr dazu in einem unserer nächsten Blog-Veröffentlichungen. Viel Spass beim Ausporbieren ...

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.

Freitag, 24. Oktober 2008

Monitoren von Oracle Text Indizes: Grundsätzliche Funktionen

Wie kann man den Textindex monitoren? Welche Möglichkeiten gibt es? Dazu stehen spezielle Data Dictionary Views, Packages oder ab Oracle Database 11g die Enterprise Manager Funktionalität zur Verfügung. Im folgenden Beispiel nutzen wir eine Tabelle die ca. 600 000 Zeilen besitzt und die mit einer TEXT-Spalte mit Kurztexten ausgestattet ist.
Zuerst erzeugen wir den Textindex TEXT_COMP und monitoren gleichzeitig den Indexvorgang. Mit dem Aufruf CTX_ADM.SET_PARAMETER läßt sich dabei das Logverzeichnis festlegen - in unserem Fall /tmp. Mit CTX_OUTPUT.START_LOG starten wir den Logvorgang. Folgender Aussschnitt zeigt die Verwendung:

SQL> execute CTXSYS.CTX_ADM.SET_PARAMETER ('LOG_DIRECTORY','/tmp'); 
PL/SQL procedure successfully completed.

SQL> execute CTX_OUTPUT.START_LOG('textlog');
PL/SQL procedure successfully completed.

SQL> CREATE INDEX text_comp ON comp_test(text)
  2  INDEXTYPE IS ctxsys.context PARAMETERS('memory 100M');
Index created.

SQL> execute CTX_OUTPUT.END_LOG;
PL/SQL procedure successfully completed.

Bevor wir uns die Logtabelle ansehen, sollten wir zuerst überprüfen, ob der Index ohne Fehler erzeugt worden ist. Folgendes Kommando gibt Aufschluss darüber. Diese Überprüfung sollte nach jeder Indexerzeugung durchgeführt werden.

SQL> SELECT err_index_name, err_timestamp, err_textkey, err_text
   2 FROM ctx_user_index_errors ORDER BY err_index_name, err_timestamp;
no rows selected

In unserem Fall liegt kein Fehler vor. Falls diese Tabelle Zeilen enthält, gibt die Spalte ERR_TEXTKEY Aufschluss über die ROWID des entsprechenden Dokuments, das den Fehler hervorgerufen hat. Damit hat man einen Startpunkt, um den Fehler zu beheben.

Die Datei "textlog" im Verzeichnis /tmp gibt nun Auskunft über den Vorgang der Indizierung. Sehr wichtig dabei ist, dass die Angabe von "PARAMETERS ('MEMORY 100M')" zu einer Beschleunigung des Indizierungsvorgangs führt. Der Wert 100M bestimmt die Größe der Token und Mapping Tabelle im Memory, bevor die Informationen in die entsprechenden Tabellen geschrieben werden. Der Vorgang wird so lange wiederholt bis der Index erzeugt worden. Daher ist es sehr sinnvoll den Parameter Memory zu setzen und den Wert so gross wie möglich zu wählen. Folgender Ausschnitt zeigt ein Beispiel einer Logtabelle und gibt Aufschluss über die Nutzung des Memory Parameters.

Oracle Text, 11.1.0.6.0
14:00:28 10/24/08 begin logging
14:00:49 10/24/08 populate index: COMP.TEXT_COMP
14:00:49 10/24/08 Begin document indexing
14:00:49 10/24/08 100 documents indexed
14:00:49 10/24/08 200 documents indexed
14:00:49 10/24/08 300 documents indexed
14:00:49 10/24/08 400 documents indexed
14:00:49 10/24/08 500 documents indexed
...
14:01:58 10/24/08 118200 documents indexed
14:01:58 10/24/08 Errors reading documents: 0
14:01:58 10/24/08 Index data for 118236 documents to be written to database
14:01:58 10/24/08    memory use: 98846623
14:01:58 10/24/08 Begin sorting the inverted list.
14:01:58 10/24/08 End sorting the inverted list.
14:01:58 10/24/08 Writing index data ($I) to database.
14:02:06 10/24/08 Wrote 313223 rows (12067 big rows) to $I.
14:02:06 10/24/08 Writing index data ($R) to database.
14:02:06 10/24/08    index data written to database.
... 

Zusätzlich bietet die Verwendung des Package CTX_REPORT weitere Möglichkeiten, Informationen zum Textindex zu erhalten. Im folgenden sollen 2 Beipiele dies illustrieren.
Möchte man z.B. die Größe des Textindex herausfinden, kann der Aufruf von CTX_REPORT.INDEX_SIZE einen detaillierten Bericht dazu liefern. Abgesehen von der Gesamtgröße des Index erhält man eine detaillierte Auflistung über die Größe der zugehörigen $I, $K, $R Tabellen usw. und der Zuordnung zu den Tablespaces.
Folgender Abschnitt zeigt einen Auszug aus dem Report:

SQL> col ausgabe format a150
SQL> set long 2000000000 head off pagesize 10000
SQL> select ctx_report.index_size('TEXT_COMP') as ausgabe from dual;

===========================================================================
                       INDEX SIZE FOR COMP.TEXT_COMP
===========================================================================
TABLE:                               COMP.DR$TEXT_COMP$I
TABLESPACE NAME:                     USERS
BLOCKS ALLOCATED:                                                    13056
BLOCKS USED:                                                         12752
BYTES ALLOCATED:                                   106,954,752 (102.00 MB)
BYTES USED:                                         104,464,384 (99.63 MB)
LOB SEGMENT:                         COMP.SYS_LOB0000094903C00006$$
TABLE NAME:                          COMP.DR$TEXT_COMP$I
LOB COLUMN:                          TOKEN_INFO
TABLESPACE NAME:                     USERS
BLOCKS ALLOCATED:                                                        8

...

TOTALS FOR INDEX COMP.TEXT_COMP
---------------------------------------------------------------------------
TOTAL BLOCKS ALLOCATED:                                              16040
TOTAL BLOCKS USED:                                                   15487
TOTAL BYTES ALLOCATED:                             131,399,680 (125.31 MB)
TOTAL BYTES USED:                                  126,869,504 (120.99 MB)

Benötigt man ein zugehöriges CREATE INDEX Skript, ist es nicht notwendig, im Data Dictionary nach den Eigenschaften des Index zu suchen. Das einfache Ausführen der Funktion CREATE_INDEX_SCRIPT, liefert eine Liste von DDL Kommandos, die einen vergleichbaren Index erzeugen:

select ctx_report.create_index_script('TEXT_COMP') from dual
begin
  ctx_ddl.create_preference('"TEXT_COMP_DST"','DIRECT_DATASTORE');
end;
/

...
begin
  ctx_output.start_log('TEXT_COMP_LOG');
end;
/

create index "COMP"."TEXT_COMP"
  on "COMP"."COMP_TEST"
      ("TEXT")
  indextype is ctxsys.context
  parameters('
    datastore       "TEXT_COMP_DST"
    filter          "TEXT_COMP_FIL"
    section group   "TEXT_COMP_SGP"
    lexer           "TEXT_COMP_LEX"
    wordlist        "TEXT_COMP_WDL"
    stoplist        "TEXT_COMP_SPL"
    storage         "TEXT_COMP_STO"
  ')
/

begin
  ctx_output.end_log;
end;
/

Wer allerdings schon Oracle Database 11g einsetzt und die Möglichkeit besitzt auf Enterprise Manager Database Control zuzugreifen, kann sich das Skripting sparen. Alle hier aufgelisteten Funktionalitäten sind durch einfaches Navigieren im Text Manager unter "Schema => Text Indexes" zu finden.

Dienstag, 7. Oktober 2008

Oracle TEXT unterstützt Office2007 - mit Patchset 11.1.0.7

Die beste Nachricht zuerst: Oracle Text unterstützt nun Office2007.
Für TEXT-Anwender ist das kürzlich erschienene Patchset 11.1.0.7 sehr wichtig, denn der bislang enthaltene Verity-Filter wurde mit diesem Patchset durch Oracle Outside in HTML Export ersetzt. Dadurch ergeben sich Konsequenzen für die unterstützten Dokumentformate.
Einige Formate, die Verity bislang unterstütze werden nicht mehr unterstützt, bspw. "Applix Words (AW), versions 3.11, 4.0, 4.1, 4.2, 4.3, 4.4". Dafür ist (endlich) eine Unterstützung für Office 2007 vorhanden. Genaueres findet Ihr ...
  • ... in der README-Datei des Patchsets (da stehen die Dokumentformate drin, die nicht mehr unterstützt werden) ...
  • ... und in der Text Reference, Appendix B - da stehen die Formate drin, die nun unterstützt werden.
Wer die Filter von Oracle TEXT nutzt (und wer nutzt die nicht?) sollte hier unbedingt mal reinsehen, bevor er das Patchset einspielt.

Beliebte Postings