n4nAI

Ingest PDFs into LlamaIndex with n4n.ai embeddings

A step-by-step llamaindex pdf ingestion n4n.ai tutorial: wire LlamaIndex to an OpenAI-compatible embeddings endpoint and index PDFs into a vector store.

n4n Team4 min read863 words

Audio narration

Coming soon — every post will get a voice note here.

This llamaindex pdf ingestion n4n.ai tutorial shows how to pipe a PDF through LlamaIndex’s reader and embed it with an OpenAI-compatible endpoint. We’ll use n4n.ai’s gateway as the embedding provider because it fronts a wide model catalog behind a single base URL. By the end you’ll have a queryable vector index built from a real document and a clear pattern for scaling to thousands of files.

Step 1: Scaffold the project and install dependencies

Start in a clean directory. LlamaIndex ships as a meta package that pulls in core, the OpenAI embedding integration, and a set of default readers. Isolate the environment so version drift in pypdf doesn’t break parsing later.

mkdir pdf_ingest && cd pdf_ingest
python -m venv .venv && source .venv/bin/activate
pip install llama-index python-dotenv pypdf

pypdf is the underlying parser LlamaIndex uses for PDFs; installing it explicitly avoids silent fallback to a slower pure-Python path. If you plan to ingest scanned PDFs, add pdf2image and a Tesseract binary, but that is out of scope for this walkthrough.

Create a .env file to keep credentials out of source control:

echo "N4N_API_KEY=sk-your-key-here" > .env

The remainder of this llamaindex pdf ingestion n4n.ai tutorial assumes Python 3.10 or newer and that you have a sample PDF at ./sample.pdf. Put any PDF there—an annual report, a product manual, or a research paper works fine.

Step 2: Point LlamaIndex at the n4n.ai embeddings endpoint

LlamaIndex decouples the embedding model from the indexing logic via a global Settings object. The OpenAIEmbedding class speaks the OpenAI /v1/embeddings contract, so any compliant gateway works by overriding api_base.

import os
from dotenv import load_dotenv
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index import Settings

load_dotenv()

Settings.embed_model = OpenAIEmbedding(
    model="text-embedding-3-small",
    api_base="https://api.n4n.ai/v1",
    api_key=os.getenv("N4N_API_KEY"),
)

n4n.ai exposes an OpenAI-compatible endpoint that fronts 240+ models and handles provider degradation automatically, so the same code paths work if you later swap to a different embedding model by changing the model string. The gateway forwards provider cache-control hints, which trims cost on repeated prefix chunks when you re-ingest revised PDFs.

Set a chunk size that matches your downstream retriever. The default 1024 tokens with 20-token overlap is fine for most reports; legal or scientific PDFs often benefit from 512-token chunks because they use dense nomenclature.

from llama_index.core.node_parser import SentenceSplitter

Settings.node_parser = SentenceSplitter(chunk_size=1024, chunk_overlap=20)

Embedding dimension is fixed by the model: text-embedding-3-small returns 1536-dim vectors. If you switch to a local model later, change both model and the vector store configuration accordingly.

Step 3: Load the PDF with LlamaIndex’s file reader

LlamaIndex treats a PDF as a collection of Document objects. The simplest entry point is SimpleDirectoryReader, which selects PDFReader based on extension and yields one Document per page.

from llama_index import SimpleDirectoryReader

documents = SimpleDirectoryReader(input_files=["sample.pdf"]).load_data()
print(f"Loaded {len(documents)} raw page documents")

If you need finer control—say, extracting only pages 3–10 or attaching custom metadata per page—use PDFReader directly:

from pathlib import Path
from llama_index.readers.file import PDFReader

reader = PDFReader()
documents = reader.load_data(file=Path("sample.pdf"))

At this stage the text is raw. LlamaIndex will apply the node_parser from Step 2 during indexing, splitting pages into retrievable nodes. You can inspect a node prematurely to confirm parsing quality:

nodes = Settings.node_parser.get_nodes_from_documents(documents)
print(f"First node preview: {nodes[0].get_content()[:200]!r}")

A empty preview string means the PDF is image-only or encrypted. Decrypt with qpdf or OCR before proceeding.

Step 4: Build and persist the vector index

With documents loaded and embeddings configured, index construction is one call. LlamaIndex batches embedding requests internally; for a 40-page PDF this is a few hundred tokens per node and finishes in seconds.

from llama_index import VectorStoreIndex

index = VectorStoreIndex.from_documents(documents)

To avoid re-embedding on every restart, persist the index to disk. LlamaIndex stores nodes and vectors in a StorageContext.

index.storage_context.persist(persist_dir="./storage")

Reload later without touching the embedding endpoint:

from llama_index import StorageContext, load_index_from_storage

ctx = StorageContext.from_defaults(persist_dir="./storage")
index = load_index_from_storage(ctx)

For production, swap the default in-memory store for a managed vector database (Qdrant, pgvector, Pinecone). The from_documents call stays identical; only the storage_context changes.

Step 5: Query to verify ingestion succeeded

A vector index is useless if the embeddings are empty or the text was mangled. Run a targeted query against content you know exists in the PDF.

query_engine = index.as_query_engine(similarity_top_k=3)
response = query_engine.query("What are the eligibility requirements for a refund?")
print(str(response))

Verification checklist:

  • The response cites specifics from your PDF, not generic filler.
  • response.source_nodes is non-empty and each node’s score is a float between 0 and 1.
  • Running the script twice with a persisted index does not trigger new embedding calls (watch your n4n.ai usage meter or stdout if you enable httpx logging).

Inspect the provenance of the answer programmatically:

for node in response.source_nodes:
    print(node.score, node.metadata.get("page_label"), node.get_content()[:80])

If source_nodes is empty, the PDF likely failed parsing—print documents[0].text to confirm it isn’t empty strings from a scanned image.

Step 6: Production hardening for PDF ingestion

The happy path above hides three issues you will hit in real pipelines.

Metadata propagation

LlamaIndex attaches metadata per document. Stamp a source tag so you can filter later:

for d in documents:
    d.metadata["source"] = "sample.pdf"
    d.metadata["ingested_at"] = "2024-05-01"

Batch size and rate limits

The OpenAIEmbedding client sends embeddings in batches of 10 by default. If your gateway enforces stricter RPM, lower it:

Settings.embed_model.embed_batch_size = 5

Concurrency and fallback

Because the endpoint honors client routing directives, you can pin a specific provider in the header if a model misbehaves:

Settings.embed_model.additional_session_headers = {
    "x-n4n-route": "openai"
}

This is optional; the automatic fallback already covers degraded providers. For large corpora, wrap ingestion in a worker pool and cap concurrent from_documents calls to avoid memory spikes from node buffering.

Verifying end-to-end success

Beyond the query check, confirm the index on disk contains vectors. A quick sanity script:

import json
from llama_index import StorageContext, load_index_from_storage

ctx = StorageContext.from_defaults(persist_dir="./storage")
index = load_index_from_storage(ctx)
print(f"Indexed nodes: {len(index.docstore.docs)}")

If the count matches your expected page chunks (pages × chunks-per-page), ingestion worked. This llamaindex pdf ingestion n4n.ai tutorial deliberately stays single-file; in a larger system you would wrap Steps 3–4 in a worker that watches an object store bucket and writes completion status to a queue.

One last note: LlamaIndex’s PDFReader does not OCR. For image-only PDFs, preprocess with pdftotext or a vision model before ingestion, otherwise you’ll embed empty strings and the query engine will confidently hallucinate. Treat the embedding step as a pure function of text quality—garbage in, garbage retrieved.

Tagsllamaindexpdfingestionn4n-ai

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All llamaindex data connectors & ingestion posts →