<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oracle Developers</title>
    <description>The latest articles on DEV Community by Oracle Developers (oracledevs).</description>
    <link>https://dev.to/oracledevs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F11587%2F7c934ee0-6aa6-42f9-b43f-91e6fa82ef41.png</url>
      <title>DEV Community: Oracle Developers</title>
      <link>https://dev.to/oracledevs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oracledevs"/>
    <language>en</language>
    <item>
      <title>How I Taught an AI to Sound Like Me: Agent Memory with Oracle AI Database</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:21:34 +0000</pubDate>
      <link>https://dev.to/oracledevs/how-i-taught-an-ai-to-sound-like-me-agent-memory-with-oracle-ai-database-pn7</link>
      <guid>https://dev.to/oracledevs/how-i-taught-an-ai-to-sound-like-me-agent-memory-with-oracle-ai-database-pn7</guid>
      <description>&lt;p&gt;&lt;strong&gt;A step-by-step tutorial for building three layers of agent memory in Oracle AI Database to help an AI agent learn to write social media posts in your voice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Companion notebook:&lt;/strong&gt; &lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/tree/main/apps/oracle-agent-memory" rel="noopener noreferrer"&gt;https://github.com/oracle-devrel/oracle-ai-developer-hub/tree/main/apps/oracle-agent-memory&lt;/a&gt;&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The problem is not that AI writes badly. It’s that AI writes from zero.&lt;/strong&gt;A stateless model has no memory of your older posts, your cadence, your weird little phrases, or what you never say. So it defaults to the internet-average voice, which is why so much AI-written social content feels the same.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Good writing help needs three kinds of memory.&lt;/strong&gt;Episodic memory gives the agent examples of what you’ve written before. Semantic memory gives it a structured style profile. Reflective memory lets that profile evolve as your writing changes.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Oracle AI Database keeps the memory stack simple.&lt;/strong&gt;Posts, vectors, JSON style profiles, and reflection logs all live in the same database. That means the agent can retrieve similar posts, load your voice profile, and update its understanding without stitching together a pile of separate services.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;The reflection loop is what makes it feel like learning.&lt;/strong&gt;Every few new posts, the agent compares your current style profile against your latest writing, creates a conservative diff, and updates the profile without overreacting to one weird week of posts.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;The actual agent is deceptively small.&lt;/strong&gt;The final &lt;code&gt;generatePost&lt;/code&gt; function only needs a style profile, a few similar examples, and one LLM call. The hard part is not the prompt. The hard part is giving the prompt the right memory.&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;The last time you went on social media, did it feel... stale? Every post you scroll past reads the same with slightly different words. A generic opener starting with "Most X think Y", three points and a call to action, the same six emojis. Yeah, those were created by AI.&lt;/p&gt;

&lt;p&gt;I'm not here to say that all AI-generated content is bad, but we're definitely seeing a lack of originality these days. Which is a shame, because using generative AI as a tool in the creative process is incredible. But copy/pasting the output of a "write me a LinkedIn post about security issues with AI agents" is the wrong way to go about it.&lt;/p&gt;

&lt;p&gt;AI models are stateless. Every time you ask one to write a post for you, it starts from zero. It has no idea what you've written before, what worked, what fell flat, or how you sound when you're not trying. So it falls back on the average... which is exactly what you're seeing in your feed these days.&lt;/p&gt;

&lt;p&gt;But it doesn't have to be this way. You can still use an AI agent to help you with social posts AND to sound like your natural voice. You just have to give it some memory.&lt;/p&gt;

&lt;p&gt;This post walks through how to build an AI agent with three layers of memory backed by &lt;a href="https://www.oracle.com/database/" rel="noopener noreferrer"&gt;Oracle AI Database 26ai&lt;/a&gt;, with a reflection loop that updates the agent's understanding of your voice over time. The stack is TypeScript end-to-end: Node.js backend, React + Vite frontend, the official &lt;code&gt;oracledb&lt;/code&gt; driver. If you prefer Python, &lt;code&gt;langchain-oracledb&lt;/code&gt; is the direct equivalent.&lt;/p&gt;

&lt;p&gt;To learn a little bit more about agent memory, &lt;a href="https://blogs.oracle.com/developers/agent-memory-why-your-ai-has-amnesia-and-how-to-fix-it" rel="noopener noreferrer"&gt;check out this blog&lt;/a&gt; by Casius Lee.&lt;/p&gt;





&lt;h2&gt;What we're building&lt;/h2&gt;

&lt;p&gt;Our agent has one job: given a topic and a platform (LinkedIn, X, whatever), drafts a post that sounds like me. Easy enough as a one-shot LLM call. But the cool part is how we make it better over time without changing the prompt.&lt;/p&gt;

&lt;p&gt;To do that, the agent needs three different kinds of memory:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;What it stores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;How it's used&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Episodic memory&lt;/td&gt;
&lt;td&gt;Every post I've written, embedded as a vector&lt;/td&gt;
&lt;td&gt;Retrieve the &lt;a href="https://www.oracle.com/database/ai-vector-search/similarity-search/#techniques" rel="noopener noreferrer"&gt;K most similar&lt;/a&gt; past posts as examples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic memory&lt;/td&gt;
&lt;td&gt;A structured JSON object describing my voice traits&lt;/td&gt;
&lt;td&gt;Inject into the system prompt as explicit guidance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reflective memory&lt;/td&gt;
&lt;td&gt;Observations about how my writing style is evolving over time&lt;/td&gt;
&lt;td&gt;Periodically refine the semantic memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three live in Oracle AI Database, which makes this easier than it looks. Vector search, JSON, and relational rows all live in the same database with the same query engine. So in a single database, we can store everything we need to make this work.&lt;/p&gt;

&lt;p&gt;Here's the loop:&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture1-3-822x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture1-3-822x1024.png" alt="Flowchart showing a writing workflow. A topic and platform feed into generatePost(), which reads a style profile, performs vector search over previous posts, and calls an LLM. After drafting, editing, and publishing, the final post is saved. Saved posts expand episodic memory. Every N new posts, a reflection step compares new posts with the existing style profile, generates updates, and feeds the revised style profile back into future post generation." width="800" height="997"&gt;&lt;/a&gt;Published posts build episodic memory, while periodic reflection updates the style profile used for future content generation.&lt;p&gt;&lt;/p&gt;





&lt;h2&gt;Setup&lt;/h2&gt;

&lt;p&gt;If you don't already have a database, the repo includes a Terraform stack that provisions an Always Free Autonomous AI Database 26ai and writes a populated .env. Just clone the repository and run these three commands:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cd terraform
terraform init &amp;amp;&amp;amp; terraform apply
terraform output -raw env_file &amp;gt; ../.env
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://www.oracle.com/cloud/free/" rel="noopener noreferrer"&gt;Always Free covers the cost&lt;/a&gt; of the database forever. OCI Generative AI isn't on the always-free tier, but new accounts get $300 in trial credits, and the per-call cost for what we're about to build costs pennies. If you already have an Oracle 26ai instance, skip the Terraform and fill in &lt;code&gt;.env&lt;/code&gt; by hand.&lt;/p&gt;

&lt;p&gt;Then install the dependencies:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once dependencies are installed, we're ready to start building! But before we do that, we should talk about the three tables in our schema, each representing one of the layers of our agentic memory.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;-- Episodic memory: every post you've ever written
CREATE TABLE posts (
    id            VARCHAR2(36) PRIMARY KEY,
    user_id       VARCHAR2(64) NOT NULL,
    platform      VARCHAR2(32) NOT NULL,
    topic         VARCHAR2(256),
    content       CLOB NOT NULL,
    embedding     VECTOR(1024, FLOAT32),
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    is_deleted    NUMBER(1) DEFAULT 0
);

CREATE VECTOR INDEX posts_hnsw_idx ON posts (embedding)
    ORGANIZATION INMEMORY NEIGHBOR GRAPH
    DISTANCE COSINE
    PARAMETERS (TYPE HNSW, NEIGHBORS 32, EFCONSTRUCTION 200);


-- Semantic memory: the style profile per user
CREATE TABLE style_profile (
    user_id       VARCHAR2(64) PRIMARY KEY,
    profile       JSON NOT NULL,
    updated_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    version       NUMBER(10) DEFAULT 1
);

