Dienstag, 8. Oktober 2013

Oracle Text in Oracle 12c: Automatic Near Real-Time Index

Oracle Database 12c steht schon seit einiger Zeit zur Verfügung und wie schon angesprochen, wollen wir im Rahmen unserer Blogpostings die neuen Features nach und nach beschreiben. Dieses Mal geht es um das sogenannte Feature "Automatic Near Real-Time Indexing". Dabei geht es um die typische Anforderung den Index möglich aktuell zu halten - sogar bei hoher Änderungsrate. Hoher Aktualitätsanspruch bedeutet allerdings häufiges Synchronisieren und führt natürlich zu hoher Fragmentierung bzw. zu vermehrtem und länger andauernden OPTIMIZE Operationen.

12c löst dieses Dilemma mit dem neuen Konzept des "two-level" Index bzw. auch unter dem Feature Name "near real time index" bekannt. Dies erlaubt einen kleinen, fragmentierten Stage Index, der alle aktuellen Veränderungen enthält, vorzuhalten, ohne Änderungen an dem großen Index vornehmen zu müssen. Die Idee dabei ist, dass der Stage Index klein genug ist, um in die SGA zu passen. Die Daten können dann nach und nach vom Stage Index zum Hauptindex verlagert werden. Dies geschieht mit einem neuen MERGE Modus für Indizes.

Folgendes Beispiel zeigt wie das Feature genutzt werden kann. Zuerst stellen wir eine leere Tabelle zur Verfügung.
drop table texttabelle
/
drop sequence s
/
create sequence s
/
create table texttabelle(
  id          number(10) default s.nextval,
  dokument varchar2(1000))
/
Realisiert wird das Feature dann mit der zusätzlichen Storage Option STAGE_ITAB.
exec ctx_ddl.drop_preference ('my_storage');
exec ctx_ddl.create_preference('my_storage', 'BASIC_STORAGE');
exec ctx_ddl.set_attribute ('my_storage', 'STAGE_ITAB', 'true');
Nun erzeugen wir den Index.
create index my_index on texttabelle (dokument) 
indextype is ctxsys.context
parameters( 'storage my_storage sync (on commit)');
Überprüfen wir die erzeugten Tabellenobjekte, stellen wir fest, dass zusätzlich eine neue Tabelle mit Namen DR$MY_INDEX$G generiert wurde, die die gleiche Struktur wir die $I Tabelle besitzt.
SQL> select * from tab
  2  /

TNAME                          TABTYPE  CLUSTERID
------------------------------ ------- ----------
DR$MY_INDEX$G                  TABLE
DR$MY_INDEX$N                  TABLE
DR$MY_INDEX$R                  TABLE
DR$MY_INDEX$K                  TABLE
TEXTTABELLE                    TABLE
DR$MY_INDEX$I                  TABLE

6 rows selected.
Darüberhinaus werden weitere Objekte wie zum Beispiel ein $H B-tree Index auf der $G Tabelle erzeugt - dieser erfüllt die gleiche Aufgabe wie der $X Index auf der $I Tabelle.

Nun fügen wir einfach zwei Zeilen ein und schließen jeweils mit COMMIT ab.
insert into texttabelle (dokument) values 
         ('A-Partei gewinnt Wahl in Hansestadt');
commit;
insert into texttabelle (dokument) values 
         ('Terror in Nahost: Kriminalität steigt immer weiter an'); 
commit;
Überprüfen wir die Inhalte der beiden Tabellen, stellen wir fest, dass die $I Tabelle leer ist, wohingegen die $G Tabelle befüllt wurde. Neue Inhalte werden also nicht mehr direkt in der $I Tabelle geschrieben, sondern in der $G Tabelle mitgeführt.
SQL> select token_text from DR$MY_INDEX$I;
no rows selected

SQL> select token_text from DR$MY_INDEX$G;

TOKEN_TEXT
----------------------------------------------------------------
GEWINNT
HANSESTADT
IMMER
KRIMINALIT?T
NAHOST
PARTEI
STEIGT
TERROR
WAHL
WEITER

10 rows selected.
Um diese Tabelle in der SGA zu halten, kann man sich entweder auf das normale Caching Verhalten verlassen oder aber - falls konfiguriert - den KEEP Pool der Datenbank nutzen. Dazu kann man die folgenden Storage Attribute verwenden.
exec ctx_ddl.set_attribute ('my_storage', 'G_TABLE_CLAUSE', 
                                                  'storage (buffer_pool keep)');
exec ctx_ddl.set_attribute ('my_storage', 'G_INDEX_CLAUSE', 
                                                 'storage (buffer_pool keep)');
Möchte man nun die Daten in den Hauptindex manuell integrieren, kann man dazu das folgende OPTIMIZE Kommando verwenden.
execute ctx_ddl.optimize_index(idx_name=>'MY_INDEX', optlevel=>'MERGE');
Damit werden die Einträge aus $G in optimierter Form in die $I Tabelle überführt und gleichzeitig aus der $G Tabelle gelöscht.

Optimal wäre es nun, wenn dieser Aufgabe von Oracle automatisch durchgeführt werden könnte. Die neue Prozedur ADD_AUTO_OPTIMIZE erfüllt diese Aufgabe.
exec ctx_ddl.add_auto_optimize( 'my_index' )
PL/SQL procedure successfully completed.
Nach der Ausführung wird der Index zur automatischen Optimierung registriert, wie wir in der folgenden View sehen können.
SQL> select * from ctx_user_auto_optimize_indexes;

AOI_INDEX_NAME                 AOI_PARTITION_NAME
------------------------------ ------------------------------
MY_INDEX
Ausgeführt wird diese Aktion von einem DBMS_SCHEDULER Job, der automatisch im Hintergrund angelegt und ausgeführt wird.
SQL> select job_name, program_name, schedule_type, last_start_date 
     from dba_scheduler_jobs 
     where owner='CTXSYS';

JOB_NAME             PROGRAM_NAME         SCHEDULE_TYP
-------------------- -------------------- ------------
LAST_START_DATE
---------------------------------------------------------------------------
DR$BGOPTJOB          DR$BGOPTPRG          IMMEDIATE
08-OCT-13 04.42.13.977418 PM EUROPE/VIENNA
Übrigens lässt sich dieses Konzept des "two-level" Index - auch nachträglich mit einem ALTER INDEX REBUILD Kommando zu einem bestehenden Index hinzufügen.
alter index my_index rebuild parameters('replace storage my_storage');

Donnerstag, 29. August 2013

DBMS_PCLXUTIL zur Erzeugung von lokalen Indizes

Um die Performance beim Aufbau eines Index zu erhöhen, können Oracle Text Indizes mit dem Schlüsselwort PARALLEL aufgebaut werden. Geht es um den parallelen Aufbau eines lokalen partitionierten Index, kann allerdings auch das altbewährte Package DBMS_PCLXUTIL eine Alternative darstellen. Wer eine Beschreibung des Package sucht, wird im PL/SQL Packages Guide fündig; die Verwendung im Oracle Text Umfeld ist etwas versteckt im Oracle Text Reference Guide nachzuschlagen. Da es in letzter Zeit vermehrt Anfragen dazu gab, haben wir uns entschlossen, die Verwendung an einem Beispiel zu demonstrieren.
Zuerst zur Definition des Package: Das Package DBMS_PCLXUTIL sieht folgendermassen aus:
desc dbms_pclxutil
PROCEDURE BUILD_PART_INDEX
 Argument Name                  Type                    In/Out Default?
 ------------------------------ ----------------------- ------ --------
 JOBS_PER_BATCH                 NUMBER                  IN     DEFAULT
 PROCS_PER_JOB                  NUMBER                  IN     DEFAULT
 TAB_NAME                       VARCHAR2                IN     DEFAULT
 IDX_NAME                       VARCHAR2                IN     DEFAULT
 FORCE_OPT                      BOOLEAN                 IN     DEFAULT
Die Verwendung von TAB_NAME und IDX_NAME muß sicherlich nicht erklärt werden. Anzumerken ist höchstens, dass man nur als Eigentürmer der Tabellen und des Index das Package verwenden kann; eine Benennung über eine zusätzliche Schemabezeichnung ist nicht möglich. Die beiden Parameter JOBS_PER_BATCH und PROCS_PER_JOB bestimmen dabei im Unterschied zu anderen Methoden die Parallelisierung auf zwei (!) Ebenen. JOBS_PER_BATCH ist für die sogenannte Inter Parallelität zuständig, die mit DBMS_JOB Prozessen realisiert wird, und PROCS_PER_JOB für die sogenannte Intra Parallelität, die mit parallelen Prozessen ausgeführt wird.

Genauer bedeutet dies:
JOBS_PER_BATCH steht für die Anzahl der Job Prozesse, die gleichzeitig arbeiten. Hier sollte gelten: Das Minimum ist 1, das Maximum stellt die Anzahl der Partitionen dar.

PROCS_PER_JOB bestimmt die Anzahl der parallelen Query Prozesse pro Job. Auch hier stellt die Zahl 1 das Minimum dar.

FORCE_OPT kann den Wert TRUE oder FALSE haben. FALSE führt nur ein REBUILD für UNUSABLE Indizes aus, TRUE hingegen für alle Partitionen.

Bevor wir starten, sollte zuerst die Einstellung der Job Prozesse geprüft werden. Der Initialisierungsparameter JOB_QUEUE_PROCESSES gibt die maximale Anzahl der Job Prozesse an.
show parameter job

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
job_queue_processes                  integer     1000
Wichtig zu wissen ist, dass die Prozedur BUILD_PART_INDEX davon ausgeht, dass die Data Dictionary Informationen zum Index schon existieren. Falls nicht erhält man folgende Fehlermeldung:
*
ERROR at line 1:
ORA-20001: Specified local index name 'PROD_DESC_IDX' does not exist
ORA-06512: at "SYS.DBMS_PCLXUTIL", line 301
ORA-06512: at line 1
Vorab ist also folgendes Kommando sinnvoll:
CREATE INDEX prod_desc_idx ON product_part (prod_desc) INDEXTYPE IS ctxsys.context LOCAL UNUSABLE;
Ab 11g Release 2 wird für UNUSABLE Index Partitionen übrigens kein Speicherplatz mehr verbraucht, so dass die Information nicht in USER_SEGMENTS verzeichnet ist. Die Data Dictionary Informationen sind aber wie immer in USER_INDEXES gespeichert.
SELECT * FROM user_segments WHERE segment_name='PROD_DESC_IDX';

no rows selected

SELECT index_name, index_type FROM user_indexes WHERE index_name LIKE 'PROD_DESC%'

INDEX_NAME           INDEX_TYPE
-------------------- ---------------------------
PROD_DESC_IDX        DOMAIN
Um eine Anwendung zu demonstrieren, erzeugen wir einen lokalen Text Index auf die Spalte PROD_DESC der partitionierten Tabelle PRODUCTS_PART mit den 4 Partitionen P_10, P_100, P_1000 und PARTMAXVALUE.
EXECUTE dbms_pclxutil.build_part_index
(JOBS_PER_BATCH => 4,
 PROCS_PER_JOB  => 1,
 TAB_NAME       => 'PRODUCT_PART',
 IDX_NAME       => 'PROD_DESC_IDX',
 FORCE_OPT      => TRUE); 
Parallel dazu lassen sich die Job Prozesse überwachen - es werden 4 Jobs gestartet jeweils mit Parallelität 1.
SELECT job, this_date, next_date, failures, what FROM dba_jobs;

       JOB THIS_DATE        NEXT_DATE        FAILURES
---------- ---------------- ---------------- ----------
WHAT
--------------------------------------------------------------------------------
        42                  27.08.2013 17:41
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_100" parallel (degree 1)');

        43                  27.08.2013 17:41
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_1000" parallel (degree 1)');

        44                  27.08.2013 17:41
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "PARTMAXVALUE" parallel (degree 1)');

        41                  27.08.2013 17:41
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_10" parallel (degree 1)');
Am Schluss sollte natürlich auch die erfolgreiche Indexerstellung überprüft werden.
SELECT err_timestamp, err_text FROM ctx_user_index_errors
ORDER BY err_timestamp DESC;

no rows selected
Möglich wäre allerdings auch folgendes Kommando mit insgesamt 2 Jobs - dabei jeweils 2 parallele Prozessen pro Job.
EXECUTE dbms_pclxutil.build_part_index
(JOBS_PER_BATCH => 2,
 PROCS_PER_JOB  => 2,
 TAB_NAME       => 'PRODUCT_PART',
 IDX_NAME       => 'PROD_DESC_IDX',
 FORCE_OPT      => TRUE);
Prüft man die Jobs, stellt man folgende Aufrufe fest.
SELECT job, this_date, next_date, failures, what FROM dba_jobs;

       JOB THIS_DATE NEXT_DATE FAILURES
---------- --------- --------- ----------
WHAT
--------------------------------------------------------------------------------
       221 28-AUG-13 28-AUG-13
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "PARTMAXVALUE" parallel (degree 2)');

       222 28-AUG-13 28-AUG-13
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_10" parallel (degree 2)');
Dann nach einer gewissen Zeit ...
SELECT job, this_date, next_date, failures, what FROM dba_jobs;

       JOB THIS_DATE NEXT_DATE FAILURES
---------- --------- --------- ----------
WHAT
--------------------------------------------------------------------------------
       241 28-AUG-13 28-AUG-13
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_100" parallel (degree 2)');

       242 28-AUG-13 28-AUG-13
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_1000" parallel (degree 2)');
Er laufen also immer zwei Jobs zu einer Zeit. Prüft man gleichzeitig die Parallelität über v$px_session und V$session, erkennt man, dass wirklich 2 Prozesse pro Job arbeiten.
Username     QC/Slave   Slave Set  SID    QC SID Requested DOP Actual DOP
------------ ---------- ---------- ------ ------ ------------- ----------
SH           QC                    34     34
 - p001      (Slave)    1          43     34                 2          2
 - p000      (Slave)    1          87     34                 2          2
SH           QC                    94     94
 - p003      (Slave)    1          95     94                 2          2
 - p002      (Slave)    1          98     94                 2          2
Wo liegt nun der Unterschied zwischen den beiden Läufen? Beim zweiten Lauf arbeiten mehrere -hier 2 - Prozesse pro Job gleichzeitig. Unter Umständen sind dadurch die Index Partitionen schneller erstellt, was bei großen Indizes mit langen Laufzeiten sicherlich wünschenswert ist. Allerdings ist folgendes zu beachten: Da wir mit mehreren Prozessen pro Job arbeiten, kann es zu einer höheren Fragmentierung der einzelnen Index Partitionen kommen. Hier wäre dann ein anschliessendes CTX_DDL.OPTIMIZE_INDEX nötig.

Zum Schluss vielleicht noch ein wichtiger Hinweis: Vergessen Sie bei all diesen Operationen die Memory Einstellung nicht. Die Überprüfung kann zum Beispiel über CTX_PARAMETERS erfolgen.
SELECT * FROM ctx_parameters WHERE par_name LIKE '%MEM%';

PAR_NAME                       PAR_VALUE
------------------------------ -----------------------------------
DEFAULT_INDEX_MEMORY           67108864
MAX_INDEX_MEMORY               1073741824
Da DBMS_PCLXUTIL ein Package zur allgemeinen Verwendung ist, sind keine Text spezifischen Einstellungen über den Package Aufruf möglich - also auch keine Memory Einstellungen. Möchte man eine spezielle Memory Einstellung verwenden, kann beispielsweise die Prozedur CTXSYS.CTX_ADM.SET_PARAMETER verwendet werden.

Montag, 26. August 2013

Alle 12c Oracle Text Features auf einen Blick!

Mit der neuen Oracle Database 12c Version sind einige interessante neue Features im Oracle Text Umfeld implementiert worden. Nach und nach wollen wir diese in unseren deutschsprachigen Postings thematisieren.

Wer jetzt allerdings schon einen Überblick über alle neuen Text Features erhalten möchte, kann entweder im
Oracle Text Application Developer's Guide 12c Release 1 (12.1) im Abschnitt New Features recherchieren oder sich über das neue Whitepaper New Features in Oracle Text with Oracle Database12c informieren.

Viel Spaß dabei!

Montag, 15. Juli 2013

Oracle TEXT in Oracle12c: Neues Feature 'Pattern Stopclass'

Heute festgestellt, dass das letzte Blog Posting schon fast 3 Monate zurückliegt. Nun wird es aber Zeit: Das neue Datenbankrelease Oracle12c bringt auch im Bereich Oracle TEXT einige neue Features mit sich - diese werden wir nach und nach in diesem Blog besprechen. Heute geht es um die neue Möglichkeit, eine Stopwortliste nicht nur mit einzelnen Wörtern, sondern mit Stop-Patterns zu versehen. Hierauf haben viele sicherlich schon lange gewartet. Ein Beispiel: Wir erzeugen eine Tabelle und füllen diese mit ein paar "Wörtern" ...
create table tab_stopclasstest (
  textcol  varchar2(200)
)
/

insert into tab_stopclasstest values ('200');
insert into tab_stopclasstest values ('100');
insert into tab_stopclasstest values ('99');
insert into tab_stopclasstest values ('Oracle TEXT');
insert into tab_stopclasstest values ('A100');
insert into tab_stopclasstest values ('01.09.2012');
Wenn es nun an die Definition der Stopwörter geht, so konnte man bislang mit CTX_DDL.CREATE_STOPLIST eine Stopliste erzeugen und mit CTX_DDL.ADD_STOPWORD die Wörter ("Oracle") hinzufügen.

begin
  ctx_ddl.create_stoplist(
    stoplist_name => 'NEUE_STOPLISTE'
  );
  ctx_ddl.add_stopword ('NEUE_STOPLISTE', 'Oracle');
  ctx_ddl.add_stopclass('NEUE_STOPLISTE', 'NUMBERS');
end;
/
Aber genau dabei beginnt in vielen Fällen das Problem: Denn wenn Zahlen nicht indiziert werden sollen, ist es nahezu unmöglich, im Vorfeld alle möglichen Varianten als Stopwörter zu bestimmen. Die Stopclasses machen nun genau das möglich (übrigens kann man die Stopklasse NUMBERS schon in Oracle 11.2 verwenden) - aber dort eben nicht mehr. Anders in Oracle12c ...
begin
  ctx_ddl.create_stoplist(
    stoplist_name => 'NEUE_STOPLISTE'
  );
  ctx_ddl.add_stopword ('NEUE_STOPLISTE', 'Oracle');
  ctx_ddl.add_stopclass('NEUE_STOPLISTE', 'NUMBERS');
  ctx_ddl.add_stopclass('NEUE_STOPLISTE', 'KLASSE_1','[A-Z]\d+');
end;
/
CTX_DDL.ADD_STOPCLASS nimmt drei Parameter entgegen. Der erste ist, wie schon bei ADD_STOPWORD, der Name der Stopliste. Danach kommt die "Stopklasse" - Oracle bringt eine vordefinierte Klasse mit: NUMBERS, die, wie gesagt, schon in 11.2 vorhanden ist. Ab Oracle 12.1 kann man aber auch einen anderen Namen eintragen - und dann braucht es noch den dritten Parameter: Dort wird ein regulärer Ausdruck hinterlegt, der die zu ignorierenden Wörter erfasst. In obigem Beispiel wäre das genau ein Buchstabe, gefolgt von mindestens einer Zahl. Legt man, basierend auf dieser Stopliste einen Index auf die obige Tabelle an, so wird dieser nur ein einziges Token enthalten: TEXT.
create index ft_stopclasstest on tab_stopclasstest (textcol)
indextype is ctxsys.context 
parameters ('stoplist neue_stopliste')
/

select token_text from dr$ft_stopclasstest$i
/

TOKEN_TEXT
-----------------------------------------
TEXT
Viel Spaß beim Ausprobieren.

Donnerstag, 18. April 2013

Indexstatistiken (INDEX_STATS) im XML-Format: Oracle TEXT Management einfach automatisieren!

Heute geht es um Statistiken für einen Oracle TEXT Index - vor einiger Zeit gab es dazu schon mal ein Posting - heute soll es darum gehen, wie man diese Statistiken so zur Verfügung stellt, dass man mit Reporting- oder Management-Tools einfach darauf zugreifen oder diese zur Automatisierung von Aufgaben verwenden kann. Indexstatistiken kann man mit CTX_REPORT.INDEX_STATS abrufen. In nahezu allen Beispielen (so auch im erwähnten Blog-Posting) werden die Statistiken als Textausgabe erzeugt - in etwa wie folgt ...
===========================================================================
                    STATISTICS FOR "TESTIT"."IDX_TEXT"
===========================================================================

indexed documents:                                                     10
allocated docids:                                                      10
$I rows:                                                               56

---------------------------------------------------------------------------
                             TOKEN STATISTICS
---------------------------------------------------------------------------
:
Dieses Format ist zwar gut lesbar, zur Automatisierung jedoch völlig ungeeignet. Allerdings bietet Oracle TEXT noch eine andere Variante zur Ausgabe der Statistiken an: XML - und das geht wie folgt.
DROP TABLE ausgabe
/

CREATE TABLE ausgabe (
  index_name  varchar2(200),
  zeitstempel date,
  resultat    xmltype
)
xmltype column resultat store as binary xml
/

 
declare
  ergebnis clob := null;
begin
  ctx_report.index_stats(
    index_name     => 'IDX_TEXT',
    report         => ergebnis,
    report_format  => ctx_report.fmt_xml,
    stat_type      => null
  );
  insert into ausgabe values ('IDX_TEXT', sysdate, xmltype(ergebnis));
  dbms_lob.freetemporary(ergebnis);
end;
/

 
set long 32000
set head off
set pagesize 10000

SELECT * FROM ausgabe
/
Das generierte Format sieht nun wie folgt aus ...
<CTXREPORT>
  <INDEX_STATS>
    <STAT_INDEX_NAME>"TESTIT"."IDX_TEXT"</STAT_INDEX_NAME>
    <STAT_INDEX_STATS>
      <STAT_STATISTIC NAME="indexed documents">10</STAT_STATISTIC>
      <STAT_STATISTIC NAME="allocated docids">10</STAT_STATISTIC>
      <STAT_STATISTIC NAME="$I rows">56</STAT_STATISTIC>
      :
Bei großen Indizes braucht der Aufruf von INDEX_STATS sehr lange - hier ist es sicher sinnvoll, mit Hilfe von DBMS_SCHEDULER einen Job zu erzeugen, welcher die Statistiken regelmäßig (bspw. über Nacht) aktualisiert. Das Interessante am XML-Format ist nun, dass es sich maschinell auswerten lässt. Dazu nutzen wir die XML-Funktionen in der Oracle-Datenbank und erstellen eine (relationale) View auf die XML-Ausgabe von CTX_REPORT.INDEX_STATS.
create or replace view view_index_stats as
select
  a.index_name,
  a.zeitstempel,
  x.indexed_documents,
  x.allocated_docids,
  x.dollar_i_rows,
  x.dollar_i_dsize,
  x.index_frag,
  x.garbage_docids,
  x.garbage_size
from 
  ausgabe a,
  xmltable(
    '/CTXREPORT/INDEX_STATS'
    passing a.resultat
    columns 
      indexed_documents number path '/INDEX_STATS/STAT_INDEX_STATS/STAT_STATISTIC[@NAME="indexed documents"]',
      allocated_docids  number path '/INDEX_STATS/STAT_INDEX_STATS/STAT_STATISTIC[@NAME="allocated docids"]',
      dollar_i_rows     number path '/INDEX_STATS/STAT_INDEX_STATS/STAT_STATISTIC[@NAME="$I rows"]',
      dollar_i_dsize    number path '/INDEX_STATS/STAT_FRAG_STATS/STAT_STATISTIC[@NAME="total size of $I data"]',
      index_frag        varchar2(10) path '/INDEX_STATS/STAT_FRAG_STATS/STAT_STATISTIC[@NAME="estimated row fragmentation"]',
      garbage_docids    number path '/INDEX_STATS/STAT_FRAG_STATS/STAT_STATISTIC[@NAME="garbage docids"]',
      garbage_size      number path '/INDEX_STATS/STAT_FRAG_STATS/STAT_STATISTIC[@NAME="estimated garbage size"]'
  ) x
/
Diese View lässt sich nun wie eine Tabelle verwenden ...
SQL> select * from view_index_stats

INDEX_NAME ZEITSTEMPEL          INDEXED_DOCUMENTS ALLOCATED_DOCIDS
---------- -------------------- ----------------- ----------------
DOLLAR_I_ROWS DOLLAR_I_DSIZE INDEX_FRAG GARBAGE_DOCIDS GARBAGE_SIZE
------------- -------------- ---------- -------------- ------------
IDX_TEXT   18.04.2013 14:43:49                 10               10
           56            198  0 %                    0            0


1 Zeile wurde ausgewählt.
Nicht so schön ist die Spalte INDEX_FRAG - denn das XML, welches von INDEX_STATS generiert wurde, enthält hier tatsächlich das Prozentzeichen - weshalb es nicht als NUMBER aufgefasst werden kann - hierfür müssen wir in der View-Definition noch ein wenig was tun ...
create or replace view view_index_stats as
select
  a.index_name,
  a.zeitstempel,
  x.indexed_documents,
  x.allocated_docids,
  x.dollar_i_rows,
  x.dollar_i_dsize,
  to_number(replace(x.index_frag, ' %', '')) as index_frag,
  x.garbage_docids,
 :
Danach ist auch die Spalte INDEX_FRAG vom Typ NUMBER und man kann nun numerische Vergleiche durchführen. Analog dazu lassen sich auch die Token-Statistiken entsprechend aufbereiten ... Dazu bauen wir eine zweite View ...
create or replace view index_stats_frag_tokens as
select
  a.index_name,
  a.zeitstempel,
  x.token_text,
  x.token_type,
  to_number(replace(x.token_frag, ' %', '')) as token_frag
from 
  ausgabe a,
  xmltable(
    '/CTXREPORT/INDEX_STATS/STAT_FRAG_STATS/STAT_TOKEN_LIST/STAT_TOKEN'
    passing a.resultat
    columns 
      token_text   varchar2(64) path '/STAT_TOKEN/STAT_TOKEN_TEXT',
      token_type   varchar2(50) path '/STAT_TOKEN/STAT_TOKEN_TYPE',
      token_frag   varchar2(20) path '/STAT_TOKEN/STAT_TOKEN_STATISTIC'
  ) x
/
  
select * from index_stats_frag_tokens
/
... deren Inhalt nun wie folgt aussieht ...
SQL> select * from index_stats_frag_tokens

INDEX_NAME ZEITSTEMPEL          TOKEN_TEXT           TOKEN_TYPE           TOKEN_FRAG
---------- -------------------- -------------------- -------------------- ----------
IDX_TEXT   18.04.2013 14:55:28  ZU                   0:TEXT                        0
IDX_TEXT   18.04.2013 14:55:28  WOLLEN               0:TEXT                        0
IDX_TEXT   18.04.2013 14:55:28  WIRTSCHAFT           0:TEXT                        0
IDX_TEXT   18.04.2013 14:55:28  WIRD                 0:TEXT                        0
IDX_TEXT   18.04.2013 14:55:28  WICHTIGER            0:TEXT                        0
IDX_TEXT   18.04.2013 14:55:28  WERDEN               0:TEXT                        0
IDX_TEXT   18.04.2013 14:55:28  WER                  0:TEXT                        0
IDX_TEXT   18.04.2013 14:55:28  WEITER               0:TEXT                        0
IDX_TEXT   18.04.2013 14:55:28  WAHLKAMPF            0:TEXT                        0
:
Lässt man das XML nun, wie schon erwähnt, per DBMS_SCHEDULER regelmäßig aktualisieren, so hat man (wie im Data Dictionary) bequem nutzbare Views mit Statistiken zum Index. Diese können nun natürlich auch für eine automatisierte Verarbeitung genutzt werden. So könnte ein Job regelmäßig alle Tokens optimieren (CTX_DDL.OPTIMIZE mit OPTLEVEL_TOKEN), die eine bestimmte Fragmentierung überschreiten. Gleiches gilt natürlich auf Indexebene.
Auch die Integration mit Management-Werkzeugen wie dem Oracle Enterprise Manager ist kein Problem. In der deutschsprachigen DBA Community ist beschrieben, wie man eine eigene "Metrik", basierend auf einer SQL-Abfrage einrichtet. Nimmt man hierfür die erstellten Views, so kann der Enterprise Manager einen Alert senden, sobald die Indexfragmentierung einen bestimmten Schwellenwert übersteigt. Die professionelle Wartung einer Oracle TEXT-Installation ist damit kein Problem mehr ...

