Building RAG Entirely Inside Oracle Database 26ai — No Vector DB, No App Layer, Just PL/SQL

Building Retrieval-Augmented Generation (RAG)

Entirely Inside Oracle Database — with PL/SQL

How I grounded a local LLM (Ollama + Qwen2.5) in Oracle 26ai's native Vector Search — no external vector database, no separate application layer.

Munish Kumar Karna

Senior Database Solution Architect | Oracle DBA | Oracle RAC, ASM, Data Guard, GoldenGate, OCI


Why This Matters

Retrieval-Augmented Generation (RAG) is usually built with a dedicated vector database, an orchestration framework, and an application server sitting between the data and the LLM. With Oracle Database 26ai, that entire pipeline can live inside the database itself.

In this walkthrough, I demonstrate a complete, self-contained RAG pipeline built purely in PL/SQL: importing an ONNX embedding model, running a local LLM through Ollama, chunking and embedding a real PDF manual, and using Oracle's native VECTOR data type and DBMS_VECTOR package to ground the model's answers in that document — all from a single SQL session.

Prerequisites

Two components need to be in place before the pipeline can run:

  • An imported ONNX embedding model — ALL_MINILM_L12_V2 — used by Oracle to generate vector embeddings natively.
  • Ollama installed locally with the qwen2.5:7b model pulled and served, so Oracle can call it as an LLM provider.

A step-by-step guide to importing an ONNX model into Oracle Database is available in this earlier post: Importing an ONNX LLM model into Oracle Database →

Verifying the ALL_MINILM_L12_V2 embedding model is registered in the database:

Verifying the ALL_MINILM_L12_V2 embedding model

Confirming Ollama is running locally with the qwen2.5:7b model loaded:

Confirming Ollama is running with qwen2.5:7b

The Problem: An LLM With No Knowledge of “Oracle 26ai”

Before building any retrieval layer, I asked the LLM directly — with no context supplied — about a very specific, current topic:

SET LONG 100000
SET SERVEROUTPUT ON
 
SELECT DBMS_VECTOR.UTL_TO_GENERATE_TEXT(
  'How Oracle RAC maintains HA in Oracle 26ai',
  JSON('{
    "provider": "ollama",
    "host": "local",
    "url": "http://localhost:11434/api/generate",
    "model": "qwen2.5:7b",
    "transfer_timeout": 800
  }')
) AS qwen_output
FROM DUAL;
LLM response without RAG

As expected, the model pushed back — it had no idea “Oracle 26ai” was a real release, and its answer, while generally correct about Oracle RAC, was generic and hedged rather than grounded in any authoritative source:

“It seems there might be a typo or misunderstanding regarding ‘Oracle 26AI.’ There is no version of Oracle Database called ‘26,’ and AI isn’t typically part of the version naming convention...”

This confirmed the gap that RAG is designed to close: the model's parametric knowledge stops at its training cutoff, and it has no way to consult a current, authoritative document unless we give it one. So I set out to implement RAG directly in Oracle PL/SQL.

Implementing RAG in Oracle PL/SQL

STEP 1Create the Source Table for the PDF

A staging table to hold the raw PDF file as a BLOB:

CREATE TABLE pdf_documents (
  id        NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  file_name VARCHAR2(255),
  pdf_blob  BLOB
);
pdf_documents table created

STEP 2Load the PDF Into the Staging Table

Using a directory object on the database server, the Oracle Clusterware Administration and Deployment Guide is loaded into the BLOB column:

-- 1. Create a directory pointing to the folder containing the PDF
CREATE OR REPLACE DIRECTORY pdf_dir AS '/home/oracle/books';
Directory object created
-- 2. Load the file into the BLOB column
DECLARE
  l_bfile BFILE;
  l_blob  BLOB;
BEGIN
  INSERT INTO pdf_documents (file_name, pdf_blob)
  VALUES ('clusterware-administration-and-deployment-guide.pdf', EMPTY_BLOB())
  RETURNING pdf_blob INTO l_blob;
 
  l_bfile := BFILENAME('PDF_DIR', 'clusterware-administration-and-deployment-guide.pdf');
  DBMS_LOB.FILEOPEN(l_bfile, DBMS_LOB.FILE_READONLY);
  DBMS_LOB.LOADFROMFILE(l_blob, l_bfile, DBMS_LOB.GETLENGTH(l_bfile));
  DBMS_LOB.FILECLOSE(l_bfile);
  COMMIT;
END;
/
PDF loaded into BLOB column

STEP 3Create the Target Chunk & Vector Table

This table stores the segmented text chunks alongside the vector embeddings generated from the model:

CREATE TABLE vector_book_chunks (
  doc_id     NUMBER,
  chunk_id   NUMBER,
  book_name  VARCHAR2(50),
  chunk_data VARCHAR2(4000),
  embedding  VECTOR(384, FLOAT32)
) TABLESPACE users;
vector_book_chunks table created

STEP 4Extract, Chunk, Embed, and Populate

A single INSERT statement parses the PDF text, splits it into overlapping chunks, computes 384-dimensional embeddings via ALL_MINILM_L12_V2, and loads everything into the target table:

INSERT INTO system.vector_book_chunks (doc_id, chunk_id, book_name, chunk_data, embedding)
SELECT
  p.id,
  JSON_VALUE(c.column_value, '$.chunk_id' RETURNING NUMBER),
  'Clusterware Administration and Deployment Guide',
  JSON_VALUE(c.column_value, '$.chunk_data'),
  VECTOR_EMBEDDING(SYS.ALL_MINILM_L12_V2 USING JSON_VALUE(c.column_value, '$.chunk_data') AS data)
FROM system.pdf_documents p,
     DBMS_VECTOR.UTL_TO_CHUNKS(
       TO_CLOB(p.pdf_blob),
       JSON('{"by":"words", "max":"300", "overlap":"50", "split":"recursively"}')
     ) c
WHERE p.file_name = 'clusterware-administration-and-deployment-guide.pdf';
 
COMMIT;
Chunks and embeddings populated

STEP 5Retrieve, Augment, and Generate

With the vector store populated, this PL/SQL block runs the full RAG loop against the same question asked earlier: it embeds the prompt, retrieves the three most relevant chunks by cosine distance, builds a grounded context, and passes it to the LLM through DBMS_VECTOR.UTL_TO_GENERATE_TEXT:

SET SERVEROUTPUT ON SIZE UNLIMITED;
DECLARE
  l_prompt        VARCHAR2(4000) := 'How Oracle RAC maintains HA in Oracle 26ai?';
  l_context       CLOB := '';
  l_json_payload  CLOB;
  l_response_text CLOB;
BEGIN
  UTL_HTTP.set_transfer_timeout(1200);
 
  -- 1. Retrieve top matching chunks for the prompt
  FOR r IN (
    SELECT chunk_data
    FROM system.vector_book_chunks,
         (SELECT VECTOR_EMBEDDING(SYS.ALL_MINILM_L12_V2 USING l_prompt AS data) AS q_vec FROM DUAL) q
    WHERE book_name = 'Clusterware Administration and Deployment Guide'
    ORDER BY VECTOR_DISTANCE(embedding, q.q_vec, COSINE) ASC
    FETCH FIRST 3 ROWS ONLY
  ) LOOP
    l_context := l_context || CHR(10) || r.chunk_data;
  END LOOP;
 
  -- 2. Build the JSON provider configuration
  SELECT JSON_OBJECT(
           'provider' VALUE 'ollama',
           'host' VALUE 'local',
           'url' VALUE 'http://localhost:11434/api/generate',
           'model' VALUE 'qwen2.5:7b',
           'transfer_timeout' VALUE 1200,
           'stream' VALUE FALSE
           RETURNING CLOB
         ) INTO l_json_payload FROM DUAL;
 
  -- 3. Generate the grounded answer
  l_response_text := DBMS_VECTOR.UTL_TO_GENERATE_TEXT(
    'Using the following context, answer the question accurately.' || CHR(10) ||
    'Context:' || CHR(10) || l_context || CHR(10) ||
    'Question:' || CHR(10) || l_prompt,
    JSON(l_json_payload)
  );
 
  SYS.DBMS_OUTPUT.PUT_LINE('LLM Response:');
  SYS.DBMS_OUTPUT.PUT_LINE(l_response_text);
EXCEPTION
  WHEN OTHERS THEN
    SYS.DBMS_OUTPUT.PUT_LINE('Error Stack: ' || DBMS_UTILITY.format_error_stack);
    RAISE;
END;
/
RAG PL/SQL block

Execution output:

Execution output part 1
Execution output part 2

The Result

After implementing RAG, the same question produced a materially different answer. Rather than hedging on an unfamiliar term, the model drew directly on the retrieved chunks from the Clusterware Administration and Deployment Guide — referencing Clusterware components, RAC processes, and platform-specific configuration topics — and grounded its response in language pulled from the source documentation.

“Oracle 26AI” was no longer unknown to the LLM.

Scaling It Up: Adding a Vector Index

The walkthrough above uses a full scan over the embedding column, which is fine for a single PDF's worth of chunks. As the corpus grows, an HNSW vector index brings similarity search down from a linear scan to an approximate nearest-neighbor lookup:

CREATE VECTOR INDEX vector_book_chunks_hnsw_idx
  ON vector_book_chunks (embedding)
  ORGANIZATION INMEMORY NEIGHBOR GRAPH
  DISTANCE COSINE
  WITH TARGET ACCURACY 95
  PARAMETERS (type HNSW, neighbors 40, efConstruction 500)
  TABLESPACE users;
HNSW vector index created

With the index in place, the same retrieval query in Step 5 now resolves through an approximate nearest-neighbor graph lookup instead of a full scan of the embedding column — the query logic stays identical, only the execution path underneath changes.

Key Takeaways

  • Oracle Database 26ai can host an end-to-end RAG pipeline — embedding, chunking, vector storage, similarity search, and generation — natively in PL/SQL.
  • VECTOR_EMBEDDING and DBMS_VECTOR.UTL_TO_CHUNKS handle chunking and embedding generation directly against a BLOB, with no external ETL step.
  • VECTOR_DISTANCE with the VECTOR data type performs similarity search in-database, right alongside the relational data.
  • DBMS_VECTOR.UTL_TO_GENERATE_TEXT can call any compatible LLM provider — local (Ollama) or cloud — directly from SQL/PL-SQL, keeping the entire RAG loop inside the database boundary.
  • A native HNSW/IVF vector index — CREATE VECTOR INDEX — lets the same retrieval query scale from a full scan to an approximate nearest-neighbor lookup as the document corpus grows.

This was a hands-on proof of concept run end-to-end in a single PL/SQL session — happy to share more detail on the setup or discuss real-world use cases for in-database RAG.

#OracleDatabase #Oracle26ai #RAG #VectorSearch #PLSQL #OracleDBA #AI #GenerativeAI #Ollama

Comments

Popular posts from this blog

Strengthening Database Security with SQL Firewall in Oracle 26ai

MySQL Replication on Oracle Cloud’s “Always Free” Compute Instance

MySQL Installation on Oracle Cloud’s “Always Free” Compute Instance