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

Beliebte Postings