extract

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package extract turns raw document bytes into extracted text.

Index

Constants

View Source
const DefaultChildMemoryLimit = 512 << 20

DefaultChildMemoryLimit caps an isolation child. Matches the 512 MB used by the reference implementation in design.md §6.

View Source
const DefaultChildTimeout = 60 * time.Second

DefaultChildTimeout bounds one isolated extraction.

View Source
const DefaultMaxDecompressedBytes = 64 << 20 // 64 MiB

DefaultMaxDecompressedBytes bounds what one archive-backed document may expand to. Generous for real documents, ruinous for a zip bomb.

View Source
const DefaultTimeout = 120 * time.Second

DefaultTimeout bounds one extraction call. Generous because OCR on a scanned multi-page PDF is genuinely slow.

Variables

View Source
var ErrUnavailable = errors.New("xberg service unavailable")

ErrUnavailable marks a transport/deployment failure — connection refused, DNS, timeout, or a 5xx from the xberg service itself. It is the only case worth retrying or falling back on; an extraction xberg *rejected* (4xx) is a verdict on the document and is returned as a plain error instead.

View Source
var ErrUnsupportedFormat = errors.New("extract: unsupported format")

ErrUnsupportedFormat marks a document LocalExtractor has no parser for. It is a verdict on the document, not an availability failure, so a Chain stops on it rather than falling through — there is nothing further down the chain that would do better, since local parsing is already the last layer.

Functions

func IsInIsolatedChild

func IsInIsolatedChild() bool

IsInIsolatedChild reports whether this process was spawned as an extraction child. Useful for a host application that wants to skip expensive startup work (opening database pools, joining a cluster) in a process that is only going to parse one file and exit.

func RunIsolatedChildIfInvoked

func RunIsolatedChildIfInvoked()

RunIsolatedChildIfInvoked must be the first statement in a host application's main() if it wants IsolatedExtractor to work:

func main() {
    extract.RunIsolatedChildIfInvoked()
    // ... normal startup
}

In the normal case it returns immediately and does nothing. When the process was spawned as an extraction child it reads one document from stdin, parses it with a LocalExtractor, writes the result to stdout, and **exits without returning** — so nothing after the call runs in a child.

A library cannot arrange this for itself: ragit does not own main(), so it cannot intercept startup the way an application re-invoking its own hidden subcommand can. This one-line requirement is the cost of that.

func SupportedExtensions

func SupportedExtensions() []string

SupportedExtensions lists what LocalExtractor can parse. Deliberately much narrower than xberg's ~101 formats — this is a fallback, not a replacement.

Types

type Chain

type Chain struct {
	// contains filtered or unexported fields
}

Chain tries each extractor in order, falling back to the next one only when the current one was *unavailable*.

The narrowness of that rule is the entire point, and it is the most expensive lesson in docs/design.md §6. A fallback fires on a transport or deployment failure — connection refused, DNS, timeout, a 5xx, a child process that could not be spawned. It does NOT fire when an extractor rejected the document itself: a corrupt PDF, an unsupported type, a clean parser error. Those are verdicts, and a verdict is final.

Retrying a rejected document down the chain means feeding bytes that already broke one parser into progressively less-contained code paths — which is precisely how the OOM incident in §6 happened. A bad document must never buy its way back in.

func NewChain

func NewChain(extractors ...Extractor) *Chain

NewChain builds a Chain from extractors in order of preference. Nil extractors are skipped, so a caller can write

NewChain(xbergOrNil, isolated, local)

without branching on whether the sidecar is configured.

func (*Chain) Extract

func (c *Chain) Extract(ctx context.Context, data []byte, filename string) (*Result, error)

Extract runs the chain.

func (*Chain) Len

func (c *Chain) Len() int

Len reports how many extractors the chain will actually try. Useful for a startup log line: a chain of one is a very different deployment from a chain of three, and the difference is otherwise invisible until something fails.

type Extractor

type Extractor interface {
	Extract(ctx context.Context, data []byte, filename string) (*Result, error)
}

Extractor turns document bytes into text. Implementations decide what a terminal, non-retryable failure looks like versus a transient one — see XbergExtractor and ErrUnavailable.

type IsolatedExtractor

type IsolatedExtractor struct {
	// MemoryLimit caps the child. Zero means DefaultChildMemoryLimit.
	MemoryLimit int64
	// Timeout bounds one extraction. Zero means DefaultChildTimeout.
	Timeout time.Duration
	// contains filtered or unexported fields
}

IsolatedExtractor runs local parsing in a short-lived child process with a memory ceiling and a timeout.

This is the containment layer from design.md §6. Its value is structural rather than parser-specific: a blow-up in *any* parser — including one not yet audited, or a future dependency upgrade — kills a child process that owns nothing, and the parent marks that one document failed and carries on. Without it, the kernel's OOM killer picks a victim host-wide, and the application is merely the most likely one, not the only eligible one.

Wiring requirement

A library cannot re-invoke itself the way an application can: ragit does not own main(). The host application must call RunIsolatedChildIfInvoked as the first statement in main(), which is what makes its binary able to serve as its own extraction child. Without that call, IsolatedExtractor's children start the host's normal startup path instead of parsing, so Extract fails with ErrUnavailable and a Chain falls through to the next layer — degraded, but not broken.

func NewIsolatedExtractor

func NewIsolatedExtractor() *IsolatedExtractor

NewIsolatedExtractor builds an IsolatedExtractor with default limits.

func (*IsolatedExtractor) Extract

func (e *IsolatedExtractor) Extract(ctx context.Context, data []byte, filename string) (*Result, error)

Extract parses data in a capped child process.

Failures are classified per design.md §6's fallback rule:

  • failing to spawn, or a child that died without a verdict (OOM kill, timeout, missing RunIsolatedChildIfInvoked wiring) → ErrUnavailable, so a Chain moves on;
  • a child that parsed and rejected the document → a plain error, so a Chain stops. A bad document does not buy its way into a less-contained parser.

type LocalExtractor

type LocalExtractor struct {
	// MaxDecompressedBytes caps the total bytes read out of a container
	// format (docx's zip). Zero means DefaultMaxDecompressedBytes. This is
	// the zip-bomb guard: a 200 kB .docx can legitimately declare gigabytes
	// of entry content.
	MaxDecompressedBytes int64
}

LocalExtractor parses documents in-process, with no sidecar.

It exists so a deployment that has not stood up an xberg sidecar still works, at a smaller supported format set and with no OCR (design.md §4). That convenience comes with real risk: these parsers run untrusted bytes through code that allocates based on what the file *claims*. A 212 kB PDF driving a parser to ~5 GB is the documented incident behind design.md §6.

So: do not hand a LocalExtractor untrusted uploads directly. Wrap it in an IsolatedExtractor, which runs exactly this code in a memory-capped child process. LocalExtractor is the thing being contained, not the containment.

func NewLocalExtractor

func NewLocalExtractor() *LocalExtractor

NewLocalExtractor builds a LocalExtractor with default limits.

func (*LocalExtractor) Extract

func (e *LocalExtractor) Extract(_ context.Context, data []byte, filename string) (*Result, error)

Extract parses data locally, dispatching on the filename's extension.

type Result

type Result struct {
	// Text is the extracted content, in Markdown.
	Text      string
	PageCount int
	// Metadata is the extractor's own structured output (detected tables,
	// warnings, source language, ...), stored verbatim rather than parsed
	// field-by-field so new fields don't require a schema migration.
	Metadata json.RawMessage
}

Result is what an Extractor produces from one document.

type XbergExtractor

type XbergExtractor struct {
	// BaseURL is the xberg server root, e.g. http://xberg:8000.
	BaseURL string
	// HTTPClient is used for every call. Nil builds a client from Timeout.
	HTTPClient *http.Client
	// Timeout bounds a single extraction. Zero means DefaultTimeout. Ignored
	// when HTTPClient is set.
	Timeout time.Duration
}

XbergExtractor extracts documents through an xberg REST server (`xberg serve`). See https://docs.xberg.io.

func NewXbergExtractor

func NewXbergExtractor(baseURL string, timeout time.Duration) *XbergExtractor

NewXbergExtractor builds an extractor with production defaults.

func (*XbergExtractor) Extract

func (e *XbergExtractor) Extract(ctx context.Context, data []byte, filename string) (*Result, error)

Extract parses data via the xberg service.

func (*XbergExtractor) Health

func (e *XbergExtractor) Health(ctx context.Context) error

Health reports whether the xberg service answers. Meant to be called once at startup so a missing sidecar is a clear log line instead of a surprise on the first upload — it should never block boot.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL