Skip to content

Medical AI · Retrieval-augmented generation (RAG) · Lesson 3 of 7

Embeddings and vector stores

Reviewed by Dr. Ram Kumar G, MBBS, MD (Internal Medicine), MPH (USA), on · Last updated

  • Intermediate
  • Technical focus
  • About 35 minutes
  • Includes exercise

Before this lesson: Chunking clinical guidelines

In short

An embedding model turns text into a list of numbers so that passages with similar meaning have similar vectors; a vector store keeps these vectors and finds the closest ones to a question. Embeddings find matches that keyword search misses, such as 'weak kidneys' for a section on kidney function, but a general-purpose model can still miss clinical abbreviations such as 'hypo'. Abbreviation expansion, hybrid keyword-plus-embedding search and domain-specific models help, and every choice should be checked with evaluation.

You will learn to

  • Explain what text embeddings and cosine similarity are
  • Compare keyword (TF-IDF) and embedding retrieval on the same questions
  • Improve retrieval of clinical abbreviations with query expansion
  • Build a minimal vector store with metadata filtering

From words to vectors

Keyword methods such as TF-IDF and BM25 score passages by shared words, weighted by how rare each word is. They are fast, transparent and good with exact terms like drug names, but they cannot connect 'weak kidneys' to 'eGFR'.

Embedding models are neural networks trained so that texts with similar meaning get nearby vectors. Similarity is usually measured with the cosine of the angle between two vectors: 1 means the same direction, around 0 means unrelated. The model used below, all-MiniLM-L6-v2, is a small open model that turns each text into 384 numbers. It downloads automatically the first time you run it (pip install sentence-transformers).

Keyword and embedding retrieval side by side

python
import re

sections = [p.strip() for p in re.split(r"\n(?=## )", PROTOCOL)[1:]]
titles = [s.splitlines()[0].lstrip("# ") for s in sections]
print(titles)

# ['1. Monitoring glucose control', '2. Kidney function and metformin', '3. Foot care', '4. Low blood glucose', '5. Eye screening']
Run the protocol block from the chunking lesson first. This splits it into sections.

Six patient- and clinician-style questions

python
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sentence_transformers import SentenceTransformer

questions = ["My sugar dropped really low, what now?", "What should I do about a hypo?",
             "Is the diabetes pill safe with weak kidneys?", "When do we need to stop the biguanide?",
             "How often are retinal photos needed?", "Numb toes test"]

vec = TfidfVectorizer(stop_words="english").fit(sections + questions)
kw = cosine_similarity(vec.transform(questions), vec.transform(sections))
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
em = model.encode(questions, normalize_embeddings=True) @ model.encode(sections, normalize_embeddings=True).T
for q, k, e in zip(questions, kw, em):
    print(f"{q:<45} keyword: {titles[k.argmax()][:24]:<25} ({k.max():.2f})  "
          f"embedding: {titles[e.argmax()][:24]:<25} ({e.max():.2f})")

# My sugar dropped really low, what now?        keyword: 4. Low blood glucose      (0.17)  embedding: 4. Low blood glucose      (0.56)
# What should I do about a hypo?                keyword: 1. Monitoring glucose co  (0.00)  embedding: 3. Foot care              (0.24)
# Is the diabetes pill safe with weak kidneys?  keyword: 5. Eye screening          (0.08)  embedding: 2. Kidney function and m  (0.42)
# When do we need to stop the biguanide?        keyword: 2. Kidney function and m  (0.08)  embedding: 2. Kidney function and m  (0.16)
# How often are retinal photos needed?          keyword: 5. Eye screening          (0.10)  embedding: 5. Eye screening          (0.36)
# Numb toes test                                keyword: 3. Foot care              (0.09)  embedding: 3. Foot care              (0.53)
Continuing from the previous block: best-matching section by TF-IDF and by embeddings.
  • Embeddings found the kidney section for 'weak kidneys', which keyword search sent to eye screening, and gave much stronger matches for paraphrases such as 'numb toes'.
  • Keyword search got 'biguanide' right only by luck: it matched the word 'stop', not the drug class. The embedding score for that question is low (0.16), a sign of uncertainty.
  • Both failed on 'hypo', common clinical shorthand for hypoglycaemia. A small general-purpose model does not know it.

Helping retrieval with clinical abbreviations

One simple fix is to expand known abbreviations and drug classes before searching. The dictionary below is tiny; real systems use curated abbreviation lists, terminologies such as SNOMED CT synonyms, or domain-trained embedding models such as MedCPT.

python
ABBREVIATIONS = {"hypo": "hypo (hypoglycaemia, low blood glucose)",
                 "biguanide": "biguanide (metformin)"}

def expand(question):
    for short, long in ABBREVIATIONS.items():
        question = re.sub(rf"\b{short}\b", long, question, flags=re.IGNORECASE)
    return question

section_emb = model.encode(sections, normalize_embeddings=True)
for q in ["What should I do about a hypo?", "When do we need to stop the biguanide?"]:
    for label, text in [("original", q), ("expanded", expand(q))]:
        scores = model.encode([text], normalize_embeddings=True) @ section_emb.T
        print(f"{label:<9} {text}\n          -> {titles[scores.argmax()]} ({scores.max():.2f})")

