Table of Contents
Developers often start RAG apps using tutorials that recommend dedicated vector databases.
Step 1: Sign up for a vector database like Pinecone or Qdrant.
This adds a costly SaaS service to your architecture or requires you to manage it yourself.
And if you are building line-of-business applications in .NET, dedicated vector databases often introduce another problem: Data Synchronization.
If core entities like Products, Customers, or SupportTickets exist in a relational database and vector embeddings reside in a specialized vector DB, you face a distributed systems challenge. What if a product is deleted or its description updated? Synchronizing datastores becomes daunting.
A pragmatic solution? Store your vectors alongside your relational data.
Using PostgreSQL, the pgvector extension transforms your relational database into a powerful vector search engine. Better yet, it integrates seamlessly with Entity Framework Core.
You can build a RAG application without adding any new infrastructure.
Step 1: Install the Required Packages
Start by adding the pgvector EF Core integration package. Run the following commands in your project:
dotnet add package Pgvector.EntityFrameworkCore
Note: The pgvector extension must be available in your PostgreSQL installation and enabled in the database you use. If you use the pgvector/pgvector Docker image, the extension is already installed, but it still needs to be enabled per database.
You can enable it manually with:
CREATE EXTENSION IF NOT EXISTS vector;
Or let EF Core handle it through:
modelBuilder.HasPostgresExtension("vector");
Step 2: Define Your Entity
Suppose you’re developing an internal knowledge base. With a Document entity, enhance storage by adding a Vector property for embeddings generated by an embedding model, for example OpenAI’s text-embedding-3-small, which produces 1536-dimensional vectors by default.
using Pgvector;
using System.ComponentModel.DataAnnotations.Schema;
public class Document
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
// 1536 is the default dimension for OpenAI text-embedding-3-small.
// Match this dimension to the embedding model you actually use.
[Column(TypeName = "vector(1536)")]
public Vector Embedding { get; set; }
// We can still have standard relational data!
public int TenantId { get; set; }
}
Note: text-embedding-3-small produces 1536-dimensional embeddings by default.
text-embedding-3-large produces 3072-dimensional embeddings by default. pgvector can store vectors larger than 2000 dimensions, but HNSW/IVFFlat indexes for the regular vector type support up to 2000 dimensions. If you use text-embedding-3-large, either request fewer dimensions from the embedding API or evaluate halfvec/HalfVector for indexed search.
Step 3: Configure the DbContext
Configure Entity Framework Core to activate the vector extension in PostgreSQL. Add an HNSW (Hierarchical Navigable Small World) index to the embedding column. For small datasets, exact search without an index can be fine. As the number of vectors grows, an approximate index such as HNSW often becomes important for latency. Just remember that HNSW trades some recall for speed.
pgvector can handle larger datasets efficiently, but HNSW is not magic. It is an approximate nearest-neighbor index with trade-offs between recall, speed, memory usage, and build time.
For HNSW indexes, tune m and ef_construction during index creation. At query time, tune hnsw.ef_search if you need better recall. Higher values usually improve recall, but increase query cost. For filtered vector search, also index your relational filter columns, for example TenantId, and test the query plan with realistic data.
using Microsoft.EntityFrameworkCore;
using Pgvector.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public DbSet<Document> Documents { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("vector");
modelBuilder.Entity<Document>()
.HasIndex(d => d.TenantId);
modelBuilder.Entity<Document>()
.HasIndex(d => d.Embedding)
.HasMethod("hnsw")
.HasOperators("vector_cosine_ops")
.HasStorageParameter("m", 16)
.HasStorageParameter("ef_construction", 64);
}
}
Make sure you register the vector types in your Program.cs when configuring the DbContext:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("DefaultConnection"),
o => o.UseVector() // <-- Don't forget this!
));
Step 4: Querying with LINQ
Because our vectors live in the same database as our relational data, we can combine semantic vector search with traditional SQL filtering in a single LINQ query.
Dedicated vector databases also support metadata filtering. Qdrant and Pinecone, for example, both provide filtered vector search. The difference is not that filtering is impossible elsewhere. The difference is architectural: if your source of truth already lives in PostgreSQL, keeping vectors, metadata, deletes, updates, permissions, and document versions in sync across another datastore adds additional system complexity.
public async Task<List<Document>> SearchKnowledgeBaseAsync(int currentTenantId, string userQuestion)
{
// 1. Turn the user's question into a vector using your preferred AI library
// (e.g., Microsoft.Extensions.AI)
float[] embeddingArray = await _aiService.GenerateEmbeddingAsync(userQuestion);
var queryVector = new Vector(embeddingArray);
// 2. Combine vector search with relational filters
var relevantDocs = await _dbContext.Documents
// Relational filter: scope results to the current tenant
.Where(d => d.TenantId == currentTenantId)
// Vector Search: Order by semantic similarity using Cosine Distance
.OrderBy(d => d.Embedding.CosineDistance(queryVector))
.Take(5)
.ToListAsync();
return relevantDocs;
}
Combining Relational Filters and Vector Search
When you call ToListAsync(), EF Core translates the CosineDistance() method directly into pgvector’s native <=> operator.
PostgreSQL can combine relational filters and vector ordering in one query. For approximate HNSW indexes, filtered search still needs proper indexing and tuning, especially for selective tenant filters.
The Takeaway
You don’t always need a dedicated vector database to build useful RAG features.
If your application already uses PostgreSQL and your retrieval data is tightly coupled with relational business data, pgvector can be a very pragmatic starting point.
You keep embeddings, metadata, permissions, and source records close together. You can query them through EF Core. And you avoid introducing a second datastore until you actually need one.
Dedicated vector databases still have their place, especially at a larger scale or when vector search becomes a standalone platform concern. But for many .NET applications, PostgreSQL with pgvector is enough to start.
Runnable Sample
I also created a small runnable sample repository for this post.
Repository: GitHub Repo
The sample uses a deterministic embedding service so it can run locally without an OpenAI or Azure OpenAI API key.
That service is only there to make the demo reproducible. It is not meant to produce production-quality semantic embeddings. For real applications, replace it with embeddings from your actual embedding model, for example text-embedding-3-small.