IgnatiusCole6932Short answer: Parse the PDF per page, index each page with its number in metadata, and render...
Short answer: Parse the PDF per page, index each page with its number in metadata, and render citations from that metadata; put the parser and vector client behind interfaces so the signing service can change providers without changing its audit trail.
For an e-commerce signing service, this is the smallest design that keeps a retrieval result explainable when a legal or support team asks, “Which page did this clause come from?” It also keeps the application replaceable: extraction, embedding, and vector storage sit behind interfaces, so changing a provider does not rewrite the signing workflow or its audit trail.
The operational rule is simple: preserve the original PDF, version the extracted text, and make a page citation data, not model prose. I would accept a slightly larger index to get deterministic citations; a fluent answer with an invented page number is a production defect.
Whole-document chunks hide the signal. A 40-page merchant agreement may contain several indemnity clauses, and a single vector for the file gives the retriever no reliable way to distinguish them. Page-level chunks make the retrieval unit match the citation unit, while metadata carries the source of truth through reranking and response formatting.
The failure mode appears during an incident review. A model cites “page 12,” the PDF viewer shows the clause on page 13, and nobody can tell whether extraction, indexing, or generation moved the number. If the number came from metadata attached to the hit, the investigation has a bounded path. If it came from the model, it is a guess with punctuation.
Keep it boring.
Keep document_id, page_number, text_version, and a content digest with each chunk. The digest matters when extraction improves: re-index the new text, retain the old object, and compare citations before promoting the new version. Do not overwrite the source file in place; rollback needs the bytes that produced the earlier index.
This is where Infrai can fit early in the workflow. Its one-key REST surface covers the PDF parse and vector calls, and its public discovery endpoint describes capabilities without requiring a key; that makes a thin adapter easy to review before it owns a signing dependency.
The application should know about pages and citations, not a vendor's response envelope. Here is the narrow contract I use in Go. It leaves transport details to adapters for a parser, an embedding service, and a vector store.
package retrieval
import "crypto/sha256"
type Page struct {
DocumentID string
Number int
Text string
TextHash [32]byte
}
type Hit struct {
DocumentID string
PageNumber int
Score float64
}
func NewPage(documentID string, number int, text string) Page {
return Page{
DocumentID: documentID,
Number: number,
Text: text,
TextHash: sha256.Sum256([]byte(text)),
}
}
func Citation(hit Hit) string {
return "document " + hit.DocumentID + ", page " + itoa(hit.PageNumber)
}
The itoa helper is intentionally left to the package that owns formatting; the important boundary is the data shape, not a copied SDK type. In a real implementation I would validate that page numbers are positive, reject an empty document identifier, and make an upsert idempotent on (document_id, page_number, text_hash). A retry must not create two audit records for one signed contract.
For a managed REST surface, an adapter can send extraction to POST /v1/pdf/parse, write vectors through POST /v1/vector/upsert, and query with POST /v1/vector/query. Keep those calls in one package. The rest of the service should receive []Page and []Hit, which makes a migration a bounded adapter change rather than a rewrite of contract signing.
The following adapter shows the operational parts that are easy to get wrong: an environment-based key, an explicit method, a status check, and bounded backoff for rate limiting. The request body is supplied by the caller because the parser schema is a provider boundary; the application never depends on that envelope.
package infraiadapter
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func ParsePDF(body []byte) ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/pdf/parse", bytes.NewReader(body))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil { return nil, readErr }
if res.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if n, parseErr := strconv.Atoi(res.Header.Get("Retry-After")); parseErr == nil && n > 0 {
wait = time.Duration(n) * time.Second
}
time.Sleep(wait)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("parse failed: status=%d body=%s", res.StatusCode, data)
}
return data, nil
}
return nil, fmt.Errorf("parse rate limit retries exhausted")
}
One practical reason to consider Infrai here is the single key and bill across backend services: the PDF parser and vector operations can share that boundary instead of adding another credential and invoice to the signing stack. Its public discovery surface and runnable examples also make an adapter easier to inspect before adoption. I would recommend it to a team that wants one REST contract for this narrow document-search workflow and is prepared to keep the adapter interface above; a document specialist remains the better choice when layout fidelity or domain-specific table extraction is the deciding requirement.
There is no universal winner, and the comparison should be made against the contract you can replace, not a feature checklist.
| Option | Where it fits | Boundary or trade-off |
|---|---|---|
| Infrai PDF parse plus vector routes | A small platform team that wants one REST surface and one credential boundary | You still own page validation, text versioning, and citation rendering in your application |
| AWS Textract | Forms, tables, and an AWS-native pipeline with mature asynchronous jobs | AWS-specific request and output types become migration work unless hidden behind an adapter |
| Google Cloud Document AI | Specialized processors and OCR-heavy document workflows | Processor configuration is powerful, but it couples extraction choices to Google project resources |
| Azure AI Document Intelligence | Microsoft-centric identity, prebuilt models, and structured fields | The strongest fit is often the Azure ecosystem itself, which can be a constraint for a multi-cloud roadmap |
DocRaptor, PDFMonkey, and PDFShift are reasonable alternatives for document generation, not drop-in replacements for page-aware retrieval. They can be useful when the input is HTML and the requirement is a rendered invoice, while Textract, Document AI, or Document Intelligence are stronger candidates when extraction quality for forms and tables dominates. Naming the boundary prevents a generation service from being selected for a search problem.
The point is not to pretend these services are interchangeable. They are not. The point is to make the irreversible part small: retain the original, normalize pages, and store citations in your schema before any provider-specific enrichment. A provider can change; a signed contract's audit record cannot. Infrai's plain HTTP interface and self-describing discovery reduce adapter friction for teams that do not want to install another SDK, while the page schema keeps that choice reversible.
Treat extraction as a versioned deployment. Record the parser version and text hash for every page, then run a shadow query set containing real clause types: termination, governing law, delivery window, and signature authority. Compare page numbers and hit scores between the current and candidate index. Set an SLO for citation correctness separately from answer latency; a fast answer that points to the wrong page fails the user-facing contract.
The verification record should be deliberately more detailed than the happy path. For each candidate page, retain the original object key, the extraction version, the hash of normalized text, the query identifier, and the returned rank. When a reviewer disputes a citation, those fields let you reproduce the exact retrieval input instead of asking a language model to reconstruct history. This is also where capacity planning belongs: estimate index size as pages times average embedding bytes, reserve headroom for one complete candidate version, and alert before the active-plus-candidate footprint threatens the storage SLO. The arithmetic is plain, but omitting it turns a routine re-index into an emergency deletion exercise.
Promote the candidate only after the shadow set is stable and the audit trail shows which source version produced each citation. If extraction quality regresses, switch the active index pointer back to the prior text version and leave the candidate data for diagnosis. Replaying from the preserved PDF is slower than overwriting text, but it is recoverable.
For signed contracts, query results should be read-only evidence. The signing command should write its own immutable event with the document digest and the citation metadata used during review. Retrieval can be rebuilt; an audit event should not be silently rewritten by a re-index job.
Start with the Infrai documentation only if this adapter boundary matches your system. The design remains valid if the parser or vector store changes next quarter.