Skip to content

Commit 7b83d25

Browse files
rhoulihanclaude
andcommitted
labs/converged-database-lab: module 03 — 10K-ticket corpus, IVF filter-first plan proof (import from working repo @bd039cc)
- 10,000-ticket deterministic seed (tickets 1-300 byte-preserved; 301+ from a fixed-LCG phrase grid) — past the CBO's index crossover (~3,000 rows with fresh stats), so proof 1 captures the IVF vector index driving the plan - proof 1: DBMS_XPLAN 'BASIC +PREDICATE' exhibit + 4 new assertions proving the relational predicates evaluate on their scans below the top-10 stopkey (filter first, then rank); module now 23 assertions - init gathers optimizer stats explicitly (deterministic index-vs-scan choice) - arch-aware mongosh install (aarch64 podman builds); CONTAINER_ENGINE env in the validator; healthcheck window widened for the longer first-boot init - validated: cold builds green on aarch64 podman and x86_64 CI (43 assertions) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent ac7a1e0 commit 7b83d25

10 files changed

Lines changed: 356 additions & 82 deletions

File tree

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
/

labs/converged-database-lab/docker/init/08-vector-model.sql

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ ALTER SESSION SET CONTAINER = FREEPDB1;
33
-- In-database embeddings infrastructure (foundational; used by the AI articles).
44
-- Runs once on first boot (gvenzl initdb), after 01-07. Loads Oracle's prebuilt
55
-- augmented all-MiniLM-L12-v2 ONNX model (baked into the image at /opt/oracle/models
6-
-- by docker/Dockerfile.oracle), embeds all 300 ticket bodies into a real VECTOR(384)
7-
-- column, and builds an IVF vector index on it. Idempotent: every CREATE is guarded
8-
-- so a recreated container over a persistent volume re-runs cleanly.
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.
911
--
1012
-- Grants used by the AI modules beyond the base 01-lab-user.sql set:
1113
-- CREATE MINING MODEL — DBMS_VECTOR.LOAD_ONNX_MODEL registers an ONNX model
@@ -59,8 +61,9 @@ BEGIN
5961
END;
6062
/
6163

62-
-- Embed all 300 ticket bodies in-database — no external embedding service, no
63-
-- copy pipeline. ~2.5s on the Free container. Re-runnable: recomputes from body.
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.
6467
UPDATE support_tickets SET body_vec = VECTOR_EMBEDDING(MINILM_L12 USING body AS data);
6568

6669
COMMIT;
@@ -77,3 +80,18 @@ END;
7780
CREATE VECTOR INDEX tickets_bodyvec_ivf ON support_tickets(body_vec)
7881
ORGANIZATION NEIGHBOR PARTITIONS
7982
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+
/

0 commit comments

Comments
 (0)