langchain-core retrieval reference
Pinned version: langchain-core==1.6.2 (released 2026-09-04) β the same package as the
langchain-core reference
; that page covers the chat and
tool types, this one covers the retrieval types. Two extras are needed for the examples:
numpy (resolved to 2.5.2), which InMemoryVectorStore and the fake embeddings import lazily and
do not declare β without it the first search fails with NameError: name 'np' is not defined β
and langchain-text-splitters==1.1.2 for the one load_and_split line. Everything below was
derived by inspecting the installed packages, and every example was executed against them on
Python 3.13. Each heading links to reference.langchain.com,
which is unversioned and always serves the latest 1.x. Links open in a new tab.
Part of a five-page set β see
how the LangChain stack fits together
for how langchain-core,
langgraph, langchain and langchain-text-splitters fit together.
What the retrieval half of langchain-core is
Retrieval-augmented generation is a pipeline of four steps, and langchain-core defines the type
that flows between them and the base class for each step:
1 load split embed + store retrieve
2BaseLoader.lazy_load β TextSplitter.split_documents β VectorStore.add_documents β BaseRetriever.invoke(query)
3 list[Document] list[Document] Embeddings.embed_documents list[Document]
| Module | What it defines |
|---|---|
langchain_core.documents | Document (text + metadata + id), BaseDocumentTransformer (what a text splitter is), BaseDocumentCompressor |
langchain_core.document_loaders | BaseLoader (yields Documents), Blob/BlobLoader/BaseBlobParser for raw bytes |
langchain_core.embeddings | Embeddings (text β vector), plus FakeEmbeddings and DeterministicFakeEmbedding for tests |
langchain_core.vectorstores | VectorStore (the store interface), InMemoryVectorStore (the one concrete store), VectorStoreRetriever |
langchain_core.retrievers | BaseRetriever β a Runnable[str, list[Document]] |
What it does not ship: loaders for real formats (PDF, HTML, S3, β¦ live in langchain-community
and per-source packages), a real embedding model (langchain-openai, langchain-huggingface,
langchain-ollama, β¦ subclass Embeddings; langchain-anthropic 1.7.1 has no embeddings class
at all), a production vector store (langchain-chroma, langchain-postgres, langchain-pinecone,
β¦ subclass VectorStore), or the splitters themselves β those are the separate
langchain-text-splitters
package, which
depends on langchain-core alone.
Two facts that shape everything below:
- A retriever is a
Runnable.BaseRetrieversubclassesRunnableSerializable[str, list[Document]], so it takes its place in a|chain, aRunnableParallel, or a langgraph node exactly like a prompt or a model β the last example on this page prints the graph of a complete RAG chain. Documentis not a message. It is the unit of retrieval; what you send to a model is still aHumanMessage/SystemMessage, usually built by formatting the retrieved documents into a prompt.
The examples use a six-line Embeddings subclass (BagOfWords, counts of four words) instead
of a provider so that similarity scores are exact and the outputs are reproducible offline.
Index
| Name | What it defines / does | Most-used methods & fields |
|---|---|---|
Document | Pydantic model: page_content (the text), metadata (a dict β source, page, headers), optional id (the key vector stores use) | page_content, metadata, id |
BaseLoader | Abstract source of documents: a subclass implements lazy_load() (a generator); load() collects it, load_and_split() chunks it | lazy_load, load, load_and_split |
Embeddings | Abstract textβvector interface: embed_documents(texts) and embed_query(text); providers subclass it | embed_documents, embed_query |
VectorStore | Abstract store: a subclass implements similarity_search and from_texts; it inherits add_documents, from_documents, get_by_ids, delete, as_retriever | from_documents, add_documents, similarity_search, as_retriever |
InMemoryVectorStore | The concrete store in core: a dict of {id, vector, text, metadata} searched by cosine similarity in numpy; dump/load to JSON | similarity_search_with_score, filter=, max_marginal_relevance_search, dump, load |
BaseRetriever | Abstract Runnable[str, list[Document]]: a subclass implements _get_relevant_documents(query); it gets invoke/batch/| and callbacks | invoke, batch |
VectorStoreRetriever | What as_retriever() returns: a BaseRetriever that runs similarity_search/max_marginal_relevance_search on its store with fixed search_kwargs | search_type, search_kwargs, invoke |
Document
The unit everything on this page produces or consumes: a piece of text plus what you know about it. Its fields:
page_content: strβ the text; the only positional argument.metadata: dictβ anything a filter, a citation or a prompt might need:source,page, the section headers a splitter recorded, astart_index. Defaults to{}.id: str | Noneβ the key a vector store files it under;add_documentsassigns one when it is missing, andget_by_ids/deletetake these.typeβ fixed to"Document", for serialisation.
It is a Pydantic model (BaseMedia β Serializable β BaseModel), so it validates on
construction and repr shows the fields; str() gives the page_content=β¦ metadata=β¦ form you
see when a list of results is printed. Loaders create them, splitters copy metadata onto every
chunk, stores keep the text beside the vector, and retrievers hand them back.
1from langchain_core.documents import Document
2
3doc = Document("Cats sleep sixteen hours a day.", metadata={"source": "cats.txt", "page": 1})
4print(doc.page_content) # Cats sleep sixteen hours a day.
5print(doc.metadata) # {'source': 'cats.txt', 'page': 1}
6print(doc.id, "|", doc.type) # None | Document
7print(repr(Document("hi", id="d1"))) # Document(id='d1', metadata={}, page_content='hi')
8print(str(doc)) # page_content='Cats sleep sixteen hours a day.' metadata={'source': 'cats.txt', 'page': 1}
BaseLoader
The interface for "where documents come from". A subclass implements one method,
lazy_load(self) -> Iterator[Document], as a generator that yields one Document per file, page,
row or record with the provenance in metadata. In return it gets:
load()βlist(self.lazy_load()), for when the whole corpus fits in memory. (Older loaders implementedloadinstead; the baselazy_loadthen wraps it, and raisesNotImplementedErrorif neither is written.)alazy_load()/aload()β async twins; the defaults run the sync versions in a thread.load_and_split(text_splitter=None)βload()followed bytext_splitter.split_documents. With no splitter it constructsRecursiveCharacterTextSplitter()fromlangchain-text-splitters, and raisesImportErrorif that package is absent. Its docstring marks it "do not override, consider it deprecated"; calling it is fine.
Core ships no concrete loader for any file format β those live in langchain-community and
per-source packages β and a loader for raw bytes is split into BlobLoader (finds Blobs) plus
BaseBlobParser (turns one into documents).
1from collections.abc import Iterator
2
3from langchain_core.document_loaders import BaseLoader
4from langchain_core.documents import Document
5
6FILES = {"cats.txt": "Cats sleep a lot.\n\nCats purr.", "dogs.txt": "Dogs fetch."}
7
8
9class DictLoader(BaseLoader):
10 """One Document per entry; lazy_load is the only method a loader must write."""
11
12 def __init__(self, files: dict[str, str]):
13 self.files = files
14
15 def lazy_load(self) -> Iterator[Document]:
16 for name, text in self.files.items():
17 yield Document(text, metadata={"source": name})
18
19
20loader = DictLoader(FILES)
21print(type(loader.lazy_load()).__name__) # generator
22for doc in loader.load(): # load() = list(lazy_load())
23 print(doc.metadata["source"], "|", repr(doc.page_content))
24# cats.txt | 'Cats sleep a lot.\n\nCats purr.'
25# dogs.txt | 'Dogs fetch.'
26
27chunks = loader.load_and_split() # RecursiveCharacterTextSplitter() by default
28print(len(chunks), [c.page_content for c in chunks]) # 2 ['Cats sleep a lot.\n\nCats purr.', 'Dogs fetch.']
29
30from langchain_text_splitters import CharacterTextSplitter
31
32chunks = loader.load_and_split(CharacterTextSplitter(chunk_size=20, chunk_overlap=0))
33for c in chunks:
34 print(c.metadata["source"], "|", repr(c.page_content))
35# cats.txt | 'Cats sleep a lot.'
36# cats.txt | 'Cats purr.'
37# dogs.txt | 'Dogs fetch.'
Embeddings
The text-to-vector interface. It is a plain ABC (not a Runnable) with two abstract methods β
embed_documents(texts: list[str]) -> list[list[float]] for what goes into the store and
embed_query(text: str) -> list[float] for what is searched for β because some providers embed
the two sides differently. The async aembed_documents/aembed_query default to running the
sync methods in a thread. A vector store holds one Embeddings and calls embed_documents on
add_documents and embed_query on every search, so the same object must be used for both, or
the vectors will not be comparable.
Core ships no provider implementation, only two test doubles (both need numpy):
FakeEmbeddings(size=)
returns random vectors, and
DeterministicFakeEmbedding(size=)
seeds the random generator with a hash of the text, so the same text always gets the same vector
β but two different texts get unrelated vectors, so nearest-neighbour search over it only finds
exact matches. That is why the examples on this page use the small subclass below instead: its
vectors mean something, and cosine scores come out exact.
1import re
2
3from langchain_core.embeddings import DeterministicFakeEmbedding, Embeddings
4
5VOCAB = ["cat", "dog", "kernel", "python"]
6
7
8class BagOfWords(Embeddings):
9 """The two members Embeddings leaves abstract, on a four-word vocabulary."""
10
11 def embed_documents(self, texts: list[str]) -> list[list[float]]:
12 return [self.embed_query(t) for t in texts]
13
14 def embed_query(self, text: str) -> list[float]:
15 words = re.findall(r"[a-z]+", text.lower())
16 return [float(words.count(w)) for w in VOCAB]
17
18
19emb = BagOfWords()
20print(emb.embed_query("cat cat dog")) # [2.0, 1.0, 0.0, 0.0]
21print(emb.embed_documents(["python kernel", "dog"])) # [[0.0, 0.0, 1.0, 1.0], [0.0, 1.0, 0.0, 0.0]]
22
23fake = DeterministicFakeEmbedding(size=4) # core's own test double: seeded by hash(text)
24a, b = fake.embed_query("cat"), fake.embed_query("cat")
25print(len(a), a == b) # 4 True <- same text, same vector
26print(fake.embed_query("cat") == fake.embed_query("dog")) # False
VectorStore
The abstract store: documents in, nearest documents out. A backend implements two members β
similarity_search(query, k=4, **kwargs) -> list[Document] and the classmethod
from_texts(texts, embedding, metadatas=None, ids=None) β plus one of add_texts/add_documents
(each is defined in terms of the other). Everything else is inherited:
- Loading.
from_documents(docs, embedding)unpacks the documents and callsfrom_texts;add_documents(docs, ids=None)returns the ids used (a document's ownidwhen it has one). - Searching.
similarity_search(documents only),similarity_search_with_score(plus the backend's raw score),similarity_search_with_relevance_scores(normalised to 0β1, which needs the backend to define a relevance function),similarity_search_by_vector(skip the query embedding),max_marginal_relevance_search(query, k, fetch_k, lambda_mult)(fetchfetch_k, then pickkthat are relevant and mutually different). Each has ana-prefixed async twin. - Housekeeping.
get_by_ids(ids),delete(ids), and theembeddingsproperty. as_retriever(search_type=β¦, search_kwargs=β¦)β wraps the store in aVectorStoreRetrieverso it can sit in a chain.
Provider packages (langchain-chroma, langchain-postgres, langchain-pinecone, β¦) subclass it;
core's own implementation is
InMemoryVectorStore
, used below.
1import re
2
3from langchain_core.documents import Document
4from langchain_core.embeddings import Embeddings
5from langchain_core.vectorstores import InMemoryVectorStore, VectorStore
6
7VOCAB = ["cat", "dog", "kernel", "python"]
8
9
10class BagOfWords(Embeddings):
11 def embed_documents(self, texts):
12 return [self.embed_query(t) for t in texts]
13
14 def embed_query(self, text):
15 words = re.findall(r"[a-z]+", text.lower())
16 return [float(words.count(w)) for w in VOCAB]
17
18
19print(sorted(VectorStore.__abstractmethods__)) # ['from_texts', 'similarity_search']
20
21docs = [
22 Document("The cat sat on the mat.", metadata={"topic": "pets"}, id="d1"),
23 Document("A dog chased the cat.", metadata={"topic": "pets"}, id="d2"),
24 Document("The kernel schedules the python process.", metadata={"topic": "os"}, id="d3"),
25]
26store = InMemoryVectorStore.from_documents(docs, BagOfWords()) # inherited: embeds + add_documents
27print(isinstance(store, VectorStore), store.embeddings.__class__.__name__) # True BagOfWords
28
29print([d.id for d in store.similarity_search("cat", k=2)]) # ['d1', 'd2']
30print(store.add_documents([Document("Another cat.", id="d4")])) # ['d4']
31print([d.id for d in store.get_by_ids(["d4", "d1"])]) # ['d4', 'd1']
32print(store.delete(["d4"]), len(store.store)) # None 3
33
34retriever = store.as_retriever(search_kwargs={"k": 1})
35print(type(retriever).__name__, retriever.invoke("kernel")[0].id) # VectorStoreRetriever d3
InMemoryVectorStore
The one concrete VectorStore in core, and the right one for tests, notebooks and anything that
fits in RAM. InMemoryVectorStore(embedding) keeps a plain dict, store, of
id β {"id", "vector", "text", "metadata"}, embeds on add_documents, and searches by computing
the cosine similarity between the query vector and every stored vector in numpy. What it adds to
the interface:
similarity_search_with_score(query, k=4, filter=None)returns(Document, score)pairs where the score is the cosine similarity β1.0for a document pointing the same way as the query,0.0for an orthogonal one.filteris a callableDocument -> boolapplied before ranking, so it can use any metadata.max_marginal_relevance_search(query, k=4, fetch_k=20, lambda_mult=0.5)is implemented, sosearch_type="mmr"works on a retriever built from it.similarity_search_with_relevance_scoresβ and thereforesearch_type="similarity_score_threshold"β is not: this store defines no relevance function, and the call raisesNotImplementedError.dump(path)/load(path, embedding)β JSON on disk. The vectors are saved, theEmbeddingsobject is not, soloadis handed the same kind of embedder again.get_by_ids,delete(ids), and the_by_vectorsearch variants.
1import os
2import re
3import tempfile
4
5from langchain_core.documents import Document
6from langchain_core.embeddings import Embeddings
7from langchain_core.vectorstores import InMemoryVectorStore
8
9VOCAB = ["cat", "dog", "kernel", "python"]
10
11
12class BagOfWords(Embeddings):
13 def embed_documents(self, texts):
14 return [self.embed_query(t) for t in texts]
15
16 def embed_query(self, text):
17 words = re.findall(r"[a-z]+", text.lower())
18 return [float(words.count(w)) for w in VOCAB]
19
20
21store = InMemoryVectorStore(BagOfWords())
22store.add_documents([
23 Document("cat cat", metadata={"lang": "en"}, id="d1"),
24 Document("cat dog", metadata={"lang": "en"}, id="d2"),
25 Document("python kernel", metadata={"lang": "code"}, id="d3"),
26])
27print(store.store["d1"])
28# {'id': 'd1', 'vector': [2.0, 0.0, 0.0, 0.0], 'text': 'cat cat', 'metadata': {'lang': 'en'}}
29
30for doc, score in store.similarity_search_with_score("cat", k=3):
31 print(f"{score:.3f}", doc.id, doc.page_content)
32# 1.000 d1 cat cat <- cosine similarity: same direction as the query
33# 0.707 d2 cat dog
34# 0.000 d3 python kernel
35
36only_code = store.similarity_search("kernel cat", k=3, filter=lambda d: d.metadata["lang"] == "code")
37print([d.id for d in only_code]) # ['d3']
38
39path = os.path.join(tempfile.mkdtemp(), "store.json")
40store.dump(path)
41reloaded = InMemoryVectorStore.load(path, BagOfWords()) # vectors are stored; the embedder is not
42print(len(reloaded.store), reloaded.similarity_search("dog", k=1)[0].id) # 3 d2
BaseRetriever
The abstract "string in, documents out" component, and the reason retrieval plugs into the rest
of the ecosystem: it subclasses RunnableSerializable[str, list[Document]]. A subclass declares
its configuration as Pydantic fields and implements one method,
_get_relevant_documents(self, query: str, *, run_manager) -> list[Document] (the run_manager
parameter may be omitted from the signature; _aget_relevant_documents is the optional async
native version). In return it gets:
- The
Runnablesurface.invoke(query),batch(queries),ainvoke,astream, and|composition βretriever | format_docs | prompt | modelis the RAG chain. Input and output schemas are generated and named after the class. - Callbacks.
invokewraps the call inon_retriever_start/on_retriever_end/on_retriever_error, so a retriever shows up as its own span in LangSmith traces with the query and the documents. - Config.
tags,metadata, and everythingwith_configaccepts.
A vector store is not a retriever, but
as_retriever()
makes one; write
your own subclass when the source is not a vector store at all β a keyword index, an HTTP API, a
database query.
1from langchain_core.callbacks import CallbackManagerForRetrieverRun
2from langchain_core.documents import Document
3from langchain_core.retrievers import BaseRetriever
4from langchain_core.runnables import Runnable, RunnableLambda
5
6
7class KeywordRetriever(BaseRetriever):
8 """Every document containing the query word, most mentions first."""
9
10 docs: list[Document]
11 k: int = 2
12
13 def _get_relevant_documents(self, query: str, *, run_manager: CallbackManagerForRetrieverRun) -> list[Document]:
14 scored = [(d.page_content.lower().split().count(query.lower()), d) for d in self.docs]
15 return [d for n, d in sorted(scored, key=lambda t: -t[0]) if n > 0][: self.k]
16
17
18retriever = KeywordRetriever(docs=[
19 Document("cat cat cat", id="d1"),
20 Document("dog", id="d2"),
21 Document("cat and dog", id="d3"),
22])
23print(isinstance(retriever, Runnable)) # True
24print([d.id for d in retriever.invoke("cat")]) # ['d1', 'd3']
25print([[d.id for d in r] for r in retriever.batch(["dog", "python"])]) # [['d2', 'd3'], []]
26
27count = retriever | RunnableLambda(lambda docs: f"{len(docs)} hit(s)")
28print(count.invoke("dog")) # 2 hit(s)
29print(retriever.get_input_schema().__name__, "->", retriever.get_output_schema().__name__)
30# KeywordRetrieverInput -> KeywordRetrieverOutput
VectorStoreRetriever
What VectorStore.as_retriever() returns: a BaseRetriever whose _get_relevant_documents
calls the store. Its fields fix how the store is searched, so the chain that holds it only
ever passes a query string:
search_typeβ"similarity"(default) callssimilarity_search;"mmr"callsmax_marginal_relevance_search;"similarity_score_threshold"callssimilarity_search_with_relevance_scoresand keeps what scores abovesearch_kwargs["score_threshold"](unsupported byInMemoryVectorStore, see above). Anything else is rejected at construction.search_kwargsβ forwarded to that call:k,filter,fetch_kandlambda_multfor MMR. Keyword arguments passed toinvokeare merged on top for a single call.vectorstore,tags(the store's class name and its embedder's, for tracing),metadata.
Because it is a Runnable, a whole RAG chain is Runnable composition and can print its own
graph, which is what the example ends with.
1import re
2
3from langchain_core.documents import Document
4from langchain_core.embeddings import Embeddings
5from langchain_core.language_models import GenericFakeChatModel
6from langchain_core.messages import AIMessage
7from langchain_core.output_parsers import StrOutputParser
8from langchain_core.prompts import ChatPromptTemplate
9from langchain_core.runnables import RunnableParallel, RunnablePassthrough
10from langchain_core.vectorstores import InMemoryVectorStore
11
12VOCAB = ["cat", "dog", "kernel", "python"]
13
14
15class BagOfWords(Embeddings):
16 def embed_documents(self, texts):
17 return [self.embed_query(t) for t in texts]
18
19 def embed_query(self, text):
20 words = re.findall(r"[a-z]+", text.lower())
21 return [float(words.count(w)) for w in VOCAB]
22
23
24store = InMemoryVectorStore.from_documents([
25 Document("A cat sleeps sixteen hours a day.", id="cat"),
26 Document("A dog needs a walk every day.", id="dog"),
27 Document("The kernel schedules every python thread.", id="os"),
28], BagOfWords())
29
30retriever = store.as_retriever(search_kwargs={"k": 1})
31print(retriever.search_type, retriever.search_kwargs) # similarity {'k': 1}
32print([d.id for d in retriever.invoke("How long does a cat sleep?")]) # ['cat']
33
34diverse = store.as_retriever(search_type="mmr", search_kwargs={"k": 2, "fetch_k": 3})
35print([d.id for d in diverse.invoke("cat cat dog")]) # ['cat', 'dog']
36
37# The retriever is a Runnable, so it is the first stage of a RAG chain.
38prompt = ChatPromptTemplate.from_messages([
39 ("system", "Answer from the context only.\n\n{context}"),
40 ("human", "{question}"),
41])
42model = GenericFakeChatModel(messages=iter([AIMessage("About sixteen hours.")]))
43
44
45def format_docs(docs: list[Document]) -> str:
46 return "\n".join(d.page_content for d in docs)
47
48
49gather = RunnableParallel(context=retriever | format_docs, question=RunnablePassthrough())
50gather.name = "gather" # names the diagram's fork/join nodes
51chain = gather | prompt | model | StrOutputParser()
52
53print(chain.invoke("How long does a cat sleep?")) # About sixteen hours.
54print(chain.get_graph().draw_mermaid(with_styles=False))
Diagram β generated by the code above:
The fork after gather_input is the RunnableParallel: the question goes down one branch to the
retriever and, unchanged, down the other; gather_output is the dict {"context", "question"}
the prompt template fills. Without the gather.name line the two nodes are called
Parallel<context,question>Input/Output.