Dienstag, 5. März 2013

Alle Posts auf einen Blick

Um einen besseren Überblick über die schon veröffentlichten Posts in diesem Blog zu bekommen, an dieser Stelle eine aktuelle Linksammlung ...

Grundsätzliche Themen: Administration, Index anlegen ...
Performance, Monitoring ...
Spezielle Suchabfragen unterstützen
Mixed Queries, Datastores ...

Mittwoch, 12. Dezember 2012

Suche nach "Aktenzeichen" oder "Autonummer" mit Oracle TEXT: Sind Printjoins die Lösung?

Heute möchte ich mich dem Thema "Suche nach Sonderzeichen" und dem damit verbundenen Thema Printjoins in Oracle TEXT widmen. Vorab schon soviel: Dieses Posting wird eine Warnung vor Printjoins - mit diesem Feature sollte sehr vorsichtig umgegangen werden. Printjoins werden mitunter verwendet, wenn man "Strukturen" wie Aktenzeichen oder "Autonummern" in den Dokumenten hat. Das könnte in etwa so aussehen.
create table dokumente(
  id      number(10),
  doc     varchar2(4000)
)
/

insert into dokumente values (
  1, 'Aktenzeichen 67.MEIER.1455-2012: Steuersache Meier.Erklärung abgegeben'
); 
insert into dokumente values (
  2, 'Aktenzeichen 12.MUSTER.1455-2012: Steuersache Muster.Erklärung abgegeben.'
); 

commit
/
Immer wieder kommt die Anforderung, exakt nach dem Aktenzeichen suchen zu können. Oracle Text erkennt diese Struktur jedoch nicht und indiziert wie folgt:
create index ft_dokumente on dokumente(doc)
indextype is ctxsys.context
/

select token_text from dr$ft_dokumente$i
/

TOKEN_TEXT
----------------------
12
1455
2012
67
ABGEGEBEN
AKTENZEICHEN
ERKLÄRUNG
MEIER
MUSTER
STEUERSACHE
Wenn man nun nach dem Term 1455 sucht, werden beide Dokumente zurückgeliefert. Fachlich ist das eigentlich falsch, denn die 1455 kommt alleinstehend nirgends vor - sie ist überall Teil des Aktenzeichens.
SQL> select * from dokumente where contains(doc, '1455') > 0
/

        ID DOC
---------- --------------------------------------------------
         1 Aktenzeichen 67.MEIER.1455-2012: Steuersache Meier
           .Erklärung abgegeben

         2 Aktenzeichen 12.MUSTER.1455-2012: Steuersache Must
           er.Erklärung abgegeben.
Um diesen Effekt zu verhindern, werden dann gerne Printjoins eingesetzt. Zeichen, die als Printjoins deklariert werden, trennen Wörter nicht mehr voneinander - sie werden dann (nicht ganz) wie Buchstaben behandelt. Ist also das Zeichen "-" als Printjoin deklariert, dann wird der Willy-Brandt-Platz als ein Token "Willy-Brandt-Platz" indiziert und nicht als drei Tokens "Willy", "Brandt" und "Platz".
begin
  ctx_ddl.drop_preference('MY_PJ_PREF');
end;
/
sho err

begin
  ctx_ddl.create_preference('MY_PJ_PREF', 'BASIC_LEXER');
  ctx_ddl.set_attribute('MY_PJ_PREF', 'PRINTJOINS', '.-');
end;
/
sho err

create index ft_dokumente on dokumente(doc)
indextype is ctxsys.context
parameters  ('lexer MY_PJ_PREF')
/

select token_text from dr$ft_dokumente$i
/

TOKEN_TEXT
-----------------------------
12.MUSTER.1455-2012
67.MEIER.1455-2012
ABGEGEBEN
AKTENZEICHEN
MEIER.ERKLÄRUNG
MUSTER.ERKLÄRUNG
STEUERSACHE
Die Anforderung, dass Teile des Aktenzeichens nicht mehr das Aktenzeichen finden, ist erfüllt. Auf den ersten Blick ist das doch eine gute Lösung, oder ...?
  
select * from dokumente where contains(doc, '1455') > 0;

Es wurden keine Zeilen ausgewählt

select * from dokumente where contains(doc, '{67.MEIER.1455-2012}') > 0;

        ID DOC
---------- ----------------------------------------------------------------------
         1 Aktenzeichen 67.MEIER.1455-2012: Steuersache Meier.Erklärung abgegeben
Wie man aber schon am Inhalt der Token-Tabelle erkennen kann, hat das ganze einige "Nebenwirkungen" ... die Suche nach dem Meier schlägt nun fehl.
SQL> select * from dokumente where contains(doc, 'Meier') > 0
/

Es wurden keine Zeilen ausgewählt
Das ist logisch, weil das Token Meier gar nicht indiziert wurde. In den Dokumenten fehlt dummerweise das Leerzeichen nach dem Punkt zwischen Meier und Erklärung. Da der Punkt selbst ein Printjoin ist, wurde Meier.Erklärung indiziert. Und eine Suche nach dem Meier schlägt nun fehl. Printjoins werden stets global für den ganzen Index definiert. Wenn also der Bindestrich eines Aktenzeichens als Printjoin deklariert wird, gilt das nicht nur für die Aktenzeichen, sondern für alle Bindestriche im gesamten Dokumentbestand:
Das Aufnehmen zusätzlicher Zeichen zu den Printjoins sollte also stets mit Vorsicht gemacht werden, es führt fast immer zu unerwünschten Nebenwirkungen, für die dann aufwändige Workarounds mit Wildcards ("Meier%") nötig werden.
Doch wie geht man mit dem Thema Aktenzeichen um? Eine denkbare Lösung könnte ein PROCEDURE_FILTER sein. Dieser sucht mit einem regulären Ausdruck nach dem Aktenzeichen und wandelt die Bindestriche und Punkte in ein Zeichen, welches definitiv keine Probleme macht, um - das könnte bspw. der Underscore ("_") sein. Zunächst erstellen wir also die Prozedur für den PROCEDURE_FILTER.
create or replace function escape_aktenzeichen(p_az in varchar2) return varchar2 deterministic is
begin
  return regexp_replace(p_az, '(\d\d)(.)([A-Z]*)(.)(\d*)(-)(\d*)', '\1_\3_\5_\7');
end escape_aktenzeichen;
/
sho err

create or replace procedure aktenzeichen_filter(
 p_src IN            VARCHAR2, 
 p_dst IN OUT NOCOPY VARCHAR2
) is begin
  p_dst := escape_aktenzeichen(p_src);
end aktenzeichen_filter;
/
sho err
Dass die eigentliche Funktionalität in eine separate Funktion gepackt wurde, hat einen Sinn - dazu weiter unten mehr. Dann erstellen wir die Filter Preference ...
begin
  ctx_ddl.create_preference('MY_AZ_FILTER', 'procedure_filter');
  ctx_ddl.set_attribute('MY_AZ_FILTER', 'procedure', 'aktenzeichen_filter');
  ctx_ddl.set_attribute('MY_AZ_FILTER', 'input_type', 'varchar2');
  ctx_ddl.set_attribute('MY_AZ_FILTER', 'output_type', 'varchar2');
  ctx_ddl.set_attribute('MY_AZ_FILTER', 'rowid_parameter', 'false');
  ctx_ddl.set_attribute('MY_AZ_FILTER', 'charset_parameter', 'false');
end;
/
sho err
... und nicht zu vergessen: Wir definieren die Lexer Preference neu, damit der Underscore (und nur der Underscore) das neue Printjoin wird.
begin
  ctx_ddl.drop_preference('MY_PJ_PREF');
end;
/
sho err

begin
  ctx_ddl.create_preference('MY_PJ_PREF', 'BASIC_LEXER');
  ctx_ddl.set_attribute('MY_PJ_PREF', 'PRINTJOINS', '_');
end;
/
sho err
Nun noch indizieren ...
create index ft_dokumente on dokumente(doc)
indextype is ctxsys.context
parameters  ('lexer MY_PJ_PREF filter MY_AZ_FILTER')
/
Und jetzt sieht die Token-Tabelle so aus:
TOKEN_TEXT
-------------------------
12_MUSTER_1455_2012
67_MEIER_1455_2012
ABGEGEBEN
AKTENZEICHEN
ERKLÄRUNG
MEIER
MUSTER
STEUERSACHE
Eine Suche nach 1455 schlägt nun fehl, so wie es sein soll.
select * from dokumente where contains(doc, '1455') > 0;

Es wurden keine Zeilen ausgewählt
Wenn nun nach einem Aktenzeichen gesucht werden soll, muss man das Aktenzeichen in der Suchanfrage natürlich auch umwandeln - es darf also nicht mehr nach 12.MUSTER.1455-2012, vielmehr muss nach 12_MUSTER_1455_2012 gesucht werden. Und jetzt ist es sehr nützlich, dass wir vorhin die Funktion ESCAPE_AKTENZEICHEN gebaut haben ...
select * from dokumente where contains(doc, escape_aktenzeichen('12.MUSTER.1455-2012')) > 0;

        ID DOC
---------- --------------------------------------------------
         2 Aktenzeichen 12.MUSTER.1455-2012: Steuersache Must
           er, Erklärung abgegeben.
Voilá. Und das ganze lässt sich natürlich auch mit binären (PDF, Office)-Dokumenten kombinieren - in diesem Fall muss der PROCEDURE_FILTER vor dem Anwenden des regulären Ausdrucks mit CTX_DOC.POLICY_FILTER das eigentliche Umwandeln des Binärformats in ASCII-Text machen.
create or replace procedure aktenzeichen_filter(
 p_src IN            VARCHAR2, 
 p_dst IN OUT NOCOPY VARCHAR2
) is begin
  CTX_DOC.POLICY_FILTER( ... );
  p_dst := escape_aktenzeichen(p_src);
end aktenzeichen_filter;
/
sho err

Donnerstag, 18. Oktober 2012

Treffer zählen:COUNT_HITS und Mixed Queries zusammen!

Vor nun schon fast vier Jahren hatten wir bereits ein Blog-Posting zum Thema "Treffer zählen mit Oracle TEXT" veröffentlicht. Zusammengefasst kann man sagen, dass man nach Möglichkeit immer mit CTX_QUERY.COUNT_HITS arbeiten sollte. Diese Prozedur stellt sicher, dass die Zählung ausschließlich im Textindex stattfindet und dass (teure) Zugriff auf die Basistabelle unterbleiben.
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
Doch was ist, wenn der Oracle Textindex mit dem in Oracle11g neuen Feature Composite Domain Index erstellt wird ...?
CREATE INDEX comp_ind ON customers(cust_first_name)
INDEXTYPE IS ctxsys.context
FILTER BY cust_id, cust_year_of_birth
ORDER BY cust_year_of_birth
Der Composite Domain Index nimmt die in der FILTER BY-Klausel angegebenen Spalten mit in den Volltextindex auf und führt mixed Queries wie die folgende dann allein mit Hilfe des Oracle TEXT Index aus.
SELECT cust_id FROM customers
WHERE contains (cust_first_name, 'A% or D% or N% or B%') > 0 AND cust_year_of_birth > 1970 
/
Wenn nun die Treffer gezählt werden sollen, tut man sich bei der Nutzung von CTX_QUERY.COUNT_HITS etwas schwer ... denn wo soll man das Filterkriterium cust_year_of_birth > 1970 einsetzen ...?
FUNCTION COUNT_HITS RETURNS NUMBER
 Argument Name                  Typ                     In/Out Defaultwert?
 ------------------------------ ----------------------- ------ --------
 INDEX_NAME                     VARCHAR2                IN
 TEXT_QUERY                     VARCHAR2                IN
 EXACT                          BOOLEAN                 IN     DEFAULT
 PART_NAME                      VARCHAR2                IN     DEFAULT
Die Lösung ist einfacher, als man denkt: Denn für jede der in der FILTER BY-Klausel angegebenen Spalten bildet Oracle TEXT eine SDATA-Section gleichen Namens. In diesem Fall haben wir also die SDATA Sections CUST_YEAR_OF_BIRTH und CUST_ID. Und diese lässt sich nun auch als Teil der CONTAINS-Abfrage explizit ansprechen. Die CONTAINS-Abfrage mit SDATA-Section sieht dann so aus ...
'A% or D% or N% or B% and SDATA(cust_year_of_birth > 1970) 
Eingesetzt in CTX_QUERY.COUNT_HITS ...
set serveroutput on

declare
  v_number number;
begin
  v_number := ctx_query.count_hits(
    index_name => 'COMP_IND',
    text_query => 'A% or D% or N% or B% and SDATA(cust_year_of_birth > 1970)',
    exact =>      true
  );
  dbms_output.put_line('Anzahl Treffer: '||v_number);
end;
/
 
Anzahl Treffer: 1623
Mehr Information in der Oracle Dokumentation - TEXT Reference.

Montag, 10. September 2012

Einige Gedanken zu Oracle TEXT und Tabellen-Partitionierung

Dieses Blog Posting widmet sich dem Thema Partitionierung und Oracle TEXT. Die Grundzüge der Partitionierung werden hier jedoch nicht mehr erläutert; es wird davon ausgegangen, dass der Leser weiss, wie Partitionierung funktioniert. Informationen zum Thema finden sich in der der Dokumentation im "VLDB and Partitioning Guide" und im Data Sheet "Partitioning".
Es wird also darüber nachgedacht, eine Tabelle mit Dokumenten, auf die ein Oracle TEXT Index angelegt werden soll, zu partitionieren. Partitionierung kann folgende Vorteile bieten:
  • Wenn der Partitionierungsschlüssel gleichzeitig ein Abfragekriterium ist, kann der Optimizer die jeweilige Abfrage auf die relevanten Partitionen beschränken (Partition Pruning). Damit Oracle TEXT davon profitiert, braucht es einen lokal partitionierten Textindex.
  • Partitionierung erlaubt administrative Arbeiten an ganzen Partitionen - so können Partitionen als Ganzes gelöscht werden. Als eigene Tabelle vorhandene Daten können per Partition Exchange als neue Partition an die Tabelle gehängt werden. Auch hier sollte der Oracle TEXT Index lokal partitioniert sein; ein globaler Textindex müsste ansonsten komplett neu gebaut werden.
Ein Oracle TEXT Index sollte also fast immer lokal partitioniert sein, also die gleiche Partitionierung aufweisen, wie die zugrundeliegende Tabelle. Ist der Index nicht partitioniert, gehen nicht nur einige Vorteile verloren; es können sogar Mehraufwände entstehen; bspw. wenn eine Tabellenpartition (alte Daten) gelöscht wird. Ein lokal partitionierter Oracle TEXT Index wird wie folgt erstellt - zunächst erzeugen wir eine Beispieltabelle.

create table doktest_part (
  id          number(10),
  text        varchar2(200),
  datum       date
)
partition by range(datum) (
  partition p_alt values less than (to_date('2010-12-31','YYYY-MM-DD')),
  partition p_2011 values less than (to_date('2011-12-31','YYYY-MM-DD')),
  partition p_2012 values less than (to_date('2012-12-31', 'YYYY-MM-DD'))
)
/

insert into doktest_part values (1, 'Dies ist ein Text aus 2011', DATE'2011-08-01');
insert into doktest_part values (2, 'Oracle TEXT indiziert Texte auch in 2012', DATE'2012-09-01');

commit
/
Danach kommt der Volltextindex:
create index ft_doktest on doktest_part (text)
indextype is ctxsys.context
local
/
Das Schlüsselwort local erzeugt einen zur Tabelle "lokal" partitionierten Textindex. Eine wichtige Einschränkung sei an dieser Stelle genannt. Alle Domain-Indizes, also auch Oracle TEXT, unterstützen nur RANGE-Partitioning. Die Tabelle muss also RANGE-Partitioniert sein, damit ein lokal partitionierter Textindex gebaut werden kann. Ist die Tabelle anders partitioniert, stößt das CREATE INDEX auf einen Fehler.
Wenn nun aber eher eine List oder Hash-Partitionierung gebraucht wird, muss man diese mit einer RANGE-Partitionierung emulieren. Wird beispielsweise ein HASH-Partitioning mit 4 Partitionen benötigt, so kann man die Partitionsnummer mit der Funktion ORA_HASH selbst generieren. Dann lässt sich die Tabelle mit einer virtuellen Spalte ausstatten - und danach kann man wieder eine normale RANGE-Partitionierung einsetzen. Diese wirkt nun aber wie eine Hash-Partitionierung; anhand des Primärschlüssels werden die Zeilen nun möglichst gleichmäßig über die vier Partitionen verteilt.
create table doktest_hashpart (
  id          number(10),
  text        varchar2(200),
  part# as (ora_hash(id, 4, 81978923))
)
partition by range(part#) (
  partition p_h1 values less than (2),
  partition p_h2 values less than (3),
  partition p_h3 values less than (4),
  partition p_h4 values less than (5)
)
/
Auch LIST-Partitioning lässt sich mit RANGE-Partitioning emulieren. Geht es um einfache Buchstabenkürzel, so kann man diese alphabetisch sortieren und direkt mit LESS THAN arbeiten; Oracle ordnet die Zeilen dann anhand binärer Sortierung zu. Manchmal ist die Praxis aber nicht so einfach. Angenommen, es soll nach einem Abteilungskürzel wie folgt partitioniert werden:
  • Partition 1: Abteilungen A, B, C
  • Partition 2: Abteilungen A1, A2, A3
  • Partition 3: Abteilungen D-F
Also - "A" ist eine andere Abteilung als "A1" - und die werden in unterschiedliche Partitionen einsortiert. Mit einem einfachen LESS THAN kann das nicht mehr ausgedrückt werden; da die Partition 1 als LESS THAN ('B') definiert werden müsste, würden A1 bis A3 ebenfalls dort einsortiert. Allerdings kann uns ein SQL CASE Konstrukt weiterhelfen ...
  case
    when abteilung in ('A', 'B', 'C') then 1
    when abteilung in ('A1', 'A2', 'A3') then 2
    when abteilung in ('D', 'E', 'F') then 3
  end
Damit können wir die Tabelle wieder als RANGE-Partionierte Tabelle erzeugen, wobei wir aber de-facto eine LIST-Partionierte Tabelle haben.
create table doktest_listpart (
  id          number(10),
  text        varchar2(200),
  abteilung   varchar2(3),
  part# as (case 
    when abteilung in ('A', 'B', 'C') then 1
    when abteilung in ('A1', 'A2', 'A3') then 2
    when abteilung in ('D', 'E', 'F') then 3
    end
  )
)
partition by range(part#) (
  partition p_abc    values less than (2),
  partition p_a1a2a3 values less than (3),
  partition p_def    values less than (4)
)
/
An dieser Stelle aber eine Warnung zu den virtuellen Spalten: Sobald eine Tabelle anhand einer virtuelle Spalte partitioniert wurde, kann diese virtuelle Spalte nicht mehr geändert werden! Wenn also in diesem Beispiel neue Abteilungen eingeführt werden, wäre es nicht mehr möglich, den CASE-Ausdruck zu erweitern, um die neue Abteilung auf eine Partition abzubilden. Man sollte also sicher sein, dass man mit der virtuellen Spalte die ganze Fachlichkeit korrekt, umfassend und nachhaltig abbildet. Wenn Ihr euch da nicht sicher seid, ist eine "normale" Spalte mit einem Trigger (wie früher) vielleicht die sicherere Alternative ...
create table doktest_listpart_tr (
  id          number(10),
  text        varchar2(200),
  abteilung   varchar2(3),
  part#       number(4)
)
partition by range(part#) (
  partition p_abc    values less than (2),
  partition p_a1a2a3 values less than (3),
  partition p_def    values less than (4)
)
/

create or replace trigger tr_setpart#
before insert or update on doktest_listpart_tr
for each row
begin
  :new.part# := ( 
    case 
      when :new.abteilung in ('A', 'B', 'C') then 1
      when :new.abteilung in ('A1', 'A2', 'A3') then 2
      when :new.abteilung in ('D', 'E', 'F') then 3
    end
  );
end;
/
... denn den Trigger kann man auch nachträglich noch beliebig ändern.
Nun kann auf jede der Tabelle mit obigem CREATE INDEX-Kommando und dem Schlüsselwort LOCAL ein lokal partitinierter Textindex erzeugt werden. For Composite Partitioning kann das Verfahren ähnlich funktionieren; allerdings dürfte dieses Partitionsverfahren für Oracle TEXT Anwendungen meist nicht das richtige sein. In Oracle10g gab es mit 9999 noch ein eigenes Limit für die Anzahl der Partitionen. Ab Oracle11g unterstützt Oracle TEXT ebensoviele Partitionen wie die Datenbank selbst, nämlich 1048575 Die Tabellenstruktur sieht nach Erstellung des Index wie folgt aus.
TNAME                          TABTYPE  CLUSTERI
------------------------------ ------- ---------
DR#FT_PART10K0001$I            TABLE
DR#FT_PART10K0001$K            TABLE
DR#FT_PART10K0001$N            TABLE
DR#FT_PART10K0001$R            TABLE
DR#FT_PART10K0002$I            TABLE
DR#FT_PART10K0002$K            TABLE
DR#FT_PART10K0002$N            TABLE
DR#FT_PART10K0002$R            TABLE
:
  • DR#
  • Name des Volltextindex
  • Laufende Nummer der Partition. Ab Partition 10000 arbeitet Oracle TEXT mit Buchstaben: 10000 ist also AAAA, 10001 ist AAAB und so fort.
  • Kürzel für den Tabellentyp ($I, $N, $K, $R, $P, $S)
Ist der Partitionsschlüssel nun Teil der Abfrage, so findet (wie immer bei Partitioning) ein Partition Pruning statt; der Optimizer beschränkt die Abfrage also auf die relevante Partition. Im Ausführungsplan sieht das dann wie folgt aus.
---------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name                | Rows  | Bytes | Cost (%CPU)| Time     | Pstart | Pstop |
---------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |                     |     1 |    14 |     4   (0)| 00:00:01 |        |       |
|   1 |  PARTITION RANGE SINGLE            |                     |     1 |    14 |     4   (0)| 00:00:01 |      2 |     2 |
|*  2 |   TABLE ACCESS BY LOCAL INDEX ROWID| DOKTEST_LISTPART    |     1 |    14 |     4   (0)| 00:00:01 |      2 |     2 |
|*  3 |    DOMAIN INDEX                    | FT_DOKTEST_LISTPART |       |       |     4   (0)| 00:00:01 |        |       |
---------------------------------------------------------------------------------------------------------------------------
Man erkennt, dass die Abfrage auf die Partition 2 beschränkt wurde. Wurde allerdings, wie oben beschrieben, eine Hash- oder List-Partitionierung "emuliert", so muss man aufpassen. Ein Beispiel anhand der oben vorgestellten Emulation eines List-Partitioning:
create table doktest_listpart (
  id          number(10),
  text        varchar2(200),
  abteilung   varchar2(3),
  part# as (case 
    when abteilung in ('A', 'B', 'C') then 1
    when abteilung in ('A1', 'A2', 'A3') then 2
    when abteilung in ('D', 'E', 'F') then 3
    end
  )
)
partition by range(part#) (
  partition p_abc    values less than (2),
  partition p_a1a2a3 values less than (3),
  partition p_def    values less than (4)
)
/
Ein paar Zeilen einfügen ...
SQL> insert into doktest_listpart values (1, 'Die Oracle-Datenbank enthält Oracle TEXT','A1', DEFAULT);

1 Zeile wurde erstellt.

SQL> insert into doktest_listpart values (2, 'Mit Oracle Spatial werden Geodaten verwaltet.','D', DEFAULT);

1 Zeile wurde erstellt.
Nun wird eine Abfrage ausgeführt - der "Partitionsschlüssel" liegt in Form der Abteilung vor ...
SQL> select * from doktest_listpart where contains(text, 'Spatial') > 0 and abteilung='D';

---------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name                | Rows  | Bytes | Cost (%CPU)| Time     | Pstart | Pstop |
---------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |                     |     1 |    14 |     4   (0)| 00:00:01 |        |       |
|   1 |  PARTITION RANGE ALL               |                     |     1 |    14 |     4   (0)| 00:00:01 |      1 |     3 |
|*  2 |   TABLE ACCESS BY LOCAL INDEX ROWID| DOKTEST_LISTPART    |     1 |    14 |     4   (0)| 00:00:01 |      1 |     3 |
|*  3 |    DOMAIN INDEX                    | FT_DOKTEST_LISTPART |       |       |     4   (0)| 00:00:01 |        |       |
---------------------------------------------------------------------------------------------------------------------------
Am PARTITION RANGE ALL erkennt man, dass der Optimizer kein Partition Pruning durchgeführt, sondern die Abfrage über alle Partitionen ausgeführt hat. Das ist -strenggenommen- auch logisch, denn aus Sicht von Oracle TEXT ist die Tabelle gar nicht anhand der Spalte ABTEILUNG partitioniert - sondern anhand der Spalte PART#. Das muss sich auch in der Abfrage widerspiegeln. Zunächst macht es an dieser Stelle absolut Sinn, das "Mapping" der Abteilung zu einer Partitionsnummer in einer PL/SQL-Funktion zu kapseln.
create or replace function get_partid_for_range (
  p_abteilung in varchar2
) return number deterministic is
  l_partnum number;
begin
  l_partnum := case 
    when p_abteilung in ('A', 'B', 'C') then 1
    when p_abteilung in ('A1', 'A2', 'A3') then 2
    when p_abteilung in ('D', 'E', 'F') then 3
    end;
  return l_partnum;
end;
/
Mit Hilfe dieser Funktion lässt sich die Abfrage recht einfach formulieren ...
select  * from doktest_listpart
where contains(text, 'Spatial') > 0 
and part# = get_partid_for_range('D')
/
Der Ausführungsplan sieht dann so aus ...

--------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name                | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
--------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |                     |     1 |    42 |     0   (0)| 00:00:01 |       |       |
|   1 |  PARTITION RANGE SINGLE            |                     |     1 |    42 |     0   (0)| 00:00:01 |   KEY |   KEY |
|*  2 |   TABLE ACCESS BY LOCAL INDEX ROWID| DOKTEST_LISTPART    |     1 |    42 |     0   (0)| 00:00:01 |   KEY |   KEY |
|*  3 |    DOMAIN INDEX                    | FT_DOKTEST_LISTPART |       |       |     0   (0)| 00:00:01 |       |       | 
--------------------------------------------------------------------------------------------------------------------------
KEY in den Spalten PSTART und PSTOP meint lediglich, dass der Wert zur Compile-Zeit des SQL noch nicht bekannt ist und erst zur Ausführungszeit Run-Time ermittelt wird. Die Abfrage wird aber, wie man erkennen kann, auf eine Partition begrenzt. Für eine "emulierte" Hash-Partition arbeitet man genauso.
Die Tatsache, ob ein Textindex partitioniert ist oder nicht, hat großen Einfluß auf die Wartung desselben. Die Aufrufe zur Synchronisierung oder zur Optimierung erwarten einen Parameter part_name. Für nicht-partitionierte Indizes kann dieser SQL NULL sein. Bei einem lokal partitinierten Index wird dagagen der Name einer Partition erwartet. Das bedeutet, dass das komplette Synchronisieren einex lokal partitionierten Oracle TEXT Index nicht mehr mit einem einzigen Aufruf von CTX_DDL.SYNC_INDEX erledigt wird, sondern es wird etwas mehr gebraucht ...
SQL> exec ctx_ddl.sync_index('FT_DOKTEST_LISTPART');
BEGIN ctx_ddl.sync_index('FT_DOKTEST_LISTPART'); END;

*
FEHLER in Zeile 1:
ORA-20000: Oracle Text-Fehler:
DRG-13102: Name von Index-Partition muss angegeben werden
ORA-06512: in "CTXSYS.DRUE", Zeile 160
ORA-06512: in "CTXSYS.CTX_DDL", Zeile 848
ORA-06512: in Zeile 1
Der Name der Indexpartition entspricht normalerweise dem der Tabellenpartition. Im Zweifelsfalle kann man sie aber aus dem Oracle TEXT Data Dictionary auslesen:
SQL> select IXP_INDEX_PARTITION_NAME from CTX_USER_INDEX_PARTITIONS where ixp_index_name='FT_DOKTEST_LISTPART';

