Building AI-Powered Image Similarity Search in Oracle APEX using Cohere & Oracle 23ai Vector


🧠 Introduction

In this blog, we’ll build a real-time Image Similarity Search Application using:

  • Oracle APEX (UI)

  • Oracle 23ai VECTOR (AI storage)

  • Cohere Embed API (Image → Vector)

  • ORDS (Image streaming)

Instead of searching by text, users can upload an image and find visually similar images instantly.

πŸ—️ Architecture Overview

The system works in 5 simple steps:

  1. Upload image in APEX

  2. Convert image → vector using Cohere API

  3. Store vector in Oracle DB

  4. Compare vectors using cosine similarity

  5. Display matching images in classic report.

Step by Step Approaches 

πŸ—„️ Step 1: Create Database Tables

 CREATE TABLE "PRODUCT_IMAGES" 

   ( "ID" NUMBER GENERATED ALWAYS AS IDENTITY, 
"PRODUCT_NAME" VARCHAR2(200), 
"IMAGE_BLOB" BLOB, 
"IMAGE_MIME" VARCHAR2(100), 
"EMBEDDING" VECTOR (1536, FLOAT32), 
PRIMARY KEY ("ID")
  USING INDEX ENABLE
   );

CREATE TABLE "SEARCH_IMAGE" 

   ( "ID" NUMBER GENERATED ALWAYS AS IDENTITY, 

"PRODUCT_NAME" VARCHAR2(200), 

"IMAGE_BLOB" BLOB, 

"IMAGE_MIME" VARCHAR2(100), 

"EMBEDDING" VECTOR (1536, FLOAT32), 

PRIMARY KEY ("ID")

  USING INDEX ENABLE

   );

πŸ‘‰ VECTOR(1536) matches Cohere embed-v4.0 output.

πŸ”‘ Step 2: Get Cohere API Key

  • Go to Cohere Dashboard - https://dashboard.cohere.com/

  • Click API Keys option and Create API Key (Trial is enough)

πŸ–Ό️ Step 3: Upload & Store Product Image Embeddings

πŸ‘‰ This runs when uploading product images

Key Logic:

  • Read image from APEX temp table

  • Convert to Base64

  • Call Cohere API

  • Store embedding in DB

Steps:

Create a new page in your Oracle APEX application and add the following page items:

  • P2_PRODUCT_NAME – Text field to enter the product name

  • P_UPLOAD_IMAGE – File Browse item to upload the product image

  • P_STATUS – Display Only item to show success or error messages

Next, create a button named UPLOAD_IMAGE to trigger the upload process.

Finally, create a page process called Insert Image, which will handle storing the uploaded image along with its embedding into the database table.




Process code:

DECLARE
    l_mime         VARCHAR2(100);
    l_blob         BLOB;
    l_blob_req     BLOB;
    l_base64       CLOB;
    l_json_req     CLOB;
    l_json_resp    CLOB;
    l_raw_chunk    RAW(7500);
    l_b64_chunk    VARCHAR2(10000);
    l_offset       INTEGER := 1;
    l_chunk_size   INTEGER := 7500;
    l_amount       INTEGER;
    l_blob_len     INTEGER;
    l_dest_off     INTEGER := 1;
    l_src_off      INTEGER := 1;
    l_lang_ctx     INTEGER := DBMS_LOB.DEFAULT_LANG_CTX;
    l_warning      INTEGER;
    l_vector_str   CLOB;
    l_dim_count    INTEGER;
    l_val          VARCHAR2(50);
BEGIN
    -- Step 1: Fetch image
    SELECT blob_content, mime_type
    INTO   l_blob, l_mime
    FROM   (
        SELECT blob_content, mime_type
        FROM   APEX_APPLICATION_TEMP_FILES
        WHERE  name = :P_UPLOAD_IMAGE
        ORDER  BY created_on DESC
    )
    WHERE ROWNUM = 1;
IF l_mime LIKE 'image/jpg%' OR l_mime = 'image/pjpeg' THEN
    l_mime := 'image/jpeg';
END IF;
    l_blob_len := DBMS_LOB.GETLENGTH(l_blob);
    IF l_blob_len IS NULL OR l_blob_len = 0 THEN
        RAISE_APPLICATION_ERROR(-20001, 'Image BLOB is empty or NULL');
    END IF;
 
    -- Step 2: BLOB → Base64 CLOB (no newlines)
    DBMS_LOB.CREATETEMPORARY(l_base64, TRUE);
    l_base64 := APEX_WEB_SERVICE.BLOB2CLOBBASE64(l_blob);
     l_base64 := REPLACE(REPLACE(l_base64, CHR(10), ''), CHR(13), '');
    -- Step 3: Build JSON WITH data URI prefix (required by Cohere v2)
DBMS_LOB.CREATETEMPORARY(l_json_req, TRUE);
DBMS_LOB.APPEND(l_json_req, TO_CLOB('{"model":"embed-v4.0",'));
DBMS_LOB.APPEND(l_json_req, TO_CLOB('"input_type":"image",'));
DBMS_LOB.APPEND(l_json_req, TO_CLOB('"embedding_types":["float"],'));
DBMS_LOB.APPEND(l_json_req, TO_CLOB('"images":["'));
DBMS_LOB.APPEND(l_json_req, TO_CLOB('data:' || l_mime || ';base64,'));
DBMS_LOB.APPEND(l_json_req, l_base64);
DBMS_LOB.APPEND(l_json_req, TO_CLOB('"]}'));
 
    -- Step 4: Convert JSON CLOB → BLOB (avoids 32767 limit)
    DBMS_LOB.CREATETEMPORARY(l_blob_req, TRUE);
    DBMS_LOB.CONVERTTOBLOB(
        dest_lob     => l_blob_req,
        src_clob     => l_json_req,
        amount       => DBMS_LOB.LOBMAXSIZE,
        dest_offset  => l_dest_off,
        src_offset   => l_src_off,
        blob_csid    => DBMS_LOB.DEFAULT_CSID,
        lang_context => l_lang_ctx,
        warning      => l_warning
    );
 
    -- Step 5: Call Cohere API
    APEX_WEB_SERVICE.G_REQUEST_HEADERS.DELETE;
    APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).NAME  := 'Content-Type';
    APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).VALUE := 'application/json';
    APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).NAME  := 'Authorization';
    APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).VALUE := 'Bearer YuJE9V6b9FiWzpmyIYtTh7IiGuidOCd2I0m3YBca';
    l_json_resp := APEX_WEB_SERVICE.MAKE_REST_REQUEST
    (
        p_url         => 'https://api.cohere.com/v2/embed',
        p_http_method => 'POST',
        p_body_blob   => l_blob_req
    );
 
    -- Step 6: Show result
    IF APEX_WEB_SERVICE.G_STATUS_CODE = 200 THEN
        :P_STATUS := 'success';
    APEX_JSON.PARSE(l_json_resp);
 
        -- Cohere v2 response path: embeddings.float[1][n]
        l_dim_count := APEX_JSON.GET_COUNT(p_path => 'embeddings.float[1]');
        IF l_dim_count IS NULL OR l_dim_count = 0 THEN
            RAISE_APPLICATION_ERROR(-20002, 'No embedding dimensions found in response');
        END IF;
 
        -- Build vector string: '[v1,v2,...,v1024]'
        DBMS_LOB.CREATETEMPORARY(l_vector_str, TRUE);
        DBMS_LOB.APPEND(l_vector_str, TO_CLOB('['));
        FOR i IN 1 .. l_dim_count LOOP
            l_val := TO_CHAR(
                         APEX_JSON.GET_NUMBER(p_path => 'embeddings.float[1][%d]', p0 => i),
                         'FM999999990.9999999999'
                     );
            IF i > 1 THEN
                DBMS_LOB.APPEND(l_vector_str, TO_CLOB(','));
            END IF;
            DBMS_LOB.APPEND(l_vector_str, TO_CLOB(l_val));
        END LOOP;
        DBMS_LOB.APPEND(l_vector_str, TO_CLOB(']'));
 
        -- Insert into PRODUCT_IMAGES
        INSERT INTO PRODUCT_IMAGES (
            product_name,
            image_blob,
            image_mime,
            embedding
        ) VALUES (
            :P2_PRODUCT_NAME,                          -- bind your product name item
            l_blob,
            l_mime,
            TO_VECTOR(l_vector_str, 1536, FLOAT32)     -- Oracle 23ai VECTOR constructor
        );
        COMMIT;
 
        --:P2_QUERY_VECTOR := 'SUCCESS — Stored ' || l_dim_count || ' dimensions, ID: ';
 
        DBMS_LOB.FREETEMPORARY(l_vector_str);
    else
     :P_STATUS := 'HTTP ' || APEX_WEB_SERVICE.G_STATUS_CODE|| ' | ' || SUBSTR(l_json_resp, 1,3000);
    END IF;
 
    -- Step 7: Cleanup
    DBMS_LOB.FREETEMPORARY(l_base64);
    DBMS_LOB.FREETEMPORARY(l_json_req);
    DBMS_LOB.FREETEMPORARY(l_blob_req);
 
