feat(ingest): ingest unstructured files into BLOB+VECTOR Hudi tables via Hudi Streamer#19278
Draft
vinothchandar wants to merge 3 commits into
Draft
feat(ingest): ingest unstructured files into BLOB+VECTOR Hudi tables via Hudi Streamer#19278vinothchandar wants to merge 3 commits into
vinothchandar wants to merge 3 commits into
Conversation
…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.
Collaborator
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Describe the issue this Pull Request addresses
Landing unstructured data (documents, images, videos) in a lakehouse today means gluing Spark's
binaryFilereader 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)DFSPathSelectorfor modification-time-checkpointed incremental discovery: each sync ingests only new/changed files; record key =pathwithmodification_timeordering makes re-ingested files upsert in place.--source-limitalready caps total inline bytes per batch).DocumentParser; defaultTikaDocumentParser(Apache Tika, in-process, no services). Parsing never fails the job: per-rowparse_status/parse_errorrecord SUCCESS/TRUNCATED/EMPTY/SKIPPED/FAILED. Extracted text is chunked (size/overlap configurable) into a nestedchunkscolumn.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)VECTOR(dimension)column by calling an embeddings API, batching at the record level within each partition (up tobatch.size, default 1024, records per request) — executors stay busy, large request batches keep request rates low, and retry withRetry-After-honoring backoff is the only flow control.EmbeddingProvider; default targets any OpenAI-compatible/v1/embeddingsendpoint (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.Also:
tika.versionproperty in the root pom;tika-coreadded 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.*andhoodie.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_searchqueries 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_searchrequiresspark-mllibon the classpath (present in standard Spark distributions), and Office-format parsing needs a commons-compress version aligned with Tika's POI.Contributor's checklist