-- Reflective memory: what changed and when
CREATE TABLE reflections (
    id            VARCHAR2(36) PRIMARY KEY,
    user_id       VARCHAR2(64) NOT NULL,
    triggered_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    posts_window  JSON NOT NULL,
    diff          JSON NOT NULL,
    profile_after JSON NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It's important to note that &lt;code&gt;VECTOR(1024, FLOAT32)&lt;/code&gt; matches OCI's &lt;code&gt;cohere.embed-english-v3.0&lt;/code&gt; model. If you swap embedding functions, &lt;em&gt;make sure you update the dimension to match&lt;/em&gt;. And as a bonus, &lt;code&gt;JSON&lt;/code&gt; is a first-class type in Oracle 26ai with indexable paths, so the style profile doesn't need to be re-parsed on every read.&lt;/p&gt;

&lt;p&gt;Before we get to the memory layers themselves, let's set up a thin wrapper around the OCI SDK for consistency and simplicity. Two functions: &lt;code&gt;embed()&lt;/code&gt;, responsible for creating text embeddings from our social posts, and &lt;code&gt;chat()&lt;/code&gt;, for communicating with the model.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// src/server/llm.ts
import * as common from 'oci-common';
import { GenerativeAiInferenceClient } from 'oci-generativeaiinference';
const provider = new common.ConfigFileAuthenticationDetailsProvider();
const client = new GenerativeAiInferenceClient({
  authenticationDetailsProvider: provider,
});

const compartmentId = process.env.OCI_COMPARTMENT_ID!;
export async function embed(texts: string[]): Promise&amp;lt;number[][]&amp;gt; {
  const res = await client.embedText({
    embedTextDetails: {
      inputs: texts,
      servingMode: { servingType: 'ON_DEMAND', modelId: 'cohere.embed-english-v3.0' },
      compartmentId
    }
  });

  return res.embedTextResult.embeddings;
}

export async function chat(args: { system: string; user: string }): Promise&amp;lt;string&amp;gt; {
  const res = await client.chat({
    chatDetails: {
      servingMode: { servingType: 'ON_DEMAND', modelId: 'cohere.command-r-plus-08-2024' },
      compartmentId,
      chatRequest: {
        apiFormat: 'COHERE',
        preambleOverride: args.system,
        message: args.user,
        temperature: 0.2,
        maxTokens: 1500
      }
    }
  });

  return res.chatResult.chatResponse.text;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;ConfigFileAuthenticationDetailsProvider&lt;/code&gt; reads &lt;code&gt;~/.oci/config&lt;/code&gt; (the DEFAULT profile) automatically. &lt;code&gt;servingType: 'ON_DEMAND'&lt;/code&gt; is the pay-as-you-go mode that uses your trial credits without provisioning a cluster. Everything from here on calls these &lt;code&gt;embed()&lt;/code&gt; and &lt;code&gt;chat()&lt;/code&gt; functions.&lt;/p&gt;





&lt;h2&gt;Episodic memory&lt;/h2&gt;

&lt;p&gt;The first layer is the simplest. Every time I publish a post, I save it. Every time I want to draft a new one, the agent retrieves the K most similar past posts to use as &lt;a href="https://blogs.oracle.com/ai-and-datascience/enhancing-rag-with-advanced-prompting" rel="noopener noreferrer"&gt;few-shot examples&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// src/server/memory.ts
import { randomUUID } from 'node:crypto';
import { withConn, oracledb } from './db';
import { embed } from './llm';
export async function savePost(args: {
  userId: string; platform: string; topic: string; content: string;
}): Promise&amp;lt;string&amp;gt; {
  const id = randomUUID();
  const [embedding] = await embed([args.content]);
  await withConn(async (conn) =&amp;gt; {
    await conn.execute(
      `INSERT INTO posts (id, user_id, platform, topic, content, embedding)
       VALUES (:id, :userId, :platform, :topic, :content, :embedding)`,
      {
        id, userId: args.userId, platform: args.platform,
        topic: args.topic, content: args.content,
        embedding: { type: oracledb.DB_TYPE_VECTOR, val: new Float32Array(embedding) }
      },
      { autoCommit: true }
    );
  });

  return id;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Retrieval is a little more nuanced, and we take full advantage of the hybrid filter here. We want to find the most similar posts &lt;em&gt;on the same platform&lt;/em&gt;, by &lt;em&gt;this user&lt;/em&gt;, that aren't deleted. To do this, we perform a vector search plus a &lt;code&gt;WHERE&lt;/code&gt; clause and are able to get the results we want with a single query.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export async function retrieveSimilarPosts(args: {
  userId: string; platform: string; topic: string; k?: number;
}) {
  const k = args.k ?? 5;
  const [queryEmbedding] = await embed([args.topic]);
  return withConn(async (conn) =&amp;gt; {
    const r = await conn.execute&amp;lt;[string, string, string, number]&amp;gt;(
      `SELECT id, content, topic, VECTOR_DISTANCE(embedding, :q, COSINE) AS distance
       FROM posts
       WHERE user_id = :userId AND platform = :platform AND is_deleted = 0
       ORDER BY distance
       FETCH APPROX FIRST :k ROWS ONLY`,
      {
        q: { type: oracledb.DB_TYPE_VECTOR, val: new Float32Array(queryEmbedding) },
        userId: args.userId, platform: args.platform, k,
      }
    );

    return (r.rows ?? []).map(([id, content, topic, distance]) =&amp;gt;
      ({ id, content, topic, distance }));
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;FETCH APPROX FIRST :k ROWS ONLY &lt;/code&gt;is what lets Oracle use the &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/overview-hierarchical-navigable-small-world-indexes.html" rel="noopener noreferrer"&gt;HNSW index&lt;/a&gt; for approximate nearest neighbor. Without &lt;code&gt;APPROX&lt;/code&gt;, the query would fall back to exact scan, which is fine for thousands of vectors, but virtually unusable for millions.&lt;/p&gt;





&lt;h2&gt;Semantic memory&lt;/h2&gt;

&lt;p&gt;Episodic retrieval gets you "what have I said about this topic before." But now we need "how do I sound when I write." This is the perfect use case for semantic memory.&lt;/p&gt;

&lt;p&gt;We keep our "style profile" as a structured JSON object. It stores attributes like voice traits more &lt;em&gt;about&lt;/em&gt; the writing rather than in the writing. To capture a good approximation of what you sound like, our profile looks for the following behaviors:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  "tone": ["direct", "self-deprecating", "slightly skeptical of hype"],
  "sentenceLength": {
    "averageWords": 16,
    "habit": "short punchy sentences mixed with longer explanatory ones"
  },
  "structuralHabits": [
    "opens with a personal anchor or a small story",
    "uses italics for the one line that should stick",
    "closes posts with a question or a single-word punchline"
  ],
  "signaturePhrases": ["Happy coding!", "Let me explain", "Here's the thing"],
  "thingsINeverDo": [
    "use 'unlock', 'leverage', 'game-changer'",
    "more than two emoji per post"
  ],
  "topicsICareAbout": ["serverless", "AI agents", "developer experience"],
  "platformQuirks": {
    "linkedin": "longer hooks, line breaks every 1-2 sentences",
    "x": "thread-friendly, one idea per tweet"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you've ever asked someone to write something like this about themselves, you know people are absolutely terrible at this type of self-reflection. So naturally, we bypass the human element and ask the model to generate it from the first N posts a user adds in the system.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import { chat } from './llm';
const SEED_SYSTEM = `You are a voice analyst. You will read several social media posts by one author and produce a JSON style profile describing how they write. Be specific and concrete. "Tone is friendly" is useless. "Tone is direct, occasionally self-deprecating, slightly skeptical of hype" is useful.

Output ONLY valid JSON matching this schema:
{ 
  "tone": [string], 
  "sentenceLength": {
    "averageWords": int, "habit": string
  },
  "structuralHabits": [string], 
  "signaturePhrases": [string],
  "thingsINeverDo": [string], 
  "topicsICareAbout": [string],
  "platformQuirks": {string: string} 
}`;

export async function seedStyleProfile(userId: string, sampleSize = 20) {
  const rows = await withConn(async (conn) =&amp;gt; {
    const r = await conn.execute&amp;lt;[string, string]&amp;gt;(
      `SELECT platform, content FROM posts
       WHERE user_id = :userId AND is_deleted = 0
       ORDER BY created_at DESC FETCH FIRST :n ROWS ONLY`,
      { userId, n: sampleSize }
    );

    return r.rows ?? [];
  });

  const postsText = rows
    .map(([p, c]) =&amp;gt; `[${p}] ${c}`).join('\n\n---\n\n');
  const response = await chat({
    system: SEED_SYSTEM,
    user: `Posts:\n\n${postsText}`
  });

  const profile = JSON.parse(response);
  await withConn(async (conn) =&amp;gt; {
    await conn.execute(
      `MERGE INTO style_profile sp
       USING (SELECT :userId AS user_id FROM dual) src ON (sp.user_id = src.user_id)
       WHEN MATCHED THEN UPDATE SET
         profile = :profile, updated_at = CURRENT_TIMESTAMP, version = version + 1
       WHEN NOT MATCHED THEN INSERT (user_id, profile) VALUES (:userId, :profile)`,
      { userId, profile: JSON.stringify(profile) },
      { autoCommit: true }
    );
  });

  return profile;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;MERGE&lt;/code&gt; statement handles both inserts and updates in one round trip (and deletes, &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/MERGE.html" rel="noopener noreferrer"&gt;it's pretty impressive&lt;/a&gt;). The Oracle &lt;code&gt;JSON&lt;/code&gt; type validates the value on insert, and if the LLM emits malformed JSON, the insert fails, which is exactly what we want.&lt;/p&gt;

&lt;p&gt;Reading it back is just as few easy lines:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export async function loadStyleProfile(userId: string) {
  return withConn(async (conn) =&amp;gt; {
    const r = await conn.execute&amp;lt;[unknown]&amp;gt;(
      `SELECT profile FROM style_profile WHERE user_id = :userId`,
      { userId },
    );

    if (!r.rows?.length) return null;
    return r.rows[0][0] as StyleProfile;
  });
}
&lt;/code&gt;&lt;/pre&gt;





&lt;h2&gt;Reflective memory&lt;/h2&gt;

&lt;p&gt;I've built many agents where I'm done after implementing episodic and semantic memory. In many instances, that's &lt;em&gt;good enough&lt;/em&gt;. But with this type of workload, aka people writing about what they care about, preferences change over time. Personally, I used to write nothing but dry, cold facts on serverless architectures. Today, I'm a pretty funny guy (right?!) and ponder on things that take software from good to great. Very different styles, but both me.&lt;/p&gt;

&lt;p&gt;Building in reflective memory allows the agent to adjust over time. It's what gives us the impression that it's actually "learning."&lt;/p&gt;

&lt;p&gt;Every K new posts (I use K=5), we trigger a reflection. The reflection is an LLM call that reads the current style profile and the K newest posts, then creates a structured diff: what's changed, what should be added, and what should be removed.&lt;/p&gt;

&lt;p&gt;We go with the structured diff instead of a straight up overwrite to avoid profile thrashing. You aren't just your last 5 posts. You're a summary of everything you've ever posted with a recency bias. Asking for a diff lets the model commit to small, intentional updates, so you stay you and don't appear like you have violent mood swings every week.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const REFLECT_SYSTEM = `You are reviewing how an author's voice may have evolved. You have:

1. Their CURRENT style profile (built from older posts)
2. Their MOST RECENT posts (not yet incorporated)

Read the recent posts and compare to the profile. Decide whether the profile needs updating. Be conservative: most of the time, voice is stable and you should change little or nothing. Only return updates that you can point to specific evidence for in the recent posts.

Output ONLY valid JSON:
{ "additions": [{"field": string, "value": any, "evidence": string}],
  "removals":  [{"field": string, "value": any, "reason": string}],
  "rationale": string 
}

If nothing should change, return empty arrays.`;

export async function reflect(userId: string, windowSize = 5) {
  const profile = await loadStyleProfile(userId);
  if (!profile) return seedStyleProfile(userId);
  const rows = await withConn(async (conn) =&amp;gt; {
    const r = await conn.execute&amp;lt;[string, string]&amp;gt;(
      `SELECT id, content FROM posts
       WHERE user_id = :userId AND is_deleted = 0
       ORDER BY created_at DESC FETCH FIRST :n ROWS ONLY`,
      { userId, n: windowSize }
    );

    return r.rows ?? [];
  });

  const postIds = rows.map(([id]) =&amp;gt; id);
  const postsText = rows.map(([, c]) =&amp;gt; c).join('\n\n---\n\n');
  const response = await chat({
    system: REFLECT_SYSTEM,
    user: `CURRENT PROFILE:\n${JSON.stringify(profile, null, 2)}\n\nRECENT POSTS:\n${postsText}`,
  });

  const diff = JSON.parse(response);
  const updated = applyDiff(profile, diff);
  await withConn(async (conn) =&amp;gt; {
    await conn.execute(
      `UPDATE style_profile SET profile = :profile,
         updated_at = CURRENT_TIMESTAMP, version = version + 1
       WHERE user_id = :userId`,
      { profile: JSON.stringify(updated), userId }
    );

    await conn.execute(
      `INSERT INTO reflections (id, user_id, posts_window, diff, profile_after)
       VALUES (:id, :userId, :window, :diff, :after)`,
      {
        id: randomUUID(), userId,
        window: JSON.stringify(postIds),
        diff: JSON.stringify(diff),
        after: JSON.stringify(updated)
      },
      { autoCommit: true }
    );
  });

  return updated;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;applyDiff&lt;/code&gt; function is simple. It iterates through the additions and removals, and edits the profile object in place. It's in the repo if you're interested. I need to point out again that the only reason &lt;code&gt;applyDiff&lt;/code&gt; works is because we tell the model to be conservative with its reflections. Remember, we don't want wild swings in the profile. Your posts won't make it past the "AI sniff test" if you're calm and collected one day, and corporate and metric-driven the next.&lt;/p&gt;

&lt;p&gt;If that does happen though, we can use the reflection log as a point-in-time snapshot we can rollback to. Just rebuild from a previous &lt;code&gt;profile_after&lt;/code&gt; snapshot and the agent effectively "unlearns" that unwanted style.&lt;/p&gt;





&lt;h2&gt;Agent memory in action&lt;/h2&gt;

&lt;p&gt;Now that we've gone through all three types of memory, it's time to build the &lt;code&gt;generatePost&lt;/code&gt; function and see them all in action as we compose the prompt.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// src/server/agent.ts
import { chat } from './llm';
import { loadStyleProfile, retrieveSimilarPosts } from './memory';
export async function generatePost(args: {
  userId: string; platform: string; topic: string;
}) {
  const profile = (await loadStyleProfile(args.userId)) ?? {};
  const examples = await retrieveSimilarPosts({ ...args, k: 5 });
  const examplesText = examples.map((e) =&amp;gt; e.content).join('\n\n---\n\n');
  const system = `You are drafting a social media post in the user's voice.
    STYLE PROFILE (how this user writes):
    ${JSON.stringify(profile, null, 2)}
    
    EXAMPLES (recent posts by this user on similar topics):
    ${examplesText}


    Write ONE draft post. Match the style profile and the cadence of the examples. Do not copy phrases from the examples. Do not mention that you are an AI or that you are following a profile.`;

  const draft = await chat({
    system,
    user: `Platform: ${args.platform}\nTopic: ${args.topic}`,
  });

  return { draft, basedOn: examples };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That's it. That's the whole thing. This is deceptively simple. It's doing two database reads to load the profile and perform a vector search, AND it's making a call to an LLM.&lt;/p&gt;

&lt;p&gt;The agent will get better over time. The first time you use it, it will sound like everything else you see on social media these days. But as you edit the drafts and build up the data with examples in your true voice, it gets better and eerily starts sounding like you.&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;FAQs&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not just prompt the model to “write in my voice”?&lt;/strong&gt;&lt;br&gt;Because the model does not actually know your voice unless you give it evidence. The article uses past posts plus a style profile so the agent has something concrete to imitate instead of guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What does episodic memory do here?&lt;/strong&gt;&lt;br&gt;It stores every past post with an embedding. When you ask for a new draft, the agent finds the most similar posts on the same platform and uses them as examples.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the style profile?&lt;/strong&gt;&lt;br&gt;It is the semantic memory layer: a JSON object that describes how you write, including tone, sentence habits, structural patterns, signature phrases, topics you care about, and things you avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why use reflective memory instead of just overwriting the profile?&lt;/strong&gt;&lt;br&gt;Because you are not just your last five posts. Reflection creates small, evidence-backed updates so the profile can evolve without thrashing every time your writing mood changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What happens if the agent learns the wrong style?&lt;/strong&gt;&lt;br&gt;The reflection table keeps a history of changes. Since each reflection stores the diff and the resulting profile, you can roll back to an earlier snapshot and effectively make the agent unlearn that bad update.&lt;/p&gt;





&lt;h2&gt;To summarize&lt;/h2&gt;

&lt;p&gt;Agent memory has a lot more to it than simply "remembering things." There are different types of memory that represent similar artifacts, summaries, and long-term audits. To build a production-ready system, you need all three. Episodic and semantic memory to satisfy the business problem, and reflective to improve over time.&lt;/p&gt;

&lt;p&gt;Oracle AI Database is the perfect database for these three types of memory. It supports vectors, JSON objects, and similarity search with filtering &lt;em&gt;all in the same database&lt;/em&gt;. The schema for this is twenty lines. Reads are three-line functions. The complexity stays out of the database layer and in the prompt engineering where it belongs.&lt;/p&gt;

&lt;p&gt;To walk through this project yourself, you can &lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/tree/main/apps/oracle-agent-memory" rel="noopener noreferrer"&gt;find the code on GitHub&lt;/a&gt;. It's built on TypeScript end-to-end, and is easily portable to whatever your preferred programming language is.&lt;/p&gt;

&lt;p&gt;If you try this out, send me what you generate. I want to see how well "sounds like you" holds up across different writers.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;



</description>
      <category>agentmemory</category>
      <category>oracle</category>
      <category>ai</category>
      <category>database</category>
    </item>
    <item>
      <title>Build an Intelligent Document Processor in One Data Store</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:19:23 +0000</pubDate>
      <link>https://dev.to/oracledevs/build-an-intelligent-document-processor-in-one-data-store-3b5c</link>
      <guid>https://dev.to/oracledevs/build-an-intelligent-document-processor-in-one-data-store-3b5c</guid>
      <description>&lt;p&gt;&lt;strong&gt;Companion notebook: &lt;/strong&gt;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/tree/main/apps/idp-oracle-ai-database" rel="noopener noreferrer"&gt;https://github.com/oracle-devrel/oracle-ai-developer-hub/tree/main/apps/idp-oracle-ai-database&lt;/a&gt;&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The use case is boring in the best possible way.&lt;/strong&gt;Intelligent Document Processing is exactly where AI makes sense: incoming business PDFs, repetitive manual work, and structured fields that need to move into a process. In this article, the example is procure-to-pay: purchase orders, delivery notes, and invoices.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;The twist is one data store.&lt;/strong&gt;Instead of splitting blobs, JSON, vectors, relational data, and AI calls across S3, DynamoDB, Pinecone, SQL, and external APIs, the whole pipeline runs through Oracle AI Database.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Classification does not need an LLM.&lt;/strong&gt;The app embeds labeled sample documents, embeds each new document, then uses k-nearest-neighbor vector search to decide whether it looks most like an invoice, purchase order, or delivery note.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;The LLM only shows up when it is actually needed.&lt;/strong&gt;Vectors can tell you what kind of document you have. They cannot reliably extract invoice numbers, totals, due dates, vendors, and line items. That structured extraction step uses &lt;code&gt;UTL_TO_GENERATE_TEXT&lt;/code&gt; and validates the result against a Zod schema.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Oracle AI Database becomes the IDP engine, not just storage.&lt;/strong&gt;It stores the original PDF as a BLOB, extracts text, summarizes it, creates embeddings, runs vector search, stores structured JSON fields, and calls OCI Generative AI from inside the database.&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;Everybody wants to build AI applications. But nobody knows a good use case.&lt;/p&gt;

&lt;p&gt;One use case I have seen over and over again is processing incoming business documents.&lt;br&gt;In this article, I will show you how to build an Intelligent Document Processing (IDP) platform around the &lt;strong&gt;procure-to-pay&lt;/strong&gt; cycle: it ingests &lt;strong&gt;purchase orders, delivery notes, and invoices&lt;/strong&gt;, classifies them, and pulls out their structured fields.&lt;/p&gt;

&lt;p&gt;The twist: we do all of it inside &lt;strong&gt;one&lt;/strong&gt; data store.&lt;/p&gt;





&lt;h2&gt;The Issue with Data Stores&lt;/h2&gt;

&lt;p&gt;IDP platforms are nothing new. They are built and used in many companies already. It is the typical internal tool where a bit of AI saves a lot of manual data entry.&lt;/p&gt;

&lt;p&gt;Building one usually means stitching together several data stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt; for the document blobs&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; for key/values&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Pinecone&lt;/strong&gt; for the vectors&lt;/li&gt;



&lt;li&gt;A &lt;strong&gt;SQL&lt;/strong&gt; database for aggregations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…sometimes even more.&lt;/p&gt;

&lt;p&gt;In this article, I want to demonstrate how you can build all of that with just one data store: &lt;strong&gt;Oracle AI Database&lt;/strong&gt;.&lt;/p&gt;





&lt;h2&gt;What Is Oracle AI Database?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Oracle AI Database&lt;/strong&gt; is Oracle's AI-native database, its flagship database with AI built &lt;strong&gt;into the engine &lt;/strong&gt;rather than bolted on through external services.&lt;/p&gt;

&lt;p&gt;It is a &lt;em&gt;converged&lt;/em&gt; database, which means it doesn't just support typical SQL workloads. It is a multi-model database, which lets you also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store JSON documents&lt;/li&gt;



&lt;li&gt;store relational rows&lt;/li&gt;



&lt;li&gt;store vectors&lt;/li&gt;



&lt;li&gt;store BLOB files&lt;/li&gt;



&lt;li&gt;… and more!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of wiring up separate OpenAI, Cohere, and vector-store APIs, the database can do the work for you.&lt;/p&gt;

&lt;p&gt;Oracle doesn't just give you a &lt;code&gt;VECTOR&lt;/code&gt; data type (you could get that from a Postgres extension too). You also get text extraction, chunking, embeddings, vector search, and even calls out to generative-AI models. All driven from SQL and PL/SQL via the &lt;code&gt;DBMS_VECTOR_CHAIN&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;Let's build with it.&lt;/p&gt;





&lt;h2&gt;The Architecture&lt;/h2&gt;

&lt;p&gt;We have a typical REST + SPA architecture. The frontend and backend run on AWS. The database lives in the free tier of Oracle Cloud (OCI).&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Farchitecture-1024x269.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Farchitecture-1024x269.png" alt="Architecture diagram: a React SPA on S3 and CloudFront calls a Hono API on AWS Lambda, which connects to Oracle AI Database on OCI" width="799" height="210"&gt;&lt;/a&gt;A React SPA on S3 and CloudFront calls a Hono API on AWS Lambda, which connects to Oracle AI Database on OCI&lt;p&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: React SPA (Vite) + TanStack Router&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Hono API on AWS Lambda (Function URL)&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Hosting&lt;/strong&gt;: S3 + CloudFront&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: Oracle AI Database (OCI Autonomous, Always Free tier)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS provides only compute and hosting. Everything &lt;em&gt;about&lt;/em&gt; a document (the original file, the extracted text, the structured JSON, and the vector), lives in Oracle AI Database.&lt;/p&gt;





&lt;h2&gt;The Documents We Process&lt;/h2&gt;

&lt;p&gt;The application detects and structures incoming documents. We picked the three documents of the&amp;nbsp;&lt;strong&gt;procure-to-pay&lt;/strong&gt;&amp;nbsp;cycle:&lt;/p&gt;

&lt;p&gt;For each type we generated a handful of sample PDFs and&amp;nbsp;&lt;strong&gt;embedded these labeled examples&lt;/strong&gt;&amp;nbsp;into the database. When a new document arrives, the app&amp;nbsp;&lt;strong&gt;compares its embedding against those labeled examples&lt;/strong&gt;&amp;nbsp;to decide which type it most resembles. There is no rules engine and no fine-tuning — just vectors and distance.&lt;/p&gt;

&lt;p&gt;We want to get specific fields from each document. For example, for invoices we look for the following data (in a Zod schema):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// packages/schemas/src/invoice.ts
export const invoiceFields = z.object({
  envelope: commonEnvelope,
  vendor: z.string(),
  invoiceNumber: z.string(),
  invoiceDate: z.string(),
  dueDate: z.string().nullable(),
  currency: z.string().length(3),
  subtotal: z.number(),
  tax: z.number(),
  total: z.number(),
  lineItems: z.array(invoiceLineItem),
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Our AI is extracting exactly this data. In an IDP application this data is used for further processing like sending out the order or validating invoices.&lt;/p&gt;





&lt;h2&gt;Viewing Documents and Content&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fdocs-app-1024x924.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fdocs-app-1024x924.png" alt="The app's document detail view showing the original PDF, its extracted fields, and similar documents found via vector search" width="800" height="722"&gt;&lt;/a&gt;The app's document detail view showing the original PDF, its extracted fields, and similar documents found via vector search&lt;p&gt;&lt;/p&gt;

&lt;p&gt;In the application you can open any document to see the original PDF, the extracted fields, and similar documents found via vector search.&lt;/p&gt;





&lt;h2&gt;Uploading Documents&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fuploading-docs-1024x356.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fuploading-docs-1024x356.png" alt="The app's upload screen for adding a new document to be classified and processed" width="800" height="278"&gt;&lt;/a&gt;The app's upload screen for adding a new document to be classified and processed&lt;p&gt;&lt;/p&gt;

&lt;p&gt;If you want to upload a new document you can do so as well! The uploader stores the document in the database, embeds it, and finds similar documents again. More details about this process follow in the rest of the article.&lt;/p&gt;





&lt;h2&gt;A Two-Minute Primer on Vectors&lt;/h2&gt;

&lt;p&gt;A vector embedding is just a list of numbers that represents the *meaning* of a piece of text. You can picture each document as a point in space.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fvectors-1024x765.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fvectors-1024x765.png" alt="Documents plotted as points in vector space, where documents of the same type cluster together and different types sit farther apart" width="800" height="598"&gt;&lt;/a&gt;Documents plotted as points in vector space, where documents of the same type cluster together and different types sit farther apart&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Documents of the same type land near each other; different types land further apart.&lt;/p&gt;

&lt;p&gt;To classify a new document, we embed it and measure the distance to the labeled examples we already stored. The closest examples win.&lt;/p&gt;

&lt;p&gt;For example, when a new document comes in we check whether it sits closer to a purchase order, a delivery note, or an invoice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;distance(new, purchase-order-sample) = 0.1 ✅&lt;/li&gt;



&lt;li&gt;distance(new, delivery-note-sample) = 0.7 ❌&lt;/li&gt;



&lt;li&gt;distance(new, invoice-sample) = 0.4 ❌&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The smallest distance is the most similar, so we classify the new document as a purchase order.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fvector-distances-1024x765.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fvector-distances-1024x765.png" alt="A new document compared by distance to a purchase order, delivery note, and invoice sample; the nearest sample wins the classification" width="800" height="598"&gt;&lt;/a&gt;A new document compared by distance to a purchase order, delivery note, and invoice sample; the nearest sample wins the classification&lt;p&gt;&lt;/p&gt;





&lt;h2&gt;Prerequisites&lt;/h2&gt;

&lt;p&gt;To follow along you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node 20+ and pnpm 10+&lt;/strong&gt;&lt;/li&gt;



&lt;li&gt;An&amp;nbsp;&lt;strong&gt;OCI Free account&lt;/strong&gt;&amp;nbsp;with an&amp;nbsp;&lt;strong&gt;Oracle AI Database&lt;/strong&gt;&amp;nbsp;(Autonomous, Always Free tier), with the wallet downloaded locally&lt;/li&gt;



&lt;li&gt;An&amp;nbsp;&lt;strong&gt;OCI API key&lt;/strong&gt;&amp;nbsp;for OCI Generative AI (used by the extraction step)&lt;/li&gt;



&lt;li&gt;An&amp;nbsp;&lt;strong&gt;AWS account&lt;/strong&gt;&amp;nbsp;(only if you want to deploy; you can run everything locally without it)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two short provisioning guides in the repo walk you through the slow parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/apps/idp-oracle-ai-database/docs/01-provision-oracle.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/01-provision-oracle.md&lt;/code&gt;&lt;/a&gt;&amp;nbsp;— create the database, download the wallet, run the migrations, load the embedding model.&lt;/li&gt;



&lt;li&gt;
&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/apps/idp-oracle-ai-database/docs/02-provision-oci-genai.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/02-provision-oci-genai.md&lt;/code&gt;&lt;/a&gt;&amp;nbsp;— create the API key and register the in-database OCI Generative AI credential.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the database is provisioned and&amp;nbsp;&lt;code&gt;.env&lt;/code&gt;&amp;nbsp;is filled in (see&amp;nbsp;&lt;code&gt;.env.example&lt;/code&gt;), bootstrap everything with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pnpm install
pnpm db:setup # creates the idp user, schema, and indexes
pnpm db:setup-onnx # loads the ONNX embedding model as "doc_embedder"
pnpm db:setup-oci-credential # registers the OCI Generative AI credential + smoke-tests it&lt;/code&gt;&lt;/pre&gt;





&lt;h2&gt;Setting Up the Database&lt;/h2&gt;

&lt;h3&gt;&lt;strong&gt;The Schema&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;The whole application lives in two tables. &lt;code&gt;documents&lt;/code&gt; holds the file and everything we derive from it, including the 384-dimension embedding as a native &lt;code&gt;VECTOR&lt;/code&gt; column:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE documents (
  id                RAW(16)         DEFAULT SYS_GUID() PRIMARY KEY,
  doc_type          VARCHAR2(16)    DEFAULT 'unknown' NOT NULL,
  status            VARCHAR2(32)    DEFAULT 'pending' NOT NULL,
  original_filename VARCHAR2(512)   NOT NULL,
  mime_type         VARCHAR2(128)   NOT NULL,
  byte_size         NUMBER          NOT NULL,
  page_count        NUMBER,
  language          VARCHAR2(8),
  failed_reason     VARCHAR2(512),
  created_at        TIMESTAMP       DEFAULT SYSTIMESTAMP NOT NULL,
  updated_at        TIMESTAMP       DEFAULT SYSTIMESTAMP NOT NULL,
  file_blob         BLOB            NOT NULL,
  extracted_text    CLOB,
  embedding         VECTOR(384, FLOAT32),
  CONSTRAINT documents_doc_type_chk
    CHECK (doc_type IN ('invoice', 'purchase_order', 'delivery_note', 'unknown')),
  CONSTRAINT documents_status_chk
    CHECK (status IN ('pending','text_extracted','classified','fields_extracted','embedded','done','failed'))
);

CREATE TABLE document_fields (
  document_id RAW(16)   PRIMARY KEY,
  payload     JSON      NOT NULL,
  created_at  TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
  updated_at  TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
  CONSTRAINT document_fields_document_fk
    FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A vector index makes nearest-neighbor search hit a graph instead of a brute-force scan:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE VECTOR INDEX documents_embedding_idx
  ON documents (embedding)
  ORGANIZATION INMEMORY NEIGHBOR GRAPH
  DISTANCE COSINE
  WITH TARGET ACCURACY 95;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The per-type structured fields live in the&amp;nbsp;&lt;code&gt;document_fields&lt;/code&gt;&amp;nbsp;table as a native&amp;nbsp;&lt;code&gt;JSON&lt;/code&gt;&amp;nbsp;column, written with a&amp;nbsp;&lt;code&gt;MERGE&lt;/code&gt;&amp;nbsp;upsert and read back with&amp;nbsp;&lt;code&gt;JSON_SERIALIZE&lt;/code&gt;. Full DDL is in&amp;nbsp;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/tree/main/apps/idp-oracle-ai-database/packages/db/migrations" rel="noopener noreferrer"&gt;&lt;code&gt;packages/db/migrations&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;&lt;strong&gt;Loading the Embedding Model&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Oracle AI Database generates embeddings&amp;nbsp;&lt;em&gt;inside&lt;/em&gt;&amp;nbsp;the database from an ONNX model you upload once. We use Oracle's pre-built&amp;nbsp;&lt;code&gt;all_MiniLM_L12_v2.onnx&lt;/code&gt;&amp;nbsp;(384-dim output), loaded with&amp;nbsp;&lt;code&gt;DBMS_VECTOR.LOAD_ONNX_MODEL&lt;/code&gt;&amp;nbsp;and registered under the name&amp;nbsp;&lt;code&gt;doc_embedder&lt;/code&gt;.&amp;nbsp;&lt;code&gt;pnpm db:setup-onnx&lt;/code&gt;&amp;nbsp;does the download and load and prints:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Phase 1: ADMIN pulls all_MiniLM_L12_v2.onnx into DATA_PUMP_DIR
✓ all_MiniLM_L12_v2.onnx = 133322334 bytes

Phase 2: idp loads "doc_embedder" from DATA_PUMP_DIR
✓ model doc_embedder loaded
✓ embedding dimension = 384&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Registering the OCI Generative AI Credential&lt;/h3&gt;

&lt;p&gt;The extraction step (the only LLM call in the pipeline) runs&amp;nbsp;&lt;em&gt;from&lt;/em&gt;&amp;nbsp;the database via&amp;nbsp;&lt;code&gt;DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT&lt;/code&gt;, which calls OCI Generative AI. For that, the database needs a credential built from your OCI API key:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BEGIN
  DBMS_VECTOR_CHAIN.CREATE_CREDENTIAL(
    credential_name =&amp;gt; 'OCI_CRED',
    params =&amp;gt; JSON('{
      "user_ocid":        "ocid1.user.oc1..xxxx",
      "tenancy_ocid":     "ocid1.tenancy.oc1..xxxx",
      "compartment_ocid": "ocid1.compartment.oc1..xxxx",
      "private_key":      "&amp;lt;PEM body, without the BEGIN/END lines&amp;gt;",
      "fingerprint":      "aa:bb:cc:..."
    }')
  );
END;
/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You also need the IAM policy&amp;nbsp;&lt;code&gt;allow group &amp;lt;your-group&amp;gt; to manage generative-ai-family in tenancy&lt;/code&gt;.&amp;nbsp;&lt;code&gt;pnpm db:setup-oci-credential&lt;/code&gt;&amp;nbsp;grants the database privileges, opens the outbound network ACL to the OCI Generative AI host, registers&amp;nbsp;&lt;code&gt;OCI_CRED&lt;/code&gt;, and runs a smoke test:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Phase 3: smoke test UTL_TO_GENERATE_TEXT against meta.llama-3.3-70b-instruct in eu-frankfurt-1
  ✓ response: PONG.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you see &lt;code&gt;PONG&lt;/code&gt;, the whole chain (API key → fingerprint → network → credential → model) works.&lt;/p&gt;





&lt;h2&gt;Ingesting Documents&lt;/h2&gt;

&lt;p&gt;The core of the application is the ingest pipeline.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fingest-process-1024x588.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fingest-process-1024x588.png" alt="The six-step ingest pipeline: store the file, extract text, summarize, embed, classify with k-NN, and extract the fields" width="799" height="459"&gt;&lt;/a&gt;The six-step ingest pipeline: store the file, extract text, summarize, embed, classify with k-NN, and extract the fields&lt;p&gt;&lt;/p&gt;

&lt;p&gt;When a document arrives, the pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stores the uploaded file as a&amp;nbsp;&lt;code&gt;BLOB&lt;/code&gt;&amp;nbsp;row with status&amp;nbsp;&lt;code&gt;pending&lt;/code&gt;.&lt;/li&gt;



&lt;li&gt;Calls&amp;nbsp;&lt;code&gt;DBMS_VECTOR_CHAIN.UTL_TO_TEXT&lt;/code&gt;&amp;nbsp;to extract the text and saves it.&lt;/li&gt;



&lt;li&gt;Calls&amp;nbsp;&lt;code&gt;DBMS_VECTOR_CHAIN.UTL_TO_SUMMARY&lt;/code&gt;&amp;nbsp;to generate a short extractive summary.&lt;/li&gt;



&lt;li&gt;Generates a 384-dim embedding with&amp;nbsp;&lt;code&gt;VECTOR_EMBEDDING&lt;/code&gt;&amp;nbsp;using the loaded ONNX model.&lt;/li&gt;



&lt;li&gt;Classifies the document by running a k-NN vector search against the labeled examples —&amp;nbsp;&lt;strong&gt;no LLM call&lt;/strong&gt;.&lt;/li&gt;



&lt;li&gt;Extracts the typed fields with&amp;nbsp;&lt;code&gt;DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It sounds straightforward, and it is — but it's worth pausing on the fact that&amp;nbsp;&lt;strong&gt;one database&lt;/strong&gt;&amp;nbsp;stores the file, extracts its text, summarizes it, computes the embedding, runs the vector search, and makes the LLM call. Let's look at each step.&lt;/p&gt;

&lt;h3&gt;Step 1 — Text Extraction with &lt;code&gt;UTL_TO_TEXT&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The first step extracts all text from the PDF. In the database we use the function&amp;nbsp;&lt;code&gt;DBMS_VECTOR_CHAIN.UTL_TO_TEXT&lt;/code&gt;&amp;nbsp;for that. It can read a file (BLOB) and returns the text within the file. We save the extracted text in the column&amp;nbsp;&lt;code&gt;extracted_text&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;UPDATE documents
SET extracted_text = DBMS_VECTOR_CHAIN.UTL_TO_TEXT(file_blob)
WHERE id = HEXTORAW(:id);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Important caveat:&lt;/strong&gt;&amp;nbsp;&lt;code&gt;UTL_TO_TEXT&lt;/code&gt;&amp;nbsp;can only read embedded text. It can not understand images, scans, or hand-written annotations on your documents. For those, you typically need a vision-capable LLM.&lt;/p&gt;

&lt;h3&gt;Step 2 — Summaries with `UTL_TO_SUMMARY`&lt;/h3&gt;

&lt;p&gt;In the next step, we want a summary of the document. For that, we use the SQL function&amp;nbsp;&lt;code&gt;UTL_TO_SUMMARY&lt;/code&gt;. With the&amp;nbsp;&lt;code&gt;database&lt;/code&gt;&amp;nbsp;provider we use here, the summary is produced by Oracle Text inside the database — an extractive summary of the most representative sentences, not an LLM call. If you want a generative summary instead,&amp;nbsp;&lt;code&gt;UTL_TO_SUMMARY&lt;/code&gt;&amp;nbsp;can also be pointed at an external provider such as Claude or OpenAI.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT DBMS_VECTOR_CHAIN.UTL_TO_SUMMARY(
extracted_text,
JSON('{"provider":"database","glevel":"sentence","numParagraphs":3}')
) FROM documents WHERE id = HEXTORAW(:id);&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Step 3 — Embeddings with &lt;code&gt;VECTOR_EMBEDDING&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Before we can classify by vectors, we need a vector. Oracle generates one inside the database from the ONNX model we loaded earlier:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;UPDATE documents
SET embedding = VECTOR_EMBEDDING(doc_embedder USING extracted_text AS data)
WHERE id = HEXTORAW(:id);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;No external embedding service, no second store. The 384-dim vector ends up in the same row as the BLOB and the extracted text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One important note on size.&lt;/strong&gt;&amp;nbsp;&lt;code&gt;VECTOR_EMBEDDING&lt;/code&gt;&amp;nbsp;here embeds the&amp;nbsp;&lt;em&gt;entire&lt;/em&gt;&amp;nbsp;extracted text in a single call. That is fine since our documents are quite small. For larger documents, you need to&amp;nbsp;&lt;strong&gt;chunk&lt;/strong&gt;&amp;nbsp;your documents first! Oracle has a built-in mechanism for that as well with&amp;nbsp;&lt;code&gt;UTL_TO_CHUNKS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fchunking-flow-1024x268.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Fchunking-flow-1024x268.png" alt="Chunking flow: UTL_TO_TEXT extracts plain text, UTL_TO_CHUNKS splits it into chunks, UTL_TO_EMBEDDINGS turns each chunk into a 384-dim vector" width="799" height="209"&gt;&lt;/a&gt;Chunking flow: UTL_TO_TEXT extracts plain text, UTL_TO_CHUNKS splits it into chunks, UTL_TO_EMBEDDINGS turns each chunk into a 384-dim vector&lt;p&gt;&lt;/p&gt;

&lt;p&gt;In one SQL statement the whole chain looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;-- TEXT -&amp;gt; CHUNKS -&amp;gt; EMBEDDINGS, in one statement
SELECT et.*
FROM documents d,
     DBMS_VECTOR_CHAIN.UTL_TO_EMBEDDINGS(
       DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(
         DBMS_VECTOR_CHAIN.UTL_TO_TEXT(d.file_blob),
         JSON('{ "by":"words", "max":"200", "overlap":"20", "split":"recursively" }')
       ),
       JSON('{ "provider":"database", "model":"doc_embedder" }')
     ) et
WHERE d.id = HEXTORAW(:id);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For us, embedding the documents directly suffices.&lt;/p&gt;

&lt;h3&gt;Step 4 — Classifying with k-NN&lt;/h3&gt;

&lt;p&gt;There are two ways to classify a document:&lt;/p&gt;

&lt;p&gt;1. Ask an LLM "what kind of document is this?"&lt;br&gt;2. Ask your vectors which labeled examples it resembles.&lt;/p&gt;

&lt;p&gt;We go with option 2. Because it doesn't incur any LLM costs. And it gives us the powers of a vector store.&lt;/p&gt;

&lt;p&gt;We run a k-nearest-neighbors search:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;embed the new document&lt;/li&gt;



&lt;li&gt;find its k nearest labeled examples&lt;/li&gt;



&lt;li&gt;take the majority document type among them&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code&gt;// packages/db/src/repositories/documents.ts (abridged)
async classifyByVector(id: string, k = 5, unknownThreshold = 0.5) {
  return withConnection(async (conn) =&amp;gt; {
    const result = await conn.execute(
      `SELECT b.doc_type AS DOC_TYPE,
              VECTOR_DISTANCE(a.embedding, b.embedding, COSINE) AS DISTANCE
       FROM documents a, documents b
       WHERE a.id = HEXTORAW(:id)
         AND b.id != HEXTORAW(:id)
         AND b.embedding IS NOT NULL
         AND b.doc_type IN ('invoice', 'purchase_order', 'delivery_note')
         AND b.status = 'done'
       ORDER BY DISTANCE
       FETCH FIRST :k ROWS ONLY`,
      { id, k },
      { outFormat: oracledb.OUT_FORMAT_OBJECT },
    );
    const neighbors = (result.rows ?? []).map((r) =&amp;gt; ({
      docType: r.DOC_TYPE,
      distance: Number(r.DISTANCE),
    }));

    if (!neighbors.length || neighbors[0].distance &amp;gt; unknownThreshold) {
      return { docType: 'unknown', confidence: 0 };
    }

    const counts: Record&amp;lt;string, number&amp;gt; = {};
    for (const n of neighbors) counts[n.docType] = (counts[n.docType] ?? 0) + 1;
    const [winner, votes] = Object.entries(counts).sort((a, b) =&amp;gt; b[1] - a[1])[0];
    return { docType: winner, confidence: votes / neighbors.length };
  });
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We only compare against labeled examples that finished processing (&lt;code&gt;status = 'done'&lt;/code&gt;), take the&amp;nbsp;&lt;code&gt;k&lt;/code&gt;&amp;nbsp;nearest by cosine distance, and let them vote. If even the closest example is farther than our&amp;nbsp;&lt;code&gt;unknownThreshold&lt;/code&gt;&amp;nbsp;of&amp;nbsp;&lt;code&gt;0.5&lt;/code&gt;, we mark the document&amp;nbsp;&lt;code&gt;unknown&lt;/code&gt;&amp;nbsp;instead of guessing.&lt;/p&gt;

&lt;p&gt;For example, for a new purchase order the nearest neighbors might come back as (nearest first):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;doc_1&lt;/code&gt;&amp;nbsp;— distance&amp;nbsp;&lt;code&gt;0.12&lt;/code&gt;&amp;nbsp;— purchase order&lt;/li&gt;



&lt;li&gt;
&lt;code&gt;doc_2&lt;/code&gt;&amp;nbsp;— distance&amp;nbsp;&lt;code&gt;0.19&lt;/code&gt;&amp;nbsp;— purchase order&lt;/li&gt;



&lt;li&gt;
&lt;code&gt;doc_3&lt;/code&gt;&amp;nbsp;— distance&amp;nbsp;&lt;code&gt;0.24&lt;/code&gt;&amp;nbsp;— purchase order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are purchase orders and well inside the threshold, so we classify the new document as a&amp;nbsp;&lt;strong&gt;purchase order&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;Step 5 — Extracting Fields with &lt;code&gt;UTL_TO_GENERATE_TEXT&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Vectors tell us&amp;nbsp;&lt;strong&gt;what&lt;/strong&gt;&amp;nbsp;a document is. They can't tell us&amp;nbsp;&lt;strong&gt;what's in it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For that we need structured output. For example for an invoice we look for the following schema:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// packages/schemas/src/invoice.ts
export const invoiceFields = z.object({
  envelope: commonEnvelope,
  vendor: z.string(),
  invoiceNumber: z.string(),
  invoiceDate: z.string(),
  dueDate: z.string().nullable(),
  currency: z.string().length(3),
  subtotal: z.number(),
  tax: z.number(),
  total: z.number(),
  lineItems: z.array(invoiceLineItem),
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This data is necessary for our business processes.&lt;/p&gt;

&lt;p&gt;This is the first time we actually need to call an LLM. And we can do that directly from the database again! With the function&amp;nbsp;&lt;code&gt;DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For each of our documents we have such a Zod validation schema. This schema is converted to&amp;nbsp;&lt;code&gt;JSON&lt;/code&gt;&amp;nbsp;and passed onto our LLM call.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// packages/schemas/src/registry.ts
export const fieldsSchemaByType = {
  invoice: invoiceFields,
  purchase_order: purchaseOrderFields,
  delivery_note: deliveryNoteFields,
} as const;

export type ExtractableDocType = keyof typeof fieldsSchemaByType;

export function getJsonSchemaForType(docType: ExtractableDocType): object {
  return zodToJsonSchema(fieldsSchemaByType[docType], {
    target: 'jsonSchema7',
    $refStrategy: 'none',
  });
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we call the database function like that:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT(
  :prompt,
  JSON('{
    "provider":        "ocigenai",
    "credential_name": "OCI_CRED",
    "url":             "https://inference.generativeai.eu-frankfurt-1.oci.oraclecloud.com/20231130/actions/chat",
    "model":           "meta.llama-3.3-70b-instruct",
    "chatRequest":     { "maxTokens": 4096, "temperature": 0 }
  }')
) AS out FROM dual;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In our &lt;code&gt;:prompt&lt;/code&gt; we say:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;You extract structured fields from a document. Respond with a single JSON object….
JSON Schema:
${JSON.stringify(jsonSchema)}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After the call returns, we validate all data against our Zod schema to make sure all fields are available.&lt;br&gt;If they are not, the processing &lt;strong&gt;fails&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is all for ingesting! We didn't need to leave our one data store at all.&lt;/p&gt;





&lt;h2&gt;Validate It End to End&lt;/h2&gt;

&lt;p&gt;With the database set up, run the API and seed it with the committed sample PDFs:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pnpm dev:api # Hono on :8787
pnpm seed # uploads every sample PDF and waits for ingest&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;pnpm seed prints a per-file result and a summary you can sanity-check:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;✓ invoice-01.pdf type=invoice status=done 3.1s
✓ purchase-order-01.pdf type=purchase_order status=done 2.8s
✓ delivery-note-01.pdf type=delivery_note status=done 2.6s
...
Summary
by type: {"invoice":10,"purchase_order":10,"delivery_note":10}
by status: {"done":30}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If a document lands on&amp;nbsp;&lt;code&gt;status=failed&lt;/code&gt;, the reason is stored in&amp;nbsp;&lt;code&gt;documents.failed_reason&lt;/code&gt;&amp;nbsp;(a common one is&amp;nbsp;&lt;code&gt;no_text_extracted&lt;/code&gt;&amp;nbsp;for a scanned/image PDF — see the OCR caveat above). The two provisioning guides each end with a troubleshooting table covering the usual wallet, credential, and region errors.&lt;/p&gt;





&lt;h2&gt;Deployment&lt;/h2&gt;

&lt;p&gt;You can deploy the whole thing into your own AWS account:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pnpm cdk:deploy&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It's hosted on S3 + CloudFront, with the API in a single Lambda Function URL. At low scale it costs essentially nothing.&lt;/p&gt;

&lt;p&gt;Have fun trying it out!&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;FAQs&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What kind of documents does the app process?&lt;/strong&gt;&lt;br&gt;Purchase orders, delivery notes, and invoices. Those three documents cover the basic procure-to-pay flow: ordering goods, receiving them, and getting billed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why use vectors for document classification?&lt;/strong&gt;&lt;br&gt;Because documents of the same type tend to land near each other in vector space. A new document can be classified by comparing its embedding to labeled examples, without fine-tuning a model or paying for an LLM call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: When does the pipeline use an LLM?&lt;/strong&gt;&lt;br&gt;Only during structured field extraction. After the document type is known, the app asks OCI Generative AI to return fields that match the right schema, such as invoice totals, dates, currency, and line items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What happens if the uploaded PDF is scanned or image-only?&lt;/strong&gt;&lt;br&gt;The article calls this out as a caveat: &lt;code&gt;UTL_TO_TEXT&lt;/code&gt; can read embedded text, but it does not understand scanned images or handwriting. Those cases usually need OCR or a vision-capable model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why is one database such a big deal here?&lt;/strong&gt;&lt;br&gt;Because the traditional version of this stack would need several systems: one for files, one for relational data, one for JSON or key-value data, one for vectors, plus external AI calls. Here, the document, extracted text, embedding, classification, structured fields, and AI workflow stay in one place.&lt;/p&gt;





&lt;h2&gt;Summary&lt;/h2&gt;

&lt;p&gt;In this article, we went through a whole IDP pipeline. From uploading PDFs, to embedding vectors, classifying documents with k-NN, and even making our own call to OCI Generative AI. All within one data store.&lt;/p&gt;

&lt;p&gt;This is one of the biggest benefits of using a converged database such as Oracle AI Database.&lt;/p&gt;

&lt;p&gt;In a traditional stack this would have been at least 4 systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 (BLOB)&lt;/li&gt;



&lt;li&gt;Postgres (relational)&lt;/li&gt;



&lt;li&gt;Pinecone (Vectors)&lt;/li&gt;



&lt;li&gt;DynamoDB/MongoDB (JSON)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…and additionally API calls to external LLM providers. With our used database all of that stays within one system.&lt;/p&gt;

</description>
      <category>documentprocessor</category>
      <category>oracle</category>
      <category>ai</category>
      <category>database</category>
    </item>
    <item>
      <title>Production RAG Evaluation: Keyword, Vector, SQL, or Hybrid Search?</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:17:57 +0000</pubDate>
      <link>https://dev.to/oracledevs/production-rag-evaluation-keyword-vector-sql-or-hybrid-search-1084</link>
      <guid>https://dev.to/oracledevs/production-rag-evaluation-keyword-vector-sql-or-hybrid-search-1084</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Production RAG evaluation should measure whether the system retrieves the right evidence, answers from that evidence, respects permissions, handles fresh data, and refuses unsupported questions. Compare keyword, vector, SQL, and hybrid retrieval against the same question set. Use retrieval metrics, answer-quality checks, and production failure tests before deciding which path belongs in the application.&lt;/p&gt;

&lt;p&gt;RAG, or retrieval-augmented generation, retrieves evidence from an authoritative source and gives that evidence to a language model before it answers. Production RAG evaluation tests both halves of that process: whether retrieval found the right evidence and whether the generated answer used it correctly.&lt;/p&gt;

&lt;p&gt;A RAG demo can pass with a few clean documents and one friendly question. Production is where the system starts meeting real users.&lt;/p&gt;

&lt;p&gt;They ask for exact IDs. They ask vague questions. They ask about data that changed five minutes ago. They ask across tenants, versions, tables, PDFs, status fields, and long conversations. Sometimes the right answer is not in the corpus at all.&lt;/p&gt;

&lt;p&gt;That is why the useful question is not "Should I use vector search or hybrid search?" The useful question is "Which retrieval path gives the application the right evidence, under the constraints this system actually has?"&lt;/p&gt;

&lt;p&gt;This article turns that question into a practical evaluation plan for developers building RAG with Oracle AI Database. The companion notebook benchmarks three retrieval methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keyword retrieval for exact terms, identifiers, and lexical matches&lt;/li&gt;
&lt;li&gt;vector retrieval for semantic similarity and vocabulary mismatch&lt;/li&gt;
&lt;li&gt;RRF hybrid retrieval when keyword and vector candidates both add value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SQL or natural-language-to-SQL is treated as a separate route for current structured data. Evaluate it with query-correctness, permission, freshness, and result-limit tests rather than forcing it into document-retrieval metrics.&lt;/p&gt;

&lt;p&gt;The goal is not to declare a universal winner. The goal is to build the evidence needed to choose the right retrieval strategy for a production RAG system.&lt;/p&gt;

&lt;h2&gt;Key takeaways&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Evaluate retrieval and generated answers separately. A model can retrieve relevant evidence and still produce an unsupported or incorrect answer.&lt;/li&gt;
&lt;li&gt;Use SQL or NL2SQL for current structured facts, and use keyword, vector, or hybrid retrieval for unstructured content. Route mixed questions across both.&lt;/li&gt;
&lt;li&gt;Compare keyword, vector, and RRF hybrid retrieval against the same ground-truth question set before choosing a production default.&lt;/li&gt;
&lt;li&gt;Test freshness, tenant isolation, metadata filters, exact identifiers, citations, and abstention alongside average retrieval metrics.&lt;/li&gt;
&lt;li&gt;Treat hybrid search as a measured option, not an automatic winner. Keep numerical claims unpublished until they are traceable to the notebook exports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;What does production RAG look like?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Production RAG is a measured retrieval and answer system. It needs repeatable ingestion, versioned chunks, access controls, freshness rules, citations, abstention, observability, and regression tests. Features such as BM25, vector search, RRF, reranking, HyDE, and incremental indexing only matter when you can prove they improve the answers users actually need.&lt;/p&gt;

&lt;p&gt;A common developer question is: "I already have hybrid search, reranking, citations, and a no-hallucination policy. What else makes RAG production ready?"&lt;/p&gt;

&lt;p&gt;The missing piece is usually the evaluation harness. Retrieval features are easy to add. Proving that they still work after a chunking change, embedding model swap, schema change, or reranker update is the harder production problem.&lt;/p&gt;

&lt;p&gt;A production RAG system should have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A ground-truth question set with required evidence and expected answer behaviour.&lt;/li&gt;
&lt;li&gt;Retrieval metrics for keyword, vector, SQL, and hybrid paths.&lt;/li&gt;
&lt;li&gt;Answer evaluation for groundedness, correctness, citation validity, and abstention.&lt;/li&gt;
&lt;li&gt;Versioned ingestion, parsing, chunking, embeddings, prompts, and retrieval configuration.&lt;/li&gt;
&lt;li&gt;Metadata filters for tenant, permission, source, status, freshness, and document version.&lt;/li&gt;
&lt;li&gt;Observability for empty results, retrieval misses, latency, citation failures, and stale evidence.&lt;/li&gt;
&lt;li&gt;A rollback path when a new retrieval change improves the average but breaks an important query class.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chunking needs its own tests. Short support notes, long PDFs, policy documents, tables, code, and product documentation do not share one ideal chunk size. Treat chunk size, overlap, parsing, parent document links, and table handling as versioned configuration. Then test those choices against the same question set before you publish the change.&lt;/p&gt;

&lt;p&gt;This is the practical bar: if you cannot detect a broken retrieval change, the system is not production ready yet.&lt;/p&gt;

&lt;h2&gt;How do I improve a RAG pipeline over a sparse SQL database?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Do not embed every row from a sparse SQL database. Route structured questions to SQL, filter null and empty fields before building searchable text, and embed only fields with useful language. Use keyword retrieval for exact IDs and status values, vector retrieval for descriptive text, and metadata filters for valid rows.&lt;/p&gt;

&lt;p&gt;A common developer question is: "My database has many empty tables and null columns. I embedded rows, but the model retrieves poor context. How do I make the RAG pipeline efficient?"&lt;/p&gt;

&lt;p&gt;The problem is usually not the model. The problem is that the retrieval corpus contains low-information chunks. If a row has many empty columns, turning the whole row into text gives the retriever noise that still competes for context-window space.&lt;/p&gt;

&lt;p&gt;Separate the jobs before adding more retrieval tricks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User question&lt;/th&gt;
&lt;th&gt;Best first route&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Aggregations, counts, dates, filters&lt;/td&gt;
&lt;td&gt;SQL or NL2SQL&lt;/td&gt;
&lt;td&gt;"How many open high risks have no mitigation?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact identifiers and controlled values&lt;/td&gt;
&lt;td&gt;Keyword plus SQL predicates&lt;/td&gt;
&lt;td&gt;"Show risk RSK-1042 with status OPEN"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Narrative similarity&lt;/td&gt;
&lt;td&gt;Vector retrieval over meaningful text&lt;/td&gt;
&lt;td&gt;"Find incidents involving delayed supplier access"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed exact and semantic intent&lt;/td&gt;
&lt;td&gt;Keyword and vector candidates fused with RRF&lt;/td&gt;
&lt;td&gt;"OPEN risks similar to the supplier outage"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Create a retrieval view rather than embedding every physical row. The view should include stable IDs, required business metadata, and a deliberate &lt;code&gt;search_text&lt;/code&gt; field built from non-empty descriptive columns.&lt;/p&gt;

&lt;p&gt;For example, a useful retrieval record might include:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;risk_id: RSK-1042
tenant_id: acme
status: OPEN
severity: HIGH
search_text: Supplier access delay caused a missed shipment window. Mitigation owner is reviewing backup routing options.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That is different from serialising twenty columns where half the fields are empty. Preserve nulls as database state for SQL reasoning. Do not turn the word "null" into semantic content unless the absence itself is the thing being searched.&lt;/p&gt;

&lt;p&gt;For sparse relational data, evaluate routes separately before combining them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL quality: does the generated or selected SQL return the correct rows?&lt;/li&gt;
&lt;li&gt;keyword quality: do exact IDs, codes, statuses, and controlled terms match reliably?&lt;/li&gt;
&lt;li&gt;vector quality: do descriptive fields retrieve semantically related incidents or risks?&lt;/li&gt;
&lt;li&gt;hybrid quality: does RRF improve mixed queries without adding noisy rows?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RRF is useful when both keyword and vector result lists contain useful evidence. It is not a cleanup step for a bad corpus.&lt;/p&gt;

&lt;h2&gt;How should RAG handle real-time dynamic data?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Frequently changing structured data should usually be queried live, not re-embedded on every update. Use SQL or NL2SQL for current rows, and reserve RAG for unstructured text that benefits from semantic retrieval. For changing documents, update affected embeddings, track freshness metadata, and test stale-versus-current answer behaviour.&lt;/p&gt;

&lt;p&gt;A common developer question is: "My backend data updates every five minutes. Should I use RAG, SQL, caching, MCP, tool calling, or something else?"&lt;/p&gt;

&lt;p&gt;Start by asking what kind of data needs to be fresh.&lt;/p&gt;

&lt;p&gt;If the answer lives in current structured rows, query the database at request time. Re-embedding the full dataset every five minutes creates a constant race with the source of truth. The vector copy can become stale before the indexing job finishes.&lt;/p&gt;

&lt;p&gt;If the answer lives in unstructured documents, use retrieval. But make freshness explicit. Store source timestamps, version IDs, current-version flags, ingestion times, and deletion state. Then evaluate whether the system chooses the current evidence instead of a stale chunk.&lt;/p&gt;

&lt;p&gt;Use a simple routing model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Current account, risk, ticket, inventory, or status data&lt;/td&gt;
&lt;td&gt;SQL or NL2SQL against governed live tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policies, manuals, support notes, or PDFs&lt;/td&gt;
&lt;td&gt;Keyword, vector, or hybrid retrieval over indexed documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current facts plus explanatory documents&lt;/td&gt;
&lt;td&gt;SQL for current state, retrieval for explanation, answer composition with citations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-session user preferences or agent context&lt;/td&gt;
&lt;td&gt;Scoped agent memory, not a document index&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Caching, MCP, and tool calling are useful, but they solve different problems.&lt;/p&gt;

&lt;p&gt;A cache reduces repeated work, but it must be invalidated when source data changes. MCP can expose database tools to an assistant, but it does not make stale data fresh. Tool calling lets a planner choose SQL, retrieval, or another service, but every tool still needs permissions, timeouts, result limits, and traceable outputs.&lt;/p&gt;

&lt;p&gt;The production pattern is not "embed everything." It is "route each question to the freshest authoritative source, then evaluate whether the answer used that source correctly."&lt;/p&gt;

&lt;h2&gt;When should I use SQL, vector search, keyword search, or hybrid search?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Use SQL for structured facts, keyword search for exact terms, vector search for semantic similarity, and hybrid search when the same workload contains both lexical and semantic queries. The right choice depends on query distribution, freshness needs, permission rules, latency, and measured retrieval quality.&lt;/p&gt;

&lt;p&gt;Each retrieval path has a job.&lt;/p&gt;

&lt;p&gt;SQL is the right first route when the question is about rows, filters, joins, dates, counts, totals, statuses, permissions, or current business state. If the data already has structure, keep using it.&lt;/p&gt;

&lt;p&gt;Keyword search is strong when the user supplies exact terms: error codes, product names, SKUs, ticket IDs, risk IDs, function names, policy clauses, and other tokens where spelling matters.&lt;/p&gt;

&lt;p&gt;Vector search is useful when the query and the document use different language for the same concept. It helps with paraphrases, fuzzy intent, natural-language descriptions, and vocabulary mismatch.&lt;/p&gt;

&lt;p&gt;Hybrid search is appropriate when the workload has both patterns and both result lists contribute. A common implementation is Reciprocal Rank Fusion, or RRF. It retrieves candidates from keyword and vector search independently, then fuses ranks:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;RRF score(document) = sum(1 / (60 + rank_in_result_set))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;RRF avoids pretending that keyword scores and vector distances live on the same numeric scale. A document found by both routes receives contributions from both rankings. A document found by only one route can still survive if it ranks well enough.&lt;/p&gt;

&lt;p&gt;The mistake is using "hybrid" as a default badge of seriousness. Hybrid retrieval adds query work, tuning, latency, and operational complexity. Add it when the evaluation shows it improves the questions that matter.&lt;/p&gt;

&lt;h2&gt;What metrics should I use for RAG evaluation?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Use retrieval metrics to test whether the system finds the right evidence, then answer metrics to test whether the model uses that evidence correctly. Retrieval metrics include NDCG, MAP, recall, and precision. Answer metrics should cover groundedness, correctness, citation validity, and abstention quality.&lt;/p&gt;

&lt;p&gt;RAG evaluation has two layers that should not be collapsed into one score.&lt;/p&gt;

&lt;p&gt;First, evaluate retrieval. Retrieval metrics are model-independent and can run without a generation API. That makes them useful for frequent regression checks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;th&gt;Useful question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NDCG@k&lt;/td&gt;
&lt;td&gt;Whether relevant documents appear high in the ranking&lt;/td&gt;
&lt;td&gt;Did the retriever put the best evidence near the top?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall@k&lt;/td&gt;
&lt;td&gt;How much known relevant evidence was recovered&lt;/td&gt;
&lt;td&gt;Did retrieval miss evidence the answer needed?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precision@k&lt;/td&gt;
&lt;td&gt;How much of the returned set was relevant&lt;/td&gt;
&lt;td&gt;How much noise did retrieval add?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAP@k&lt;/td&gt;
&lt;td&gt;Ranking quality across multiple queries&lt;/td&gt;
&lt;td&gt;Is performance consistently useful across the test set?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The notebook records retrieval results for keyword, vector, and RRF hybrid retrieval:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;NDCG@10&lt;/th&gt;
&lt;th&gt;MAP@10&lt;/th&gt;
&lt;th&gt;Recall@10&lt;/th&gt;
&lt;th&gt;Precision@10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keyword&lt;/td&gt;
&lt;td&gt;{{KEYWORD_NDCG_10}}&lt;/td&gt;
&lt;td&gt;{{KEYWORD_MAP_10}}&lt;/td&gt;
&lt;td&gt;{{KEYWORD_RECALL_10}}&lt;/td&gt;
&lt;td&gt;{{KEYWORD_PRECISION_10}}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector&lt;/td&gt;
&lt;td&gt;{{VECTOR_NDCG_10}}&lt;/td&gt;
&lt;td&gt;{{VECTOR_MAP_10}}&lt;/td&gt;
&lt;td&gt;{{VECTOR_RECALL_10}}&lt;/td&gt;
&lt;td&gt;{{VECTOR_PRECISION_10}}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RRF hybrid&lt;/td&gt;
&lt;td&gt;{{HYBRID_NDCG_10}}&lt;/td&gt;
&lt;td&gt;{{HYBRID_MAP_10}}&lt;/td&gt;
&lt;td&gt;{{HYBRID_RECALL_10}}&lt;/td&gt;
&lt;td&gt;{{HYBRID_PRECISION_10}}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These placeholders must stay placeholders until the notebook has been run cleanly and the exported metrics have been reviewed. Do not turn them into claims manually.&lt;/p&gt;

&lt;p&gt;Second, evaluate generated answers. A retrieved document can be relevant while the generated answer is still wrong, unsupported, overconfident, or badly cited.&lt;/p&gt;

&lt;p&gt;Use answer-level checks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;groundedness: does the answer stay within the retrieved context?&lt;/li&gt;
&lt;li&gt;correctness: does it match the reference answer?&lt;/li&gt;
&lt;li&gt;citation validity: do cited documents support the claims attached to them?&lt;/li&gt;
&lt;li&gt;abstention quality: does the system refuse when the corpus does not contain enough evidence?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Model-based judging is a review signal, not ground truth. Keep the raw generated answers, retrieved IDs, reference answers, scores, and rationales so a human can inspect surprising results.&lt;/p&gt;

&lt;h2&gt;How do I test production failure modes?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Add a production challenge set next to the clean benchmark. Include exact identifiers, paraphrases, stale and current versions, tenant isolation, metadata filtering, messy questions, multi-hop questions, and unsupported questions. These cases catch the failures that average retrieval scores often hide.&lt;/p&gt;

&lt;p&gt;A clean benchmark is useful, but production traffic is not clean. Developers need a small challenge set that reflects the application’s actual risk.&lt;/p&gt;

&lt;p&gt;The notebook includes ten production-style cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An exact identifier query.&lt;/li&gt;
&lt;li&gt;A paraphrase query.&lt;/li&gt;
&lt;li&gt;A stale version query that should not use old evidence.&lt;/li&gt;
&lt;li&gt;A current version query that should prefer the latest evidence.&lt;/li&gt;
&lt;li&gt;A tenant isolation query.&lt;/li&gt;
&lt;li&gt;A messy user question with irrelevant wording.&lt;/li&gt;
&lt;li&gt;A question requiring two documents.&lt;/li&gt;
&lt;li&gt;An unsupported question where the system should abstain.&lt;/li&gt;
&lt;li&gt;A lexical entity query.&lt;/li&gt;
&lt;li&gt;An embedding-migration sequence question.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those cases should be adapted to the domain before publication. For a risk-register system, include risk IDs, mitigation statuses, sparse rows, tenant boundaries, and null-heavy records. For a support assistant, include error codes, product versions, stale documentation, and unsupported product claims. For a real-time operational chatbot, include recently changed records and cache-invalidation cases.&lt;/p&gt;

&lt;p&gt;The key is to keep the challenge set stable. Run it before changing chunking, embedding models, metadata filters, SQL generation, rerankers, or prompts. If a change improves the average but breaks tenant isolation or stale-data handling, it is not an improvement.&lt;/p&gt;

&lt;h2&gt;Decision guide: keyword, vector, SQL, or hybrid&lt;/h2&gt;

&lt;p&gt;Use the query type and failure cost to pick the starting route.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your workload contains&lt;/th&gt;
&lt;th&gt;Start with&lt;/th&gt;
&lt;th&gt;Then test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Current structured facts&lt;/td&gt;
&lt;td&gt;SQL or NL2SQL&lt;/td&gt;
&lt;td&gt;Query correctness, permissions, freshness, and safe result limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error codes, IDs, SKUs, names, exact clauses&lt;/td&gt;
&lt;td&gt;Keyword retrieval&lt;/td&gt;
&lt;td&gt;Whether vector search improves paraphrases without losing exact matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Natural-language questions and vocabulary mismatch&lt;/td&gt;
&lt;td&gt;Vector retrieval&lt;/td&gt;
&lt;td&gt;Exact-identifier failures and metadata constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both lexical and semantic questions&lt;/td&gt;
&lt;td&gt;RRF hybrid retrieval&lt;/td&gt;
&lt;td&gt;Candidate depth, latency, and ranking gains over both baselines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sparse relational tables&lt;/td&gt;
&lt;td&gt;SQL plus selective semantic fields&lt;/td&gt;
&lt;td&gt;Null handling, exact IDs, and route-level quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data that changes every few minutes&lt;/td&gt;
&lt;td&gt;Live SQL for structured data, incremental indexing for documents&lt;/td&gt;
&lt;td&gt;Stale-versus-current answer behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulated or multi-tenant data&lt;/td&gt;
&lt;td&gt;Any method with mandatory metadata filters&lt;/td&gt;
&lt;td&gt;Isolation, auditability, deletion, and freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsupported questions&lt;/td&gt;
&lt;td&gt;Retrieval plus abstention policy&lt;/td&gt;
&lt;td&gt;False answers and false refusals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important move is not choosing one retrieval method forever. It is making the retrieval route explicit, measuring it, and changing it only when the evidence says the system gets better.&lt;/p&gt;

&lt;h2&gt;Frequently asked questions about production RAG evaluation&lt;/h2&gt;

&lt;h3&gt;When should I use hybrid search instead of vector search for RAG?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; No. Hybrid search helps when a workload contains both exact lexical queries and semantic queries, and when both candidate lists contribute relevant evidence. It can add latency and tuning work. Compare hybrid retrieval against keyword and vector baselines on the same question set before adopting it.&lt;/p&gt;

&lt;h3&gt;Can I evaluate RAG without an LLM API?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Yes. Retrieval evaluation can run without a generation model. Use relevance judgements to calculate NDCG, MAP, recall, and precision for each retrieval method. Add answer-level evaluation later to test groundedness, correctness, citation validity, and abstention.&lt;/p&gt;

&lt;h3&gt;Should I use RAG or NL2SQL for a SQL database?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Use SQL or NL2SQL when the answer depends on structured rows, joins, filters, dates, counts, or current state. Use RAG for unstructured documents and descriptive text. Many production applications need routing: SQL for live facts and retrieval for supporting explanations.&lt;/p&gt;

&lt;h3&gt;What is Reciprocal Rank Fusion in hybrid search?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Reciprocal Rank Fusion, or RRF, combines independently ranked keyword and vector results by rank position. It avoids comparing raw keyword scores with vector distances directly. Documents that rank highly in either list can survive, while documents found by both methods receive contributions from both rankings.&lt;/p&gt;

&lt;h3&gt;How often should I evaluate a production RAG system?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Run retrieval regression tests whenever chunking, parsing, embeddings, indexes, filters, rerankers, prompts, or source schemas change. Also run a scheduled production challenge set to detect corpus drift, stale evidence, permission failures, and changing user-query patterns.&lt;/p&gt;

&lt;h2&gt;Next steps&lt;/h2&gt;

&lt;p&gt;For Oracle AI Database RAG, start with the implementation and documentation that matches the route you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/oracle_rag_with_evals.ipynb" rel="noopener noreferrer"&gt;Run the production RAG evaluation notebook&lt;/a&gt; to compare keyword, vector, and RRF hybrid retrieval and export the evidence used by this article.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/" rel="noopener noreferrer"&gt;Read the Oracle AI Vector Search User's Guide&lt;/a&gt; for vector data, indexes, similarity search, and retrieval implementation details.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/understand-hybrid-search.html" rel="noopener noreferrer"&gt;Understand hybrid search in Oracle AI Database&lt;/a&gt; for keyword and semantic search modes, RRF, weighted RRF, and score fusion.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/selai/select-ai.html" rel="noopener noreferrer"&gt;Use Select AI for natural-language interaction with a database&lt;/a&gt; when current structured data should be queried through SQL or NL2SQL.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.oracle.com/en/database/oracle/agent-memory/26.6/guide/get-started.html" rel="noopener noreferrer"&gt;Get started with Oracle AI Agent Memory&lt;/a&gt; when the application also needs persistent, scoped memory across agent sessions.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rag</category>
      <category>oracle</category>
      <category>ai</category>
      <category>database</category>
    </item>
    <item>
      <title>How to Troubleshoot Vector Search in an AI Application</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:16:20 +0000</pubDate>
      <link>https://dev.to/oracledevs/how-to-troubleshoot-vector-search-in-an-ai-application-224m</link>
      <guid>https://dev.to/oracledevs/how-to-troubleshoot-vector-search-in-an-ai-application-224m</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Troubleshoot vector search by isolating the retrieval pipeline one stage at a time. Check that document and query embeddings use the same model, dimensions match the vector column and index, chunks preserve useful context, filters are not hiding relevant rows, top-k results contain expected evidence, and hybrid search is tested when exact terms matter.&lt;/p&gt;

&lt;p&gt;When an AI application gives a bad answer, the model may not be the problem. Often the answer was not present in the retrieved context. Vector search troubleshooting should start before generation: inspect embeddings, chunk text, metadata filters, similarity scores, index state, and retrieval results directly.&lt;/p&gt;

&lt;p&gt;For Oracle AI Database, the useful posture is simple: keep vectors, source metadata, SQL filters, and evaluation checks close enough that you can debug retrieval like an application data path, not like a black box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debug retrieval before changing prompts.&lt;/li&gt;



&lt;li&gt;Check embedding consistency and vector dimensions first.&lt;/li&gt;



&lt;li&gt;Use known-query tests and the RAG evaluation notebook to measure changes.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I know whether vector search is the problem?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Run the user query, inspect the top-k chunks or rows, and ask whether the answer is actually present. If the expected evidence is missing, too low, stale, filtered out, or duplicated, the failure is retrieval-side. If the evidence is correct but the answer is wrong, investigate generation, prompting, citations, or answer evaluation.&lt;/p&gt;

&lt;p&gt;Use a two-step diagnostic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run retrieval only.&lt;/li&gt;



&lt;li&gt;Inspect the returned chunks without the LLM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are the expected source documents in top-k?&lt;/li&gt;



&lt;li&gt;Is the correct chunk ranked high enough?&lt;/li&gt;



&lt;li&gt;Are similarity scores clustered or clearly separated?&lt;/li&gt;



&lt;li&gt;Are irrelevant chunks crowding out useful evidence?&lt;/li&gt;



&lt;li&gt;Did a metadata, tenant, version, or permission filter remove the right answer?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieval inspection is faster than prompt tuning.&lt;/li&gt;



&lt;li&gt;Top-k evidence should be readable and attributable.&lt;/li&gt;



&lt;li&gt;If the answer is not in retrieved context, generation cannot reliably fix it.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Are my embeddings consistent?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Poor vector search often starts with inconsistent embeddings. Use the same embedding model and preprocessing path for documents and queries. Record the embedding model ID, vector dimension, chunking version, parser version, and embedding timestamp so mixed-model or stale-vector problems are visible.&lt;/p&gt;

&lt;p&gt;Check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;document embeddings created by one model and query embeddings created by another&lt;/li&gt;



&lt;li&gt;vectors with dimensions that do not match the table or index configuration&lt;/li&gt;



&lt;li&gt;a partial embedding-model migration&lt;/li&gt;



&lt;li&gt;preprocessing differences between ingestion and query time&lt;/li&gt;



&lt;li&gt;HTML, boilerplate, or table formatting embedded inconsistently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production systems, store the embedding model name and version with every vector row. That makes troubleshooting possible when quality drops after a model or preprocessing change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embedding model drift can look like poor search quality.&lt;/li&gt;



&lt;li&gt;Vector dimensions should be validated before retrieval tests.&lt;/li&gt;



&lt;li&gt;Store embedding metadata with the vector row.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Are chunking and parsing hurting retrieval?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Chunking can make vector search fail even when embeddings and indexes are correct. Chunks that are too large mix unrelated topics. Chunks that are too small lose context. Tables, PDFs, code, transcripts, and nested sections need structure-aware parsing before they become useful retrieval evidence.&lt;/p&gt;

&lt;p&gt;Inspect failed retrieved chunks manually. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;headings missing from the chunk&lt;/li&gt;



&lt;li&gt;tables split away from headers&lt;/li&gt;



&lt;li&gt;sentences cut in the middle&lt;/li&gt;



&lt;li&gt;repeated boilerplate dominating embeddings&lt;/li&gt;



&lt;li&gt;source IDs or page references missing&lt;/li&gt;



&lt;li&gt;transcript chunks without speakers or timestamps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DBMS_VECTOR_CHAIN can help build text-processing and chunking workflows, but the production decision is still empirical: run the same known-query set before and after a chunking change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Good chunk text should make sense outside the original document.&lt;/li&gt;



&lt;li&gt;Bad parsing can make every vector index look weak.&lt;/li&gt;



&lt;li&gt;Chunking changes need retrieval regression tests.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Are filters, ACLs, or metadata hiding good results?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Vector search can return poor results when the right evidence exists but mandatory filters exclude it. Check tenant IDs, ACLs, document status, source version, language, date range, delete flags, and current-version markers before assuming similarity search failed.&lt;/p&gt;

&lt;p&gt;This is common in enterprise RAG. A query can fail because:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Filter issue&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Troubleshooting check&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tenant mismatch&lt;/td&gt;
&lt;td&gt;Correct document does not appear&lt;/td&gt;
&lt;td&gt;Run the same query with expected tenant scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale current flag&lt;/td&gt;
&lt;td&gt;Old content ranks or new content is hidden&lt;/td&gt;
&lt;td&gt;Compare source timestamp and chunk state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete flag missing&lt;/td&gt;
&lt;td&gt;Removed content still appears&lt;/td&gt;
&lt;td&gt;Verify deleted chunks are excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over-strict metadata filter&lt;/td&gt;
&lt;td&gt;Top-k is empty or weak&lt;/td&gt;
&lt;td&gt;Test filters one at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACL propagation gap&lt;/td&gt;
&lt;td&gt;User sees too much or too little&lt;/td&gt;
&lt;td&gt;Compare source ACLs with chunk metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metadata filters are part of retrieval, not a post-processing detail.&lt;/li&gt;



&lt;li&gt;Empty or weak results can be a filter problem.&lt;/li&gt;



&lt;li&gt;Troubleshooting should log both the vector query and the applied filters.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Is the vector index configured and queried correctly?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Validate that vectors are stored in the expected column, the query uses the same vector field, the index is built and usable, the distance metric matches the embedding model, and the top-k or threshold setting is appropriate for the application.&lt;/p&gt;

&lt;p&gt;At minimum, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the vector column is populated&lt;/li&gt;



&lt;li&gt;the query embedding has the expected dimension&lt;/li&gt;



&lt;li&gt;the index exists and is available&lt;/li&gt;



&lt;li&gt;the query uses the intended vector column&lt;/li&gt;



&lt;li&gt;the similarity metric is appropriate&lt;/li&gt;



&lt;li&gt;top-k is large enough to expose relevant candidates&lt;/li&gt;



&lt;li&gt;thresholds are not cutting off acceptable results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If approximate nearest neighbor settings are used, test recall against a smaller exact or high-recall baseline when possible. ANN tuning is a tradeoff between latency and recall; do not tune it without a known-query set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Index state and query field mistakes are common and easy to miss.&lt;/li&gt;



&lt;li&gt;Top-k and threshold settings should be measured, not guessed.&lt;/li&gt;



&lt;li&gt;ANN tuning should be validated against retrieval quality.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;When should I compare vector-only and hybrid search?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Compare vector-only and hybrid search when users ask for exact identifiers, error codes, product names, file names, commands, versions, or clauses. Vector search handles semantic similarity. Keyword search handles exact lexical evidence. Hybrid search should prove it improves the workload before becoming the default.&lt;/p&gt;

&lt;p&gt;Use this decision table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Query type&lt;/th&gt;
&lt;th&gt;Likely failure in vector-only search&lt;/th&gt;
&lt;th&gt;What to test&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Error code or product ID&lt;/td&gt;
&lt;td&gt;Exact token gets buried&lt;/td&gt;
&lt;td&gt;Keyword and hybrid retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Natural-language paraphrase&lt;/td&gt;
&lt;td&gt;Keyword may miss synonyms&lt;/td&gt;
&lt;td&gt;Vector retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clause, command, or file name&lt;/td&gt;
&lt;td&gt;Similar text outranks exact text&lt;/td&gt;
&lt;td&gt;Hybrid retrieval with RRF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed exact and semantic query&lt;/td&gt;
&lt;td&gt;One signal dominates&lt;/td&gt;
&lt;td&gt;Keyword, vector, and hybrid baselines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Oracle AI Database supports hybrid search patterns that combine full-text and vector similarity search. The key is to compare routes against the same questions, not separate anecdotes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hybrid search is a measured option, not a badge.&lt;/li&gt;



&lt;li&gt;Exact identifiers need lexical signals.&lt;/li&gt;



&lt;li&gt;The RAG evaluation notebook is the right proof path for comparisons.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I measure whether troubleshooting improved retrieval?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Create a small evaluation set with known questions, expected documents, forbidden documents, expected rank, and notes about failure mode. Track recall@k, precision@k, MRR, NDCG, and examples of failed queries before and after each change.&lt;/p&gt;

&lt;p&gt;Start with a table like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Expected evidence&lt;/th&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;Pass condition&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"How do I reset my API key?"&lt;/td&gt;
&lt;td&gt;API key rotation guide&lt;/td&gt;
&lt;td&gt;Chunking or filter miss&lt;/td&gt;
&lt;td&gt;Expected chunk appears in top 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"ORA-12345 error"&lt;/td&gt;
&lt;td&gt;Error reference&lt;/td&gt;
&lt;td&gt;Exact identifier miss&lt;/td&gt;
&lt;td&gt;Exact match appears in top 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Current refund policy"&lt;/td&gt;
&lt;td&gt;Latest policy version&lt;/td&gt;
&lt;td&gt;Stale embedding&lt;/td&gt;
&lt;td&gt;Current version appears; old version excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/oracle_rag_with_evals.ipynb" rel="noopener noreferrer"&gt;RAG evaluation notebook&lt;/a&gt; already gives a useful pattern: separate retrieval methods, run a challenge set, export metrics, and keep a manifest so results are reproducible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Troubleshooting needs before-and-after metrics.&lt;/li&gt;



&lt;li&gt;Keep failed queries as regression tests.&lt;/li&gt;



&lt;li&gt;Do not publish a fix until the known-query set improves.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I troubleshoot vector search with Oracle AI Database?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Use Oracle AI Database to inspect vectors, chunks, metadata, filters, source state, and retrieval routes together. Start with Oracle AI Vector Search for semantic retrieval, add hybrid search when exact terms matter, and use DBMS_VECTOR_CHAIN for repeatable chunking and text-processing workflows.&lt;/p&gt;

&lt;p&gt;Useful docs and runnable assets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/" rel="noopener noreferrer"&gt;Oracle AI Vector Search User's Guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/understand-hybrid-search.html" rel="noopener noreferrer"&gt;Understand Hybrid Search&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/arpls/dbms_vector_chain1.html" rel="noopener noreferrer"&gt;DBMS_VECTOR_CHAIN&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/oracle_rag_with_evals.ipynb" rel="noopener noreferrer"&gt;RAG evaluation notebook: oracle_rag_with_evals.ipynb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical Oracle troubleshooting workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm vectors exist and dimensions match.&lt;/li&gt;



&lt;li&gt;Inspect the exact chunk text returned by top-k.&lt;/li&gt;



&lt;li&gt;Run the query with and without metadata filters.&lt;/li&gt;



&lt;li&gt;Compare vector-only, keyword, and hybrid retrieval.&lt;/li&gt;



&lt;li&gt;Check source timestamps, delete flags, and embedding model versions.&lt;/li&gt;



&lt;li&gt;Run the known-query evaluation set and export results.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep retrieval artifacts and metadata queryable.&lt;/li&gt;



&lt;li&gt;Use hybrid search when lexical and semantic evidence both matter.&lt;/li&gt;



&lt;li&gt;Treat evaluation output as the proof that troubleshooting worked.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Vector search troubleshooting checklist&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; A healthy vector search path has consistent embeddings, valid dimensions, useful chunks, correct index configuration, appropriate filters, inspected top-k results, known-query tests, and retrieval logs that show what changed when quality improved or regressed.&lt;/p&gt;

&lt;p&gt;Use this checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same embedding model for indexing and querying.&lt;/li&gt;



&lt;li&gt;Matching vector dimensions.&lt;/li&gt;



&lt;li&gt;Correct vector column queried.&lt;/li&gt;



&lt;li&gt;Index exists and is usable.&lt;/li&gt;



&lt;li&gt;Chunk text is readable and context-rich.&lt;/li&gt;



&lt;li&gt;Metadata filters are visible and testable.&lt;/li&gt;



&lt;li&gt;Top-k results include expected evidence.&lt;/li&gt;



&lt;li&gt;Similarity thresholds do not remove good candidates.&lt;/li&gt;



&lt;li&gt;Hybrid search is tested for exact identifiers.&lt;/li&gt;



&lt;li&gt;Stale and duplicate embeddings are monitored.&lt;/li&gt;



&lt;li&gt;Retrieval metrics are logged over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Troubleshooting vector search is pipeline debugging.&lt;/li&gt;



&lt;li&gt;Most fixes should be measurable with retrieval metrics.&lt;/li&gt;



&lt;li&gt;Oracle AI Database gives the strongest story when vectors, SQL filters, source metadata, and evaluation evidence stay close together.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>oracle</category>
      <category>ai</category>
      <category>database</category>
      <category>vectorsearch</category>
    </item>
    <item>
      <title>How to Detect RAG Index Drift: Deleted Docs, Stale Chunks, and Duplicate Embeddings</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:14:54 +0000</pubDate>
      <link>https://dev.to/oracledevs/how-to-detect-rag-index-drift-deleted-docs-stale-chunks-and-duplicate-embeddings-1fec</link>
      <guid>https://dev.to/oracledevs/how-to-detect-rag-index-drift-deleted-docs-stale-chunks-and-duplicate-embeddings-1fec</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Detect RAG index drift by reconciling source records, chunk hashes, deletion markers, embedding versions, and retrieval results against the source of truth. A production RAG system should prove that deleted content no longer retrieves, updated content replaces stale chunks, duplicate embeddings are suppressed, and freshness filters are tested before users find stale citations.&lt;/p&gt;

&lt;p&gt;The boring production RAG failures are usually the expensive ones. A document is updated, but the old chunks still rank. A source row is deleted, but its embedding remains active. A re-ingestion job runs twice, creates near-duplicates, and retrieval starts returning conflicting evidence.&lt;/p&gt;

&lt;p&gt;That is RAG index drift. It is not a model problem first. It is a lifecycle problem across source data, chunks, embeddings, metadata, indexes, and evaluation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat the source of truth as authoritative, not the vector index.&lt;/li&gt;



&lt;li&gt;Track chunk lifecycle state explicitly: current, superseded, deleted, failed, or quarantined.&lt;/li&gt;



&lt;li&gt;Test deletion and duplicate handling as production behaviours, not cleanup chores.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What is RAG index drift?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; RAG index drift happens when the retrieval index no longer matches the authoritative source. The source changed, but chunks, embeddings, metadata, or retrieval filters did not change with it. The result is stale evidence, orphan embeddings, duplicate chunks, wrong citations, and answers grounded in content that should no longer be active.&lt;/p&gt;

&lt;p&gt;A RAG system has at least two views of the world:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it believes&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source system&lt;/td&gt;
&lt;td&gt;The current document, row, policy, ticket, or file state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval system&lt;/td&gt;
&lt;td&gt;The chunks, embeddings, metadata, and indexes available to search&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Drift appears when those views diverge. It can happen after a failed ingestion job, a partial batch update, a source-system delete, an embedding-model migration, a parser change, or a re-ingestion run that does not enforce idempotency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drift is a source-to-index consistency problem.&lt;/li&gt;



&lt;li&gt;Vector search can faithfully retrieve content that should no longer exist.&lt;/li&gt;



&lt;li&gt;Freshness metadata is only useful if retrieval filters enforce it.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Why do deleted documents still show up in retrieval?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Deleted documents still show up when deletion is handled in the source system but not propagated to chunk rows, embedding rows, metadata filters, and search indexes. A delete event must either remove derived retrieval records or mark them inactive before they can appear in top-k results.&lt;/p&gt;

&lt;p&gt;"We deleted it from the database" and "it no longer shows up in retrieval" are different claims. RAG creates derived artifacts: chunks, embeddings, summaries, cached answers, and sometimes reranker inputs. If only the source row is deleted, derived records can continue to rank.&lt;/p&gt;

&lt;p&gt;Use explicit deletion semantics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Delete pattern&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Safer production behaviour&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hard delete source only&lt;/td&gt;
&lt;td&gt;Orphan chunks remain searchable&lt;/td&gt;
&lt;td&gt;Cascade or reconcile derived records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft delete source&lt;/td&gt;
&lt;td&gt;Retriever ignores source state&lt;/td&gt;
&lt;td&gt;Add mandatory &lt;code&gt;is_current&lt;/code&gt; and &lt;code&gt;is_deleted&lt;/code&gt; filters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-ingest after delete&lt;/td&gt;
&lt;td&gt;Deleted content returns as a new chunk&lt;/td&gt;
&lt;td&gt;Use source IDs and tombstones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache survives delete&lt;/td&gt;
&lt;td&gt;Bot cites removed content&lt;/td&gt;
&lt;td&gt;Invalidate answer and retrieval caches&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deletion must propagate to every derived retrieval artifact.&lt;/li&gt;



&lt;li&gt;Tombstones help prevent deleted content from returning during re-ingestion.&lt;/li&gt;



&lt;li&gt;The retrieval query should exclude deleted and superseded content by default.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I detect stale chunks, orphan embeddings, and when to refresh RAG embeddings?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Compare the retrieval tables with the source of truth on a schedule. Check source IDs, source modified timestamps, chunk hashes, current-version flags, deletion markers, embedding model IDs, and index participation. Then run challenge queries that verify stale and deleted evidence cannot appear in top-k results.&lt;/p&gt;

&lt;p&gt;A reconciliation job should answer simple questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does every active chunk point to an active source?&lt;/li&gt;



&lt;li&gt;Does every active source have the expected current chunks?&lt;/li&gt;



&lt;li&gt;Did the source text change without the chunk hash changing?&lt;/li&gt;



&lt;li&gt;Did the chunk text change without a new embedding?&lt;/li&gt;



&lt;li&gt;Are deleted or superseded chunks still searchable?&lt;/li&gt;



&lt;li&gt;Are chunks embedded with an old model mixed into the current retrieval path?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The existing &lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/oracle_rag_with_evals.ipynb" rel="noopener noreferrer"&gt;RAG evaluation notebook&lt;/a&gt; pattern is useful here. Reuse the separation between retrieval methods, challenge questions, exported metrics, and a run manifest. Extend the challenge set with deletion, stale-version, duplicate, and orphan-vector cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reconciliation detects corpus integrity problems.&lt;/li&gt;



&lt;li&gt;Retrieval evaluation detects whether those problems affect user-visible answers.&lt;/li&gt;



&lt;li&gt;Keep a run manifest so drift checks are repeatable and debuggable.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How should I handle re-ingestion duplicates?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Make ingestion idempotent. Use stable source IDs, chunk ordinals, canonical text hashes, parser-version metadata, embedding-model metadata, and uniqueness rules. Near-duplicate chunks should be merged, superseded, or quarantined before they compete in retrieval.&lt;/p&gt;

&lt;p&gt;Duplicates are not harmless. Two near-identical chunks can both rank, crowd out better evidence, or disagree because one is stale. The user sees this as confused citations or answers that mix versions.&lt;/p&gt;

&lt;p&gt;Use duplicate controls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Duplicate type&lt;/th&gt;
&lt;th&gt;Detection signal&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same source, same chunk hash&lt;/td&gt;
&lt;td&gt;Exact duplicate&lt;/td&gt;
&lt;td&gt;Skip insert or update existing row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same source, new chunk hash&lt;/td&gt;
&lt;td&gt;Source changed&lt;/td&gt;
&lt;td&gt;Supersede old chunk and embed new chunk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different source, near-identical text&lt;/td&gt;
&lt;td&gt;Possible copied content&lt;/td&gt;
&lt;td&gt;Keep both only if provenance differs meaningfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same text, different embedding model&lt;/td&gt;
&lt;td&gt;Model migration artifact&lt;/td&gt;
&lt;td&gt;Route by active embedding model version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same source, different parser version&lt;/td&gt;
&lt;td&gt;Parser migration artifact&lt;/td&gt;
&lt;td&gt;compare retrieval quality before promoting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Idempotent ingestion is a production requirement.&lt;/li&gt;



&lt;li&gt;Chunk hashes prevent unnecessary re-embedding.&lt;/li&gt;



&lt;li&gt;Near-duplicates need policy because they can degrade retrieval quality.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What should a RAG reconciliation job check?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; A RAG reconciliation job should compare source state, chunk state, embedding state, and retrieval behaviour. It should produce counts, examples, and failed challenge queries rather than only saying the job succeeded.&lt;/p&gt;

&lt;p&gt;Use a reconciliation report like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;th&gt;Example failure&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Active chunk has missing source&lt;/td&gt;
&lt;td&gt;Orphan embedding&lt;/td&gt;
&lt;td&gt;Source document deleted but chunk still active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source updated after chunk hash&lt;/td&gt;
&lt;td&gt;Stale chunk&lt;/td&gt;
&lt;td&gt;Policy changed but old text remains searchable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deleted source returned in top-k&lt;/td&gt;
&lt;td&gt;Delete propagation failure&lt;/td&gt;
&lt;td&gt;Bot can cite removed document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate active chunks by source and hash&lt;/td&gt;
&lt;td&gt;Non-idempotent ingestion&lt;/td&gt;
&lt;td&gt;Batch job inserted the same chunk twice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Near-duplicate active chunks&lt;/td&gt;
&lt;td&gt;Re-ingestion or parser drift&lt;/td&gt;
&lt;td&gt;Old and new versions compete in retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding model mismatch&lt;/td&gt;
&lt;td&gt;Partial migration&lt;/td&gt;
&lt;td&gt;Old vectors mixed with current vectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current-version flag mismatch&lt;/td&gt;
&lt;td&gt;Bad lifecycle state&lt;/td&gt;
&lt;td&gt;Superseded chunk marked current&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The report should be actionable. Include source IDs, chunk IDs, timestamps, hashes, model versions, retrieval route, and sample queries that expose the issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reconciliation needs both data checks and retrieval checks.&lt;/li&gt;



&lt;li&gt;Counts are not enough; include examples engineers can inspect.&lt;/li&gt;



&lt;li&gt;Failed reconciliation should block promotion of a new ingestion run.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I measure whether drift is affecting answers?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Add drift cases to the same evaluation harness used for retrieval quality. Test whether deleted documents are absent, current versions beat stale versions, duplicate chunks do not crowd out better evidence, and generated answers cite only active evidence. Track retrieval metrics and answer-quality checks separately.&lt;/p&gt;

&lt;p&gt;Start with a small drift challenge set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask for a document that was deleted and should not be cited.&lt;/li&gt;



&lt;li&gt;Ask a question where old and new versions both exist; only the current one should rank.&lt;/li&gt;



&lt;li&gt;Ask an exact-ID query after source deletion; retrieval should return no active evidence.&lt;/li&gt;



&lt;li&gt;Ask a question affected by duplicate chunks; top-k should not be crowded by copies.&lt;/li&gt;



&lt;li&gt;Ask after an embedding-model migration; results should come from the active model path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For retrieval, measure whether forbidden evidence appears in top-k. For answers, measure groundedness, citation validity, and abstention quality. If retrieval returns deleted content, the answer generator is already in a bad position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drift tests belong in the production evaluation set.&lt;/li&gt;



&lt;li&gt;Forbidden evidence is as important as required evidence.&lt;/li&gt;



&lt;li&gt;Do not publish freshness claims without traceable evaluation results.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I keep generated code from using stale database patterns?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Treat AI-generated implementation code as another drift surface. Coding assistants can produce outdated connection-pooling or driver patterns when they are not grounded in current documentation. For database-backed RAG, verify generated code against the current driver docs, pin package versions, and add tests for pool creation, acquisition, release, health, and shutdown.&lt;/p&gt;

&lt;p&gt;This matters because production RAG is not only retrieval logic. It is also connection handling, pooling, timeouts, retries, lifecycle management, and observability. An assistant can generate code that looks plausible but reflects an older driver style or misses the current recommended pooling API.&lt;/p&gt;

&lt;p&gt;Use a documentation-grounded review loop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Code area&lt;/th&gt;
&lt;th&gt;Drift risk&lt;/th&gt;
&lt;th&gt;Review check&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Connection pool creation&lt;/td&gt;
&lt;td&gt;Deprecated or outdated API shape&lt;/td&gt;
&lt;td&gt;Match current driver docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pool sizing&lt;/td&gt;
&lt;td&gt;Too many sessions or no backpressure&lt;/td&gt;
&lt;td&gt;Set min, max, increment, and queue behaviour deliberately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection acquisition&lt;/td&gt;
&lt;td&gt;Leaks under errors&lt;/td&gt;
&lt;td&gt;Use context managers or explicit release paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health checks&lt;/td&gt;
&lt;td&gt;Dead connections reused&lt;/td&gt;
&lt;td&gt;Test pool health and recovery behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async code&lt;/td&gt;
&lt;td&gt;Sync pool used in async path&lt;/td&gt;
&lt;td&gt;Verify async pool APIs separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutdown&lt;/td&gt;
&lt;td&gt;Pool left open&lt;/td&gt;
&lt;td&gt;Close the pool during app teardown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The practical rule is simple: do not accept generated infrastructure code just because it runs once. Point the assistant at the current docs, then test the behaviour you expect in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI-generated code can drift from current database-driver practices.&lt;/li&gt;



&lt;li&gt;Connection pooling should be reviewed against current python-oracledb documentation.&lt;/li&gt;



&lt;li&gt;Add runtime tests for connection acquisition, release, health, and shutdown.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How to implement this with Oracle AI Database&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Use Oracle AI Database to keep source metadata, chunks, embeddings, SQL filters, provenance, and deletion state close together. Store lifecycle fields next to retrieval fields, enforce freshness and delete filters in SQL, and run keyword, vector, and hybrid retrieval against the same governed metadata.&lt;/p&gt;

&lt;p&gt;A practical Oracle AI Database design should store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source ID and source system&lt;/li&gt;



&lt;li&gt;source modified timestamp and ingestion timestamp&lt;/li&gt;



&lt;li&gt;chunk ID, chunk ordinal, and chunk hash&lt;/li&gt;



&lt;li&gt;parser version and embedding model version&lt;/li&gt;



&lt;li&gt;current, superseded, deleted, and quarantined flags&lt;/li&gt;



&lt;li&gt;tenant, ACL, classification, and provenance fields&lt;/li&gt;



&lt;li&gt;retrieval-route metrics and challenge-set results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful docs and runnable assets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/" rel="noopener noreferrer"&gt;Oracle AI Vector Search User's Guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/understand-hybrid-search.html" rel="noopener noreferrer"&gt;Understand Hybrid Search&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/arpls/dbms_vector_chain1.html" rel="noopener noreferrer"&gt;DBMS_VECTOR_CHAIN&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/dbseg/" rel="noopener noreferrer"&gt;Oracle AI Database Security Guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#connection-pooling" rel="noopener noreferrer"&gt;python-oracledb connection pooling&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/oracle_rag_with_evals.ipynb" rel="noopener noreferrer"&gt;RAG evaluation notebook: oracle_rag_with_evals.ipynb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important architecture point is proximity. If chunks, vectors, metadata, permissions, and deletion state live together, reconciliation can be expressed as database checks plus retrieval tests. If every layer lives in a different system, deletion and freshness become distributed-systems problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep lifecycle metadata in the retrieval path, not in a separate spreadsheet or job log.&lt;/li&gt;



&lt;li&gt;Apply &lt;code&gt;is_current&lt;/code&gt;, &lt;code&gt;is_deleted&lt;/code&gt;, tenant, and permission filters before generation.&lt;/li&gt;



&lt;li&gt;Use evaluation artifacts and manifests when promoting parser, chunking, or embedding changes.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Production checklist for RAG freshness&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; A production RAG system is fresh only if source updates, deletes, parser changes, embedding changes, cache invalidation, and retrieval filters are observable and tested. If the first signal of drift is a user complaint, the system is missing reconciliation.&lt;/p&gt;

&lt;p&gt;Use this checklist before calling a RAG deployment production-ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every active chunk has an active source.&lt;/li&gt;



&lt;li&gt;Every active source has expected current chunks.&lt;/li&gt;



&lt;li&gt;Deleted sources cannot appear in retrieval.&lt;/li&gt;



&lt;li&gt;Superseded chunks are excluded by default.&lt;/li&gt;



&lt;li&gt;Chunk hashes prevent duplicate inserts.&lt;/li&gt;



&lt;li&gt;Embedding model versions are recorded and filterable.&lt;/li&gt;



&lt;li&gt;Parser and chunking versions are recorded.&lt;/li&gt;



&lt;li&gt;Retrieval challenge sets include stale, deleted, and duplicate cases.&lt;/li&gt;



&lt;li&gt;Caches invalidate on source update and delete.&lt;/li&gt;



&lt;li&gt;Reconciliation failures create tickets or block promotion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Freshness is not an ingestion schedule; it is a tested invariant.&lt;/li&gt;



&lt;li&gt;Reconciliation should run before users notice drift.&lt;/li&gt;



&lt;li&gt;The vector index is not the source of truth. The source system is.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rag</category>
      <category>oracle</category>
      <category>ai</category>
      <category>database</category>
    </item>
    <item>
      <title>Agent Memory Is Not RAG: Conversation IDs, Durable State, and Scoped Recall</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:13:14 +0000</pubDate>
      <link>https://dev.to/oracledevs/agent-memory-is-not-rag-conversation-ids-durable-state-and-scoped-recall-19j4</link>
      <guid>https://dev.to/oracledevs/agent-memory-is-not-rag-conversation-ids-durable-state-and-scoped-recall-19j4</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; RAG retrieves external evidence. Agent memory stores durable context about users, agents, threads, preferences, decisions, and prior work. Production agents need explicit conversation IDs, tenant scopes, provenance, deletion rules, and permission-aware recall. Without those boundaries, memory becomes prompt stuffing and state leaks across sessions.&lt;/p&gt;

&lt;p&gt;Developers often ask memory questions while talking about RAG. That makes sense: both involve retrieval. But they solve different problems.&lt;/p&gt;

&lt;p&gt;RAG answers, "What source evidence should the model use right now?" Memory answers, "What should this agent remember across turns, sessions, and workflows?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RAG is not a memory system by itself.&lt;/li&gt;



&lt;li&gt;A bigger context window is not durable memory.&lt;/li&gt;



&lt;li&gt;Production memory needs scope, lifecycle, provenance, deletion, and governance.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;RAG vs agent memory: what is the difference?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; RAG retrieves documents or data to ground an answer. Agent memory persists useful state across interactions. RAG should cite source evidence. Memory should recall scoped facts, preferences, decisions, summaries, tool results, and workflow state when they remain valid and allowed.&lt;/p&gt;

&lt;p&gt;Use this distinction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;RAG&lt;/th&gt;
&lt;th&gt;Agent memory&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary job&lt;/td&gt;
&lt;td&gt;Retrieve external evidence&lt;/td&gt;
&lt;td&gt;Persist and recall useful state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical unit&lt;/td&gt;
&lt;td&gt;Document chunk, row, source result&lt;/td&gt;
&lt;td&gt;User preference, thread summary, decision, durable fact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main risk&lt;/td&gt;
&lt;td&gt;Wrong or inaccessible evidence&lt;/td&gt;
&lt;td&gt;Stale, overbroad, or cross-session memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required scope&lt;/td&gt;
&lt;td&gt;Source, tenant, ACL, version&lt;/td&gt;
&lt;td&gt;User, agent, tenant, thread, conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RAG can be stateless.&lt;/li&gt;



&lt;li&gt;Memory is stateful by definition.&lt;/li&gt;



&lt;li&gt;Memory must be correctable and deletable.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Why does an MCP tool need a conversation ID?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; A user ID identifies the person, not the conversation. A conversation ID or correlation ID lets tools isolate state, connect logs, avoid cross-tab leakage, and prove which retrieved data belonged to which interaction.&lt;/p&gt;

&lt;p&gt;If an LLM application calls your MCP tool without a conversation ID, your tool cannot tell whether two requests belong to the same thread or two parallel sessions. In regulated systems, that is not just inconvenient. It is a traceability problem.&lt;/p&gt;

&lt;p&gt;Ask the calling application to pass:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user ID&lt;/li&gt;



&lt;li&gt;tenant ID&lt;/li&gt;



&lt;li&gt;agent ID&lt;/li&gt;



&lt;li&gt;conversation or thread ID&lt;/li&gt;



&lt;li&gt;request ID&lt;/li&gt;



&lt;li&gt;permission context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conversation IDs are state isolation, not decoration.&lt;/li&gt;



&lt;li&gt;Correlation IDs make debugging and audit possible.&lt;/li&gt;



&lt;li&gt;Memory scope should come from explicit user, tenant, agent, and conversation identifiers rather than user ID alone.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What should an agent remember?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; An agent should remember durable, useful, scoped information: user preferences, task state, decisions, summaries, validated facts, and tool results that remain relevant. It should not remember secrets, transient noise, unsupported claims, or data the user is not allowed to retain.&lt;/p&gt;

&lt;p&gt;Memory needs policy. Without policy, every interaction becomes a candidate memory, and the system becomes noisy.&lt;/p&gt;

&lt;p&gt;Use memory categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Memory type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Preference&lt;/td&gt;
&lt;td&gt;User prefers Python examples&lt;/td&gt;
&lt;td&gt;Keep if useful and non-sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task state&lt;/td&gt;
&lt;td&gt;Draft article awaiting benchmark data&lt;/td&gt;
&lt;td&gt;Keep with thread scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable fact&lt;/td&gt;
&lt;td&gt;Project uses Oracle AI Database&lt;/td&gt;
&lt;td&gt;Keep with provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool result&lt;/td&gt;
&lt;td&gt;Retrieval run completed&lt;/td&gt;
&lt;td&gt;Keep with timestamp and source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensitive value&lt;/td&gt;
&lt;td&gt;Password, private key, token&lt;/td&gt;
&lt;td&gt;Do not store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory promotion should be deliberate.&lt;/li&gt;



&lt;li&gt;Memories need provenance and timestamps.&lt;/li&gt;



&lt;li&gt;Deletion and correction are product requirements.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I implement this with Oracle AI Database?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Use Oracle AI Agent Memory for persistent, scoped memory, and keep RAG, SQL, and tool outputs governed by the same access rules. Store user, agent, tenant, thread, memory, provenance, and retrieval metadata in a durable database-backed path.&lt;/p&gt;

&lt;p&gt;Useful docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/agent-memory/26.6/guide/get-started.html" rel="noopener noreferrer"&gt;Oracle AI Agent Memory getting-started guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/dbseg/" rel="noopener noreferrer"&gt;Oracle AI Database Security Guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/selai/select-ai-about.html" rel="noopener noreferrer"&gt;About Select AI&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/" rel="noopener noreferrer"&gt;Oracle AI Vector Search User's Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Oracle positioning, the important message is that memory is a data problem. Once memory needs persistence, scoping, retrieval, lifecycle rules, audit, and deletion, it belongs in a governed data layer rather than an ad hoc prompt or local cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use Oracle AI Agent Memory as the canonical product path for scoped persistent memory.&lt;/li&gt;



&lt;li&gt;Keep live facts in SQL and source-grounded evidence in RAG.&lt;/li&gt;



&lt;li&gt;Apply the same security rules to memory retrieval that apply to document retrieval.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How does memory affect latency?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Memory can reduce repeated work when it recalls the right scoped context, but it can add latency if every turn performs broad retrieval. Keep memory retrieval narrow, scoped, and measurable. Use summaries for thread continuity and durable memories for facts that matter beyond the current conversation.&lt;/p&gt;

&lt;p&gt;A practical memory stack separates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;current prompt context&lt;/li&gt;



&lt;li&gt;thread summary&lt;/li&gt;



&lt;li&gt;durable user or task memories&lt;/li&gt;



&lt;li&gt;retrieved source evidence&lt;/li&gt;



&lt;li&gt;live tool or SQL results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory retrieval should be selective.&lt;/li&gt;



&lt;li&gt;Thread summaries and long-term memories serve different purposes.&lt;/li&gt;



&lt;li&gt;Measure whether memory improves answer quality enough to justify the cost.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What should I do next?&lt;/h2&gt;

&lt;p&gt;Start by defining memory scope. Decide what is stored by user, tenant, agent, thread, and conversation. Then define promotion, update, delete, and retrieval rules. Only after that should you wire memory into an agent loop.&lt;/p&gt;

</description>
      <category>agentmemory</category>
      <category>rag</category>
      <category>oracle</category>
      <category>ai</category>
    </item>
    <item>
      <title>RAG Chunking and Parsing for Tables, PDFs, Transcripts, and Media</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:12:21 +0000</pubDate>
      <link>https://dev.to/oracledevs/rag-chunking-and-parsing-for-tables-pdfs-transcripts-and-media-3e1p</link>
      <guid>https://dev.to/oracledevs/rag-chunking-and-parsing-for-tables-pdfs-transcripts-and-media-3e1p</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; RAG chunking fails when it cuts content away from the context that makes it meaningful. A chunk should preserve headings, table headers, source IDs, timestamps, speaker labels, media traits, and security metadata. Good parsing and chunking make retrieved evidence understandable before the model ever sees it.&lt;/p&gt;

&lt;p&gt;Most RAG tutorials spend a few minutes on chunking and then move on to embeddings. In production, chunking is often where retrieval quality is won or lost.&lt;/p&gt;

&lt;p&gt;The model cannot use evidence that was damaged before retrieval. If a table row loses its headers, a transcript loses the speaker, or a bullet loses its parent heading, the retriever may find text that looks related but cannot support an answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chunking is retrieval design, not housekeeping.&lt;/li&gt;



&lt;li&gt;The goal is not equal-sized text. The goal is evidence that can stand alone.&lt;/li&gt;



&lt;li&gt;Parsing quality matters as much as embedding quality.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Why does chunking break a RAG application with vector search?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Chunking breaks RAG when boundaries ignore meaning. Fixed-size chunks can split sentences, detach bullets from headings, cut tables mid-row, and separate numbers from labels. The retriever then returns fragments that are semantically close but incomplete.&lt;/p&gt;

&lt;p&gt;The common failure is a chunk that is technically relevant but useless. For example, a chunk that says "up to 30 days" is not answerable unless it also carries what the 30 days refers to.&lt;/p&gt;

&lt;p&gt;Structure-aware chunking should preserve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;document title&lt;/li&gt;



&lt;li&gt;section and parent headings&lt;/li&gt;



&lt;li&gt;list context&lt;/li&gt;



&lt;li&gt;table headers&lt;/li&gt;



&lt;li&gt;page or slide number&lt;/li&gt;



&lt;li&gt;source path&lt;/li&gt;



&lt;li&gt;version and timestamp&lt;/li&gt;



&lt;li&gt;tenant and access metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inspect retrieved chunks from failed queries before changing models.&lt;/li&gt;



&lt;li&gt;Attach parent context to small chunks.&lt;/li&gt;



&lt;li&gt;Keep metadata with the chunk, not in a separate system that retrieval cannot filter.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How should I chunk tables and spreadsheets?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Keep table headers, row labels, sheet names, and units with every retrieved table fragment. Do not let a row become a list of disconnected numbers. If a table is central to the answer, store a text representation and structured fields that can be queried directly.&lt;/p&gt;

&lt;p&gt;Tables are hard because their meaning is relational. The value &lt;code&gt;12&lt;/code&gt; means nothing without the column name, row label, unit, and sometimes the preceding section.&lt;/p&gt;

&lt;p&gt;Use a table strategy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Table problem&lt;/th&gt;
&lt;th&gt;Safer approach&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Row loses column headers&lt;/td&gt;
&lt;td&gt;Repeat headers in each table chunk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sheet context disappears&lt;/td&gt;
&lt;td&gt;Include workbook, sheet, and section names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numeric values need filtering&lt;/td&gt;
&lt;td&gt;Store structured fields for SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table spans pages&lt;/td&gt;
&lt;td&gt;preserve continuation markers and page references&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table RAG often needs both text retrieval and structured querying.&lt;/li&gt;



&lt;li&gt;Repeat headers deliberately; do not rely on proximity.&lt;/li&gt;



&lt;li&gt;Preserve provenance so citations can point to the real source.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How should I parse PDFs?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Treat PDFs as layout artifacts, not clean documents. Parse text, headings, reading order, tables, page numbers, and source coordinates where possible. Test parsing output before embedding it, because a bad PDF extraction can make every downstream retrieval method look worse than it is.&lt;/p&gt;

&lt;p&gt;PDFs can have multi-column layouts, footnotes, tables, captions, scanned pages, and broken reading order. If parsing turns a policy into scrambled text, embeddings will faithfully index the mess.&lt;/p&gt;

&lt;p&gt;A production parser should produce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clean text&lt;/li&gt;



&lt;li&gt;reading order&lt;/li&gt;



&lt;li&gt;table boundaries&lt;/li&gt;



&lt;li&gt;headings and hierarchy&lt;/li&gt;



&lt;li&gt;page references&lt;/li&gt;



&lt;li&gt;image captions or extracted descriptions when needed&lt;/li&gt;



&lt;li&gt;source IDs for citation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not benchmark retrieval until extraction quality is visible.&lt;/li&gt;



&lt;li&gt;Keep page and source references for citations.&lt;/li&gt;



&lt;li&gt;Use a challenge set with tables, scanned pages, and multi-column layouts.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How should I chunk transcripts and media summaries?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Keep timestamps, speaker labels, cleaned text, summary fields, and raw references together. For video or creator search, evaluate extracted traits as structured metadata before using vectors to rank results.&lt;/p&gt;

&lt;p&gt;Raw transcripts are long, repetitive, and full of filler. Summary-only storage loses exact quotes. The practical path is both: cleaned chunks for retrieval, source timestamps for audit, and summaries or topics for navigation.&lt;/p&gt;

&lt;p&gt;For media RAG, extracted traits should be tested. If the app needs to find "curly haired creator" or "risk discussion at minute 42," those labels need an evaluation set. Vector similarity is not a yes/no trait detector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Speaker and timestamp metadata are part of the evidence.&lt;/li&gt;



&lt;li&gt;Store raw references even when retrieval uses cleaned text.&lt;/li&gt;



&lt;li&gt;Evaluate extracted media traits before ranking on them.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I implement this with Oracle AI Database?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Use Oracle AI Database to keep chunks, embeddings, metadata, and retrieval filters close together. Use DBMS_VECTOR_CHAIN for text processing and chunking workflows, Oracle AI Vector Search for embeddings and similarity search, and hybrid search when lexical and semantic retrieval both matter.&lt;/p&gt;

&lt;p&gt;Useful docs and runnable assets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/arpls/dbms_vector_chain1.html" rel="noopener noreferrer"&gt;DBMS_VECTOR_CHAIN&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/" rel="noopener noreferrer"&gt;Oracle AI Vector Search User's Guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/understand-hybrid-search.html" rel="noopener noreferrer"&gt;Understand Hybrid Search&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/oracle_rag_with_evals.ipynb" rel="noopener noreferrer"&gt;RAG evaluation notebook: oracle_rag_with_evals.ipynb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep chunk text and metadata in the same governed retrieval path.&lt;/li&gt;



&lt;li&gt;Version chunking configuration so retrieval changes can be compared.&lt;/li&gt;



&lt;li&gt;Test tables, PDFs, transcripts, and media separately because each fails differently.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What should I do next?&lt;/h2&gt;

&lt;p&gt;Build a chunking evaluation set before tuning chunk size. Take failed user questions, inspect the retrieved chunks, and identify whether the missing evidence was a parsing problem, a boundary problem, a metadata problem, or a retrieval-ranking problem. That diagnosis is faster than changing the model and hoping the corpus improves.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>database</category>
      <category>oracle</category>
    </item>
    <item>
      <title>Real-Time RAG: Live SQL, Incremental Indexing, and Freshness Tests</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:10:59 +0000</pubDate>
      <link>https://dev.to/oracledevs/real-time-rag-live-sql-incremental-indexing-and-freshness-tests-4hlp</link>
      <guid>https://dev.to/oracledevs/real-time-rag-live-sql-incremental-indexing-and-freshness-tests-4hlp</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Real-time RAG should not re-embed every changing record. Query current structured data live with SQL or NL2SQL, and use retrieval for documents that benefit from semantic search. For changing documents, update affected chunks incrementally, track versions and timestamps, and test whether answers prefer current evidence over stale evidence.&lt;/p&gt;

&lt;p&gt;The common real-time RAG question is simple: "My data changes every few minutes. Should I embed it, cache it, query it, or build an agent?"&lt;/p&gt;

&lt;p&gt;The answer depends on the data shape. Current rows are not documents. Operational data should usually stay operational. Documents, transcripts, support notes, and policies can be indexed, but they need freshness metadata and incremental updates.&lt;/p&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query live structured data instead of re-embedding it on every update.&lt;/li&gt;
&lt;li&gt;Use incremental indexing for changing documents.&lt;/li&gt;
&lt;li&gt;Freshness must be evaluated, not assumed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;When you move RAG to production, when should live SQL beat retrieval?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Use live SQL when the answer depends on current rows, counts, filters, dates, statuses, permissions, or joins. RAG is the wrong first route for questions that a database can answer directly and deterministically.&lt;/p&gt;

&lt;p&gt;If a risk register changes every five minutes, embedding every row creates a stale copy. If the user asks for open risks, overdue mitigations, current owners, or counts, the system should query live tables.&lt;/p&gt;

&lt;p&gt;Use this routing table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question type&lt;/th&gt;
&lt;th&gt;Best route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Current count, status, owner, balance, risk, ticket&lt;/td&gt;
&lt;td&gt;SQL or NL2SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy explanation, support note, manual text&lt;/td&gt;
&lt;td&gt;RAG over indexed documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current fact plus explanation&lt;/td&gt;
&lt;td&gt;SQL for fact, RAG for explanation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact ID plus description&lt;/td&gt;
&lt;td&gt;SQL or keyword first, vector second&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not force structured facts into a vector-only path.&lt;/li&gt;
&lt;li&gt;SQL has built-in advantages for current state, permissions, and joins.&lt;/li&gt;
&lt;li&gt;NL2SQL still needs guardrails, result limits, and inspection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;How should documents stay fresh?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Track source timestamps, chunk hashes, embedding model versions, current-version flags, and delete state. Re-embed only changed chunks. Test stale and current versions in the same evaluation set so freshness regressions are visible.&lt;/p&gt;

&lt;p&gt;Incremental indexing is not just a performance optimization. It is a correctness requirement. A stale chunk can produce a confident wrong answer.&lt;/p&gt;

&lt;p&gt;A production document pipeline should record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source ID and source system&lt;/li&gt;
&lt;li&gt;source modified timestamp&lt;/li&gt;
&lt;li&gt;ingestion timestamp&lt;/li&gt;
&lt;li&gt;chunk hash&lt;/li&gt;
&lt;li&gt;embedding model&lt;/li&gt;
&lt;li&gt;current version flag&lt;/li&gt;
&lt;li&gt;deletion or superseded state&lt;/li&gt;
&lt;li&gt;tenant and ACL metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Freshness metadata belongs in retrieval filters.&lt;/li&gt;
&lt;li&gt;Re-embedding unchanged chunks wastes compute and can create noise.&lt;/li&gt;
&lt;li&gt;Deletion and supersession must propagate to retrieval.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;What role do caching, MCP, and tool calling play?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Caching reduces repeated work, MCP exposes tools, and tool calling chooses actions. None of them makes stale data fresh. Each still needs permissions, timeouts, result limits, audit, and a freshness strategy.&lt;/p&gt;

&lt;p&gt;Use caching for stable, permission-safe results. Invalidate it when source data changes. Use MCP or tool calling when the assistant needs to query a database, call an API, or run a retrieval tool. Keep each tool narrow and auditable.&lt;/p&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool calling is routing, not governance.&lt;/li&gt;
&lt;li&gt;MCP needs scoped tools and traceable outputs.&lt;/li&gt;
&lt;li&gt;Cache invalidation should be tied to source change events.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;How do I implement this with Oracle AI Database?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Use Select AI or application-controlled SQL for live structured data, Oracle AI Vector Search for document retrieval, and hybrid search where keyword and semantic search both matter. Keep source timestamps, version flags, tenant filters, and ACLs in the database path.&lt;/p&gt;

&lt;p&gt;Useful docs and runnable assets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/selai/select-ai-about.html" rel="noopener noreferrer"&gt;About Select AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/" rel="noopener noreferrer"&gt;Oracle AI Vector Search User's Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/understand-hybrid-search.html" rel="noopener noreferrer"&gt;Understand Hybrid Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/arpls/dbms_vector_chain1.html" rel="noopener noreferrer"&gt;DBMS_VECTOR_CHAIN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/oracle_rag_with_evals.ipynb" rel="noopener noreferrer"&gt;RAG evaluation notebook: oracle_rag_with_evals.ipynb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use live SQL for current structured facts.&lt;/li&gt;
&lt;li&gt;Use vector and hybrid retrieval for document evidence.&lt;/li&gt;
&lt;li&gt;Keep freshness filters near the data, not in the prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;How should I test freshness?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Create challenge questions where old and new evidence both exist. The system should choose the current record or current document version, cite it, and ignore stale evidence unless the user explicitly asks for history.&lt;/p&gt;

&lt;p&gt;Freshness tests should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recently updated row&lt;/li&gt;
&lt;li&gt;stale document version&lt;/li&gt;
&lt;li&gt;deleted document&lt;/li&gt;
&lt;li&gt;superseded policy&lt;/li&gt;
&lt;li&gt;cached answer invalidation&lt;/li&gt;
&lt;li&gt;user asking for current state&lt;/li&gt;
&lt;li&gt;user asking for historical state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Freshness is a user-visible quality metric.&lt;/li&gt;
&lt;li&gt;The evaluation set should include stale evidence on purpose.&lt;/li&gt;
&lt;li&gt;A system that cannot distinguish current from old evidence is not real-time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;What should I do next?&lt;/h2&gt;

&lt;p&gt;Map each question type to live SQL, document retrieval, hybrid retrieval, or agent tool use. Then build a freshness challenge set before scaling ingestion. If the system cannot pass stale-versus-current tests, do not publish it as real-time RAG.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>sql</category>
      <category>oracle</category>
      <category>ai</category>
    </item>
    <item>
      <title>Secure Enterprise RAG: ACLs, Tenant Filters, Provenance, and Oracle Deep Data Security</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:09:37 +0000</pubDate>
      <link>https://dev.to/oracledevs/secure-enterprise-rag-acls-tenant-filters-provenance-and-oracle-deep-data-security-3nc9</link>
      <guid>https://dev.to/oracledevs/secure-enterprise-rag-acls-tenant-filters-provenance-and-oracle-deep-data-security-3nc9</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Secure enterprise RAG means access policy travels with the evidence. Source ACLs, tenant IDs, labels, provenance, masking, audit, and deletion state must be enforced before retrieved chunks reach the model. "Private" or "self-hosted" is not enough. The system is secure only when retrieval follows the same rules as the source data.&lt;/p&gt;

&lt;p&gt;Enterprise RAG usually starts with a reasonable goal: let employees ask questions over documents, tickets, policies, emails, and operational data without sending sensitive information to the wrong place.&lt;/p&gt;

&lt;p&gt;The risk is that teams build retrieval first and security later. That is backwards. Once chunks, embeddings, summaries, and generated answers exist, the data has already moved through several surfaces.&lt;/p&gt;

&lt;p&gt;Oracle Deep Data Security is the right frame for this article: enforce security close to governed data, retrieval, SQL, metadata, audit, masking, labels, roles, and access policy before sensitive evidence reaches the model. Treat it as a security architecture message, not as a standalone product claim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secure RAG starts at ingestion, not at prompt time.&lt;/li&gt;



&lt;li&gt;ACLs and tenant filters must be retrieval controls.&lt;/li&gt;



&lt;li&gt;Self-hosted RAG can still leak if permissions, deletion, and audit are weak.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What does production RAG governance have to secure?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Secure the source data, chunks, embeddings, metadata, retrieval filters, prompts, tool calls, generated answers, citations, logs, and memory. If any layer can bypass source permissions, the system can expose data that the user should not see.&lt;/p&gt;

&lt;p&gt;A secure RAG system needs controls across the path:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Security requirement&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;Capture owner, tenant, role, classification, and delete state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chunk&lt;/td&gt;
&lt;td&gt;Preserve source ACLs and provenance metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding&lt;/td&gt;
&lt;td&gt;Treat vectors as derived sensitive data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;Filter by permission before generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generation&lt;/td&gt;
&lt;td&gt;Cite only accessible evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;Record evidence and tool use without leaking secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Scope by user, tenant, agent, and conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embeddings and summaries can still reveal sensitive information.&lt;/li&gt;



&lt;li&gt;Permission filters must run before the model receives evidence.&lt;/li&gt;



&lt;li&gt;Logs need enough detail for audit without becoming a second data leak.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How should ACLs and tenant filters work?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; ACLs should be stamped onto chunks during ingestion and enforced during retrieval. Tenant filters should be mandatory query predicates, not optional prompt instructions. A model cannot be trusted to ignore evidence that retrieval already exposed.&lt;/p&gt;

&lt;p&gt;The retrieval query should only consider evidence the user can access. That means every chunk needs enough metadata to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which tenant owns this evidence?&lt;/li&gt;



&lt;li&gt;Which user, group, role, or department can see it?&lt;/li&gt;



&lt;li&gt;What source system did it come from?&lt;/li&gt;



&lt;li&gt;Is it current, deleted, superseded, or embargoed?&lt;/li&gt;



&lt;li&gt;What classification or label applies?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ACL propagation is part of ingestion.&lt;/li&gt;



&lt;li&gt;Tenant filters are non-negotiable retrieval predicates.&lt;/li&gt;



&lt;li&gt;Prompt instructions are not a permission boundary.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How does provenance reduce risk?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Provenance tells you which source, version, row, chunk, document, or tool result contributed to an answer. Without provenance, teams cannot audit a response, fix a bad retrieval path, or prove that the model used accessible evidence.&lt;/p&gt;

&lt;p&gt;Every generated answer should be traceable back to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;document or table source&lt;/li&gt;



&lt;li&gt;source version&lt;/li&gt;



&lt;li&gt;chunk or row ID&lt;/li&gt;



&lt;li&gt;retrieval route&lt;/li&gt;



&lt;li&gt;user and tenant scope&lt;/li&gt;



&lt;li&gt;tool call or SQL query&lt;/li&gt;



&lt;li&gt;timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not just compliance paperwork. It is how engineers debug production RAG. When users report a bad answer, provenance tells you whether the source was stale, the chunk was damaged, the filter failed, or the generation step overreached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provenance is both a security control and a debugging tool.&lt;/li&gt;



&lt;li&gt;Citations should point to accessible evidence.&lt;/li&gt;



&lt;li&gt;Store enough retrieval metadata to reconstruct what happened.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I implement this with Oracle AI Database?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Keep sensitive data, metadata, vectors, SQL, retrieval filters, audit, and memory close to the governed database layer. Use Oracle AI Database security capabilities for authentication, roles, application context, network encryption, auditing, sensitive data protection, and security products where required.&lt;/p&gt;

&lt;p&gt;Useful docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/dbseg/" rel="noopener noreferrer"&gt;Oracle AI Database Security Guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/dbseg/introduction-to-oracle-database-security.html" rel="noopener noreferrer"&gt;Introduction to Oracle AI Database Security&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/" rel="noopener noreferrer"&gt;Oracle AI Vector Search User's Guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/selai/select-ai-about.html" rel="noopener noreferrer"&gt;About Select AI&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/agent-memory/26.6/guide/get-started.html" rel="noopener noreferrer"&gt;Oracle AI Agent Memory getting-started guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The security guide describes Oracle AI Database security areas including users, authentication, privileges, application security, application contexts, sensitive data protection, network encryption, auditing, and additional security products such as Oracle Advanced Security, Oracle Label Security, Oracle Database Vault, Oracle Data Safe, Audit Vault and Database Firewall, and Oracle Key Vault.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use database security controls where the data and retrieval metadata live.&lt;/li&gt;



&lt;li&gt;Do not copy enterprise data into a separate AI layer and then rebuild governance from scratch.&lt;/li&gt;



&lt;li&gt;Treat Oracle Deep Data Security as the data-layer story for governed AI applications.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What should I do next?&lt;/h2&gt;

&lt;p&gt;Build a security checklist before building the chatbot UI. For every source, define the ACL metadata, tenant field, provenance fields, deletion behaviour, masking rule, audit trail, and memory scope. Then test with users who should and should not see the same evidence.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>oracle</category>
      <category>deepdata</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Evaluate Production RAG: Keyword, Vector, SQL, and Hybrid Retrieval</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:04:12 +0000</pubDate>
      <link>https://dev.to/oracledevs/how-to-evaluate-production-rag-keyword-vector-sql-and-hybrid-retrieval-4d0</link>
      <guid>https://dev.to/oracledevs/how-to-evaluate-production-rag-keyword-vector-sql-and-hybrid-retrieval-4d0</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Evaluate production RAG by testing each retrieval route against the same questions. Keyword search should handle exact terms. Vector search should handle semantic matches. SQL should handle current structured facts. Hybrid retrieval should prove it improves mixed workloads. Then evaluate whether the generated answer is grounded, cited, current, permission-safe, and willing to abstain.&lt;/p&gt;

&lt;p&gt;A RAG demo can look good with one document, one question, and one happy path. Production is different. Users ask for IDs, stale policies, tenant-specific records, live data, tables, and questions that are not actually answerable from the corpus.&lt;/p&gt;

&lt;p&gt;The wrong move is to debate retrieval methods in the abstract. The useful move is to build an evaluation harness that makes each method earn its place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Evaluate retrieval before changing prompts or models.&lt;/li&gt;



&lt;li&gt;Treat SQL as a first-class route for current structured data.&lt;/li&gt;



&lt;li&gt;Do not call hybrid search a win until it beats keyword and vector baselines on your actual query mix.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What should production RAG evaluation measure?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Production RAG evaluation should measure retrieval quality, answer quality, freshness, permission safety, and operational reliability. A high average retrieval score is not enough if the system fails exact identifiers, stale documents, tenant isolation, or unsupported questions.&lt;/p&gt;

&lt;p&gt;Use two layers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;Failure it catches&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval evaluation&lt;/td&gt;
&lt;td&gt;Whether the right evidence appears in top-k&lt;/td&gt;
&lt;td&gt;Wrong chunk, missing row, stale document, noisy table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer evaluation&lt;/td&gt;
&lt;td&gt;Whether the model uses evidence correctly&lt;/td&gt;
&lt;td&gt;Unsupported answer, bad citation, false confidence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For retrieval, start with recall@k, precision@k, NDCG@k, and MAP@k. For answers, score groundedness, correctness, citation validity, and abstention quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieval metrics are useful because they can run without an LLM call.&lt;/li&gt;



&lt;li&gt;Answer metrics are necessary because relevant evidence can still produce a bad answer.&lt;/li&gt;



&lt;li&gt;Production cases need to sit next to clean benchmark cases.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I compare vector-only vs hybrid RAG with keyword and SQL retrieval?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Build one question set and run every route against it. Keyword should win exact lexical queries. Vector should win paraphrases. SQL should win current structured facts. Hybrid should improve mixed intent without making exact or governed queries worse.&lt;/p&gt;

&lt;p&gt;Use this comparison model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Query type&lt;/th&gt;
&lt;th&gt;First route to test&lt;/th&gt;
&lt;th&gt;What to measure&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Error codes, product names, risk IDs&lt;/td&gt;
&lt;td&gt;Keyword retrieval&lt;/td&gt;
&lt;td&gt;Exact match, top-k position, false positives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paraphrases and fuzzy intent&lt;/td&gt;
&lt;td&gt;Vector retrieval&lt;/td&gt;
&lt;td&gt;Semantic recall, noise, ranking quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counts, filters, current state&lt;/td&gt;
&lt;td&gt;SQL or NL2SQL&lt;/td&gt;
&lt;td&gt;Query correctness, permissions, freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed exact and semantic intent&lt;/td&gt;
&lt;td&gt;Hybrid retrieval&lt;/td&gt;
&lt;td&gt;Gain over both baselines, latency cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Oracle AI Database supports vector search and hybrid search patterns. The Oracle hybrid-search documentation describes combining full-text and vector similarity search, including fusion approaches such as RRF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compare retrieval routes against the same questions, not separate anecdotes.&lt;/li&gt;



&lt;li&gt;SQL is not a fallback for RAG. It is the correct path for structured current facts.&lt;/li&gt;



&lt;li&gt;Hybrid search should be evaluated as a tradeoff: better recall versus added complexity and latency.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What should the evaluation set include?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Include the cases that break real systems: exact identifiers, paraphrases, stale and current versions, tenant boundaries, metadata filters, table lookups, multi-hop questions, and unsupported questions. A clean evaluation set gives clean scores and hides production risk.&lt;/p&gt;

&lt;p&gt;A practical first set can be 50 to 100 questions. Each question should include expected evidence, expected answer behaviour, and known failure modes.&lt;/p&gt;

&lt;p&gt;Include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exact IDs, error codes, SKUs, names, and model numbers&lt;/li&gt;



&lt;li&gt;paraphrases where the corpus uses different words than the user&lt;/li&gt;



&lt;li&gt;stale and current versions of the same document&lt;/li&gt;



&lt;li&gt;tenant and permission boundaries&lt;/li&gt;



&lt;li&gt;questions that require SQL, not semantic retrieval&lt;/li&gt;



&lt;li&gt;table and spreadsheet questions&lt;/li&gt;



&lt;li&gt;unsupported questions where the system should abstain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the evaluation set stable so changes are comparable.&lt;/li&gt;



&lt;li&gt;Add new failed production queries after triage.&lt;/li&gt;



&lt;li&gt;Label required evidence and forbidden evidence, not only final answers.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;How do I implement this with Oracle AI Database?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Store documents, chunks, embeddings, metadata, and structured records in the database path. Run keyword, vector, SQL, and hybrid retrieval as separate routes. Use metadata filters before generation, and export metrics after every retrieval change.&lt;/p&gt;

&lt;p&gt;An Oracle implementation should separate the routes clearly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Oracle Text or keyword search for exact terms.&lt;/li&gt;



&lt;li&gt;Oracle AI Vector Search for semantic retrieval.&lt;/li&gt;



&lt;li&gt;Select AI or application-controlled SQL for current structured data.&lt;/li&gt;



&lt;li&gt;Hybrid search for workloads where keyword and vector evidence both matter.&lt;/li&gt;



&lt;li&gt;Mandatory filters for tenant, source, version, status, and permissions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Useful docs and runnable assets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/" rel="noopener noreferrer"&gt;Oracle AI Vector Search User's Guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/understand-hybrid-search.html" rel="noopener noreferrer"&gt;Understand Hybrid Search&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/selai/select-ai-about.html" rel="noopener noreferrer"&gt;About Select AI&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/agent-memory/26.6/guide/get-started.html" rel="noopener noreferrer"&gt;Oracle AI Agent Memory getting-started guide&lt;/a&gt;&lt;/li&gt;



&lt;li&gt;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/oracle_rag_with_evals.ipynb" rel="noopener noreferrer"&gt;RAG evaluation notebook: oracle_rag_with_evals.ipynb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep retrieval logic visible and testable.&lt;/li&gt;



&lt;li&gt;Apply permission and freshness filters before evidence reaches the model.&lt;/li&gt;



&lt;li&gt;Publish numerical claims only after the notebook or benchmark run has exported traceable results.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;What should I do next?&lt;/h2&gt;

&lt;p&gt;Run the evaluation before choosing the architecture. If the system fails exact terms, improve lexical retrieval. If it fails paraphrases, improve vector retrieval or parsing. If it fails current facts, route to SQL. If it fails mixed intent, test hybrid retrieval. If it fails memory continuity, move to scoped agent memory rather than stuffing more context into the prompt.&lt;/p&gt;

</description>
      <category>vectordatabase</category>
      <category>sql</category>
      <category>rag</category>
      <category>oracle</category>
    </item>
    <item>
      <title>One Database for the Whole LangChain Ecosystem: Memory, Persistence, and Deep Agents on Oracle AI Database</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:01:57 +0000</pubDate>
      <link>https://dev.to/oracledevs/one-database-for-the-whole-langchain-ecosystem-memory-persistence-and-deep-agents-on-oracle-ai-34og</link>
      <guid>https://dev.to/oracledevs/one-database-for-the-whole-langchain-ecosystem-memory-persistence-and-deep-agents-on-oracle-ai-34og</guid>
      <description>&lt;p&gt;&lt;strong&gt;Retrieval, memory, persistence, and a bring-your-own-model deep-agents harness for LangChain and LangGraph, all on one Oracle AI Database instance behind a single connection pool.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Companion notebook:&lt;/strong&gt; &lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/langchain_ecosystem/research_agent_with_deepagents_oracle.ipynb" rel="noopener noreferrer"&gt;https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/langchain_ecosystem/research_agent_with_deepagents_oracle.ipynb&lt;/a&gt;&lt;/p&gt;





&lt;h2&gt;Key Takeaways&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Oracle AI Database is the unified backend for LangChain and LangGraph agent infrastructure.&lt;/strong&gt; The blog argues that vectors, chat history, semantic cache, checkpoints, documents, and relational data can live behind one Oracle connection pool instead of several separate services.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;&lt;code&gt;langchain-oracledb&lt;/code&gt; now covers both retrieval and memory.&lt;/strong&gt; It adds &lt;code&gt;OracleSemanticCache&lt;/code&gt; for paraphrase-aware LLM caching and &lt;code&gt;OracleChatMessageHistory&lt;/code&gt; for durable, session-scoped chat history. &lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;&lt;code&gt;langgraph-oracledb&lt;/code&gt; adds production persistence for LangGraph agents.&lt;/strong&gt; The new package provides checkpointing and long-term memory so agent workflows can resume across restarts, deployments, and long-running tasks. &lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;The deep-agents setup is model-flexible.&lt;/strong&gt; Developers can use Claude, OCI Generative AI, OpenAI, vLLM, or another LangChain chat model while keeping the agent’s state and memory in Oracle. &lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;The main production benefit is reduced fragmentation.&lt;/strong&gt; Instead of managing consistency, backup, governance, latency, and audit across multiple stores, the blog argues teams can run retrieval, memory, and persistence in one database system.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Foracle-langchain-ecosystem-cover-portrait-819x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Foracle-langchain-ecosystem-cover-portrait-819x1024.png" alt="Architecture diagram connecting LangChain and LangGraph to Oracle-native integration packages. Packages provide retrieval, memory, persistence, and deep-agent features, all backed by a single Oracle AI Database instance storing vectors, chat history, semantic cache, checkpoints, documents, and agent memory." width="800" height="1000"&gt;&lt;/a&gt;Oracle-native LangChain and LangGraph integrations share one Oracle AI Database for retrieval, memory, persistence, and agent state.&lt;p&gt;&lt;/p&gt;

&lt;p&gt;We’ve observed that most Langchain examples keep vectors in one service, chat history in another, cache in an entirely different service and documents in a fourth (a vector DB, a Redis, a Postgres, an object store). It works in a notebook and perhaps even in PoCs but the fragmentation shows up as a massive disadvantage in production, where the consistency, backup, latency, TTFT(time-to-first-token) and audit stories of four systems all have to line up with each other.&lt;/p&gt;

&lt;p&gt;Agent memory is everything an agent stores and retrieves as it works. It's the mix of components, tools, and libraries that let an agent recall information, reuse key details in later interactions, hold onto context for long horizon tasks, and refine what it knows to adapt over time. In practice, that means the conversation so far, the durable facts it has learned across sessions, the working state of a multi-step task, and a cache of answers it has already computed.&lt;/p&gt;

&lt;p&gt;An agent that holds those stores together stays continuous and grounded, but if built on a fragmented infrastructure, the cognitive and operational load on the agent increases which leads to failure modes and data synchronization issues.&lt;/p&gt;

&lt;p&gt;That’s why over at Oracle, the team have invested in the langchain ecosystem to bring the benefits of the converged AI database to AI developers through three major improvements and updates to the open source libraries of the langchain ecosystem.&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;The &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/aintg/langchain-oracledb-integration-guide/langchain-python.html#GUID-LANGCHAIN-PYTHON-MIGRATE-COMMUNITY" rel="noopener noreferrer"&gt;&lt;code&gt;langchain-oracledb&lt;/code&gt;&lt;/a&gt; package adds &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/aintg/langchain-oracledb-integration-guide/langchain-python.html#GUID-LANGCHAIN-PYTHON-SEMANTIC-CACHE" rel="noopener noreferrer"&gt;semantic LLM caching&lt;/a&gt; via the &lt;code&gt;OracleSemanticCache&lt;/code&gt;&amp;nbsp; class and durable &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/aintg/langchain-oracledb-integration-guide/langchain-python.html#GUID-LANGCHAIN-PYTHON-CHAT-MESSAGE-HISTORY" rel="noopener noreferrer"&gt;chat message history&lt;/a&gt; via the &lt;code&gt;OracleChatMessageHistory&lt;/code&gt; class, completing its retrieval-and-memory story.&lt;/li&gt;



&lt;li&gt;
&lt;code&gt;langgraph-oracledb&lt;/code&gt; launches as a new package, bringing graph checkpointing and long-term agent memory to &lt;a href="https://docs.langchain.com/oss/python/langgraph/add-memory#example-using-oracle-store" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt;.&lt;/li&gt;



&lt;li&gt;And &lt;code&gt;langchain-oci&lt;/code&gt; puts a provider-agnostic deep-agents factory on top: the same agent harness running on Claude, OCI Generative AI, or your own model. All of it backed by a single Oracle AI Database instance and a single &lt;code&gt;oracledb&lt;/code&gt; connection pool.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Memory is becoming the defining layer of the agent stack. The work Oracle is doing to integrate Oracle AI Database on OCI with LangChain gives developers a real path to building memory-first agents that persist, retrieve, and reason over context at scale.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;Harrison Chase, Co-Founder and CEO, LangChain&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this &lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/langchain_ecosystem/research_agent_with_deepagents_oracle.ipynb" rel="noopener noreferrer"&gt;companion notebook&lt;/a&gt;, we walk through an end-to-end example of using LangChain ecosystem packages to build a deep research agent, a common use case we see in enterprise.&lt;/p&gt;

&lt;p&gt;The update to &lt;code&gt;langchain-oracledb&lt;/code&gt; package makes Oracle AI Database a first-class backing store for LangChain applications, ensuring AI Developers build enterprise ready AI applications. See a working full code example of the features across the langchain ecosystem packages in this deep research use case.&lt;/p&gt;

&lt;p&gt;The update adds two new primitives: &lt;code&gt;OracleSemanticCache&lt;/code&gt; for LLM response caching and &lt;code&gt;OracleChatMessageHistory&lt;/code&gt; for durable session memory. They join the package's existing retrieval primitives: &lt;code&gt;OracleVS&lt;/code&gt; for vector search, &lt;code&gt;OracleEmbeddings&lt;/code&gt; for in-database embedding generation, &lt;code&gt;OracleHybridSearchRetriever&lt;/code&gt; and &lt;code&gt;OracleTextSearchRetriever&lt;/code&gt; for retrieval, and &lt;code&gt;OracleDocLoader&lt;/code&gt;, &lt;code&gt;OracleTextSplitter&lt;/code&gt;, and &lt;code&gt;OracleSummary&lt;/code&gt; for document processing.&lt;/p&gt;

&lt;p&gt;Alongside it, Oracle released &lt;code&gt;langgraph-oracledb&lt;/code&gt;, a brand-new package that gives LangGraph agents Oracle-native checkpointing and long-term memory. Both packages are available now on PyPI and documented in the official LangChain documentation.&lt;/p&gt;

&lt;p&gt;This release collapses that stack into one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;langchain-oracledb&lt;/code&gt; already solved retrieval: vectors, hybrid search, and the document pipeline in one engine. What stayed scattered was the memory. The chat history lived in one service, the LLM cache in another. With this update, a single Oracle AI Database instance holds the vector index, the chat history, the semantic cache, and the staged documents, behind one Oracle AI Database connection pool and one set of credentials.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Agent memory isn't one thing. A production agent needs short-term memory across turns, long-term memory across sessions, semantic caching for repeat queries, and retrieval over both structured and unstructured data. Putting that substrate in one database — vector search natively built in — reduces cognitive load for both the developers building the system and the agents operating inside it.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;Richmond Alake, Director of AI Developer Experience, Oracle Database&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2&gt;&lt;strong&gt;What's New in langchain-oracledb&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;The update extends the package from a retrieval integration into a full retrieval-and-memory layer, with two new primitives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OracleSemanticCache&lt;/code&gt;: semantic LLM response caching. &lt;/strong&gt;A drop-in &lt;code&gt;BaseCache&lt;/code&gt; that matches prompts by vector distance rather than exact string, so paraphrased questions hit the cache too. A tunable &lt;code&gt;score_threshold&lt;/code&gt; decides how loose a paraphrase still counts, and entries are isolated by LangChain's &lt;code&gt;llm_string&lt;/code&gt;, so a model upgrade invalidates its own entries without touching the rest. Wiring it in globally is one line, &lt;code&gt;set_llm_cache(...)&lt;/code&gt;, or attach it to a single chat model and leave an agent's intermediate calls uncached.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OracleChatMessageHistory&lt;/code&gt;: durable, session-scoped chat history. &lt;/strong&gt;Implements &lt;code&gt;BaseChatMessageHistory&lt;/code&gt; as rows in an Oracle table. One table holds thousands of concurrent sessions, survives application restarts, and supports bounded reads (&lt;code&gt;history_size=N&lt;/code&gt;) so token costs stay capped without deleting older rows. It plugs straight into &lt;code&gt;RunnableWithMessageHistory&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Foracle-rag-pipeline-portrait-819x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Foracle-rag-pipeline-portrait-819x1024.png" alt="Pipeline illustrating six RAG stages: document loading, text splitting, embedding, indexing, retrieval, and answer generation. Each stage maps to an Oracle integration component and executes against Oracle AI Database, ending with semantic caching of generated answers." width="800" height="1000"&gt;&lt;/a&gt;&lt;em&gt;Figure 1: A RAG pipeline where every stage maps to a langchain-oracledb primitive.&lt;/em&gt;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;For teams still on the Oracle classes in &lt;code&gt;langchain-community&lt;/code&gt;, &lt;code&gt;langchain-oracledb&lt;/code&gt; replaces them with identical class names and constructor signatures, so migration is an import-path change. The new primitives sit beyond that entirely: the semantic cache and chat message history do not exist in the community package. (The vector store, embeddings, and document pipeline are also available for JavaScript as &lt;code&gt;@oracle/langchain-oracledb&lt;/code&gt; on npm. The new memory primitives are Python-first.)&lt;/p&gt;

&lt;p&gt;The whole picture fits in a screenful:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import oracledb
from langchain_core.globals import set_llm_cache
from langchain_oracledb import OracleChatMessageHistory, OracleSemanticCache
from langchain_oracledb.vectorstores.oraclevs import OracleVS
 
connection = oracledb.connect(user="agent", password="...", dsn="localhost:1521/FREEPDB1")
 
# Retrieval, session memory, and LLM caching: one backend, one connection.
vector_store = OracleVS(client=connection, embedding_function=embeddings, table_name="DOCS")
history = OracleChatMessageHistory(session_id="customer-42", client=connection, table_name="CHAT_HISTORY")
set_llm_cache(OracleSemanticCache(client=connection, embedding=embeddings, table_name="LLM_CACHE"))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Three primitives that would normally mean three services, three SDKs, and three failure modes. Here, three tables.&lt;/p&gt;

&lt;p&gt;In the customer sessions and developer workshops we’ve run, the pattern is remarkably consistent: teams don't struggle to prototype agent memory — they struggle to ship it. The prototype dies somewhere between the laptop and the security review. Development runs against the Oracle AI Database container on a laptop; production runs against Autonomous AI Database with wallet-based authentication. AI teams find moving between them is a connect-string change, not a re-architecture.&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;langgraph-oracledb: A New Package for LangGraph Agents&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;The second piece of news is a launch, not an update.&lt;code&gt; langgraph-oracledb&lt;/code&gt; is a new package that implements LangGraph's persistence interfaces on Oracle AI Database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OracleSaver&lt;/code&gt; and &lt;code&gt;AsyncOracleSaver&lt;/code&gt;: graph checkpointing. &lt;/strong&gt;Every step of a LangGraph agent's state is checkpointed per &lt;code&gt;thread_id&lt;/code&gt;, so a conversation or long-running task resumes exactly where it left off, across invocations, restarts, and deploys.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OracleStore&lt;/code&gt; and &lt;code&gt;AsyncOracleStore&lt;/code&gt;: long-term agent memory. &lt;/strong&gt;A namespaced key-value store with &lt;code&gt;put&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt;, and batch operations, plus optional HNSW or IVF vector indexes so agents can search their own memories semantically, not just by key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both accept the same &lt;code&gt;oracledb&lt;/code&gt; connections and pools as &lt;code&gt;langchain-oracledb&lt;/code&gt;, which is the point. An application that mixes LangChain chains and LangGraph agents shares one system of record (same backend, same pool, same vector index semantics) rather than fragmenting across several.&lt;/p&gt;

&lt;p&gt;The packages ship with hands-on worked examples:&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;The&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/langchain_ecosystem/ai_oncall_triage_langchain.ipynb" rel="noopener noreferrer"&gt; &lt;/a&gt;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/langchain_ecosystem/ai_oncall_triage_langchain.ipynb" rel="noopener noreferrer"&gt;AI on-call triage assistant&lt;/a&gt; in the Oracle AI Developer Hub builds a LangGraph supervisor that delegates to an issue analyst and a policy specialist: vector search over a real past-issue corpus, each on-caller's saved preferences, and per-thread checkpoints all sharing one Oracle AI Database.&lt;/li&gt;



&lt;li&gt;A companion&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/main/notebooks/langchain_ecosystem/langchain_oracle_semantic_cache_chat_history.ipynb" rel="noopener noreferrer"&gt; semantic-caching and durable chat-history notebook&lt;/a&gt; covers the primitives underneath: &lt;code&gt;OracleSemanticCache&lt;/code&gt; skips the model when a new question &lt;em&gt;means&lt;/em&gt; the same as one already answered — so you never pay Claude twice for the same answer — and &lt;code&gt;OracleChatMessageHistory&lt;/code&gt; keeps each session's transcript durable and isolated across restarts.&lt;/li&gt;
&lt;/ol&gt;





&lt;h2&gt;&lt;strong&gt;Deep Agents on Oracle, Any Model&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;LangGraph is what developers reach for when an agent outgrows a single prompt-response loop and becomes a stateful workflow: one that branches, pauses for human approval, and runs long enough that state has to survive a restart. The framework's mechanism for all of this is persistence. Graph state is checkpointed at every step, so a run can be resumed, replayed, or continued in a later session. Every one of those checkpoints needs somewhere durable to live.&lt;/p&gt;

&lt;p&gt;langgraph-oracledb is a new package that gives them one. It implements LangGraph's persistence interfaces in full on Oracle AI Database:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langchain_oci import create_deepagents_agent
 
@tool
def get_stock_quote(ticker: str) -&amp;gt; str:
    """Return the latest stock quote for a ticker symbol."""
    return QUOTES.get(ticker.upper(), f"No quote for {ticker}")
 
agent = create_deepagents_agent(
    tools=[get_stock_quote],
    model=ChatAnthropic(model="claude-sonnet-4-6"),  # bring your own model: Claude, OCI GenAI, vLLM
    system_prompt="You are a concise financial assistant. Use your tools for live data.",
)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The model is the one thing you swap; the system of record stays Oracle. Claude does the reasoning, and the agent's plan, files, and per-thread state checkpoint to the same Oracle AI Database backing the chains and the LangGraph persistence: one pool, one transaction surface.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Foracle-agent-workflow-portrait-819x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Foracle-agent-workflow-portrait-819x1024.png" alt="Workflow showing an agent loop of Plan, Act, Observe, and Respond driven by a reasoning model. Oracle AI Database persists graph checkpoints with OracleSaver, long-term memory with OracleStore, and retrieval with OracleVS while the loop repeats until completion." width="800" height="1000"&gt;&lt;/a&gt;&lt;em&gt;Figure 2: An agent loop that checkpoints every step to Oracle, with any model.&lt;/em&gt;&lt;p&gt;&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;Oracle AI Database as the Unified Memory Core for AI Agents&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;Oracle AI Database is the unified retrieval and memory core across all three packages. Instead of treating the database as a passive persistence layer, the integrations treat it as the active retrieval engine that makes each LangChain and LangGraph pattern work in production.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Foracle-agent-memory-portrait-819x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2Foracle-agent-memory-portrait-819x1024.png" alt="Diagram showing four agent memory types—short-term, long-term, working state, and semantic cache—all implemented on Oracle AI Database. Each maps to an Oracle component (OracleChatMessageHistory, OracleStore, OracleSaver, and OracleSemanticCache), illustrating that different memory functions share one database system of record." width="800" height="1000"&gt;&lt;/a&gt;&lt;em&gt;Figure 3: Agent memory is not a single thing; Oracle holds every kind in one place.&lt;/em&gt;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Oracle AI Vector Search brings the retrieval strategies LangChain developers actually need into a single engine: vector similarity for semantic recall and unstructured knowledge retrieval, full-text and hybrid search for precision over keywords, and relational queries for structured, transactional memory that demands consistency. Combined with Oracle's operational story (backups, replication, high availability, governance), teams get a path from prototype to production without swapping storage layers along the way.&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;Who This Release Is For&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;The updated &lt;code&gt;langchain-oracledb&lt;/code&gt;, the new &lt;code&gt;langgraph-oracledb&lt;/code&gt;, and the deep-agents factory in &lt;code&gt;langchain-oci &lt;/code&gt;are designed for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI developers and engineers building LangChain RAG pipelines, applications, retrievers, or conversational agents who need durable memory and retrieval in one place&lt;/li&gt;



&lt;li&gt;Teams building LangGraph agents who need checkpointing and long-term memory on a production-grade backend&lt;/li&gt;



&lt;li&gt;Teams building deep agents who want a provider-agnostic harness (Claude, OCI Generative AI, or a self-hosted model) with the agent's plan and state persisted to Oracle&lt;/li&gt;



&lt;li&gt;ML engineers moving LangChain prototypes from ephemeral in-memory stores to production-grade persistence&lt;/li&gt;



&lt;li&gt;Teams already running Oracle AI Database who want LangChain and LangGraph applications to write to the system of record directly&lt;/li&gt;



&lt;li&gt;Technical leaders evaluating Oracle AI Database for unified agent infrastructure at scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Documentation and quickstarts live in the Oracle AI Database&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/aintg/langchain-oracledb-integration-guide/langchain.html" rel="noopener noreferrer"&gt; &lt;/a&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/aintg/langchain-oracledb-integration-guide/langchain.html" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt; and&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/aintg/langgraph-oracledb-integration-guide/langgraph.html" rel="noopener noreferrer"&gt; &lt;/a&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/aintg/langgraph-oracledb-integration-guide/langgraph.html" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; integration guides; source is in the&lt;a href="https://github.com/oracle/langchain-oracle" rel="noopener noreferrer"&gt; &lt;/a&gt;&lt;a href="https://github.com/oracle/langchain-oracle" rel="noopener noreferrer"&gt;oracle/langchain-oracle&lt;/a&gt; repo, with runnable&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/tree/main/notebooks/langchain_ecosystem" rel="noopener noreferrer"&gt; &lt;/a&gt;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/tree/main/notebooks/langchain_ecosystem" rel="noopener noreferrer"&gt;example notebooks&lt;/a&gt; in the Oracle AI Developer Hub. Install both packages in the same environment to run the full Oracle-native integration across LangChain and LangGraph.&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;Frequently Asked Questions&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need Oracle Cloud to use these packages?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Everything runs against Oracle AI Database Free in a local Docker container, with your own embedding model and your own LLM, and no OCI account. &lt;code&gt;langchain-oci &lt;/code&gt;adds OCI Generative AI and other managed options when you want them, but they are opt-in. The code that runs on the free container runs unchanged against Oracle Autonomous AI Database in the cloud, so moving to production is a connect-string change, not a rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which Oracle Database version do I need?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Oracle AI Database 23ai or later, which is where AI Vector Search and the &lt;code&gt;VECTOR&lt;/code&gt; data type live. That covers &lt;code&gt;OracleVS&lt;/code&gt;, &lt;code&gt;OracleEmbeddings&lt;/code&gt;, and the memory primitives. Hybrid keyword-and-vector search through &lt;code&gt;DBMS_HYBRID_VECTOR.SEARCH &lt;/code&gt;needs 26ai. The free 23ai container runs the chains, the LangGraph persistence, and the deep-agents examples end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I bring my own model, or am I tied to OCI Generative AI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bring your own. The deep-agents factory takes any LangChain chat model through &lt;code&gt;model=&lt;/code&gt;, so Claude, OpenAI, a self-hosted vLLM endpoint, or OCI Generative AI all drop in with nothing else changed. When you pass your own model, the OCI model id and auth are ignored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I migrate from the Oracle classes in langchain-community?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is an import-path change. &lt;code&gt;langchain-oracledb&lt;/code&gt; ships the same class names and constructor signatures as the Oracle classes in &lt;code&gt;langchain-community&lt;/code&gt;, so existing retrieval code keeps working once you swap the import. The new memory primitives, &lt;code&gt;OracleSemanticCache&lt;/code&gt; and &lt;code&gt;OracleChatMessageHistory&lt;/code&gt;, are not in the community package, so there is nothing to migrate there, only to adopt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do langchain-oracledb and langgraph-oracledb share one database and pool?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, and that is the point. Both accept the same &lt;code&gt;oracledb&lt;/code&gt; connections and pools, so an application that mixes LangChain chains and LangGraph agents writes to one system of record, with one set of credentials and one transaction surface, rather than fragmenting across separate backends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is this different from adding a dedicated vector database?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A dedicated vector database gives you similarity search and sits beside your operational data rather than with it. Here the vectors live in the same database as your chat history, your checkpoints, and your relational data, so vector, full-text, hybrid, and SQL retrieval all run in one engine, inside one transaction, under one backup and governance story. Cross-service consistency stops being something you engineer around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there JavaScript or TypeScript support?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The vector store, embeddings, and document pipeline are available for JavaScript as &lt;code&gt;@oracle/langchain-oracledb&lt;/code&gt; on npm. The new memory primitives, the LangGraph persistence, and the deep-agents factory are Python-first.&lt;/p&gt;

&lt;p&gt;All three packages are available now on PyPI:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pip install langchain-oracledb            # updated: semantic cache + chat history
pip install langgraph-oracledb            # new: checkpointing + agent memory
pip install "langchain-oci[deepagents]"   # new: bring-your-own-model deep-agents factory
&lt;/code&gt;&lt;/pre&gt;

</description>
      <category>database</category>
      <category>langchain</category>
      <category>agents</category>
      <category>oracle</category>
    </item>
    <item>
      <title>From Prompt to Persistence (Part 2): Putting the Multi-Tenant Agent Memory Schema to Work</title>
      <dc:creator>Anya Summers</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:00:29 +0000</pubDate>
      <link>https://dev.to/oracledevs/from-prompt-to-persistence-part-2-putting-the-multi-tenant-agent-memory-schema-to-work-317b</link>
      <guid>https://dev.to/oracledevs/from-prompt-to-persistence-part-2-putting-the-multi-tenant-agent-memory-schema-to-work-317b</guid>
      <description>&lt;p&gt;&lt;strong&gt;Retrieval, sharing, and the agent loop on top of a multi-tenant memory schema&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Companion notebook: &lt;/strong&gt;&lt;a href="https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/8c9ed028b1bea545f2cbefe057470a0294bf29b7/notebooks/multitenant_schema_walkthrough.ipynb" rel="noopener noreferrer"&gt;https://github.com/oracle-devrel/oracle-ai-developer-hub/blob/8c9ed028b1bea545f2cbefe057470a0294bf29b7/notebooks/multitenant_schema_walkthrough.ipynb&lt;/a&gt;&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Part 2 turns the schema into an operating system for agent memory.&lt;/strong&gt; It explains how short-term memory, durable long-term memory, shared memory, and retrieval work together in the agent loop.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;The Memory Manager is the control point.&lt;/strong&gt; Agents should not write directly to memory tables; the manager enforces tenant context, provenance, deduplication, versioning, deletion, and typed reads and writes.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Shared memory is scope-based, not a separate table.&lt;/strong&gt; A memory becomes shared when it is written broadly enough inside a tenant, such as with &lt;code&gt;agent_id IS NULL&lt;/code&gt;, so multiple agents can read and update it safely.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;One database engine simplifies tenant-safe retrieval.&lt;/strong&gt; The article argues that keeping policies, personas, entities, summaries, workflows, toolbox data, knowledge base chunks, and conversation events in one engine reduces cross-system security and deletion risks.&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;&lt;a href="https://blogs.oracle.com/developers/from-prompt-to-persistence-part-1-designing-multi-tenant-agent-memory-schemas-for-saas%E2%86%97" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; designed the durable layer: eight typed memory tables, each carrying the same four scope columns (&lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;agent_id&lt;/code&gt;, &lt;code&gt;thread_id&lt;/code&gt;) and the same lifecycle columns (&lt;code&gt;version&lt;/code&gt;, &lt;code&gt;valid_from&lt;/code&gt;, &lt;code&gt;valid_until&lt;/code&gt;, &lt;code&gt;deleted_at&lt;/code&gt;, &lt;code&gt;source_event_id&lt;/code&gt;). Tenant isolation lives in the database through row-level security (RLS). The other three scope dimensions are application-supplied filters. Provenance on every durable row is what makes versioned supersession and a provable right-to-forget cascade possible. If you haven't read it, start there, because the system we create in this post assumes all of it.&lt;/p&gt;

&lt;p&gt;A schema doesn't do anything on its own. It says what can be stored and how it's isolated, but it doesn't retrieve or rank rows, and it doesn't decide what's worth keeping. This post is about the code that does. We'll start with the short-term layer that sits in front of the durable tables, then work up through shared memory, the Memory Manager that fronts the whole schema, the single retrieval query that one engine makes possible, and the agent loop that ties reads and writes to specific tables at specific steps.&lt;/p&gt;





&lt;h2&gt;
&lt;a&gt;&lt;/a&gt;Short-term memory in a multi-tenant context&lt;/h2&gt;

&lt;p&gt;STM is structurally different from LTM. It's ephemeral by design and scoped to the current run, and most of it lives outside the database. But it interacts with LTM at well-defined seams, and in a multi-tenant system those seams need the same discipline as the durable layer.&lt;/p&gt;

&lt;h3&gt;
&lt;a&gt;&lt;/a&gt;Working memory: LLM Context Window + Session Memory&lt;/h3&gt;

&lt;p&gt;The LLM context window is the most visible piece of working memory: the tokens passed to the model on this turn. The Memory Manager assembles it from the durable layer (active guidelines, active personas, retrieved entities, retrieved summaries) plus the volatile tail (the last N conversation events for this thread). Nothing about the assembly is database-level. The database supplies the inputs, and the manager composes them into the prompt the model actually sees.&lt;/p&gt;

&lt;p&gt;Session memory is the in-process scratchpad. Tool call results, intermediate reasoning state, retrieval candidates the agent decided not to surface yet, partial work the agent might want to reference later in the same run. In a single-tenant prototype, this could easily live in a Python dict on the AgentSession object. In multi-tenant SaaS, the question is where to persist it, and do we persist it at all?&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;class AgentSession:
    def __init__(self, tenant_id, user_id, agent_id, thread_id, run_id, memory):
        # Identity
        self.tenant_id = tenant_id
        self.user_id   = user_id
        self.agent_id  = agent_id
        self.thread_id = thread_id
        self.run_id    = run_id

        # Ephemeral STM (lost at end of run; reconstructable from conversation_memory)
        self.scratch     = {}     # tool outputs, intermediate state
        self.turn_buffer = []     # current turn's events before flush

        # Durable, via the memory manager
        self.memory = memory
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The case for persisting session memory in multi-tenant SaaS is crash recovery. If the agent process dies mid-run, the user shouldn't have to start over. Since conversation memory already records everything durably, we can pull session state from it after a crash. The pattern that works: write session memory as conversation_memory events with &lt;code&gt;event_type = 'session_state'&lt;/code&gt;, keyed by &lt;code&gt;run_id&lt;/code&gt;. Reads after a crash restore the scratchpad from the most recent state event for that run. No special retention class needed; these events follow the same lifecycle as every other conversation record.&lt;/p&gt;

&lt;p&gt;That keeps session memory architecturally consistent with conversation memory (the same table, scope columns, RLS policy, and retention sweep). Anything recorded to conversation memory is durable anyway, so this isn't adding a new storage class; it's using the existing retention lifecycle. The &lt;code&gt;retention_class&lt;/code&gt; column controls how long records stick around (short, standard, or audit windows), and a nightly sweep drops the partitions that have aged out. Session state tagged as 'short' gets cleaned up automatically after the retention window closes.&lt;/p&gt;

&lt;h3&gt;
&lt;a&gt;&lt;/a&gt;Semantic Cache&lt;/h3&gt;

&lt;p&gt;The semantic cache is a vector index over recent conversation history, sitting between Working Memory and Long-Term Memory. The pattern handles a specific failure mode: a user references something from earlier in the conversation that's already fallen out of the volatile-tail window, but the context they're referring to isn't important enough to have been promoted to entity or summarization memory. Pure recent-turn retrieval misses it; semantic search over the full conversation history finds it.&lt;/p&gt;

&lt;p&gt;In implementation, the semantic cache is a derived projection over &lt;code&gt;conversation_memory&lt;/code&gt;. Two valid shapes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-thread (narrow).&lt;/strong&gt; A vector index over &lt;code&gt;conversation_memory&lt;/code&gt; rows filtered by (&lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;thread_id&lt;/code&gt;). Useful for long-running threads where context drift within the same conversation is the failure mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-user-recent (broad).&lt;/strong&gt; A vector index over &lt;code&gt;conversation_memory&lt;/code&gt; rows filtered by (&lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;) and &lt;code&gt;created_at &amp;gt; NOW() - INTERVAL '30 days'&lt;/code&gt;. Useful for cross-thread continuity ("you mentioned this last week when we were working on something else").&lt;/p&gt;

&lt;p&gt;The per-user-recent shape is more useful in SaaS because users come back across sessions and threads more often than they hit the long-thread case. Both shapes can coexist; the manager picks one based on the query intent.&lt;/p&gt;

&lt;p&gt;The semantic cache doesn't need its own table. It's a vector index over an existing table, plus a retrieval function that knows how to fuse its hits with the volatile tail of the prompt. Retrieval fusion deserves its own deep dive; for this post, the schema is just &lt;code&gt;CREATE VECTOR INDEX idx_conv_semantic ON conversation_memory (embedding) … on a conversation_memory&lt;/code&gt; table that has a populated embedding column for events older than the volatile-tail window.&lt;/p&gt;





&lt;h2&gt;
&lt;a&gt;&lt;/a&gt;Shared Memory and Coordination&lt;/h2&gt;

&lt;p&gt;Both of these are top-level categories in the Oracle blog taxonomy because they're concerns that cut across the type hierarchy. In single-agent systems they collapse into normal LTM scoping. In multi-agent and multi-tenant systems they need separate treatment.&lt;/p&gt;

&lt;h3&gt;
&lt;a&gt;&lt;/a&gt;Shared Memory&lt;/h3&gt;

&lt;p&gt;Shared memory is any LTM row that multiple agents read and write under the same access boundary. The structural definition falls out of the scope columns directly: a row is shared when it sits at a scope broader than a single agent, which means &lt;code&gt;agent_id IS NULL&lt;/code&gt; (shared across every agent in the tenant) or &lt;code&gt;agent_id = '&amp;lt;group_id&amp;gt;' &lt;/code&gt;(shared across a defined coordination group).&lt;/p&gt;

&lt;p&gt;A few examples to make this concrete inside a single SaaS tenant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A support agent learns that "Acme's production database moved from us-east-1 to eu-west-1." Writing this as an entity_memory row at &lt;code&gt;(tenant_id = 'acme', user_id = NULL, agent_id = NULL)&lt;/code&gt; makes it visible to every agent serving Acme. Whatever the support agent learns, every agent on the tenant can use. Compounding advantage across the whole tenant.&lt;/li&gt;



&lt;li&gt;A billing agent and a support agent need to coordinate on whether a refund request has been approved. The decision lives in &lt;code&gt;summarization_memory&lt;/code&gt; at &lt;code&gt;(tenant_id = 'acme', user_id = :user_id, agent_id = NULL)&lt;/code&gt;, where both agents can read it, neither agent owns it exclusively.&lt;/li&gt;



&lt;li&gt;A research-assistant agent maintains a workflow ("how we evaluate competitor papers") that should be available to every research assistant Acme spawns. The workflow row sits at &lt;code&gt;(tenant_id = 'acme', agent_id = NULL)&lt;/code&gt; so any agent instance can find it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shared memory doesn't need a new table type. It's a write-side discipline: when promoting a candidate to LTM, the manager picks the narrowest scope that's actually correct, and "actually correct" sometimes means broader than the agent that wrote it. The promotion gate from &lt;a href="https://blogs.oracle.com/developers/from-rag-to-memory-systems-building-stateful-ai-architecture" rel="noopener noreferrer"&gt;"From RAG to Memory Systems: Building Stateful AI Architecture"&lt;/a&gt; is where that decision belongs. Default to user scope, and promote to tenant scope only when the subject of the fact is the tenant entity itself and the fact has been observed from two independent sources.&lt;/p&gt;

&lt;p&gt;What shared memory does need is read-side coordination. Two agents writing facts about the same subject at the same time can produce contradictory rows if the writes overlap. The supersession pattern handles the resolution (later write wins by version), but the agents themselves need to know they might be writing on top of each other. The simplest pattern is optimistic concurrency: the manager's &lt;code&gt;supersede_fact&lt;/code&gt; method checks that the row being superseded is still at the version the caller saw at read time, and raises a retry-able conflict if it isn't.&lt;/p&gt;

&lt;h3&gt;
&lt;a&gt;&lt;/a&gt;Coordination&lt;/h3&gt;

&lt;p&gt;Coordination is the cross-agent messaging layer. Agent A finishes a step and hands off to Agent B. Agent C broadcasts an event that other agents subscribe to. Agent D queries the system for "who's working on this customer right now."&lt;/p&gt;

&lt;p&gt;This post doesn't define a schema for coordination memory because the design space is still wide open. Many different patterns have emerged and new ones are still being experimented with. The right shape depends on the orchestration runtime as much as on the memory layer. The thing worth naming explicitly is that coordination is a separate concern from shared memory. Shared memory is "two agents read the same row." Coordination is "Agent A tells Agent B that something happened."&lt;/p&gt;

&lt;p&gt;A working baseline for coordination in a multi-tenant SaaS context: events go into a partitioned table scoped by (&lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;coordination_group_id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;), with the same partitioning strategy as conversation_memory. Subscribers poll or stream from the table; producers append. The table participates in the same RLS policy as everything else, so coordination events never cross tenants by accident.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture1-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture1-2.png" alt="Side-by-side comparison of two multi-agent patterns. Shared memory shows multiple agents reading and writing the same tenant-scoped entity-memory row. Coordination shows one agent publishing coordination events that another agent reads by polling or streaming. Shared memory exchanges state, while coordination exchanges signals, with tenant isolation enforced in both patterns." width="800" height="405"&gt;&lt;/a&gt;&lt;em&gt;Shared memory and coordination, side by side.&lt;/em&gt;&lt;p&gt;&lt;/p&gt;





&lt;h2&gt;
&lt;a&gt;&lt;/a&gt;The Memory Manager: one door into the schema&lt;/h2&gt;

&lt;p&gt;The schema is the contract. The Memory Manager is the only code allowed to touch it. Every read and write goes through the manager, and it enforces what the schema can't. Every operation carries tenant context, all durable write carry provenance, data lands in the table that matches its type, and supersession runs in one transaction instead of an error prone multi-step process.&lt;/p&gt;

&lt;p&gt;The interface is small and shaped exactly like the typed tables it wraps. Eight write methods, eight read methods, one supersession method per supersedable type, one delete method. The one rule worth holding the line on is no escape hatch that bypasses the schema, because the moment one exists, every team that finds it will use it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt; class MemoryManager:
      """One door into the schema. Tenant context comes from scope_ctx and is
      enforced by RLS; the manager adds provenance, dedup, embedding, and
      transactional supersession on top."""

      def __init__(self, db, scope_ctx): ...   # scope_ctx sets the tenant context for RLS

      # WRITES — one typed method per memory type; provenance required on durable types
      def write_entity(self, *, subject, predicate, content, confidence,
                       written_by, source_event_id,
                       user_id=None, agent_id=None, thread_id=None): ...
      # write_guideline, write_persona, write_summarization, write_workflow,
      # write_toolbox, ingest_document, write_conversation_event follow the same shape

      # READS — scoped retrieval; the tenant predicate is applied automatically
      def search_entities(self, query, *, user_id=None, agent_id=None, top_k=10): ...
      # read_active_guidelines, read_active_personas, read_active_toolbox,
      # search_summarizations, search_workflows, search_knowledge_base, read_thread

      # SUPERSESSION — versioned, with optimistic concurrency
      def supersede_entity(self, entity_id, *, new_content, written_by,
                           source_event_id, confidence, expected_version): ...

      # DELETION — right-to-forget, one transaction each
      def forget_user(self, user_id): ...
      def forget_tenant(self): ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Four invariants show up across every method. Tenant context is read from &lt;code&gt;scope_ctx&lt;/code&gt;, never passed by the caller, because the application set it once per request and the database is already enforcing it via RLS. Provenance is a required parameter on durable types; the manager refuses any write to a durable type (&lt;code&gt;entity&lt;/code&gt;, &lt;code&gt;summarization&lt;/code&gt;, &lt;code&gt;conversation_event&lt;/code&gt;) that's missing its &lt;code&gt;source_event_id&lt;/code&gt;. Supersession is one method per supersedable type, and it takes &lt;code&gt;expected_version&lt;/code&gt; so concurrent writes get a retry-able conflict instead of silently clobbering each other. And deletion comes in two flavors (user, tenant), both wrapped in a single transaction internally.&lt;/p&gt;

&lt;p&gt;The read side has its own invariants, and they're as important as the write ones. Every read filters the same way: &lt;code&gt;deleted_at IS NULL&lt;/code&gt;, &lt;code&gt;valid_until IS NULL&lt;/code&gt; or still in the future. A superseded fact carries a stamped &lt;code&gt;valid_until&lt;/code&gt;, so the moment a newer version lands the old one drops out of every read without anyone asking for it, and an expired record drops out the same way when its clock runs out. Because that predicate lives in the manager rather than in each caller, there's no read path that can accidentally surface a stale or superseded row.&lt;/p&gt;

&lt;p&gt;Precedence across types is really two questions, and conflating them is where retrieval designs usually go wrong. The first is governance: an authored guideline outranks an inferred preference, every time. Policies load in full into the static prefix because a rule that applies has to apply, and an inferred value carries a confidence the manager can gate against a policy-supplied floor, so a weak guess never overrides an explicit instruction. The second is relevance: when an entity, a summary, and a vector hit all speak to the same query, the manager doesn't crown a winner. It tags each result with its type and a relevance tier and hands them back for the prompt to slot into the right region. Fusing those evidence types into a single ranking, or reranking across them, is a real pipeline and deserves its own post. The manager's job is to keep the candidates honest and labeled. Collapsing them comes later, in a stage built for it.&lt;/p&gt;

&lt;p&gt;The manager is also the seam where filesystem-style ergonomics meet database substrate. From the agent's perspective, calling &lt;code&gt;manager.write_entity(...)&lt;/code&gt; feels like writing a row to a notebook. The manager handles storage and indexing, the scope check, provenance, dedup, embeddings, supersession bookkeeping, and the tenant boundary. The agent code never writes raw SQL and never sees the schema directly. When the schema evolves, the manager changes in one place and every caller upgrades for free.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture2-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture2-2.png" alt="Diagram showing a memory manager between agents and eight memory tables. Agents call typed methods such as writing entities, personas, documents, reading threads, and forgetting users. All requests pass through the memory manager, which is the only component that accesses the schema. It routes operations to guideline, persona, entity, summarization, workflow, toolbox, knowledge-base, and conversation memory tables, centralizing schema changes and business rules." width="799" height="398"&gt;&lt;/a&gt;&lt;em&gt;Memory Manager surface.&lt;/em&gt;&lt;p&gt;&lt;/p&gt;





&lt;h2&gt;
&lt;a&gt;&lt;/a&gt;Why one engine wins for multi-tenant SaaS&lt;/h2&gt;

&lt;p&gt;By this point the schema is eight tables, four scope dimensions, row-level security on every table, transactional lifecycles, and a manager that wraps it all. The remaining question is where to put it. The polyglot persistence trap from &lt;a href="https://blogs.oracle.com/developers/from-rag-to-memory-systems-building-stateful-ai-architecture" rel="noopener noreferrer"&gt;From RAG to Memory Systems: Building Stateful AI Architecture&lt;/a&gt; applies here with extra force, because every cross-system pain point in a single-org agent compounds in a SaaS provider running thousands of tenants.&lt;/p&gt;

&lt;p&gt;The accidental architecture goes the same way it always does. Postgres for users, accounts, and policies, and while Postgres handles vectors and full-text search these days, at multi-tenant SaaS scale the workloads tend to split out anyway. A dedicated vector database for the entity, summarization, and knowledge-base embeddings. Elasticsearch or OpenSearch for the lexical side of hybrid retrieval. Object storage for raw conversation transcripts and ingested documents. Maybe a graph database for entity relationships.&lt;/p&gt;

&lt;p&gt;Each component is a reasonable choice for the job it was built for. The pain begins the moment a cross-system operation has to honor tenant boundaries.&lt;/p&gt;

&lt;p&gt;Backups split four ways and each one needs a tenant-partitioned strategy. Security models split four ways and each one needs the same tenant predicate enforced. Deletion splits four ways and a partial deletion in one of them is a regulatory finding. Per-tenant encryption splits four ways and key rotation has to coordinate across all of them. Per-tenant data residency (Acme's data must stay in EU; Globex's in US) splits four ways and each system has to support the same residency rules independently.&lt;/p&gt;

&lt;p&gt;Oracle AI Database is a great choice for this architecture because one engine can host policy data, persona data, entity data with embedding columns, summarization data with embedding columns, knowledge-base documents and chunks, workflow definitions, toolbox definitions, and conversation events, all under one row-level security model that ties tenant isolation to a session context the application sets once per request.&lt;/p&gt;

&lt;p&gt;Because it's all one engine, the agent's Retrieve step pulls every memory type it needs in a single round trip. Each branch of a UNION ALL returns the same shape (type, vec_score, lex_score, relevance, payload), so guidelines, personas, toolbox definitions, entities, summarizations, workflows, knowledge-base chunks, and the recent conversation tail all come back from a single query plan, with the tenant predicate enforced by RLS on every branch (condensed for brevity):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT type, vec_score, lex_score,
         CASE WHEN vec_score IS NULL  THEN NULL
              WHEN vec_score &amp;gt;= 0.7   THEN 'high'
              WHEN vec_score &amp;gt;= 0.5   THEN 'standard'
              ELSE                         'low'
         END AS relevance,
         payload, sort_bucket
  FROM (
    WITH
    guidelines AS (              -- enumerated: loads in full, no score
      SELECT 'guideline' AS type, CAST(NULL AS NUMBER) AS vec_score,
             CAST(NULL AS NUMBER) AS lex_score, 0 AS sort_bucket,
             JSON_OBJECT('guideline_key' VALUE guideline_key,
                         'guideline_value' VALUE guideline_value) AS payload
        FROM guideline_memory
       WHERE deleted_at IS NULL AND valid_until IS NULL
         AND (user_id   IS NULL OR user_id   = :user_id)
         AND (agent_id  IS NULL OR agent_id  = :agent_id)
         AND (thread_id IS NULL OR thread_id = :thread_id)
    ),
    -- Entity: hybrid. A vector candidate pool and an Oracle Text pool, FULL OUTER JOINed.
    entity_vec AS (
      SELECT id, subject, predicate, confidence,
             VECTOR_DISTANCE(embedding,
               VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA), COSINE) AS vec_dist
        FROM entity_memory
       WHERE deleted_at IS NULL AND (valid_until IS NULL OR valid_until &amp;gt; SYSTIMESTAMP)
         AND (user_id IS NULL OR user_id = :user_id)
       ORDER BY vec_dist FETCH FIRST 10 ROWS ONLY
    ),
    entity_lex AS (
      SELECT id, subject, predicate, confidence, SCORE(11) AS lex_raw
        FROM entity_memory
       WHERE deleted_at IS NULL AND (valid_until IS NULL OR valid_until &amp;gt; SYSTIMESTAMP)
         AND (user_id IS NULL OR user_id = :user_id)
         AND CONTAINS(content, :lex_query, 11) &amp;gt; 0
       ORDER BY lex_raw DESC FETCH FIRST 10 ROWS ONLY
    ),
    entities AS (
      SELECT * FROM (
        SELECT 'entity' AS type,
               CASE WHEN v.vec_dist IS NOT NULL THEN 1.0 / (1.0 + v.vec_dist) END AS vec_score,
               l.lex_raw AS lex_score,   -- raw lexical SCORE, shown alongside, not fused
               3 AS sort_bucket,
               JSON_OBJECT('subject'   VALUE COALESCE(v.subject, l.subject),
                           'predicate' VALUE COALESCE(v.predicate, l.predicate),
                           'confidence' VALUE COALESCE(v.confidence, l.confidence)) AS payload
          FROM entity_vec v
          FULL OUTER JOIN entity_lex l ON v.id = l.id
         ORDER BY COALESCE(1.0 / (1.0 + v.vec_dist), 0) DESC
      ) WHERE ROWNUM &amp;lt;= 5
    )
    -- persona, toolbox (enumerated); summarization (hybrid); workflow,
    -- knowledge_base (vector-only); recent_conversation (the volatile tail)
    -- all follow these same shapes and the same scope predicates.
    SELECT type, vec_score, lex_score, payload, sort_bucket FROM guidelines
    UNION ALL
    SELECT type, vec_score, lex_score, payload, sort_bucket FROM entities
    -- UNION ALL ... the remaining six branches
  )
  ORDER BY sort_bucket, vec_score DESC NULLS LAST
  -- tenant_id predicate appended automatically by RLS on every branch
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Three retrieval styles live in that one statement. Enumerated types (guideline, persona, toolbox) load in full with no score, because a policy or tool that applies must apply. Hybrid types (entity, summarization) pair a vector candidate pool with an Oracle Text pool through a &lt;code&gt;FULL OUTER JOIN&lt;/code&gt;, so each row carries both a vector score and a raw lexical score, shown side by side rather than fused. Fusing the two into a single ranking, or adding a reranking pass over the merged candidates, is its own deeper topic. Workflow and knowledge base rank by vector similarity alone. The outer query maps the vector score to a relevance tier (&lt;code&gt;high&lt;/code&gt;, &lt;code&gt;standard&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;), and the type column tags every row so the application can route each one to the right slice of the prompt. A single branch is worth seeing on its own, because it shows how a policy shapes what comes back.&lt;/p&gt;

&lt;p&gt;The example below shows a single branch governed by a policy that supplies the confidence floor:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT  e.id, e.subject, e.content, e.confidence, e.source_event_id,
        VECTOR_DISTANCE(
          e.embedding,
          VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :query AS DATA),
          COSINE
        ) AS vec_dist
  FROM  entity_memory e
  JOIN  guideline_memory g
    ON  g.guideline_key = 'entity_retrieval'
   AND  g.valid_until IS NULL
   AND  g.user_id IS NULL                    -- tenant-scoped guideline
 WHERE  e.deleted_at IS NULL
   AND  (e.valid_until IS NULL OR e.valid_until &amp;gt; SYSTIMESTAMP)
   AND  e.confidence &amp;gt;= JSON_VALUE(g.guideline_value, '$.min_confidence')
   AND  (e.user_id IS NULL OR e.user_id = :user_id)   -- app-supplied filter, like the canonical predicate
 ORDER BY vec_dist
 FETCH FIRST 10 ROWS ONLY;
-- tenant_id predicate appended automatically by RLS on both tables
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It collapses to one query plan over one transactional snapshot, governed by a single security model, backup strategy, and tenant boundary. The query optimizer decides whether to apply the JSON predicate before or after the vector ranking. The application doesn't coordinate across systems because there are no other systems to coordinate with.&lt;/p&gt;

&lt;p&gt;A few things still don't collapse cleanly. High-volume conversation memory at SaaS scale (millions of events per day across thousands of tenants) sometimes belongs in a store tuned for high-cardinality append-only workloads, with the OLTP engine handling everything else. Large blob assets (raw PDFs of ingested documents, generated images, video tool outputs) belong in object storage, with only their metadata living in the database. The collapse that matters is between the seven LTM tables that hold the agent's actual knowledge (guidelines, personas, entities, summarizations, workflows, toolbox, knowledge base) which is where every meaningful retrieval join lives. Conversation memory and blobs can sit alongside; the join across the seven core types is the one the converged engine wins.&lt;/p&gt;

&lt;p&gt;In a multi-tenant SaaS context, "one less system to enforce tenant isolation in" is the load-bearing benefit. Every external system is a place tenant boundaries can be forgotten. Collapsing the join surface into one engine collapses the security surface into one system, and that's the architectural property that compounds across every feature added afterward.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture2-3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture2-3.png" alt="Side-by-side comparison of polyglot and converged architectures. The polyglot approach stores data across PostgreSQL, Pinecone, Elasticsearch, S3, and a graph database, requiring tenant isolation to be enforced separately in each store. The converged approach uses a single Oracle AI Database with one row-level security policy protecting relational, vector, knowledge, workflow, and conversation data, providing one place to enforce tenant isolation." width="800" height="419"&gt;&lt;/a&gt;&lt;em&gt;Polyglot vs converged in multi-tenant SaaS.&lt;/em&gt;&lt;p&gt;&lt;/p&gt;





&lt;h2&gt;
&lt;a&gt;&lt;/a&gt;From schema to running agent&lt;/h2&gt;

&lt;p&gt;The schema doesn't exist for its own sake. It exists to be read from and written to by the agent loop, in a specific pattern that gives the architecture its predictability. Tying the two together is what turns the design from a data dictionary into an architecture.&lt;/p&gt;

&lt;p&gt;The loop has five steps: Ingest, Retrieve, Infer &amp;amp; Act, Evaluate, Promote. Each step reads from and writes to specific tables, and the write rules are deliberately narrow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingest&lt;/strong&gt; writes to &lt;code&gt;conversation_memory&lt;/code&gt;. Every user message, every tool call, every tool result, every model response lands as a conversation row first, before anything else happens. Conversation is the source from which everything else gets derived; if a piece of state didn't make it into a conversation row, it can't be reconstructed later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieve&lt;/strong&gt; reads from the durable tables. Active guidelines via exact match by scope. Active personas via exact match by scope (the full set, every turn). Active toolbox via exact match by scope. Entities via hybrid retrieval. Summarizations via hybrid retrieval. Workflows via similarity match if the current task looks like one we've seen before. Knowledge base via vector search if the query is grounded in reference content. The semantic cache via vector search over recent conversation_memory if the user's reference points outside the volatile tail. No writes in this phase. Retrieve is read-only by design.&lt;/p&gt;

&lt;p&gt;Freshness and conflict deserve a callout here. The manager filters deleted, expired, and superseded rows at read time, so nothing stale reaches assembly. When current rows still disagree, Retrieve doesn't pick a winner. The canonical typed tables are the source of truth, so a summary or semantic-cache hit ranks below the structured fact it came from. Each candidate comes back tagged by type, with its confidence and provenance, leaving fusion and reranking to a later stage. Guidelines are the exception. They apply in full, above the scored evidence, for the governance reasons the Memory Manager section covers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infer &amp;amp; Act&lt;/strong&gt; reads the assembled context and writes back to &lt;code&gt;conversation_memory&lt;/code&gt; (model response, tool calls, tool results, optional session_state events). It does not write to any other table directly. The agent's reasoning doesn't get promoted to entity memory by virtue of being uttered; it has to earn its way in through the promotion gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluate&lt;/strong&gt; runs the extraction passes for candidate entities, candidate personas, candidate summarizations, and candidate workflow updates. These get scored and queued. They don't get written to durable tables yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Promote&lt;/strong&gt; is the only step allowed to call &lt;code&gt;write_entity&lt;/code&gt;, &lt;code&gt;write_persona&lt;/code&gt;, &lt;code&gt;write_summarization&lt;/code&gt;, &lt;code&gt;write_workflow&lt;/code&gt;, &lt;code&gt;supersede_*&lt;/code&gt;, or &lt;code&gt;ingest_document&lt;/code&gt;. The promotion gate lives here. Each candidate gets type-specific verification, dedup, scope assignment, and a transactional write. Promote is the only step that modifies the durable LTM layer.&lt;/p&gt;

&lt;p&gt;This narrow write rule is why provenance works. Every durable write happens inside the Promote step, which means every durable write has a clear &lt;code&gt;source_event_id&lt;/code&gt; (the conversation event the candidate was extracted from) and a clear &lt;code&gt;written_by&lt;/code&gt; (the agent that ran the extraction, or &lt;code&gt;'promotion_job'&lt;/code&gt; if it ran in a background worker). Writing to durable memory from inside Infer &amp;amp; Act is a code-review smell worth catching every time.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture2-4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Fwp-content%2Fuploads%2Fsites%2F129%2F2026%2F07%2FPicture2-4.png" alt="Diagram showing a five-stage agent memory lifecycle: Ingest, Retrieve, Infer and Act, Evaluate, and Promote. Tenant context is applied at ingest and enforced throughout with row-level security. Conversation memory receives writes during ingestion and inference. Durable memory tables, including guideline, persona, entity, workflow, toolbox, summarization, and knowledge base, are read during retrieval and written only during the Promote step." width="800" height="533"&gt;&lt;/a&gt;&lt;em&gt;Loop steps and table access in a multi-tenant context.&lt;/em&gt;&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
&lt;a&gt;&lt;/a&gt;Where do we go from here&lt;/h3&gt;

&lt;p&gt;This schema is an advanced starting point for multi-tenant agent memory.&lt;/p&gt;

&lt;p&gt;The eight core tables above are the canonical layer: the governed, versioned, tenant-isolated source of truth. Derived context (alternate embeddings, summarization rollups, pre-joined retrieval views, materialized projections) is everything optimized for retrieval that can be rebuilt from the canonical layer. Mixing the two is how memory systems start to drift. Separating them is how the schema stays honest as it scales to thousands of tenants.&lt;/p&gt;

&lt;p&gt;Querying the derived layer well is its own challenge. Hybrid retrieval (vector plus lexical plus metadata, fused and reranked) is a multi-stage pipeline, and there's no single library call that does it for you. The converged query above is what makes that pipeline tractable to write as one query plan instead of four cross-system round trips per tenant, but it stops where the ranking starts: it returns vector and lexical scores side by side without fusing them. Fusing and reranking those candidates, and querying the canonical-versus-derived layer above, are where the next posts go. A vector store is not a memory system, and a single-org schema isn't a SaaS memory system. A typed, multi-tenant schema with provenance, scope, tenant isolation, and lifecycle is. Get this part right and everything else is optimization. Get it wrong and you'll spend the next year explaining why one tenant's data showed up in another tenant's results.&lt;/p&gt;





&lt;h2&gt;
&lt;a&gt;&lt;/a&gt;Appendix: the seed dataset&lt;/h2&gt;

&lt;p&gt;The companion repo for this article ships a runnable version of the DDL from &lt;a href="https://blogs.oracle.com/developers/from-prompt-to-persistence-part-1-designing-multi-tenant-agent-memory-schemas-for-saas%E2%86%97" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; plus a seed dataset that covers all eight core LTM tables (plus the document chunks and deletion_events tables) across three example tenants. The seed follows a SaaS research-assistant scenario: each tenant has its own users, agents, ingested document collection, learned workflows, and toolbox configuration. The notebook walks through tenant provisioning (creating the per-tenant guideline and toolbox defaults), one write per type, the hybrid retrieval query against entity memory, the supersession of a fact after a paper is updated, the right-to-forget cascade for one user inside one tenant, and the tenant-termination cascade for an entire tenant.&lt;/p&gt;

&lt;p&gt;If you only have time to try one thing from this article, clone the repo and run the seed. The schema and dataset are the foundation we'll build on in future posts.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agentmemory</category>
      <category>oracle</category>
      <category>multitenant</category>
    </item>
  </channel>
</rss>