EXCEPTION
    WHEN OTHERS THEN
        :P_STATUS  := 'ERROR [' || SQLCODE || ']: ' || SQLERRM
                            || ' | ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE();
END;

πŸ” Step 4: Store Search Image Embedding 

What it does:

  • Reads uploaded image

  • Converts to Base64

  • Calls Cohere API

  • Extracts 1536 vector values

  • Stores in SEARCH_IMAGE

Steps:

Create a new page in your Oracle APEX application and add the following page items:

  • P4_UPLOAD_IMAGE – File Browse item to upload the search image

  • P4_UPLOADED_IMAGE – Display item to preview the uploaded image

  • P_SEARCH_RESULTS – Hidden item to store the JSON response of similar images

  • P4_STATUS – Display Only item to show success or error messages

Next, create a button named SEARCH_SIMILAR_IMAGE to trigger the search process.

Add a Classic Report region to display the similar images retrieved from the database.

Finally, create two page processes:

  1. Insert Image Process – Converts the uploaded image into a vector using the Cohere API and stores it in the SEARCH_IMAGE table

  2. Search Similar Images Process – Compares the uploaded image vector with existing product images and returns the top matching results



Classic Report Code:

SELECT
    ranked.rank#,
    ranked.product_id                              AS id,
    ranked.product_name,
    ranked.similarity                              AS similarity_pct,
    '<div style="text-align:center;">'
    || '<img src="'
    || 'https://oracleapex.com/ords/sabarinath/product-images/'
    || ranked.product_id
    || '" style="width:300px;height:200px;object-fit:scale-down;'
    || 'border-radius:8px;border:1px solid #ddd;" />'
    || '<br/><b>' || ranked.product_name || '</b>'
    || '<br/>Match: <span style="color:green;font-weight:bold;">'
    || ranked.similarity || '%</span>'
    || '</div>'                                    AS image_html
FROM (
    SELECT
        ROWNUM                        AS rank#,
        TO_NUMBER(jt.product_id)      AS product_id,
        jt.product_name,
        TO_NUMBER(jt.similarity)      AS similarity
    FROM
        JSON_TABLE(
            :P_SEARCH_RESULTS,
            '$[*]'
            COLUMNS (
                product_id   VARCHAR2(20)  PATH '$.product_id',
                product_name VARCHAR2(200) PATH '$.product_name',
                mime_type    VARCHAR2(100) PATH '$.mime_type',
                similarity   VARCHAR2(20)  PATH '$.similarity'
            )
        ) jt
    ORDER BY TO_NUMBER(jt.similarity) DESC
) ranked
JOIN PRODUCT_IMAGES pi
    ON pi.id = ranked.product_id
WHERE ranked.similarity > 60

Insert image Process Code:

DECLARE
    l_mime         VARCHAR2(100);
    l_blob         BLOB;
    l_blob_req     BLOB;
    l_base64       CLOB;
    l_json_req     CLOB;
    l_json_resp    CLOB;
    l_raw_chunk    RAW(7500);
    l_b64_chunk    VARCHAR2(10000);
    l_offset       INTEGER := 1;
    l_chunk_size   INTEGER := 7500;
    l_amount       INTEGER;
    l_blob_len     INTEGER;
    l_dest_off     INTEGER := 1;
    l_src_off      INTEGER := 1;
    l_lang_ctx     INTEGER := DBMS_LOB.DEFAULT_LANG_CTX;
    l_warning      INTEGER;
    l_vector_str   CLOB;
    l_dim_count    INTEGER;
    l_val          VARCHAR2(50);
BEGIN
    -- Step 1: Fetch image
    SELECT blob_content, mime_type
    INTO   l_blob, l_mime
    FROM   (
        SELECT blob_content, mime_type
        FROM   APEX_APPLICATION_TEMP_FILES
        WHERE  name = :P4_UPLOAD_IMAGE
        ORDER  BY created_on DESC
    )
    WHERE ROWNUM = 1;
IF l_mime LIKE 'image/jpg%' OR l_mime = 'image/pjpeg' THEN
    l_mime := 'image/jpeg';
END IF;
    l_blob_len := DBMS_LOB.GETLENGTH(l_blob);
    IF l_blob_len IS NULL OR l_blob_len = 0 THEN
        RAISE_APPLICATION_ERROR(-20001, 'Image BLOB is empty or NULL');
    END IF;
 
    -- Step 2: BLOB → Base64 CLOB (no newlines)
    DBMS_LOB.CREATETEMPORARY(l_base64, TRUE);
    l_base64 := APEX_WEB_SERVICE.BLOB2CLOBBASE64(l_blob);
     l_base64 := REPLACE(REPLACE(l_base64, CHR(10), ''), CHR(13), '');
    -- Step 3: Build JSON WITH data URI prefix (required by Cohere v2)
DBMS_LOB.CREATETEMPORARY(l_json_req, TRUE);
DBMS_LOB.APPEND(l_json_req, TO_CLOB('{"model":"embed-v4.0",'));
DBMS_LOB.APPEND(l_json_req, TO_CLOB('"input_type":"image",'));
DBMS_LOB.APPEND(l_json_req, TO_CLOB('"embedding_types":["float"],'));
DBMS_LOB.APPEND(l_json_req, TO_CLOB('"images":["'));
DBMS_LOB.APPEND(l_json_req, TO_CLOB('data:' || l_mime || ';base64,'));
DBMS_LOB.APPEND(l_json_req, l_base64);
DBMS_LOB.APPEND(l_json_req, TO_CLOB('"]}'));
 
    -- Step 4: Convert JSON CLOB → BLOB (avoids 32767 limit)
    DBMS_LOB.CREATETEMPORARY(l_blob_req, TRUE);
    DBMS_LOB.CONVERTTOBLOB(
        dest_lob     => l_blob_req,
        src_clob     => l_json_req,
        amount       => DBMS_LOB.LOBMAXSIZE,
        dest_offset  => l_dest_off,
        src_offset   => l_src_off,
        blob_csid    => DBMS_LOB.DEFAULT_CSID,
        lang_context => l_lang_ctx,
        warning      => l_warning
    );
 
    -- Step 5: Call Cohere API
    APEX_WEB_SERVICE.G_REQUEST_HEADERS.DELETE;
    APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).NAME  := 'Content-Type';
    APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).VALUE := 'application/json';
    APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).NAME  := 'Authorization';
    APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).VALUE := 'Bearer YuJE9V6b9FiWzpmyIYtTh7IiGuidOCd2I0m3YBca';
 
    l_json_resp := APEX_WEB_SERVICE.MAKE_REST_REQUEST
    (
        p_url         => 'https://api.cohere.com/v2/embed',
        p_http_method => 'POST',
        p_body_blob   => l_blob_req
    );
 
    -- Step 6: Show result
    IF APEX_WEB_SERVICE.G_STATUS_CODE = 200 THEN
        :P_STATUS := 'success';
    APEX_JSON.PARSE(l_json_resp);
 
        -- Cohere v2 response path: embeddings.float[1][n]
        l_dim_count := APEX_JSON.GET_COUNT(p_path => 'embeddings.float[1]');
 
        IF l_dim_count IS NULL OR l_dim_count = 0 THEN
            RAISE_APPLICATION_ERROR(-20002, 'No embedding dimensions found in response');
        END IF;
 
        -- Build vector string: '[v1,v2,...,v1024]'
        DBMS_LOB.CREATETEMPORARY(l_vector_str, TRUE);
        DBMS_LOB.APPEND(l_vector_str, TO_CLOB('['));
 
        FOR i IN 1 .. l_dim_count LOOP
            l_val := TO_CHAR(
                         APEX_JSON.GET_NUMBER(p_path => 'embeddings.float[1][%d]', p0 => i),
                         'FM999999990.9999999999'
                     );
            IF i > 1 THEN
                DBMS_LOB.APPEND(l_vector_str, TO_CLOB(','));
            END IF;
            DBMS_LOB.APPEND(l_vector_str, TO_CLOB(l_val));
        END LOOP;
        delete from search_image;
        DBMS_LOB.APPEND(l_vector_str, TO_CLOB(']'));
 
        -- Insert into PRODUCT_IMAGES
        INSERT INTO SEARCH_IMAGE (
            product_name,
            image_blob,
            image_mime,
            embedding
        ) VALUES (
            'Search_image',                          -- bind your product name item
            l_blob,
            l_mime,
            TO_VECTOR(l_vector_str, 1536, FLOAT32)     -- Oracle 23ai VECTOR constructor
        );
 
        COMMIT;
 
        --:P7_QUERY_VECTOR := 'SUCCESS — Stored ' || l_dim_count || ' dimensions, ID: ';
 
        DBMS_LOB.FREETEMPORARY(l_vector_str);
 
    else
 
     :P4_STATUS := 'HTTP ' || APEX_WEB_SERVICE.G_STATUS_CODE|| ' | ' || SUBSTR(l_json_resp, 1,3000);
 
        
    END IF;
 
    -- Step 7: Cleanup
    DBMS_LOB.FREETEMPORARY(l_base64);
    DBMS_LOB.FREETEMPORARY(l_json_req);
    DBMS_LOB.FREETEMPORARY(l_blob_req);
 
EXCEPTION
    WHEN OTHERS THEN
        :P4_STATUS  := 'ERROR [' || SQLCODE || ']: ' || SQLERRM
                            || ' | ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE();
END;

πŸ”Ž Step 5: Search Similar Images (Vector Comparison)

 This is your second process in same page.

Search Similiar Images Process Code:

DECLARE
    l_search_vector    VECTOR(1536, FLOAT32);
    l_result_json      CLOB;
BEGIN
    -- Step 1: Get the latest (or specific) search image vector
    SELECT embedding
    INTO   l_search_vector
    FROM   (
        SELECT embedding
        FROM   SEARCH_IMAGE
        ORDER  BY id DESC
    )
    WHERE ROWNUM = 1;
 
    -- Step 2: Build JSON result of top-N matches (for APEX display)
    SELECT JSON_ARRAYAGG(
               JSON_OBJECT(
                   'product_id'   VALUE p.id,
                   'product_name' VALUE p.product_name,
                   'mime_type'    VALUE p.image_mime,
                   'similarity'   VALUE ROUND(
                                      (1 - VECTOR_DISTANCE(p.embedding, l_search_vector, COSINE)) * 100,
                                     2
                                  )
               )
               ORDER BY VECTOR_DISTANCE(p.embedding, l_search_vector, COSINE) ASC
           )
    INTO  l_result_json
    FROM  PRODUCT_IMAGES p
    WHERE p.embedding IS NOT NULL
      AND ROWNUM <= 20;   -- top 5 matches
 
    -- Step 3: Bind to APEX page item (e.g. a Hidden or Display item)
    :P_SEARCH_RESULTS := l_result_json;
    :P4_STATUS         := 'success';
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        :P4_STATUS := 'No search vector found.';
    WHEN OTHERS THEN
        :P4_STATUS := 'ERROR [' || SQLCODE || ']: ' || SQLERRM
                     || ' | ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE();
END;

Logic:

  • Get latest search vector

  • Compare with all product vectors

  • Return top matches as JSON

πŸ‘‰ Output stored in:

:P_SEARCH_RESULTS

πŸ“Š Step 6: Display Results (Classic Report)

Use this concept:

  • Parse JSON using JSON_TABLE

  • Show:

    • Image

    • Product Name

    • Similarity %

Similarity Formula:

Similarity % = (1 - cosine distance) * 100

🌐 Step 7: ORDS Image Rendering (Step-by-Step)

To display images without session issues in Oracle APEX, we use ORDS REST Services to stream

images directly from the database.

πŸ”§ Step 7.1: Create REST Module

  1. Go to SQL Workshop → RESTful Services

  2. Click Modules → Create Module

  3. Enter:

  • Module Name: product-images

  • Base Path: /product-images/

  • Status: Published

πŸ‘‰ Click Create Module

πŸ”§ Step 7.2: Create Resource Template

  1. Open your module: product-images

  2. Click Create Template

  3. Enter:

  • URI Template: :id

πŸ‘‰ Click Create Template

πŸ”§ Step 7.3: Create Resource Handler

  1. Inside the template, click Create Handler

  2. Configure:

  • Method: GET

  • Source Type: Media Resource

  • Format: Binary Representation

🧾 Step 7.4: Add SQL Query (Handler Source)

Paste the below SQL:

SELECT image_mime AS content_type,
image_blob AS content
FROM PRODUCT_IMAGES
WHERE id =:id

πŸ‘‰ This tells ORDS:

  • content_type → MIME type (image/jpeg, image/png)

  • content → actual image BLOB

πŸ‘‰ This avoids:

  • Session issues

  • Broken images

  • Performance problems

✅ Step 7.5: Apply Changes

  • Click Apply Changes

  • Ensure module is Published

🌍 Step 7.6: Test URL

Use this format:
https://oracleapex.com/ords/{workspace}/product-images/{id}
Example:
https://oracleapex.com/ords/sabarinath/product-images/1
πŸ‘‰ It should directly display the image in browser

⚙️ Step 8: APEX Page Configuration

Page 2 (Product Upload)

  • Product Name

  • File Upload

  • Process → Store embedding

Page 4 (Search Image)

  • File Upload

  • Button: Search Similar Image

Processes:

  1. Insert Image (Embedding)

  2. Search Similar Images

  3. Refresh Report

🎯 Step 9: Final Output

User flow:

  1. Upload image

  2. Click Search Similar Image

  3. See results like:

  • Car → Similar Cars

  • Bike → Similar Bikes



πŸš€ Advanced Improvements

You can enhance this project by adding:

  • πŸ” Similarity slider (dynamic threshold)

  • πŸ“‚ Category-based filtering

  • ⚡ Vector Index for performance

  • πŸ€– Multi-modal search (text + image)

🏁 Conclusion

We successfully built an AI-powered Image Similarity Search system in Oracle APEX using:

  • Cohere for embeddings

  • Oracle 23ai VECTOR for storage

  • APEX for UI

This is a powerful real-world use case of AI inside Oracle applications.

Comments

Popular posts from this blog

Building Secure RESTful Services in Oracle APEX 24.2 Using ORDS, OAuth2 Client Credentials, and PL/SQL

Building a Portfolio Generator in Oracle APEX: A Step-by-Step Guide to Generate Downloadable Portfolio Documents

Implementing WhatsApp OTP Verification in Oracle APEX Using UltraMsg API