Mittwoch, 2. Dezember 2009

Der CTXCAT-Index

Neben dem "normalen" Context Index gibt es schon seit geraumer Zeit den kaum bekannten "kleinen Bruder" CTXCAT. Dieses Posting möchte ich daher gerne dem CTXCAT-Index widmen. Möchte man die Unterschiede in einem Satz herausarbeiten, so bietet der CTXCAT-Index zunächst mal weniger Features, ist für kleinere Dokumente gedacht und arbeitet synchron, ist also einfacher zu verwalten. Im einzelnen ...
  • Der CTXCAT Index ist für kleinere Textfragmente gedacht - in der Dokumentation ist von "wenigen Zeilen" die Rede; um das auf den Punkt zu bringen; das Maximum sollten so 50 Worte sein ...
  • Der Index arbeitet komplett synchron - per DML gemachte Änderungen sind sofort im Index sichtbar. Der Prozess des Index-Synchronisierens und -Optimierens fällt hier also weg.
  • Es werden weniger "Features" unterstützt - so gibt es keine Data Stores wie beim CONTEXT-Index. Die zu indizierenden Daten müssen also genau so in der Tabellenspalte drinstehen.
  • XML wird nicht unterstützt, da CTXCAT keine Section Groups kennt
  • CTXCAT unterstützt allerdings sog. Sub-Indexes, mit denen man Mixed Queries unterstützen kann. Eine Mixed Query ist eine Abfrage, die sowohl relationale (strukturierte) als auch Volltextkriterien enthält.
Der CTXCAT-Index wird genauso angelegt wie ein CONTEXT-Index, nur mit einem anderen Indextypen ... Als Beispiel nehmen wir eine Art Umfragetabelle, in der die Ergebnisse einer Umfrage zur "Servicequalität" gespeichert werden ...
create table umfrageergebnis(
  id        number(10),
  altersang number(3),
  note      number(1),
  kommentar varchar2(1000)
);
Der Index kann sofort erzeugt werden ...
create index suche_idx on UMFRAGEERGEBNIS(KOMMENTAR)
indextype is ctxsys.ctxcat;
Wie der CONTEXT-Index unterstützt auch der CTXCAT-Index linguistische Features. Das folgende Beispiel legt den Index explizit als Case-Sensitiv fest und definiert den Bindestrich als sog. Printjoin.
begin
  ctx_ddl.create_preference('CAT_LEXER','BASIC_LEXER');
  ctx_ddl.set_attribute('CAT_LEXER','MIXED_CASE','YES');
  ctx_ddl.set_attribute('CAT_LEXER','PRINTJOINS', '-');
end;
/

create index suche_idx on UMFRAGEERGEBNIS(KOMMENTAR)
indextype is ctxsys.ctxcat
parameters ('LEXER CAT_LEXER');
Stopwortlisten werden natürlich analog unterstützt. Nun kann man ein paar Dokumente einpflegen ...
insert into umfrageergebnis values (1, 35, 1, 'service war ausgezeichnet');
insert into umfrageergebnis values (2, 55, 3, 'ganz gut');
insert into umfrageergebnis values (3, 45, 5, 'ich wurde unfreundlich bedient');
insert into umfrageergebnis values (4, 42, 2, 'etwas unfreundlich, aber fehlerfrei und korrekt');
Und man kann sofort abfragen ...
select * from umfrageergebnis where catsearch(kommentar, 'unfreundlich', null) > 0;

        ID  ALTERSANG       NOTE KOMMENTAR
---------- ---------- ---------- ------------------------------------------------
         3         45          5 ich wurde unfreundlich bedient
         4         42          2 etwas unfreundlich, aber fehlerfrei und korrekt
Interessant wären nun die Mixed Queries. Angenommen, wir interessieren uns für die Ergebnisse, die aufgrund von "Unfreundlichkeit" schlecht waren. Also brauchen wir noch ein relationales Kriterium auf der Schulnote. Zunächst also den Index wieder droppen ...
drop index suche_idx;
Und nun erzeugen wir ein Index Set; dieses enthält die strukturierten "Unter-Indizes". Dem Index-Set wird dann ein Index auf die Spalten "Note", "Alter" und auf die Kombination "Note und Alter" hinzugefügt.
begin
  ctx_ddl.create_index_set('umfrage_iset');
  ctx_ddl.add_index('umfrage_iset','note'); 
  ctx_ddl.add_index('umfrage_iset','altersang'); 
  ctx_ddl.add_index('umfrage_iset','note, altersang');
end;
Danach den Index neu anlegen ...
create index suche_idx on UMFRAGEERGEBNIS(KOMMENTAR)
indextype is ctxsys.ctxcat
parameters ('LEXER CAT_LEXER INDEX SET UMFRAGE_ISET');
Nun wieder abfragen ... mit dem CATSEARCH-Operator ...
select * from umfrageergebnis where catsearch(kommentar, 'unfreundlich', 'note >= 4') > 0;

        ID  ALTERSANG       NOTE KOMMENTAR
---------- ---------- ---------- ------------------------------------------------
         3         45          5 ich wurde unfreundlich bedient
Schauen wir uns mal den Index genauer an. Wie der CONTEXT-Index besteht auch der CTXCAT-Index aus Tabellen, die ins Schema gelegt werden, und die mit dem Präfix DR$ beginnen. Nur ist es beim CTXCAT-Index genau eine-Tabelle ...
SQL> desc DR$SUCHE_IDX$I
 Name                                      Null?    Typ
 ----------------------------------------- -------- ------------------

 DR$TOKEN                                  NOT NULL VARCHAR2(64)
 DR$TOKEN_TYPE                             NOT NULL NUMBER(3)
 DR$ROWID                                  NOT NULL ROWID
 DR$TOKEN_INFO                             NOT NULL RAW(2000)
 ALTERSANG                                 NOT NULL NUMBER(3)
 NOTE                                      NOT NULL NUMBER(1)
Und hier seht Ihr, dass der CTXCAT-Index wesentlich simpler aufgebaut ist als der CONTEXT-Index. So werden ROWIDs direkt verwendet (der CONTEXT-Index generiert die kompakteren DOCIDs). Die soeben erzeugten "Sub-Indexes" führen dazu, dass zusätzliche Spalten angelegt werden. Das alles führt dazu, dass CTXCAT-Indizes recht viel Platz verbrauchen - die ROWIDs belegen 10 Byte und auch die Werte der zusätzlichen Spalten werden für jedes Wort (Token) dupliziert. Es ist einleuchtend, warum dieser Index nur für kleine Textfragmente geeignet ist - bei großen Dokumenten würde er schlicht explodieren ...
Hierzu ein kleiner Test. Wir haben eine Tabelle mit ca. 300.000 kleineren Texten (ein CTXCAT-Index kommt also in Frage). Die Tabelle ist (festgestellt mit diesem Skript) ca. 30MB groß.
select * from table(get_space_info('TEST_CTXCAT'));

SEGMENT_NAME                     COLUMN_NAME                    PARTITION_NAME                   SEGMENT_TYPE                     ALLOC_BYTES FREE_BYTES
-------------------------------- ------------------------------ -------------------------------- -------------------------------- ----------- ----------
TEST_CTXCAT                                                                                      TABLE                               31457280      65536

