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