Skip to content

feat(ingest): ingest unstructured files into BLOB+VECTOR Hudi tables via Hudi Streamer#19278

Draft
vinothchandar wants to merge 3 commits into
apache:masterfrom
vinothchandar:hudi-unstructured-ingest
Draft

feat(ingest): ingest unstructured files into BLOB+VECTOR Hudi tables via Hudi Streamer#19278
vinothchandar wants to merge 3 commits into
apache:masterfrom
vinothchandar:hudi-unstructured-ingest

Conversation

@vinothchandar

Copy link
Copy Markdown
Member

Describe the issue this Pull Request addresses

Landing unstructured data (documents, images, videos) in a lakehouse today means gluing Spark's binaryFile reader to hand-rolled parsing UDFs, with no incremental ingestion, no upsert semantics, no blob management, and no embedding lifecycle. With the BLOB and VECTOR types available since 1.2, HoodieStreamer can offer this as one command: point at a folder on cloud storage, get a queryable Hudi table with parsed text, chunks, and embedding vectors that stay current as files arrive or change.

Summary and Changelog

Two additive pieces in hudi-utilities, no changes to existing behavior:

UnstructuredFileDFSSource (RowSource)

  • Reuses DFSPathSelector for modification-time-checkpointed incremental discovery: each sync ingests only new/changed files; record key = path with modification_time ordering makes re-ingested files upsert in place.
  • Per-record blob placement by configurable size threshold (default 1 MiB): small files store bytes INLINE in the BLOB column; larger files store an OUT_OF_LINE reference to the original file in place. Large-file bytes never enter Spark rows, so memory and shuffle stay bounded regardless of file sizes (--source-limit already caps total inline bytes per batch).
  • Embedded text extraction behind a pluggable DocumentParser; default TikaDocumentParser (Apache Tika, in-process, no services). Parsing never fails the job: per-row parse_status/parse_error record SUCCESS/TRUNCATED/EMPTY/SKIPPED/FAILED. Extracted text is chunked (size/overlap configurable) into a nested chunks column.
  • Only tika-core (~800 KB) ships in the utilities bundles; format parser modules (PDF, Office, ...) are supplied at runtime (e.g. spark-submit --packages org.apache.tika:tika-parsers-standard-package), degrading gracefully to EMPTY parses when absent.

EmbeddingTransformer (chained via --transformer-class)

  • Appends a VECTOR(dimension) column by calling an embeddings API, batching at the record level within each partition (up to batch.size, default 1024, records per request) — executors stay busy, large request batches keep request rates low, and retry with Retry-After-honoring backoff is the only flow control.
  • Pluggable EmbeddingProvider; default targets any OpenAI-compatible /v1/embeddings endpoint (Ollama, TEI, vLLM, OpenAI, Voyage). API keys come from an environment variable named in config, never config values. Errors after bounded retries fail the batch loudly; rows without text (images, videos, failed parses) get a null vector and are never sent to the API.
  • Since only new/changed records flow through each sync, embeddings stay current with the data incrementally.

Also: tika.version property in the root pom; tika-core added to both utilities bundle include lists. No code copied from other projects.

Impact

New optional source + transformer in hudi-utilities; no existing Hudi APIs, configs, storage format, or bundles change behavior. New user-facing configs under hoodie.streamer.source.unstructured.* and hoodie.streamer.transformer.embedding.* (documented via @ConfigClassProperty). Utilities bundles grow by ~800 KB (tika-core).

Risk Level

low — additive code paths only. Verified by 10 offline tests (chunker boundaries, Tika status mapping on generated fixtures, source-level inline/out-of-line + checkpoint semantics, BLOB logical type surviving Row-to-Avro schema conversion, embedding batching/429-retry/fail-fast against a stub HTTP server) plus a streamer E2E writing a Parquet COW table across two syncs (blob struct round-trip for both placements, upsert refresh, vector logical type present in the committed table schema). Additionally validated end to end locally with real PDFs/DOCX/HTML/images, live Ollama embeddings, and hudi_vector_search queries across insert/update/delete cycles.

