A vector store is a database built to save embeddings (lists of numbers that represent the meaning of text) and to find the ones closest to a query. It is the memory of a RAG (Retrieval-Augmented Generation) app. Without it, you would have to compare your question with every document by hand on each request. In this lesson you will learn how storing and searching works, how LangChain wraps it in one common interface, and how to use it safely.
Overview: how it works
An embedding model turns a piece of text into a vector, for example 1536 floating point numbers. Texts with similar meaning get vectors that point in similar directions. A vector store keeps three things for every record:
- the vector (the embedding),
- the original text (the page content),
- the metadata (a dictionary such as source file, page number or language) and an id.
Storing happens in two steps. First the store calls your embedding model on each text. Then it writes vector, text, metadata and id into its index. Searching repeats the first step for the query: the query text is embedded with the same model, and the store returns the records whose vectors are closest to the query vector.
What does closest mean?
Closeness is measured by a distance or similarity metric. The most common is cosine similarity, which compares the angle between two vectors (1 means same direction, 0 unrelated, -1 opposite). Others are Euclidean (L2) distance and dot product. Different stores use different defaults, so a “score” from one store cannot be compared directly with a score from another. Some stores return a similarity (higher is better), others return a distance (lower is better). Always check the documentation of your store.
Exact and approximate search
The simplest search compares the query with every stored vector. This is exact, but slow for millions of records. Production databases build an approximate nearest neighbour (ANN) index, such as HNSW, that finds very close matches much faster and may occasionally miss a true neighbour. For learning and small datasets, exact search is fine.
One interface for many databases
LangChain defines a common VectorStore base class in langchain_core.vectorstores. Chroma, FAISS, Pinecone, Qdrant, pgvector and others implement it through partner packages. Because the method names are the same, you can start with an in-memory store and later switch to a hosted database by changing only the line that creates the store.
Syntax / Key concepts
store = SomeVectorStore(embedding=embeddings, ...)
ids = store.add_documents(documents, ids=[...])
hits = store.similarity_search(query, k=4, filter=...)
pairs = store.similarity_search_with_score(query, k=4)
store.delete(ids=[...])
retriever = store.as_retriever(search_kwargs={'k': 4})
| Part | Meaning |
|---|---|
embedding |
The embedding model object. It is used for both documents and queries. |
add_documents |
Embeds each Document and saves it. Returns the list of ids. |
add_texts |
Same, but from plain strings and optional metadata dictionaries. |
similarity_search |
Returns the k most similar Document objects. |
similarity_search_with_score |
Returns a list of (Document, score) pairs. |
filter |
Restricts search by metadata. Its form depends on the store. |
delete |
Removes records by id. |
as_retriever |
Wraps the store as a Runnable you can place in an LCEL chain. |
Examples
Example 1: store and search offline
To follow along without an API key we use DeterministicFakeEmbedding, which creates a repeatable pseudo-random vector from the text. It is only for tests: its vectors carry no real meaning, so the ranking of results is not meaningful. The InMemoryVectorStore keeps everything in a Python dictionary.
from langchain_core.documents import Document
from langchain_core.embeddings import DeterministicFakeEmbedding
from langchain_core.vectorstores import InMemoryVectorStore
embeddings = DeterministicFakeEmbedding(size=64)
store = InMemoryVectorStore(embedding=embeddings)
docs = [
Document(page_content='Reset your password from the login page.',
metadata={'source': 'faq.md'}),
Document(page_content='Refunds are processed within 7 days.',
metadata={'source': 'policy.md'}),
Document(page_content='Contact support by email or chat.',
metadata={'source': 'support.md'}),
]
ids = store.add_documents(docs)
print(len(ids))
results = store.similarity_search('How do I reset my password?', k=2)
print(len(results))
print(type(results[0]).__name__)
Output:
3
2
Document
Three documents were embedded and stored, and the store generated three ids for us. The search embedded the question, compared it with the three vectors and returned the top 2 as Document objects. With a real embedding model, the password document would rank first. With the fake model the order is arbitrary, which is why we print only counts and types.
Example 2: scores, metadata filter, retriever and delete
Now we give our own ids, add a lang field to the metadata, and use more features. For InMemoryVectorStore the filter is a Python function that receives a Document and returns True to keep it. Other stores use their own filter syntax (often a dictionary), so read the store’s documentation.
from langchain_core.documents import Document
from langchain_core.embeddings import DeterministicFakeEmbedding
from langchain_core.vectorstores import InMemoryVectorStore
store = InMemoryVectorStore(embedding=DeterministicFakeEmbedding(size=64))
docs = [
Document(page_content='Our refund policy', metadata={'lang': 'en'}),
Document(page_content='Shipping takes three days', metadata={'lang': 'en'}),
Document(page_content='Votre politique de remboursement', metadata={'lang': 'fr'}),
Document(page_content='Contact our support team', metadata={'lang': 'en'}),
]
store.add_documents(docs, ids=['d1', 'd2', 'd3', 'd4'])
english = store.similarity_search(
'refund', k=10, filter=lambda doc: doc.metadata['lang'] == 'en'
)
print(len(english))
pairs = store.similarity_search_with_score('refund', k=4)
print(all(-1.0001 <= score <= 1.0001 for _, score in pairs))
retriever = store.as_retriever(search_kwargs={'k': 2})
print(len(retriever.invoke('refund policy')))
store.delete(ids=['d1'])
print(len(store.similarity_search('anything', k=10)))
Output:
3
True
2
3
The filter removed the French document, so only 3 English documents could be returned even though we asked for up to 10. In this store the score is cosine similarity, so it always lies between -1 and 1. The retriever is a Runnable: calling invoke with a string returns a list of documents, and you can put it in a chain with the | operator. After deleting d1, 3 records remain.
Example 3: a persistent store with real embeddings
An in-memory store disappears when the program ends. Chroma can save to a folder on disk. This example needs a package install and an API key, so its output is illustrative.
pip install langchain-chroma langchain-openai
export OPENAI_API_KEY='your-key-here'
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model='text-embedding-3-small')
store = Chroma(
collection_name='faq',
embedding_function=embeddings,
persist_directory='./chroma_db',
)
store.add_documents([
Document(page_content='Reset your password from the login page.',
metadata={'source': 'faq.md'}),
Document(page_content='Refunds are processed within 7 days.',
metadata={'source': 'policy.md'}),
])
for doc, score in store.similarity_search_with_score('forgot my password', k=1):
print(doc.page_content, '|', doc.metadata['source'], '|', round(score, 3))
Reset your password from the login page. | faq.md | 0.742
The OpenAI key is read from the environment variable OPENAI_API_KEY, never written in code. The exact score above is only an example. Chroma returns a distance (lower is closer), so do not compare it with the cosine scores from Example 2. Model names change over time, so check your provider’s documentation for current names.
Under the hood: step by step
- Add:
add_documentscollects thepage_contentof every document. - Embed: the store calls
embeddings.embed_documents(texts)once for the whole batch, which returns one vector per text. - Write: vectors, texts, metadata and ids are saved in the index. If an id already exists, most stores overwrite that record.
- Search:
similarity_searchcallsembeddings.embed_query(query)to get the query vector. - Compare: the store scores the query vector against stored vectors (all of them, or ANN candidates), applies the metadata filter and keeps the top
k. - Return: it rebuilds
Documentobjects from the saved text and metadata.
Common Mistakes
Mistake 1: using different embedding models for storing and searching
store = Chroma(collection_name='faq', embedding_function=OpenAIEmbeddings(model='text-embedding-3-small'), persist_directory='./chroma_db')
store.add_documents(docs)
# later, in another script, a different model is used
store = Chroma(collection_name='faq', embedding_function=OpenAIEmbeddings(model='text-embedding-3-large'), persist_directory='./chroma_db')
store.similarity_search('password')
Vectors from different models live in different spaces, and they often have different lengths. The search will fail with a dimension error, or worse, return meaningless results when the lengths happen to match. Fix: use the same model for the whole collection. If you change the model, re-embed all documents into a new collection.
Mistake 2: adding the same documents on every run
store = Chroma(collection_name='faq', embedding_function=embeddings, persist_directory='./chroma_db')
store.add_documents(docs) # runs on every start of the app
With a persistent store and no ids, each start adds duplicate records. Search then returns the same text several times and wastes your context window. Fix: give each document a stable id (for example a hash of its source and chunk number) so a repeat run overwrites instead of duplicating, or check whether the collection is already filled.
Mistake 3: treating fake embeddings as real
DeterministicFakeEmbedding is great for tests, but results from it are not based on meaning. Never judge the quality of your retrieval with it.
Best Practices
- Use one embedding model per collection, and record its name next to your data.
- Set your own stable ids so you can update and delete records exactly.
- Store useful metadata (source, page, date, language, access level) at indexing time; you cannot filter on what you did not save.
- Add documents in batches, because embedding calls over a network are the slow and costly part.
- Choose
kdeliberately. A small value such as 3 to 5 keeps the prompt short; raise it if answers miss facts. - Use the in-memory store for tests and tutorials, and a persistent or hosted store for production.
- Do not compare scores across different stores or metrics; check whether higher or lower is better.
- Keep API keys in environment variables and out of source control.
- Evaluate retrieval on a few real questions before you connect the LLM.
Practice Exercises
- Create an
InMemoryVectorStorewithDeterministicFakeEmbedding(size=32). Add five documents, each with atopicmetadata field. Search with a filter that keeps only one topic and print how many documents come back. - Add three documents with ids
a,b,c. Deleteb, then confirm that a search withk=10returns two documents. Hint: usestore.delete(ids=[...]). - Turn your store into a retriever with
k=1and callinvokeon it. Print the type of the returned value and its length.
Summary
- A vector store saves vectors with text, metadata and ids, and finds the closest vectors to an embedded query.
- Documents and queries must be embedded with the same model.
- LangChain gives all stores one interface:
add_documents,similarity_search,similarity_search_with_score,deleteandas_retriever. - Metadata filters narrow results, but their syntax depends on the store.
- Scores differ between stores: some are similarities, some are distances.
- Use stable ids to avoid duplicates, and persistent storage when data must survive restarts.
- Fake embeddings let you test the code offline, but not the quality of retrieval.
