Benutzer-Werkzeuge

Webseiten-Werkzeuge


dba:oracle_text_ctx_entity_ner_extraction

Oracle 26ai – Named Entity Extraction (NER) mit CTX_ENTITY zur Analyse von Tarifvertragsanfragen mit einem Hybrid Vector Ansatz

Aufgabe: Anfrage an die Datenbank semantisch verstehen – ohne Einsatz von externes KI‑Modellen

In einer Applikation zur Analyse von Tarifverträgen gibt es ein Freitextsuchfeld, in das Anwender ihre Frage eingeben.

Die Herausforderung besteht darin, aus dieser natürlichsprachlichen Anfrage zu erkennen, welcher Tarifvertrag und welche tarifliche Regelung für die Frage relevant ist um diese Daten in der Folge für die konkrete Antwort einer Frage an ein LLM zu senden.

Ein Fragestellung wie „Ich arbeite in Hamburg in einer Werft als Schweißer und frage mich wie viel Urlaubsgeld ich bekomme“. kann die LLM nur mit Hilfe des richtigen Tarifvertrags für diese Person lösen.

D.h. im ersten Schritt wird aus dem Anfrage Satz eine Liste von fachlichen Token generieret um den passenden Vertrag zur Anfrage zu finden bzw. den dazu notwendigen Dialog mit dem Anwender zu starten falls entscheidende Parameter fehlen.

Dazu kommen in der „modernen“ KI Welt ein KI BERT Modell zum Einsatz. Das Modell wandelt einen Satz in Tocken wandeln und ermittelten deren Bedeutung/Wert.

In einer Oracle 26ai haben wir zwar schon die Möglichkeit KI Modell zu landen und damit Vectoren zu erzeugen, ein interne Verarbeitung mit eigenen KI Ansätzen in PL/SQL ist aber nicht so einfach, hier wird meist dann per REST ein externer Dienst eingebunden.

Das genau soll hier aber vermieden werden, eine Suche soll zuvor logisch schon in der DB analysiert und verarbeitet werden können.

Dazu bietet sich das Oracle Text Feature „Named Entity Extraction“ (Package CTX_ENTITY) an, kombiniert mit einer Oracle Vector Search auf unscharfe Begriffe im Suchstring.

Für das Aufbereiten der Daten werden wiederum die Oracle Text Basis Funktionen wie CTX_DOC.TOKENS eingesetzt.


Lösung NER mit Oracle Text

Als Basis dienen die Stammdaten der Tarifverträge und weitere, zuvor mit einem LLM, extrahierte fachliche Details wie Fachliche Wörter in dem Verträgen, wie der Begriff „Weihnachtsgeld“ aus unsere Beispiel Anfragen.

Mit diesen Begriffen werden Regeln definiert, wird im Text ein Begriff gefunden, gibt Oracle Text die Position und den Typ des Begriffes zurück.

In unseren Beispiel „Ich arbeite in Hamburg in einer Werft als Schweißer und frage mich wie viel Urlaubsgeld ich bekomme“ erhalten wir also:

<entities>
	<entity id="0" offset="15" length="7" source="SuppliedDictionary">
		<text>Hamburg</text>
		<type>city</type>
	</entity>
	<entity id="1" offset="15" length="7" source="UserRule" ruleid="12">
		<text>Hamburg</text>
		<type>region</type>
	</entity>
	<entity id="2" offset="76" length="11" source="UserRule" ruleid="125">
		<text>Urlaubsgeld</text>
		<type>FACHTERM</type>
	</entity>
</entities>

Mit diesen Informationen können wir dann alle Tarifverträge in der Region „Hamburg“ als dem Stammdaten suchen und alle Verträge die wir zuvor per KI Analyse mit dem Fachbegriff „Urlaubsgeld“ verknüpft haben.


Hybrid-Ansatz: Falls Oracle Text NER kein oder nur wenig Ergebnis liefert

Hybrid-Ansatz mit einem „intfloat/multilingual-e5-base“-Model, siehe https://dataloop.ai/library/model/intfloat_multilingual-e5-base/.

Zunächst wird ein Oracle NER durchgeführt.

Für „nicht erkannte“ oder unklare Texte wird anschließend ein Embedding-Fallback mit dem Modell „intfloat/multilingual-e5-base“ direkt in der Datenbank gefahren.

Bei einer Frage wie „Ich arbeite in Kassel und baue Panzer, ich frage mich, wie viel Urlaubsgeld ich bekomme“ müssen wir nun erkennen, dass wir nach „Nordhessen“ und „Metallindustrie“ suchen müssen.

Das geben unsere Stammdaten aber nicht her, daher müssen wir aus „Panzer bauen“ ein „Metallindustrie“ erkennen.

Dazu verwenden wir:

  • CTX_DOC.TOKENS liefert eine Tokenliste
  • ein Stemmer im Index normalisiert Verbformen/Wortstämme
  • Stopwords werden entfernt, sodass nur „echte“ Begriffe übrig bleiben

