Who this is for and what you will build
This article is for readers who want semantic search over their own PDF files and plan to connect a language model later for question answering. By the end you will split a PDF into chunks, index their vectors, run similarity queries that return relevant passages, and wrap the search in a retriever.
How semantic search works
Semantic search turns text into numeric vectors, so texts with similar meanings end up close together. A query is converted into a vector too, and a similarity metric such as cosine similarity finds the closest passages[1]. The example covers creating Document objects, generating embeddings, loading and splitting a PDF, writing to a vector store, querying by similarity, and wrapping the store as a retriever[1].
Prerequisites
- Python reads PDFs with the
pypdfpackage. It can be installed withpip install pypdf,conda install pypdf -c conda-forgeoruv add pypdf. JavaScript usespdf-parse, installed withnpm i pdf-parse[1]. - The sample file is a Nike 10-K filing from 2023. Download it first, then set
file_pathto its absolute path or a path relative to the working directory[1]. - Tracing is optional. Setting
LANGSMITH_TRACINGandLANGSMITH_API_KEYstarts the recording of traces[1]:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."Step 1: What a Document is
A LangChain Document holds a piece of text and its metadata. It has three attributes: page_content for the text, metadata for an arbitrary dictionary, and an optional id[1]. A Document is often a chunk of a larger file, and its metadata can record where the chunk came from[1]. Start with a few sample texts to see the structure:
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"},
),
]Step 2: Read the PDF and split it
The helper load_pdf_pages reads each page with pypdf and creates one Document per page, with the file path and page number in the metadata[1]. The code comment marks the helper as minimal and meant for demonstration. For production work, a dedicated PDF document loader is the better choice[1]:
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))The sample file produced 107 pages. A full page is too coarse for retrieval, because relevant text gets diluted by everything around it, so the pages are split further. RecursiveCharacterTextSplitter splits recursively on separators such as newlines until each chunk reaches the target size. It is the recommended splitter for general text[1]:
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))The 107 pages became 516 chunks. With add_start_index=True, each chunk keeps a start_index value that records its character offset in the full document[1]. The example uses chunk_size=1000 and chunk_overlap=200, and these values can be adjusted for other documents.
Step 3: Embed the chunks and build an index
embed_query turns a piece of text into a vector. In the example, two test texts produced vectors of the same length, 1536[1]. The chunks are then written to a vector store with vector_store.add_documents[1]:
ids = vector_store.add_documents(documents=all_splits)Vector store integrations vary. Some are hosted and need credentials, some run on separate infrastructure, and some run in memory for lightweight workloads[1]. Most integrations can also connect to an existing index or collection, and the details depend on the integration[1].
Step 4: Run a similarity query
A string query is the simplest option, and similarity_search returns a list of Document objects. The example asks how many distribution centers Nike has in the US[1]:
results = vector_store.similarity_search(
"How many distribution centers does Nike have in the US?"
)
print(results[0])similarity_search_with_score also returns a score. In the example, the score is a distance, so a smaller value means a closer match. Different providers define scores differently, so check the definition before you rely on it[1]. asimilarity_search is the asynchronous version. To search with a vector you computed yourself, use similarity_search_by_vector. For a balance between relevance and variety, maximum marginal relevance search is available[1].
Step 5: Wrap the search as a retriever
A vector store object is not a Runnable, so it does not provide the standard invoke and batch methods. To get them, decorate a function with @chain and call similarity_search inside it. The example uses k=1 and runs a batch of two questions[1]:
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?",
],
)The resulting retriever can feed retrieval-augmented generation (RAG) or other LLM workflows[1].
Common problems
- Weak results: check the splitting settings first. The example uses
chunk_size=1000andchunk_overlap=200, and it keepsadd_start_indexon. Adjust them one at a time for your documents[1]. - Scores that seem reversed: in the example, a smaller score means a closer match[1].
- Choosing a loader: the helper in the example is for demonstration. A project should use a dedicated PDF loader, and LangChain lists several PDF loaders, including
PyPDFLoader[1].
Next steps
Continue with the list of embedding and vector store integrations, or read about retrieval-augmented generation. The example also points to a minimal RAG implementation built on the retriever[1].
Adapted from the Build a semantic search engine with LangChain page of LangChain's open-source documentation[1], released under the MIT License.
Sources
- 1Build a semantic search engine with LangChainLangChain docs (MIT)
Cite this page: Building a knowledge base with LangChain. TellWorld, https://www.tellworld.net/practice/practice-langchain-knowledge-base. Updated 2026-11-14