IXP_INDEX_PARTITION_NAME
------------------------------
P_ABC
P_A1A2A3
P_DEF
Braucht man nun doch ein Kommando, welches den ganzen Index synchronisiert, kann man sich also mit dieser Abfrage helfen ...
create or replace procedure sync_full_index(p_idx_name in varchar2) as 
begin
  for ip in (
    select IXP_INDEX_PARTITION_NAME from CTX_USER_INDEX_PARTITIONS 
    where ixp_index_name =  p_idx_name
  ) loop
    ctx_ddl.sync_index(
      idx_name      => p_idx_name,  
      part_name     => ip.IXP_INDEX_PARTITION_NAME
  );
  end loop;
end;
/
Wobei die Möglichkeit, eine Synchronisierung bzw. eine Optimierung nur partitionsweise durchführen zu können, in vielen Fällen gerade ein Vorteil sein dürfte. Denn es kann ja sein, dass unterschiedliche Partitionen unterschiedliche Anforderungen haben. Und mit der Partitionierung kann man den Aufwand auf das nötige Maß begrenzen. Nicht nur die Synchronisierung wird partitionsweise durchgeführt, auch einige andere Operationen arbeiten so ...
  • Index-Synchronisierung (CTX_DDL.SYNC_INDEX)
  • Asynchroner Index-Aufbau (CTX_DDL.POPULATE_PENDING)
  • Indexoptimierung (CTX_DDL.OPTIMIZE_INDEX)
  • Indexoptimierung (CTX_DDL.OPTIMIZE_INDEX)
  • Indexstatistiken werden ebenfalls partitionsweise ermittelt (CTX_REPORT.INDEX_STATS, CTX_REPORT.INDEX_SIZE und CTX_REPORT.TOKEN_INFO)
  • Online Index Rebuild (CTX_DDL.CREATE_SHADOW_INDEX und CTX_DDL.REBUILD_INDEX_ONLINE) arbeiten ebenfalls partitionsweise
Eine Operation wie ALTER TABLE DROP PARTITION funktioniert mit Oracle TEXT transparent; die relevante Partition des Oracle TEXT Index wird mitsamt der Tabellenpartition gelöscht. Gerade bei Oracle TEXT ergibt sich jedoch ein entscheidender Vorteil: Durch ein "klassisches" SQL DELETE würden die Einträge in die Negativliste wandern ($N-Tabelle) und es würde eine Indexoptimierung nötig. Beim partitinierten Textindex wird die Tabellenpartition einfach mitsamt der Indexpartition in einem Zug gelöscht. Es ist weder ein Index-Sync noch ein Index Optimize nötig. Ein partitionierter Oracle TEXT Index kann bei großen Datenmengen also eine Menge Vorteile bieten - und das betrifft nicht nur die Abfrageperformance, sondern auch, und besonders die Wartungs- und "Housekeeping"-Arbeiten an Tabelle und Index.
  • Partition Pruning bei Volltextabfragen - Abfragen werden auf die relevante Indexpartition begrenzt - was zu besserer Performance führt. Davon profitieren übrigens nicht die die Abfragen, sondern auch etwaige Sortierungen.
  • Mit einem ALTER TABLE DROP PARTITION können veraltete Daten recht einfach und ohne viel Aufwand gelöscht werden.
  • Neue Datenbestände können in eine eigene Tabelle geladen, indiziert und dann per ALTER TABLE EXCHANGE PARTITION in die Zieltabelle eingehängt werden. Auf gleichem Wege kann so ein Online-Rebuild einer Indexpartition erfolgen. Die Daten werden in eine eigene Tabelle kopiert; darauf wird der Index neu erstellt und ein ALTER TABLE EXCHANGE PARTITION nimmt Tabelle und Index auf einmal auf.
  • Auch Oracle-TEXT spezifische Operationen können partitionsweise durchgeführt und damit besser gesteuert werden.

Donnerstag, 19. Juli 2012

Filter Preferences - Grundsätzliches

Wir haben in unseren Textblogs schon mehrfach über einige Texteinstellungen - die sogenannten Preferences - gesprochen.  Nun ist es an der Zeit, Grundlagen zu den Filter Preferencen zu legen. Filter Preferences sind, wie der Name schon verrät, dazu da den Text zum Indizieren zu filtern. Formatierte Dokumente werden zwar in nativer Format gespeichert; der Filter sorgt allerdings dafür, dass eine vorläufige Version in reinem (plain) Text oder HTML Version des Dokuments zur Verfügung steht, um dann die Wörter zum Indizieren zu erhalten.

Filter-Preferences werden mit dem CREATE INDEX oder ALTER INDEX Statement erzeugt. Man sollte sich dabei nicht auf System Preferencen verlassen, sondern den Filter immer beim Anlegen des Index mitangeben. Folgende Filter Typen existieren:

  • CHARSET_FILTER: Character Set Konvertierung
  • AUTO_FILTER: Für formatierte Dokumente
  • NULL_FILTER: Kein Filter ist erforderlich, nützlich für Plain Text, HTML und XML
  • MAIL_FILTER: Transformiert RFC-822 und RFC-2045 Nachrichten in indizierbaren Text
  • USER_FILTER: Externer Filter für benutzerdefinertes Filtern
  • PROCEDURE_FILTER: Benutzerdefiniertes Filtern definiert über eine Prozedur

Hier im Blog gebe ich Beispiele für NULL_FILTER, AUTO_FILTER und den USER_FILTER. Weitere Beispiele dazu finden sich im Handbuch.
Zuerst wird eine Tabelle erzeugt, die Dokumente im Format Powerpoint, PDF, HTML und ASCII enthält.

drop table filter_test; 
create table filter_test(id number primary key, docs blob);

Zum Laden verwende ich den SQL *Loader. Die CTL Datei sieht folgendermassen aus:
LOAD DATA
INFILE 'filter_load.dat'
INTO TABLE null_filter
FIELDS TERMINATED BY ','
(id SEQUENCE (MAX,1) ,
ext_fname FILLER CHAR(50),
docs LOBFILE(ext_fname) TERMINATED BY EOF)

Die filter_load.dat Datei hat folgende Inhalte.
snaps.sql
powerp.pptx
plain.txt
replay.html
one_page.pdf

Im ersten Beispiel wird ein Index mit Filter Preference NULL_FILTER erzeugt. Um zu überprüfen, ob im Index Create Prozeß keine Fehler passiert sind, schalte ich Logging ein und überprüfe den Index nach dem Anlegen mit CTX_USER_INDEX_ERRORS.
drop index filter_test_idx force;
execute CTXSYS.CTX_ADM.SET_PARAMETER ('LOG_DIRECTORY','/tmp'); 
exec CTX_OUTPUT.START_QUERY_LOG('filterlog');

create index filter_test_idx on filter_test ( docs )
indextype is ctxsys.context
parameters ('FILTER ctxsys.NULL_FILTER');

execute CTX_OUTPUT.END_QUERY_LOG;
SQL> select * from ctx_user_index_errors;
no rows selected

Nun wird das Ergebnis überprüft. Um einen Einruck von dem Inhalt der Ergebnismenge zu bekommen, verwende ich CTX_DOC.SNIPPED. Die REGEXP_REPLACE Funktion macht das Ergebnis lesbarer und eliminiert die überflüssigen Leerzeilen.

