Run Your Own AI Model Inside Oracle 26ai: Load an ONNX Embedding Model Step by Step

Oracle APEX Oracle 26ai Free ONNX Series Part 2

Run Your Own AI Model Inside Oracle 26ai: Load an ONNX Embedding Model Step by Step

No API key, no cloud service, no data leaving the server. One ONNX file, one PL/SQL call, and your Oracle database turns text into vectors with plain SQL. Tested for real, with every error I hit.

📅 October 2026 👤 Sajjad Hanifa ⏱ ~15 min read 🏢 S&H Software Solutions

Most "AI in APEX" demos I see work the same way. The text leaves the database, goes to an external API, and a vector comes back. That works, but it costs money per call, it needs an API key in your database, and your customer data travels to somebody else's server. For a lot of my projects that last point alone is a deal breaker.

Oracle 23ai, now called Oracle AI Database 26ai, can do it differently. You load an AI model into the database as an ONNX file, and from then on a single SQL function turns any text into a vector. The model runs inside the database process, next to your data. In this post I do exactly that on the Docker stack from part 1, step by step, and at the end my APEX app from part 1 finds tasks by meaning instead of by matching words.

It went smoother than the installation in part 1. Loading the model took 2.8 seconds. But I still ran into three errors on the way, and one of them broke every insert in my APEX app for a moment. They are all in here.

Your own AI model inside the database: load an ONNX embedding model into Oracle 26ai and search APEX data by meaning
APEX SQL Commands running a semantic search with VECTOR_EMBEDDING and VECTOR_DISTANCE
Real runWhere we are heading. I ask in plain English, and APEX finds the matching task by meaning. All of it computed inside the database.
This is part 2 of 3.

In part 1 I installed Oracle AI Database 26ai Free, ORDS and APEX 26.2 in Docker. Today the database gets its own embedding model. In part 3 we build a real semantic search page in APEX on top of it, with a vector index.

What an Embedding Model Does

An embedding model reads a piece of text and returns a list of numbers, a vector. The model we use returns 384 of them. The numbers on their own mean nothing to a human. What matters is that texts with a similar meaning get vectors that are close to each other, even when they do not share a single word.

"My containers run out of RAM" and "Review memory limits of the containers" have almost no words in common that a LIKE would catch. Their vectors are very close. That is the whole trick behind semantic search, and also the base for RAG and chat with your own data later.

PieceWhat I usedWhy
Modelall-MiniLM-L12-v2, Oracle's prebuilt "augmented" ONNX fileSmall (128 MB), fast, good quality for English. The tokenizer is already inside the file.
LoaderDBMS_VECTOR.LOAD_ONNX_MODELPart of every 23ai and 26ai database, also Free
SQL functionsVECTOR_EMBEDDING, VECTOR_DISTANCEText to vector, and how far apart two vectors are
Data typeVECTOR(384, FLOAT32)A normal column next to your data

What You Need

Just the stack from part 1. Oracle AI Database 26ai Free 23.26.3 in a container, APEX 26.2, and the small DEMO_TASKS table behind the Task Board app. I did not have to change a single memory setting for this post. Everything here also runs on Oracle Database 23ai Free (23.4 and newer) and on Autonomous Database.


The Setup in 12 Steps

01
Step one

Check the Starting Point

SYS · FREEPDB1 · DBMS_VECTOR

A quick look before I touch anything. Which release, how much PGA the database may use, whether DBMS_VECTOR is there, and whether the demo table from part 1 still has its ten rows.

SQL01_check-starting-point.sqlas SYS in FREEPDB1
set lines 140 pages 50
col banner_full format a90
col name format a24
col value format a16
col object_name format a20
select banner_full from v$version;
select name, value from v$parameter
 where name in ('pga_aggregate_target','pga_aggregate_limit','sga_target','vector_memory_size')
 order by name;
-- the PL/SQL package that loads ONNX models is part of every 23ai/26ai database
select object_name, object_type, status from dba_objects
 where owner = 'SYS' and object_name = 'DBMS_VECTOR' order by object_type;
-- the demo table from part 1
select count(*) as demo_tasks_rows from apexnote.demo_tasks;
SQL*Plus output with the 26ai banner, PGA parameters, DBMS_VECTOR valid and 10 demo rows
Real runRelease 23.26.3, 512 MB PGA target, DBMS_VECTOR valid, ten rows waiting. vector_memory_size is 0, which is fine for today.

I run all SQL scripts in this post the same way as in part 1. No Oracle client on the host, the script goes into the container and SQL*Plus runs it there.

Shellhow every script runson the server
docker cp script.sql apexnote-db:/tmp/ && \
  docker exec -i apexnote-db sqlplus -l sys/<your_sys_password>@//localhost:1521/FREEPDB1 as sysdba @/tmp/script.sql
02
Step two

Download the Prebuilt Model

117 MB zip · no Oracle account · 5.5 s

You can convert almost any Hugging Face model to ONNX yourself with Oracle's OML4Py. For a start you do not need to. Oracle offers a few ready models, and the most popular one is all-MiniLM-L12-v2. It is called "augmented" because the tokenizer and the post-processing are already built into the ONNX graph, so the database can feed it plain text. The download link is public and comes from the Oracle AI Vector Search User's Guide.

Shell02_download-model.shin the project folder
set -e
mkdir -p models && cd models          # inside the project folder from part 1
curl -sS -o all_MiniLM_L12_v2_augmented.zip \
  "https://adwc4pm.objectstorage.us-ashburn-1.oci.customer-oci.com/p/TtH6hL2y25EypZ0-rrczRZ1aXp7v1ONbRBfCiT-BDBN8WLKQ3lgyW6RxCfIFLdA6/n/adwc4pm/b/OML-ai-models/o/all_MiniLM_L12_v2_augmented.zip"
unzip -o all_MiniLM_L12_v2_augmented.zip
ls -lh
sha256sum all_MiniLM_L12_v2.onnx
curl, unzip and ls of the model files with sha256 checksum
Real runOne 128 MB .onnx file, a license file and a README. I note the checksum so I know later exactly which model I loaded.
03
Step three

Make the File Visible Inside the Container

docker cp · data volume

This is the Docker specific part. The database reads the file through a directory object, and that directory must exist inside the container. I put it on the data volume under /opt/oracle/oradata, so it would survive a re-created container. You could also mount a host folder in the compose file, but for a one-time import docker cp is simpler.

Shell03_copy-model-into-container.sh
# We put it on the data volume (/opt/oracle/oradata), so it survives a container re-create.
set -e
docker exec apexnote-db mkdir -p /opt/oracle/oradata/models
docker cp models/all_MiniLM_L12_v2.onnx apexnote-db:/opt/oracle/oradata/models/
docker exec apexnote-db ls -l /opt/oracle/oradata/models
ls of the model file inside the database container
Real runThe file is inside the container now.
04
Step four

A Schema That Owns the Model

DB_DEVELOPER_ROLE · CREATE MINING MODEL · directory

I do not load the model into the APEX workspace schema. A separate schema, APEXNOTE_VEC, owns it, and every schema that needs it gets a grant. It is the same idea as a shared lookup table. One model, one owner, many users, and replacing the model later touches only one place.

The only special privilege is CREATE MINING MODEL. ONNX models live in the same dictionary as the classic Oracle Machine Learning models, that is where the name comes from. DB_DEVELOPER_ROLE is the 23ai role that bundles the usual developer privileges.

SQL04_create-model-schema.sqlas SYS
create user apexnote_vec identified by "<your_vec_password>"
  default tablespace users quota 1g on users;

-- DB_DEVELOPER_ROLE (new in 23ai) = create session, table, view, procedure, ...
-- CREATE MINING MODEL is the extra privilege you need to import an ONNX model
grant db_developer_role, create mining model to apexnote_vec;

create or replace directory onnx_models as '/opt/oracle/oradata/models';
grant read on directory onnx_models to apexnote_vec;

set lines 120
col privilege format a24
col directory_name format a14
col directory_path format a32
select 'ROLE' as kind, granted_role as privilege from dba_role_privs where grantee = 'APEXNOTE_VEC'
union all
select 'SYSTEM PRIV', privilege from dba_sys_privs where grantee = 'APEXNOTE_VEC';
select directory_name, directory_path from dba_directories where directory_name = 'ONNX_MODELS';
User created, grants, directory ONNX_MODELS pointing to /opt/oracle/oradata/models
Real runOne role, one privilege, read access on one directory. Read is enough, the loader does not write anything.
05
Step five

Load the Model

DBMS_VECTOR.LOAD_ONNX_MODEL · 2.78 s

This is the moment the database gets its AI. One call as the model owner. The metadata JSON tells Oracle three things. The model is an embedding model, its output is called embedding, and its input input receives a column we will call DATA.

SQL05_load-onnx-model.sqlas APEXNOTE_VEC
set timing on
begin
  dbms_vector.load_onnx_model(
    directory  => 'ONNX_MODELS',
    file_name  => 'all_MiniLM_L12_v2.onnx',
    model_name => 'ALL_MINILM_L12_V2',
    metadata   => json('{
      "function"        : "embedding",
      "embeddingOutput" : "embedding",
      "input"           : { "input": ["DATA"] }
    }'));
end;
/
OutputSQL*Plusreal run
PL/SQL procedure successfully completed.

Elapsed: 00:00:02.78
PL/SQL procedure successfully completed in 2.78 seconds
Real run2.78 seconds. During the load the database container went from 2.4 to 2.8 GiB of RAM and came back down afterwards.
Same name, different model?

The model name you choose here (ALL_MINILM_L12_V2) is what every query will use. If you load a newer version later, either use a new name or drop the old one first with DBMS_VECTOR.DROP_ONNX_MODEL(model_name => 'ALL_MINILM_L12_V2', force => true). Vectors from two different models can not be compared with each other, so re-embed your data when you switch.

06
Step six

Check the Model in the Dictionary

USER_MINING_MODELS · attributes

The model is now a database object like a table or a package. You can query it, grant it, and it goes into a Data Pump export of the schema.

SQL06_check-model.sqlas APEXNOTE_VEC
set lines 140 pages 50
col model_name format a18
col mining_function format a16
col algorithm format a10
col attribute_name format a14
col attribute_type format a14
col data_type format a10
col vector_info format a24
select model_name, mining_function, algorithm, round(model_size/1024/1024, 1) as size_mb,
       to_char(creation_date, 'YYYY-MM-DD HH24:MI') as created
  from user_mining_models;

-- input and output of the model: text in, VECTOR(384, FLOAT32) out
select attribute_name, attribute_type, data_type, vector_info
  from user_mining_model_attributes
 where model_name = 'ALL_MINILM_L12_V2';
OutputSQL*Plusreal run
MODEL_NAME         MINING_FUNCTION  ALGORITHM     SIZE_MB CREATED
------------------ ---------------- ---------- ---------- ----------------
ALL_MINILM_L12_V2  EMBEDDING        ONNX            127.1 2026-10-13 18:35

ATTRIBUTE_NAME ATTRIBUTE_TYPE DATA_TYPE  VECTOR_INFO
-------------- -------------- ---------- ------------------------
ORA$ONNXTARGET VECTOR         VECTOR     VECTOR(384,FLOAT32)
DATA           TEXT           VARCHAR2
USER_MINING_MODELS shows ALL_MINILM_L12_V2 as EMBEDDING ONNX 127.1 MB
Real runText goes in as DATA, a VECTOR(384,FLOAT32) comes out. Note the 384, we need it for the column in step 10.
07
Step seven

The First Embedding

VECTOR_EMBEDDING · 0.81 s cold · 0.01 s warm

Now the database can turn text into vectors with a plain SELECT. The USING ... AS data part maps my text to the input name from the metadata.

SQL07_first-embedding.sqlas APEXNOTE_VEC
set lines 140 pages 50 long 200 longchunksize 200
col first_values format a100
set timing on
select vector_embedding(all_minilm_l12_v2 using 'Oracle APEX runs inside the database' as data) as embedding
  from dual;

-- the same call again in the same session: the model is already loaded now
select vector_embedding(all_minilm_l12_v2 using 'Oracle APEX runs inside the database' as data) as embedding
  from dual;
VECTOR_EMBEDDING returns a list of floating point numbers
Real runThe first call in a session took 0.81 seconds, because the model is loaded into the session. The same call right after took 0.01 seconds.

A wall of numbers is not very readable, so I wanted to see only the dimension count and the first few values. My first try failed.

SQL08_readable-embedding_FAILED-ora-51810.sqlfails
set lines 140 pages 50
col fmt format a8
col first_values format a90
select vector_dimension_count(v) as dims,
       vector_dimension_format(v) as fmt,
       substr(from_vector(v), 1, 84) || ' ...' as first_values
  from (select vector_embedding(all_minilm_l12_v2 using 'Oracle APEX runs inside the database' as data) as v
          from dual);
OutputSQL*Plusreal run
       substr(from_vector(v), 1, 84) || ' ...' as first_values
              *
ERROR at line 3:
ORA-51810: Insufficient buffer size for VECTOR to CHAR or VARCHAR conversion.
SQL09_readable-embedding.sqlworks
set lines 140 pages 50
col fmt format a8
col first_values format a90
select vector_dimension_count(v) as dims,
       vector_dimension_format(v) as fmt,
       dbms_lob.substr(from_vector(v returning clob), 84, 1) || ' ...' as first_values
  from (select vector_embedding(all_minilm_l12_v2 using 'Oracle APEX runs inside the database' as data) as v
          from dual);
OutputSQL*Plusreal run
      DIMS FMT      FIRST_VALUES
---------- -------- ------------------------------------------------------------------------------------------
       384 FLOAT32  [7.27938786E-002,4.20977548E-002,-8.40219781E-002,-3.98895852E-002,-3.34222168E-002, ...
ORA-51810 Insufficient buffer size for VECTOR to CHAR or VARCHAR conversion
Real runORA-51810. FROM_VECTOR returns a VARCHAR2(4000) by default, and 384 numbers in this notation need about 6,000 characters.
384 dimensions, FLOAT32 and the first values of the vector
Real runWith RETURNING CLOB it works. 384 dimensions, FLOAT32.
08
Step eight

See Why This Is Useful

VECTOR_DISTANCE · cosine

Before I touch any real table, a small test that shows what the model understands. One question, five sentences, and the cosine distance between them. 0 means same meaning, values close to 1 mean unrelated.

SQL10_compare-sentences.sqlas APEXNOTE_VEC
set lines 140 pages 50
col sentence format a52
col distance format 0.000
with q as (
  select vector_embedding(all_minilm_l12_v2 using 'My container keeps running out of memory' as data) as v from dual
), s (sentence) as (
  values ('The Docker container was killed by the OOM killer'),
         ('Increase the RAM limit of the database container'),
         ('How do I change the APEX admin password?'),
         ('Schedule a nightly backup with Data Pump'),
         ('My favourite pizza is margherita')
)
select s.sentence,
       vector_distance(q.v, vector_embedding(all_minilm_l12_v2 using s.sentence as data), cosine) as distance
  from s cross join q
 order by distance;
OutputSQL*Plusreal run
SENTENCE                                             DISTANCE
---------------------------------------------------- --------
Increase the RAM limit of the database container        0.383
The Docker container was killed by the OOM killer       0.510
Schedule a nightly backup with Data Pump                0.890
My favourite pizza is margherita                        0.928
How do I change the APEX admin password?                0.974
Cosine distances: RAM limit 0.383, OOM killer 0.510, Data Pump 0.890, pizza 0.928, APEX password 0.974
Real run"Increase the RAM limit" wins, although the question says "memory" and not "RAM". The pizza is not far behind the unrelated IT sentences, which is honest. A small model knows the topic, not every detail.

The VALUES list in the WITH clause is another 23ai feature, by the way. Before 23ai you needed five SELECT ... FROM dual UNION ALL lines for that.

09
Step nine

Grant the Model to the APEX Schema

ORA-40284 · GRANT SELECT ON MINING MODEL

My APEX app runs in the schema APEXNOTE, not in the model owner. So I tried the model from there.

SQL11_use-model-from-app-schema_FAILED-no-grant.sqlas APEXNOTE
select vector_dimension_count(
         vector_embedding(apexnote_vec.all_minilm_l12_v2 using 'hello' as data)) as dims
  from dual;
OutputSQL*Plusreal run
  from dual
       *
ERROR at line 3:
ORA-40284: model does not exist
SQL12_grant-model-to-app-schema.sqlas APEXNOTE_VEC
grant select on mining model all_minilm_l12_v2 to apexnote;

set lines 120
col grantee format a10
col table_name format a20
col privilege format a10
col type format a20
select grantee, table_name, privilege, type from user_tab_privs_made where table_name = 'ALL_MINILM_L12_V2';
ORA-40284 model does not exist
Real run"Model does not exist." It does. The message just does not tell you that the real problem is a missing grant.
Grant succeeded, USER_TAB_PRIVS_MADE shows SELECT on the mining model
Real runThe grant syntax has the extra words MINING MODEL. The dictionary lists it as a "(Data Mining) MODEL".
10
Step ten

Add a Vector Column and Fill It

VECTOR(384, FLOAT32) · 10 rows in 0.91 s

Now the part that turns this into something useful for APEX. The task table gets a new column with the exact shape the model returns, and one UPDATE fills it for all existing rows.

SQL13_add-vector-column-and-embed.sqlas APEXNOTE
set lines 140 pages 50 timing on
alter table demo_tasks add (title_vec vector(384, float32));

update demo_tasks
   set title_vec = vector_embedding(apexnote_vec.all_minilm_l12_v2 using title as data);
commit;

set timing off
col title format a48
select id, title, vector_dimension_count(title_vec) as dims from demo_tasks order by id;
ALTER TABLE add vector column, update 10 rows, all with 384 dimensions
Real run0.11 seconds for the column, 0.91 seconds to embed all ten titles.
Declare the dimensions.

VECTOR without arguments would accept vectors of any size. I always write VECTOR(384, FLOAT32). If somebody ever writes vectors from a different model into this column, Oracle rejects them instead of silently mixing two models that can not be compared.

11
Step eleven

The First Semantic Search

ORDER BY VECTOR_DISTANCE · FETCH FIRST

This is the moment I was waiting for. The question is turned into a vector on the fly and compared with every task. Smallest distance first.

SQL14_first-similarity-search.sqlas APEXNOTE
set lines 140 pages 50
col title format a48
col status format a12
col distance format 0.000
variable q varchar2(200)

exec :q := 'my containers run out of RAM'
select id, title, status,
       vector_distance(title_vec,
                       vector_embedding(apexnote_vec.all_minilm_l12_v2 using :q as data),
                       cosine) as distance
  from demo_tasks
 order by distance
 fetch first 3 rows only;

exec :q := 'backup the database every night'
/

-- compare: a classic LIKE search finds nothing for the same question
select count(*) as like_hits from demo_tasks where lower(title) like '%backup%';
OutputSQL*Plusreal run
-- 'my containers run out of RAM'
        ID TITLE                                            STATUS       DISTANCE
---------- ------------------------------------------------ ------------ --------
        10 Review memory limits of the containers           Open            0.227
         1 Install Oracle AI Database 26ai Free in Docker   Done            0.773
         6 Schedule a nightly Data Pump export              Open            0.836

-- 'backup the database every night'
        ID TITLE                                            STATUS       DISTANCE
---------- ------------------------------------------------ ------------ --------
         6 Schedule a nightly Data Pump export              Open            0.472
         1 Install Oracle AI Database 26ai Free in Docker   Done            0.825
         4 Create developer account                         Done            0.920

 LIKE_HITS
----------
         0
Semantic search results for two questions and a LIKE search with zero hits
Real run"Backup the database every night" finds the Data Pump task. A LIKE '%backup%' finds nothing, because the word backup is not in any title.

And because it is just SQL, it works everywhere APEX runs SQL. Here is the same query in SQL Workshop, logged in as the developer account from part 1.

SQLAPEX SQL Workshop > SQL Commandsas DEMO_DEV
select title, status,
round(vector_distance(title_vec,
vector_embedding(apexnote_vec.all_minilm_l12_v2
using 'the server is running out of memory' as data),
cosine), 3) as distance
from demo_tasks
order by distance
fetch first 3 rows only
APEX SQL Commands with the semantic search query and three results
Real runAPEX SQL Commands. "The server is running out of memory" finds the memory task first. In part 3 this becomes a real search page.
12
Step twelve

Keep the Vectors in Sync

before insert or update trigger · APEX form test

New tasks come from the APEX form, and titles get changed. The vector has to follow without anybody thinking about it. A trigger is the simplest way, so I wrote the obvious one.

PL/SQL15_keep-vectors-in-sync_FAILED-pls-00103.sqlfails
create or replace trigger demo_tasks_vec_biu
  before insert or update of title on demo_tasks
  for each row
begin
  :new.title_vec := vector_embedding(apexnote_vec.all_minilm_l12_v2 using :new.title as data);
end;
/
show errors
OutputSQL*Plusreal run
Warning: Trigger created with compilation errors.

LINE/COL ERROR
-------- -----------------------------------------------------------------
2/86     PLS-00103: Encountered the symbol "AS" when expecting one of the
         following:
         )

ERROR at line 1:
ORA-04098: trigger 'APEXNOTE.DEMO_TASKS_VEC_BIU' is invalid and failed
re-validation
PLS-00103 in the trigger and ORA-04098 on insert and update
Real runPLS-00103, and from then on every insert and update on the table failed with ORA-04098. Including the ones from the APEX app.

VECTOR_EMBEDDING is a SQL function. Its USING ... AS data syntax does not exist in a PL/SQL expression, so the compiler stops at the AS. The fix is to call it through SQL with SELECT ... INTO.

PL/SQL16_keep-vectors-in-sync.sqlas APEXNOTE
create or replace trigger demo_tasks_vec_biu
  before insert or update of title on demo_tasks
  for each row
begin
  select vector_embedding(apexnote_vec.all_minilm_l12_v2 using :new.title as data)
    into :new.title_vec
    from dual;
end;
/
show errors

insert into demo_tasks (title, priority, owner) values ('Renew the TLS certificate of the web server', 'High', 'Demo Admin');
update demo_tasks set title = 'Review memory and CPU limits of the containers' where id = 10;
commit;

set lines 140
col title format a48
select id, title, vector_dimension_count(title_vec) as dims from demo_tasks  where id = 10 or title like 'Renew%' order by id;
Trigger created without errors, new and updated rows have 384 dimensions
Real runNo errors, the new row and the changed row both have their vector. The ID 22 is not a typo. The failed inserts and the identity cache used up some numbers, which is normal.
My lesson from this.

A trigger with compilation errors does not just do nothing. It blocks every DML on the table. In a real application that is an outage for every user of that page. I now create triggers like this one in a disabled state, test them, and enable them only when show errors is clean.

Then the real test. Does the Task Board app from part 1 still work with the new column and the trigger? I changed the title of the task "Document stop/start commands" in the APEX form.

APEX form drawer with the new task title
Real runThe normal APEX form from part 1. Nothing in the app was changed.
Task Board interactive report after saving the new title
Real runSaved without errors. The form does not know about the vector column, the trigger takes care of it.
SQL22_search-after-apex-edit.sqlas APEXNOTE
set lines 140 pages 50
col title format a50
col distance format 0.000
select title,
       vector_distance(title_vec,
                       vector_embedding(apexnote_vec.all_minilm_l12_v2 using 'how do I restart everything?' as data),
                       cosine) as distance
  from demo_tasks
 order by distance
 fetch first 3 rows only;
how do I restart everything finds the runbook task first
Real run"How do I restart everything?" finds the task I just edited in APEX. The distance of 0.768 is higher than before. Short titles give the model little to work with, that is something we fix in part 3.

Troubleshooting. The Three Errors I Hit

ErrorWhereReal causeFix
ORA-51810 Insufficient buffer size for VECTOR to CHAR or VARCHAR conversionFROM_VECTOR(v)Default return type is VARCHAR2(4000), a 384 dimension vector as text is longerFROM_VECTOR(v RETURNING CLOB)
ORA-40284 model does not existModel used from another schemaMissing grant, the model existsGRANT SELECT ON MINING MODEL owner.model TO app_schema
PLS-00103 Encountered the symbol "AS", then ORA-04098 on every DMLTrigger with :new.x := vector_embedding(...)VECTOR_EMBEDDING is SQL onlySELECT vector_embedding(...) INTO :new.x FROM dual
Two more things worth knowing.

Typing SQL into APEX SQL Commands, the editor closes brackets for you, so pasted or typed queries can end with extra )). And if a load fails because the file can not be read, check the path from inside the container (docker exec apexnote-db ls -l /opt/oracle/oradata/models), not on the host.

Speed and Memory

Ten rows are not a benchmark, so I generated 2,000 short support ticket sentences and embedded all of them in one statement.

SQL17_how-fast-is-it.sqlas APEXNOTE_VEC
set lines 140 pages 50 timing on
create table bench_texts as
select level as id,
       'Ticket ' || level || ': the ' ||
       case mod(level, 4) when 0 then 'database' when 1 then 'APEX page' when 2 then 'REST service' else 'backup job' end ||
       ' is ' ||
       case mod(level, 3) when 0 then 'slow since the last release' when 1 then 'failing with an error' else 'working again' end
       as txt
  from dual connect by level <= 2000;

create table bench_vectors as
select id, vector_embedding(all_minilm_l12_v2 using txt as data) as v
  from bench_texts;

set timing off
select count(*) as embedded_rows from bench_vectors;
drop table bench_vectors purge;
drop table bench_texts purge;
OutputSQL*Plusreal run
Table created.
Elapsed: 00:00:00.18

Table created.
Elapsed: 00:00:24.19

EMBEDDED_ROWS
-------------
         2000
2000 rows embedded in 24.19 seconds
Real run2,000 sentences in 24 seconds. About 12 milliseconds per sentence, on one CPU core of the Free edition.
docker stats after the model was loaded, file sizes, onnx file removed from the container
Real runAfter everything, the database container uses 2.6 GiB of its 4 GiB. I also deleted the .onnx file from the container.
SQL19_model-still-works.sqlafter deleting the file
set lines 140
col title format a48
col distance format 0.000
select title,
       vector_distance(title_vec, vector_embedding(apexnote_vec.all_minilm_l12_v2 using 'SSL expires soon' as data), cosine) as distance
  from demo_tasks
 order by distance
 fetch first 1 row only;
SSL expires soon finds Renew the TLS certificate of the web server
Real runThe file is gone and the model still answers. "SSL expires soon" finds the TLS certificate task. The model is stored in the database, the file was only needed for the import.
WhatMeasured
Download (zip)117 MB, 5.5 s
Model in the database127.1 MB
Load with LOAD_ONNX_MODEL2.78 s, RAM peak +0.4 GiB
First embedding in a new session0.81 s
Every embedding after thatabout 10 ms
2,000 sentences in one statement24.2 s, one core at 100 %
Database container RAM, idle after all tests2.6 GiB of 4 GiB (part 1 was 2.05 GiB)
Changes to the memory limits from part 1none

The 0.81 seconds for the first call matter for APEX. Every APEX request can land on a different database session, so the very first search after a quiet period can feel slower. In my tests the following ones were instant.

What About Other Languages?

Many of my customers write their tasks in German, so I had to try it. Same question as in step 11, only in German.

SQL23_other-languages.sqlas APEXNOTE
set lines 140 pages 50
col title format a50
col distance format 0.000
select title,
       vector_distance(title_vec,
                       vector_embedding(apexnote_vec.all_minilm_l12_v2 using 'Den Arbeitsspeicher der Container erhöhen' as data),
                       cosine) as distance
  from demo_tasks
 order by distance
 fetch first 3 rows only;
German query finds the memory task with distance 0.565
Real runIt still finds the right task, but with a distance of 0.565 instead of 0.227 for the English question. The model was trained on English.

For anything that is not English, Oracle offers prebuilt multilingual models on the same documentation page, for example multilingual-e5-small. The steps are exactly the same, only the file name, the model name and the number of dimensions in your column change. Check USER_MINING_MODEL_ATTRIBUTES after the load, like in step 6, and use that number.

Need a hand?
AI features in your Oracle APEX app, without sending data away

Semantic search, similar records, smart suggestions or chat with your own data. We build AI features into Oracle APEX applications that run inside your database. Get in touch.


What Comes Next

The database can now understand text. The task "Build semantic search page" is still open in the Task Board, and that is part 3.

  • A real search page in APEX. One search field, results ranked by meaning, with the distance as a relevance score.
  • A vector index. With ten rows a full scan is fine. With a hundred thousand it is not. That is where vector_memory_size = 0 from part 1 comes back.
  • Better input. Short titles give the model little context. We will embed descriptions and combine them with normal filters like status and owner.

Final Thoughts

What surprised me most is how ordinary it feels. The model is a database object. You grant it like a table, it ends up in your exports, and you use it with a SQL function in a normal SELECT, a trigger or an APEX report. No Python service next to the database, no API key in a credential store, no per-call bill.

It is not magic either. A small English model has limits, and you see them in the distances. For many APEX applications it is still a big step up from LIKE '%word%', and the data never leaves the server it already lives on.

If you try this and hit an error that is not in my list, write to me. I read every message.

SH
Sajjad Hanifa
Software Developer · S&H Software Solutions · Oracle APEX, PL/SQL, AI Assistants
Oracle APEX Oracle 26ai Oracle 23ai AI Vector Search ONNX Embeddings DBMS_VECTOR Troubleshooting

 {fullWidth}

Please Select Embedded Mode To Show The Comment System.*

Previous Post Next Post

نموذج الاتصال