Every RAG tutorial starts the same way: spin up a Pinecone index, generate some embeddings, wire it into LangChain, and call it a day. That's exactly what I did for a customer-support search tool I built last year until I realized I was paying to sync data between two databases that both claimed to be "the source of truth." Oracle 23ai's native VECTOR type looked like a way to collapse that stack into one system, so I spent a week migrating a real workload off Pinecone and into Oracle to see if it actually held up. Here's what I found.
Why bother moving off a dedicated vector database
The pitch for Oracle 23ai's vector support isn't "AI database," it's "one less database." Instead of exporting rows to a separate vector store and keeping the two in sync with webhooks or batch jobs, the embeddings live in the same table as the row they describe. VECTOR is a first-class column type, not a JSON blob or an external extension, and it's supported directly in the SQL parser alongside similarity operators. For a support-ticket search feature where the source data was already in Oracle, that meant deleting an entire sync pipeline.
Setting up the VECTOR column and index
Adding a vector column is close to adding any other typed column. You specify a dimension and a distance metric up front, then build an index the same way you would for a normal search column.
sql
CREATE TABLE support_articles (
id NUMBER GENERATED ALWAYS AS IDENTITY,
title VARCHAR2(200),
body CLOB,
embedding VECTOR(1536, FLOAT32)
);
CREATE VECTOR INDEX support_articles_vec_idx
ON support_articles (embedding)
ORGANIZATION NEIGHBOR PARTITIONS
DISTANCE COSINE;
The dimension (1536 here) just needs to match whatever embedding model you're using. I was testing against an OpenAI-style embedding size, but Oracle documents support for 90+ models, so the column doesn't care where the vectors come from.
Generating and loading embeddings
The embedding generation step doesn't change much from a Pinecone setup; you still call an embedding model from application code. The difference is where the vector lands afterward.
python
import oracledb
import array
connection = oracledb.connect(user="app", password="app_pw", dsn="localhost/freepdb1")
cursor = connection.cursor()
def embed(text: str) -> list[float]:
# placeholder for whatever embedding model/client you use
return model.embed(text)
rows = [
("Resetting your password", "To reset your password, go to Settings > Security..."),
("Exporting invoices", "Invoices can be exported as CSV from the Billing tab..."),
]
for title, body in rows:
vec = array.array("f", embed(body))
cursor.execute(
"INSERT INTO support_articles (title, body, embedding) VALUES (:1, :2, :3)",
[title, body, vec],
)
connection.commit()
No separate upsert API, no metadata filters syntax to learn; it's a normal INSERT, and the embedding column behaves like any other bind variable.
Similarity queries
This is where the SQL-native approach actually pays off. Instead of a separate query language for the vector store plus a separate SQL query for metadata filters, both live in one statement.
sql
SELECT id, title
FROM support_articles
ORDER BY VECTOR_DISTANCE(embedding, :query_embedding, COSINE)
FETCH FIRST 5 ROWS ONLY;
Filtering by metadata, say, only articles from a specific product line, is just an ordinary WHERE clause next to the vector search, rather than a separate filter object bolted onto an API call.
sql
SELECT id, title
FROM support_articles
WHERE product_line = 'billing'
ORDER BY VECTOR_DISTANCE(embedding, :query_embedding, COSINE)
FETCH FIRST 5 ROWS ONLY;
Hooking it into LangChain
Oracle's LangChain integration swaps in as a drop-in vector store, so the retrieval side of an existing RAG chain barely changes.
python
from langchain_community.vectorstores import OracleVS
from langchain_community.embeddings import OpenAIEmbeddings
vs = OracleVS(
client=connection,
embedding_function=OpenAIEmbeddings(),
table_name="support_articles",
)
results = vs.similarity_search("how do I export an invoice", k=5)
The rest of the chain prompt template, LLM call, and response parsing is untouched. The only thing that moved is where the nearest-neighbor lookup happens.
Performance and cost notes
For a corpus in the low hundreds of thousands of rows, query latency was comparable to Pinecone's managed tier once the vector index was properly tuned. The ORGANIZATION NEIGHBOR PARTITIONS index type matters here, and skipping it in favor of a naive full scan makes similarity search noticeably slower. The bigger difference was operational: one database to back up, one connection pool to manage, one bill. Pinecone's pricing scales with vector count and query volume as a separate line item; folding vectors into Oracle meant the cost showed up inside infrastructure I was already paying for, rather than as a new per-service subscription. The trade-off is that Oracle's vector indexing is newer than Pinecone's, so very large-scale workloads (tens of millions of vectors) are less battle-tested than those on a purpose-built vector database with years of production tuning behind it.
For a mid-sized RAG workload where the source data already lived in Oracle, the migration removed a sync pipeline, a second bill, and a second set of credentials to rotate without giving up much on query performance.