Most teams hit a wall when they try to give Semantic Kernel long-term memory without coupling to a specific vector DB. This semantic kernel qdrant vector memory tutorial shows you how to plug Qdrant into SK’s memory stack using the official connectors and a minimal console setup. You’ll end up with a working pipeline that embeds text, persists it to Qdrant, and retrieves it by similarity.
Step 1: Start a Qdrant instance
Qdrant ships a single Docker image that is sufficient for local dev. Run it and expose both the gRPC and REST ports:
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
Verify it is up before writing code:
curl http://localhost:6333/healthz
# {"status":"ok"}
The REST API listens on 6333; the dashboard is at http://localhost:6333/dashboard. You do not need to pre-create collections—the SDK will do that.
Step 2: Add the NuGet packages
Create a .NET 8 console project and pull in the core SDK plus the Qdrant and OpenAI connectors:
dotnet new console -n SkQdrantDemo
cd SkQdrantDemo
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Connectors.Qdrant
dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI
Pin to a recent 1.x version if you need reproducibility. The vector-store abstraction used here stabilized in SK 1.0+; do not use the legacy IMemoryStore for new work.
Step 3: Define the record schema
Semantic Kernel maps POCOs to vector records via attributes. The key must be a string or numeric, the vector must be ReadOnlyMemory<float>, and the dimension must match your embedding model exactly.
using Microsoft.SemanticKernel.Data;
public class MemoryRecord
{
[VectorStoreRecordKey]
public string Id { get; set; } = string.Empty;
[VectorStoreRecordData]
public string Text { get; set; } = string.Empty;
[VectorStoreRecordVector(1536)] // text-embedding-3-small
public ReadOnlyMemory<float> Embedding { get; set; }
}
If you swap to all-MiniLM-L6-v2 (384 dims) or text-embedding-3-large (3072 dims), change the attribute and the model ID together. Mismatched dimensions fail at upsert with a gRPC error from Qdrant.
Step 4: Configure Semantic Kernel
Build the kernel with an embedding generator and register the Qdrant vector store. The example uses OpenAI’s embedding API, but any ITextEmbeddingGenerationService works.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Connectors.Qdrant;
var builder = Kernel.CreateBuilder();
builder.AddOpenAITextEmbeddingGeneration(
modelId: "text-embedding-3-small",
apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")!);
builder.Services.AddQdrantVectorStore("http://localhost:6333");
var kernel = builder.Build();
Choosing an embedding model
text-embedding-3-small is cheap and good enough for most RAG demos. For on-prem, use Microsoft.SemanticKernel.Connectors.HuggingFace or ONNX local inference; just implement the same interface.
Pointing at a gateway (optional)
If you don’t want to juggle multiple provider API keys, point the embedding client at an OpenAI-compatible gateway like n4n.ai, which exposes one endpoint for 240+ models and applies automatic fallback when a provider is rate-limited. The SK OpenAI connector accepts a custom endpoint:
builder.AddOpenAITextEmbeddingGeneration(
modelId: "text-embedding-3-small",
apiKey: "your-gateway-key",
endpoint: "https://api.n4n.ai/v1");
Step 5: Create the collection and upsert documents
Resolve the store and collection, ensure it exists, then embed and write records. Batch upserts are preferred for volume; here we show single writes for clarity.
var vectorStore = kernel.Services.GetRequiredService<IVectorStore>();
var collection = vectorStore.GetCollection<string, MemoryRecord>("sk_memory");
await collection.CreateCollectionIfNotExistsAsync();
var embedder = kernel.Services.GetRequiredService<ITextEmbeddingGenerationService>();
var docs = new[]
{
"Semantic Kernel simplifies orchestration of LLM calls.",
"Qdrant is a vector database written in Rust.",
"Vector search retrieves by cosine similarity, not keywords."
};
for (var i = 0; i < docs.Length; i++)
{
var vec = await embedder.GenerateEmbeddingAsync(docs[i]);
await collection.UpsertAsync(new MemoryRecord
{
Id = $"doc-{i}",
Text = docs[i],
Embedding = vec
});
}
Qdrant creates the collection with the distance metric defaulting to Cosine when driven by the SK connector. You can confirm via the dashboard: the sk_memory collection appears with 3 points.
Step 6: Run similarity search
Generate a query embedding and call VectorizedSearchAsync. The returned type is an IAsyncEnumerable of records with scores.
var queryVec = await embedder.GenerateEmbeddingAsync("How do I orchestrate LLMs?");
var search = await collection.VectorizedSearchAsync(queryVec, top: 2);
await foreach (var result in search.Results)
{
Console.WriteLine($"Score: {result.Score:F3} | {result.Record.Text}");
}
Expected output ranks the “Semantic Kernel simplifies orchestration” line first. Scores are cosine similarities in [0,1]; higher is closer.
Step 7: Verify the pipeline works
A working semantic kernel qdrant vector memory tutorial implementation passes three checks:
- Health:
curl localhost:6333/healthzreturns ok after the app ran. - Persistence: Restart the console app, re-run only the search block (skip upsert), and you still get results. Qdrant stored the points on disk.
- Dimension integrity: No
DimensionMismatcherrors in the Qdrant logs (docker logs <container>).
If search returns nothing, print search.Results.Count() after buffering with ToListAsync()—an empty collection usually means the embedding service returned zeros or the collection name differs.
Production notes
- Use
collection.UpsertAsync(batch)withList<MemoryRecord>to cut round-trips. - Add a
VectorStoreRecordDatafield for timestamps or tenant IDs and filter withFilterclauses inVectorizedSearchAsync. - Set Qdrant API keys and TLS before exposing the port beyond localhost.
- The SK Qdrant connector uses gRPC by default for upserts; the REST port is fine for health checks.
The pattern above is the minimum viable link between Semantic Kernel and Qdrant. From here, wrap the upsert in a KernelFunction or a background worker, and you have durable memory for any SK agent.