This repository accompanies the article:
This project implements the article sample as a runnable .NET console app:
- PostgreSQL stores documents, tenant metadata, and vector embeddings together.
- EF Core enables the
vectorextension withmodelBuilder.HasPostgresExtension("vector"). - The
Document.Embeddingcolumn is mapped asvector(1536). - The embedding column has an HNSW index with
vector_cosine_ops. - Retrieval combines
TenantIdfiltering and cosine-distance ordering in one LINQ query.
From the repository root:
docker compose up -dThe compose file uses the pgvector/pgvector:pg16 image. The extension is installed in the image. The app enables it in the target database through the EF Core model configuration.
This works with the default local postgres user from the compose file. In managed or locked-down PostgreSQL environments, the database user may need permission to create extensions.
From the repository root:
dotnet run -- "How do I filter vector search by tenant?"On first run, the app creates the schema, enables the vector extension, creates the HNSW index, seeds sample documents, and runs a tenant-scoped vector search.
Expected behavior: the document from tenant 7 should not appear when searching as tenant 42.
If you want to inspect the generated SQL, add --log-sql:
dotnet run -- --log-sql "How do I filter vector search by tenant?"The default connection string is:
Host=localhost;Port=5432;Database=rag_efcore_pgvector;Username=postgres;Password=postgres
Override it with an environment variable from the repository root:
POSTGRES_CONNECTION_STRING="Host=localhost;Port=5432;Database=rag_efcore_pgvector;Username=postgres;Password=postgres" \
dotnet runOr pass it as an argument from the repository root:
dotnet run -- \
--connection="Host=localhost;Port=5432;Database=rag_efcore_pgvector;Username=postgres;Password=postgres" \
"How should I tune HNSW?"Stop PostgreSQL when you are done:
docker compose downTo remove the database volume and start from a clean database:
docker compose down -vThis is especially useful after schema or seed-data changes because the compose file uses a named volume and EnsureCreatedAsync() will not update an existing schema after model changes.
The sample uses EnsureCreatedAsync() to keep local setup small:
await dbContext.Database.EnsureCreatedAsync();This is demo-only schema setup. For production applications, prefer EF Core migrations. EnsureCreatedAsync() is best suited to clean local databases, tests, prototypes, or transient data stores, and it does not work well together with migrations.
This sample uses DeterministicEmbeddingService to keep the demo local and reproducible.
It is not meant to produce production-quality semantic embeddings.
In a real RAG application, replace DeterministicEmbeddingService with an embedding provider such as OpenAI, Azure OpenAI, or another model exposed through your preferred .NET AI library.
Keep Document.EmbeddingDimensions, the deterministic embedding stub output, and vector(1536) aligned with the embedding model output. If you use larger embeddings and need indexed search, reduce the embedding dimensions or evaluate halfvec/HalfVector.
The sample uses transaction-scoped query tuning:
await using var transaction = await dbContext.Database.BeginTransactionAsync();
await dbContext.Database.ExecuteSqlRawAsync("SET LOCAL hnsw.ef_search = 80");
var results = await searchService.SearchKnowledgeBaseAsync(
currentTenantId,
question,
matchCount: 5);
await transaction.CommitAsync();pgvector documents SET LOCAL inside a transaction for query-scoped hnsw.ef_search. The setting stays scoped to this transaction, which is a safer pattern with EF Core and Npgsql connection pooling than setting a session value globally. Higher values usually improve recall, but increase query cost.
Models/Document.cs: entity with relational metadata andVectorembedding.Data/AppDbContext.cs: pgvector extension, tenant index, and HNSW index configuration.Services/KnowledgeBaseSearchService.cs: tenant-filtered cosine vector search in LINQ.Program.cs: schema creation, seed data,hnsw.ef_searchtuning, and sample query.