Vector index map

Vector store: finding the nearest

An ANN index returns the top-k nearest neighbours by cosine - without scanning the whole archive

cosine closeness 1.0 → 0.0 top-k (near) cut off (far)
How to read the map. Every fragment is stored in the index as a vector (dim 1536) together with its metadata. The query turns into a vector; nearest-neighbour search (approximate, ANN, via an HNSW graph) returns the top-k best by cosine closeness in a logarithmic number of steps instead of a full scan. Click a point (including distant ones) to look inside - semantic zoom shows the neighbour's cosine and metadata, or why it did not make the top-k.

Enable JavaScript to open the interactive index map: points, cosine rings, semantic zoom into a neighbour's metadata. Below is the same query and top-k, static.

Query

"how to get my money back for a purchase" -> query vector (dim 1536), ANN search by cosine, k = 3.

top-k by cosine (k = 3)

  1. #1refund policy - faq.md / section refund0.91
  2. #2electronics warranty - faq.md / section warranty0.85
  3. #3product exchange - faq.md / section refund0.78
top-k boundary (k = 3)
  1. #4city delivery0.24
  2. #5loyalty program0.19
  3. #6store opening hours0.12

Next to each vector the store keeps metadata (source / section / date / access); a filter over it narrows the search down to a nearest-neighbour lookup.

The problem

You have vectors - one per fragment from the previous chapter. When a query arrives, you need to find a few fragments closest in meaning to the query vector. You could compare the query against every vector in turn - and with a thousand fragments that works. But with a million fragments, scanning each one on every query becomes too slow.

The solution is a vector store: a specialized storage that holds vectors and can quickly find nearest neighbours without comparing the query against the whole archive. Here is what the store step plus search look like using a vector-database client:

Vector store: upsert vectors with metadata, then search the top-k nearest with a metadata filter.
# pip install pinecone openai
from pinecone import Pinecone
from openai import OpenAI

oai = OpenAI()
pc = Pinecone()
index = pc.Index("docs")

# store: put the vector + metadata + chunk text
def embed(text):
    return oai.embeddings.create(
        model="text-embedding-3-small", input=text
    ).data[0].embedding

index.upsert(vectors=[
    {"id": "c1", "values": embed("Refund policy: a product may be returned within 30 days."),
     "metadata": {"source": "faq.md", "section": "refund", "text": "..."}},
    {"id": "c2", "values": embed("The warranty on electronics is 12 months."),
     "metadata": {"source": "faq.md", "section": "warranty", "text": "..."}},
])

# search: top-k nearest to the query vector
res = index.query(
    vector=embed("how to get a refund for a purchase"),
    top_k=3,
    include_metadata=True,
    filter={"section": "refund"},   # metadata filter
)
for m in res.matches:
    print(m.id, round(m.score, 3), m.metadata["section"])
  1. Clients and index -- The OpenAI client for embeddings and the docs index in Pinecone - the vector store.
  2. embed helper -- A helper: text -> vector with the same model for both store and search.
  3. Upsert with metadata -- Put each vector into the store together with its metadata (source, section, text) - the store keeps them side by side.
  4. Filtered search -- Search for the top-3 nearest, but first narrow the search with a metadata filter (section refund).
  5. Print matches -- Print the id, cosine score and section of each returned match.

The database stores the vectors itself, searches for the nearest itself, and returns the top-k together with their cosine closeness - you do not have to write a scan by hand.

Why a separate database

A vector database solves two tasks at once: it stores millions of vectors and searches among them for the nearest by meaning (Pinecone, vector database basics). An ordinary database searches by exact equality or by keywords; a vector database searches by geometric closeness in dim-dimensional space, that is, by meaning (Pinecone, what is a vector database).

This is exactly what was missing at the search step: fast semantic search over the whole archive without letter-by-letter comparison.

Approximate nearest neighbour search (ANN)

Exact nearest-neighbour search (kNN) compares the query against all vectors - this guarantees the correct answer but grows linearly with the archive size. With millions of fragments people use approximate search (ANN, approximate nearest neighbour): it almost always finds the same neighbours but many times faster, sacrificing a small fraction of accuracy for speed.

A common ANN algorithm is HNSW (Hierarchical Navigable Small World): a multi-layer graph through which the search "hops" from distant nodes to the nearest in a logarithmic number of steps instead of scanning the whole set (Malkov & Yashunin, 2016, HNSW). Other systems build indexes on top of libraries like FAISS, which is purpose-built for fast search over millions of vectors.

Top-k and metadata filters

In a search you ask not for the single closest vector but for the top-k - several nearest ones (often k=3..10). This gives the model a few spare pieces in case the closest one does not fully cover the question.

Together with the vector, the database stores metadata - source, section, date, access rights. Metadata filters narrow the search to a matching subset in combination with the nearest-neighbour search - rather than scanning the whole index: for example, only documents from this department or only what is fresher than a certain date (Pinecone, metadata filtering). In the code above this is filter={"section": "refund"}.

Scale: millions of fragments without a scan

It is exactly the ANN index that makes semantic search practical on a large archive. Instead of comparing the query against each of millions of vectors, the HNSW graph leads to the answer in a logarithmic number of steps (Malkov & Yashunin, 2016, HNSW). So from 100000+ fragments the top-5 nearest are fetched in milliseconds, not in a full pass over the database.

When you do not need a vector database

A vector database is not always the right choice:

  • Small corpus. With a few hundred or a thousand fragments, an exact in-memory scan (kNN) is simple, fast enough, and needs no separate infrastructure.
  • Exact match matters more than meaning. If you need to search by exact codes, SKUs, or IDs - an ordinary database or a full-text index is more precise and cheaper.
  • You already have a suitable database. Some ordinary DBMSs support vector search as an extension (for example pgvector for PostgreSQL, pgvector) - then a separate specialized database may be redundant.

The rule: take a vector database when the corpus is large AND the search is genuinely by meaning. Otherwise it adds complexity without payoff.

Sources

Try it yourself

  • Open the ann-topk-drill interaction: find the query node and walk the drawn query path to its top-k neighbours. Drill (semantic zoom) into one of the selected neighbours and look at its cosine and metadata.
  • Compare what comes back with and without a metadata filter: in the code above remove filter={"section": "refund"} and see how the top-k set changes.
  • Estimate whether a vector database fits your case: how many fragments do you have, and do you search by meaning or by exact match - check against the section "When you do not need a vector database".

What is next

The vectors are in the index and the nearest are found quickly - next you need to turn the user's query into a vector and assemble the top-k on a live request. The next stop: search (searching by meaning at query time).

Back to the route map

About this recipe