You Probably Don't Need a Vector Database. Here's How to Do AI in PostgreSQL or SurrealDB Instead.
Let's be honest: the "modern" AI stack has become a tangled mess. You're told you need your primary database, a separate specialized vector database for similarity search, and a third-party API or a dedicated inference engine just to generate embeddings. This isn't just complex; it's an expensive, slow, and brittle data pipeline.
But what if most of it is an unnecessary abstraction? What if the hype around dedicated vector databases is just that—hype?
This article is a practical, controversial guide to simplifying your AI stack. I'm going to show you how to bypass not only dedicated vector databases but also external inference engines by generating and searching embeddings directly within your database. This isn't a toy example; it's a production-ready approach that treats an embedding model (like GloVe or fastText) as just another piece of data. We'll explore two powerful blueprints for achieving this: one for the ever-reliable PostgreSQL and one for the modern multi-model SurrealDB.
It's time to ditch the expensive middleware and bring your AI logic back home to your data. All the code discussed is open-source and available in this GitHub repository.
The Foundation: DDL as a Blueprint for Simplicity
It all starts with the DDL. How we define our tables reveals how simple our architecture can be.
PostgreSQL: Turning a Workhorse into a Vector Engine
CREATE TABLE embedding (
word TEXT PRIMARY KEY,
embedding vector
);
CREATE INDEX idx_embedding_word ON embedding (word);
CREATE TABLE sample_content (
id bigserial PRIMARY KEY,
content TEXT,
embedding vector(50)
);
CREATE INDEX ON sample_content USING hnsw (embedding vector_l2_ops);
Analysis:
SurrealDB: Making In-Database AI Effortless
REMOVE TABLE IF EXISTS embedding_model;
DEFINE TABLE embedding_model TYPE NORMAL SCHEMAFULL;
DEFINE FIELD word ON embedding_model TYPE string;
DEFINE FIELD embedding ON embedding_model TYPE array<float>;
REMOVE TABLE IF EXISTS sample_content;
DEFINE TABLE sample_content TYPE NORMAL SCHEMAFULL;
DEFINE FIELD content ON sample_content TYPE string;
DEFINE FIELD embedding ON sample_content TYPE option<array<float>> DEFAULT ALWAYS fn::content_to_vector(content);
DEFINE INDEX embedding_index_hnsw ON sample_content FIELDS embedding HNSW DIMENSION 50 M 32 EFC 300;
DEFINE ANALYZER IF NOT EXISTS large_name_analyzer TOKENIZERS class FILTERS lowercase, ascii, edgengram(2, 10);
Analysis:
The Engine Room: Bypassing the Inference Engine with SQL/SurrealQL
This is where the magic happens. By writing functions in our database, we are creating a lightweight, on-demand inference engine that lives right next to our data.
PostgreSQL: A Procedural, DIY Inference Engine
The plpgsql code requires a chain of functions to work, but the result is the same: text goes in, a vector comes out, with no external API call.
-- A key snippet from the main function, demo_get_sentence_vectors
-- 1. Try to find the word's vector in our model table.
SELECT embedding INTO word_embedding
FROM embedding
WHERE word = current_word;
-- 2. If found, use it. If not, handle the OOV case.
IF word_embedding IS NOT NULL THEN
all_sentence_vectors := array_append(all_sentence_vectors, word_embedding);
ELSE
-- Generate n-grams for the unknown word and average their vectors.
oov_ngrams := demo_generate_edgengrams(current_word, 2, 10);
-- ... (looping and averaging logic omitted for brevity)
END IF;
SurrealDB (functions.surql): A Declarative, Functional Inference Engine
SurrealDB's functional approach is far more concise and powerful, showing what's possible when AI is a native concept.
DEFINE FUNCTION fn::retrieve_vectors_for_text_with_oov($text:string)
{
-- One statement to tokenize, look up vectors via Record ID, and handle misses.
LET $all_vectors = (SELECT VALUE {'word':$this, 'embedding':type::thing("embedding_model",$this).embedding} FROM $text.lowercase().words());
-- Separate found vectors from unfound words.
LET $matched_word_vectors = (SELECT VALUE embedding FROM $all_vectors WHERE embedding IS NOT NONE);
LET $unmatched_words = (SELECT VALUE word FROM $all_vectors WHERE embedding IS NONE);
-- For unfound words, use the built-in analyzer to find and average n-gram vectors.
LET $umatched_word_vectors = $unmatched_words.map(
|$word| fn::mean_vector((SELECT VALUE type::thing("embedding_model",$this).embedding FROM search::analyze("large_name_analyzer", $word.word)))
);
RETURN $matched_word_vectors.concat($umatched_word_vectors);
};
Beyond RAG: The Untapped Potential of Vector Search
When most people hear "vector search," they think of Retrieval-Augmented Generation (RAG) for chatbots. That's a powerful use case, but it's just the tip of the iceberg. Thinking of vectors solely for semantic search is like thinking of SQL solely for SELECT *. The real power lies in using vector similarity as a general-purpose tool for pattern matching and data analysis.
By training models on your own domain-specific data, you can unlock a new class of applications:
Conclusion: Stop Buying Middleware, Start Using Your Database
The evidence is clear. Before you add another service, another pipeline, and another monthly bill to your stack, look at the incredible power sitting in the databases you can already use.
For a vast number of AI-powered applications, a dedicated vector database isn't just overkill—it's an unnecessary architectural and financial burden. The future of AI applications isn't about adding more boxes and arrows to our diagrams; it's about simplifying, integrating, and moving the logic closer to the data.
A Look Ahead: The Next Frontiers
Now, for some food for thought. While the patterns shown here are powerful, two major challenges lie on the horizon:
The real question isn't which vector database to choose, but whether you need one at all.
Ready to challenge the status quo and build a leaner AI stack? Let's connect. I specialize in finding pragmatic, powerful solutions that bypass the hype.