Embeddings and Semantic Search for ASP.NET Core Developers
How meaning becomes searchable—and why retrieval still needs engineering judgement
Context engineering asks us to give a model the smallest trustworthy collection of information needed for one task. That creates an immediate practical problem.
If a company has millions of document sections, database records and knowledge articles, how does an ASP.NET Core application find the five pieces relevant to the user's question?
It cannot send everything to the model. It needs retrieval.
Embeddings and vector search make it possible to retrieve content by related meaning, even when the user's wording differs from the source. Keyword search remains essential for exact terms. Hybrid search combines both. Metadata filtering keeps results inside the correct tenant, project, document version and permission scope.
This guide develops those ideas slowly, then joins them into a realistic BuildEstate Pro example. BuildEstate Pro is my public enterprise property-development case study. The search architecture described here is a proposed teaching extension rather than a claim that this feature already exists in the repository.
The central lesson is:
Embeddings do not answer a question. They help the application locate information with related meaning so that useful evidence can be supplied to the model.
1. Why literal words are sometimes not enough
Imagine a planning document contains this sentence:
Construction vehicles must not enter the development from the western road.The user asks:
Are there any restrictions on lorry access?The ideas are closely related, but the phrases differ:
| User wording | Document wording |
|---|---|
| lorry | construction vehicle |
| restrictions | must not |
| access | enter |
This does not make keyword search obsolete. Exact identifiers, legal references, error codes and names are often served better by lexical matching. Production retrieval normally combines techniques instead of declaring one universally superior.
2. What is an embedding?
An embedding is a numerical representation produced by an embedding model. For text, it represents aspects of the text's meaning as an ordered list of floating-point values.
Conceptually:
"Approval of the drainage strategy is required before construction"
↓ embedding model
[0.017, -0.248, 0.761, 0.034, ... many more values ...]
That list is a vector.
The individual positions do not normally have simple labels such as “drainage” or “planning.” Meaning is represented by the overall pattern across many dimensions.
Texts with related meanings tend to occupy nearby regions in the embedding space. The application can therefore compare the vector for a question with vectors stored for document chunks.
An embedding model does not normally write the answer. Its output is the vector. A generative model has a different role: it uses retrieved evidence to explain, summarise or reason within the application's constraints.
3. A vector is an ordinary data structure with an unusual meaning
C# developers already understand ordered numeric data.
ReadOnlyMemory<float> vector =
new float[] { 0.017f, -0.248f, 0.761f, 0.034f };
Real embeddings commonly contain hundreds or thousands of dimensions. Each vector represents a point in a high-dimensional mathematical space.
Humans can draw two dimensions and imagine three. A search engine can calculate across thousands without needing to visualise them.
Think of a meaning-organised library. Cricket books are near one another, football books form another neighbourhood and software engineering sits elsewhere. A book about AI analysis in cricket may sit between the cricket and AI regions. An embedding model produces the coordinates; vector search finds nearby coordinates.
The analogy is useful, but remember that the space is learned from data and mathematics—not a hand-labelled map of every concept.
4. Generate embeddings through a .NET abstraction
Microsoft.Extensions.AI provides IEmbeddingGenerator, a provider-neutral abstraction similar in spirit to IChatClient.
using Microsoft.Extensions.AI;
public sealed class TextEmbeddingService(
IEmbeddingGenerator<string, Embedding<float>> generator)
{
public Task<ReadOnlyMemory<float>> GenerateAsync(
string text,
CancellationToken cancellationToken)
=> generator.GenerateVectorAsync(text, cancellationToken: cancellationToken);
}
For batches, generate several vectors in one operation when supported.
GeneratedEmbeddings<Embedding<float>> embeddings =
await generator.GenerateAsync(
chunks.Select(chunk => chunk.Text),
cancellationToken: cancellationToken);
The concrete generator may use an approved Azure-hosted model, another provider or a local implementation. Keep the provider configuration in infrastructure and expose business-focused services to application code.
Embedding calls still need cancellation, timeouts, rate control, telemetry and privacy review. Numerical output does not remove the fact that source text was sent to a model service.
5. Documents are prepared before users search
In a typical RAG ingestion flow:
- locate an authorised source document;
- extract and clean its text;
- split it into meaningful chunks;
- enrich each chunk with metadata;
- generate an embedding for each chunk;
- store text, vector and metadata in a searchable index.
Planning document
↓ extract
Clean structured text
↓ chunk
Section 1 Section 2 Section 3 ...
↓ embed each
Vector 1 Vector 2 Vector 3 ...
↓ index
Text + vector + source + version + permissions
Why not embed the whole document once?
A planning report may discuss drainage, highways, noise, landscaping, ecology and working hours. One document-level vector blends these subjects. Chunk-level vectors let retrieval return the exact section relevant to the question.
Chunking is not a mechanical “every 500 characters” decision. A useful chunk should preserve enough surrounding meaning to stand on its own. The next complete ingestion guide will explore headings, boundaries, overlap, tables, OCR and versioning in detail.
6. Store the vector with human-readable content and provenance
A useful search record includes much more than an array of numbers.
public sealed record SearchChunk(
string ChunkId,
string Text,
ReadOnlyMemory<float> Vector,
string TenantId,
Guid ProjectId,
string DocumentId,
int DocumentVersion,
int? PageNumber,
string Section,
string DocumentType,
string SecurityClassification,
DateTimeOffset IndexedAt,
string EmbeddingModel,
int EmbeddingDimensions,
string ChunkingVersion);
The text is returned to the user or generative model. Metadata provides citations, deterministic filtering, freshness checks and deletion. Model and chunking versions make re-indexing manageable.
A vector store is a search-optimised knowledge layer, not automatically the source of truth. SQL Server can remain authoritative for project state and transactions while Azure AI Search contains selected representations for retrieval.
7. The query is embedded at runtime
When a user asks:
Can construction start before the drainage plan is approved?the application:
- creates an embedding for the question;
- searches for nearby stored vectors;
- applies metadata and security filters;
- returns the original chunk text and provenance;
- decides whether the matches are good enough;
- supplies approved evidence to the generative model.
Approval of the drainage strategy is required before construction begins.Different words, related meaning.
public sealed class PlanningEvidenceRetriever(
IEmbeddingGenerator<string, Embedding<float>> embeddings,
IPlanningVectorSearch search)
{
public async Task<IReadOnlyList<RetrievedChunk>> RetrieveAsync(
PlanningSearchRequest request,
CancellationToken cancellationToken)
{
var queryVector = await embeddings.GenerateVectorAsync(
request.Question,
cancellationToken: cancellationToken);
return await search.FindAsync(
queryVector,
new PlanningSearchFilter(
request.TenantId,
request.ProjectId,
CurrentVersionsOnly: true,
request.AccessGroups),
cancellationToken);
}
}
The metadata filter is part of the search contract, not an optional cleanup performed after confidential text has already been returned.
8. Use the same compatible embedding space
Stored chunk vectors and query vectors must be comparable. In practice, use the same embedding model and dimensional configuration for both.
Coordinates from two unrelated maps cannot be compared meaningfully. Likewise, vectors from incompatible models do not share a reliable geometry.
Store:
- embedding model and deployment identifier;
- dimensions;
- generation date;
- source content hash and version;
- preprocessing version;
- chunking strategy version.
public sealed record EmbeddingProfile(
string ModelId,
int Dimensions,
string PreprocessingVersion,
string ChunkingVersion);
Changing the model or dimensions usually means generating new document embeddings and rebuilding or migrating the index. Treat this as versioned data infrastructure, not a hidden configuration toggle.
A safe migration can build a new index alongside the old one, evaluate it, switch reads gradually and retain rollback until confidence is established.
9. Similarity measures relatedness, not truth
Vector search uses a similarity or distance measure. Cosine similarity is a common example: it considers how similarly two vectors point within the embedding space.
You do not need to implement the mathematics yourself to use it responsibly. You do need to understand what the score can and cannot claim.
If the user asks:
Has the drainage condition been approved?search may retrieve:
The drainage condition must be approved before construction.The passage is highly related. It does not prove that approval occurred.
The application may need a current SQL query or later approval notice.
Similarity score says: this content appears related.
It does not say: this statement is current, permitted or correct.
Vector relevance is one signal inside a retrieval and validation pipeline.
10. Exact search and approximate nearest-neighbour search
With 100 vectors, a search system can compare the query with every vector. This is exhaustive K-nearest-neighbour search: calculate all distances and return the closest k items.
With 200 million vectors, repeating a full comparison for every request is expensive. Approximate nearest-neighbour algorithms use an index to navigate toward promising regions without checking everything.
Imagine searching for a house:
United Kingdom
→ London
→ West London
→ town
→ street
→ matching property
The search avoids obviously distant areas.
Azure AI Search supports:
- exhaustive KNN, which checks the vector space and provides exact nearest neighbours;
- HNSW, a graph-based approximate nearest-neighbour algorithm designed for high-recall, low-latency search at scale.
Exhaustive search is useful for smaller datasets and for creating a ground-truth set against which to measure approximate recall.
Approximate does not mean careless. It means deliberately trading exhaustive comparison for scalable performance, then measuring whether retrieval quality meets the business need.
11. Top-K controls how many candidates return
K is simply the requested number of nearest results.
For “Why is project 152 delayed?”, retrieval might rank:
- unapproved drainage strategy;
- outstanding highways approval;
- incomplete environmental survey;
- landscaping condition;
- working-hours restriction;
- bicycle storage requirement.
k = 1, the final answer may mention drainage and miss two other material causes.
If k = 50, the model may receive the three useful chunks plus considerable noise. That adds cost and latency, and can distract generation.
There is no universal best K.
Evaluate by use case:
- simple policy lookup may need three chunks;
- risk analysis may need five or ten;
- broad research may retrieve a larger candidate set and rerank it to a smaller final set.
k = 50 candidates so the semantic ranker has sufficient input. That is a platform-specific candidate-stage recommendation, not a rule that all 50 chunks should be passed to the language model.
12. Nearest does not always mean relevant enough
A search engine can return the closest results even when every result is poor.
Ask a planning index:
Which medicine should I take for a headache?Something must still rank first unless the application rejects weak matches.
A similarity threshold or relevance rule can say: accept a result only when it is strong enough for this domain and query type.
var accepted = candidates
.Where(item => item.Score >= evaluatedThreshold)
.Take(finalEvidenceLimit)
.ToList();
if (accepted.Count == 0)
return RetrievalOutcome.InsufficientEvidence;
Do not copy a score threshold from a tutorial. Score behaviour varies with model, dimensions, metric, search product, chunking, domain and query style. Hybrid RRF scores also have a different scale from raw vector similarity scores.
A high threshold creates false negatives by discarding useful evidence. A low threshold creates false positives by accepting noise. Choose it with labelled evaluation queries.
13. Keyword, vector and semantic ranking are different layers
The phrase “semantic search” is sometimes used broadly for any meaning-aware retrieval. In Azure AI Search, it helps to distinguish the mechanisms precisely.
Keyword or full-text search
Uses textual terms and BM25 ranking. It is strong for:
- planning reference numbers;
- invoice and customer IDs;
- postcodes;
- legal clause numbers;
- product and error codes;
- exact names and specialist terms.
Vector search
Compares embeddings and finds semantically related content even when words differ.
Semantic ranker
Applies language-understanding models as a secondary ranking stage over an initial candidate set. It can promote results that better match the query's intent.
Vector search and semantic ranker are not synonyms. A system can use vector search without semantic ranking, keyword search with semantic ranking, or a hybrid candidate set followed by semantic ranking.
14. Hybrid search combines exactness and meaning
Consider:
What access restrictions apply to planning application PLN-2026-00452?Keyword search can strongly locate the exact reference. Vector search can find sections discussing vehicle movements, delivery routes, entrances and construction traffic.
Azure AI Search can run keyword and vector queries in parallel and merge their ranked lists using Reciprocal Rank Fusion (RRF). An optional semantic-ranker stage can then reorder the combined candidates.
Question
├── keyword/BM25 search ──┐
└── vector search ────────┤
↓
Reciprocal Rank Fusion
↓
optional semantic reranking
↓
filtered evidence candidates
Hybrid search is often a strong enterprise starting point because business data contains both natural language and exact identifiers.
Do not interpret a low-looking RRF score as a weak vector similarity score. It comes from a different ranking method and scale.
15. A representative Azure AI Search index
The exact SDK evolves, but the index design principles are stable.
{
"name": "planning-chunks-v1",
"fields": [
{ "name": "chunkId", "type": "Edm.String", "key": true, "filterable": true },
{ "name": "content", "type": "Edm.String", "searchable": true },
{ "name": "contentVector", "type": "Collection(Edm.Single)", "searchable": true, "dimensions": 1536, "vectorSearchProfile": "planning-hnsw" },
{ "name": "tenantId", "type": "Edm.String", "filterable": true },
{ "name": "projectId", "type": "Edm.String", "filterable": true },
{ "name": "documentId", "type": "Edm.String", "filterable": true },
{ "name": "documentVersion", "type": "Edm.Int32", "filterable": true },
{ "name": "isCurrent", "type": "Edm.Boolean", "filterable": true },
{ "name": "accessGroups", "type": "Collection(Edm.String)", "filterable": true },
{ "name": "pageNumber", "type": "Edm.Int32", "filterable": true },
{ "name": "section", "type": "Edm.String", "searchable": true }
]
}
The dimension value must match the selected embedding configuration. Security and version fields are filterable. Human-readable content remains searchable and retrievable; vector fields normally do not need to be returned to the user.
Index names and aliases can carry schema versions so migrations do not overwrite the live index blindly.
16. Apply deterministic filters with semantic retrieval
Suppose a user is viewing project 152 and asks about drainage restrictions. The globally closest chunk could belong to another tenant's project 829.
The intended search is:
Find meaning-related drainage content, but only for this tenant, project, current document versions and authorised access groups.
tenantId = authenticated tenant
AND projectId = authorised route project
AND isCurrent = true
AND accessGroups contains one of caller's groups
Permissions must never be decided by vector similarity.
Be careful with filter mode. Prefiltering narrows the vector search surface before nearest-neighbour selection. Post-filtering removes items after candidate selection and can leave fewer useful results. For security trimming, ensure the chosen query structure applies mandatory access filters consistently to every relevant retrieval branch.
Authorization should be testable without a model call.
17. Rerank candidates before giving evidence to the model
Retrieval often optimises for recall: collect enough possible evidence not to miss the answer. Generation benefits from precision: a small set of highly relevant chunks.
Retrieve 30–50 candidates
↓
Apply metadata and security rules
↓
Rerank against the actual question
↓
Deduplicate related chunks
↓
Give the best 5–8 evidence chunks to the model
Semantic ranking adds latency and may add cost, so evaluate the improvement. It is useful when the initial set contains plausible but marginally relevant results.
Keep source identifiers and scores through every stage. If the final answer is poor, you need to know whether the right chunk was never retrieved, retrieved but ranked too low, or supplied and ignored by the model.
18. Evaluate retrieval separately from answer generation
A RAG answer can fail before generation begins.
Create a labelled evaluation set:
public sealed record RetrievalTestCase(
string Question,
string TenantId,
Guid ProjectId,
IReadOnlySet<string> ExpectedRelevantChunkIds,
IReadOnlySet<string> ForbiddenChunkIds);
Measure:
- Recall@K: how many expected relevant chunks appeared in the top K?
- Precision@K: how many returned chunks were actually useful?
- Mean reciprocal rank: how early did the first relevant result appear?
- Latency: how long did retrieval and reranking take?
- Security correctness: did any forbidden chunk appear?
- Freshness correctness: were superseded versions excluded?
Then test generation separately:
Was the required evidence retrieved?
no → retrieval/index/chunking problem
yes → Did the model use it correctly?
no → prompt/model/context problem
yes → Did the application validate and present it correctly?
This is more actionable than calling every poor answer a hallucination.
19. Protect embeddings as derived business data
An embedding looks like an unreadable number array. It is still derived from the original content.
Before embedding business data, ask:
- Is the source authorised for this purpose?
- Does it contain unnecessary personal or confidential information?
- Is the provider and processing region approved?
- What are the retention terms?
- Which identities can query the index?
- How will deletion and access changes propagate?
- Are logs exposing source text or vectors?
Source document deleted
↓
Remove extracted text
↓
Remove chunks and vectors
↓
Invalidate caches
↓
Record deletion completion
Deleting a PDF while retaining all derived chunks and embeddings is not a complete deletion design.
20. Embeddings beyond RAG
Embeddings support many similarity problems.
Similar-case discovery
BuildEstate Pro could find earlier developments with related combinations of drainage, highways and environmental issues.
Recommendations
A developer reading an ASP.NET Core performance article could receive related material about caching, SQL optimisation and async I/O.
Duplicate detection
Near-identical policies or documents can be flagged before redundant indexing.
Clustering
Customer messages may group naturally into payments, complaints, login problems and cancellation requests.
Suggested classification
A chunk can be compared with example category vectors such as drainage, financial, environmental and highways risk.
Similarity can suggest. Deterministic rules and human judgement still decide where consequences matter.
21. Complete BuildEstate Pro flow
A current planning document for project 152 is uploaded.
The ingestion pipeline extracts and chunks it. One chunk says:
Construction vehicles must not enter the development from the western road.The embedding model converts the chunk into a vector. The index stores:
- original text;
- vector;
- tenant and project;
- document ID and version;
- page and section;
- access groups;
- current-version flag;
- embedding and chunking profile.
Can our delivery lorries use the west entrance?ASP.NET Core:
- authorises the project;
- embeds the question with the compatible model;
- runs keyword and vector retrieval;
- filters by tenant, project, version and access group;
- reranks and rejects weak results;
- retrieves the western-road passage;
- supplies it to the generative model with its citation;
- validates that the answer refers to supplied evidence.
No. The current planning condition states that construction vehicles must not enter from the western road. The delivery route should be changed or reviewed through the appropriate planning process.Each component did a different job:
- the embedding model represented meaning;
- keyword search preserved exact matching;
- vector search found related wording;
- metadata filters protected scope;
- semantic ranking improved candidate order;
- the generative model explained evidence;
- ASP.NET Core controlled authorization, validation and the workflow.
22. Practical implementation checklist
Embedding profile
- Record model, dimensions, preprocessing and chunking versions.
- Use compatible document and query embeddings.
- Plan re-embedding and index migration before changing the model.
Index design
- Store original text and provenance with every vector.
- Include filterable tenant, project, version and access metadata.
- Keep SQL Server or another operational system as the source of truth.
Retrieval
- Combine keyword and vector retrieval where the domain benefits.
- Evaluate HNSW recall against exhaustive search on representative data.
- Tune K, candidate size, thresholds and reranking with labelled queries.
- Do not confuse vector, RRF and semantic-ranker score scales.
Security
- Authorise before search.
- Apply mandatory filters consistently.
- Preserve source classification and deletion lifecycle.
- Treat retrieved text as untrusted data when it enters the model context.
Operations
- Observe model/profile version, filters, scores, latency and result IDs.
- Diagnose retrieval separately from generation.
- Monitor index freshness and failed ingestion.
- Avoid logging confidential chunks and raw vectors by default.
23. What an ASP.NET Core developer should remember
Embeddings are not mysterious intelligence trapped in decimals. They are search representations generated for a purpose.
The difficult production work lies in the boundaries around them:
- deciding what should be embedded;
- preserving document structure;
- choosing chunks and metadata;
- keeping models and dimensions compatible;
- combining retrieval approaches;
- enforcing tenant and permission filters;
- evaluating recall and precision;
- versioning, refreshing and deleting derived data;
- proving that retrieved evidence is relevant enough to use.
The final mental model
Offline preparation
Document → extract → chunk → embed → index text + vector + metadata
Runtime retrieval
Question → embed → keyword + vector search → filter → rerank
→ accept useful evidence → give evidence to generative model
Application control
ASP.NET Core authorises, versions, filters, validates, observes and deletes
Vector search finds the nearest available content, but your application must decide whether that content is relevant enough, permitted, current and safe to use.
Continue learning
- Context engineering for ASP.NET Core
- Building a reliable LLM application
- From business data to production RAG
- How RAG and vector databases work together
- AI Engineering learning journey
- BuildEstate Pro repository
