Subscribe Now

Edit Template

Subscribe Now

Edit Template

Building Context-Aware Search in Python with LLM Embeddings + Metadata


In this article, you will learn how to build a context-aware semantic search engine in Python that combines embedding-based similarity with structured metadata filtering.

Topics we will cover include:

  • How sentence embeddings and cosine similarity work together to find semantically relevant documents.
  • How to build a metadata-aware search index that filters by team, status, priority, and date before scoring candidates.
  • How to persist the index to disk so embeddings are computed only once and reloaded efficiently on subsequent runs.
Building Context-Aware Search in Python with LLM Embeddings + Metadata

Building Context-Aware Search in Python with LLM Embeddings + Metadata

Introduction

Keyword search breaks the moment a user types something a document doesn’t literally say. A support engineer searching for “login keeps failing” won’t find a ticket titled “OAuth2 token refresh race condition”, even though that’s exactly what they need. This is the core problem that context-aware semantic search aims to solve.

Semantic search solves this by converting text into dense vector representations called embeddings, where meaning determines proximity rather than exact word overlap. Layer structured metadata filters on top — by date, status, team, priority — and you get a system that understands what someone is asking while respecting contextual constraints at the same time.

This article walks through building that system end-to-end: embeddings from a local pretrained model, a metadata-aware index, cosine similarity ranking, and an index that persists across restarts without requiring re-encoding.

You can get the code on GitHub.

What You Will Build

A simple context-aware search engine over a corpus of engineering support tickets. By the end you will have:

  • 384-dimensional embeddings generated locally from a pretrained model, no API key required
  • A search index that filters by team, status, priority, and date before scoring
  • Cosine similarity ranking over the filtered candidate pool
  • A persisted index that reloads without re-encoding

Prerequisites: Python 3.8+, basic familiarity with NumPy and working with lists of dictionaries.

Install dependencies:

Understanding How Semantic Search Works

A sentence embedding model takes a string and returns a fixed-length vector of floating-point numbers. The model is trained so that sentences with similar meanings produce vectors pointing in similar directions in high-dimensional space.

Cosine similarity measures the angle between two vectors:
\[
\text{cosine similarity}(A, B) =
\frac{A \cdot B}{\|A\| \, \|B\|}
\]

When vectors are unit-normalized — meaning their length equals 1.0 — this simplifies to the dot product: A · B. Scores range from -1 (opposite) to 1 (identical). In practice, unrelated documents score around 0.1–0.25, and strong matches score above 0.6.

So why does metadata filtering matter? Embedding models encode semantic content. They do not encode who wrote a document, what team owns it, or when it was created. These attributes live outside the text and must be handled separately. Combining both signals — semantic score and metadata constraints — is what makes search useful in real systems.

Setting Up the Dataset

We’ll work with 20 engineering support tickets across three teams — infrastructure, backend, and frontend — with four priority levels, two statuses, and a two-month date window.

Each ticket is a plain dictionary. The text field is what gets embedded; everything else is metadata for filtering.

To keep things concise, a truncated list is shown here instead of the full code block. The complete set of tickets is available in this GitHub gist.

A quick check on the shape of the corpus before moving on:

Output:

Running the snippet confirms the distribution: 20 tickets total, 14 open and 6 resolved, spread across the three teams.

Step 1: Generating Embeddings

all-MiniLM-L6-v2 maps any sentence to a 384-dimensional vector. It runs entirely on CPU, downloads once from Hugging Face (~22 MB), is cached locally after that, and requires no API key.

We pass normalize_embeddings=True so each output vector comes out with L2 norm exactly 1.0. Once vectors sit on the unit hypersphere, cosine similarity between any two of them is just their dot product, so no division is needed at query time. That means scoring the entire candidate pool reduces to a single matrix multiplication.

Output:

Sentence Embeddings for 20 Tickets

Sentence Embeddings for 20 Tickets

We get back a (20, 384) float32 matrix — one row per ticket. The norm of 1.0 confirms the normalization worked.

Step 2: Building the Index

The index stores the embedding matrix alongside the associated metadata and exposes a search method that accepts optional keyword arguments for every metadata field.

The key design decision here is filtering before scoring, not after. Post-hoc filtering wastes dot-product compute on documents you’d discard anyway. Filtering first also ensures min_score can drop irrelevant results instead of returning noisy low-confidence matches.

Step 3: Running Queries

We’ll run three queries to show different aspects of the system: semantic search alone, the same query with metadata filters, and a cross-team query scoped by priority.

