Traditional search is “broken” for modern applications.
You type “comfortable running shoes for marathon training” into a search engine, and it looks for exact matches of those words. Miss one keyword? You miss relevant results. Use synonyms? The system doesn’t understand them.
Fast but dumb.
Vector search solves this by representing data as high-dimensional vectors (embeddings) and finding semantically similar results, even when the exact words don’t match.
This is how modern AI applications, from ChatGPT’s retrieval systems to Netflix recommendations, actually work behind the scenes.
In this article, we’ll explore how vector search works.
How Vector Search Works
Vector search operates on a fundamentally different principle than traditional search.
Instead of matching keywords using = or “like”, it converts your data into numerical representations (vectors) and calculates mathematical distances between them. Documents that are semantically similar end up close together in vector space, even if they use completely different words.
1 – Converting Data to Vectors
You start with text (or images, audio, etc.) and transform it into a vector, an array of floating-point numbers. This is called an embedding.
For example, the sentence “The dog ran quickly” might become:
[0.23, -0.45, 0.67, 0.12, ..., -0.33] // 768, 1024 or 1536 dimensions typically
The key insight: Semantically similar text produces similar vectors. “The dog ran fast” would generate a vector very close to the first dog one, even though the words are different.
Dimensions are the number of numeric values in a vector, each representing a semantic feature of the data. More dimensions capture finer nuances but require more storage and computation.
2 – Saving and searching the data
These vectors are stored in a specialized data structure using vector databases optimized for similarity search. Unlike a traditional database index that looks for exact matches, vector indexes organize data spatially to find nearest neighbors quickly.
And here we need Approximate Nearest Neighbor (ANN) algorithms like HNSW or IVF to have better performance and a more efficient result.
Why Vector Search Is Critical for AI and RAG?
In RAG, we use AI models to convert the input to vectors and then do a query in a vector database to retrieve the most semantically similar documents. These results provide the context needed for the model to produce precise, context-aware responses, bridging the gap between raw data and intelligent generation.
Without vector search, RAG systems would struggle to find the right context efficiently, limiting the effectiveness of AI in applications like chatbots, document summarization, and recommendation engines.
Implementing Vector Insert in .NET
We’ll build a vector search API using:
- PostgreSQL + pgvector
- Ollama (local embeddings)
- Entity Framework Core
- ASP.NET Core Minimal APIs and Semantic Kernel
Prerequisites:
ollama pull mxbai-embed-large
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Pgvector.EntityFrameworkCore
dotnet add package OllamaSharp
dotnet add package Microsoft.SemanticKernel.Connectors.Ollama --prerelease
dotnet add package Microsoft.SemanticKernel.Connectors.Pgvector --prerelease
Note that at the current time, some of these packages are in their alpha version or have not yet been made to release.
Now that we have all the items installed, let’s move on to configuring the context:
public class TextEmbedding
{
public int Id { get; set; }
public required string Text { get; set; }
public Vector? Embedding { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
And as for the context, it goes like this:
public class VectorDbContext : DbContext
{
public VectorDbContext(DbContextOptions<VectorDbContext> options)
: base(options) { }
public DbSet<TextEmbedding> TextEmbeddings { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Enable pgvector extension
modelBuilder.HasPostgresExtension("vector");
modelBuilder.Entity<TextEmbedding>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Text).IsRequired();
// CRITICAL: Dimension must match your model
// mxbai-embed-large = 1024 dimensions
entity.Property(e => e.Embedding).HasColumnType("vector(1024)");
entity.Property(e => e.CreatedAt).HasDefaultValueSql("NOW()");
});
}
}
Important: vector(1024) must match your embedding model dimensions.
Here are the sizes of some popular models:
| Model | Dimensions |
|---|---|
OpenAI text-embedding-3-small | 1,536 |
OpenAI text-embedding-3-large | 3,072 |
Nomic nomic-embed-text | 768 |
Mxbai mxbai-embed-large | 1,024 |
Sentence Transformers all-MiniLM-L6-v2 | 384 |
Sentence Transformers all-mpnet-base-v2 | 768 |
Cohere embed-english-v3.0 | 1,024 |
Google Universal Sentence Encoder | 512 |
Program.cs Configuration:
// PostgreSQL with pgvector support
builder.Services.AddDbContext<VectorDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("DefaultConnection"),
o => o.UseVector()));
// Ollama client for embeddings
builder.Services.AddTransient<OllamaApiClient>(c =>
new OllamaApiClient(
"http://localhost:11434",
"mxbai-embed-large"
));
Now, we need to create an endpoint that will use the client and context to generate the embedding and save it to the database:
app.MapPost("/api/embeddings", async (
string text,
OllamaApiClient client,
VectorDbContext dbContext) =>
{
// Generate embedding from Ollama
var service = client.AsTextEmbeddingGenerationService();
var embeddings = await service.GenerateEmbeddingAsync(text);
// Store in PostgreSQL
var textEmbedding = new TextEmbedding
{
Text = text,
Embedding = new Vector(embeddings)
};
await dbContext.TextEmbeddings.AddAsync(textEmbedding);
await dbContext.SaveChangesAsync();
return Results.Ok(new
{
message = $"Successfully created the embedding for '{text}'",
data = textEmbedding
});
});
What’s happening here:
- Ollama generates the embedding – Converts your text into a 1024-dimensional vector
- Vector wrapper – new Vector(embeddings) converts the float array to pgvector’s Vector type
- EF Core handles the rest – Serializes the Vector to PostgreSQL’s vector column
Then create the semantic search endpoint, and let’s see how it work:
app.MapPost("/api/search", async (
SearchRequest request,
OllamaApiClient client,
VectorDbContext dbContext) =>
{
// Convert query to embedding
var service = client.AsTextEmbeddingGenerationService();
var queryEmbedding = await service.GenerateEmbeddingAsync(request.Query);
var queryVector = new Vector(queryEmbedding);
// pgvector similarity search
var results = await dbContext.TextEmbeddings
.AsNoTracking()
.Select(e => new
{
e.Id,
e.Text,
Distance = e.Embedding!.CosineDistance(queryVector)
})
.OrderBy(x => x.Distance)
.Take(request.Limit)
.ToListAsync();
return Results.Ok(results);
});
Key method: CosineDistance() – finds semantically similar vectors.
Testing the Complete Flow
1. Insert test data:
curl -X POST http://localhost:5000/api/embeddings \
-d '{"text": "Implementing retry logic with Polly in C#"}'
curl -X POST http://localhost:5000/api/embeddings \
-d '{"text": "Error handling best practices in ASP.NET Core"}'
curl -X POST http://localhost:5000/api/embeddings \
-d '{"text": "Using circuit breaker pattern for resilience"}'
2. Search with different words:
curl -X POST http://localhost:5000/api/search \
-H "Content-Type: application/json" \
-d '{"query": "handling failures in dotnet", "limit": 3}'
The response should be a json like this returning the distance:
[
{
"id": 18,
"text": "Error handling best practices in ASP.NET Core",
"distance": 0.20312039880039134
},
{
"id": 17,
"text": "Implementing retry logic with Polly in C#",
"distance": 0.3103744654731445
},
{
"id": 19,
"text": "Using circuit breaker pattern for resilience",
"distance": 0.4387163404475538
}
]
Notice: Query used “handling failures in dotnet” but found “error handling” and “retry logic”, that’s semantic search.
In production you can create indexes, add cache, limit the distance, combine vectors with LLMs creating RAGs and much more.
Conclusion
Vector search isn’t magic. It’s just math.
Text becomes numbers. Numbers that are close = similar meaning. PostgreSQL with pgvector stores those numbers and finds the closest matches fast.
Every modern AI application needs vector search. Chatbots need it to find relevant context. Recommendation engines need it to find similar items. RAG systems need it to retrieve knowledge.
You now have everything to build these systems. No external dependencies. No API costs. No separate vector database to manage.
Start small. Then scale. Add indexes. Optimize. Build RAG. The foundation is simple.
For now, stop reading and start building.
Thank you for reading.
See you next time!



