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:
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.
Ablauf:
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;
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.
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>
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>
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;
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>
Folgende Schritte sind notwendig:
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
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; /
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.
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.
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
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.
Ziel: Mit NER Entitäten in den Suchanfragen erkennen und dann mit einem Thesaurus semantisch einordnen.
Ergebnis:
Es wird kombiniert:
Die eigene Tarif‑Logik wählt aus diesen „verstanden“ Daten den passenden Tarifvertrag für das RAG.
Noch zu lösendes Problem: Etwas langsam …..