Vector stores
A vector store holds documents alongside their embedding vectors and supports similarity search. LogicGrid ships three implementations and an interface so you can plug in your own.
IVectorStore contract
public interface IVectorStore
{
Task UpsertAsync(VectorDocument document, CancellationToken ct = default);
Task UpsertBatchAsync(IList<VectorDocument> documents, CancellationToken ct = default);
Task<IList<VectorSearchResult>> SearchAsync(
float[] queryVector,
int topK = 5,
float minimumScore = 0.0f,
CancellationToken ct = default);
Task DeleteAsync(string id, CancellationToken ct = default);
Task ClearAsync(CancellationToken ct = default);
Task<int> CountAsync(CancellationToken ct = default);
}
VectorDocument contains the following fields { Id, Text, Vector, Metadata }.
Id— your unique key for the document. Used for upsert and delete.Text— the original text the vector was generated from. Returned with every search hit so you can feed it into a prompt.Vector— the embedding (afloat[]). Length must match the store's configured dimensions.Metadata— an optionalIDictionary<string, string>of arbitrary key/value tags you attach to the document at upsert time. It's not part of the similarity calculation; it's the extra context you'll want at retrieval time. Typical uses:- provenance —
source,url,file,page,chunk_index - scope / filtering —
tenant,user_id,language,version - freshness —
created_at,ingested_at - classification —
doc_type,category,tagsEmpty{}if you passnullat construction.
- provenance —
VectorSearchResult returns { Document, Score } where Document is the
same VectorDocument you originally upserted (Id, Text, Vector, Metadata).
Score is the cosine similarity in the range [-1.0, 1.0] (higher = more similar).
InMemoryVectorStore
Fast, ephemeral, perfect for tests and small corpuses. Loses everything on process exit.
using LogicGrid.Memory.VectorStores;
var store = new InMemoryVectorStore();
await store.UpsertAsync(new VectorDocument(
id: "doc-1",
text: "The sky is blue because of Rayleigh scattering.",
vector: await embedder.EmbedAsync("…")));
var hits = await store.SearchAsync(queryVector, topK: 5);
foreach (var hit in hits)
Console.WriteLine($"{hit.Score:F3} {hit.Document.Text}");
A solid default whenever the corpus fits in process memory and you don't need persistence — unit tests, prototypes, single-run scripts, or short-lived semantic search over an admin's recent messages.
QdrantVectorStore
Production-grade, persistent, distributed. Connects to a Qdrant instance over its HTTP API. The collection is created on first upsert if it doesn't already exist.
public QdrantVectorStore(
string collectionName,
int dimensions,
string baseUrl = "http://localhost:6333",
string? apiKey = null)
var store = new QdrantVectorStore(
collectionName: "my-docs",
dimensions: 768, // must match embedder
baseUrl: "http://localhost:6333");
await store.UpsertAsync(new VectorDocument(
id: "doc-1",
text: "…",
vector: await embedder.EmbedAsync("…")));
Run Qdrant in Docker for local dev:
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
HybridVectorStore
Wraps any IVectorStore and adds an in-memory BM25 keyword index.
Hybrid searches combine dense (vector) and sparse (BM25) results
through Reciprocal Rank Fusion. Drop-in replacement for IVectorStore
plus a HybridSearchAsync method for the fused mode.
var dense = new InMemoryVectorStore();
var hybrid = new HybridVectorStore(dense, new HybridSearchOptions
{
RrfK = 60,
Bm25K1 = 1.5,
Bm25B = 0.75,
});
await hybrid.UpsertAsync(/* … */);
IList<HybridSearchResult> hits = await hybrid.HybridSearchAsync(
query: "rerank fusion",
queryVector: await embedder.EmbedAsync("rerank fusion"),
topK: 5);
See Hybrid search for the full documentation.
Choosing the right store
| Factor | InMemory | Qdrant | Hybrid (wraps either) |
|---|---|---|---|
| Persistence | No | Yes | Inherits from inner store |
| Scale | <100k docs | Millions | Same as inner |
| Setup | None | Docker / managed service | Same as inner |
| Latency | <1 ms | 5–30 ms | Inner + tiny BM25 overhead |
| Filtering by metadata | Yes | Yes (richer) | Inner |
| Search modes | Dense | Dense | Dense, BM25, hybrid (RRF) |
| Best for | semantic search over an admin's recent messages, Tests, prototypes | Production, large corpora | When exact-keyword matches matter |
Building your own
Implement IVectorStore. The contract is small — six methods. Any
backend that supports K-NN search over dense vectors works:
- Pinecone — wrap their REST API.
- Weaviate — they expose a similar shape.
- Postgres + pgvector — straightforward with Npgsql and the
<->operator. - Elasticsearch with
dense_vector— for teams already on Elastic.
Where next
- Hybrid search — how RRF combines dense and sparse results.
- RAG pipeline — full ingest → search → ask flow.
- Semantic memory — durable, role-scoped memory
built on top of
IVectorStore.