First, a small helper that formats results consistently across all three examples.

Query 1: Searching Without Filters

To establish a baseline, we search without any metadata constraints, letting the embedding model rank the full corpus on semantic similarity alone.

Running this against the full 20-ticket corpus returns the following four backend tickets:

Query 2: Filtering by Status and Date

The query text is identical to the previous one. What changes is the candidate pool: this time we restrict to open tickets created before November 10th, 2025, simulating a workflow where a team wants only unresolved issues within a certain window.

Output:

Query 3: Searching Across Teams with a Priority Filter

Resource exhaustion appears in both infrastructure and backend tickets; they share semantic territory regardless of team ownership. This query tests whether the model groups them correctly across that boundary.

This outputs:

Step 4: Persisting the Index

Re-encoding the corpus on every startup defeats the purpose of building an index. The right pattern is to encode once, save the embedding matrix and metadata to disk, and reload them on subsequent runs.

The embedding matrix saves as a binary .npy file. Metadata saves as JSON, but Python’s date objects must be converted to ISO strings first. When starting a new session, the loading process works in two stages:

Model loading (from cache): The SentenceTransformer model first checks your local cache (e.g. .cache/huggingface/hub/). If the model is already available there, it loads immediately. Otherwise, it downloads the model once from Hugging Face and stores it locally to avoid repeated downloads in the future.

Index reloading (from saved data): The saved ticket embeddings (ticket_embeddings.npy) and metadata (ticket_metadata.json) are loaded from disk. This allows the ContextAwareIndex to be rebuilt instantly without recomputing embeddings, saving both time and compute.

The encoding step runs once. Every subsequent startup is two file reads and one model load from cache.

Summary

Context-aware semantic search combines an embedding model to convert text into vectors, normalization to align cosine similarity with dot products, a metadata mask to restrict candidates before scoring, and a ranking step that orders results by similarity.

Here’s what you can do next:

  • Add new documents: Encode with model.encode, stack with np.vstack, append metadata — no re-indexing needed.
  • Multi-value metadata filters: Store teams as a list of strings and check doc["team"] against the list.
  • Scale beyond 100k documents: Replace brute-force scoring with an approximate nearest neighbor index like FAISS and keep the metadata pre-filter unchanged.
  • Hybrid scoring: Combine semantic and keyword signals with a weighted mix.

Happy building!

crossroad.joykonark.com

Writer & Blogger

Considered an invitation do introduced sufficient understood instrument it. Of decisively friendship in as collecting at. No affixed be husband ye females brother garrets proceed. Least child who seven happy yet balls young. Discovery sweetness principle discourse shameless bed one excellent. Sentiments of surrounded friendship dispatched connection is he.

Leave a Reply

Your email address will not be published. Required fields are marked *

About Me

Kapil Kumar

Founder & Editor

As a passionate explorer of the intersection between technology, art, and the natural world, I’ve embarked on a journey to unravel the fascinating connections that weave our world together. In my digital haven, you’ll find a blend of insights into cutting-edge technology, the mesmerizing realms of artificial intelligence, the expressive beauty of art.

Popular Articles

  • All Posts
  • AIArt
  • Blog
  • EcoStyle
  • Nature Bytes
  • Technology
  • Travel
  • VogueTech
  • WildTech
Edit Template
As a passionate explorer of the intersection between technology, art, and the natural world, I’ve embarked on a journey to unravel the fascinating connections.
You have been successfully Subscribed! Ops! Something went wrong, please try again.

Quick Links

Home

Features

Terms & Conditions

Privacy Policy

Contact

Recent Posts

  • All Posts
  • AIArt
  • Blog
  • EcoStyle
  • Nature Bytes
  • Technology
  • Travel
  • VogueTech
  • WildTech

Contact Us

© 2024 Created by Shadowbiz

As a passionate explorer of the intersection between technology, art, and the natural world, I’ve embarked on a journey to unravel the fascinating connections.
You have been successfully Subscribed! Ops! Something went wrong, please try again.

Quick Links

Home

Features

Terms & Conditions

Privacy Policy

Contact

Recent Posts

  • All Posts
  • AIArt
  • Blog
  • EcoStyle
  • Nature Bytes
  • Technology
  • Travel
  • VogueTech
  • WildTech

Contact Us

© 2024 Created by Shadowbiz

Fill Your Contact Details

Fill out this form, and we’ll reach out to you through WhatsApp for further communication.

Popup Form