Das Ergebnis ist der ideale Input für e5-base-Embeddings.

Eine Vektor-Suche ordnet zu Branche/Region/Tarifvertrag zu.

Allerdings benötigen wir dann eine Tabelle mit entsprechenden Texten, die ein Vektor-Embedding zu „Panzer bauen“ auch zu einer Industrie mit entsprechenden Eigenschaften mappt.

Da aber kein Firmenkatalog vorliegt und es praktisch nicht möglich ist, direkt alle Firmen in einer Datenbank zu hinterlegen, versuchen wir, über Branchen zu gehen und dabei Branchenterminologie zu verwenden.

Unsere harte Datenbankstruktur ist dann: Tarifvertrag zu Industrie zu Fachbegriff der Industrie, wobei Firmennamen wie Krupp als Fachbegriff auch einzuordnen sind.

Gegen diese Datenbasis vergleichen wir dann unseren String „Panzer bauen“, um auf Wehrtechnik und damit auf die Metallindustrie zu kommen.


Umsetzung

Ablauf:

  • ListenpunktDB Schema Owner bereichtigen (im Beispiel GPITARIF)
  • Ruleset erzeugen
  • Ruleset anwenden

DB Schema Owner berechtigen

Neben den normalen Oracle Text rechten wird noch benötigt CTXSYS.CTX_ENTITY aufrufen zu können.

Alle CTX Rechte:

--rolle 
-- CTXAPP → Erlaubt die Nutzung aller CTX_* APIs (CTX_DDL, CTX_QUERY, CTX_DOC, CTX_ENTITY).
GRANT CTXAPP TO GPITARIF;
 
-- DB Views
GRANT SELECT ON CTXSYS.CTX_ENTITY_TYPES TO GPITARIF;
GRANT SELECT ON CTXSYS.CTX_STOPLISTS TO GPITARIF;
GRANT SELECT ON CTXSYS.CTX_THES_TABLES TO GPITARIF;
GRANT SELECT ON CTXSYS.CTX_POLICY TO GPITARIF;
 
-- ENTITY → Named Entity Extraction 
GRANT EXECUTE ON CTXSYS.CTX_ENTITY TO GPITARIF;
 
--CTX_DDL → Erstellen & Verwalten von Preferences, Stoplists, Lexer, Policies
--CTX_QUERY → CONTAINS-Abfragen, Explain‑Plan, Optimierungen
--CTX_DOC → Highlighting, Snippets, Document Sections
 
GRANT EXECUTE ON CTXSYS.CTX_DDL TO GPITARIF;
GRANT EXECUTE ON CTXSYS.CTX_QUERY TO GPITARIF;
GRANT EXECUTE ON CTXSYS.CTX_DOC TO GPITARIF;
 
--CTX_REPORT → Ausführungsanalyse bei Bedarf 
GRANT EXECUTE ON CTXSYS.CTX_REPORT TO GPITARIF;

Eingabe in Tocken zerlegen

Policiy anlegen:

BEGIN
  CTX_DDL.CREATE_PREFERENCE('POL_DE_LEXER', 'BASIC_LEXER');
  CTX_DDL.SET_ATTRIBUTE    ('POL_DE_LEXER', 'INDEX_THEMES', 'NO');
 
  CTX_DDL.CREATE_STOPLIST('POL_DE_STOP');
  -- Optional: Stopwörter hinzufügen, z. B. 'und','aus','bin' ...
 
  CTX_DDL.CREATE_POLICY(
    policy_name => 'POL_EMBED_TEXT',
    lexer       => 'POL_DE_LEXER',
    stoplist    => 'POL_DE_STOP'
  );
END;
/

In Tocken zerlegen:

SET SERVEROUTPUT ON;
DECLARE
  v_input   CLOB := 'bin aus kassel und baue Panzer';
 
  -- In-Memory Token-Resultat
  the_tokens  CTXSYS.CTX_DOC.TOKEN_TAB;
 
  -- Ausgabe
  v_embedding_text  VARCHAR2(4000);
 
  -- app-seitige Mini-Stopliste (ergänzt die CTX-Stoplist)
  l_stop CONSTANT SYS.ODCIVARCHAR2LIST :=
    SYS.ODCIVARCHAR2LIST('bin','und','aus','ich','du','wir');
 
  -- Hilfsfunktion: prüft, ob Wort in unserer kleinen Stopliste ist
  FUNCTION is_stop(p_word VARCHAR2) RETURN BOOLEAN IS
  BEGIN
    FOR i IN 1 .. l_stop.COUNT LOOP
      IF p_word = l_stop(i) THEN
        RETURN TRUE;
      END IF;
    END LOOP;
    RETURN FALSE;
  END;
BEGIN
  --------------------------------------------------------------------
  -- Tokens direkt aus freiem Text extrahieren (OHNE Index):
  -- restab => TOKEN_TAB  (in-memory)
  -- language => 'GERMAN' (Hint an die Linguistik)
  -- thes_name / thes_toktype optional (z.B. 'MY_THES', 'SYN')
  --------------------------------------------------------------------
  CTX_DOC.POLICY_TOKENS(
    policy_name  => 'POL_EMBED_TEXT',
    document     => v_input,
    restab       => the_tokens,
    language     => 'GERMAN',
    format       => NULL,
    charset      => NULL,
    thes_name    => NULL,        -- z.B. 'MY_THES' wenn ein Thesaurus genutzt wird
    thes_toktype => 'SYN'        -- Standard: Synonyme (falls Thesaurus gesetzt)
  );
  -- Siehe Signatur und Beschreibung der POLICY_* Prozeduren. [1](https://oracle-base.com/articles/9i/full-text-indexing-using-oracle-text-9i)
 
  --------------------------------------------------------------------
  -- Tokens in Dokumentreihenfolge durchlaufen, Stopwörter filtern,
  -- lowercasing und zu einem robusten Embedding-String zusammenbauen.
  -- the_tokens(i).token / .offset / .length sind verfügbar. [2](https://docs.oracle.com/cd/B13789_01/text.101/b10730/cdocpkg.htm)
  --------------------------------------------------------------------
  FOR i IN 1 .. the_tokens.COUNT LOOP
    IF the_tokens(i).token IS NOT NULL THEN
     DBMS_OUTPUT.put_line('Tocken: '||the_tokens(i).token);
      DECLARE
        w VARCHAR2(200) := LOWER(the_tokens(i).token);
      BEGIN
        IF NOT is_stop(w) THEN
          v_embedding_text :=
            v_embedding_text || CASE WHEN v_embedding_text IS NULL THEN '' ELSE ' ' END || w;
        END IF;
      END;
    END IF;
  END LOOP;
 
  DBMS_OUTPUT.put_line('Embedding-Text: '||v_embedding_text);
  -- -> Erwartet: "kassel bau panzer"
  --
  -- Ab hier: vektor := ai_embedding.e5_base(v_embedding_text); (modellabhängig)
END;
/
 
Tocken: BIN
Tocken: AUS
Tocken: KASSEL
Tocken: UND
Tocken: BAUE
Tocken: PANZER
 
 
Embedding-Text: kassel baue panzer
 
 
PL/SQL PROCEDURE successfully completed.

Rule Set erstellen in 26ai erstellen

Die XSD einer Rule lautete:

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="rule">
  <xsd:sequence>
    <xsd:element name="expression" type="xsd:string"/>
    <xsd:complexType>
    </xsd:complexType>
    <xsd:element name="comments type="xsd:string" default="\0"/>
  </xsd:sequence>
  </xsd:attribute name="language" type="xsd:string" default="ALL"/>
</xsd:element>
</xsd:schema>
Wichtig ist das die Expression eine Art Regulärer Ausdruck ist, diese Regel wird dann intern übersetzt. Das was Matchen soll muss in () stehen! Ohne die () wird kein Fehler geworfen aber auch nichts gefunden.

Die Sprachunterstützung laut Doku ist auch nur English, mit ALL ist man auf der sicheren Seite und es funktioniert.

Beispiel für eine Regel:

<rule language="ALL">
	<expression>(Hamburg)</expression>
	<type>REGION</type>
	<comments>Hamburg</comments>
</rule>

Oder auch (aus der Doku):

 <rule language="ALL">
     <expression>(\(d{3}\) \d{3}-\d{3}-\d{4})</expression>
     <comments>Telefonnummer</comments>
     <type>KONTAKTDATEN</type>
  </rule>

Anlegen

Erste Test Rule anlegen mit:

DECLARE
   CURSOR c_create_policy IS
      SELECT 
         d.bezeichnung,         -- z.B. 'Solingen'
         d.entity_class         -- z.B. 'REGION' (eigener Typ)
      FROM (
         SELECT BEZ AS bezeichnung, 'REGION' AS entity_class FROM V_MDB_TARIFGEBIET
         UNION ALL
         SELECT BEGRIFF,        'FACHTERM' FROM TVERTRAG_BEGRIFFE
      ) d;
 
   v_rule_xml  CLOB;
   v_id        PLS_INTEGER := 1;
 
    FUNCTION escape_regex(p IN VARCHAR2) RETURN VARCHAR2 IS
    BEGIN
      -- escaped: . ^ $ | ? * + ( ) [ ] { } -
      RETURN REGEXP_REPLACE(p, '([\\.^$|?*+()\\[\\]{}\\-])', '\\\1'); -- geht noch nicht wie es soll! muss optimiert werden!
    END;
 
BEGIN
   -- 1) Bereinigen/Neu anlegen
   BEGIN
      ctx_entity.DROP_EXTRACT_POLICY('TARIF_ENTITIES');
   EXCEPTION WHEN OTHERS THEN NULL;
   END;
 
   ctx_entity.CREATE_EXTRACT_POLICY(
      policy_name => 'TARIF_ENTITIES'
      -- optional: include_supplied_rules/dictionary beibehalten
   );
 
   -- 2) Regeln hinzufügen (XML pro Regel, mit language="ALL")
   FOR rec IN c_create_policy LOOP
      v_rule_xml :=
            '<rule language="ALL" >'
         || '  <expression>(' || DBMS_XMLGEN.CONVERT(escape_regex(rec.bezeichnung), DBMS_XMLGEN.ENTITY_ENCODE) || ')</expression>'
         || '  <type>'       || DBMS_XMLGEN.CONVERT(rec.entity_class, DBMS_XMLGEN.ENTITY_ENCODE) || '</type>'
         || '  <comments>'       || DBMS_XMLGEN.CONVERT(rec.bezeichnung, DBMS_XMLGEN.ENTITY_ENCODE) || '</comments>'
         || '</rule>';
 
      ctx_entity.ADD_EXTRACT_RULE(
         policy_name     => 'TARIF_ENTITIES',
         rule_id         => v_id,     -- numerische ID (Pflicht)
         extraction_rule => v_rule_xml
      );
 
      v_id := v_id + 1;
   END LOOP;
 
   -- 3) Regeln wirksam machen
   ctx_entity.COMPILE('TARIF_ENTITIES');  -- ohne Compile keine Wirkung!
END;
/

Was wurde angelegt:

SELECT * FROM   CTX_USER_EXTRACT_RULES WHERE  ERL_POLICY_NAME = 'TARIF_ENTITIES' AND ERL_COMMENTS LIKE 'Url%';
 
-- Optionale Stop-Entities (falls verwendet)
SELECT *
FROM   CTX_USER_EXTRACT_STOP_ENTITIES
WHERE  ESE_POLICY_NAME = 'TARIF_ENTITIES';
 
-- Alle bekannten Typen (inkl. eigener Typen)
SELECT *
FROM   CTX_USER_EXTRACT_TYPE
ORDER  BY ERT_TYPE;

Ruleset testen

SET SERVEROUTPUT ON;
DECLARE
    v_results CLOB;    
BEGIN   
 
   DBMS_LOB.CREATETEMPORARY(v_results, TRUE);
 
    -- Wir nutzen nun die kompilierte Policy auf dem User-Input
    ctx_entity.EXTRACT(
        policy_name => 'TARIF_ENTITIES'
        ,document    => 'Ich arbeite in Hamburg in einer Werft als Schweißer und frage mich wie viel Urlaubsgeld ich bekomme'
        ,LANGUAGE    =>  NULL
        ,RESULT      => v_results
    );
 
    DBMS_OUTPUT.PUT_LINE(v_results);
 
    DBMS_LOB.FREETEMPORARY(v_results);
END;
/
 
Ausgabe:
 
<entities>
	<entity id="0" offset="15" LENGTH="7" SOURCE="SuppliedDictionary">
		<text>Hamburg</text>
		<type>city</type>
	</entity>
	<entity id="1" offset="15" LENGTH="7" SOURCE="UserRule" ruleid="12">
		<text>Hamburg</text>
		<type>region</type>
	</entity>
	<entity id="2" offset="76" LENGTH="11" SOURCE="UserRule" ruleid="125">
		<text>Urlaubsgeld</text>
		<type>FACHTERM</type>
	</entity>
</entities>

In der Anwendung integrieren

Folgende Schritte sind notwendig:

  • PL/SQL Package für das Anlegen eines Rule Sets aus den vorhanden Stammdaten
  • APEX Oberfläche für die Pflege / Anlegen der Rule Sets
  • Funktion die extrahierte Werte über den Thesaurus anreichert
  • Funktion für den Contains Operator
    • Oracle Text Suchstring wird aus einem Satz mit den erkannten Stichworten ermitteln, z.b. wird ein Fachbegriff in der Sucheanfrage gefunden für den ein passender Thesaurus zur Verfügung steht wird hier über diesen gesucht etc.
  • Funktion, die für die verwendete APEX Facet Search aus dem gefunden Facet Stammdaten ein passende SQL an die Where Bedingung „baut“
  • Funktion, die der LLM bei einer Nutzeranfrage über den Chat Bot erweitere Informationen im Hintergrund mit liefert

XML auslesen

So kann das Ergebnis dann ausgelesen werden:

DECLARE
   v_clob   CLOB;
   v_xml    XMLTYPE;
 
   -- DOM-Objekte
   doc      DBMS_XMLDOM.DOMDocument;
   nodes    DBMS_XMLDOM.DOMNodeList;
   nd       DBMS_XMLDOM.DOMNode;
 
   -- Ergebniswerte pro Entity
   v_type   VARCHAR2(100);
   v_text   VARCHAR2(4000);
   v_offset NUMBER;
   v_length NUMBER;
 
   ------------------------------------------------------------------
   -- Hilfsfunktionen
   ------------------------------------------------------------------
   -- Liefert den Textwert des ersten Kindelements mit Namen p_name
   FUNCTION get_first_child_text(
      p_parent DBMS_XMLDOM.DOMNode,
      p_name   VARCHAR2
   ) RETURN VARCHAR2 IS
      nl  DBMS_XMLDOM.DOMNodeList;
      el  DBMS_XMLDOM.DOMNode;
   BEGIN
      nl := DBMS_XMLDOM.getElementsByTagName(DBMS_XMLDOM.makeElement(p_parent), p_name);
      IF DBMS_XMLDOM.getLength(nl) > 0 THEN
         el := DBMS_XMLDOM.item(nl, 0);
         RETURN DBMS_XMLDOM.getNodeValue(DBMS_XMLDOM.getFirstChild(el));
      END IF;
      RETURN NULL;
   END;
 
   -- Liefert Attributwert (NULL-sicher)
   FUNCTION get_attr(
      p_parent DBMS_XMLDOM.DOMNode,
      p_name   VARCHAR2
   ) RETURN VARCHAR2 IS
   BEGIN
      RETURN DBMS_XMLDOM.getAttribute(DBMS_XMLDOM.makeElement(p_parent), p_name);
   EXCEPTION
      WHEN OTHERS THEN
         RETURN NULL;
   END;
 
BEGIN
   ------------------------------------------------------------------
   -- 1) NER ausführen → Ergebnis in CLOB
   ------------------------------------------------------------------
   CTX_ENTITY.EXTRACT(
        policy_name => 'TARIF_ENTITIES',
        document    => 'Ich arbeite in Hamburg in einer Werft als Schweißer und frage mich wie viel Urlaubsgeld ich bekomme',
        language    => NULL,
        result      => v_clob
   );
 
   ------------------------------------------------------------------
   -- 2) CLOB → XMLTYPE → DOM
   ------------------------------------------------------------------
   v_xml := XMLTYPE(v_clob);
   doc   := DBMS_XMLDOM.newDOMDocument(v_xml);
 
   ------------------------------------------------------------------
   -- 3) Alle <entity>-Knoten
   ------------------------------------------------------------------
   nodes := DBMS_XMLDOM.getElementsByTagName(doc, 'entity');
 
   FOR i IN 0 .. DBMS_XMLDOM.getLength(nodes) - 1 LOOP
      nd := DBMS_XMLDOM.item(nodes, i);
 
      -- Typ: bevorzugt aus Child <type>, sonst Attribut type
      v_type := NVL(get_first_child_text(nd, 'type'), get_attr(nd, 'type'));
 
      -- Begriff: Child <text>
      v_text := get_first_child_text(nd, 'text');
 
      -- Positionen: bevorzugt Attribute offset/length
      v_offset := TO_NUMBER(NVL(get_attr(nd, 'offset'), get_first_child_text(nd, 'startpos')));
      v_length := CASE
                    WHEN get_attr(nd, 'length') IS NOT NULL
                      THEN TO_NUMBER(get_attr(nd, 'length'))
                    ELSE
                      -- Fallback: aus startpos/endpos berechnen
                      CASE
                        WHEN get_first_child_text(nd, 'startpos') IS NOT NULL
                         AND get_first_child_text(nd, 'endpos')   IS NOT NULL
                        THEN
                          -- Hinweis: endpos kann je nach Format exklusiv sein.
                          -- Bei Bedarf +1 addieren.
                          TO_NUMBER(get_first_child_text(nd, 'endpos'))
                          - TO_NUMBER(get_first_child_text(nd, 'startpos'))
                        ELSE NULL
                      END
                  END;
 
      -- Ausgabe 
      DBMS_OUTPUT.put_line(
         NVL(v_type,'?')||' | '||
         '"'||NVL(v_text,'')||'"'||' | '||
         NVL(TO_CHAR(v_offset),'')||' | '||
         NVL(TO_CHAR(v_length),'')
      );
   END LOOP;
END;
/
 
 
city      | "Hamburg"     | 15 | 7
region    | "Hamburg"     | 15 | 7
FACHTERM  | "Urlaubsgeld" | 76 | 11

KI Model verwenden

Model bereitstellen und in die DB laden

Im ersten Schritt benötigen wir den OML4Py Client um das Model intfloat/multilingual-e5-base in das ONNX Format zu wandeln und es dann in die DB zuladen, siehe dazu auch Oracle Datenbank 23ai - Modelle für Vector Search über Python OML4Py bereitstellen und mit Hybrid Vector Search testen und auch ür die Installation von OML4Py und das Laden des Models in eine ADB Oracle Datenbank 26ai – Vector Search: Ergebnisse per Re-Ranking schärfen und Rauschen minimieren.

