Implementing Node.js Support Triage — Summarize PDF Pages with Embeddings A developer outlines a practical approach for B2B support triage that separates PDF retrieval from answer generation, reranks a small evidence set, and assigns a queue only when cited pages support the decision. The recommended default is embedding search followed by reranking and summarization, with deterministic rules reserved for high-consequence queues. The design emphasizes an evidence contract, independent timeouts, and preserving page identity to avoid citation reconstruction errors. Short answer: for B2B support triage, split PDF retrieval from answer generation, rerank a small evidence set, and let the final summary assign a queue only when the cited pages support that decision. | Design | Triage quality | Request latency | Operational load | Use it when | |---|---|---|---|---| | Embedding search, then summary | Baseline | Lowest of these three | Low | The corpus is narrow and ticket language closely matches the manuals | | Embedding search, rerank, then summary | Better evidence ordering | One extra model step | Medium | Wrong routing costs more than one extra call | | Precomputed issue-to-page rules | Predictable for known cases | Low | High maintenance | The issue taxonomy is stable and auditability outranks recall | The practical default is the middle row. Keep the first row as a latency fallback, and keep deterministic rules for a small set of high-consequence queues. This isn't a model contest. It is a revenue-per-hour decision: spend complexity where a bad handoff creates another support cycle, and outsource the undifferentiated model calls behind three tiny interfaces. Ship the narrow path first. A ticket such as “CSV export drops custom fields after the workspace migration” may share very few words with the page that explains schema mapping. Embedding search is useful because it can retrieve semantically related pages, but retrieval rank is still only a candidate order. The summarizer should not inherit that order as truth. Reranking gives the ticket and candidate pages a second comparison step before generation, which is the part worth paying for when queue selection depends on a precise product condition. For a one-person SaaS, “quality” needs an operational definition. I use an evidence contract rather than a vague score: the output names one queue, cites page identifiers, states what the evidence supports, and returns needs review when the pages do not justify a route. That contract can be checked without pretending that fluent prose is correct. It also keeps weekly shipping possible; a new manual changes indexed content, not the orchestration code. Measure that. Latency has two clocks. The customer sees time to acknowledgement, while the support operator sees time to a trustworthy route. A fast acknowledgement can be sent before enrichment finishes, so the internal triage path does not need to win an artificial race against the first response. Still, cap each stage. Retrieval, reranking, and summarization should have independent timeouts because a single end-to-end timeout won't tell you which budget was exhausted. The catch is that reranking every ticket is wasteful when the first retrieval result is already decisive. Use a policy, not instinct: skip the extra pass only for an exact document or error-code match that your own evaluation set has shown to be reliable. I’m not sure a universal similarity threshold exists across embedding models and corpora; a labeled ticket set from the actual help desk is what resolves that uncertainty. Treat a PDF page as evidence with identity, not as an anonymous string. Preserve the document ID, page number, revision, and text from extraction onward. Then embed the ticket query, retrieve more pages than the final prompt can afford, rerank those candidates against the full ticket, and pass only the best supported pages to the summary step. The final result must carry the selected page IDs back out. That sequence prevents a common architecture error: using the generated summary to reconstruct citations after the fact. Once page identity has been discarded, matching prose back to a source is another fuzzy retrieval problem. Keep the IDs attached instead. Simple. The boundaries matter more than a clever framework. Define three ports: vector search, reranking, and structured generation. Each adapter can call a hosted API, an internal service, or a self-hosted model; the pipeline does not care. It does care about input order, bounded candidate counts, and whether every returned citation belongs to the supplied evidence. The quality gate should be built from representative support tickets, including vague wording, copied error text, stale terminology, and two issues described in one message. Label the acceptable queues and supporting pages. Then compare the embedding-only path with the reranked path on the same cases. Your mileage may vary because manuals, ticket vocabulary, and queue taxonomies differ; publish no generic accuracy claim when the relevant evidence is local. The following code is the orchestration layer. It deliberately owns the request and response contracts while leaving model transport in injected adapters. That keeps the example runnable in tests without baking a commercial endpoint into the application. type PdfPage = { id: string; documentId: string; revision: string; pageNumber: number; text: string; }; type Ticket = { id: string; subject: string; body: string; }; type RetrievedPage = PdfPage & { similarity: number }; type RankedPage = RetrievedPage & { rerankScore: number }; type TriageResult = { queue: "billing" | "data" | "integrations" | "needs review"; summary: string; citedPageIds: string ; confidence: "high" | "medium" | "low"; }; interface SemanticIndex { search query: string, limit: number, signal: AbortSignal : Promise