SQL> execute ctx_doc.set_key_type('ROWID');
SQL> select id, regexp_replace(ctx_doc.snippet 
('FILTER_TEST_IDX', rowid, 'plain or OLTP or replay or snapshot or SPA or
 REPLAY',starttag=>'###',endtag =>'###'),'\s+',' ') snippet
 from filter_test
 where contains ( docs, 'plain or OLTP or replay or snapshot or SPA or REPLAY') > 0; 
ID
----------
SNIPPET
--------------------------------------------------------------------------------
1
hh24:mi') from dba_hist_###snapshot### order by BEGIN_INTERVAL_TIME
3
dies ist ein ###plain### text
4
performance of a workload ###replay### against the performance of...The f
irst ###replay### would try to mimic the captured

Wie zu erwarten, enthält das Ergebnis die Dokumente snaps.sql (mit 1), plain.txt (mit 2) und replay.html (mit 3).
Nun verwenden wir den AUTO_FILTER. Das Vorgehen ist ähnlich wie oben, nur die FILTER Preference lautet nun AUTO_FILTER.
drop index filter_test_idx force; 
execute CTXSYS.CTX_ADM.SET_PARAMETER ('LOG_DIRECTORY','/tmp'); 
execute CTX_OUTPUT.START_QUERY_LOG('filterlog');

create index filter_test_idx on filter_test (docs)
indextype is ctxsys.context
parameters ('FILTER ctxsys.AUTO_FILTER');

execute CTX_OUTPUT.END_QUERY_LOG;
SQL> select * from ctx_user_index_errors;
no rows selected
Nun wird wieder das Ergebnis selektiert.
SQL> execute ctx_doc.set_key_type('ROWID');

SQL>select id, regexp_replace(ctx_doc.snippet
('FILTER_TEST_IDX', rowid, 'plain or OLTP or replay or snapshot or SPA or
 REPLAY',starttag=>'###',endtag =>'###'),'\s+',' ') snippet
 from filter_test
 where contains ( docs, 'plain or OLTP or replay or snapshot or SPA or REPLAY') > 0; 
ID
----------
SNIPPET
--------------------------------------------------------------------------------
1
hh24:mi') from dba_hist_###snapshot### order by BEGIN_INTERVAL_TIME
2
Sesssions und Calls ###OLTP###-Datenbanken mit ca.12.000...Siehe Enhancem
ent Request ?7523016: ###REPLAY### REPORTS ORA-933 ON SET ROLE
3
dies ist ein ###plain### text
4
performance of a workload ###replay### against the performance of...The f
irst ###replay### would try to mimic the captured
5
2) Before using the ###SPA### or Database Replay, understand...3) Perform
STS Capture for ###SPA### & workload capture for Database

Es werden nun alle Dokumente gefiltert, indiziert und gefunden. Möchte man einen Überblick über alle unterstützten Formate erhalten, kann man dies im Handbuch nachschlagen.

Nun kommen wir zum letzten Beispiel - einem USER_FILTER. Hierzu ist ein Programm notwendig, das beim Filtern für jede Zeile ausgeführt wird. Explizit wird nun eine Preference MY_FILTER mit einem speziellen Attribut erzeugt, das den Programmnamen festlegt - hier upper.pl
execute ctx_ddl.drop_preference('my_filter');
BEGIN
ctx_ddl.create_preference ('my_filter', 'user_filter');
ctx_ddl.set_attribute ('my_filter', 'command', 'upper.pl');
END;
/
PL/SQL procedure successfully completed.
Das Programm ist ein Perl-Programm, das den Text in Großbuchstaben konvertiert. Es muß unbedingt im Verzeichnis $ORACLE_HOME/ctx/bin zu finden sein.

#!/usr/bin/perl
open(IN, $ARGV[0]);
open(OUT, ">".$ARGV[1]);
while ()
{
tr/a-z/A-Z/;
print OUT;
}
close (IN);
close (OUT);

Nun wird der Index erzeugt.

drop index filter_test_idx force;

create index filter_test_idx on filter_test (docs) 
indextype is ctxsys.context
parameters ('filter my_filter');

SQL> select * from ctx_user_index_errors;
no rows selected

Das Ergebnis sieht dann folgendermassen aus.

SQL> select id, regexp_replace(ctx_doc.snippet
('FILTER_TEST_IDX', rowid, 'plain or OLTP or replay or snapshot or SPA or
REPLAY',starttag=>'###',endtag =>'###'),'\s+',' ') snippet
from filter_test
where contains ( docs, 'plain or OLTP or replay or snapshot or SPA or REPLAY') > 0;
ID
----------
SNIPPET
--------------------------------------------------------------------------------
4
CAPTURE VS. ###REPLAY###
...PERFORMANCE OF A WORKLOAD ###REPLAY### AGAINST THE PERFORMANCE
OF...CAPTURED SYSTEM, WHILE "###REPLAY###" REFERS TO THE
REPLAYED WORKLOAD.

Mittwoch, 6. Juni 2012

Name Matching (NDATA) mit einem Thesaurus "erweitern"

Zum Thema Oracle TEXT Name Matching hatten wir ja bereits einige Blog Postings. Heute möchte ich das Thema nochmal aufgreifen und besonders die Erweiterbarkeit mit einem Thesaurus vorstellen. Zunächst zur Ausgangssituation: Wir legen eine Tabelle an und füllen diese mit ein paar Namen:
create table tab_namen (
  id           number(10),
  vorname      varchar2(100),
  nachname     varchar2(100)
)
/

insert into tab_namen values (1, 'Carsten','Czarski');
insert into tab_namen values (2, 'Ulrike', 'Schwinn');
insert into tab_namen values (3, 'Max',    'Meier');
insert into tab_namen values (4, 'Moritz', 'Meyer');
insert into tab_namen values (5, 'Franz',  'Mayer');
insert into tab_namen values (6, 'Fritz',  'Maier');
Damit man einen Oracle TEXT Index anlegen kann, der auch alle Namen enthält, muss man nun mit einem Multicolumn-Datastore oder einem User-Datastore arbeiten. Da ich die Namen aber gerne per NDATA (Name Matching) und per "klassischer" Fuzzy-Suche (FUZZY-Operator) finden möchte, brauche ich einen User-Datastore (Blog-Posting). Also zuerst die PL/SQL-Prozedur anlegen ...
 create or replace procedure namen_uds_proc(
  rid  in            rowid,
  tlob in out nocopy varchar2
) is
  l_vorname  tab_namen.vorname%type;
  l_nachname tab_namen.nachname%type;
begin
  select vorname, nachname into l_vorname, l_nachname
  from tab_namen where rowid = rid;
  tlob := 
    '<VORNAME>'   || l_vorname                       || '</VORNAME>'  ||  
    '<NACHNAME>'  || l_nachname                      || '</NACHNAME>' ||  
    '<NAME>'      || l_vorname || ' ' || l_nachname  || '</NAME>';
end;
/
sho err
Die Prozedur als User Datastore registrieren ...

begin
  ctx_ddl.create_preference('names_ds', 'user_datastore');
  ctx_ddl.set_attribute('names_ds', 'procedure', 'namen_uds_proc');
end;
/
sho err
Mit dem Section Group-Objekt wird festgelegt, wie die einzelnen Sections (XML-Tags) von Oracle TEXT behandelt werden sollen ... VORNAME und NACHNAME werden normal behandelt; der zusammengesetzte "NAME" wird als NDATA-Section deklariert.
begin
  ctx_ddl.create_section_group('names_sg', 'xml_section_group');
  ctx_ddl.add_field_section('names_sg', 'VORNAME', 'VORNAME', false);
  ctx_ddl.add_field_section('names_sg', 'NACHNAME', 'NACHNAME', false);
  ctx_ddl.add_ndata_section('names_sg', 'NAME', 'NAME');
end;
/
Schließlich den Index anlegen ...
create index ft_names on tab_namen (nachname)
indextype is ctxsys.context
parameters('
  datastore     names_ds
  section group names_sg
  memory        100m
')
/
Und dann kann man abfragen - zuerst mit "Meier" ...
select * from tab_namen where contains(nachname, 'ndata(name, Meier)')>0
/

        ID VORNAME         NACHNAME
---------- --------------- ---------------
         3 Max             Meier
         4 Moritz          Meyer
         6 Fritz           Maier
Ups ... da fehlt doch einer ... der "Mayer" ist für den NDATA-Algorithmus wohl "zu weit" weg ... Probieren wir noch ein wenig mit dem "Czarski".
select * from tab_namen where contains(nachname, 'ndata(name, Czarsky)')>0
/

        ID VORNAME         NACHNAME
---------- --------------- -------------
         1 Carsten         Czarski

select * from tab_namen where contains(nachname, 'ndata(name, Tsarski)')>0
/

        ID VORNAME         NACHNAME
---------- --------------- -------------
         1 Carsten         Czarski

select * from tab_namen where contains(nachname, 'ndata(name, Tsarsky)')>0
/

Es wurden keine Zeilen ausgewählt
Funktioniert ganz gut, lässt aber doch noch Wünsche offen. Eine Ähnlichkeitssuche wird aber immer Wünsche offenlassen, gerade wenn sie nicht für eine bestimmte Sprache optimiert ist und global funktionieren soll. Die gute Nachricht ist aber, dass NDATA durch einen Thesaurus erweitert werden kann. In diesem Thesaurus können unterschiedliche Schreibweisen von Namen als Synonymbeziehungen hinterlegt und die Treffermenge von NDATA so erweitert werden. Und das Gute ist, dass NDATA dann auch zu den Synonymen ähnliche (!) Namen finden wird. Die Treffermenge wird also nicht nur um die Synonyme selbst, sondern auch um den Synonymen (nach NDATA) ähnliche Namen erweitert.
Wie man einen Thesaurus anlegt und pflegt, ist in diesem Blog-Posting beschrieben. Also erzeugen wir einen Thesaurus wie folgt ...
begin
  ctx_thes.create_thesaurus('namesthes');
  ctx_thes.create_relation('namesthes', 'meyer', 'SYN', 'maier');
  ctx_thes.create_relation('namesthes', 'meyer', 'SYN', 'meier');
  ctx_thes.create_relation('namesthes', 'meyer', 'SYN', 'mayer');
  ctx_thes.create_relation('namesthes', 'czarski', 'SYN', 'czarsky');
end;
/
Als nächstes müssen wir Oracle TEXT sagen, dass es den Thesaurus für NDATA nutzen soll - das geschieht mit einer Wordlist Preference.
begin
  ctx_ddl.create_preference('names_wl', 'BASIC_WORDLIST');
  ctx_ddl.set_attribute('names_wl', 'NDATA_ALTERNATE_SPELLING', 'FALSE');
  ctx_ddl.set_attribute('names_wl', 'NDATA_BASE_LETTER',        'TRUE');
  ctx_ddl.set_attribute('names_wl', 'NDATA_THESAURUS',          'namesthes');
end;
/
Und dann muss der Index gelöscht und neu angelegt werden ...
drop index ft_names
/

create index ft_names on tab_namen (nachname)
indextype is ctxsys.context
parameters('
  datastore     names_ds
  section group names_sg
  wordlist      names_wl
  memory        100m
')
/
Und dann probieren wir die Abfragen erneut ...
select * from tab_namen where contains(nachname, 'ndata(name, Mayer)')>0
/

        ID VORNAME         NACHNAME
---------- --------------- ---------------
         3 Max             Meier
         4 Moritz          Meyer
         5 Franz           Mayer
         6 Fritz           Maier

select * from tab_namen where contains(nachname, 'ndata(name, Tsarsky)')>0
/

        ID VORNAME         NACHNAME
---------- --------------- ---------------
         1 Carsten         Czarski
Voilá - man sieht, dass man die NDATA-Funktionalität sehr gut erweitern kann. Wenn man das von vorneherein in seinen Index einbaut, so kann man den Index aufgrund von Nutzerfeedback "lernen" lassen. Mit einem Namens-Thesaurus lässt sich die NDATA-Funktinalität auf jeden Fall sehr gut abrunden.

Beliebte Postings