Memory with SQL (pgvector) and Qdrant β Simple SK patterns
Not every app needs Cosmos DB. If you already run Postgres or prefer a light, dedicated vector DB, Postgres with pgvector and Qdrant are both excellent choices for app memory. Below are minimal patterns using Semantic Kernel to store and recall user memory.
When I pick which store
- Qdrant
- Simple to run (Docker), highβquality HNSW vector search, great defaults.
- Perfect for feature teams that want a focused vector DB without managing a full RDBMS.
- Postgres + pgvector
- Fits where Postgres is already the backbone; benefits from SQL joins, migrations, backups.
- Great for βmemory + metadataβ scenarios and unified infra.
Tip: start with what your team can operate confidently. For small scopes, both perform well.
SK quickstart: Qdrant user memory
Install packages: Microsoft.SemanticKernel, Microsoft.SemanticKernel.Connectors.Qdrant, and an embeddings provider (OpenAI/Azure OpenAI).
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Connectors.Qdrant;
string openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
int vectorSize = 1536; // must match your embedding model
// 1) Build a standalone memory service
var memory = new MemoryBuilder()
.WithOpenAITextEmbeddingGeneration(
modelId: "text-embedding-3-small",
apiKey: openAiKey)
.WithQdrantMemoryStore(
endpoint: "http://localhost:6333",
vectorSize: vectorSize,
collectionNamePrefix: "sk_") // optional
.Build();
// 2) Save some user memory
await memory.SaveInformationAsync(
collection: "user-profile",
text: "Prefers concise answers",
id: "user:42:pref-1",
description: "style");
// 3) Recall by semantic similarity
await foreach (var m in memory.SearchAsync(
collection: "user-profile",
query: "concise replies",
limit: 3,
minRelevanceScore: 0.7))
{
Console.WriteLine($"{m.Metadata.Id} | {m.Metadata.Text} | score {m.Relevance}");
}
Whatβs happening:
- Embeddings are created automatically for
SaveInformationAsync. - Qdrant collections are created on demand (configurable), with HNSW indexing.
SearchAsyncretrieves topβK by similarity with a relevance threshold.
SK quickstart: Postgres (pgvector)
Install packages: Microsoft.SemanticKernel, Microsoft.SemanticKernel.Connectors.Postgres, and your embeddings provider. Ensure the pgvector extension is enabled on your database.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Connectors.Postgres;
string openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
string conn = "Host=localhost;Username=pg;Password=pg;Database=sk;";
int vectorSize = 1536;
// 1) Create a Postgres memory store (requires pgvector extension)
var store = await PostgresMemoryStore.ConnectAsync(
connectionString: conn,
vectorSize: vectorSize,
schema: "public");
// 2) Build memory with Postgres store
var memory = new MemoryBuilder()
.WithOpenAITextEmbeddingGeneration(
modelId: "text-embedding-3-small",
apiKey: openAiKey)
.WithMemoryStore(store)
.Build();
// 3) Save & search user memory
await memory.SaveInformationAsync(
collection: "user-profile",
text: "Enjoys biking and TypeScript",
id: "user:42:pref-2");
await foreach (var m in memory.SearchAsync(
collection: "user-profile",
query: "What hobbies?",
limit: 3,
minRelevanceScore: 0.6))
{
Console.WriteLine($"{m.Metadata.Id} | {m.Metadata.Text} | score {m.Relevance}");
}
Notes:
ConnectAsyncinitializes tables if missing; vector size must match your embedding model.- You can share the same store across collections (user/team/company memories).
- Use SQL for additional filtering (e.g., userId/teamId) before similarity search when feasible.
Kernelβintegrated pattern (optional)
If you prefer everything via a single Kernel, register embeddings and the memory store with the DI container, then use kernel.Memory:
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Connectors.Qdrant;
var builder = Kernel.CreateBuilder();
// embeddings (OpenAI/Azure OpenAI β pick one)
builder.AddOpenAITextEmbeddingGeneration(
modelId: "text-embedding-3-small",
apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")!);
// register the memory store (Qdrant example)
var qdrant = new QdrantMemoryStore(
endpoint: "http://localhost:6333",
vectorSize: 1536);
builder.Services.AddSingleton<IMemoryStore>(qdrant);
var kernel = builder.Build();
await kernel.Memory.SaveInformationAsync(
collection: "user-profile",
text: "Works in M365 ecosystem",
id: "user:42:role-1");
await foreach (var m in kernel.Memory.SearchAsync(
collection: "user-profile",
query: "Microsoft 365",
limit: 2,
minRelevanceScore: 0.5))
{
Console.WriteLine($"{m.Metadata.Id} | {m.Metadata.Text} | score {m.Relevance}");
}
Minimal flow
Why this is easy for user memory
- You can start with a single collection per scope (e.g.,
user-profile), a handful of strings, and builtβin embeddings. - Saving is one line; searching is a few more. The operational model is small.
- You can add ACLs and scope partitioning later (store metadata like
userIdin the record and filter before similarity).
Wrapβup
Cosmos DB, Qdrant, and Postgres + pgvector are all viable for memory. I lean Qdrant for quick starts and Postgres when I want to live inside SQL. With Semantic Kernel, the integration is straightforward β pick your store, wire embeddings, and you have user memory in minutes.