Voraussetzung OML4Py wie oben beschrieben installiert:

su - oracle
source /home/oracle/oml_rerank_env/bin/activate
cd /opt/oracle/products/OML4Py
 
python3
 
>>> import oml
>>> oml
<module 'oml' from '/opt/oracle/products/OML4Py/oml/__init__.py'>
>>> oml.EmbeddingModelConfig.show_preconfigured()
 
>>> from oml.utils import EmbeddingModel, EmbeddingModelConfig
>>> em = EmbeddingModel(model_name="intfloat/multilingual-e5-base")
 
>>> em.export2file("multilingual-e5-base", output_dir="/opt/oracle/datadump")
 
>>> exit()

Modell unter /opt/oracle/datadump/multilingual-e5-base.onnx in einen Objektstore laden, freigeben und in die ADB importieren (siehe Artikel Links weiter oben für die Details).

Laden in die ADB:

DECLARE
  v_uri VARCHAR2(1000) := 'https://objectstorage.eu-frankfurt-1.oraclecloud.com/p/xxxxxxxxxxxxxxxxxx/GPIDATAEXCHANGE/o/multilingual-e5-base.onnx';
  v_blob BLOB := NULL;
BEGIN
  v_blob := DBMS_CLOUD.GET_OBJECT(
      -- nur notwendig wenn nicht mit einem re-authenticated reques gearbeitet wird
      -- credential_name => 'OCI_OBJ_STORE_CRED' , -- Credential für den Zugriff
       object_uri => v_uri
  );
  -- Modell aus dem BLOB laden
  DBMS_VECTOR.LOAD_ONNX_MODEL(
    model_name => 'MULTILINGUAL_E5_BASE', -- darauf achten Modelnamen mit _ angeben sonst Fehler!
    model_data => v_blob,
    metadata   => JSON ('{ "function" : "embedding", "embeddingOutput" : "embedding", "input": { "input": ["DATA"] } }')
  );
END;
/

Model in der DB verwenden

Das Modell multilingual-e5-base erwartet zwingend ein Prefix auch bei der Verwendung in der Datenbank, exakt wie im Hugging‑Face‑Original.

Query‑Prefix: query: Passage/Text‑Prefix: passage:

Für normale Embeddings eines Satzes nutzt man damit „passage: Panzer bauen“.

Testaufruf ob das Model sich so verwenden läßt:

SELECT VECTOR_EMBEDDING(MULTILINGUAL_E5_BASE   USING  'passage: Panzer bauen' AS DATA) AS vec FROM dual;

oder auch:

SET SERVEROUTPUT ON;
 
DECLARE
    v_vec VECTOR;
    v_dim NUMBER;
    v_json CLOB;
BEGIN
 
    v_json := '{"provider":"database","model":"MULTILINGUAL_E5_BASE"}';
 
    --dynamisch das passende Model verwenden:
 
    SELECT DBMS_VECTOR.UTL_TO_EMBEDDING(
               'passage: Panzer bauen',
               JSON(v_json)
           )
      INTO v_vec
      FROM dual;
 
    -- Dimension über SQL abfragen
    SELECT VECTOR_DIMS(v_vec)
      INTO v_dim
      FROM dual;
 
    DBMS_OUTPUT.put_line('Embedding Dimension: ' || v_dim);
END;
/
 
 
 
Embedding Dimension: 768

Unser Vergleich mit der Suchanfrage:

WITH
q AS (
    SELECT DBMS_VECTOR.UTL_TO_EMBEDDING(
               'query: Baue Panzer',
               JSON('{"provider":"database","model":"MULTILINGUAL_E5_BASE"}')
           ) AS vec
    FROM dual
),
p AS (
    SELECT DBMS_VECTOR.UTL_TO_EMBEDDING(
               'passage: Montage Panzer',
               JSON('{"provider":"database","model":"MULTILINGUAL_E5_BASE"}')
           ) AS vec
    FROM dual
)
SELECT
    vector_distance( q.vec, p.vec ,COSINE ) AS cosine_distance,   1 - vector_distance( q.vec, p.vec ,COSINE ) AS cosine_similarity
FROM q, p;
 
COSINE_DISTANCE       COSINE_SIMILARITY
---------------       -----------------
0.1249144971572671    0.8750855028427329

VECTOR_DISTANCE(…, COSINE) liefert 1 – CosineSimilarity d. h. 0 = gleiche Bedeutung, 1 = sehr verschieden.

1 - VECTOR_DISTANCE(…, COSINE) ergibt die klassische Cosinus‑Ähnlichkeit mit 1.0 = identisch / extrem ähnlich bis 0.0 = keine Ähnlichkeit.

Also in unsere Beispiel scheint „Baue Panzer“ schon in Richtung „Montage Panzer“ zu passen.

Wir wollen ja am ende mit der Beschreibung der Tätigkeit auf die Industrie schließen.


Modell für unsere Fragestellung einsetzen

Nach dem wir nun gesehen haben, das wir das Model prinzipiell in der DB verwenden können und wie, geht es darum das praktisch für die Aufgaben zu verwenden.

Als nächstes Brauchen wir eine Tabelle mit einer Text Spalte mit den entsprechenden Informationen um den Ausdruck „baue Panzer“ überhaupt zu Wehrtechnik ⇒ „ Metall Industrie “ zuordnen zu können.

Tabelle mit Beispiel Daten anlegen

CREATE SEQUENCE SEQ_FACHGEBIETE;
 
CREATE TABLE FACHGEBIETE (
    ID              NUMBER(10)        PRIMARY KEY,
    FACHGEBIET      VARCHAR2(256 CHAR),
    HAUPTBRANCHE    VARCHAR2(256 CHAR),
    BESCHREIBUNG    VARCHAR2(2000 CHAR),
    SEARCH_EMBEDDING VECTOR
);
 
 
CREATE OR REPLACE TRIGGER FACHGEBIETE_EMB_TRG
BEFORE INSERT OR UPDATE ON FACHGEBIETE
FOR EACH ROW
DECLARE
    v_json CLOB;
BEGIN
    v_json := '{"provider":"database","model":"MULTILINGUAL_E5_BASE"}';
 
    :NEW.search_embedding :=
        DBMS_VECTOR.UTL_TO_EMBEDDING(
            'passage: ' ||
            :NEW.FACHGEBIET || ' ' ||
            :NEW.HAUPTBRANCHE || ' ' ||
            :NEW.BESCHREIBUNG,
            JSON(v_json)
        );
 
     -- PK setzen   
     IF :OLD.id IS NULL AND :NEW.id IS NULL THEN
       :NEW.id:=SEQ_FACHGEBIETE.nextval;
     END IF;    
END;
/
 
CREATE VECTOR INDEX FACHGEBIETE_EMB_HNSW_IDX
ON FACHGEBIETE (SEARCH_EMBEDDING)
ORGANIZATION INMEMORY NEIGHBOR GRAPH
DISTANCE COSINE;

Zum Index siehe auch Oracle Datenbank 26ai - Vectoren indizieren – HNSW – Der "Hierarchical Navigable Small World" Index im Detail

Beispiel Daten einfügen:

 
 
INSERT INTO FACHGEBIETE (ID, FACHGEBIET, HAUPTBRANCHE, BESCHREIBUNG)
VALUES (SEQ_FACHGEBIETE.nextval, 'Wehrtechnik', 'Metall', 'Montage Panzer');
 
INSERT INTO FACHGEBIETE (ID, FACHGEBIET, HAUPTBRANCHE, BESCHREIBUNG)
VALUES (SEQ_FACHGEBIETE.nextval, 'Wehrtechnik', 'Metall', 'Fertigung Munition');
 
INSERT INTO FACHGEBIETE (ID, FACHGEBIET, HAUPTBRANCHE, BESCHREIBUNG)
VALUES (SEQ_FACHGEBIETE.nextval, 'Automobil', 'Metall', 'PKW Montage');
 
INSERT INTO FACHGEBIETE (ID, FACHGEBIET, HAUPTBRANCHE, BESCHREIBUNG)
VALUES (SEQ_FACHGEBIETE.nextval, 'Automobil', 'Metall', 'Schäffler');
 
 
COMMIT;

Abfragen mit:

WITH q AS (
  SELECT DBMS_VECTOR.UTL_TO_EMBEDDING(
           'query: '||:SEARCH_QUEST,
           JSON('{"provider":"database","model":"MULTILINGUAL_E5_BASE"}')
         ) AS vec
  FROM dual
)
, qresult AS (
SELECT
    f.ID,
    f.FACHGEBIET,
    f.HAUPTBRANCHE,
    f.BESCHREIBUNG,
    VECTOR_DISTANCE( q.vec, f.search_embedding , COSINE ) AS dist,
    prediction( 
         BGE_RERANKER 
           USING :SEARCH_QUEST  AS DATA1 -- Query
         , f.search_embedding AS DATA2                                       -- Treffer
       ) AS relevance_score
FROM FACHGEBIETE f, q
ORDER BY dist ASC
FETCH FIRST 3 ROWS ONLY
)
SELECT r.*
      ,  0.7 * (1-r.dist) + 0.3 * r.relevance_score AS  final_score  
FROM qresult r ;

Als nächstes füllen wir die Daten mit über eine LLM mit einen entsprechenden Prompt :

Erstelle eine strukturierte und hochwertige Liste typischer Fachgebiete und dazugehöriger Beschreibungen für die Hauptbranche „Metall“. 
Die Ausgabe soll später als CSV importiert werden können.
 
Liefere pro Zeile:
- Fachgebiet (konkretes, real existierendes Teilsegment der Metallindustrie)
- Hauptbranche (immer: Metall)
- Beschreibung (kurze, präzise, domänenspezifische Tätigkeits- oder Produktbeschreibung)
 
Inhaltliche Anforderungen:
- Mindestens 30 klar unterscheidbare Fachgebiete.
- Berücksichtige reale industrielle Segmente, große Unternehmen, typische Herstellungsbereiche und Spezialisierungen der Metall- und metallverarbeitenden Industrie.
- Verwende in Beschreibungen typische Unternehmensbezüge und umgangssprachliche Synonyme großer Firmen (z. B. „Krupp“ für ThyssenKrupp, „Salzgitter“, „Schaeffler“, „Voith“, „MAN“, „Bosch Rexroth“, „ZF“, etc.).
- Nutze klassische Industriebereiche (z. B. Stahlbau, Gießerei), moderne Fertigungstechnologien (z. B. additive Fertigung) und Nischenbereiche (z. B. Präzisionslagerfertigung, Hartmetallbearbeitung).
- Beschreibungen müssen realistisch, präzise und berufsbezogen formuliert sein.
- Keine Oberbegriffe, keine Wiederholungen – jede Zeile muss ein eindeutig unterscheidbares Fachgebiet darstellen.
- Fokus auf Tätigkeiten, Produkten, Werkstoffen, Fachprozessen und typischen Industrieabläufen.
- Jede Beschreibung soll semantisch relevante Schlüsselbegriffe enthalten, damit sie sich gut für Embeddings eignet.
 
Format:
- Gib die Antwort ausschließlich als CSV mit folgendem Header aus:
FACHGEBIET;HAUPTBRANCHE;BESCHREIBUNG
 
Beispiel (nicht wiederholen):
Wehrtechnik;Metall;Montage Panzer
 
Jetzt generiere die vollständige Liste.

das gibt uns eine CSV in der Art:

FACHGEBIET;HAUPTBRANCHE;BESCHREIBUNG
Stahlbau;Metall;Herstellung von Tragwerken, Brücken, Hallen und Stahlkonstruktionen für Bau und Industrie
Gießereitechnik;Metall;Formguss von Eisen-, Stahl- und Nichteisenmetallen für Maschinenbau und Fahrzeugtechnik
Pulvermetallurgie;Metall;Fertigung von Bauteilen aus Metallpulver durch Sintern und Pressen
Zerspanungstechnik;Metall;CNC-Drehen und Fräsen von Präzisionsbauteilen aus Metall
Umformtechnik;Metall;Walzen, Schmieden, Pressen und Tiefziehen metallischer Werkstoffe
Schweißtechnik;Metall;Fügen von Metallen durch Lichtbogen-, Laser-, MIG/MAG- und WIG-Schweißverfahren
Nächste Schritte

In PL/SQL implementieren wir nun eine Routine, die über den angebundenen Cloud‑AI‑Service die relevanten Daten zu unseren Hauptbranchen aus den vorhandenen Stammdaten abruft und anschließend automatisiert in die Anwendungstabelle einträgt.

Parallel dazu wird in einer Mapping‑Tabelle des Datenmodells hinterlegt, wie die bestehenden, „harten“ Stammdaten zur Hauptbranche mit den neu ermittelten Einträgen der FACHGEBIETE‑Tabelle verknüpft sind.

Dadurch entsteht eine eindeutige und konsistente Beziehung zwischen klassischer Stammdatenstruktur und KI‑basiert ermittelter Branchenzuordnung, die später für semantische Suche, Klassifikation und regelbasierte Logik verwendet werden kann.



Fazit

Ziel: Mit NER Entitäten in den Suchanfragen erkennen und dann mit einem Thesaurus semantisch einordnen.

Ergebnis:

Es wird kombiniert:

  • CTX_ENTITY → findet Bamberg
  • CTX_THES / CTX_THES_RULE / CTX_DDL.ADD_THESAURUS_ENTRY → liefert Bayern
  • Hybrid-Ansatz: Falls Oracle Text NER kein oder nur wenig Ergebnis liefert wird eine Vector Search auf eine Entsprechenden Tabellen eingesetzt und lieft ebenfalls ein Ergebnis.

Die eigene Tarif‑Logik wählt aus diesen „verstanden“ Daten den passenden Tarifvertrag für das RAG.

Noch zu lösendes Problem: Etwas langsam …..


Quellen

Diese Website verwendet Cookies. Durch die Nutzung der Website stimmen Sie dem Speichern von Cookies auf Ihrem Computer zu. Außerdem bestätigen Sie, dass Sie unsere Datenschutzbestimmungen gelesen und verstanden haben. Wenn Sie nicht einverstanden sind, verlassen Sie die Website.Weitere Information
"Autor: Gunther Pipperr"
dba/oracle_text_ctx_entity_ner_extraction.txt · Zuletzt geändert: von gpipperr