# original  What should I do about a hypo?
#           -> 3. Foot care (0.24)
# expanded  What should I do about a hypo (hypoglycaemia, low blood glucose)?
#           -> 4. Low blood glucose (0.61)
# original  When do we need to stop the biguanide?
#           -> 2. Kidney function and metformin (0.16)
# expanded  When do we need to stop the biguanide (metformin)?
#           -> 2. Kidney function and metformin (0.42)
Continuing from the previous blocks: expand abbreviations, then search with embeddings.
  • Expansion fixed the 'hypo' failure and made the 'biguanide' match far more confident.
  • Expansion lists need clinical ownership: an abbreviation can mean different things in different specialties (for example 'MS'), so expand only where the meaning is unambiguous in context.

A minimal vector store

A vector store keeps embeddings together with their text and metadata, and returns the nearest ones to a query. Metadata filters, for example on document version, stop outdated sources from being retrieved.

python
import numpy as np

class TinyVectorStore:
    """Holds normalised embeddings with their text and metadata; finds the nearest by cosine similarity."""
    def __init__(self, model):
        self.model, self.vectors, self.records = model, None, []

    def add(self, texts, metadata):
        emb = self.model.encode(texts, normalize_embeddings=True)
        self.vectors = emb if self.vectors is None else np.vstack([self.vectors, emb])
        self.records += [{"text": t, **m} for t, m in zip(texts, metadata)]

    def search(self, query, k=2, **filters):
        scores = self.vectors @ self.model.encode([query], normalize_embeddings=True)[0]
        hits = [(s, r) for s, r in zip(scores, self.records)
                if all(r.get(key) == value for key, value in filters.items())]
        return sorted(hits, key=lambda h: -h[0])[:k]

store = TinyVectorStore(model)
store.add(sections, [{"source": "clinic-protocol", "version": "2026", "section": t} for t in titles])
for score, record in store.search("Is the diabetes pill safe with weak kidneys?", k=2, version="2026"):
    print(f"{score:.2f}  {record['section']}  (source {record['source']} v{record['version']})")
print(f"Stored {store.vectors.shape[0]} vectors of {store.vectors.shape[1]} dimensions")

# 0.42  2. Kidney function and metformin  (source clinic-protocol v2026)
# 0.35  4. Low blood glucose  (source clinic-protocol v2026)
# Stored 5 vectors of 384 dimensions
Continuing from the previous blocks: store sections with metadata and search with a version filter.
  • This store compares the query with every vector, which is fine for thousands of chunks. Libraries such as FAISS and graph-based indexes such as HNSW find approximate nearest neighbours among millions of vectors quickly.
  • Production vector databases add persistence, access control, filtering and updates; choose one through your organisation's normal technology and security review.

Embeddings are sensitive data too

  • Research has shown that much of the original text can be reconstructed from its embeddings. Treat embeddings of clinical documents or patient records with the same protection as the text, and check where any external embedding service processes and stores them.

Exercise

Compare keyword and embedding retrieval, improve abbreviation handling and build a filtered vector store.

Data: Synthetic protocol in the notebook; small open embedding model all-MiniLM-L6-v2 (Course content; model under Apache 2.0)

  1. Reproduce the six-question comparison and add six questions of your own, including two abbreviations and two drug-class names.
  2. Implement BM25 (or use the rank_bm25 package) and add it to the comparison.
  3. Extend the abbreviation dictionary and measure how many of your questions it fixes, and whether any get worse.
  4. Add the outdated 2024 protocol to the vector store with version '2024' and show that the version filter excludes it.
  5. Optional: try a medical embedding model (for example MedCPT) and compare results on the same questions.

The runnable notebook and worked solution will be published soon. You can work through the steps with the code on this page in the meantime.

Key takeaways

  • Embeddings match by meaning; keyword search matches by shared words. Each catches what the other misses.
  • General embedding models can miss clinical abbreviations; expansion and domain models help.
  • Vector stores pair vectors with metadata; filter out outdated or irrelevant sources.
  • Protect embeddings like the text they came from.

Terms used: Approximate nearest-neighbour search, BM25, Embedding, Vector store

Related: Chunking clinical guidelines, Evaluating retrieval, Data standards: SNOMED CT synonyms

FAQ

What is an embedding?

A list of numbers that represents a piece of text so that texts with similar meaning have similar numbers. It allows search by meaning rather than exact words.

Are embeddings always better than keyword search?

No. Keyword search is strong for exact terms such as drug names and codes, while embeddings handle paraphrase. Many systems combine both (hybrid search).

Do I need a medical embedding model?

Not always, but general models can miss clinical abbreviations and jargon. Compare models on questions from your own users before choosing.

What is a vector database?

A store optimised for saving embeddings with metadata and quickly finding the most similar ones to a query, usually with approximate nearest-neighbour search.

Can embeddings leak patient information?

Yes. Studies show text can be partly reconstructed from embeddings, so embeddings of patient data must be protected like the data itself.

Educational content only. This material teaches methods; it is not clinical advice. Code, models and examples here are not validated medical devices and must not be used to make decisions about real patients without proper clinical validation, governance and any required regulatory approval. Examples use synthetic or openly licensed de-identified data only. Never paste identifiable patient information into public AI tools. Mentions of models, tools or vendors are for identification, not endorsement.