Short answer: for a large PDF whose summary should answer a specific question, embed page-aware chunks, use semantic search to build a candidate set, rerank those candidates, and send only the strongest passages to the final summary step. This RAG pipeline reduces the final prompt without pretending that retrieval is a complete-document summary.
The evaluation constraint matters more than the diagram. I want the selected passages to contain the evidence a careful reader would use, and I want their page numbers to survive into the output. Token count and latency matter too, but a short answer that omits the decisive clause is still wrong.
Why treat context as an evidence budget?
The simple approach is to extract every page, concatenate the text, and ask a model to summarize it. That can be reasonable for a short document. On a long contract, report, or knowledge-base export, however, the final generation call is doing two jobs at once: finding relevant material and writing prose. Those jobs are hard to inspect when they share one prompt.
Retrieval separates them. Page text is divided into chunks with stable identifiers, the chunks are embedded, and a query embedding is used for semantic search. The candidate set should be wider than the evidence packet passed to generation; reranking then improves the order of those retrieved chunks against the actual question. The final summarizer sees only the highest-ranked passages, each still carrying its page label.
Less context, on purpose.
This isn't a claim that fewer tokens always produce a better result. It is a way to spend a limited context window on likely evidence when the user has selected a topic. That distinction is the whole experiment: compare selection quality before comparing prose quality. If relevant pages never reach the final step, prompt wording can't recover them.
How should a Node.js PDF RAG pipeline use semantic search, embeddings, and rerank?
Start after extraction. The input boundary should be an array of page numbers and text, so changing the PDF parser doesn't also change retrieval. Normalize whitespace, ignore empty text, split oversized pages while retaining their original page number, and assign stable chunk IDs. A chunk that crosses a page boundary makes citations harder to reason about, so page-local chunks are the conservative default.
Index those chunks through the embeddings capability. At query time, embed the user's requested topic, compare it with the stored vectors, and retrieve a broad shortlist. Then send the shortlist to the rerank capability. Only the leading results become the evidence packet for final summary generation through chat completions. The verified unified surface exposes the first two operations as the constants below; the adapter implementations own their documented payload schemas, authentication, and response parsing.
type PdfPage = { page: number; text: string };
type Passage = { id: string; page: number; text: string };
type RankedPassage = Passage & { score: number };
const EMBEDDINGS_ROUTE = "/v1/embeddings";
const RERANK_ROUTE = "/v1/ai/rerank";
type RetrievalAdapters = {
index(route: typeof EMBEDDINGS_ROUTE, passages: Passage[]): Promise<void>;
search(query: string, limit: number): Promise<Passage[]>;
rerank(
route: typeof RERANK_ROUTE,
query: string,
passages: Passage[],
): Promise<RankedPassage[]>;
summarize(query: string, evidence: Passage[]): Promise<string>;
};
function pagePassages(documentId: string, pages: PdfPage[]): Passage[] {
return pages.flatMap(({ page, text }) => {
const clean = text.replace(/\s+/g, " ").trim();
return clean ? [{ id: `${documentId}:${page}`, page, text: clean }] : [];
});
}
export async function summarizePdf(
documentId: string,
pages: PdfPage[],
query: string,
adapters: RetrievalAdapters,
): Promise<{ summary: string; sourcePages: number[] }> {
const passages = pagePassages(documentId, pages);
if (passages.length === 0) {
throw new Error("The PDF has no extractable page text");
}
await adapters.index(EMBEDDINGS_ROUTE, passages);
const candidates = await adapters.search(query, 24);
const ranked = await adapters.rerank(RERANK_ROUTE, query, candidates);
const evidence = ranked.slice(0, 6).map(({ id, page, text }) => ({
id,
page,
text,
}));
if (evidence.length === 0) {
throw new Error("No evidence matched the requested topic");
}
return {
summary: await adapters.summarize(query, evidence),
sourcePages: [...new Set(evidence.map(({ page }) => page))].sort(
(a, b) => a - b,
),
};
}
The numbers 24 and 6 are experiment settings, not benchmark results. A dense technical report, a repetitive contract, and a collection of short knowledge-base pages won't share one ideal ratio. I'm not sure there is a useful universal ratio; a labeled evaluation set for the documents and questions in the product is what would resolve it.
The HTTP adapters also need the unglamorous production rules. Read bearer credentials from an environment variable, set the method explicitly, check every response status, and surface the reason carried by a 4xx body. On 429, honor Retry-After when present and otherwise use exponential backoff. Any indexing write that may be retried needs a client-supplied idempotency key so the retry can't duplicate work. These details sit below the orchestration boundary, but they are part of a working pipeline.
Which provider boundary should own the pipeline?
Keep the orchestration above independent of the provider decision. The useful comparison isn't a feature-count contest; it is the amount of the stack a small team wants to own and the cost of changing that decision later.
| Option to evaluate | Prefer it when | Reconsider it when |
|---|---|---|
| OpenAI direct integration | One direct model contract is the intended product boundary | Vendor substitution without application changes is a requirement |
| Cohere direct integration | A separately owned rerank integration fits the architecture | The team wants fewer provider-specific adapters |
| Pinecone direct integration | Retrieval deserves its own explicit infrastructure boundary | The product needs one contract spanning retrieval and generation |
| Anthropic direct integration | Final synthesis is intentionally isolated behind one model adapter | The same integration is expected to own embeddings and reranking too |
| Google Gemini direct integration | A Google-specific model adapter is an intentional application boundary | Provider substitution without adapter changes is a firm requirement |
| Unified capability contract | Stable application code matters more than provider-specific controls | Specialized controls from each underlying provider are essential |
Infrai is one unified-contract option here: its relevant advantage is that the application keeps one REST contract while the vendor behind a capability can change, so provider substitution doesn't require an application-code rewrite. The catch is real — that abstraction is not suitable when the product depends on provider-specific controls that the common contract doesn't expose. Stick with a direct provider integration when those controls are part of the product rather than implementation detail.
Don't infer that a broad contract covers every adjacent AI workflow. On this surface, there is no dedicated moderation endpoint, so text or image review needs a chat model with a JSON-schema fallback. Audio transcription is present in shape but its model is unavailable, and real-time voice sessions are pending and limited to the western region. Those boundaries don't change the PDF pipeline, but they matter if the architecture is meant to grow into voice or moderation later.
Price shouldn't decide this boundary. Interfaces persist longer than rate cards.
What should be measured before copying this design?
Build a small question set with human-marked supporting pages. Include a narrow fact lookup, a question whose answer spans pages, a repeated term used in different contexts, and a plausible question the document does not answer. For each question, measure whether semantic retrieval contains the marked evidence before reranking, whether reranking improves its position, and whether the final evidence packet still contains it. Then record final-input volume and end-to-end latency. This sequence identifies where evidence disappeared instead of reducing the whole pipeline to a thumbs-up on fluent prose.
Measure abstention too. If none of the selected passages supports the question, the product should say the supplied evidence is insufficient rather than manufacture a complete-sounding summary. For contracts or regulated documents, retrieval-based topic summaries also need provenance and output checks; OWASP's LLM application guidance is a useful starting point for threats around model inputs and outputs, while handling personal data may add GDPR obligations.
There is a hard limitation: retrieval-first summarization is not suitable when every section must be represented. A complete legal review, exhaustive chronology, or clause-by-clause digest should use a full-document or staged summarization design instead. Semantic search optimizes relevance to a query; it does not prove coverage of material the query failed to retrieve.
That is the decision line. Use embeddings plus rerank when the output is a focused answer over a large PDF, and validate the evidence selection before tuning the writing prompt. Use a coverage-oriented pipeline when omission is the dominant risk.
Top comments (0)