1 Zeile wurde ausgewählt.
Auf diese Tabelle legen wir nun einen CTXCAT-Index und einen CONTEXT-Index und stellen auch hierfür die jeweilige Größe fest. Zunächst der CTXCAT-Index (er besteht nur aus einer einzigen "DR$"-Tabelle.
select
  segment_name, 
  column_name, 
  segment_type, 
  alloc_bytes / 1048576 alloc_mb, 
  free_bytes / 1048576 free_mb 
from table(get_space_info('DR$IDX_TEST_CTXCAT$I'))

SEGMENT_NAME                     COLUMN_NAME                              SEGMENT_TYPE                     ALLOC_MB  FREE_MB
-------------------------------- ---------------------------------------- -------------------------------- -------- --------
DR$IDX_TEST_CTXCAT$I                                                      TABLE                               80,00     0,00
DR$IDX_TEST_CTXCAT$R             DR$ROWID                                 INDEX                               62,00     0,56
DR$IDX_TEST_CTXCAT$X             DR$TOKEN,DR$TOKEN_TYPE,DR$ROWID          INDEX                               59,00     0,14
Das wären also ca. 200MB für den CTXCAT-Index. Schauen wir uns den CONTEXT-Index an. Hierfür muss die GET_SPACE_INFO-Funktion für alle zum CONTEXT-Index gehörenden DR$-Tabellen ausgeführt werden - das wären die $I, die $N, die $R und die $K-Tabelle.
TABLE SEGMENT_NAME                     COLUMN_NAME                              SEGMENT_TYPE                     ALLOC_MB  FREE_MB
----- -------------------------------- ---------------------------------------- -------------------------------- -------- --------
$I    DR$IDX_TEST_CONTEXT$I                                                     TABLE                               11,00     0,95  
$I    SYS_LOB0000225699C00006$$        TOKEN_INFO                               LOBSEGMENT                           0,06     0,00
$I    DR$IDX_TEST_CONTEXT$X            TOKEN_TEXT,TOKEN_TYPE,TOKEN_FIRST, ...   INDEX                                0,19     0,03
$I    SYS_IL0000225699C00006$$                                                  LOBINDEX                             0,06     0,03

$N    SYS_IOT_TOP_225707               NLT_DOCID                                INDEX                                0,06     0,03

$R    DR$IDX_TEST_CONTEXT$R                                                     TABLE                                0,06     0,00
$R    SYS_LOB0000225704C00002$$        DATA                                     LOBSEGMENT                           6,00     0,13
$R    SYS_IL0000225704C00002$$                                                  LOBINDEX                             0,06     0,00

$K    SYS_IOT_TOP_225702               TEXTKEY                                  INDEX                                9,00     0,59
Zusammengezählt in etwa 25MB. Alle linguistischen Einstellungen Stopwortlisten, Lexer sind identisch. Man sieht hier sehr deutlich den Unterschied.
Wann ist also ein CTXCAT-Index sinnvoll ... ? Naja, vor allem dann, wenn Ihr kleine Datenmengen in der Tabellenspalte habt, auf einen ständig synchronen Volltextindex Wert legt und Mixed Queries benötigt. Letzteres ist ab Oracle11g allerdings auch mit dem CONTEXT Index möglich (Composite Domain Index). Wenn der Text aber in mehreren Spalten ist oder die Textfragmente größer werden, dann werden CTXCAT-Indizes schnell zu groß. Ein CONTEXT-Index mit Multi-Column Datastore wesentlich kompakter und effizienter. Der CTXCAT-Index ist halt, wie eingangs gesagt, der "kleine Bruder" des CONTEXT Index und eignet sich nur für bestimmte Fälle ...

Montag, 2. November 2009

Hochverfügbarer Textindex mit Schattenindex-Technologie

Heutzutage müssen auch Anwendungen mit Oracle Textindizes hochverfügbar sein. Allerdings kann schon ein REBUILD des Index dazu führen, dass der Index nicht mehr online zur Verfügung steht. Ein Neuaufbau des Index könnte z.B. aus mehreren Gründen nötig werden:
  • Spezielle Index-Preferences bzw. -Optionen wie Lexereinstellungen sollen geändert werden oder Stoppwörter hinzugefügt werden.
  • Aus Maintenance- und Performance-Gründen soll der Index neu aufgebaut werden - um z.B. eine stark vergrößerte $I Tabelle zu optimieren.
Ein REBUILD des Index ist bis einschliesslich Oracle Database 10g nicht immer vollständig online möglich. Eine mögliche Lösung wäre einen zusätzlichen zweiten Schatten-Index manuell zu erzeugen und diesen mitzupflegen. Die Tabelle könnte dabei folgendermassen aussehen:

CREATE TABLE mytable (text CLOB, dummy1 CHAR, dummy2 CHAR);

Da nur ein einziger Textindex auf einer Spalte möglich ist, wird die Indizierung über den Einsatz von USER_DATA_STORE bzw. MULTI_COLUMN_DATATORE gelöst. Diese erlauben es, einen Index auf der Spalte DUMMY1 zu erzeugen, aber die aktuellen zu indizierenden Daten werden von einer anderen Spalte z.B. TEXT geliefert. Die Abfragen sehen also folgendermassen aus:

... WHERE CONTAINS (dummy1, '') > 0

Im Falle eines Neuaufbaus steht die Spalte DUMMY2 zur Verfügung, die ihre Daten ebenfalls aus der Spalte TEXT erhält. Falls der Aufbau beendet ist, müssen die Queries folgendermassen geändert werden:

... WHERE CONTAINS (dummy2, '') > 0

In Oracle Database 11g ist es nun möglich, ein vollständiges REBUILD online durchzuführen - entweder in einem Ein-Schritt-Verfahren oder um mehr Kontrolle zu gewährleisten bzw. falls Partitionen verwendet werden als mehrstufiger Prozess. Die Idee dabei ist der Einsatz eines Schatten-Index (auch Shadow Index), der parallel zum urspünglichen Indexaufbau mitgeführt wird. Um die Nutzung an einem Beispiel zu demonstrieren, nehmen wir folgenden Index, der auf eine Tabelle SEC_TABLE und der Spalte TEXT erzeugt wurde als Grundlage.

CREATE INDEX sec_ind ON sec_table(text) INDEXTYPE IS ctxsys.context PARALLEL 4;
SELECT * FROM ctx_user_index_errors;
no rows selected

Nun wollen wir eine zusätzliche Spalte für Sprachen ergänzen und den folgenden Multi-Lexer nutzen.

execute ctx_ddl.create_preference('en_lx','basic_lexer');
execute ctx_ddl.set_attribute('en_lx','index_themes','yes');
execute ctx_ddl.create_preference('f_lx','basic_lexer');
execute ctx_ddl.set_attribute('f_lx','base_letter','yes');
execute ctx_ddl.create_preference('d_lx','basic_lexer');
execute ctx_ddl.set_attribute('d_lx','composite','german');
execute ctx_ddl.set_attribute('d_lx','mixed_case','yes');
execute ctx_ddl.set_attribute('d_lx','alternate_spelling','german');
begin
ctx_ddl.create_preference('multi_lx','multi_lexer');
ctx_ddl.add_sub_lexer('multi_lx','german','d_lx');
ctx_ddl.add_sub_lexer('multi_lx','french','f_lx');
ctx_ddl.add_sub_lexer('multi_lx','default','en_lx');
end;
PL/SQL procedure successfully completed.

ALTER TABLE sec_table ADD (lang varchar2(10) default 'de');

Nun erzeugen wir den Schattenindex in einem Einschrittverfahren. Laut Syntax ist zwar die Angabe einer DOP (Degree Of Parallelism) möglich, leider aber noch nicht im aktuellen Release verwendbar. Die Dokumentation gibt dazu folgenden Hinweis: "Reserved for future use. Specify the degree of parallelism. Parallel operation is not supported in the current release."

SQL> execute CTX_DDL.RECREATE_INDEX_ONLINE('SEC_IND','REPLACE LEXER multi_lx language column lang');
PL/SQL procedure successfully completed.

Schaut man zwischendurch in CTX_USER_INDEXES kann man den Aufbau des temporären Index (siehe Präfix RIO) monitoren.

SQL> SELECT idx_name, idx_table, idx_status FROM ctx_user_indexes;
IDX_NAME                       IDX_TABLE                      IDX_STATUS
------------------------------ ------------------------------ ------------
BASIC_IND1                     BASIC_TABLE1                   INDEXED
COMPRESS_IND                   COMPRESS_TABLE                 INDEXED
RIO$1880                       SEC_TABLE                      POPULATE
SEC_IND                        SEC_TABLE                      INDEXED

Nach Aufbau der Schattenindexstruktur erfolgt automatisch ein Austausch (auch Exchange). Textabfragen sind zu jedem Zeitpunkt möglich und dabei ist kein Eingriff in die Applikationen notwendig. Überprüft man zum Schluss die Struktur, kann man die Veränderungen beispielsweise mit CTX_REPORT.CREATE_INDEX_SCRIPT feststellen.

SQL> set long 10000
SQL> set pagesize 1000
SQL> SELECT ctx_report.create_index_script('SEC_IND') FROM dual;

CTX_REPORT.CREATE_INDEX_SCRIPT('SEC_IND')
--------------------------------------------------------------------------------
begin
  ctx_ddl.create_preference('"SEC_IND_DST"','DIRECT_DATASTORE');
end;
/
begin
  ctx_ddl.create_preference('"SEC_IND_FIL"','NULL_FILTER');
end;
/
begin
  ctx_ddl.create_section_group('"SEC_IND_SGP"','NULL_SECTION_GROUP');
end;
/
begin
  ctx_ddl.create_preference('"SEC_IND_LEX"','MULTI_LEXER');
end;
/
begin
  ctx_ddl.create_preference('"SEC_IND_L00"','BASIC_LEXER');
  ctx_ddl.set_attribute('"SEC_IND_L00"','INDEX_THEMES','YES');
end;
/
begin
  ctx_ddl.create_preference('"SEC_IND_LF"','BASIC_LEXER');
  ctx_ddl.set_attribute('"SEC_IND_LF"','BASE_LETTER','YES');
end;
/
begin
  ctx_ddl.create_preference('"SEC_IND_LD"','BASIC_LEXER');
  ctx_ddl.set_attribute('"SEC_IND_LD"','COMPOSITE','GERMAN');
  ctx_ddl.set_attribute('"SEC_IND_LD"','MIXED_CASE','YES');
  ctx_ddl.set_attribute('"SEC_IND_LD"','ALTERNATE_SPELLING','GERMAN');
end;
/
begin
  ctx_ddl.add_sub_lexer('"SEC_IND_LEX"','DEFAULT','"SEC_IND_L00"');
  ctx_ddl.add_sub_lexer('"SEC_IND_LEX"','FRENCH','"SEC_IND_LF"');
  ctx_ddl.add_sub_lexer('"SEC_IND_LEX"','GERMAN','"SEC_IND_LD"');
end;
/
begin
  ctx_ddl.create_preference('"SEC_IND_WDL"','BASIC_WORDLIST');
  ctx_ddl.set_attribute('"SEC_IND_WDL"','STEMMER','ENGLISH');
  ctx_ddl.set_attribute('"SEC_IND_WDL"','FUZZY_MATCH','GENERIC');
end;
/
begin
  ctx_ddl.create_stoplist('"SEC_IND_SPL"','BASIC_STOPLIST');
...

Möchte man das Ganze kontrolliert in einem 2 Schrittverfahren durchführen, kann man die folgenden beiden Prozeduren verwenden. Auch hier ist noch (im aktuellen Release) keine Parallelisierung möglich.

execute CTX_DDL.CREATE_SHADOW_INDEX (idx_name=>'SEC_IND',parameter_string=>'REPLACE LEXER multi_lx language column lang');
execute CTX_DDL.EXCHANGE_SHADOW_INDEX(idx_name=>'SEC_IND');

Um den Schattenindex zu löschen, kann man die spezielle Prozedur CTX_DDL.DROP_SHADOW_INDEX verwenden. Mehr Tipps und Tricks in einem der nächsten Blogs.....

Donnerstag, 10. September 2009

Oracle TEXT und Database Links ...

Heute widmen wir uns in einem ganz kurzen Tipp dem Thema Database Links, diese können mit Oracle TEXT verwendet werden. Es sind allerdings ein paar Feinheiten zu beachten: Angenommen, wir haben eine Tabelle DOK_TAB auf einer entfernten Datenbank. Diese hat eine Spalte DOK, die auch mit Oracle TEXT indiziert ist.
Auf der lokalen Datenbank können wir nun einen Database Link anlegen ...
create database link {dblink-name} connect to {remote-username} identified by {remote-passwd} using '{connection-string}'
Ein erster Versuch führt nun scheinbar ins Leere ...
select * from dok_tab@{dblink-name} where contains(dok, '$Bahnhof') > 0;
                                       *
FEHLER in Zeile 1:
ORA-20000: Oracle Text error:
DRG-10599: column is not indexed
Diese Fehlermeldung kommt, obwohl die Spalte indiziert ist. Oracle TEXT hat die Eigenheit, dass der Database Link auch bei der CONTAINS-Funktion angegeben werden muss. Damit wird sichergestellt, dass die CONTAINS-Funktion auch auf der entfernten Datenbank ausgeführt wird. Also ...
select * from dok_tab@{dblink-name} where contains@{dblink-name}(dok, '$Bahnhof') > 0;

        ID DOK
---------- ------------------------------
         2 Hauptbahnhof
Eins ist im Zusammenhang mit Database Links noch wichtig: CLOB- oder BLOB-Spalten können nicht via Database Link übertragen werden. Was geht, sind die ersten 32 Kilobyte - dazu kann man mit DBMS_LOB.SUBSTR arbeiten. Zur Selektion der ganzen Dokumente kann man mit Database Links also nicht arbeiten - die Selektion von IDs, URLs oder Titeln ist jedoch problemlos möglich.

Mittwoch, 19. August 2009

Abfrage-Optimierung mit Composite Domain Index

Wie in vorangegangen Blogs gezeigt wurde, kann die sogenannte Mixed Query Problematik - eine Kombination aus relationalem und Volltextrecherche-Anteil - mit neuen Section Features in Oracle Database 10g bzw. 11g angegangen und teilweise gelöst werden. Der MData Section und MULTI_COLUMN_DATASTORE-Blog verwendete dabei zur Lösung, die in 10g eingeführte MDATA-Sections und der SData Section-Blog die neue SDATA-Section. Darüber hinaus ist in 11g eine neue Form des Context Index, der sogenannte „Composite Domain Index“ (kurz CDI) neu eingeführt worden, um speziell bei der Optimierung von Mixed Queries eine einfache direkte Lösung zu bieten. Ein Composite Domain Index ist dabei ein zusammengesetzter Index, der sich nicht auf die Textinformation beschränkt, sondern auch strukturelle Informationen mitführt und mit einem einzigen Aufruf an die Textengine ausgeführt wird.
So können Queries die aus
  • Textanteilen und strukturierten Anteilen in the SQL WHERE Klausel
  • Textanteilen und strukturierten Anteilen in der ORDER BY Klausel
  • eine Kombination aus beidem

  • einfach optimiert werden. Dieses Feature kann dabei unabhängig von den Section Features genutzt werden. Keine Sections oder gar Änderung am Abfragecode ist notwendig. Die Technologie verwendet allerdings aus Optimierungsgründen im Hintergrund genau wie bei SDATA-Sections ein zusätzliches Indexsegment, eine IOT-Tabelle mit Namen DR$SDATA_INDEX$S. Das Anlegen des CDI erfolgt dann mit folgender einfacher erweiterter Syntax.
    
    CREATE INDEX comp_ind ON customers(cust_first_name)
    INDEXTYPE IS ctxsys.context
    FILTER BY cust_id
    ORDER BY cust_year_of_birth
    
    Oracle Text wird nun die Daten aus CUST_ID und CUST_YEAR_OF_BIRTH im Textindex speichern; dabei besteht keine Notwendigkeit, die Queries anzupassen. Der Optimizer wird feststellen, dass die Abfrage durch den Textindex allein verifiziert werden kann. Die Erweiterung mit ORDER BY führt sogar dazu, dass die abgerufenen Zeilen danach sortiert ausgeliefert werden können. Im Unterschied zu B*Tree Indizes können allerdings nur die Informationen gefiltert werden, die auch schon synchronisiert worden sind.
    An einem Beispiel wollen wir die Optimierung aufzeigen. Im ersten Fall verwenden wir folgenden einfachen CONTEXT Index
    
    CREATE INDEX text_ind ON customers(cust_first_name) 
    INDEXTYPE IS ctxsys.context;
    
    Sehen wir uns nun die Ausführungszeit folgender Query an. Die Abfrage ist wie häufig bei Webanwendungen anzutreffen optimiert im Hinblick auf sortierten Zugriffe der ersten Zeilen.
    
    SELECT /*+ first_rows(10) */ cust_id
    FROM (select cust_id, cust_first_name,cust_year_of_birth from customers
    WHERE contains (cust_first_name, 'A% or D% or N% or B%',1)>0  AND cust_id>100000 
    ORDER BY cust_year_of_birth, score(1))
    WHERE rownum<10
    
       CUST_ID
    ----------
        102011
        103921
        100199
        100930
        104242
        103080
        103187
        103412
        103684
    
    9 rows selected.
    
    Elapsed: 00:00:00.12
    
    Der folgende Ausführungsplan ist etwas länglich, zeigt allerdings schon ohne genaue Analyse dass Textindex TEXT_IND und B*Index CUSTOMERS_PK für die Ausführung notwendig sind.
    
    SELECT * FROM table(dbms_xplan.display_cursor(format=>'basic'))
    
    PLAN_TABLE_OUTPUT
    --------------------------------------------------------------------------------
    EXPLAINED SQL STATEMENT:
    ------------------------
    SELECT /*+ first_rows(10) */ cust_id FROM (select cust_id,
    cust_first_name,cust_year_of_birth from customers WHERE contains
    (cust_first_name, 'A% or D% or N% or B%',1)>0  AND cust_id>100000 order
    by cust_year_of_birth, score(1)) where rownum<10
    Plan hash value: 1704212880
    ------------------------------------------------------------
    | Id  | Operation                           | Name         |
    ------------------------------------------------------------
    |   0 | SELECT STATEMENT                    |              |
    |   1 |  COUNT STOPKEY                      |              |
    |   2 |   VIEW                              |              |
    |   3 |    SORT ORDER BY STOPKEY            |              |
    |   4 |     TABLE ACCESS BY INDEX ROWID     | CUSTOMERS    |
    |   5 |      BITMAP CONVERSION TO ROWIDS    |              |
    |   6 |       BITMAP AND                    |              |
    |   7 |        BITMAP CONVERSION FROM ROWIDS|              |
    |   8 |         SORT ORDER BY               |              |
    |   9 |          DOMAIN INDEX               | TEXT_IND     |
    |  10 |        BITMAP CONVERSION FROM ROWIDS|              |
    |  11 |         SORT ORDER BY               |              |
    |  12 |          INDEX RANGE SCAN           | CUSTOMERS_PK |
    ------------------------------------------------------------
    
    Zum Vergleich verwenden wir statt des einfachen Context Index nun den neuen CDI mit der oben angegebenen Syntax und führen die Abfrage noch einmal durch:
    
    SELECT /*+ first_rows(10) */ cust_id
    FROM (select cust_id, cust_first_name,cust_year_of_birth from customers
    WHERE contains (cust_first_name, 'A% or D% or N% or B%',1)>0  AND cust_id>100000 
    ORDER BY cust_year_of_birth, score(1))
    WHERE rownum<10
    
       CUST_ID
    ----------
        102011
        103921
        100199
        100930
        104242
        103080
        103187
        103412
        103684
    
    9 rows selected.
    
    Elapsed: 00:00:00.02
    
    Die Abfragezeit beträgt nur noch ein Sechstel der Zeit. Nun schauen wir uns noch den zugehörigen Ausführungsplan an:
     
    SELECT * FROM table (dbms_xplan.display_cursor());
    ...
    Plan hash value: 3210723938
    -------------------------------------------------------------------------------------------
    | Id  | Operation                      | Name      | Rows  | Bytes | Cost (%CPU)| Time    |
    -------------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT               |           |       |       |     5 (100)|         |
    |*  1 |  COUNT STOPKEY                 |           |       |       |            |         |
    |   2 |   VIEW                         |           |     1 |    13 |     5  (20)| 00:00:01|
    |*  3 |    SORT ORDER BY STOPKEY       |           |     1 |    28 |     5  (20)| 00:00:01|
    |   4 |     TABLE ACCESS BY INDEX ROWID| CUSTOMERS |     1 |    28 |     4   (0)| 00:00:01|
    |*  5 |      DOMAIN INDEX              | COMP_IND  |       |       |     4   (0)| 00:00:01|
    -------------------------------------------------------------------------------------------
    Predicate Information (identified by operation id):
    -------------------------------------------------------------------------------------------
       1 - filter(ROWNUM<10)
       3 - filter(ROWNUM<10)
       5 - access("CTXSYS"."CONTAINS"("CUST_FIRST_NAME",'A% or D% or N% or B%',1)>0)
    
    Da weniger oder keine DOCID->ROWID Transformationen für Sätze, die nicht in der finalen Ergebnisliste sind, notwendig sind, erhalten wir einen effizienteren Zugriff. Generell zeigt sich, dass grosse Ergebnismengen im Textindex in Verbindung mit den stark nachfilternden Indizes, am Besten im CDI abgebildet werden sollten.
    Mehr zur Tipps und Tricks in einem der nächsten Blogs.....

    Dienstag, 28. Juli 2009

    CTX_QUERY.EXPLAIN: Erklärungen für eine TEXT-Abfrage

    Vor einiger Zeit hatten wir ein Blog Posting zum Thema "Fuzzy-Suche". Darin könnt Ihr nachlesen, wie eine Ähnlichkeitssuche mit Oracle TEXT funktioniert und wie Ihr sie parametrisieren könnt. Wie beschrieben, führt Oracle TEXT eine sog. Termexpansion durch. Die Suchabfrage wird also zunächst um Tokens aus dem Textindex, die dem gesuchten Begriff ähnlich sind, erweitert und anschließend wird mit dieser Token-Liste eine OR-Suche durchgeführt. Die Parameter des FUZZY-Operators steuern diese Termexpansion.
    Nun wäre es schön, wenn man sich ansehen könnte, was er da tut - welche Tokens also in die Liste mit aufgenommen werden. Und genau das ist mit der Prozedur CTX_QUERY.EXPLAIN möglich (übrigens nicht nur für die Fuzzy-Suche, sondern für alle TEXT-Abfragen). Die EXPLAIN-Funktion generiert eine Art "Ausführungsplan" für den Oracle TEXT Index.
    Zunächst benötigen wir eine EXPLAIN TABLE - dort schreibt die Prozedur die Erklärungen zu einer Textquery hinein. In der Oracle-Dokumentation "Text Reference" findet Ihr Erlärungen zu Aufbau und Inhalt. Erzeugt wird sie mit diesem CREATE TABLE-Kommando:
    create table meine_explain_tabelle(
      explain_id  varchar2(30),
      id          number,
      parent_id   number,
      operation   varchar2(30),
      options     varchar2(30),
      object_name varchar2(64),
      position    number,
      cardinality number
    );
    
    Wenn Ihr die Tabelle erstellt habt, könnt Ihr euch eine Textquery beschreiben lassen.
    begin
      ctx_query.explain(
        index_name    => 'IDX_DOKUMENTE',
        text_query    => '?sptial or geodaten',
        explain_table => 'MEINE_EXPLAIN_TABELLE'
      );
    end;
    /
    
    Anschließend stehen die Erklärungen in der erzeugten EXPLAIN-Tabelle. Wichtig ist der Parameter SHARELEVEL, der in diesem Aufruf nicht angegeben wurde. Der Default ist "0" (Null), was bedeutet, dass die Tabelle vorher leergeräumt wurd (TRUNCATE). Wenn Ihr EXPLAIN-Ergebnisse aufheben möchtet, setzt den Parameter auf "1" - zusätzlich benötigt Ihr dann eine EXPLAIN_ID, damit Ihr die Erklärungen später wiederfinden könnt.
    Die Einträge in der Tabelle sind hierarchisch organisiert - am besten fragt Ihr sie daher mit einem START WITH - CONNECT BY wie folgt ab.
    select
      lpad(' ',level * 2)|| to_char(id, '99') id, 
      operation,
      options,
      object_name,
      position
    from meine_explain_tabelle
    start with parent_id =0 
    connect by parent_id = prior id
    /
    
    Das Ergebnis sieht dann etwa so aus
    ID         OPERATION       OPTIONS    OBJECT_NAME     POSITION 
    ---------- --------------- ---------- --------------- --------
        1      OR                                                1
          2    EQUIVALENCE     (?)        sptial                 1
            3  WORD                       Spatial                1
            4  WORD                       special                2
            5  WORD                       Special                3
            6  WORD                       Spezial                4
            7  WORD                       Spiel                  5
            8  WORD                       spielt                 6
            9  WORD                       spielte                7
           10  WORD                       Spitze                 8
           11  WORD                       sptial                 9
         12    WORD                       geodaten               2
    
    Man erkennt sehr schön die ähnlichen Worte, die Oracle TEXT in die Abfrage eingebunden hat; man könnte nun, wie im Blog Posting zur "Fuzzy-Suche" beschrieben, das Schlüsselwort FUZZY nutzen und parametrisieren: Wie versuchen also die Abfrage FUZZY(sptial,70,5) or geodaten. Die in Frage kommenden Wörter müssen sich also ähnlicher sein (Wert "70" im Gegensatz zum Default von "60") und es werden maximal fünf Wörter in die Termexpansion einbezogen. Das Ergebnis ...
    ID         OPERATION       OPTIONS    OBJECT_NAME     POSITION 
    ---------- --------------- ---------- --------------- -------- 
        1      OR                                                1
          2    EQUIVALENCE     (?)        sptial                 1
            3  WORD                       Spatial                1
            4  WORD                       sptial                 2
          5    WORD                       geodaten               2
    
    Und wenn man nach "Spatial" im Zusammenhang mit "Geodaten" sucht, erkennt man sofort, dass diese Suche viel zielgenauer ist. Gerade wenn es um das Spielen mit den Parametern für eine FUZZY-Suche geht, ist die EXPLAIN-Funktion eine wertvolle Hilfe.
    Viel Spaß beim Ausprobieren!

    Dienstag, 7. Juli 2009

    Mixed Queries in 11g

    MDATA Sections sind in 10g eingeführt worden, um gemischte Abfragen (auch mixed queries genannt) - also Abfragen mit Text- und relationalen Anteilen, besser handhaben zu können. Generell können damit kurze Textfelder(sogenannte Metadaten), die als Ganzes im Textindex indiziert wurden, einfach abgefragt werden. Mehr Informationen zur MDATA-Nutzung finden Sie Metadatensuche mit MDATA Blog und im MData Section und MULTI_COLUMN_DATASTORE Blog. Abfragen auf die Metadaten wie prod_list_price und flag sehen dann beispielsweise folgendermassen aus:
    
    SELECT prod_id, prod_list_price, prod_desc
    FROM products
    WHERE contains (prod_desc, 'Card AND MDATA(prod_list_price, 69.99)
    AND MDATA(flag,N)') > 0;
    
    
    Ein wichtiger Unterschied zu gewohntem Sectionverhalten ist, dass MDATA Bereiche transaktionell verändert werden können, ohne den Rest des Index zu beeinträchtigen bzw. zu re-indizieren. Nachteile dieser Technologie ist die Tatsache, dass nur auf Gleichheit abgfragt werden kann und zusätzlich die MDATA Werte als einziges Token behandelt und minimal normalisiert werden können (Whitespace-Entfernung etc). Daher ist in 11g eine weitere Form der Section Suche eingeführt worden - die SDATA Section (SDATA steht dabei für Structured Data). Die Indizierung der SDATA Section erlaubt Operationen wie Range Scans, Nutzung von Funktionen, Projektionen usw. So können neue Kombinationen aus Text und strukturierte Anteilen abgefragt werden. Um die Unterschiede aufzuzeigen, nehmen wir das Beispiel aus MData Section und MULTI_COLUMN_DATASTORE und verwenden dabei die neue SDATA Section. Wir belassen den MULTI_COLUMN_DATASTORE my_multi_pref, und erzeugen eine SDATA Section mit Namen prod_list_price.
    
    connect sh/sh
    execute ctx_ddl.drop_section_group('my_seg');
    begin
    ctx_ddl.create_section_group(group_name=>'my_seg',group_type=>'basic_section_group');
    ctx_ddl.add_sdata_section('my_seg','PROD_LIST_PRICE','prod_list_price', 'NUMBER');
    end;
    /
    DROP INDEX mdata_index;
    DROP INDEX sdata_index;
    CREATE INDEX sdata_index ON products(prod_desc)
    INDEXTYPE IS ctxsys.context
    PARAMETERS ('DATASTORE my_multi_pref SECTION GROUP my_seg sync (on commit)');
    
    SELECT err_text FROM ctx_user_index_errors WHERE err_index_name = 'SDATA_INDEX';
    no rows selected
    
    
    Folgende Abfrageart mit dem SDATA-Operator ist nun möglich.
    
    SELECT prod_id, prod_list_price, prod_desc FROM products
    WHERE contains (prod_desc, 'Card AND SDATA(prod_list_price >= 69.99)') > 0;
    
       PROD_ID PROD_LIST_PRICE
    ---------- ---------------
    PROD_DESC
    --------------------------------------------------------------------------------
            25          112.99
    SIMM- 8MB PCMCIAII card
    
            26          149.99
    SIMM- 16MB PCMCIAII card
    
           138           69.99
    256MB Memory Card
    
    
    Untersucht man genauer die neuangelegten Objekte, wird man feststellen, dass ein zusätzliches Indexsegment, eine IOT-Tabelle mit Namen DR$SDATA_INDEX$S, erzeugt wurde.

    Nun stellt sich die Frage, ob das Ganze nicht einfacher zu bewerkstelligen ist, ohne zusätzlich Sections zu verwenden. Die Antwort dazu gibt die neue Composite Domain Index Technologie. Mit nur einem CREATE INDEX-Kommando ohne zusätzliche SDATA Sections kann dies erreicht werden. Folgendes Kommando zeigt die Implementierung in unserem Fall. Die FILTER BY Klausel ermöglicht dabei die Teilabfrage auf die Spalte PROD_LIST_PRICE vollständig im Text-Index durchzuführen.
    
    DROP INDEX sdata_index;
    
    CREATE INDEX comp_index ON products(prod_desc)
    INDEXTYPE IS ctxsys.context
    FILTER BY prod_list_price;
    
    
    Ein kurzer Blick auf die erzeugten Objekte, gibt den Hinweis darauf, dass die SDATA Technologie offensichtlich als Grundlage dient, da wir nun eine zusätzliche IOT-Tabelle DR$COMP_INDEX$S besitzen. Die Abfragen können nun im gewohnten Stil ohne Verwendung von speziellen Operatoren verwendet werden.
    
    SELECT prod_id, prod_list_price, prod_desc 
    FROM products
    WHERE contains (prod_desc, 'Card')>0  AND prod_list_price <= 69.99;
    
       PROD_ID PROD_LIST_PRICE
    ---------- ---------------
    PROD_DESC
    --------------------------------------------------------------------------------
           136           32.99
    64MB Memory Card
    
           137           52.99
    128MB Memory Card
    
           138           69.99
    256MB Memory Card
    
     
    Mehr zur Composite Domain Index Technik in einem der nächsten Blogs.....

    Montag, 22. Juni 2009

    Wie ähnlich soll es sein ...? Ein paar Worte zum FUZZY-Operator

    Es ist weitgehend bekannt, dass Oracle TEXT eine Ähnlichkeitssuche mit dem Fuzzy-Operator unterstützt. Die einfachste Variante ist die Verwendung des Fragezeichens ?. Der Fuzzy-Operator ist gut geeignet, um mit etwaigen Rechtschreibfehlern in den Dokumenten umzugehen. Sucht man bspw. nach ?Spatial, so findet der Index auch Dokumente, in denen fälschlicherweise "Sptial" geschrieben wurde.
    select dateiname, score(0) from dokumente where contains (content, '?Spatial', 0) > 0
    
    Diese Ähnlichkeitssuche kann übrigens auch parametrisiert werden: Neben dem Fragezeichen steht auch das Schlüsselwort FUZZY zur Verfügung. Oder anders ausgedrückt: Obige SQL-Abfrage ließe sich auch so schreiben:
    select dateiname, score(0) from dokumente where contains (content, 'FUZZY(Spatial, 60, 100, N)'), 0) > 0
    
    Und mit diesen Parametern kann man das Verhalten des FUZZY-Operators nun steuern. Der erste Parameter legt fest, wie ähnlich die Tokens im Dokument dem Suchbegriff sein müssen. Erlaubt sind Werte von 1 bis 80. Je niedriger Ihr den Wert ansetzt, desto mehr Begriffe kommen in Frage; desto mehr Dokumente werden also gefunden. Allerdings stellt sich die Frage, wie relevant die Dokumente bei sehr niedrigen Grenzen noch sind.
    Der zweite Parameter legt fest, wieviele Werte überhaupt in die Termexpansion einbezogen werden. Dazu kurz einige Worte: Oracle TEXT führt die Fuzzy-Suche über eine Termexpansion durch; es werden also zunächst aus der Token-Tabelle ($I) die ähnlichen Tokens herausgesucht (der erste Parameter legt, wie gesagt, fest, wie ähnlich die Tokens sein müssen). Mit den so gefundenen Tokens wird dann eine ODER Suche durchgeführt.
    Mit dem zweiten Parameter legt man also fest, wieviele ähnliche Wörter maximal einbezogen werden sollen. Erlaubt sind Werte zwischen 1 und 5000. Ein Wert von 20 würde bewirken, dass maximal 20 ähnliche Wörter in der Suche berücksichtigt werden, auch wenn anhand des ersten Parameters mehr in Frage kämen. Hier gilt also: Je höher der Wert gesetzt wird, desto mehr Dokumente werden tendenziell gefunden ...
    Der letzte Parameter wirkt sich nur auf den Score aus, den ein Dokument im Relevanz-Ranking bekommt. Mit einem W werden die Scores anhand der Ähnlichkeit der Suchwörter gewichtet; mit einem N geschieht das nicht. Ein W führt zu tendenziell höheren Scores.
    Mit diesen Parametern könnt Ihr also spielen, um mit der Fuzzy-Suche mehr oder weniger Ergebnisse zu bekommen. Was konkret gebraucht wird, hängt von den Anforderungen des Projekts ab ... zur Verdeutlichung hier nochmals zwei Extrembeispiele. Das erste ist zwar "formal" eine Ähnlichkeitssuche, aber die Parameter "würgen" die Fuzzy-Logik weitgehend ab.
    select dateiname, score(0) from dokumente 
    where contains(content, 'fuzzy(sptial, 80, 1, W)',0) > 0;
    
    Das zweite bohrt die Grenzen so weit auf, dass sehr viele Dokumente in Frage kommen ...
    select dateiname, score(0) from dokumente 
    where contains(content, 'fuzzy(sptial, 1, 5000, W)',0) > 0;
    
    Dies lässt sich auch sehr gut mit Query Relaxation verbinden. In einer ersten Stufe würde ohne den Fuzzy-Operator suchen, in einer nächsten Stufe dann mit dem Fuzzy-Operator, aber eher restriktiven Kriterien und danach mit sehr weit gehenden Parametern. Query Relaxation arbeitet die Stufen dann bekanntlich so lange ab, bis genügend Treffer gefunden sind - mehr dazu im Blog Posting.

    Montag, 8. Juni 2009

    USER_DATASTORE ... indiziert wirklich alles

    Eine der interessanten Eigenschaften des Oracle TEXT-Index ist, dass die zu indizierenden Dokumente nicht nur einfach aus einer Tabellenspalte, sondern aus unterschiedlichen Data Stores kommen können.
    • DIRECT_DATASTORE: der "Normalfall"
    • MULTICOLUMN_DATASTORE: Mehrere Tabellenspalten (dazu hatten wir schon Blog-Postings
    • FILE_DATASTORE: Die Dokumente liegen im Dateisysten; die Tabelle enthält für jedes Dokument einen vollständigen, absoluten Pfad
    • URL_DATASTORE: Die Dokumente liegen im "Netzwerk"; die Tabelle enthält für jedes Dokument einen vollständigen, absoluten URL (FTP oder HTTP)
    • DETAIL_DATASTORE: Die Dokumente liegen in einer anderen Tabelle, welche mit der zu indizierenden in einer Master-Detail-Beziehung steht
    • NESTED_DATASTORE: Die Dokumente liegen in nicht direkt in der zu indizierenden Tabelle, sondern in einer Nested Table (dürfte selten vorkommen).
    Und schließlich gibt es den USER_DATA_STORE, der quasi "alles kann". Und um den soll es in diesem Posting auch gehen. Als Ausgangspunkt haben wir eine Tabelle mit Dokumenten und diese sollen nicht direkt indiziert werden, sondern über einen USER_DATA_STORE. Warum? Weil wir über eine zusätzliche Spalte (INDEX_DOCUMENT) kontrollieren möchten, ob die Dokumente indiziert werden sollen oder nicht. Beginnen wir mit dem Tabellenaufbau ...
    SQL> desc dokumente
     Name                                      Null?    Typ
     ----------------------------------------- -------- ---------------------
    
     ID                                                 NUMBER
     FILENAME                                           VARCHAR2(2000)
     DOCUMENT                                           BLOB
     INDEX_DOCUMENT                                     CHAR(1)
    
    Nun geht es ans Erstellen des Textindex mit dem User Datastore. Ein User Datastore bedeutet nichts weiter als dass der Index seine Dokumente von einer PL/SQL-Prozedur zugewiesen bekommt und diese eben nicht direkt aus der Tabelle holt. Da man in der PL/SQL-Prozedur programmieren kann, was man möchte, ist das Verhalten des User Datastore auch sehr individuell. Dieses Beispiel soll (wie gesagt), das Dokument normal indizieren, wenn die Spalte INDEX_DOCUMENT auf "Y" steht und gar nicht indizieren, wenn die Spalte auf "N" steht. Beginnen wir mit der PL/SQL-Prozedur:
    create or replace procedure dokument_uds_proc (
      rid  in              rowid,
      tlob in out NOCOPY   blob    
    ) is
    begin
      begin
        select document into tlob
        from dokumente where rowid = rid and index_document = 'Y';
      exception
        when NO_DATA_FOUND then tlob := null;
      end;
    end;
    /
    
    Wichtig an dieser Prozedur ist die Signatur, diese muss genau so aussehen, wie in diesem Beispiel vorgegeben. Die zu indizierende ROWID wird in die Prozedur hineingegeben, das zu indizierende Dokument kommt als OUT-Parameter wieder zurück. Man sieht, dass man das Dokument selbst nun beliebig zusammensetzen kann. Die Inhalte können sehr wohl aus verschiedensten Tabellen oder anderen Datenquellen kommen.
    Aber bis jetzt ist die Prozedur nur eine normale PL/SQL-Prozedur. Damit sie für etwas ORACLE Text etwas bewirken kann, muss sie zunächst in Oracle TEXT bekannt gemacht werden; die folgenden PL/SQL-Blöcke richten den User Datastore im Dictionary von Oracle TEXT ein.
    begin
      ctx_ddl.drop_preference('DOKUMENT_UDS');
    end;
    /
    sho err
    
    begin
      ctx_ddl.create_preference('DOKUMENT_UDS','user_datastore');
      ctx_ddl.set_attribute('DOKUMENT_UDS','procedure','dokument_uds_proc');
      ctx_ddl.set_attribute('DOKUMENT_UDS','output_type','blob_loc');
    end;
    /
    sho err
    
    Bevor der Index nun erzeugt wird, fehlt noch eine Kleinigkeit: Der Index muss in zwei Fällen aktualisiert werden: Erstens wenn sich das Dokument ändert und zweitens wenn die Spale INDEX_DOCUMENT verändert wird. Wenn der Index also auf die Spalte INDEX_DOCUMENT gelegt wird, benötigen wir noch einen Trigger, der UPDATE-Operationen in DOCUMENT auf die Spalte INDEX_DOCUMENT überträgt:
    create or replace trigger trg_uds_document
    before update on dokumente
    for each row
    begin
      :new.index_document := :new.index_document;
    end;
    /
    
    Nun kann der Index erstellt werden. Wie schon beschrieben, wird der Index auf die Spalte INDEX_DOCUMENT gelegt:
    create index idx_text_dokumente
    on dokumente (index_document)
    indextype is ctxsys.context
    parameters ('filter ctxsys.auto_filter
                 datastore dokument_uds
                 memory 200M
                 transactional')
    /
    
    Wenn alle Einträge ein "N" in der Spalte INDEX_DOCUMENT haben, ist der Index nach Erstellung immer noch leer. Setzt man eine oder mehrere Zeilen auf "Y", so können diese anschließend dank des TRANSACTIONAL-Parameters (Posting!) gefunden werden, das dauert aber sehr lange. Kein Wunder, denn der Index ist noch nicht synchronisiert; die Dokumente in der Pending-Tabelle werden also on-the-fly durchsucht und dabei muss natürlich auch die PL/SQL-Prozedur des User Datastore durchlaufen werden. Nach einem CTX_DDL.SYNC_INDEX('IDX_TEXT_DOKUMENTE') befinden sich die Einträge denn auch im Textindex.
    Dieses einfache Beispiel zeigt, wie man einen User Datastore nutzen kann. Wichtig ist, dass hier keine Grenzen existieren; die PL/SQL-Prozedur des UDS ist für Oracle TEXT eine "Black Box"; man kann dort hineinprogrammieren, was man möchte ...

    Freitag, 15. Mai 2009

    MDATA Section und MULTI_COLUMN_DATASTORE

    Ab Oracle Database 10g gibt es ein neues „Section“ Feature - die neue MDATA Section, um Dokument-Metadaten separat zu handhaben. Die MDATA Section ist vergleichbar mit einer Zone- oder Field- Section, d.h. das Dokument muss eine interne Struktur („Section“) wie HTML oder XML besitzen. Ein Beispiel dazu findet sich in folgendem Blog. Der MULTI_COLUMN_DATASTORE (siehe dazu auch Blog) führt die Spalten einer Tabellen zu einem Dokument zusammen und trennt die Informationen durch XML Tags.
    Bringt man nun diese beiden Techniken zusammen, so lassen sich Spalten im MULTI_COLUMN_DATASTORE zusammenführen und danach als MDATA Metadaten Sections kennzeichen und nutzen.

    Für das folgenden Beispiel verwenden wir die Tabelle PRODUCTS im Schema SH. Zu beachten ist, um das Paket CTX_DDL nutzen zu können, benötigt man die Rolle CTXAPP (oder ein explizites EXECUTE-Privileg).
    Zuerst legen wir den MULTI_COLUMN_DATASTORE mit folgenden Statements an. Die Spalten "PROD_NAME", "PROD_STATUS", "PROD_LIST_PRICE" und "PROD_DESC" werden für den MULTI_COLUMN_DATASTORE konfiguriert:
    
    connect sh/sh
    execute ctx_ddl.drop_preference (preference_name =>'MY_MULTI_PREF');
    begin 
      ctx_ddl.create_preference(preference_name =>'MY_MULTI_PREF',
                                object_name => 'multi_column_datastore' );
      ctx_ddl.set_attribute(preference_name => 'MY_MULTI_PREF',
               attribute_name  => 'COLUMNS',
               attribute_value => 'PROD_NAME, PROD_STATUS, PROD_LIST_PRICE, PROD_DESC');
    end;
    /
    sho err
    
    
    Im nächsten Schritte werden die MDATA Sections "PROD_NAME", "PROD_STATUS", "PROD_LIST_PRICE" mit ADD_MDATA_SECTION konfiguriert. Die Section "PROD_DESC" wird als eine FIELD-Section angelegt. Zusätzlich zu den existierenden Sections erzeugen wir eine neue Section "flag", die neue Informationen, die nicht in der Tabelle enthalten sind, aufnehmen kann. Eine typische Verwendung wäre zum Beispiel eine zusätzliche Kennzeichnung der Daten durch spezielle Zugriffsrechte.
    
    execute ctx_ddl.drop_section_group('my_seg');
    
    execute ctx_ddl.create_section_group(group_name=>'my_seg',   
                                         group_type=>'basic_section_group');
    execute ctx_ddl.add_mdata_section(group_name=>'my_seg', section_name=>'PROD_NAME', 
                                      tag=>'prod_name');
    execute ctx_ddl.add_mdata_section(group_name=>'my_seg', section_name=>'PROD_STATUS', 
                                      tag=>'prod_status');
    execute ctx_ddl.add_mdata_section(group_name=>'my_seg',  
                                      section_name=>'PROD_LIST_PRICE', 
                                      tag=>'prod_list_price');
    execute ctx_ddl.add_field_section(group_name=>'my_seg', section_name=>'PROD_DESC', 
                                      tag=>'prod_desc', visible=>true);
    execute ctx_ddl.add_mdata_section(group_name=>'my_seg', section_name=>'flag', 
                                      tag=>'flag');
    
    sho err
    
    
    Nun kann der Index mit dem DATASTORE my_multi_pref und der SECTION GROUP my_seg erzeugt werden. Die Synchronisierung soll dabei automatisch nach jedem COMMIT erfolgen.
    
    DROP INDEX mdata_index;
    
    CREATE INDEX mdata_index ON products(prod_desc)
    indextype IS ctxsys.context 
    parameters ('DATASTORE my_multi_pref SECTION GROUP my_seg sync (on commit)');
    
    SELECT err_text FROM ctx_user_index_errors WHERE err_index_name = 'MDATA_INDEX';
    no rows selected
    
    
    Folgende Abfrageart mit dem MDATA-Operator ist nun möglich.
    
    SELECT prod_id, prod_list_price, prod_desc FROM products 
    WHERE contains (prod_desc, 'Card AND MDATA(prod_list_price, 69.99)') > 0;
    
       PROD_ID PROD_LIST_PRICE
    ---------- ---------------
    PROD_DESC
    --------------------------------------------------------------------------------
           138           69.99
    256MB Memory Card
    
    
    Die Tokentypes größer gleich 400 in der $I-Tabelle zeigen an, dass MDATA-Sectionen vorhanden sind, da MDATA Tokens mit Tokentype zwischen 400 und 499 gekennzeichnet sind. Gibt es zusätzlich negative Werte (kleiner gleich 400), die sogenannten "Delta-Zeilen", ist der Index nicht optimiert. Dies wird nun mit folgender Abfrage verifiziert:
    
    SELECT token_type, count(*) 
    FROM dr$mdata_index$i 
    WHERE token_type<= -400 OR token_type>=400 GROUP BY token_type;
    TOKEN_TYPE   COUNT(*)
    ---------- ----------
           400         71
           402         42
           401          1
    
    
    Die 3 Zeilen zeigen, dass 3 verschiedene MDATA Tokentypes mit 400, 401 und 402 existieren. Diese stehen für die Sections "PROD_NAME", "PROD_STATUS" und "PROD_LIST_PRICE".
    Nun werden manuell Werte mit der Prozedur ADD_MDATA in die "flag" Metadaten-Section eingefügt. Dazu erzeugen wir mit folgendem Skript die entsprechenden Aufrufe, um "flag"-Einträge mit Wert "J" bzw. "N" je nach Größe des PROD_LIST_PRICE zu generieren.
    
    spool liste.lst
    SELECT 
    'execute ctx_ddl.add_mdata'||'(''MDATA_INDEX'''||','||'''flag'''||','||'''N'''||','||''''||rowid||''''||')'||';' 
    FROM products WHERE prod_list_price<=70;
    SELECT 'execute ctx_ddl.add_mdata'||'(''MDATA_INDEX'''||','||'''flag'''||','||'''J'''||','||''''||rowid||''''||')'||';'
    FROM products WHERE prod_list_price>70;
    COMMIT;
    spool off
    
    -- start des ablaufbaren Skripts mit
    start liste.lst
    execute ctx_ddl.add_mdata('MDATA_INDEX','flag','N','AAAew2AAEAADLwUAAG');
    execute ctx_ddl.add_mdata('MDATA_INDEX','flag','N','AAAew2AAEAADLwUAAJ');
    execute ctx_ddl.add_mdata('MDATA_INDEX','flag','N','AAAew2AAEAADLwUAAK');
    ...
    
    
    ADD_MDATA ist transaktional und muss mit COMMIT oder ROLLBACK abgeschlossen werden. Danach ist die "flag" Section in Abfragen wie folgt nutzbar:
    
    SELECT prod_id, prod_list_price, prod_desc 
    FROM products WHERE contains (prod_desc, 
    'Card AND MDATA(prod_list_price, 69.99) AND MDATA(flag,N)') > 0;
       PROD_ID PROD_LIST_PRICE
    ---------- ---------------
    PROD_DESC
    --------------------------------------------------------------------------------
           138           69.99
    256MB Memory Card
    
    
    SELECT prod_id, prod_list_price, prod_desc 
    FROM products WHERE contains (prod_desc, 
    'Card AND MDATA(prod_list_price, 69.99) AND MDATA(flag,J)') > 0;
    
    no rows selected
    
    
    Der Index ist allerdings noch nicht optimiert, wie die Überprüfung der $I Tabelle zeigt:
    
    SELECT token_type, count(*)
    FROM dr$mdata_index$i 
    WHERE token_type<= -400 OR token_type>=400 GROUP BY token_type;
    
    TOKEN_TYPE   COUNT(*)
    ---------- ----------
           400         71
           403          2
           402         42
          -403         70
           401          1
    
    
    Tokenweise Optimierung ist mit folgender speziellen OPTIMIZE_INDEX Prozedur möglich:
    
    execute ctx_ddl.optimize_index(idx_name=> 'MDATA_INDEX',
                  optlevel=> ctx_ddl.optlevel_token_type, 
                  token_type=> ctx_report.token_type('MDATA_INDEX', 'mdata flag')); 
    
    
    Der Index liegt nun optimiert vor. Das Ergebnis in der Token-Tabelle sieht nun folgendermassen aus:
    
    SELECT token_type, count(*)
    FROM dr$mdata_index$i 
    WHERE token_type<= -400 OR token_type>=400 GROUP BY token_type;
    
    TOKEN_TYPE   COUNT(*)
    ---------- ----------
           400         71
           403          2
           402         42
           401          1
    
    
    Zusätzlich zu den MDATA Sections gibt es in 11g SDATA Sections, die einige der Einschränkungen von MDATA aufheben. Darüberhinaus eröffnet der Composite Domain Index in 11g ganz neue Möglichkeiten, um die Problematik der Mixed Queries zu lösen. Mehr dazu in einer der nächsten Postings...

    Dienstag, 28. April 2009

    Index-Optimierung: Einige Grundlagen

    Häufige Indexsynchronisierung nach DML Operationen kann zu Indexfragmentierungen führen. Indexfragmentierungen wiederum können dabei die Antwortzeiten der Textabfragen negativ beeinflussen. Weniger häufige Synchronisierungen - also Batchoperation und längere Listen beim Synchronisieren - führen zu weniger Zeilen im Index und daher geringerer Fragmentierung. Häufig hängt allerdings das entsprechende Intervall der Synchronisierung von Anwenderanforderungen ab. Falls Sie mehr Informationen zur Synchronisierung bzw. zur Indexfragmentierung benötigen, schauen Sie einfach in den folgenden Blog. Regelmässige Optimierung ist also wichtige Operation, da die Indexfragmentierung und Indexgröße dabei reduziert werden und somit die Performance der Abfragen erhöht werden kann. Optimiert wird der Index mit der Prozedur CTX_DDL.OPTIMIZE_INDEX, die in unterschiedlichen Modi durchgeführt werden können:
    • FAST: Aufhebung der fragmentierten Zeilen, allerdings keine Optimierung der gelöschten Daten
    • FULL: Aufhebung der fragmentierten Zeilen und Optimierung der gelöschten Daten (Garbage)
    • REBUILD: wie FULL nur dass die $I Tabelle neuaufgebaut wird-u.U. schneller als FULL
    • TOKEN: volle Optimierung einzelner Tokens
    • TOKEN_TYPE: volle Optimierung bezogen auf einen bestimmten Tokentyp

    Der Anwender kann auch während einer OPTIMIZE-Operation weitersuchen und einfügen. Je nach Modus kann die OPTIMIZE-Operation allerdings sehr lange dauern, CPU und I/O intensiv sein und somit das ganze System beeinflussen. Daher ist es im Modus FULL möglich die OPTIMIZE-Operation mithilfe des Arguments MAXTIME auf eine gewisse Zeit zu beschränken. Dabei wird sich der letzte optimierte Zeitpunkt gemerkt und beim nächsten Anlauf dort weiter fortgeführt. Um eine genauere Vorstellung von der Funktionsweise zu erhalten, werden wir im Folgenden einige Optimierungen durchführen. Folgende Indexfragmentierung sei beim Index vorhanden, ausgelöst durch INSERT und DELETE Operationen. Die Ausgabe erfolgt analog zum Blog.

    
    ---------------------------------------------------------------------------
    FRAGMENTATION STATISTICS
    ---------------------------------------------------------------------------
    
    total size of $I data:                                270,000 (263.67 KB)
    
    $I rows:                                                           90,000
    estimated $I rows if optimal:                                          72
    estimated row fragmentation:                                        100 %
    
    garbage docids:                                                     7,001
    estimated garbage size:                                 52,297 (51.07 KB)
    
    most fragmented tokens:
      SAT (0:TEXT)                                                      100 %
      MAT (0:TEXT)                                                      100 %
      MARY (0:TEXT)                                                     100 %
      LAMB (0:TEXT)                                                     100 %
      CAT (0:TEXT)                                                      100 %
      LITTLE (0:TEXT)                                                   100 %
    
    
    Offensichtlich ist der Index fragmentiert und besitzt Garbage (gelöschte Daten). Wir werden nun den Index mit folgendem Kommando optimieren und dabei den Modus FAST verwenden:
    
    execute ctx_ddl.optimize_index (idx_name => 'TESTX', optlevel => 'FAST', parallel_degree => 1);
    
    
    Nach Abschluss der OPTIMIZE-Operation monitoren wir wieder die Fragmentieren. Die Fragmentierung ist verschwunden, allerdings ist noch Garbage vorhanden.
    
    ---------------------------------------------------------------------------
    FRAGMENTATION STATISTICS
    ---------------------------------------------------------------------------
    
    total size of $I data:                                270,003 (263.67 KB)
    
    $I rows:                                                               72
    estimated $I rows if optimal:                                          72
    estimated row fragmentation:                                          0 %
    
    garbage docids:                                                     7,001
    estimated garbage size:                                 52,298 (51.07 KB)
    
    most fragmented tokens:
      SAT (0:TEXT)                                                        0 %
      MAT (0:TEXT)                                                        0 %
      MARY (0:TEXT)                                                       0 %
      LAMB (0:TEXT)                                                       0 %
      CAT (0:TEXT)                                                        0 %
      LITTLE (0:TEXT)                                                     0 %
    
    
    
    Nun führen wir das Ganze statt im FAST Modus im FULL Modus aus:
    
    execute ctx_ddl.optimize_index (idx_name => 'TESTX', optlevel => 'FULL', parallel_degree => 1);
    
    
    Parallel können wir in einer zusätzlichen Session, die Optimierung beobachten:
    
    SQL>set pagesize 100
    SQL>col indx_name format a30
    SQL>col idx_opt_type format a20
    
    SQL> SELECT idx_name,idx_opt_token,idx_opt_type,idx_opt_count FROM ctxsys.dr$index; 
    IDX_NAME                              IDX_OPT_TOKEN IDX_OPT_TYPE IDX_OPT_COUNT
    -------------------- ------------------------------ ------------ -------------
    D01_F_ALL
    SUP_TEXT_IDX
    AUTH_IDX
    IDX_TEXT
    TESTX MAT                                                      0          7001
    TEXT_IND
    
    
    Mit unserem Monitoring Skript erhalten wir nun folgende Information über die Fragmentierung:
    
    ---------------------------------------------------------------------------
    FRAGMENTATION STATISTICS
    ---------------------------------------------------------------------------
    
    total size of $I data:                                206,997 (202.15 KB)
    $I rows:                                                               57
    estimated $I rows if optimal:                                          57
    estimated row fragmentation:                                          0 %
    
    garbage docids:                                                         0
    estimated garbage size:                                                 0
    
    most fragmented tokens:
      SAT (0:TEXT)                                                        0 %
      MAT (0:TEXT)                                                        0 %
      MARY (0:TEXT)                                                       0 %
      LAMB (0:TEXT)                                                       0 %
      CAT (0:TEXT)                                                        0 %
      LITTLE (0:TEXT)                                                     0 %
    
    
    
    
    Stellt man fest, dass häufig nach einzelnen Tokens gesucht wird, könnte man die Optimierung auch auf einzelne Tokens oder Token-Types (im Fall der MDATA Nutzung) beschränken. Im folgenden Beispiel werden wir dies für das Token LITTLE durchführen: Das Ergebnis sieht dann folgendermassen aus:
    
    ---------------------------------------------------------------------------
                             FRAGMENTATION STATISTICS
    ---------------------------------------------------------------------------
    
    total size of $I data:                                263,998 (257.81 KB)
    
    $I rows:                                                           80,007
    estimated $I rows if optimal:                                          71
    estimated row fragmentation:                                        100 %
    
    garbage docids:                                                     7,001
    estimated garbage size:                                 51,134 (49.94 KB)
    
    most fragmented tokens:
      SAT (0:TEXT)                                                      100 %
      MAT (0:TEXT)                                                      100 %
      MARY (0:TEXT)                                                     100 %
      LAMB (0:TEXT)                                                     100 %
      CAT (0:TEXT)                                                      100 %
      LITTLE (0:TEXT)                                                     0 %
    
    
    Mehr zur Optimierung in einem der nächsten Blogs....

    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 ...

    Montag, 16. Februar 2009

    Stopwörter und Stoplisten: Unwichtige Wörter ... (wirklich unwichtig? ...)

    Bekanntlich unterstützt Oracle TEXT Stopwortlisten. Wird ein Wort als Stopwort deklariert, wird es von Oracle TEXT völlig ignoriert - Stopwörter werden nicht in den Index aufgenommen und bei Abfragen insofern nicht berücksichtigt. Bei Abfragen auf einzelne Wörter (Tokens) ist das relativ klar; eine Suche nach "und" wäre auch sinnlos. Stopwörter haben allerdings einen Einfluß auf die Phrasensuche - sie passen auf jedes Wort. Sucht man beispielsweise nach der Phrase "Radio und Fernseher", so sucht Oracle TEXT (wenn die Stopwortliste aktiv ist und bspw. die Wörter "und", "oder" und "unter" enthält) im Index nach "Radio {?} Fernseher". Gefunden würden auch Dokumente, in denen die Phrase Radio oder Fernseher oder Radio im Fernseher vorkommt. Wenn solcherlei Effekte nicht gewünscht sind, muss man die Stopwortliste abschalten.
    Aber welche Wörter sind überhaupt Stopwörter ...? Dazu schaut man erstmal nach, welche Stopwortlisten es überhaupt gibt - die Stopwörter sind in Listen organisiert ...
    SQL> select * from ctx_stoplists
    
    SPL_OWN SPL_NAME                   SPL_COUNT SPL_TYPE
    ------- ------------------------- ---------- ----------------
    CTXSYS  DEFAULT_STOPLIST                 235 BASIC_STOPLIST
    WKSYS   WK_STOPLIST                       76 BASIC_STOPLIST
    CTXSYS  EMPTY_STOPLIST                     0 BASIC_STOPLIST
    CTXSYS  EXTENDED_STOPLIST                  0 BASIC_STOPLIST
    TM      SWL_BRANCHEN                      10 BASIC_STOPLIST
    
    5 Zeilen ausgewählt.
    
    Die CTXSYS.DEFAULT_STOPLIST ist eine Standardliste für die Sprache, mit der die Datenbank aufgesetzt wurde - Hierzulande ist das im Normalfall Deutsch. Und auch da kann man reingucken ...
    SQL> select spw_type, SPW_WORD from ctx_stopwords where SPW_STOPLIST='DEFAULT_STOPLIST'
    
    SPW_TYPE   SPW_WORD
    ---------- ------------------------------
    STOP_WORD  Ihnen
    STOP_WORD  Ihre
    STOP_WORD  Ihrem
    STOP_WORD  Ihren
    STOP_WORD  Ihrer
    STOP_WORD  Ihres
    :          :
    
    Enthalten sind also Bindewörter, Präpositionen, Artikel und ähnliche Wörter; also alles, was typischerweise in jedem Dokument vorkommt und daher ignoriert werden kann.
    Die Skripte, mit denen diese Defaults erzeugt werden befinden sich in $ORACLE_HOME/ctx/admin/defaults. So spielt das darin vorhandene Skript drdefd.sql die deutsche Standard-Stopliste ein; drdefus.sql entsprechend die US-amerikanische. Wenn in der Datenbank also eine englische Stopliste enthalten ist (weil beim Aufsetzen alles mit englischer Sprache gelaufen ist), so lässt sich das (als User CTXSYS) einfach ändern ...
    SQL> start ?/ctx/admin/defaults/dr0defdp.sql
    SQL> start ?/ctx/admin/defaults/drdefd.sql
    
    Stoplisten sind wie alle anderen Datenbankobjekte einem Schema zugeordnet; das Schema CTXSYS enthält die Standardlisten. Eigene Stoplisten sollten stets auch ins eigene Datenbankschema (und nicht in CTXSYS) abgelegt werden. Die Skripte können auch als Basis für Erweiterungen verwendet werden. Möchte man mit Stopwortlisten arbeiten, kann es durchaus sinnvoll sein, diese durch branchentypische Fachbegriffe zu erweitern. Die Standardlisten enthalten nur solche Wörter, von denen Oracle sicher ist, dass sie in nahezu jedem Dokument vorkommen. Es mag nun branchenspezifische Wörter geben, die man hinzufügen möchte: In einer Bank könnten dies Begriffe wie "Bank", "Betrag", "Konto" oder ähnliche sein. Beim Festlegen solcher Begriffe sollte man aber stets darauf achten, dass die Stopwörter (wie eingangs erwähnt) auch bei der Phrasensuche ignoriert werden.
    begin
      ctx_ddl.add_stopword(
        'MY_STOPLIST',
        'bank'
      );
    end;
    
    Ein Index mit dieser Stopliste wird dann so gebaut:
    create index idx_volltext on dokument_tab (spalte)
    indextype is ctxsys.context
    parameters ('stoplist my_stoplist')
    /
    
    Diese Syntax hat keinen Einfluß auf schon bestehende Volltextindizes. Möchte man auf einem schon erzeugten Index ein (zusätzliches) Stopwort einrichten, gibt es eine spezielle Syntax:
      alter index idx_volltext add stopword bank;
    
    Der Index wird dadurch nicht neu erzeugt, auch werden diese Wörter (so sie vorhanden sind) nicht aus dem Index gelöscht. Das neue Stopwort wird einfach nur in die Metadaten eingetragen, so dass es bei künftigen Abfragen und neuen Dokumenten wirken kann. Diese Operation kann es logischerweise nicht für das Entfernen von Stopwörtern geben; denn dazu müssen diese ja neu in den Index aufgenommen werden. Möchte man Stopwörter aus einem Index entfernen, so muss dieser neu gebaut werden.
    begin
      ctx_ddl.remove_stopword( 'MY_STOPLIST', 'bank');
      ctx_ddl.remove_stopword( 'MY_STOPLIST', '{...}');
    end;
    /
    
    ALTER INDEX idx_volltext REBUILD PARAMETERS('replace stoplist MY_STOPLIST')
    /
    
    Mehr zum Thema findet sich in der Oracle-Dokumentation: TEXT Application Developers' Guide und TEXT Reference.

    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
    

    Beliebte Postings