Documentation Update

Website docs for the new source/transformer configs and a quickstart example are planned as a follow-up PR (config reference tables generate from the config classes). Two notes to include: hudi_vector_search requires spark-mllib on the classpath (present in standard Spark distributions), and Office-format parsing needs a commons-compress version aligned with Tika's POI.

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable

…files as BLOB tables

HoodieStreamer can now point at a DFS folder of arbitrary files (documents,
images, videos) and produce a queryable Hudi table using the 1.2 BLOB type
on plain Parquet base files:

- UnstructuredFileDFSSource (RowSource) reuses DFSPathSelector for
  mtime-checkpointed incremental discovery; each sync ingests only new or
  modified files, and keying on path with modification_time ordering makes
  re-ingested files upsert in place.
- Blob placement is decided per record by a configurable size threshold
  (default 1MiB): small files store bytes INLINE; larger files store an
  OUT_OF_LINE reference to the original file in place (managed=false), so
  large-file bytes never enter Spark rows -- memory and shuffle stay bounded
  regardless of file sizes (source-limit already caps total inline bytes).
- Text extraction runs embedded in executors behind a pluggable
  DocumentParser; the default TikaDocumentParser (Apache Tika, in-process,
  no services) never throws: per-row parse_status/parse_error record
  SUCCESS/TRUNCATED/EMPTY/SKIPPED/FAILED outcomes. Extracted text is chunked
  (size/overlap configurable) into a nested chunks column for retrieval use.
- Only tika-core (~800KB) ships in the utilities bundles; parser modules for
  PDF/Office/etc. are supplied at runtime (e.g. spark-submit packages
  org.apache.tika:tika-parsers-standard-package), degrading gracefully to
  EMPTY parses when absent.

Verified: unit tests for chunker boundaries, parser status mapping on
generated fixtures, source-level inline/out-of-line + checkpoint semantics,
BLOB logical type surviving StructType->Avro conversion, and a streamer E2E
(two syncs into a Parquet COW table: blob struct round-trip both placements,
upsert refresh on file change).
…m an embeddings API

Chains after UnstructuredFileDFSSource (or any source with a text column) to
keep a VECTOR(dimension) embedding column current with ingested data:

- Record-level batching inside each partition: up to batch.size (default
  1024) records' texts per API request, then the next buffer. Executors stay
  busy end to end and large request batches keep the request rate low
  without any parallelism tuning knobs.
- Pluggable EmbeddingProvider; the default calls any OpenAI-compatible
  /v1/embeddings endpoint (Ollama, TEI, vLLM, OpenAI, Voyage) with
  exponential backoff honoring Retry-After on 429/5xx. API keys come from an
  environment variable named in config, never from config values. Errors
  after bounded retries fail the batch loudly -- no silently vectorless rows.
- Rows without text (images, videos, failed parses) get a null vector and
  are never sent to the API.
- VECTOR(dim) metadata is re-attached after the row-encoder round trip (the
  encoder strips StructField metadata), so the writer detects the vector
  column and the logical type lands in the committed table schema. Since the
  streamer feeds only new/changed records per sync, embeddings stay current
  incrementally.

Verified: transformer tests against a stub OpenAI-compatible server
(deterministic batching, null-vector rows, metadata on apply() and
transformedSchema(), 429-retry-then-success, fail-fast on client errors)
plus the streamer E2E extended to assert embedding values, upsert-refreshed
vectors, and the vector logical type in the committed schema.
@vinothchandar vinothchandar changed the title feat(ingest): ingest unstructured files into BLOB+VECTOR Hudi tables via HoodieStreamer feat(ingest): ingest unstructured files into BLOB+VECTOR Hudi tables via Hudi Streamer Jul 13, 2026
@hudi-bot

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

Encoders.row(StructType) only exists on Spark 3.5+, breaking the 3.3/3.4
profile builds. SparkAdapter.getCatalystExpressionUtils().getEncoder covers
every supported Spark line.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL PR with lines of changes > 1000

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants