Skip to content

Commit f1bd2e2

Browse files
committed
Merge article/03-vector-vs-converged: module 03 lab (10K corpus, IVF filter-first proof, HVI)
# Conflicts: # labs/converged-database-lab/docker/init/08-vector-model.sql
2 parents 386532b + 7b83d25 commit f1bd2e2

13 files changed

Lines changed: 1129 additions & 4 deletions

File tree

labs/converged-database-lab/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ validator/results.json
55
validator/.venv/
66
docker/.data/
77
*.pyc
8+
tasks/

labs/converged-database-lab/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# converged-database-lab
22

3-
![validate](https://github.com/oracle-devrel/oracle-umt-developer-hub/actions/workflows/converged-database-lab.yml/badge.svg)
3+
![validate](https://github.com/rhoulihan/converged-database-lab/actions/workflows/validate.yml/badge.svg)
44

55
Runnable proofs for the converged-database article series — every technical claim in the articles executes here, nightly, against a free container.
66

labs/converged-database-lab/docker-compose.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ services:
1717
test: ['CMD-SHELL', '/opt/oracle/healthcheck.sh && curl -so /dev/null -w "%{http_code}" http://localhost:8181/ | grep -q "302\|200"']
1818
interval: 15s
1919
timeout: 10s
20-
retries: 40
20+
# 80 retries: first boot now embeds 10,000 ticket bodies in-database and
21+
# builds the HVI over them (~7-9 min of init on top of DB create), so the
22+
# container must not be declared unhealthy before init can finish.
23+
retries: 80
2124
start_period: 120s
2225
deploy:
2326
resources:

labs/converged-database-lab/docker/Dockerfile.oracle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ RUN chown -R oracle:oinstall /etc/ords
99

1010
# mongosh: used to verify and exercise the MongoDB API (port 27017) from inside
1111
# the container — the validator harness runs *.js scripts through it.
12-
RUN printf '[mongodb-org-8.0]\nname=MongoDB Repository\nbaseurl=https://repo.mongodb.org/yum/redhat/8/mongodb-org/8.0/x86_64/\ngpgcheck=1\nenabled=1\ngpgkey=https://pgp.mongodb.com/server-8.0.asc\n' > /etc/yum.repos.d/mongodb-org-8.0.repo \
12+
# Arch-aware repo URL: CI builds on x86_64; local builds on Apple-silicon (aarch64)
13+
# podman machines get MongoDB's aarch64 el8 packages instead of failing dependency
14+
# resolution against the x86_64 repo.
15+
RUN printf '[mongodb-org-8.0]\nname=MongoDB Repository\nbaseurl=https://repo.mongodb.org/yum/redhat/8/mongodb-org/8.0/%s/\ngpgcheck=1\nenabled=1\ngpgkey=https://pgp.mongodb.com/server-8.0.asc\n' "$(uname -m)" > /etc/yum.repos.d/mongodb-org-8.0.repo \
1316
&& microdnf install -y mongodb-mongosh \
1417
&& microdnf clean all
1518

labs/converged-database-lab/docker/init/06-seed.sql

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ ALTER SESSION SET CURRENT_SCHEMA = lab_user;
44
DECLARE
55
-- All item declarations must precede subprogram bodies (PLS-00103 otherwise).
66
seed NUMBER := 42;
7+
-- Ticket volume. 10,000 tickets is deliberately past the point where the
8+
-- cost-based optimizer chooses the IVF vector index over exact full-scan
9+
-- distance for module 03's filtered FETCH APPROX query (the crossover
10+
-- arrives by roughly 3,000 rows on the Free container with fresh stats) —
11+
-- so the captured plan shows the index + in-plan predicate evaluation, not
12+
-- a toy-size full scan.
13+
c_tickets CONSTANT PLS_INTEGER := 10000;
714
v_vec VARCHAR2(400);
815
v_norm NUMBER;
916
TYPE t_f IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
@@ -18,13 +25,71 @@ DECLARE
1825
v_prod PLS_INTEGER;
1926
v_qty PLS_INTEGER;
2027
v_ts TIMESTAMP;
28+
v_cat PLS_INTEGER;
29+
v_body VARCHAR2(2000);
30+
-- Enriched ticket-body corpus for tickets 301..c_tickets: 6 intros x 8 issue
31+
-- categories x 4 details x 50 products x 5 closers ~= 48k distinct bodies, so
32+
-- the 384-dim embeddings form real semantic clusters instead of 4 duplicated
33+
-- points (matters for a meaningful IVF index and hybrid-search demo).
34+
TYPE t_v IS TABLE OF VARCHAR2(400);
35+
intro t_v := t_v('Customer reports an issue with their recent experience.',
36+
'Contacting support after several attempts to resolve this alone.',
37+
'Opening a ticket on behalf of the account holder.',
38+
'Follow-up on an earlier conversation with the support team.',
39+
'New request submitted through the help center.',
40+
'Escalated from chat: the problem is still not resolved.');
41+
refund t_v := t_v('The package arrived damaged and a refund is requested.',
42+
'The box was crushed in transit and the item inside is damaged beyond use.',
43+
'Item arrived with a cracked casing; customer asks for their money back.',
44+
'Shipment was left in the rain and the contents are damaged and unusable.');
45+
login t_v := t_v('Login fails with an authentication error after reset.',
46+
'Password reset completed but sign-in still rejects the new credentials.',
47+
'Two-factor prompt never arrives and access to the account is blocked.',
48+
'Account is locked out after the security update and recovery does not work.');
49+
shipdl t_v := t_v('The order has not arrived within the promised window.',
50+
'Tracking has shown no movement for a week and the delivery date passed.',
51+
'Carrier marked the parcel delivered but nothing has arrived.',
52+
'Estimated delivery keeps slipping and no update has been provided.');
53+
warr t_v := t_v('Customer asks whether the warranty covers accidental damage.',
54+
'Requesting warranty service for a unit that stopped powering on.',
55+
'Screen developed dead pixels within the coverage period.',
56+
'Asking how to start a warranty claim for a failed component.');
57+
billing t_v := t_v('The statement shows a duplicate charge for a single order.',
58+
'A promotional discount was not applied at checkout.',
59+
'The invoice total does not match the order confirmation email.',
60+
'Customer was billed after cancelling the subscription.');
61+
exch t_v := t_v('Requesting an exchange for a different size of the same product.',
62+
'The wrong color variant was delivered and a swap is requested.',
63+
'Received a different model than the one ordered.',
64+
'Asking to exchange a gift for store credit instead.');
65+
install t_v := t_v('The setup guide does not match the ports on the device.',
66+
'Firmware update fails at forty percent every attempt.',
67+
'The companion app cannot discover the device during pairing.',
68+
'Installation completes but the device reboots continuously.');
69+
perf t_v := t_v('The device becomes very slow after a few hours of use.',
70+
'Battery drains overnight even when the unit is idle.',
71+
'Audio cuts out intermittently during playback.',
72+
'The connection drops whenever a second device joins.');
73+
closing t_v := t_v('Customer asks for a response today.',
74+
'This is the second time the issue has been reported.',
75+
'The customer mentions they are a long-time subscriber.',
76+
'A callback number has been left on the account.',
77+
'No workaround has been found so far.');
2178
FUNCTION nxt RETURN NUMBER IS -- LCG: deterministic across runs
2279
BEGIN
2380
seed := MOD(seed * 1103515245 + 12345, 2147483648);
2481
RETURN seed / 2147483648;
2582
END;
2683
FUNCTION pick(n IN PLS_INTEGER) RETURN PLS_INTEGER IS
2784
BEGIN RETURN TRUNC(nxt * n) + 1; END;
85+
FUNCTION cat_detail(p_cat PLS_INTEGER) RETURN VARCHAR2 IS
86+
BEGIN
87+
RETURN CASE p_cat
88+
WHEN 1 THEN refund (pick(4)) WHEN 2 THEN login(pick(4))
89+
WHEN 3 THEN shipdl (pick(4)) WHEN 4 THEN warr (pick(4))
90+
WHEN 5 THEN billing(pick(4)) WHEN 6 THEN exch (pick(4))
91+
WHEN 7 THEN install(pick(4)) ELSE perf(pick(4)) END;
92+
END;
2893
BEGIN
2994
FOR i IN 1..200 LOOP
3095
INSERT INTO customers (email, full_name, segment)
@@ -95,6 +160,9 @@ BEGIN
95160
UPDATE orders SET total_amount = ROUND(v_total,2) WHERE order_id = i;
96161
END LOOP;
97162

163+
-- Tickets 1..300: the ORIGINAL 4-template corpus, byte-for-byte (module 01's
164+
-- ticket #1 proofs and module 03's read-after-write probe depend on ticket #1
165+
-- keeping its "Login fails with an authentication error after reset." body).
98166
FOR i IN 1..300 LOOP
99167
v_norm := 0;
100168
FOR d IN 1..8 LOOP f(d) := nxt*2 - 1; v_norm := v_norm + f(d)*f(d); END LOOP;
@@ -119,6 +187,38 @@ BEGIN
119187
CASE WHEN MOD(i,3)=0 THEN 'closed' WHEN MOD(i,7)=0 THEN 'pending' ELSE 'open' END,
120188
TO_VECTOR(v_vec, 8, FLOAT32));
121189
END LOOP;
190+
191+
-- Tickets 301..c_tickets: the enriched scaled corpus. The LCG is re-seeded to
192+
-- a fixed value here so this block is deterministic regardless of how the
193+
-- upstream seeding evolves — same bodies on every fresh build.
194+
seed := 342;
195+
FOR i IN 301..c_tickets LOOP
196+
v_cat := pick(8);
197+
v_cust := pick(200);
198+
v_body := intro(pick(6))||' '||cat_detail(v_cat)||
199+
' Relates to Product '||pick(50)||'. '||closing(pick(5));
200+
v_norm := 0;
201+
FOR d IN 1..8 LOOP f(d) := nxt*2 - 1; v_norm := v_norm + f(d)*f(d); END LOOP;
202+
v_norm := SQRT(v_norm);
203+
v_vec := '[';
204+
FOR d IN 1..8 LOOP
205+
v_vec := v_vec || TO_CHAR(ROUND(f(d)/v_norm, 6),'FM990.999999') || CASE WHEN d<8 THEN ',' END;
206+
END LOOP;
207+
v_vec := v_vec || ']';
208+
INSERT INTO support_tickets (customer_id, subject, body, status, embedding)
209+
VALUES (v_cust,
210+
CASE v_cat WHEN 1 THEN 'Refund request for damaged item'
211+
WHEN 2 THEN 'Cannot sign in after password reset'
212+
WHEN 3 THEN 'Shipping delay on recent order'
213+
WHEN 4 THEN 'Warranty question for product'
214+
WHEN 5 THEN 'Billing discrepancy on statement'
215+
WHEN 6 THEN 'Exchange request for recent order'
216+
WHEN 7 THEN 'Device setup and installation issue'
217+
ELSE 'Performance problem with product' END || ' #'||i,
218+
v_body,
219+
CASE WHEN MOD(i,3)=0 THEN 'closed' WHEN MOD(i,7)=0 THEN 'pending' ELSE 'open' END,
220+
TO_VECTOR(v_vec, 8, FLOAT32));
221+
END LOOP;
122222
COMMIT;
123223
END;
124224
/
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
ALTER SESSION SET CONTAINER = FREEPDB1;
2+
3+
-- In-database embeddings infrastructure (foundational; used by the AI articles).
4+
-- Runs once on first boot (gvenzl initdb), after 01-07. Loads Oracle's prebuilt
5+
-- augmented all-MiniLM-L12-v2 ONNX model (baked into the image at /opt/oracle/models
6+
-- by docker/Dockerfile.oracle), embeds all 10,000 ticket bodies into a real
7+
-- VECTOR(384) column, builds an IVF vector index on it, and gathers optimizer
8+
-- stats so the CBO's index-vs-scan choice is deterministic. Idempotent: every
9+
-- CREATE is guarded so a recreated container over a persistent volume re-runs
10+
-- cleanly.
11+
--
12+
-- Grants used by the AI modules beyond the base 01-lab-user.sql set:
13+
-- CREATE MINING MODEL — DBMS_VECTOR.LOAD_ONNX_MODEL registers an ONNX model
14+
-- READ ON ONNX_MODELS — read the baked .onnx from the image directory
15+
-- EXECUTE DBMS_VECTOR — VECTOR_EMBEDDING / model load
16+
-- EXECUTE DBMS_RLS — VPD policies (permission-aware retrieval proofs)
17+
-- CREATE/DROP ANY CONTEXT — application context for VPD tenant/user predicates
18+
GRANT CREATE MINING MODEL TO lab_user;
19+
GRANT CREATE ANY CONTEXT TO lab_user;
20+
GRANT DROP ANY CONTEXT TO lab_user;
21+
GRANT EXECUTE ON DBMS_VECTOR TO lab_user;
22+
GRANT EXECUTE ON DBMS_RLS TO lab_user;
23+
24+
-- Directory over the baked model path; READ for lab_user so the load can open it.
25+
CREATE OR REPLACE DIRECTORY ONNX_MODELS AS '/opt/oracle/models';
26+
GRANT READ ON DIRECTORY ONNX_MODELS TO lab_user;
27+
28+
ALTER SESSION SET CURRENT_SCHEMA = lab_user;
29+
30+
-- Load the model into the lab_user schema as MINILM_L12 so VECTOR_EMBEDDING
31+
-- (MINILM_L12 USING <text> AS data) resolves for lab_user. The augmented model
32+
-- carries its own tokenizer + pooling, so the metadata only needs to declare the
33+
-- embedding function and the input attribute name (DATA, matching the AS data
34+
-- alias used everywhere downstream). DROP first for idempotent re-runs.
35+
DECLARE
36+
v_model_path VARCHAR2(400);
37+
BEGIN
38+
BEGIN
39+
DBMS_VECTOR.DROP_ONNX_MODEL(model_name => 'MINILM_L12', force => TRUE);
40+
EXCEPTION WHEN OTHERS THEN NULL; /* not yet loaded — nothing to drop */
41+
END;
42+
DBMS_VECTOR.LOAD_ONNX_MODEL(
43+
directory => 'ONNX_MODELS',
44+
file_name => 'all_MiniLM_L12_v2.onnx',
45+
model_name => 'MINILM_L12',
46+
metadata => JSON('{"function":"embedding","embeddingOutput":"embedding","input":{"input":["DATA"]}}'));
47+
END;
48+
/
49+
50+
-- Real 384-dim embedding column. The module-01 VECTOR(8) `embedding` column and
51+
-- its IVF index (ticket_vec_idx) are LEFT UNTOUCHED. Guarded so a recreated
52+
-- container does not error on the already-present column.
53+
DECLARE
54+
v_exists NUMBER;
55+
BEGIN
56+
SELECT COUNT(*) INTO v_exists FROM user_tab_columns
57+
WHERE table_name = 'SUPPORT_TICKETS' AND column_name = 'BODY_VEC';
58+
IF v_exists = 0 THEN
59+
EXECUTE IMMEDIATE 'ALTER TABLE support_tickets ADD (body_vec VECTOR(384, FLOAT32))';
60+
END IF;
61+
END;
62+
/
63+
64+
-- Embed all 10,000 ticket bodies in-database — no external embedding service, no
65+
-- copy pipeline. ~3 min on the Free container (~55 embeddings/s, single-threaded).
66+
-- Re-runnable: recomputes from body.
67+
UPDATE support_tickets SET body_vec = VECTOR_EMBEDDING(MINILM_L12 USING body AS data);
68+
69+
COMMIT;
70+
71+
-- IVF (NEIGHBOR PARTITIONS) vector index on the real 384-dim column. IVF needs
72+
-- NO Vector Pool / VECTOR_MEMORY_SIZE, so it builds within the Free container's
73+
-- SGA. Guarded for idempotent re-runs.
74+
DECLARE
75+
BEGIN
76+
EXECUTE IMMEDIATE 'DROP INDEX tickets_bodyvec_ivf';
77+
EXCEPTION WHEN OTHERS THEN NULL; /* absent — first run */
78+
END;
79+
/
80+
CREATE VECTOR INDEX tickets_bodyvec_ivf ON support_tickets(body_vec)
81+
ORGANIZATION NEIGHBOR PARTITIONS
82+
DISTANCE COSINE;
83+
84+
-- Fresh optimizer statistics on the tables module 03's filtered-ANN plan spans.
85+
-- Load-bearing for determinism: without stats the CBO's dynamic sampling can pick
86+
-- the IVF plan even at toy sizes; with stats the choice is honestly cost-based —
87+
-- full-scan exact distance at hundreds of rows, the IVF index + in-plan filter
88+
-- predicates at 10,000 (the crossover arrives by roughly 3,000 rows on the
89+
-- Free container).
90+
-- (schema named explicitly: this script runs as SYS with CURRENT_SCHEMA=lab_user,
91+
-- and DBMS_STATS resolves USER to the logon user, not the current schema)
92+
BEGIN
93+
DBMS_STATS.GATHER_TABLE_STATS('LAB_USER', 'SUPPORT_TICKETS');
94+
DBMS_STATS.GATHER_TABLE_STATS('LAB_USER', 'CUSTOMERS');
95+
DBMS_STATS.GATHER_TABLE_STATS('LAB_USER', 'ORDERS');
96+
END;
97+
/
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
ALTER SESSION SET CONTAINER = FREEPDB1;
2+
ALTER SESSION SET CURRENT_SCHEMA = lab_user;
3+
4+
-- Hybrid Vector Index on the ticket body: one domain index unifying Oracle Text
5+
-- (keyword) and a vector index (semantic) over the same column, queried via
6+
-- DBMS_HYBRID_VECTOR.SEARCH with RRF/score fusion (module 03, 04-hybrid-search.sql).
7+
-- Leaf-3-specific: the foundational model + body_vec + IVF live in 08 (on main);
8+
-- this HVI is the article-3 hybrid-search showcase and stays on this branch.
9+
--
10+
-- COLLISION NOTE: a CONTEXT/SEARCH text index and the HVI's internal text
11+
-- component are the SAME indextype, so two cannot sit on the same column
12+
-- (ORA-29879). The shared 05-text-vector.sql creates ticket_text_idx (a SEARCH
13+
-- index) on body; this module needs the richer body text for a meaningful hybrid
14+
-- proof, so we DROP ticket_text_idx here and let the HVI own body's text search.
15+
-- The HVI exposes the same CONTAINS(body, ...) capability, so keyword-only search
16+
-- still works. (On the eventual merge with the module-02 branch, whose
17+
-- read-after-write SEARCH proof uses ticket_text_idx, that proof moves to the HVI
18+
-- or to the subject column — a merge-time decision.)
19+
--
20+
-- VECTOR_IDXTYPE IVF keeps the HVI's vector component off the Vector Pool
21+
-- (Free-safe); MEMORY 256M caps the Oracle Text build memory modestly. Builds in
22+
-- ~6s on the Free container.
23+
DECLARE
24+
BEGIN
25+
EXECUTE IMMEDIATE 'DROP INDEX tickets_hvi';
26+
EXCEPTION WHEN OTHERS THEN NULL; /* absent — first run */
27+
END;
28+
/
29+
DECLARE
30+
BEGIN
31+
EXECUTE IMMEDIATE 'DROP INDEX ticket_text_idx';
32+
EXCEPTION WHEN OTHERS THEN NULL; /* HVI takes over body text search */
33+
END;
34+
/
35+
CREATE HYBRID VECTOR INDEX tickets_hvi ON support_tickets(body)
36+
PARAMETERS('MODEL MINILM_L12 VECTOR_IDXTYPE IVF MEMORY 256M');

0 commit comments

Comments
 (0)