Build a semantic search engine with LangChain
import EmbeddingsTabsPy from '/snippets/embeddings-tabs-py.mdx'; import EmbeddingsTabsJS from '/snippets/embeddings-tabs-js.mdx'; import VectorstoreTabsPy from '/snippets/vectorstore-tabs-py.mdx'; import VectorstoreTabsJS from '/snippets/vectorstore-tabs-js.mdx';
Overview
Build a semantic search engine over a PDF with LangChain embeddings (opens in a new tab) and vector stores (opens in a new tab). Use it to retrieve passages similar to a query, then plug the retriever into retrieval-augmented generation (RAG) (opens in a new tab) or other LLM workflows.
This tutorial covers:
- Create
Documentobjects from a PDF. - Generate embeddings.
- Load and split a PDF.
- Index chunks in a vector store and query by similarity.
- Wrap the store as a retriever.
The guide also includes a minimal RAG implementation on top of the search engine.
Concepts
This tutorial focuses on text retrieval and covers the following concepts:
- @[
Document] - Text splitters (opens in a new tab)
- Embeddings (opens in a new tab)
- Vector stores (opens in a new tab) and retrievers (opens in a new tab)
Setup
Install dependencies
This tutorial reads a PDF using the pypdf package:
pip install pypdfconda install pypdf -c conda-forgeuv add pypdfFor more details, see the Installation guide (opens in a new tab).
Configure LangSmith
Many of the applications you build with LangChain will contain multiple steps with multiple invocations of LLM calls. As these applications get more and more complex, it becomes crucial to be able to inspect what exactly is going on inside your chain or agent. The best way to do this is with LangSmith (opens in a new tab).
After you sign up at the link above, make sure to set your environment variables to start logging traces:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."In a notebook, you can set them with:
import getpass
import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = getpass.getpass()Create documents
LangChain implements a @[Document] abstraction for a unit of text and associated metadata. It has three attributes:
page_content: a string representing the content.metadata: a dict containing arbitrary metadata.id: (optional) a string identifier for the document.
metadata can capture the source of the document, its relationship to other documents, and other information. An individual @[Document] often represents a chunk of a larger document.
The following code creates sample documents:
from langchain_core.documents import Document
documents = [
Document(
page_content="Dogs are great companions, known for their loyalty and friendliness.",
metadata={"source": "mammal-pets-doc"},
),
Document(
page_content="Cats are independent pets that often enjoy their own space.",
metadata={"source": "mammal-pets-doc"},
),
]Generate embeddings
Vector search stores numeric vectors associated with text. Embed a query as a vector of the same dimension, then use similarity metrics (such as cosine similarity) to find related text.
LangChain supports embeddings from many providers (opens in a new tab). Select a model to specify how text should be converted into a numeric vector:
vector_1 = embeddings.embed_query(documents[0].page_content)
vector_2 = embeddings.embed_query(documents[1].page_content)
assert len(vector_1) == len(vector_2)
print(f"Generated vectors of length {len(vector_1)}\n")
print(vector_1[:10])Generated vectors of length 1536
[-0.008586574345827103, -0.03341241180896759, -0.008936782367527485, -0.0036674530711025, 0.010564599186182022, 0.009598285891115665, -0.028587326407432556, -0.015824200585484505, 0.0030416189692914486, -0.012899317778646946]Next, store embeddings in a vector store that supports efficient similarity search.
Select a vector store
LangChain @[VectorStore] objects add text and @[Document] objects to a store and query them with similarity metrics. They are often initialized with embedding (opens in a new tab) models that translate text into numeric vectors.
LangChain includes integrations (opens in a new tab) with many vector store technologies. Some are hosted and need credentials, some run in separate infrastructure (local or third-party), and others run in-memory for lightweight workloads. Select a vector store:
Load and split a PDF
Load content from a PDF, then split it into smaller chunks before indexing. This example uses a sample Nike 10-K filing from 2023 (opens in a new tab).
For dedicated PDF document loaders (including PyPDFLoader and partner packages), see PDF document loaders (opens in a new tab).
import pypdf
from langchain_core.documents import Document
# Below is a minimal helper for demonstration purposes.
def load_pdf_pages(file_path: str) -> list[Document]:
reader = pypdf.PdfReader(file_path)
return [
Document(
page_content=page.extract_text() or "",
metadata={"source": file_path, "page": i},
)
for i, page in enumerate(reader.pages)
]
file_path = "../example_data/nke-10k-2023.pdf"
docs = load_pdf_pages(file_path)
print(len(docs))107A page is often too coarse for retrieval. Split pages further so relevant passages are not diluted by surrounding text. @[RecursiveCharacterTextSplitter] recursively splits on common separators (such as newlines) until each chunk is the target size. This is the recommended text splitter for generic text use cases.
Set add_start_index=True so each split keeps a start_index metadata field for its character offset in the original document.
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(docs)
print(len(all_splits))516Index documents
Index the chunks into the vector store:
ids = vector_store.add_documents(documents=all_splits)Most vector store integrations also support connecting to an existing store (for example with a client or index name). See the docs for a specific integration (opens in a new tab) for details.
Query the vector store
After you have added the documents to the @[VectorStore], you can query it:
- Synchronously and asynchronously
- By string query and by vector
- With and without similarity scores
- By similarity and @[maximum marginal relevance][VectorStore.max_marginal_relevance_search] (to balance similarity with diversity)
These methods generally return a list of @[Document] objects.
Search by string
Embeddings map text to dense vectors so similar meanings are geometrically close. This means you can Retrieve relevant passages by passing a natural-language question:
results = vector_store.similarity_search(
"How many distribution centers does Nike have in the US?"
)
print(results[0])Async query:
results = await vector_store.asimilarity_search("When was Nike incorporated?")
print(results[0])Return scores
You can return similarity scores with the documents. Score meaning varies by provider. In this case, the score is a distance metric that varies inversely with similarity:
# Note that providers implement different scores; the score here
# is a distance metric that varies inversely with similarity.
results = vector_store.similarity_search_with_score("What was Nike's revenue in 2023?")
doc, score = results[0]
print(f"Score: {score}\n")
print(doc)Search by vector
Embed the query yourself, then search with the resulting vector:
embedding = embeddings.embed_query("How were Nike's margins impacted in 2023?")
results = vector_store.similarity_search_by_vector(embedding)
print(results[0])Learn more:
- @[API Reference][VectorStore]
- Integration-specific docs (opens in a new tab)
Use retrievers
LangChain @[VectorStore] objects do not subclass @[Runnable]. @[Retrievers] are Runnables, so they support standard methods such as sync and async invoke and batch.
You can also build retrievers from vector stores, and retrievers can also wrap non-vector sources (such as external APIs).
In this case, create a simple retriever without subclassing Retriever by wrapping similarity_search:
from typing import List
from langchain_core.documents import Document
from langchain_core.runnables import chain
@chain
def retriever(query: str) -> List[Document]:
return vector_store.similarity_search(query, k=1)
retriever.batch(
[
"How many distribution centers does Nike have in the US?",
"When was Nike incorporated?",
],
)[[Document(metadata={'page': 4, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 3125}, page_content='direct to consumer operations sell products through the following number of retail stores in the United States:\nU.S. RETAIL STORES NUMBER\nNIKE Brand factory stores 213 \nNIKE Brand in-line stores (including employee-only stores) 74 \nConverse stores (including factory stores) 82 \nTOTAL 369 \nIn the United States, NIKE has eight significant distribution centers. Refer to Item 2. Properties for further information.\n2023 FORM 10-K 2')],
[Document(metadata={'page': 3, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0}, page_content='Table of Contents\nPART I\nITEM 1. BUSINESS\nGENERAL\nNIKE, Inc. was incorporated in 1967 under the laws of the State of Oregon. As used in this Annual Report on Form 10-K (this "Annual Report"), the terms "we," "us," "our,"\n"NIKE" and the "Company" refer to NIKE, Inc. and its predecessors, subsidiaries and affiliates, collectively, unless the context indicates otherwise.\nOur principal business activity is the design, development and worldwide marketing and selling of athletic footwear, apparel, equipment, accessories and services. NIKE is\nthe largest seller of athletic footwear and apparel in the world. We sell our products through NIKE Direct operations, which are comprised of both NIKE-owned retail stores\nand sales through our digital platforms (also referred to as "NIKE Brand Digital"), to retail accounts and to a mix of independent distributors, licensees and sales')]]Vector stores implement an as_retriever method that returns a VectorStoreRetriever (opens in a new tab). These retrievers expose search_type and search_kwargs to select and parameterize the underlying store methods. Replicate the example above with:
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": 1},
)
retriever.batch(
[
"How many distribution centers does Nike have in the US?",
"When was Nike incorporated?",
],
)[[Document(metadata={'page': 4, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 3125}, page_content='direct to consumer operations sell products through the following number of retail stores in the United States:\nU.S. RETAIL STORES NUMBER\nNIKE Brand factory stores 213 \nNIKE Brand in-line stores (including employee-only stores) 74 \nConverse stores (including factory stores) 82 \nTOTAL 369 \nIn the United States, NIKE has eight significant distribution centers. Refer to Item 2. Properties for further information.\n2023 FORM 10-K 2')],
[Document(metadata={'page': 3, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0}, page_content='Table of Contents\nPART I\nITEM 1. BUSINESS\nGENERAL\nNIKE, Inc. was incorporated in 1967 under the laws of the State of Oregon. As used in this Annual Report on Form 10-K (this "Annual Report"), the terms "we," "us," "our,"\n"NIKE" and the "Company" refer to NIKE, Inc. and its predecessors, subsidiaries and affiliates, collectively, unless the context indicates otherwise.\nOur principal business activity is the design, development and worldwide marketing and selling of athletic footwear, apparel, equipment, accessories and services. NIKE is\nthe largest seller of athletic footwear and apparel in the world. We sell our products through NIKE Direct operations, which are comprised of both NIKE-owned retail stores\nand sales through our digital platforms (also referred to as "NIKE Brand Digital"), to retail accounts and to a mix of independent distributors, licensees and sales')]]VectorStoreRetriever supports search types of "similarity" (default), "mmr" (maximum marginal relevance), and "similarity_score_threshold". Use the last option to filter documents by similarity score.
You can use retrievers in more complex apps such as retrieval-augmented generation (RAG) (opens in a new tab), which combine a question with retrieved context in a prompt for an LLM. To learn more about building such an application, check out the RAG tutorial (opens in a new tab) tutorial.
Next steps
You've now seen how to build a semantic search engine over a PDF document.
For more information see:
- Available embedding integrations (opens in a new tab)
- Available vector store integrations (opens in a new tab)
For more on RAG: