<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[spark-nlp - Medium]]></title>
        <description><![CDATA[Natural Language Understanding Library for Apache Spark. - Medium]]></description>
        <link>https://medium.com/spark-nlp?source=rss----81310c8f4868---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>spark-nlp - Medium</title>
            <link>https://medium.com/spark-nlp?source=rss----81310c8f4868---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 19 May 2026 04:00:29 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/spark-nlp" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Spark NLP 6.3.3: ModernBERT Embeddings, Vector DB Integration, and Layout-Aware Document Processing]]></title>
            <link>https://medium.com/spark-nlp/spark-nlp-6-3-3-modernbert-embeddings-vector-db-integration-and-layout-aware-document-processing-1c0d4d38a174?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/1c0d4d38a174</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Muhammad Abdullah]]></dc:creator>
            <pubDate>Fri, 15 May 2026 19:44:56 GMT</pubDate>
            <atom:updated>2026-05-15T19:44:55.362Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gDyhZBuV1AwSyJl1xBkstQ.png" /></figure><p>Building production NLP pipelines means juggling tradeoffs: embeddings that can’t handle long documents, custom glue code to push vectors into a database, document readers that lose spatial context, and inference engines that can’t carry metadata. <a href="https://github.com/JohnSnowLabs/spark-nlp/releases/tag/6.3.3">Spark NLP 6.3.3</a> addresses all of these in one release.</p><p>This version ships five new capabilities: <strong>ModernBertEmbeddings</strong> for faster, longer-context text embeddings; <strong>VectorDBConnector</strong> for seamless vector database ingestion; <strong>LayoutAlignerForVision</strong> and <strong>LayoutAlignerForText</strong> for layout-aware multimodal document understanding; <strong>MultiColumnAssembler</strong> for merging annotation columns; and enhanced <strong>LightPipeline</strong> with metadata support.</p><p>Here’s a quick overview before we go deep on each feature:</p><h3><strong>TL;DR</strong></h3><ul><li><strong>ModernBertEmbeddings: </strong>8x faster BERT with 8,192-token context and 5x less memory</li><li><strong>VectorDBConnector: </strong>push embeddings straight into Pinecone from your pipeline, no glue code</li><li><strong>LayoutAlignerForVision/Text: </strong>keeps images and text spatially linked when processing PDFs and PPTX files</li><li><strong>MultiColumnAssembler: </strong>merge separate annotation columns into one with source tracking</li><li><strong>LightPipeline Metadata: </strong>pass context like source or category through your inference pipeline</li></ul><h3><strong>ModernBertEmbeddings: 8x Faster, 8192-Token Embeddings</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vFhj1B2ngoN1UFLNc5IE8w.png" /><figcaption>Three ways ModernBERT outperforms classic BERT. Token context window scaled to reflect the full 16× difference.</figcaption></figure><p>If you’ve been working with BERT-based embeddings and hitting walls around sequence length or throughput, ModernBertEmbeddings is the upgrade you’ve been waiting for. Based on the <a href="https://arxiv.org/abs/2412.13663"><em>Smarter, Better, Faster, Longer</em></a> paper, ModernBERT was trained on <strong>2 trillion tokens</strong> and brings three major improvements over classic BERT:</p><ul><li><strong>8x faster inference</strong> through architectural optimizations including Flash Attention, Unpadding, and GeGLU activation</li><li><strong>5x lower memory usage</strong>, enabling larger batches and more cost-effective deployments</li><li><strong>8,192-token native sequence length </strong>eight times the 512-token limit of classic BERT eliminating the need to truncate long documents, legal texts, or code files</li></ul><blockquote>These gains are reported compared to <strong>BERT-base under equivalent benchmark conditions</strong>.</blockquote><p>For proof, see the authors’ benchmark reporting in the ModernBERT paper and the official model card evaluation tables: <a href="https://arxiv.org/abs/2412.13663">arXiv paper</a>, <a href="https://arxiv.org/pdf/2412.13663">PDF</a>, and <a href="https://huggingface.co/answerdotai/ModernBERT-base#evaluation">ModernBERT-base model card</a>.</p><p>ModernBERT produces <strong>768-dimensional</strong> token-level WORD_EMBEDDINGS, making it a drop-in replacement for existing BERT-based embedding stages in your pipelines.</p><h4><strong>Getting Started</strong></h4><p>The default pretrained model is modernbert-base. Here’s how to use it:</p><pre>from sparknlp.base import *<br>from sparknlp.annotator import *<br>from pyspark.ml import Pipeline<br><br>document_assembler = DocumentAssembler() \<br>    .setInputCol(&quot;text&quot;) \<br>    .setOutputCol(&quot;document&quot;)<br><br>tokenizer = Tokenizer() \<br>    .setInputCols([&quot;document&quot;]) \<br>    .setOutputCol(&quot;token&quot;)<br><br>embeddings = ModernBertEmbeddings.pretrained(&quot;modernbert-base&quot;, &quot;en&quot;) \<br>    .setInputCols([&quot;document&quot;, &quot;token&quot;]) \<br>    .setOutputCol(&quot;modernbert_embeddings&quot;) \<br>    .setMaxSentenceLength(8192)<br><br>embeddings_finisher = EmbeddingsFinisher() \<br>    .setInputCols([&quot;modernbert_embeddings&quot;]) \<br>    .setOutputCols([&quot;finished_embeddings&quot;]) \<br>    .setOutputAsVector(True)<br><br>pipeline = Pipeline().setStages([<br>    document_assembler,<br>    tokenizer,<br>    embeddings,<br>    embeddings_finisher<br>])<br><br>data = spark.createDataFrame([[&quot;Spark NLP is an open-source library.&quot;]]).toDF(&quot;text&quot;)<br>result = pipeline.fit(data).transform(data)</pre><pre>result.selectExpr(&quot;explode(finished_embeddings) as result&quot;).show()<br><br>+--------------------+<br>|              result|<br>+--------------------+<br>|[0.27688124775886...|<br>|[-0.4956612586975...|<br>|[0.57157814502716...|<br>|[0.40465933084487...|<br>|[0.00266735162585...|<br>|[0.48091220855712...|<br>|[0.20593170821666...|<br>+--------------------+</pre><h4><strong>Key Parameters</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/575/1*CDQU5QMHIEfOECEQmZvx3g.png" /></figure><h4><strong>Engine Support</strong></h4><p>ModernBertEmbeddings supports <strong>three inference backends</strong>: TensorFlow, ONNX, and OpenVINO. You can import custom HuggingFace ModernBERT models via ONNX using loadSavedModel():</p><pre>embeddings = ModernBertEmbeddings.loadSavedModel(<br>    &quot;/path/to/onnx/model/folder&quot;,<br>    spark<br>)</pre><p>For more examples, including how to import custom HuggingFace models, see the <a href="https://github.com/JohnSnowLabs/spark-nlp/tree/master/examples/python/annotation/text/english/embeddings/ModernBertEmbeddings.ipynb">ModernBertEmbeddings notebook</a>.</p><h3><strong>VectorDBConnector: From Embeddings to Vector Search in One Pipeline</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sM4BCOCJSPlJRZaWSYFIrQ.png" /></figure><p>For teams building <strong>semantic search</strong>, <strong>retrieval-augmented generation (RAG)</strong>, or <strong>similarity-based recommendation</strong> systems, the gap between generating embeddings and storing them in a vector database has always required custom integration code extracting embeddings from DataFrames, formatting payloads, managing batch upserts, and handling API authentication.</p><p><strong>VectorDBConnector</strong> eliminates all of that. It plugs directly into your Spark NLP pipeline and automatically stores embeddings from any Spark NLP embedding annotator into a vector database.</p><h4><strong>How It Works</strong></h4><p>VectorDBConnector takes two input columns a DOCUMENT column and a SENTENCE_EMBEDDINGS column and upserts the embedding vectors to your configured vector database index. It handles batching, ID management, and metadata serialization automatically.</p><pre>from sparknlp.annotator.vector_db import VectorDBConnector<br><br>vectorDB = VectorDBConnector() \<br>    .setInputCols([&quot;document&quot;, &quot;sentence_embeddings&quot;]) \<br>    .setOutputCol(&quot;vectordb_result&quot;) \<br>    .setProvider(&quot;pinecone&quot;) \<br>    .setIndexName(&quot;my-semantic-index&quot;) \<br>    .setNamespace(&quot;production&quot;) \<br>    .setIdColumn(&quot;doc_id&quot;) \<br>    .setMetadataColumns([&quot;text&quot;, &quot;category&quot;]) \<br>    .setBatchSize(100)</pre><h4><strong>Key Parameters</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/753/1*957Uhj278YX1y4GnjAEAMQ.png" /></figure><h4><strong>Configuring Your API Key</strong></h4><p>The Pinecone API key can be set via Spark configuration or an environment variable:</p><pre>import sparknlp<br><br>spark = sparknlp.start(params={<br>    &quot;spark.jsl.settings.vectordb.api.key&quot;: &quot;your-pinecone-api-key&quot;<br>})</pre><h4><strong>Output</strong></h4><p>Each processed row produces an output annotation containing:</p><ul><li><strong>result</strong>: The vector ID (either from your idColumn or a generated UUID)</li><li><strong>metadata</strong>: Includes vectordb_status: “upserted” and provider: “pinecone”</li></ul><h4><strong>A Complete RAG Ingestion Pipeline</strong></h4><p>Here’s a full pipeline that reads documents, generates embeddings, and stores them in Pinecone:</p><pre>from sparknlp.base import *<br>from sparknlp.annotator import *<br>from pyspark.ml import Pipeline<br><br>document = DocumentAssembler() \<br>    .setInputCol(&quot;text&quot;) \<br>    .setOutputCol(&quot;document&quot;)<br><br>sentence = SentenceDetector() \<br>    .setInputCols([&quot;document&quot;]) \<br>    .setOutputCol(&quot;sentence&quot;)<br><br>embeddings = BertSentenceEmbeddings.pretrained() \<br>    .setInputCols([&quot;sentence&quot;]) \<br>    .setOutputCol(&quot;sentence_embeddings&quot;)<br><br>vectorDB = VectorDBConnector() \<br>    .setInputCols([&quot;sentence&quot;, &quot;sentence_embeddings&quot;]) \<br>    .setOutputCol(&quot;vectordb_result&quot;) \<br>    .setProvider(&quot;pinecone&quot;) \<br>    .setIndexName(&quot;my-rag-index&quot;) \<br>    .setMetadataColumns([&quot;text&quot;, &quot;source&quot;]) \<br>    .setBatchSize(100)<br><br>pipeline = Pipeline().setStages([document, sentence, embeddings, vectorDB])<br>pipeline.fit(data).transform(data)</pre><p>For a full walkthrough, check out the <a href="https://github.com/JohnSnowLabs/spark-nlp/tree/master/examples/python/annotation/text/english/vector-db/VectorDBConnector_Pinecone_Demo.ipynb">VectorDBConnector Pinecone Demo notebook</a>.</p><h3><strong>LayoutAlignerForVision and LayoutAlignerForText: Multimodal Document Understanding</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/907/1*tv2Mtmz7WvM4npLS8QL9Rw.png" /></figure><p>When processing rich documents like PDFs or PowerPoint presentations, text and images are spatially interleaved. A revenue chart sits next to the paragraph that discusses it. A product diagram is surrounded by specifications. Without layout awareness, downstream models operating on extracted content lose this spatial context entirely the chart gets separated from its explanation, and the diagram floats without context.</p><p><strong>LayoutAlignerForVision</strong> and <strong>LayoutAlignerForText</strong> solve this problem by forming a two-stage pipeline that preserves the spatial relationship between text and images throughout document processing.</p><h4><strong>Two-Stage Flow (at a glance)</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OPp1Qq_v_2ZAuUC7b2ZxUw.png" /><figcaption><strong>Selective Multimodal Enrichment</strong></figcaption></figure><p><strong>Stage 1: LayoutAlignerForVision: Aligning Images with Text</strong></p><p>LayoutAlignerForVision takes document text chunks and images extracted by ReaderAssembler and aligns each image with its spatially nearby text paragraphs based on actual page coordinates. It uses a distance-based confidence scoring system:</p><ul><li><strong>Distance ≤ 10px</strong>: Confidence 0.95 (very close alignment)</li><li><strong>Distance ≤ paragraph spacing</strong>: Confidence 0.75 (moderate alignment)</li><li><strong>Greater distance</strong>: Confidence 0.40 (loose alignment)</li></ul><p>The annotator is format-aware it understands slide boundaries in PowerPoint files and page boundaries in PDFs, scoping its alignment search to the correct visual context.</p><p>It produces <strong>three output columns</strong> for each alignment:</p><ul><li>&lt;outputCol&gt;_doc: The aligned text chunk</li><li>&lt;outputCol&gt;_image: The aligned image</li><li>&lt;outputCol&gt;_prompt: A captioning prompt ready for a Vision-Language Model (VLM)</li></ul><pre>from sparknlp.reader import LayoutAlignerForVision<br><br>aligner_vision = LayoutAlignerForVision() \<br>    .setInputCols([&quot;data_text&quot;, &quot;data_image&quot;]) \<br>    .setOutputCol(&quot;aligned&quot;) \<br>    .setMaxDistance(40) \<br>    .setIncludeContextWindow(True) \<br>    .setAddNeighborText(True) \<br>    .setImageCaptionBasePrompt(<br>        &quot;Describe the image with concise business details.&quot;<br>    ) \<br>    .setNeighborTextCharsWindow(500) \<br>    .setExplodeDocs(True)</pre><p><strong>Key Parameters:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/788/1*0zk7LNuQFV55ORZpSrBqVQ.png" /></figure><h4><strong>Stage 2: LayoutAlignerForText: Rebuilding Coherent Documents</strong></h4><p>After LayoutAlignerForVision pairs images with text and a VLM generates captions, LayoutAlignerForText weaves those captions back into the document’s text flow. It replaces raw image placeholders with meaningful captions and re-computes character offsets so the resulting document is coherent for downstream NLP tasks like chunking, NER, or embedding.</p><p>The annotator is smart about image placement it classifies each image as “before text” or “after text” based on the image’s position type (inline vs. floating) and its x-coordinate, deduplicates captions that may have been matched to multiple paragraphs, and produces a clean rebuilt document.</p><pre>from sparknlp.reader import LayoutAlignerForText<br><br>aligner_text = LayoutAlignerForText() \<br>    .setInputCols([&quot;aligned_doc&quot;, &quot;image_caption&quot;]) \<br>    .setOutputCol(&quot;aligned_text&quot;) \<br>    .setJoinDelimiter(&quot;\n&quot;) \<br>    .setExplodeElements(False)</pre><p><strong>Key Parameters:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/668/1*IgbN2nfpSACUlSU88EWJoQ.png" /></figure><h4><strong>The Complete End-to-End Pipeline</strong></h4><p>Here’s how all the pieces fit together for a full multimodal document understanding pipeline:</p><pre>from sparknlp.base import *<br>from sparknlp.reader import *<br>from sparknlp.annotator import *<br>from pyspark.ml import Pipeline<br><br># Step 1: Extract text and images from documents<br>reader = ReaderAssembler() \<br>    .setContentType(&quot;application/pdf&quot;) \<br>    .setContentPath(&quot;./pdf-files&quot;) \<br>    .setOutputAsDocument(False) \<br>    .setOutputCol(&quot;data&quot;)<br><br># Step 2: Align images with their nearby text<br>aligner_vision = LayoutAlignerForVision() \<br>    .setInputCols([&quot;data_text&quot;, &quot;data_image&quot;]) \<br>    .setOutputCol(&quot;aligned&quot;) \<br>    .setAddNeighborText(True) \<br>    .setNeighborTextCharsWindow(500) \<br>    .setImageCaptionBasePrompt(<br>        &quot;Describe the image with concise business details.&quot;<br>    )<br><br># Step 3: Caption images using a VLM<br>vlm = AutoGGUFVisionModel.pretrained() \<br>    .setInputCols([&quot;aligned_prompt&quot;, &quot;aligned_image&quot;]) \<br>    .setOutputCol(&quot;image_caption&quot;) \<br>    .setBatchSize(1) \<br>    .setNGpuLayers(99)<br><br># Step 4: Rebuild coherent text with captions woven in<br>aligner_text = LayoutAlignerForText() \<br>    .setInputCols([&quot;aligned_doc&quot;, &quot;image_caption&quot;]) \<br>    .setOutputCol(&quot;aligned_text&quot;) \<br>    .setExplodeElements(False)<br><br># Step 5: Chunk for downstream retrieval<br>text_splitter = DocumentCharacterTextSplitter() \<br>    .setInputCols([&quot;aligned_text&quot;]) \<br>    .setOutputCol(&quot;chunks&quot;) \<br>    .setChunkSize(1200) \<br>    .setChunkOverlap(120)<br><br># Step 6: Embed for vector search<br>embedder = BertSentenceEmbeddings.pretrained() \<br>    .setInputCols([&quot;chunks&quot;]) \<br>    .setOutputCol(&quot;chunk_embeddings&quot;)</pre><p>This pipeline takes raw PDF documents and produces layout-aware, image-captioned text chunks with embeddings ready for semantic search or RAG. For an in-depth walkthrough of building this pipeline with your own documents, read our blog post <a href="https://medium.com/spark-nlp/efficient-document-ingestion-with-layout-aware-annotators-a-case-study-on-mixed-type-documents-8b73d8d02fc9">Efficient Document Ingestion with Layout Aware Annotators: A Case Study on Mixed-Type Documents</a> and see the <a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/examples/python/data-preprocessing/SparkNLP_LayoutAligners_Document_Understanding_Demo.ipynb">LayoutAligners Document Understanding Demo notebook</a>.</p><h3><strong>MultiColumnAssembler: Merge Annotation Columns with One Line</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QzZPKxnVOgGT82_l99H8DA.png" /></figure><p>When using ReaderAssembler to process documents such as PDFs or PPTX files, content is extracted into separate typed columns document_text, document_table, and image-related outputs. But many downstream annotators (like AutoGGUFVisionModel) expect a <strong>single input column</strong>. Previously, bridging this split required custom Spark transformations.</p><p><strong>MultiColumnAssembler</strong> solves this directly within Spark NLP. It merges any number of DOCUMENT type annotation columns into a single output column, preserving all annotation metadata and automatically adding a source_column key to each annotation so you can trace which column it originated from.</p><pre>from sparknlp.base import MultiColumnAssembler<br><br>merger = MultiColumnAssembler() \<br>    .setInputCols([&quot;document_text&quot;, &quot;document_table&quot;]) \<br>    .setOutputCol(&quot;merged_document&quot;) \<br>    .setSortByBegin(True)</pre><h4><strong>Key Parameters</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/710/1*a7mFq2YiqaGH1mhHHBku8A.png" /></figure><h4><strong>How Sorting Works</strong></h4><ul><li><strong>sortByBegin=False (default)</strong>: Annotations appear in input column order all annotations from the first column, then the second, and so on.</li><li><strong>sortByBegin=True</strong>: Annotations from all columns are interleaved by their begin offset, reconstructing the original document order regardless of which column they came from.</li></ul><h4><strong>Source Tracking</strong></h4><p>Every merged annotation gets a source_column metadata key, making it easy to inspect provenance:</p><pre>result.selectExpr(&quot;explode(merged_document) as ann&quot;) \<br>    .selectExpr(&quot;ann.result&quot;, &quot;ann.metadata.source_column&quot;) \<br>    .show(truncate=False)</pre><h4><strong>Integration with ReaderAssembler</strong></h4><p>This is particularly useful when merging text and table outputs from document readers:</p><pre>from sparknlp.reader import ReaderAssembler<br>from sparknlp.base import MultiColumnAssembler<br>from pyspark.ml import Pipeline<br><br>reader = ReaderAssembler() \<br>    .setContentType(&quot;application/pdf&quot;) \<br>    .setContentPath(&quot;./documents&quot;) \<br>    .setOutputAsDocument(False) \<br>    .setOutputCol(&quot;data&quot;)<br><br>merger = MultiColumnAssembler() \<br>    .setInputCols([&quot;data_text&quot;, &quot;data_table&quot;]) \<br>    .setOutputCol(&quot;merged_document&quot;) \<br>    .setSortByBegin(True)<br><br>pipeline = Pipeline().setStages([reader, merger])<br>result = pipeline.fit(emptyDf).transform(emptyDf)</pre><blockquote><strong>Note:</strong> Columns using the AnnotationImage schema (IMAGE-typed columns from ReaderAssembler) are not supported by MultiColumnAssembler.</blockquote><p>For a full walkthrough, see the <a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/examples/python/annotation/text/english/multi-column-assembler/MergingAnnotationColumns_MultiColumnAssembler.ipynb">Merging Annotation Columns notebook</a>.</p><h3><strong>LightPipeline Metadata Support: Context-Aware Inference</strong></h3><p>LightPipeline is the fast, single-machine inference mode in Spark NLP ideal for real-time applications and batch processing without the overhead of full Spark DataFrames. Starting in 6.3.3, LightPipeline now supports passing <strong>metadata columns</strong> alongside text inputs in both annotate() and fullAnnotate().</p><p>This is especially useful when routing or post-processing should behave differently by content type, such as handling <strong>news articles</strong> and <strong>legal documents</strong> with different downstream logic.</p><p>This means you can now attach contextual information document source, language, category, user ID to your inputs and have it flow through the entire annotation pipeline. Annotators that implement the HasLightPipelineAnnotate trait can access this metadata in their beforeAnnotateLight and afterAnnotateLight hooks, enabling context-aware processing, routing, and filtering.</p><h4><strong>Supported Call Signatures</strong></h4><p>Metadata can be passed as a keyword argument or as a positional trailing argument. Both annotate() and fullAnnotate() support the same patterns:</p><p><strong>Single text with metadata:</strong></p><pre>result = light_pipeline.fullAnnotate(<br>    &quot;U.N. official Ekeus heads for Baghdad.&quot;,<br>    metadata={&quot;source&quot;: [&quot;news_article&quot;]}<br>)<br># result[0][&quot;document&quot;][0].metadata<br># → {&quot;sentence&quot;: &quot;0&quot;, &quot;source&quot;: &quot;news_article&quot;}</pre><p><strong>Multiple texts with row-format metadata (list of dicts):</strong></p><pre>results = light_pipeline.annotate(<br>    [&quot;Breaking: Market rally continues&quot;, &quot;New study on climate change&quot;],<br>    metadata=[<br>        {&quot;source&quot;: [&quot;reuters&quot;], &quot;category&quot;: [&quot;finance&quot;]},<br>        {&quot;source&quot;: [&quot;nature&quot;], &quot;category&quot;: [&quot;science&quot;]}<br>    ]<br>)</pre><p><strong>Multiple texts with columnar-format metadata (dict of lists):</strong></p><pre>results = light_pipeline.annotate(<br>    [&quot;Breaking: Market rally continues&quot;, &quot;New study on climate change&quot;],<br>    metadata={<br>        &quot;source&quot;: [&quot;reuters&quot;, &quot;nature&quot;],<br>        &quot;category&quot;: [&quot;finance&quot;, &quot;science&quot;]<br>    }<br>)</pre><h4><strong>PretrainedPipeline Support</strong></h4><p>This feature is also surfaced through PretrainedPipeline:</p><pre>from sparknlp.pretrained import PretrainedPipeline<br><br>pipeline = PretrainedPipeline(&quot;recognize_entities_dl&quot;, lang=&quot;en&quot;)<br><br>result = pipeline.fullAnnotate(<br>    &quot;Google announced a new product.&quot;,<br>    metadata={&quot;source&quot;: [&quot;tech_news&quot;]}<br>)</pre><h4><strong>Validation</strong></h4><p>The implementation includes validation to ensure metadata is well-formed:</p><ul><li>In columnar format, each metadata value must be a list with the same length as the text inputs</li><li>Metadata is only supported for text inputs (not audio, image, or question-answering modes)</li></ul><h3>What else is new</h3><p>The Apache POI dependency used by Spark NLP’s document readers (ReaderAssembler) has been upgraded from <strong>4.1.2 to 5.4.1</strong> (poi-ooxml-full). This upgrade removes deprecated sub-dependencies and ensures compatibility with the latest Office document formats. If you’re processing Word, Excel, or PowerPoint files with ReaderAssembler, you benefit automatically from this upgrade.</p><h3><strong>References</strong></h3><p><strong>Research Papers</strong></p><ul><li><a href="https://arxiv.org/abs/2412.13663">Smarter, Better, Faster, Longer: A Technical Report on ModernBERT</a> The paper behind ModernBertEmbeddings; covers the architecture improvements (Flash Attention, Unpadding, GeGLU, RoPE), training data (2T tokens), and benchmarks vs. classic BERT</li><li><a href="https://arxiv.org/pdf/2412.13663">ModernBERT Paper (PDF)</a> Direct PDF source for the benchmark methodology and performance discussion</li></ul><p><strong>Benchmark Evidence (for the speed/memory claims)</strong></p><ul><li><a href="https://huggingface.co/answerdotai/ModernBERT-base#evaluation">ModernBERT-base model card: Evaluation</a> Official benchmark tables and task-level results</li><li><a href="https://huggingface.co/blog/modernbert">Hugging Face ModernBERT release post</a> Additional context on performance and efficiency claims</li></ul><p><strong>Example Notebooks</strong></p><ul><li><a href="https://github.com/JohnSnowLabs/spark-nlp/tree/master/examples/python/annotation/text/english/embeddings/ModernBertEmbeddings.ipynb">ModernBertEmbeddings : Getting Started</a> Full walkthrough including loading from HuggingFace via ONNX</li><li><a href="https://github.com/JohnSnowLabs/spark-nlp/tree/master/examples/python/annotation/text/english/vector-db/VectorDBConnector_Pinecone_Demo.ipynb">VectorDBConnector: Pinecone Demo</a> End-to-end RAG ingestion pipeline into Pinecone</li><li><a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/examples/python/data-preprocessing/SparkNLP_LayoutAligners_Document_Understanding_Demo.ipynb">LayoutAligners: Document Understanding Demo</a> LayoutAlignerForVision + VLM captioning + LayoutAlignerForText on mixed-type documents</li><li><a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/examples/python/annotation/text/english/multi-column-assembler/MergingAnnotationColumns_MultiColumnAssembler.ipynb">MultiColumnAssembler: Merging Annotation Columns</a> Merging document_text and document_table from ReaderAssembler into a single column</li></ul><p><strong>Blog Posts</strong></p><ul><li><a href="https://medium.com/spark-nlp/efficient-document-ingestion-with-layout-aware-annotators-a-case-study-on-mixed-type-documents-8b73d8d02fc9">Efficient Document Ingestion with Layout Aware Annotators: A Case Study on Mixed-Type Documents</a> In-depth walkthrough of the full multimodal document pipeline on real PDF and PPTX files</li><li><a href="https://medium.com/spark-nlp">Spark NLP on Medium</a> All Spark NLP articles and tutorials</li></ul><p><strong>Pre-trained Models</strong></p><ul><li>ModernBERT-base on HuggingFace (<a href="https://huggingface.co/answerdotai/ModernBERT-base">answerdotai/ModernBERT-base</a>) The upstream model behind modernbert-base in Spark NLP</li><li><a href="https://sparknlp.org/models">Spark NLP Models Hub</a> Browse all available pretrained models for Spark NLP</li></ul><p><strong>External Services</strong></p><ul><li><a href="https://www.pinecone.io/">Pinecone</a> Vector database supported by VectorDBConnector in this release</li></ul><h3><strong>Community &amp; Resources</strong></h3><ul><li><a href="https://join.slack.com/t/spark-nlp/shared_invite/zt-198dipu77-L3UWNe_AJ8xqDk0ivmih5Q"><strong>Slack</strong></a> real-time discussion with the Spark NLP community and team</li><li><a href="https://github.com/JohnSnowLabs/spark-nlp"><strong>GitHub</strong></a> issue tracking, feature requests, and contributions</li><li><a href="https://github.com/JohnSnowLabs/spark-nlp/discussions"><strong>Discussions</strong></a> community ideas and showcases</li><li><a href="https://medium.com/spark-nlp"><strong>Medium</strong></a> latest Spark NLP articles and tutorials</li><li><a href="https://www.youtube.com/@Johnsnowlabs"><strong>YouTube</strong></a> educational videos and demos</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1c0d4d38a174" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/spark-nlp-6-3-3-modernbert-embeddings-vector-db-integration-and-layout-aware-document-processing-1c0d4d38a174">Spark NLP 6.3.3: ModernBERT Embeddings, Vector DB Integration, and Layout-Aware Document Processing</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Efficient Document Ingestion with Layout Aware Annotators: A Case Study on Mixed-Type Documents]]></title>
            <link>https://medium.com/spark-nlp/efficient-document-ingestion-with-layout-aware-annotators-a-case-study-on-mixed-type-documents-8b73d8d02fc9?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/8b73d8d02fc9</guid>
            <category><![CDATA[layout-aware]]></category>
            <category><![CDATA[multimodal]]></category>
            <category><![CDATA[retrieval-augmented-gen]]></category>
            <category><![CDATA[document-ingestion]]></category>
            <category><![CDATA[rag-ingestion]]></category>
            <dc:creator><![CDATA[Danilo Burbano]]></dc:creator>
            <pubDate>Tue, 10 Mar 2026 14:22:28 GMT</pubDate>
            <atom:updated>2026-03-10T14:22:26.622Z</atom:updated>
            <content:encoded><![CDATA[<h3>The RAG Ingestion Problem</h3><p>In real-world RAG systems, the quality of the final answer is constrained by the quality of the indexed representation. If the ingestion layer fails to capture the meaning encoded in charts, diagrams, screenshots, tables, legends, and other layout-dependent visual artifacts, the retriever is not operating over the true document semantics. It is operating over an incomplete surrogate of the source page.</p><p>That failure mode is especially pronounced in multimodal business and technical documents. Consider a quarterly revenue report that contains a paragraph introducing a chart, but where the actual trend, inflection point, or category comparison only appears in the figure. A text only pipeline will embed the surrounding prose and perhaps some OCR fragments, yet it will often miss the precise visual claim that a human reader immediately extracts from the chart. In a RAG setting, that means relevant chunks may never be retrieved for questions such as <em>Which product line declined in Q3?</em> or <em>What trend is shown in the revenue breakdown?</em> even though the answer is visually obvious on the page.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/986/1*sJAQ7FJktB2JH7zokwC9sg.png" /></figure><p>Clinical and scientific documents introduce another variant of the same issue. Endpoint plots, cohort diagrams, treatment arm schemas, and adverse event tables often encode the most decision relevant information in highly structured visual form. If those artifacts are not semantically reconstructed during ingestion, a RAG system may retrieve a general summary paragraph while overlooking the image that actually contains the efficacy pattern, patient-group distinction, or safety signal needed to answer the user question precisely.</p><p>In other words, multimodal RAG does not fail only at generation time. It often fails much earlier, during ingestion, when visually grounded meaning is discarded or flattened into weak OCR text. Once that information is absent from the index, prompt engineering and reranking can only compensate so much.</p><h3><strong>Layout-Aware Multimodal Ingestion</strong>.</h3><p>At enterprise scale, document ingestion rarely happens over a clean corpus of plain text files. Real world knowledge bases are usually composed of heterogeneous, mixed-type assets such as PDFs, PPTX decks, DOCX reports, technical summaries, clinical dossiers, financial statements, and slide based architecture reviews. These assets are multimodal by construction: they combine narrative text with charts, diagrams, screenshots, tables, icons, and other layout-dependent visual artifacts whose semantic contribution is often critical to downstream retrieval quality.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/841/1*bpMTx2wCAf9c5hbBMkRuCw.png" /><figcaption><strong>Multimodal Document</strong></figcaption></figure><p>When teams deploy ingestion pipelines for these corpora, they typically fall into one of two sub-optimal patterns:</p><ol><li>Push everything through text centric parsing and embedding pipelines, effectively treating the document as if its machine readable text were the whole signal.</li><li>Over correct by sending entire documents through vision language models (VLMs), even when most pages are predominantly textual and only a small subset of regions actually require visual interpretation.</li></ol><p>Both strategies create avoidable failure modes.</p><p>Text-only ingestion pipelines are computationally efficient, but they systematically under represent visually encoded meaning, especially in charts, topology diagrams, annotated screenshots, and figure-heavy reports.</p><p>Full document VLM ingestion captures more multimodal context, but it is operationally expensive, introduces unnecessary latency, and allocates vision inference to document regions that are already well handled by OCR and standard NLP components.</p><p>A more robust design pattern is <strong>layout-aware selective multimodal ingestion</strong>. Instead of captioning the entire document, the pipeline first identifies the non-text visual regions that actually require multimodal interpretation, aligns those regions with their nearest textual context, prompts the VLM with localized semantic grounding, and then reconstructs the document so that the generated image understanding is reinserted into the final reading flow. This produces a retrieval ready representation that is both semantically richer and substantially more efficient than a brute force multimodal pass.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*onytk2EVnjtkxGmYd4vsNg.png" /><figcaption><strong>Layout-Aware Selective Multimodal Ingestion</strong></figcaption></figure><p>This is precisely the problem space addressed by Spark NLP’s new <strong>LayoutAlignerForVision</strong> and <strong>LayoutAlignerForText</strong> annotators. Together, they provide an end-to-end mechanism for aligning extracted text and image annotations, generating context-aware captions only where needed, and rebuilding coherent multimodal document text for downstream chunking, embedding, indexing, and retrieval workflows.</p><h3><strong>Methodology</strong></h3><p>The core methodology follows a <strong>selective multimodal enrichment</strong> architecture designed for retrieval and indexing pipelines rather than generic document captioning. Conceptually, the workflow separates document understanding into two stages:</p><ul><li>Identify and align the regions that require vision reasoning</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*byExk8_t9xr_E3pkC5027w.png" /><figcaption><strong>Selective Multimodal Enrichment</strong></figcaption></figure><ul><li>Propagate the resulting visual semantics back into a text centric representation that can be consumed by conventional embedding and search infrastructure.</li></ul><p>The processing path can be summarized as follows:</p><p>1. Ingest each document and extract both text annotations and image annotations.</p><p>2. Apply layout-aware alignment so that each relevant image is paired with the nearest textual region using positional heuristics.</p><p>3. Optionally enrich the image caption prompt with neighboring textual context by enabling:</p><pre>addNeighborText=True<br>neighborTextCharsWindow=&lt;scope size&gt;</pre><p>4. Run VLM captioning only on those aligned image regions instead of the full document.</p><p>5. Reconstruct the document by reinserting captioned visual meaning into the surrounding text flow with <strong>LayoutAlignerForText</strong>.</p><p>6. Split the reconstructed multimodal text into retrieval sized chunks.</p><p>7. Generate dense sentence or document embeddings for each chunk.</p><p>8. Hand off the chunk text, vectors, and metadata to Elasticsearch for downstream vector indexing.</p><p>From a systems perspective, the important design choice is that multimodal inference is applied <strong>surgically</strong>, not globally. This reduces VLM utilization to the subset of content where it adds actual value, while preserving the strong throughput and distributed execution characteristics of Spark based text processing. The result is a retrieval oriented representation that retains chart and figure semantics without paying the cost of end-to-end VLM processing across all pages.</p><h3><strong>Why LayoutAligners Matter?</strong></h3><p>The key innovation in this approach is not merely that images are captioned, but that they are captioned <strong>in layout context</strong>. In rich documents, the meaning of a visual artifact is rarely self-contained. A chart may depend on the title above it, the explanatory paragraph below it, or the KPI definitions introduced in the previous section. A network diagram may only become interpretable when paired with adjacent architectural prose. Captioning such images in isolation often produces generic or weak descriptions that are insufficient for high-quality semantic retrieval.</p><p><strong>LayoutAlignerForVision</strong> addresses this by aligning <em>DOCUMENT</em> and <em>IMAGE</em> annotations through layout-aware heuristics and emitting three derived outputs from the configured output column.</p><p><strong>LayoutAlignerForText</strong> completes the second half of the workflow by rebuilding coherent text from document chunks and generated captions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*223a5G8MDvzb1go6F4wuBw.png" /><figcaption><strong>Inputs/Outputs for LayoutAlignerForVision, VLM, and LayoutAlignerForText</strong></figcaption></figure><p>Taken together, these annotators convert what is usually a disconnected multimodal intermediate state into a deterministic and retrieval friendly document representation. That is the architectural reason they matter: they bridge the gap between layout parsing and downstream semantic indexing.</p><h3><strong>Implementation</strong></h3><p>This walk through follows the pipeline structure demonstrated in the notebook and frames it as a production oriented ingestion pattern rather than a one-off captioning demo. The implementation can be thought of as four major phases: extraction, alignment, caption generation, and reconstruction for embeddings</p><h4><strong>1) Ingestion with ReaderAssembler</strong></h4><pre> ReaderAssembler()<br>   .setContentType(&quot;application/pdf&quot;)<br>   .setContentPath(pdf_directory)<br>   .setOutputAsDocument(False)<br>   .setOutputCol(&quot;data&quot;)<br>   .setUseEncodedImageBytes(True)</pre><p>The ingestion layer starts with <strong><em>ReaderAssembler</em></strong>, which parses the source files and emits structured text and image annotations instead of collapsing the entire document into a monolithic string. In this configuration, the assembler is reading PDFs and generating separate annotation streams such as <em>data_text</em> and <em>data_image</em>. That distinction is fundamental, because the downstream alignment stage relies on having explicit document chunks and image objects available as first-class annotations rather than implicit artifacts buried inside a raw payload. The notebook also enables encoded image bytes so that the extracted visual regions can be passed directly into downstream vision inference without an additional serialization or image reconstruction step.</p><h4><strong>2) Layout-aware image-text pairing</strong></h4><p>Default mode (no neighbor text):</p><pre> LayoutAlignerForVision()<br>  .setInputCols([&quot;data_text&quot;, &quot;data_image&quot;])<br>  .setOutputCol(&quot;aligned&quot;)</pre><p>Neighbor-aware mode:</p><pre>LayoutAlignerForVision()<br>    .setInputCols([&quot;data_text&quot;, &quot;data_image&quot;])<br>    .setOutputCol(&quot;aligned&quot;)<br>    .setAddNeighborText(True)<br>    .setNeighborTextCharsWindow(500)</pre><p>This stage is where layout intelligence enters the pipeline. <strong><em>LayoutAlignerForVision</em></strong> consumes <em>DOCUMENT</em> and <em>IMAGE</em> annotations and applies proximity based heuristics to determine which text region should serve as the semantic anchor for each image. According to the implementation, the alignment logic considers distance, paragraph geometry, slide or page scope, optional contextual windows for floating images, and a confidence model derived from relative vertical distance. The annotator can also fall back to same slide or same page strategies when a strict local match is not found, which makes it more resilient across variable layouts such as presentations, PDF reports, and mixed visual documents.</p><p>The aligned outputs are: <em>aligned_doc</em>, <em>aligned_image</em>, <em>aligned_prompt</em></p><p>These outputs are important because they enforce a structured data transition between layout alignment and VLM inference, replacing manual prompt assembly with a formalized process. The <em>aligned_prompt</em> column is not just a static instruction string; when neighbor text is enabled, it becomes a localized captioning prompt that blends a base instruction with surrounding textual context. In practice, this means a chart can be captioned with awareness of nearby business, scientific, or technical narrative, which substantially improves grounding quality for visuals whose meaning depends on local prose rather than pixel content alone. The notebook explicitly demonstrates this difference by comparing a default prompt path against a neighbor-aware prompt path that injects up to 500 characters of surrounding context.</p><h4><strong>3) VLM captioning only where needed</strong></h4><pre> AutoGGUFVisionModel.pretrained()<br>    .setInputCols([&quot;aligned_prompt&quot;, &quot;aligned_image&quot;])<br>    .setOutputCol(&quot;image_caption&quot;)</pre><p>At this point the pipeline hands only the aligned image regions, plus their layout informed prompts, to the VLM. This is the operational efficiency win of the overall design. The model is not asked to reinterpret entire pages or complete documents; it is asked to caption the specific image regions that survived alignment and confidence filtering. The demo notebook wraps this step with a helper builder for <strong><em>AutoGGUFVisionModel</em></strong> so that inference parameters such as <em>batchSize</em>, <em>nCtx</em>, <em>nPredict</em>, <em>temperature</em>, <em>topK</em>, and <em>topP</em> remain consistent across experiments, making it easier to isolate the effect of layout aware prompt construction.</p><p>From a machine learning systems perspective, this selective captioning pattern is much closer to how one would design a production ingestion service. It conserves GPU budget, reduces unnecessary multimodal tokens, improves throughput, and keeps vision inference bounded to the document subregions that are most likely to alter retrieval semantics.</p><h4><strong>4) Reassemble text and image meaning</strong></h4><p>Once captions are generated, <strong><em>LayoutAlignerForText</em></strong> reconstructs a text centric representation of the document in which visual meaning is inserted back into the reading flow. In the demo notebook this stage is fed with <em>data_text</em> and <em>image_caption</em>, while the annotator implementation itself is explicitly designed for aligned document caption reconstruction. The important outcome is that the final <em>aligned_text</em> column is no longer plain extracted text; it is a semantically enriched document representation that incorporates the informational payload of previously non-textual regions.</p><p>Internally, the annotator rebuilds text at the element or file scope by pairing document chunks with captions, normalizing layout metadata, deduplicating repeated image assignments, deciding whether captions should be inserted before or after paragraph text, and optionally merging all rebuilt elements into a single file level annotation. This reconstruction step is what makes the downstream embedding stage materially better: instead of embedding isolated paragraphs and separately storing opaque image captions, the system embeds a unified multimodal text stream whose semantics better reflect how a human reader would interpret the source document.</p><h4><strong>5) Split into chunks and generate sentence embeddings</strong></h4><pre>DocumentCharacterTextSplitter()<br>   .setInputCols([&quot;aligned_text&quot;])<br>   .setOutputCol(&quot;chunked_docs&quot;)<br>   .setChunkSize(1200)<br>   .setChunkOverlap(120)<br><br>BertSentenceEmbeddings.pretrained(&quot;sent_small_bert_L2_128&quot;, &quot;en&quot;)<br>   .setInputCols([&quot;chunked_docs&quot;])<br>   .setOutputCol(&quot;chunk_embeddings&quot;)<br><br>EmbeddingsFinisher()<br>   .setInputCols([&quot;chunk_embeddings&quot;])<br>   .setOutputCols([&quot;finished_embeddings&quot;])<br>   .setOutputAsVector(True)</pre><p>After multimodal reconstruction, the document is returned to a familiar retrieval pipeline. <strong><em>DocumentCharacterTextSplitter</em></strong> partitions the enriched document into index sized chunks, and <strong><em>BertSentenceEmbeddings</em></strong> converts each chunk into a dense vector representation suitable for semantic search. <strong><em>EmbeddingsFinisher</em></strong> then materializes the embedding annotations into vector form so they can be persisted in tabular or search-index-ready structures. In the demo notebook, the resulting dataset is flattened to one row per chunk so that each chunk can map cleanly to an indexable unit containing <em>chunk_text</em>, metadata, and its embedding vector. The sequencing here is critical. The chunking happens <strong>after</strong> the layout aware multimodal reconstruction, not before it. That ordering ensures the embedding model sees a coherent semantic unit in which visual meaning has already been grounded and merged, rather than forcing retrieval to correlate independent text chunks and disconnected caption artifacts later in the pipeline.</p><h4><strong><em>6) Indexing hand-off (next step)</em></strong></h4><p>This post intentionally stops before the Elasticsearch write path, but the operational hand-off is straightforward. The next stage is to send: <em>chunk_text</em>, <em>embedding_vector</em>, <em>chunk, </em>and<em> source </em>metadata into an Elasticsearch vector index, where the chunks can participate in k-NN search, hybrid lexical-semantic retrieval, filtered retrieval, or downstream re-ranking pipelines. The important point is that the <strong>LayoutAligners</strong> do not replace the retrieval stack; they improve the semantic quality of the content being indexed into it.</p><h3><strong>Representative Use Cases</strong></h3><p>Although the notebook demonstrates the workflow on a small sample of three PDFs, the pattern generalizes well across several high value enterprise scenarios. The included demo examples highlight financial reports, clinical trial summaries, and cloud architecture documents, each of which contains high density visual content whose meaning depends heavily on adjacent text.</p><p>In financial reporting, charts, KPI summaries, and annotated performance graphics often encode the most retrieval worthy facts in the document. Without layout aware captioning, a vector index may capture the narrative commentary but miss the visual explanation of revenue trend inflections, category breakdowns, or quarter-over-quarter variance. Aligning charts with nearby explanatory paragraphs improves the probability that downstream search will retrieve chunks containing both the business narrative and the visual evidence.</p><p>In clinical and life sciences content, endpoint plots, cohort flow diagrams, and adverse event tables are especially sensitive to contextual grounding. A caption generated without surrounding text may correctly identify that an image is a graph or table, yet still miss the medically salient variables, treatment groups, or outcome semantics that make the visual useful during evidence retrieval. Local neighbor text helps constrain the caption toward domain relevant interpretation.</p><p>In cloud and software architecture documentation, topology diagrams, service dependency visuals, and deployment schematics are often more informative than the prose alone. By aligning those diagrams to nearby technical paragraphs before caption generation, the reconstructed text can better preserve system boundaries, component relationships, and infrastructure intent, making architecture search and RAG-based troubleshooting more precise.</p><h3><strong>Benefits</strong></h3><p>This layout aware ingestion pattern produces several concrete benefits for mixed-type corpora.</p><ul><li><strong>Higher multimodal efficiency:</strong> Vision inference is targeted at image regions that actually need semantic interpretation, rather than being wasted on full pages whose dominant signal is already present in extracted text. This makes the approach more cost-efficient and more production friendly for large scale corpora.</li><li><strong>Better semantic grounding: </strong>Neighbor aware prompts enable captions to incorporate local narrative context, which is particularly valuable for charts, diagrams, and screenshots whose meaning is not fully recoverable from pixels alone. The result is caption output with higher contextual fidelity and stronger downstream retrieval utility.</li><li><strong>Stronger reconstructed text for embeddings: </strong>Because <strong><em>LayoutAlignerForText</em></strong> reinserts captioned visual meaning into the document reading order, the chunks that reach the embedding model are semantically more complete. This improves the representation quality of retrieval units without forcing the rest of the search stack to become natively multimodal.</li><li><strong>Spark-native pipelines: T</strong>he pattern is still a standard Spark pipeline and therefore inherits Spark’s distributed execution model for large document batches. In other words, the design scales not because the VLM sees bigger documents, but because the overall ingestion DAG remains partitionable, pipeline based, and aligned with Spark execution semantics</li><li><strong>Cleaner downstream indexing: </strong>By the time the data reaches Elasticsearch or any other vector backend, each row can represent a retrieval-ready chunk with aligned text, grounded caption semantics, and associated metadata. This leads to a cleaner interface between document understanding and search infrastructure, which is particularly valuable in production RAG and enterprise search systems where observability and deterministic pre-processing matter. For large ingestion workloads, this becomes a high-leverage design pattern: apply multimodal reasoning only where it is information bearing, then collapse the result back into a text first representation that standard embedding and search systems can consume efficiently.</li></ul><h3><strong>Conclusion</strong></h3><p>For mixed-type document ingestion, the architectural goal should not be to maximize vision usage, but to maximize <strong>useful multimodal signal per unit of compute</strong>. That is the broader lesson behind <strong>LayoutAligners</strong>. They offer a middle path between two extremes: the semantic blind spots of text-only ingestion and the computational overkill of sending whole documents to VLMs.</p><p>In practice, the workflow is straightforward but powerful: detect non-text content, align it to nearby text, caption it with localized context, merge the resulting visual semantics back into the reading flow, and only then split and embed the final representation for search. The end result is a corpus that is not merely parsed, but <strong>semantically reconstructed</strong> for retrieval.</p><p>For data scientists, machine learning engineers, and AI platform teams, this is the practical value proposition. <strong><em>LayoutAlignerForVision</em></strong> and <strong><em>LayoutAlignerForText</em></strong> do not just add two new annotators to Spark NLP; they introduce a more disciplined ingestion pattern for multimodal enterprise content. That pattern improves semantic completeness, constrains multimodal inference cost, and creates a stronger foundation for vector indexing, RAG, document understanding, and large-scale search over rich business and technical content.</p><h3>Do you want to know more?</h3><ul><li>Read a related story of document ingestion with Spark NLP <a href="https://medium.com/spark-nlp/evaluating-document-ai-frameworks-spark-nlp-vs-unstructured-for-large-scale-text-processing-0d50874982cd">here</a></li><li>Check the example notebooks in the Spark NLP repository, available <a href="https://github.com/JohnSnowLabs/spark-nlp/blob/release/633-release-candidate/examples/python/data-preprocessing/SparkNLP_LayoutAligners_Document_Understanding_Demo.ipynb">here</a></li><li>Visit <a href="https://www.johnsnowlabs.com/">John Snow Labs</a> and <a href="https://nlp.johnsnowlabs.com/">Spark NLP Technical Documentation</a> websites</li><li>Follow us on Medium: <a href="https://medium.com/spark-nlp">Spark NLP</a> and <a href="https://vkocaman.medium.com/">Veysel Kocaman</a></li><li>Write to support@johnsnowlabs.com for any additional requests you may have</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8b73d8d02fc9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/efficient-document-ingestion-with-layout-aware-annotators-a-case-study-on-mixed-type-documents-8b73d8d02fc9">Efficient Document Ingestion with Layout Aware Annotators: A Case Study on Mixed-Type Documents</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Evaluating Document AI Frameworks: Spark NLP vs Unstructured for Large-Scale Text Processing]]></title>
            <link>https://medium.com/spark-nlp/evaluating-document-ai-frameworks-spark-nlp-vs-unstructured-for-large-scale-text-processing-0d50874982cd?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/0d50874982cd</guid>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[data-ingestion]]></category>
            <category><![CDATA[unstructured-data]]></category>
            <category><![CDATA[spark]]></category>
            <category><![CDATA[information-extraction]]></category>
            <dc:creator><![CDATA[Danilo Burbano]]></dc:creator>
            <pubDate>Mon, 12 Jan 2026 13:07:07 GMT</pubDate>
            <atom:updated>2026-01-12T13:07:06.313Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>Problem1: Extracting Complete Text Coverage from Complex Documents</strong></h3><p>In many enterprise pipelines from compliance auditing to enterprise search and knowledge-graph building teams must extract every piece of visible text from large collections of mixed documents (PDFs, HTML pages, and DOCX files).<br> This includes not just paragraphs and headings, but also text found in:</p><ul><li>Navigation menus and footers</li><li>Captions and embedded annotations</li><li>Tables, figure titles, disclaimers, and metadata fields</li></ul><p>Capturing all visible text is essential when building <strong>traceable, auditable corpora</strong> where any omission (even from navigation or footer content) could lead to information loss or compliance gaps.</p><h4><strong>How Spark NLP Solves It</strong></h4><p>Spark NLP provides a unified data-processing and NLP pipeline that can read, parse, and clean text from diverse formats at scale using its Readers2X components.</p><p>To clean text extracted from HTML using Spark NLP, we leveraged the following annotators:</p><pre>reader2doc = Reader2Doc() \<br>    .setContentType(&#39;text/html&#39;) \<br>    .setContentPath(directory) \<br>    .setOutputCol(&#39;document&#39;)<br><br>normalizer = DocumentNormalizer() \<br>    .setInputCols([&#39;document&#39;]) \<br>    .setOutputCol(&#39;normalized&#39;) \<br>    .setAutoMode(&quot;HTML_CLEAN&quot;) \<br>    .setPatterns([(&quot;:&quot;)])<br><br>sentence_detector = SentenceDetectorDLModel() \<br>    .pretrained() \<br>    .setInputCols([&#39;normalized&#39;]) \<br>    .setOutputCol(&#39;sentences&#39;) \<br>    .setExplodeSentences(True)</pre><p>When processing large volumes of documents, we simply leverage Spark’s native distributed engine to scale efficiently:</p><pre>pipeline = Pipeline(stages=[reader2doc, normalizer, sentence_detector])<br>model = pipeline.fit(empty_df)<br>result_df = model.transform(empty_df)<br><br>flat_df = (<br>    result_df<br>    .withColumn(&quot;sentence&quot;, explode(&quot;sentences&quot;))<br>    .select(<br>        col(&quot;filename&quot;),<br>        col(&quot;sentence.result&quot;).alias(&quot;result&quot;)<br>    )<br>)</pre><ul><li><strong>Complete coverage:</strong> Reader2Doc extracts the full visible text layer, including navigation menus, headers, and footers. It does not force semantic filtering or heuristics that skip content.</li><li><strong>Scalable processing:</strong> Built on Apache Spark, it can handle millions of files distributed across clusters, ensuring fast ingestion and consistent structure.</li><li><strong>Unified pipeline:</strong> The extracted text can flow directly into tokenizers, sentence detectors, embeddings, or downstream NLP models without reformatting.</li><li><strong>Traceability:</strong> Every document keeps metadata such as source path, page number, and character offsets, supporting audit and compliance needs.</li></ul><p>This makes Spark NLP particularly strong for <strong>enterprise-scale ingestion</strong>, full-text indexing, and document alignment tasks where completeness and consistency outweigh minimalism.</p><h4>How Unstructured Handles It</h4><p>Unstructured’s partition is designed with a different philosophy: to extract <strong>semantic content</strong>. They segment a document into structured “elements” e.g. Titles, NarrativeText, Tables, etc. while discarding what appears to be <em>boilerplate</em>, such as &lt;nav&gt; menus or repetitive links.</p><p>To clean text extracted from HTML using Unstructured, we relied on its built-in cleaning utilities and added a custom function to remove colon characters:</p><pre>def remove_colons(text: str) -&gt; str:<br>    return re.sub(r&quot;:&quot;, &quot;&quot;, text)<br><br>def clean_element_text(text: str) -&gt; str:<br>    text = clean_extra_whitespace(text)<br>    text = replace_unicode_quotes(text)<br>    text = clean_non_ascii_chars(text)<br>    text = clean_bullets(text)<br>    text = remove_colons(text)<br>    return text.strip()</pre><p>When processing large volumes of documents, a specialized function is required to iterate over the entire directory and apply this logic to each HTML file:</p><pre>def ingest_and_clean_unstructured_html(html_path: str):<br>    elements = partition_html(filename=html_path)<br>    cleaned_output = []<br><br>    for el in elements:<br>        if hasattr(el, &#39;text&#39;) and el.text:<br>            cleaned_text = clean_element_text(el.text)<br>            cleaned_output.append({<br>                &quot;filename&quot;: os.path.basename(html_path),<br>                &quot;type&quot;: el.category if hasattr(el, &#39;category&#39;) else el.__class__.__name__,<br>                &quot;text&quot;: cleaned_text<br>            })<br>    return cleaned_output<br><br>def process_html_directory(directory_path: str):<br>    all_results = []<br><br>    # Loop through all files in the directory<br>    for filename in os.listdir(directory_path):<br>        if filename.lower().endswith(&quot;.html&quot;):<br>            file_path = os.path.join(directory_path, filename)<br>            print(f&quot;🔍 Processing: {file_path}&quot;)<br><br>            try:<br>                output_html = ingest_and_clean_unstructured_html(file_path)<br>                all_results.extend(output_html)<br>            except Exception as e:<br>                print(f&quot;⚠️ Error processing {filename}: {e}&quot;)</pre><p>While this produces <strong>cleaner and more human-readable content</strong>, it also means that:</p><ul><li>Navigation or meta text is intentionally dropped.</li><li>Structural cues like captions may be separated from their context.</li><li>When completeness is required, downstream users have <strong>no direct way to recover filtered text</strong>, because Unstructured doesn’t retain the full raw text document stream.</li><li>Processing is file-by-file on CPU, without distributed scaling or Spark integration.</li></ul><p>Thus, while Unstructured is ideal for <strong>content-centric summarization or LLM preprocessing</strong>, it is <strong>not appropriate</strong> for pipelines that require full document coverage or raw-text fidelity.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fph7iK%2F1%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fph7iK%2F1%2F&amp;image=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fph7iK%2Fplain-s.png%3Fv%3D1&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="330" frameborder="0" scrolling="no"><a href="https://medium.com/media/803da64b5df304de32a794542ce163b1/href">https://medium.com/media/803da64b5df304de32a794542ce163b1/href</a></iframe><p>To illustrate this scenario, we developed a notebook that processes a set of medical records and evaluates the <strong>quality of text extraction</strong> using a simple <strong>Jaccard similarity</strong> metric. The results show both frameworks performing closely:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/540/1*Bsg7nBsOEf8w0X2hgIjWsw.png" /></figure><p>However, as the saying goes, <em>the devil is in the details</em>. A deeper token-level analysis revealed that both frameworks missed the token <strong>dashboard</strong>, but <strong>Unstructured</strong> also omitted several contextually important words such as:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/785/1*2a4Zwjj-8eX13VCpXKtlRw.png" /></figure><p>These missing tokens can be <strong>critical in clinical and biomedical contexts</strong>, where small lexical gaps may significantly affect the outcomes of downstream NLP tasks. Therefore, despite the seemingly similar similarity scores, these subtle omissions could lead to <strong>substantial performance differences</strong> in real-world NLP pipelines.</p><p>You can review the full notebook for this experiment and the result metrics👉 <a href="https://github.com/JohnSnowLabs/spark-nlp-workshop/blob/master/open-source-nlp/24.0.Benchmark_Unstructured_Sparknlp_Html.ipynb">here</a></p><h3>Problem2: Maintaining Structural Context for Data-Rich Documents</h3><p>In many enterprise domains such as healthcare, finance, insurance, scientific publishing, and legal discovery critical insights critical insights are embedded in <strong>structured elements</strong> like tables and figures. These are not just blobs of text; their meaning depends heavily on their <strong>position within the document</strong>, including headers, captions, nearby narrative text, and visual layout.</p><p>Without preservation of this <strong>structural context</strong>, downstream NLP systems struggle to interpret, relate, and reason over the extracted information. For example:</p><ul><li>A clinical lab table needs to be associated with its section heading (“Most Recent Laboratory Results”) so decision support systems know <em>which test belongs to which patient visit</em>.</li><li>A financial table summarizing quarterly results must be tied to the correct caption and date range to feed into a BI dashboard.</li><li>Scientific documents often contain dozens of tables and figures where <em>semantic relationships</em> between text and tables are essential for accurate knowledge extraction and reasoning.</li></ul><p>This structural understanding matters not just for <em>content extraction</em> but for <strong>semantic NLP tasks</strong> such as information extraction, table-aware question answering, contextual reasoning, and knowledge graph construction. Research shows that incorporating <strong>structural and layout information</strong> significantly improves document understanding and extraction quality because it helps NLP systems interpret data <em>in context</em>, not just as isolated text or table cells [1].</p><h4>How Spark NLP Solves It</h4><p>Spark NLP’s Reader2Table orReader2Imageaddresses this challenge by preserving <strong>structural and positional metadata</strong> during extraction.<br>Each table or image is enriched with information such as its <strong>DOM path</strong>, <strong>nearest header</strong>, and <strong>section hierarchy</strong>, ensuring every piece of extracted data remains tied to its original context.</p><pre>empty_df = spark.createDataFrame([], &#39;string&#39;).toDF(&#39;text&#39;)<br><br>reader2doc = Reader2Table() \<br>    .setContentType(&#39;text/html&#39;) \<br>    .setContentPath(&#39;html_docs/EHR-2025-12-000002.html&#39;) \<br>    .setOutputCol(&#39;table&#39;) \<br>    .setExplodeDocs(True)<br><br>pipeline = Pipeline(stages=[reader2doc])<br>model = pipeline.fit(empty_df)<br>result_df = model.transform(empty_df)</pre><p>JSON output</p><pre>+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|result                                                                                                                                                                                                                                                                                                                                                                                                                                         |<br>+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|[{&quot;caption&quot;:&quot;&quot;,&quot;header&quot;:[&quot;Test&quot;,&quot;Result&quot;,&quot;Units&quot;,&quot;Reference Range&quot;,&quot;Status&quot;],&quot;rows&quot;:[[&quot;PSA&quot;,&quot;0.32&quot;,&quot;ng/mL&quot;,&quot;0-4.0&quot;,&quot;Excellent&quot;],[&quot;Testosterone&quot;,&quot;125&quot;,&quot;ng/dL&quot;,&quot;300-1000&quot;,&quot;Recovering&quot;],[&quot;Hemoglobin&quot;,&quot;14.3&quot;,&quot;g/dL&quot;,&quot;13.5-17.5&quot;,&quot;Normal&quot;],[&quot;WBC&quot;,&quot;7.2&quot;,&quot;K/uL&quot;,&quot;4.5-11.0&quot;,&quot;Normal&quot;],[&quot;Creatinine&quot;,&quot;0.9&quot;,&quot;mg/dL&quot;,&quot;0.7-1.3&quot;,&quot;Normal&quot;],[&quot;ALT&quot;,&quot;22&quot;,&quot;U/L&quot;,&quot;7-56&quot;,&quot;Normal&quot;]]}]                                                                        |<br>|[{&quot;caption&quot;:&quot;&quot;,&quot;header&quot;:[&quot;Test&quot;,&quot;Result&quot;,&quot;Units&quot;,&quot;Reference Range&quot;,&quot;Status&quot;],&quot;rows&quot;:[[&quot;Testosterone&quot;,&quot;105&quot;,&quot;ng/dL&quot;,&quot;300-1000&quot;,&quot;Recovering&quot;],[&quot;Hemoglobin&quot;,&quot;12.3&quot;,&quot;g/dL&quot;,&quot;13.5-17.5&quot;,&quot;Normal&quot;],[&quot;Creatinine&quot;,&quot;0.7&quot;,&quot;mg/dL&quot;,&quot;0.7-1.3&quot;,&quot;Normal&quot;]]}]                                                                                                                                                                                               |<br>|[{&quot;caption&quot;:&quot;&quot;,&quot;header&quot;:[&quot;Medication&quot;,&quot;Dose&quot;,&quot;Frequency&quot;,&quot;Indication&quot;,&quot;Status&quot;],&quot;rows&quot;:[[&quot;Atorvastatin (Lipitor)&quot;,&quot;10 mg PO&quot;,&quot;Daily&quot;,&quot;Hyperlipidemia&quot;,&quot;Active&quot;],[&quot;Aspirin&quot;,&quot;81 mg PO&quot;,&quot;Daily&quot;,&quot;Cardiovascular prophylaxis&quot;,&quot;Active&quot;],[&quot;Vitamin D3&quot;,&quot;2000 IU PO&quot;,&quot;Daily&quot;,&quot;Bone health&quot;,&quot;Active&quot;],[&quot;Calcium carbonate&quot;,&quot;500 mg PO&quot;,&quot;BID&quot;,&quot;Bone health (post-ADT)&quot;,&quot;Active&quot;],[&quot;Multivitamin&quot;,&quot;1 tab PO&quot;,&quot;Daily&quot;,&quot;Nutritional support&quot;,&quot;Active&quot;]]}]|<br>+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</pre><p>HTML output</p><pre>reader2doc = Reader2Table() \<br>    .setContentType(&#39;text/html&#39;) \<br>    .setContentPath(&#39;html_docs/EHR-2025-12-000002.html&#39;) \<br>    .setOutputCol(&#39;table&#39;) \<br>    .setOutputFormat(&#39;html-table&#39;) \<br>    .setExplodeDocs(True)</pre><pre>+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|result                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |<br>+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|[&lt;table class=&quot;lab-table&quot;&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Test&lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;th&gt;Units&lt;/th&gt;&lt;th&gt;Reference Range&lt;/th&gt;&lt;th&gt;Status&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;PSA&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.32&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;ng/mL&lt;/td&gt;&lt;td&gt;0-4.0&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Excellent&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Testosterone&lt;/td&gt;&lt;td&gt;&lt;strong&gt;125&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;ng/dL&lt;/td&gt;&lt;td&gt;300-1000&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-completed&quot;&gt;Recovering&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hemoglobin&lt;/td&gt;&lt;td&gt;&lt;strong&gt;14.3&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;g/dL&lt;/td&gt;&lt;td&gt;13.5-17.5&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Normal&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;WBC&lt;/td&gt;&lt;td&gt;&lt;strong&gt;7.2&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;K/uL&lt;/td&gt;&lt;td&gt;4.5-11.0&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Normal&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Creatinine&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.9&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;mg/dL&lt;/td&gt;&lt;td&gt;0.7-1.3&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Normal&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ALT&lt;/td&gt;&lt;td&gt;&lt;strong&gt;22&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;U/L&lt;/td&gt;&lt;td&gt;7-56&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Normal&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;]|<br>|[&lt;table class=&quot;lab-table&quot;&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Test&lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;th&gt;Units&lt;/th&gt;&lt;th&gt;Reference Range&lt;/th&gt;&lt;th&gt;Status&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Testosterone&lt;/td&gt;&lt;td&gt;&lt;strong&gt;105&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;ng/dL&lt;/td&gt;&lt;td&gt;300-1000&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-completed&quot;&gt;Recovering&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hemoglobin&lt;/td&gt;&lt;td&gt;&lt;strong&gt;12.3&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;g/dL&lt;/td&gt;&lt;td&gt;13.5-17.5&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Normal&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Creatinine&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.7&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;mg/dL&lt;/td&gt;&lt;td&gt;0.7-1.3&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Normal&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;]                                                                                                                                                                                                                                                                                                                                                                                                                                         |<br>|[&lt;table class=&quot;lab-table&quot;&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Medication&lt;/th&gt;&lt;th&gt;Dose&lt;/th&gt;&lt;th&gt;Frequency&lt;/th&gt;&lt;th&gt;Indication&lt;/th&gt;&lt;th&gt;Status&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Atorvastatin (Lipitor)&lt;/td&gt;&lt;td&gt;10 mg PO&lt;/td&gt;&lt;td&gt;Daily&lt;/td&gt;&lt;td&gt;Hyperlipidemia&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Active&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Aspirin&lt;/td&gt;&lt;td&gt;81 mg PO&lt;/td&gt;&lt;td&gt;Daily&lt;/td&gt;&lt;td&gt;Cardiovascular prophylaxis&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Active&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Vitamin D3&lt;/td&gt;&lt;td&gt;2000 IU PO&lt;/td&gt;&lt;td&gt;Daily&lt;/td&gt;&lt;td&gt;Bone health&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Active&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Calcium carbonate&lt;/td&gt;&lt;td&gt;500 mg PO&lt;/td&gt;&lt;td&gt;BID&lt;/td&gt;&lt;td&gt;Bone health (post-ADT)&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Active&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Multivitamin&lt;/td&gt;&lt;td&gt;1 tab PO&lt;/td&gt;&lt;td&gt;Daily&lt;/td&gt;&lt;td&gt;Nutritional support&lt;/td&gt;&lt;td&gt;&lt;span class=&quot;status-badge status-active&quot;&gt;Active&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;]                                                                                                                      |<br>+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</pre><p>Metadata output:</p><pre>+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|metadata                                                                                                                                                                                                 |<br>+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|[orderTableIndex -&gt; 1, nearestHeader -&gt; 🔬 Most Recent Laboratory Results (10/22/2016), pageNumber -&gt; 1, domPath -&gt; /html[1]/body[1]/div[1]/div[3]/div[4]/table[1], elementType -&gt; Table, sentence -&gt; 8}]|<br>|[orderTableIndex -&gt; 2, nearestHeader -&gt; History Laboratory Results (10/22/2016), pageNumber -&gt; 1, domPath -&gt; /html[1]/body[1]/div[1]/div[3]/div[4]/table[2], elementType -&gt; Table, sentence -&gt; 10}]      |<br>|[orderTableIndex -&gt; 1, nearestHeader -&gt; 💊 Current Medications, pageNumber -&gt; 1, domPath -&gt; /html[1]/body[1]/div[1]/div[3]/div[5]/table[1], elementType -&gt; Table, sentence -&gt; 12}]                       |<br>+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</pre><p>This output captures rich structural information alongside extracted content:</p><ul><li><strong>DOM paths</strong> (e.g., /html[1]/body[1]/div[3]/div[5]/table[1]) that identify exactly where in the HTML document a table or image came from.</li><li><strong>Nearest section header context</strong> so that a table is semantically linked to its surrounding narrative (“Laboratory Results”, “Current Medications”, etc.).</li><li><strong>Order and hierarchy metadata</strong> such as orderTableIndex, allowing precise reconstruction of document structure.</li><li>A <strong>structured JSON representation</strong> of tables (with headers, rows, captions, and field metadata)</li><li>A <strong>HTML representation</strong> for visualization, rendering, or further processing.</li></ul><p>These enriched representations help downstream NLP tasks such as:</p><ul><li><strong>Table-aware question answering:</strong> Models like TAPAS leverage structured table data to answer <em>natural language questions over tables</em> with high accuracy, something that plain text extraction alone cannot support [2].</li><li><strong>Contextual table interpretation:</strong> Structural metadata enables models to understand <em>why</em> a table occurs where it does, improving joint inference between narrative text and tabular data, which is known to boost extraction quality when the context is considered [1].</li><li><strong>Semantic integration with knowledge graphs and IE systems:</strong> By preserving layout and section cues, extracted table data can be merged into structured knowledge representations with clear provenance.</li></ul><p>In practice, this means that Spark NLP pipelines don’t just <em>flatten</em> structured content they provide <strong>traceable, semantically rich extractions</strong> that downstream models can consume with minimal ambiguity.</p><h4>How Unstructured Handles It</h4><p>Unstructured’s partition_html module focuses on extracting <strong>semantic content </strong>titles, paragraphs, tables, and images but does not preserve the structural layout or positional hierarchy of those elements.</p><pre>def ingest_and_clean_unstructured_html_tables(html_path: str):<br>    &quot;&quot;&quot;<br>    Extract and clean only HTML table data using Unstructured.<br>    Returns a list of dicts with text and HTML (if available).<br>    &quot;&quot;&quot;<br>    elements = partition_html(filename=html_path)<br>    cleaned_output = []<br><br>    for el in elements:<br>        if hasattr(el, &quot;category&quot;) and el.category == &quot;Table&quot;:<br>            table_text = getattr(el, &quot;text&quot;, None)<br>            cleaned_entry = {<br>                &quot;filename&quot;: os.path.basename(html_path),<br>                &quot;type&quot;: &quot;Table&quot;,<br>            }<br><br>            # clean and add plain text<br>            if table_text:<br>                cleaned_entry[&quot;text&quot;] = clean_element_text(table_text)<br><br>            # look in metadata for HTML, if it exists<br>            if hasattr(el, &quot;metadata&quot;) and isinstance(el.metadata, dict):<br>                html_content = el.metadata.get(&quot;text_as_html&quot;)<br>                if html_content:<br>                    cleaned_entry[&quot;text_as_html&quot;] = html_content<br><br>            cleaned_output.append(cleaned_entry)<br><br>    return cleaned_output</pre><p>Output example:</p><pre>[{&#39;filename&#39;: &#39;EHR-2025-12-000002.html&#39;,<br>  &#39;type&#39;: &#39;Table&#39;,<br>  &#39;text&#39;: &#39;Test Result Units Reference Range Status PSA 0.32 ng/mL 0-4.0 Excellent Testosterone 125 ng/dL 300-1000 Recovering Hemoglobin 14.3 g/dL 13.5-17.5 Normal WBC 7.2 K/uL 4.5-11.0 Normal Creatinine 0.9 mg/dL 0.7-1.3 Normal ALT 22 U/L 7-56 Normal&#39;},<br> {&#39;filename&#39;: &#39;EHR-2025-12-000002.html&#39;,<br>  &#39;type&#39;: &#39;Table&#39;,<br>  &#39;text&#39;: &#39;Test Result Units Reference Range Status Testosterone 105 ng/dL 300-1000 Recovering Hemoglobin 12.3 g/dL 13.5-17.5 Normal Creatinine 0.7 mg/dL 0.7-1.3 Normal&#39;},<br> {&#39;filename&#39;: &#39;EHR-2025-12-000002.html&#39;,<br>  &#39;type&#39;: &#39;Table&#39;,<br>  &#39;text&#39;: &#39;Medication Dose Frequency Indication Status Atorvastatin (Lipitor) 10 mg PO Daily Hyperlipidemia Active Aspirin 81 mg PO Daily Cardiovascular prophylaxis Active Vitamin D3 2000 IU PO Daily Bone health Active Calcium carbonate 500 mg PO BID Bone health (post-ADT) Active Multivitamin 1 tab PO Daily Nutritional support Active&#39;}]</pre><p>Unstructured does not support text_as_html field for HTML files.</p><p>While the output for other file types may include a text_as_html field containing the visual representation of a table, it lacks:</p><ul><li>The document’s DOM ancestry</li><li>Section or caption linkage (no nearest header tracking)</li><li>Element order within the page layout</li></ul><p>As a result, Unstructured’s table output is essentially <em>context-agnostic</em>. While the extracted content may be correct in isolation, the surrounding structural relationships (vital for holistic NLP tasks) are lost. This limitation inhibits the use of Unstructured outputs in workflows that depend on understanding <em>how</em> the table fits into the document narrative or layout.</p><p><a href="https://github.com/JohnSnowLabs/spark-nlp-workshop/blob/master/open-source-nlp/25.0.Table_Structural_Context_Unstructured_Sparknlp.ipynb">This notebook </a>showcases the extraction of tabular data across both frameworks and emphasizes Spark NLP’s capability to generate rich DOM-structured output for precise contextual alignment.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2FAuIbl%2F1%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FAuIbl%2F1%2F&amp;image=https%3A%2F%2Fdatawrapper.dwcdn.net%2FAuIbl%2Fplain-s.png%3Fv%3D1&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="458" frameborder="0" scrolling="no"><a href="https://medium.com/media/fdff6e8e0076e602ea7b46d9bd7e12da/href">https://medium.com/media/fdff6e8e0076e602ea7b46d9bd7e12da/href</a></iframe><p>In real-world enterprise pipelines, accurate understanding of <strong>where</strong> structured data appears not just <em>what</em> it contains enables advanced NLP use cases such as table question answering, context-aware information extraction, and integration into knowledge graphs. Spark NLP’s DOM-aware, dual JSON/HTML representations provide the structural foundation these tasks require, whereas simpler extraction tools lack the necessary positional fidelity.</p><h3>Problem3: Processing Millions of Documents Efficiently and Reliably</h3><p>Modern organizations are often tasked with processing <strong>massive volumes of unstructured documents</strong> PDFs, HTML pages, contracts, medical reports, or regulatory filings often numbering in the millions. These files arrive daily via ingestion pipelines, enterprise content systems, or compliance workflows.</p><p>While <strong>sequential or single-machine processing</strong> might suffice for small datasets, scaling becomes a serious challenge as data grows. Common bottlenecks include:</p><ul><li><strong>Excessive processing time</strong> when files must be handled one by one</li><li><strong>Inconsistent outputs</strong> when pipelines fail mid-run and require manual restarts</li><li><strong>Escalating infrastructure costs</strong> due to lack of distributed workload handling</li><li><strong>Difficulty scaling NLP pipelines</strong> as tokenization, entity recognition, and classification steps are added</li></ul><p>This becomes a critical bottleneck for data engineering teams tasked with maintaining real-time compliance, analytics, or document understanding workflows.</p><h4>How Spark NLP Solves It</h4><p>Spark NLP is built <strong>natively on Apache Spark</strong>, bringing distributed data processing to text analytics and NLP workloads..<br>This means text extraction, normalization, and NLP tasks can be performed <strong>in parallel across clusters</strong>, allowing millions of documents to be processed efficiently, reproducibly, and at scale.</p><p>Key advantages include:</p><ul><li><strong>Scalable architecture:</strong> Workloads are automatically partitioned across Spark executors, ensuring linear scalability as cluster resources grow.</li><li><strong>Fault tolerance:</strong> Automatic checkpointing and resilient distributed datasets (RDDs) guarantee recovery from node or job failures.</li><li><strong>Unified pipeline integration:</strong> Document ingestion, extraction (Reader2Doc, Reader2Table, Reader2Image, ReaderAssembler), tokenization, and NLP inference can all run as a single Spark job no need to move data between tools.</li><li><strong>Operational efficiency:</strong> Ideal for enterprise pipelines that process terabytes of data daily.</li></ul><p>Here’s a minimal example that ingests all files in a directory using Spark NLP’s <strong>ReaderAssembler</strong> and saves the results as a Parquet dataset:</p><pre>reader_assembler = ReaderAssembler() \<br>    .setContentPath(directory) \<br>    .setOutputCol(&quot;document&quot;)<br><br>pipeline = Pipeline(stages=[reader_assembler])<br>model = pipeline.fit(empty_df)<br><br>df = model.transform(empty_df)<br>df.select(&quot;document_text.result&quot;).write.mode(&quot;overwrite&quot;).parquet(output)</pre><h4>How Unstructured Handles It</h4><p>Unstructured, by design, is a <strong>single node</strong> Python library optimized for lightweight document parsing and LLM preprocessing, not distributed workloads.<br>While it provides easy-to-use functions like partition_html and partition_pdf, each document must be processed <strong>individually</strong> on a single CPU core.</p><pre>def extract_text_from_file(filepath: Path) -&gt; str:<br>   try:<br>        elements = partition(filename=str(filepath))<br>    except Exception as e:<br>        print(f&quot;⚠️ Failed to read {filepath.name}: {e}&quot;)<br>        return &quot;&quot;<br><br>    text_content = []<br>    for element in elements:<br>        try:<br>            txt = getattr(element, &quot;text&quot;, None)<br>            if txt:<br>                text_content.append(txt)<br>        except Exception:<br>            continue<br><br>    return &quot;\n&quot;.join(text_content)<br><br>for idx, file_path in enumerate(files, start=1):<br>    file_t0 = time.perf_counter()<br><br>    text = extract_text_from_file(file_path)<br>    print(f&quot;✔ [{idx}/{len(files)}] {file_path.name} processed in {file_t1 - file_t0:.2f}s&quot;)</pre><p>This approach works well for small or ad-hoc datasets but faces clear limitations at enterprise scale:</p><ul><li>No built-in <strong>parallel or distributed processing</strong> across clusters</li><li>Requires external orchestration tools (like Dask or Ray) to scale horizontally</li><li>Limited integration with Spark-based ETL or NLP workflows</li><li>Higher latency and I/O overhead when processing millions of files sequentially</li></ul><p>Thus, for large-scale ingestion pipelines, Unstructured’s simplicity becomes a constraint increasing operational complexity and total runtime.</p><h4><strong>Experiment Results: Spark NLP vs Unstructured</strong></h4><p>To evaluate ingestion performance, we processed <strong>60 mixed-format documents</strong> using both frameworks under the same conditions.</p><p>All experiments were executed on a <strong>single machine </strong>no Spark cluster, no distributed environment to ensure a fair comparison.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FfGRlGNF8c2PBVixOrtUHg.png" /><figcaption>Average processing time for 60 documents. Spark NLP achieves ~2× faster throughput than Unstructured.</figcaption></figure><p>Even in this single-node setup, <strong>Spark NLP achieved nearly a 2x speedup</strong>, completing the full pipeline in roughly <strong>half the time</strong> of Unstructured.</p><p>This improvement comes primarily from Spark NLP’s ability to <strong>automatically parallelize work across all available CPU cores</strong>, distributing file reads and transformations efficiently under the hood.</p><p>Reproduce the benchmark and explore the full pipeline setup <a href="https://github.com/JohnSnowLabs/spark-nlp-workshop/blob/master/open-source-nlp/26.0.Benchmark_Unstructured_and_SparkNLP_Files_Ingestion.ipynb">here</a>.</p><h4><strong>Scaling Beyond a Single Machine</strong></h4><p>While this benchmark ran on one machine, Spark NLP’s real advantage emerges at <strong>larger scales</strong>:</p><ul><li>The same pipeline can run <strong>unchanged across a Spark cluster</strong>, leveraging multiple nodes for linear scalability.</li><li>As document volume grows to thousands or millions, Spark’s distributed scheduler automatically partitions the workload each executor handling its own batch of documents in parallel.</li><li>This architecture ensures both <strong>speed and fault tolerance</strong>, something single-threaded Python tools can’t easily replicate.</li></ul><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2FWYs8u%2F2%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FWYs8u%2F2%2F&amp;image=https%3A%2F%2Fdatawrapper.dwcdn.net%2FWYs8u%2Fplain-s.png%3Fv%3D2&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="346" frameborder="0" scrolling="no"><a href="https://medium.com/media/6be5ba8cff390276cad09f47ffad6f9b/href">https://medium.com/media/6be5ba8cff390276cad09f47ffad6f9b/href</a></iframe><p>When processing scales from hundreds to millions of documents, <strong>architecture becomes the differentiator</strong>.<br>Spark NLP’s distributed design allows organizations to scale text extraction and NLP workloads horizontally, maintaining both speed and reliability something single node solutions like Unstructured simply aren’t built to achieve.</p><p>Whether for compliance auditing, enterprise search, or large scale document intelligence, Spark NLP ensures that scale doesn’t compromise consistency.</p><p><strong>References:</strong></p><ol><li><a href="https://aclanthology.org/P13-2116/">https://aclanthology.org/P13-2116/</a></li><li><a href="https://www.johnsnowlabs.com/tapas-question-answering-from-tables-in-spark-nlp/">https://www.johnsnowlabs.com/tapas-question-answering-from-tables-in-spark-nlp/</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0d50874982cd" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/evaluating-document-ai-frameworks-spark-nlp-vs-unstructured-for-large-scale-text-processing-0d50874982cd">Evaluating Document AI Frameworks: Spark NLP vs Unstructured for Large-Scale Text Processing</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Semantic Search Across Text and Images? Meet E5-V in Spark NLP]]></title>
            <link>https://medium.com/spark-nlp/semantic-search-across-text-and-images-meet-e5-v-in-spark-nlp-378d286d71a7?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/378d286d71a7</guid>
            <category><![CDATA[multimodal-ai]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[spark-nlp]]></category>
            <dc:creator><![CDATA[Muhammad Abdullah]]></dc:creator>
            <pubDate>Fri, 27 Jun 2025 10:12:36 GMT</pubDate>
            <atom:updated>2025-06-27T10:12:35.959Z</atom:updated>
            <content:encoded><![CDATA[<blockquote>“What if you could compare a sentence and an image with just one model?”</blockquote><p>Multimodal AI — models that can understand both text and images — has been making huge waves lately. Tools like CLIP and GPT-4 Vision have demonstrated what’s possible when language and vision are combined. Now, with Spark NLP 6.0.3, <strong>you can tap into that power at scale</strong> using a new feature called <strong>E5-V embeddings</strong>.</p><p>Whether you’re building search engines, recommendation systems, or just playing with embeddings, this new addition might be your next favorite trick.</p><h3>So… What Is E5-V?</h3><p>E5-V (short for “Embedding Everything Everywhere”) is a <strong>universal multimodal embedding model</strong>. The basic idea? It can take in <strong>text</strong>, <strong>images</strong>, or <strong>both</strong>, and map them all into the <em>same vector space</em>.</p><p><strong><em>So if you feed it:</em></strong></p><ul><li><em>A sentence like “A dog playing fetch”</em></li><li><em>And a photo of that happening…</em></li></ul><p>Their resulting embeddings will land close together in vector space. That’s pretty wild, especially when you consider that E5-V <strong>doesn’t even need to be fine-tuned on images</strong>. It just uses an innovative prompting method with a large language model that already “gets” a lot about the world.</p><p>How? It works by adding prompts like:</p><pre>&lt;text&gt;<br>What is a short description of this?<br><br>&lt;image&gt;<br>Describe this image in one phrase:</pre><p>These prompts help the model produce similar meanings for similar content, even across different formats.</p><h3>How E5-V Embeds Images and Text Together (A Visual Breakdown)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Xn4_SLWTaHefsoQ2oVb1pw.png" /></figure><p>To truly understand what makes E5-V special, it is helpful to examine how it <em>learns</em> to integrate both text and images into a shared space.</p><p>Here’s what’s going on:</p><ol><li><strong>On the left</strong>, a traditional large language model (LLM) is trained on a large amount of text using <strong>contrastive learning</strong> — essentially teaching it to pull semantically similar examples (such as two captions about dogs) closer together in vector space.</li><li>Then, in the <strong>Multimodal LLM (MLLM)</strong> on the right, they bring in images. Each image is passed through a <strong>vision encoder and</strong> then projected into the same embedding space as the text. The same LLM now handles both modalities.</li><li>The cool part is: it’s <strong>prompt-based</strong>. During training, the model is given tasks like:<br><em>“Summarize the sentence in one word.”<br>“Summarize the image in one word.”</em></li><li>But at inference time (far right), it can generalize to <em>new prompts</em> it’s never seen, like:<br><em>“Modify this image with ‘change cat to dog’, and describe the modified image in one word.”</em></li></ol><p>So, not only does E5-V embed different kinds of content into the same vector space, but it also does so with <strong>flexible, prompt-driven control</strong>, which is crucial for making it useful across tasks without requiring additional fine-tuning.</p><p>This means that your sentence, your image, or their combination gets mapped into the same semantic space, ready for search, retrieval, or downstream tasks.</p><h3>Why Should You Care?</h3><p>Because this opens up a bunch of valuable things:</p><ul><li><strong>Semantic Search</strong>: Search images using text queries (or vice versa).</li><li><strong>Content Recommendation</strong>: Show relevant articles, images, or products based on a user’s query, no matter the format.</li><li><strong>Multimodal Retrieval</strong>: Find content that “means the same thing,” not just textually or visually, but across both.</li></ul><p>And the best part? You can do all of this <strong>without training your model</strong>. Spark NLP handles the heavy lifting, and E5-V takes care of the rest.</p><h3>🔧 Using E5-V in Spark NLP</h3><p>With the Spark NLP 6.0.3 release, using E5-V is straightforward. There’s a new annotator called E5VEmbeddings, and it plugs into your existing Spark NLP pipelines.</p><p>Here’s a simple example comparing an Image and a Sentence with E5-V</p><pre>import sparknlp<br>from sparknlp.base import DocumentAssembler, ImageAssembler<br>from sparknlp.annotator import E5VEmbeddings<br>from pyspark.ml import Pipeline<br>from pyspark.sql.functions import lit<br>from sparknlp.util import EmbeddingsDataFrameUtils<br><br>import os<br>from pathlib import Path<br><br># Start Spark NLP session<br>spark = sparknlp.start()<br><br># Download an image to test<br>img_url = &quot;https://github.com/openvinotoolkit/openvino_notebooks/assets/29454499/d5fbbd1a-d484-415c-88cb-9986625b7b11&quot;<br>Path(&quot;images&quot;).mkdir(exist_ok=True)<br><br># Save the image manually or use the line below if your environment supports it<br>os.system(f&quot;wget -q -O images/image1.jpg {img_url}&quot;)<br><br># Load the image<br>images_path = &quot;file://&quot; + os.getcwd() + &quot;/images/&quot;<br>image_df = spark.read.format(&quot;image&quot;).option(&quot;dropInvalid&quot;, True).load(images_path)<br><br># Add a prompt to guide the embedding<br>image_prompt = (<br>    &quot;&lt;|start_header_id|&gt;user&lt;|end_header_id|&gt;\n\n&lt;image&gt;\n&quot;<br>    &quot;Summary above image in one word: &lt;|eot_id|&gt;&quot;<br>    &quot;&lt;|start_header_id|&gt;assistant&lt;|end_header_id|&gt;\n\n&quot;<br>)<br><br># Attach text prompt to image DataFrame<br>image_df = image_df.withColumn(&quot;text&quot;, lit(image_prompt))<br><br># Create a text-only row for comparison<br>text_prompt = (<br>    &quot;&lt;|start_header_id|&gt;user&lt;|end_header_id|&gt;\n\n&lt;sent&gt;\n&quot;<br>    &quot;Summary above sentence in one word: &lt;|eot_id|&gt;&quot;<br>    &quot;&lt;|start_header_id|&gt;assistant&lt;|end_header_id|&gt;\n\n&quot;<br>)<br>text_desc = &quot;A cat sitting in a box.&quot;<br>empty_image_df = spark.createDataFrame(<br>    [EmbeddingsDataFrameUtils.emptyImageRow],<br>    schema=EmbeddingsDataFrameUtils.imageSchema<br>)<br>text_df = empty_image_df.withColumn(&quot;text&quot;, lit(text_prompt.replace(&quot;&lt;sent&gt;&quot;, text_desc)))<br><br># Combine image and text into a single DataFrame<br>test_df = image_df.union(text_df)<br><br># Set up the pipeline<br>image_assembler = ImageAssembler() \<br>    .setInputCol(&quot;image&quot;) \<br>    .setOutputCol(&quot;image_assembler&quot;)<br><br># Load pretrained E5-V model (adjust the path if needed)<br>e5v = E5VEmbeddings.pretrained(&quot;E5-V_LLM_8B&quot;, &quot;xx&quot;) \<br>    .setInputCols([&quot;image_assembler&quot;]) \<br>    .setOutputCol(&quot;e5v&quot;)<br><br>pipeline = Pipeline(stages=[image_assembler, e5v])<br>results = pipeline.fit(test_df).transform(test_df)<br><br># View the resulting embeddings<br>results.select(&quot;text&quot;, &quot;e5v.embeddings&quot;).show(truncate=False)</pre><p>This gives you a clean, comparable vector for each input, whether it’s an image, a sentence, or both. From here, you can measure similarity, group content by meaning, or plug the embeddings into whatever downstream task you’re working on. Super flexible, surprisingly easy.</p><h3>💡 Final Thoughts</h3><p>E5-V might not be the flashiest name, but it’s one of the most practical tools I’ve seen for working with text and images together. It simplifies a challenging problem — <strong><em>reasoning across</em></strong><em> different </em><strong><em>modalities</em></strong> — into something usable with just a few lines of code.</p><p>If you’re already in the Spark NLP ecosystem (or thinking about it), this release is worth checking out.</p><h4>Want to dive deeper? The following resources might be of interest:</h4><ul><li><strong>GitHub Repository: </strong><a href="https://github.com/JohnSnowLabs/spark-nlp"><em>Spark NLP on GitHub</em></a><em>: Source code, issue tracking, and community contributions.</em></li><li><strong>E5V Embeddings Class Source: </strong><a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/python/sparknlp/annotator/embeddings/e5v_embeddings.py"><em>Python</em></a><em> | </em><a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/src/main/scala/com/johnsnowlabs/nlp/embeddings/E5VEmbeddings.scala"><em>Scala</em></a></li><li><strong>Hands-On Notebooks on how to import E5V Embeddings models from Huggingface:</strong><em> </em><a href="https://colab.research.google.com/github/JohnSnowLabs/spark-nlp/blob/master/examples/python/transformers/openvino/HuggingFace_OpenVINO_in_Spark_NLP_E5VEmbeddings.ipynb"><em>HuggingFace_to_Spark_NLP_E5VEmbeddings</em></a></li><li><strong>Spark NLP Release Notes: </strong><a href="https://github.com/JohnSnowLabs/spark-nlp/releases/tag/6.0.3"><em>Version 6.0.3</em></a></li></ul><h4>❤ Join the Community</h4><ul><li><a href="https://join.slack.com/t/spark-nlp/shared_invite/zt-198dipu77-L3UWNe_AJ8xqDk0ivmih5Q"><strong>Slack</strong></a><strong>:</strong> <em>Join the Spark NLP community and team for live discussions.</em></li><li><a href="https://github.com/JohnSnowLabs/spark-nlp/discussions"><strong>Discussions</strong></a>: <em>Engage with community members, share ideas, and showcase how you use Spark NLP!</em></li><li><a href="https://medium.com/spark-nlp"><strong>Medium</strong></a>: <em>Read Spark NLP articles on its official Medium page.</em></li><li><a href="https://www.youtube.com/channel/UCmFOjlpYEhxf_wJUDuz6xxQ/videos"><strong>YouTube</strong></a>: <em>Watch Spark NLP video tutorials for in-depth guidance.</em></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=378d286d71a7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/semantic-search-across-text-and-images-meet-e5-v-in-spark-nlp-378d286d71a7">Semantic Search Across Text and Images? Meet E5-V in Spark NLP</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Making Multi-Format Ingestion Easier with the New Partition and PartitionTransformer in Spark NLP 6.]]></title>
            <link>https://medium.com/spark-nlp/making-multi-format-ingestion-easier-with-the-new-partition-and-partitiontransformer-in-spark-nlp-6-ca15ad0b9369?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/ca15ad0b9369</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[data-ingestion]]></category>
            <category><![CDATA[enterprise-architecture]]></category>
            <category><![CDATA[spark-nlp]]></category>
            <category><![CDATA[partition]]></category>
            <dc:creator><![CDATA[Muhammad Abdullah]]></dc:creator>
            <pubDate>Wed, 28 May 2025 15:44:17 GMT</pubDate>
            <atom:updated>2025-05-28T15:44:16.952Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r58M8zHWgDdchKTiwRjCdQ.jpeg" /></figure><h3>Making Multi-Format Ingestion Easier with the New Partition and PartitionTransformer in Spark NLP 6.0.2</h3><p>If you’ve ever wrestled with reading different document formats in your NLP pipelines, PDFs here, Word docs there, the occasional HTML file thrown in, you know how frustrating it can get. The new <strong>Partition</strong> and <strong>PartitionTransformer</strong> annotators in Spark NLP 6.0.2 addresses this head-on.</p><h4>What is the Partition Annotator?</h4><p>In a nutshell, <strong>Partition</strong>is a high-level abstraction for document ingestion. Think of it as an adapter for all file types. You don’t need to specify a reader (like PDF or DOCX); <strong>Partition</strong>figures it out for you. All you have to do is point it at your files, and it handles the rest for you.</p><p>This is especially helpful when you’re working with pipelines that need to process a variety of document types, like a legal or compliance workflow, without writing custom logic for each one.</p><h3>🔧 Example Usage</h3><pre>from sparknlp.base import Partition<br><br># Automatically detect and parse the file<br>df = Partition().partition(&quot;./pdf-files/text_3_pages.pdf&quot;)<br><br>df.show()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/685/1*BUqnaC4MB7Crz8BuSQ15iA.png" /></figure><blockquote>For a more <strong>hands-on tutorial</strong> using the <strong><em>Partition</em> </strong>annotator in Spark NLP 6.0.2, check out this Colab notebook: 👉 <a href="https://github.com/JohnSnowLabs/spark-nlp/blob/b17e1eb622a00e033cc1e8982f44094728d57da5/examples/python/data-preprocessing/SparkNLP_Partition_Demo.ipynb">Open Tutorial in Google Colab</a></blockquote><h3>Partition easily ingests files from:</h3><ul><li><strong>Databricks (dbfs://)</strong></li><li><strong>HDFS (hdfs://)</strong></li><li><strong>Microsoft Fabric OneLake (abfss://)</strong></li></ul><p>No extra configuration is needed to access these distributed environments.</p><h3>Customization with Parameters</h3><p>You can fine-tune ingestion using keyword arguments. One key parameter is content_type, which lets you override automatic detection:</p><pre>df = Partition(content_type=&quot;application/msword&quot;).partition(&quot;./word-files&quot;)<br>df.show()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/316/1*uoWH6Au5Z-OqWDQ5kDV9hA.png" /></figure><blockquote>Other configurable options include:</blockquote><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b15f361df3e3de08f01a4c8c7b5f8c24/href">https://medium.com/media/b15f361df3e3de08f01a4c8c7b5f8c24/href</a></iframe><h3>Smoother Pipelines with PartitionTransformer</h3><p>When you’re working inside a Spark NLP pipeline, you often want everything from ingestion-to-annotation to flow seamlessly. That’s where PartitionTransformer comes in. It brings the power of Partition directly into your pipeline, letting you handle file and URL ingestion as just another stage</p><p>It supports reading from:</p><ul><li><strong><em>Local files</em></strong></li><li><strong><em>URLs</em></strong></li><li><strong><em>In-memory strings or byte arrays</em></strong></li></ul><p>…and it automatically detects formats like <strong>PDF</strong>, <strong>DOCX</strong>, <strong>HTML</strong>, <strong>emails</strong>, and more.</p><h4>🔧 Example Usage</h4><pre>from sparknlp.base import DocumentAssembler<br>from sparknlp.annotator import PartitionTransformer<br>from pyspark.ml import Pipeline<br><br># Sample input: a single URL<br>data = spark.createDataFrame([(&quot;https://www.blizzard.com&quot;)], [&quot;text&quot;])<br><br># Convert raw text into a Spark NLP document<br>documentAssembler = DocumentAssembler() \<br>    .setInputCol(&quot;text&quot;) \<br>    .setOutputCol(&quot;document&quot;)<br><br># Ingest the content from the URL using PartitionTransformer<br>partition = PartitionTransformer() \<br>    .setInputCols([&quot;document&quot;]) \<br>    .setOutputCol(&quot;partition&quot;) \<br>    .setContentType(&quot;url&quot;)<br><br># Build and run the pipeline<br>pipeline = Pipeline(stages=[documentAssembler, partition])<br>result = pipeline.fit(data).transform(data)<br><br># Show the ingested document content<br>result.select(&quot;partition&quot;).show(truncate=False)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/429/1*FqF8_pcZK-NN7n6B8CztSw.png" /></figure><p>Use PartitionTransformer when you want ingestion as part of a pipeline. It’s a clean way to <strong>reuse and scale workflows</strong> without extra steps.</p><h3>When to Use Partition vs. SparkNLPReader</h3><ul><li><strong>Use </strong><strong>Partition</strong>when you want <strong>simplicity and automation</strong>, especially when dealing with multiple formats or distributed environments. It offers a <strong>automatic format detection, cleaner syntax, mixed-format handling</strong> and less boilerplate code.</li><li><strong>Stick with </strong><strong>SparkNLPReader</strong>when you need <strong>fine-grained control</strong> or want to customize how a specific format is processed.</li></ul><h3>Real-World Use Case: Legal Automation</h3><p>Let’s say you’re automating contract review for a legal team. They’ve got NDAs, employment agreements, and compliance documents in all kinds of formats. Instead of building a different ingestion flow for each one, you use <strong>Partition</strong> to read everything in bulk.</p><p>Downstream, you apply NLP components to extract metadata like parties involved, governing law, and obligations, then feed it into a legal CRM. What used to take hours of manual work now happens automatically.</p><h3>Conclusion</h3><p>The new <strong>Partition </strong>class brings a much-needed layer of abstraction to Spark NLP’s ingestion process. I love it because it’s so simple, powerful, and makes my life easier, especially when working at scale.</p><h3>Want to dive deeper? The following resources might be of interest:</h3><ul><li><strong>GitHub Repository: </strong><a href="https://github.com/JohnSnowLabs/spark-nlp"><em>Spark NLP on GitHub</em></a><em>: Source code, issue tracking, and community contributions.</em></li><li><strong>Partition Class Source<br></strong> <em>• </em><a href="https://github.com/JohnSnowLabs/spark-nlp/blob/feature/SPARKNLP-1174-Adding-PartitionTransformer/python/sparknlp/partition/partition.py"><em>Python</em></a><em> |</em><a href="https://github.com/JohnSnowLabs/spark-nlp/blob/feature/SPARKNLP-1174-Adding-PartitionTransformer/src/main/scala/com/johnsnowlabs/partition/Partition.scala"><em> Scala</em></a></li><li><strong>PartitionTransformer Source<br></strong> <em>• </em><a href="http://github.com/JohnSnowLabs/spark-nlp/tree/master/python/sparknlp/partition/partition_transformer.py"><em>Python</em></a><em> | </em><a href="http://github.com/JohnSnowLabs/spark-nlp/tree/master/src/main/scala/com/johnsnowlabs/partition/PartitionTransformer.scala"><em>Scala</em></a></li><li><strong>Hands-On Notebooks (Google Colab)<br></strong><em>• </em><a href="https://github.com/JohnSnowLabs/spark-nlp/blob/b17e1eb622a00e033cc1e8982f44094728d57da5/examples/python/data-preprocessing/SparkNLP_Partition_Demo.ipynb"><em>Partition</em></a><em> Tutorial</em></li><li><strong>Spark NLP Release Notes<br></strong> <em>•</em><a href="https://github.com/JohnSnowLabs/spark-nlp/releases/tag/6.0.0"><em> Version 6.0.0</em></a><em> | </em><a href="https://github.com/JohnSnowLabs/spark-nlp/releases/tag/6.0.1"><em>Version 6.0.1</em></a><em> | </em><a href="https://github.com/JohnSnowLabs/spark-nlp/releases/tag/6.0.2"><em>Version 6.0.2</em></a></li></ul><h3>❤️ Join the Community</h3><ul><li><a href="https://join.slack.com/t/spark-nlp/shared_invite/zt-198dipu77-L3UWNe_AJ8xqDk0ivmih5Q"><strong>Slack</strong></a><strong>:</strong> <em>Join the Spark NLP community and team for live discussions.</em></li><li><a href="https://github.com/JohnSnowLabs/spark-nlp/discussions"><strong>Discussions</strong></a>: <em>Engage with community members, share ideas, and showcase how you use Spark NLP!</em></li><li><a href="https://medium.com/spark-nlp"><strong>Medium</strong></a>: <em>Read Spark NLP articles on its official Medium page.</em></li><li><a href="https://www.youtube.com/channel/UCmFOjlpYEhxf_wJUDuz6xxQ/videos"><strong>YouTube</strong></a>: <em>Watch Spark NLP video tutorials for in-depth guidance.</em></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ca15ad0b9369" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/making-multi-format-ingestion-easier-with-the-new-partition-and-partitiontransformer-in-spark-nlp-6-ca15ad0b9369">Making Multi-Format Ingestion Easier with the New Partition and PartitionTransformer in Spark NLP 6.</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Transforming Document Ingestion at Scale with Spark NLP]]></title>
            <link>https://medium.com/spark-nlp/transforming-document-ingestion-at-scale-with-spark-nlp-c9a77de0f1cf?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/c9a77de0f1cf</guid>
            <category><![CDATA[apache-spark]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[spark-nlp]]></category>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[data-ingestion]]></category>
            <dc:creator><![CDATA[Muhammad Abdullah]]></dc:creator>
            <pubDate>Wed, 28 May 2025 15:30:54 GMT</pubDate>
            <atom:updated>2025-05-28T15:30:53.940Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/870/1*PYTnondAnRulyQ4bI6nzcQ.png" /></figure><h3>Spark NLP 6.0.0: A New Era of Enterprise-Scale Data Ingestion</h3><h3>Introduction</h3><p>The release of Spark NLP <a href="https://github.com/JohnSnowLabs/spark-nlp/releases/tag/6.0.0">6.0.0</a> marks a significant shift in the platform’s capabilities. Originally architected as a high-performance natural language processing library atop Apache Spark, Spark NLP has now evolved into a full-scale, enterprise-grade ingestion engine for unstructured and semi-structured data. With native support for a broad range of enterprise file formats, Spark NLP <a href="https://github.com/JohnSnowLabs/spark-nlp/releases/tag/6.0.0">6.0.0</a> empowers organizations to ingest, process, and analyze documents at massive scale, directly within Spark ecosystems.</p><h3>Key Enhancements in the new release</h3><h4>Redefining Spark NLP: From NLP Library to Ingestion Engine</h4><p>While Spark NLP remains renowned for advanced NLP components such as tokenization, named entity recognition, and deep contextual embeddings, the latest iteration enhances its utility by embedding native ingestion features. This update allows users to interface directly with primary data sources, eliminating the fragmentation between ingestion (ETL) and downstream analysis. It facilitates an uninterrupted transition from raw documents to actionable insights.</p><h4>Built for Scale: Distributed and Parallel Ingestion</h4><p>Spark NLP inherits Apache Spark’s native support for distributed data processing. That means ingestion tasks, such as reading thousands of PDFs or parsing extensive Excel files, are processed in parallel across the cluster. The platform handles massive datasets efficiently, thanks to its built-in fault tolerance, lazy evaluation, and optimized memory management.</p><h4>Direct Ingestion of Enterprise Files</h4><p>Version 6.0.0 introduces SparkNLPReader, a set of readers designed to natively parse:</p><ul><li><strong>PDFs:</strong> Including encrypted files, font-specific rendering, and bounding-box-based extraction.</li><li><strong>Excel Spreadsheets:</strong> Supports .xls and .xlsx formats, multi-sheet reading, and intelligent schema detection.</li><li><strong>PowerPoint Presentations:</strong> Extracts content from slides and speaker notes.</li><li><strong>Raw Text Logs and CSVs:</strong> Handling different encodings, delimiters, and irregular formats.</li></ul><p>By supporting these formats out of the box, Spark NLP eliminates the need for intermediate file converters or manual preprocessing scripts.</p><p>With the new ingestion readers, developers can create ingestion pipelines declaratively. No custom file parsing or glue code is needed:</p><pre>import sparknlp<br>from sparknlp.reader import SparkNLPReader<br><br># Start Spark NLP session<br>spark = sparknlp.start()<br>reader = SparkNLPReader(spark)<br><br># Read HTML<br>html_df = reader.html(&quot;https://www.wikipedia.org&quot;)<br><br># Read PDFs from local directory<br>pdf_df = reader.pdf(&quot;/home/user/pdfs-directory&quot;)<br><br># Read Excel files from local directory<br>excel_df = reader.xls(&quot;home/user/excel-directory&quot;)<br><br># Read Emails from local directory<br>email_df = reader.email(&quot;/home/user/emails-directory&quot;)<br><br># Example: shorthand syntax for HTML<br>html_df_short = sparknlp.read().html(&quot;https://www.wikipedia.org&quot;)<br><br># Use shorthand for simplicity when custom configuration isn&#39;t needed</pre><p>These high-level APIs make ingestion easy to integrate with Spark DataFrames, enabling rapid prototyping and deployment.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/92724d7eb41e34c335389fa1455a7bc1/href">https://medium.com/media/92724d7eb41e34c335389fa1455a7bc1/href</a></iframe><h3>Real-World Use Case: Legal Document Automation</h3><p>Consider a law firm or in-house legal team that processes tens of thousands of documents monthly, including contracts, NDAs, compliance filings, and more. Traditionally, reviewing these manually is slow, expensive, and error-prone.</p><p>With Spark NLP 6.0.0, documents are ingested in bulk from PDFs, emails, and Word files, where structural elements like clauses and tables are automatically parsed, and NLP components extract key details such as parties, dates, governing law clauses, and obligations, with the processed data then routed into legal CRMs or contract intelligence platforms.</p><p>The result? Legal teams can dramatically reduce document review time, enabling attorneys to focus on high-value analysis instead of manual data entry.</p><h3>Conclusion</h3><p>Spark NLP keeps getting better and better, constantly expanding its capabilities. Now, it’s not just about NLP — it’s a powerful platform for handling all your document processing needs at scale with easy integration. Spark NLP makes it simple to build smart AI pipelines. Whether you’re in legal, finance, or healthcare, it’s designed to keep up with your growing data needs and help you work faster and more efficiently.</p><h3>For further reading and resources, consider exploring the following:</h3><ul><li><strong>GitHub Repository</strong> — <a href="https://github.com/JohnSnowLabs/spark-nlp">Spark NLP on GitHub</a>: Source code, issue tracking, and community contributions.</li><li><strong>SparkNLPReader Source </strong>— <a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/python/sparknlp/reader/sparknlp_reader.py">Python</a> |<a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/src/main/scala/com/johnsnowlabs/reader/SparkNLPReader.scala"> Scala</a></li><li><strong>Spark NLP</strong> <strong>Release Notes</strong> — <a href="https://github.com/JohnSnowLabs/spark-nlp/releases/tag/6.0.0">6.0.0</a></li><li><strong>Hands-On Notebooks (Google Colab)<br></strong>• <a href="https://github.com/JohnSnowLabs/spark-nlp/tree/master/examples/python/reader">SparkNLPReader</a> Tutorial<br>• <a href="https://github.com/JohnSnowLabs/spark-nlp/tree/master/examples/python/data-preprocessing">Data Preprocessing</a> Tutorials</li></ul><h3>❤️ Community Support</h3><ul><li><a href="https://join.slack.com/t/spark-nlp/shared_invite/zt-198dipu77-L3UWNe_AJ8xqDk0ivmih5Q"><strong>Slack</strong></a><strong>:</strong> <em>Join the Spark NLP community and team for live discussions.</em></li><li><a href="https://github.com/JohnSnowLabs/spark-nlp/discussions"><strong>Discussions</strong></a>: <em>Engage with community members, share ideas, and showcase how you use Spark NLP!</em></li><li><a href="https://medium.com/spark-nlp"><strong>Medium</strong></a>: <em>Read Spark NLP articles on its official Medium page.</em></li><li><a href="https://www.youtube.com/channel/UCmFOjlpYEhxf_wJUDuz6xxQ/videos"><strong>YouTube</strong></a>: <em>Watch Spark NLP video tutorials for in-depth guidance.</em></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c9a77de0f1cf" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/transforming-document-ingestion-at-scale-with-spark-nlp-c9a77de0f1cf">Transforming Document Ingestion at Scale with Spark NLP</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Vision-Language Model (VLM) Inference at Scale with Spark NLP 6.0 + llama.cpp]]></title>
            <link>https://medium.com/spark-nlp/vision-language-model-vlm-inference-at-scale-with-spark-nlp-6-0-llama-cpp-f084ad3bd705?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/f084ad3bd705</guid>
            <category><![CDATA[llama-cpp]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[spark]]></category>
            <category><![CDATA[large-language-models]]></category>
            <category><![CDATA[vision-language-model]]></category>
            <dc:creator><![CDATA[Devin Ha]]></dc:creator>
            <pubDate>Wed, 14 May 2025 16:16:28 GMT</pubDate>
            <atom:updated>2025-05-14T16:16:27.955Z</atom:updated>
            <content:encoded><![CDATA[<h4>Large scale distributed inference of vision-language llama.cpp models in Spark NLP 6.0</h4><figure><img alt="Spark NLP X LLaMA C++ with a drawing of a llama with binoculars" src="https://cdn-images-1.medium.com/max/1024/1*WaCDpuIHZOj7TNBrMQ-lbw.png" /><figcaption>Spark NLP 6.0 features Vision LLM inference based on llama.cpps GGUF models.</figcaption></figure><p>The world isn’t just text. Increasingly, the data we need to process and understand is multimodal. From analyzing social media feeds and product catalogs to understanding complex documents with diagrams and figures, the ability to seamlessly process both vision and language together is becoming essential for many data science and AI tasks.</p><p>The release of <a href="https://medium.com/spark-nlp/spark-nlp-6-0-0-a-new-era-for-universal-ingestion-and-multimodal-llm-processing-at-scale-9078ff5859b4">Spark NLP 6.0</a> marks another milestone for scalable, distributed AI pipelines. With the introduction of the AutoGGUFVisionModel to Spark NLP, we can leverage <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a> to directly load quantized vision-language models (VLM) and infer them at scale inside Apache Spark. <strong>VLMs from llama.cpp are now first-class citizens inside Spark Data Frames</strong>, delivering <strong>multimodal inference at scale</strong> <strong>with native performance</strong>.</p><p>In this post, we will dive deeper into our new llama.cpp VLM feature and walk through an example together. This should get you started on using this feature for your own projects.</p><h3>Why is this important?</h3><p>Integrating vision-language models into large-scale data processing pipelines has traditionally posed<strong> challenges due to infrastructure complexity, scalability constraints and integration overhead. </strong>Custom solutions using cloud-based APIs can be difficult to scale efficiently and pose concerns for data privacy.</p><p>Spark NLP allows you to pass your large image-text datasets directly to efficient quantized GGUF models from llama.cpp that can describe, summarize, or reason over visual inputs <strong>entirely on-premise. </strong>You can rest assured that sensitive data stays entirely with you.</p><p>In addition, Spark NLP can scale the model to the resources of your computing cluster effortlessly, especially if they are equipped with GPUs. This unlocks <strong>massively-parallel multimodal batch processing</strong> across large image datasets with Spark’s native scalability, enabling powerful new use cases:</p><ul><li><strong>Image Captioning:</strong> Generate descriptive text for vast image collections.</li><li><strong>Visual Question Answering (VQA):</strong> Answer text-based questions about the content of images.</li><li><strong>Enhanced Document Understanding:</strong> Process scanned documents, PDFs, and images by analyzing both text and visual layout/figures.</li><li><strong>Multimodal Content Analysis:</strong> Analyze social media feeds or web content that integrates images and text for deeper insights.</li><li><strong>Automated Product Descriptions:</strong> Generate e-commerce descriptions directly from product images.</li></ul><h3>How to use it — Example Walk-through</h3><p>Let’s take a look at the AutoGGUFVisionModel in action. We will use it to caption a folder of images. You can follow along this example <a href="https://colab.research.google.com/github/JohnSnowLabs/spark-nlp/blob/master/examples/python/llama.cpp/llama.cpp_in_Spark_NLP_AutoGGUFVisionModel.ipynb">with this Google Colab notebook</a>, or you can also find the notebook at <a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/examples/python/llama.cpp/llama.cpp_in_Spark_NLP_AutoGGUFVisionModel.ipynb">this link</a>. If you want to run it locally, you can set up Spark NLP using the following instructions: <a href="https://sparknlp.org/docs/en/install">Spark NLP — Installation</a>.</p><h4>Spark NLP Setup</h4><p>Let’s setup Spark NLP to load our VLM as a AutoGGUFVisionModel. If you are running it in Google Colab, now is the time to install Spark NLP. Skip this step if you have already set it up.</p><pre># Only execute this if you are on Google Colab<br>! wget -q http://setup.johnsnowlabs.com/colab.sh -O - | bash</pre><p>We start Spark NLP via our simple start() function. Depending on the platform, we can pass gpu=True, apple_silicon=True, or aarch64=True to load the right dependencies.</p><pre>import sparknlp<br><br># Let&#39;s start Spark with Spark NLP with GPU enabled. Skip it if running on CPU.<br># You can also pass apple_silicon=True or aarch64=True<br>spark = sparknlp.start(gpu=True)</pre><p>We can now load the default pretrained model llava_v1.5_7b_Q4_0_gguf with:</p><pre>from sparknlp.annotator import *<br><br>autoGGUFModel = (<br>    AutoGGUFVisionModel.pretrained()<br>    .setInputCols([&quot;caption_document&quot;, &quot;image_assembler&quot;])<br>    .setOutputCol(&quot;completions&quot;)<br>)</pre><p>If LLaVA 1.5 is good enough for your use-case, then you are all set and can skip ahead to the image captioning section. You can also explore more models on our <a href="https://nlp.johnsnowlabs.com/models">Models Hub</a>.</p><p>If you want to bring your own GGUF model, then keep reading, where we will cover the import of a custom model.</p><h4>Import and save your custom GGUF VLM in Spark NLP</h4><p>As an example, we choose <a href="https://huggingface.co/Mozilla/llava-v1.5-7b-llamafile/tree/main">Mozilla/llava-v1.5–7b</a> as our VLM, the default pretrained model. It is a 7B parameter model in the GGUF format used by llama.cpp, which also is available in 4-bit quantization. This VLM consists of <strong>two parts</strong>: A<strong> multimodal projection</strong> model (<em>mmproj</em> in llama.cpp) based on CLIP and a <strong>large language model </strong>(LLM) based on Vicuna (fine-tuned Llama 2). The projection model encodes the image to embedding vectors, which the LLM is then conditioned on to generate relevant text.</p><p>To download the models, you can run the following commands:</p><pre>EXPORT_PATH_MODEL = &quot;llava-v1.5-7b-Q4_K.gguf&quot;<br>EXPORT_PATH_MMPROJ = &quot;llava-v1.5-7b-mmproj-Q4_0.gguf&quot;<br>! wget &quot;https://huggingface.co/Mozilla/llava-v1.5-7b-llamafile/resolve/main/{EXPORT_PATH_MODEL}?download=true&quot; -O  {EXPORT_PATH_MODEL}<br>! wget &quot;https://huggingface.co/Mozilla/llava-v1.5-7b-llamafile/resolve/main/{EXPORT_PATH_MMPROJ}?download=true&quot; -O  {EXPORT_PATH_MMPROJ}</pre><p>Now we can use the loadSavedModel function in AutoGGUFVisionModelto load the model into Spark NLP:</p><pre>from sparknlp.annotator import *<br><br>autoGGUFModel = (<br>    AutoGGUFVisionModel.loadSavedModel(EXPORT_PATH_MODEL, EXPORT_PATH_MMPROJ, spark)<br>    .setInputCols([&quot;caption_document&quot;, &quot;image_assembler&quot;])<br>    .setOutputCol(&quot;completions&quot;)<br>    .setChatTemplate(&quot;vicuna&quot;)<br>    .setBatchSize(4)<br>    .setNGpuLayers(99)<br>    .setNCtx(4096)<br>    .setMinP(0.05)<br>    .setNPredict(40)<br>    .setTemperature(0.05)<br>    .setTopK(40)<br>    .setTopP(0.95)<br>)</pre><p>The function loadSavedModel accepts three parameters:</p><ol><li>the path to the exported GGUF model</li><li>the path to the exported mmproj GGUF model</li><li>the SparkSession that is the spark variable we previously started via sparknlp.start()</li></ol><p>At this point, the model is loaded and ready to go. We can save it to disk so it is easier to be moved around and loaded with the Spark native .load function.</p><pre>autoGGUFModel.write().overwrite().save(f&quot;llava_v1.5_7b_Q4_0_gguf_spark_nlp&quot;)</pre><p>This is your GGUF model loaded and saved by Spark NLP! You can now use it on other machines, clusters, or any place you wish.</p><h4>Captioning Images</h4><p>Now let’s see how we can use the model to caption some images, for which we need some examples. The following command will download some from our repository. For brevity, I skipped the plotting code, but you can find it in the linked notebook above.</p><pre>!wget -q https://s3.amazonaws.com/auxdata.johnsnowlabs.com/public/resources/en/images/images.zip<br>plot_images()</pre><figure><img alt="Example images for captioning consisting of various animals, a palace and a tractor" src="https://cdn-images-1.medium.com/max/990/1*KmTTJhByhorswkrzV_8oFg.png" /><figcaption>Example images for captioning consisting of various animals, a palace and a tractor</figcaption></figure><p>The next step is to load the images into an Apache Spark DataFrame. <strong>Note that instead of reading the images as a Spark data source, we need to read the images in a different format. </strong>Using Spark&#39;s native reader, the images will be loaded to a OpenCV compatible format. However, llama.cpp and AutoGGUFVisionModel expect <em>raw</em> image bytes.</p><p>For this, we can use the helper function loadImagesAsBytes from the ImageAssembler. It will load the images in the right format in a Spark DataFrame. Additionally, we will add a column for the caption:</p><pre>from sparknlp.base import *<br>from pyspark.sql.functions import lit<br><br># Load images as raw bytes to Spark DataFrame<br>data = ImageAssembler.loadImagesAsBytes(spark, images_path)<br># Add a caption to each image.<br>data = data.withColumn(&quot;caption&quot;, lit(&quot;Caption this image.&quot;))</pre><p>Having the data loaded in the right format, we can construct our pipeline. We require an ImageAssembler and DocumentAssembler to turn the images and captions into the right format for Spark NLP. We also use the model we just loaded above. Then we can assemble a pipeline and run it!</p><pre>import sparknlp<br>from sparknlp.base import *<br>from sparknlp.annotator import *<br>from pyspark.ml import Pipeline<br><br>documentAssembler = (<br>    DocumentAssembler().setInputCol(&quot;caption&quot;).setOutputCol(&quot;caption_document&quot;)<br>)<br>imageAssembler = ImageAssembler().setInputCol(&quot;image&quot;).setOutputCol(&quot;image_assembler&quot;)<br><br>pipeline = Pipeline().setStages([documentAssembler, imageAssembler, autoGGUFModel])<br><br>pipeline.fit(data).transform(data).selectExpr(<br>    &quot;reverse(split(image.origin, &#39;/&#39;))[0] as image_name&quot;, &quot;completions.result&quot;<br>).show(truncate=False)</pre><p>And this is the result:</p><pre>+-----------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|image_name       |result                                                                                                                                                                                          |<br>+-----------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|bluetick.jpg     |[ A dog with a red collar is sitting on the floor.]                                                                                                                                             |<br>|chihuahua.jpg    |[ A small brown dog wearing a sweater and collar is sitting on the floor.]                                                                                                                      |<br>|egyptian_cat.jpeg|[ The image features two cats lying on a pink surface, possibly a bed or sofa. One cat is positioned towards the left side of the frame and appears to be sleeping while holding]               |<br>|hen.JPEG         |[ The image features a large white chicken standing next to several baby chicks. There are at least five visible chickens in the scene, with one adult and four young ones surrounding it. They]|<br>|hippopotamus.JPEG|[ A large brown hippo is swimming in a pond, with its head above the water. The hippo appears to be enjoying itself as it floats on top of the water.]                                          |<br>|junco.JPEG       |[ A small bird with a black head and white chest is standing on the snow.]                                                                                                                      |<br>|ostrich.JPEG     |[ A large ostrich stands in a grassy field, surrounded by trees and bushes. The bird is the main focus of the image with its long neck stretched out as it looks around at]                     |<br>|ox.JPEG          |[ A large bull with long horns is standing in a grassy field.]                                                                                                                                  |<br>|palace.JPEG      |[ The image depicts a large, ornate room with high ceilings and yellow walls. It features an elegant sitting area with several chairs arranged around the space. There are also multiple c]     |<br>|tractor.JPEG     |[ A man is sitting in the driver&#39;s seat of a green tractor, which has yellow wheels. The tractor appears to be parked on top of an agricultural field with rows of]                             |<br>+-----------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</pre><p>As we are running everything within Spark, <strong>the whole pipeline will be distributed automatically to your entire cluster</strong>. With batch inference enabled, you could potentially be processing hundreds of rows of your text and images in parallel!</p><h3>Conclusion</h3><p>Spark NLP 6.0 represents a major leap in large-scale AI by introducing the AutoGGUFVisionModel. In this post, we explored the implications of this model and showed how it can be used to caption images at scale (<a href="https://github.com/JohnSnowLabs/spark-nlp/blob/master/examples/python/llama.cpp/llama.cpp_in_Spark_NLP_AutoGGUFVisionModel.ipynb">link to notebook</a>).</p><p>By natively integrating efficient, quantized GGUF vision-language models via Llama.cpp into Spark pipelines, we overcome previous complexities and scalability barriers. This enables users to perform distributed, privacy-preserving multimodal inference on vast datasets, unlocking powerful new capabilities for tasks like image captioning, VQA, and document analysis, all within the familiar, scalable Spark NLP framework.</p><p><em>We constantly update Spark NLP with the latest state-of-the-art models! To keep up to date and read more, consider exploring the following</em>:</p><ul><li>Medium Publications: <a href="https://medium.com/spark-nlp">https://medium.com/spark-nlp</a></li><li>Spark NLP Blog: <a href="https://www.johnsnowlabs.com/spark-nlp-blog/">https://www.johnsnowlabs.com/spark-nlp-blog/</a></li><li>Home repository: <a href="https://github.com/JohnSnowLabs/spark-nlp">https://github.com/JohnSnowLabs/spark-nlp</a></li><li>Models Hub: <a href="https://nlp.johnsnowlabs.com/models">https://nlp.johnsnowlabs.com/models</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f084ad3bd705" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/vision-language-model-vlm-inference-at-scale-with-spark-nlp-6-0-llama-cpp-f084ad3bd705">Vision-Language Model (VLM) Inference at Scale with Spark NLP 6.0 + llama.cpp</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[RAG with Spark NLP]]></title>
            <link>https://medium.com/spark-nlp/rag-with-spark-nlp-61921c39349d?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/61921c39349d</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[spark]]></category>
            <category><![CDATA[large-language-models]]></category>
            <category><![CDATA[retrieval-augmented-gen]]></category>
            <category><![CDATA[nlp]]></category>
            <dc:creator><![CDATA[Paulami Bhattacharya]]></dc:creator>
            <pubDate>Mon, 12 May 2025 17:02:29 GMT</pubDate>
            <atom:updated>2025-05-12T17:02:28.932Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2A7p6N6HLAnaPjf2CrqbiA.png" /></figure><h3>Retrieval Augmented Generation (RAG) with Spark NLP</h3><p>RAG is the process of optimizing the output of a large language model, so it references an authoritative knowledge base outside its training data before generating a response.</p><p>It has 3 steps:</p><ol><li><strong>Ingestion</strong> — source data is ingested, cleaned, converted into embeddings, and stored in a vector database.</li><li><strong>Retrieval</strong> — the system finds the most relevant documents based on the user’s query.</li><li><strong>Generation</strong> — the retrieved information is fed into a language model to create a more accurate and context-aware response.</li></ol><p>In this tutorial, we show how to build a simple Retrieval-Augmented Generation (RAG) pipeline using Spark NLP. We load a dataset into Spark and use the AutoGGUFModel provided by Spark NLP to generate answers based on retrieved information.</p><p>Before we get started, download any text file to use as your dataset and setup a <a href="https://www.pinecone.io/">pinecone</a> instance. For this tutorial, we have stored the text from the Harry Potter Wikipedia page into a text file.</p><h3>Part 1 — Ingestion</h3><ol><li>Read your source text into a DataFrame.</li><li>Clean the text by tokenizing and normalizing.</li><li>Generate embeddings for your text.</li><li>Connect to pinecone server.</li><li>Create a collection to store your embeddings in pinecone.</li></ol><h4><strong>1.1 Insert embeddings into pinecone collection.</strong></h4><pre>from sparknlp.base import DocumentAssembler, Finisher<br>from sparknlp.annotator import SentenceDetector, AutoGGUFModel, Tokenizer, Normalizer<br>from sparknlp.annotator.embeddings import AutoGGUFEmbeddings<br>from pyspark.ml import Pipeline<br><br>text_data = (<br>  spark<br>  .read<br>  .text(&quot;harry-potter.txt&quot;)<br>  .withColumnRenamed(&quot;value&quot;, &quot;text&quot;) #Add your own text file<br>)<br><br>text_data.show()<br><br># OUTPUT<br>+--------------------+<br>|                text|<br>+--------------------+<br>|Harry Potter is a...|<br>+--------------------+</pre><h4>1.2 Clean the text by tokenizing and normalizing and generate embeddings for your text.</h4><p>We build a SparkNLP Pipeline with the following stages:</p><ul><li>DocumentAssembler: Entry annotator for our pipelines; it creates the data structure for the Annotation Framework.</li><li>SentenceDetector: Annotator to pragmatically separate complete sentences inside each document.</li><li>Tokenizer: Annotator used to convert text into tokens.</li><li>Normalizer: Annotator that cleans out tokens. Removes all dirty characters from text following a regex pattern and transforms words based on a provided dictionary.</li><li>Finisher: Converts the cleaned text back into a string sentence.</li><li>AutoGGUFEmbeddings: Used to generate embeddings for each sentence. Any embeddings of your choice can be used in this step.</li></ul><pre><br>from pyspark.sql.functions import monotonically_increasing_id<br>from pyspark.sql.types import StringType<br><br>document_assembler = (<br>    DocumentAssembler()<br>        .setInputCol(&quot;text&quot;)<br>        .setOutputCol(&quot;document&quot;)<br>)<br><br>sentence_detector = (<br>    SentenceDetector()<br>        .setInputCols([&quot;document&quot;])<br>        .setOutputCol(&quot;sentence&quot;)<br>        .setExplodeSentences(True)<br>)<br><br>tokenizer = (<br>    Tokenizer()<br>    .setInputCols(&quot;sentence&quot;)<br>    .setOutputCol(&quot;token&quot;)<br>)<br><br>normalizer = (<br>    Normalizer()<br>    .setInputCols(&quot;token&quot;)<br>    .setOutputCol(&quot;normalized&quot;)<br>)<br><br>finisher = (<br>    Finisher()<br>    .setInputCols(&quot;normalized&quot;)<br>    .setOutputCols(&quot;normalized_sentence&quot;)<br>    .setOutputAsArray(False)<br>    .setAnnotationSplitSymbol(&quot; &quot;)<br>)<br><br>normalized_document = (<br>    DocumentAssembler()<br>        .setInputCol(&quot;normalized_sentence&quot;)<br>        .setOutputCol(&quot;sentence&quot;)<br>)<br><br>embeddings = (<br>    AutoGGUFEmbeddings<br>    .pretrained(&quot;Nomic_Embed_Text_v1.5.Q8_0.gguf&quot;)<br>    .setInputCols([&quot;sentence&quot;])<br>    .setOutputCol(&quot;embeddings&quot;) \<br>    .setBatchSize(4) \<br>    .setNGpuLayers(99) \<br>    .setNCtx(8191)\<br>)<br><br>pipeline = Pipeline(<br>    stages=[<br>        document_assembler,<br>        sentence_detector,<br>        tokenizer,<br>        normalizer,<br>        finisher,<br>        normalized_document,<br>        embeddings,<br>    ]<br>)<br>result = pipeline.fit(text_data).transform(text_data)<br><br>sentence_and_embeddings_df = result.selectExpr(<br>    &quot;explode(sentence.result) as sentence&quot;,<br>    &quot;explode(embeddings.embeddings) as vector&quot;<br>)<br><br>df_pinecone = sentence_and_embeddings_df<br>                .withColumn(&quot;id&quot;, monotonically_increasing_id().cast(StringType())) \<br>                .withColumnRenamed(&quot;vector&quot;, &quot;values&quot;) \<br>                .withColumn(&quot;metadata&quot;, sentence_and_embeddings_df.sentence) \<br>                .drop(&quot;sentence&quot;)  # Store sentence as metadata, drop original column<br><br>df_pinecone.show()<br><br>#OUTPUT<br>+--------------------+---+--------------------+<br>|              values| id|            metadata|<br>+--------------------+---+--------------------+<br>|[0.009264344, 0.0...|  0|Harry Potter is a...|<br>|[-0.05384197, 0.0...|  1|The novels chroni...|<br>|[-0.056440134, -0...|  2|The main story ar...|<br>|[0.056693483, 0.0...|  3|The series was or...|<br>|[-0.041599147, 0....|  4|A series of many ...|<br>|[-0.030768316, 0....|  5|Major themes in t...|<br>|[6.1618997E-4, 0....|  6|Since the release...|<br>|[-0.047986094, -0...|  7|They have attract...|<br>|[0.027252585, 0.0...|  8|As of February th...|<br>|[0.04030519, 0.04...|  9|The last four boo...|<br>|[0.026217783, 0.0...| 10|It holds the Guin...|<br>|[0.007119997, -0....| 11|         Warner Bros|<br>|[0.0041893553, 0....| 12|Pictures adapted ...|<br>|[0.034328587, 0.0...| 13|In the total valu...|<br>|[0.017639026, 0.0...| 14|Harry Potter and ...|<br>|[-0.026738804, 0....| 15|A television seri...|<br>|[-0.02948736, 0.0...| 16|Themed attraction...|<br>|[-0.0024689648, 0...| 17|In the first book...|<br>|[-0.041317504, 0....| 18|At the age of Har...|<br>|[-0.059096724, 0....| 19|He meets a halfgi...|<br>+--------------------+---+--------------------+<br>only showing top 20 rows</pre><h4>1.3 Connect to Pinecone Server and create a collection to store your embeddings in Pinecone.</h4><pre>from pyspark.sql.functions import struct, array, lit<br>from pinecone import Pinecone<br>from pinecone import ServerlessSpec<br><br># Set these environment variables<br>URL = &lt;pinecone-url&gt;<br>API_KEY = &lt;pinecone-api-key&gt;<br>INDEX_NAME = &lt;index-name&gt;<br>EMBEDDING_DIM = 768<br><br>pc = Pinecone(api_key=API_KEY)<br><br>pc.create_index(<br>    name=INDEX_NAME,<br>    dimension=EMBEDDING_DIM,<br>    metric=&quot;cosine&quot;,<br>    spec=ServerlessSpec(<br>        cloud=&quot;aws&quot;,<br>        region=&quot;us-east-1&quot;<br>    )<br>)</pre><h4>1.4 Insert the embeddings into Pinecone.</h4><pre># Function to insert a batch of vectors into Pinecone<br>def insert_batch(rows):<br>    pc2 = Pinecone(api_key=API_KEY)<br>    index = pc2.Index(INDEX_NAME)<br>    vectors = []<br>    for row in rows:<br>        vector = {<br>            &quot;id&quot;: row.id,<br>            &quot;values&quot;: row.values,<br>            &quot;metadata&quot;: {&quot;text&quot;: row.metadata}<br>        }<br>        if hasattr(row, &quot;namespace&quot;) and row.namespace is not None:<br>            vector[&quot;namespace&quot;] = row.namespace<br>        vectors.append(vector)<br>    <br>    if vectors:<br>        index.upsert(vectors=vectors)<br><br># Convert DataFrame to RDD and process partitions in parallel<br>df_pinecone.rdd.foreachPartition(insert_batch)</pre><h3>Part 2 — Retrieval</h3><ol><li>Write your queries into a DataFrame.</li><li>Generate the embedding for the queries.</li><li>Query the pinecone vector database using the embeddings to find the relevant context.</li></ol><h4>2.1 Write your queries into a DataFrame.</h4><p>We create a DataFrame which contains our queries. And we run it through the same pipeline which was used to generate the embeddings in the ingestion step. As a result, we get a new DataFrame with the sentence and the embedding of the query. We store the embeddings into a list.</p><pre>from pyspark.sql.types import StringType, StructType, StructField<br>from pyspark.sql import Row<br><br># Create your own queries<br>queries = [<br>    Row(text=&quot;Who are Harry Potter&#39;s parents?&quot;),<br>    Row(text=&quot;Which House did Harry belong to?&quot;),<br>    Row(text=&quot;Who were Harry&#39;s friends?&quot;),<br>    Row(text=&quot;What are the major themes in the Harry Potter series?&quot;)<br>]<br><br>schema = StructType([StructField(&quot;text&quot;, StringType(), True)])<br>query_df = spark.createDataFrame(queries, schema)<br>transformed_query = pipeline.fit(query_df).transform(query_df)<br><br>transformed_query = transformed_query.selectExpr(<br>    &quot;explode(sentence.result) as sentence&quot;,  # Extract sentences<br>    &quot;explode(embeddings.embeddings) as vector&quot;  # Extract embeddings<br>)<br><br>vector_list = (<br>    transformed_query<br>    .select(&quot;vector&quot;)<br>    .rdd<br>    .flatMap(lambda x: x)<br>    .collect()<br>)<br><br>print(f&quot;Total queries: {len(vector_list)}&quot;)<br><br>#OUTPUT <br>4</pre><h4><strong>2.2 Query the Pinecone vector database using the embeddings to find the relevant context.</strong></h4><p>For each query in the list we use the query function from pinecone which fetches text from the ingested data that is relevant to our query. You can change the top_k parameter to set the number of relevant vectors that should be fetched from the vector database.</p><pre>from collections import defaultdict<br><br>query_and_context = defaultdict(list)<br>vector_database = pc.Index(INDEX_NAME)<br><br>for index, rag_query in enumerate(vector_list):<br>    response = vector_database.query(<br>        vector=rag_query,<br>        top_k=3,<br>        include_metadata=True<br>    )<br><br>    sentences = []<br>    matches = response[&quot;matches&quot;]<br>    for match in matches:<br>        context = match[&quot;metadata&quot;][&quot;text&quot;]<br>        sentences.append(context)<br><br>    for idx, sentence in enumerate(sentences, start=1):<br>        query_and_context[queries[index].text].append(sentence)<br><br>print(query_and_context)<br><br>#OUTPUT<br>defaultdict(&lt;class &#39;list&#39;&gt;, {&quot;Who are Harry Potter&#39;s parents?&quot;: [&#39;Harry learns that his parents Lily and James Potter also had magical powers and were murdered by the dark wizard Lord Voldemort when Harry was a baby&#39;, &#39;wizards of Muggle parentage are the primary targets&#39;, &#39;He gains the friendship of Ron Weasley a member of a large but poor wizarding family and Hermione Granger a witch of nonmagical or Muggle parentage&#39;], &#39;Which House did Harry belong to?&#39;: [&#39;The event made Harry famous among the community of wizards and witchesHarry becomes a student at Hogwarts and is sorted into Gryffindor House&#39;, &#39;Harry learns that his parents Lily and James Potter also had magical powers and were murdered by the dark wizard Lord Voldemort when Harry was a baby&#39;, &#39;In the first book Harry Potter and the Philosophers Stone Harry Potter and the Sorcerers Stone in the US Harry lives in a cupboard under the stairs in the house of the Dursleys his aunt uncle and cousin who all treat him poorly&#39;], &quot;Who were Harry&#39;s friends?&quot;: [&#39;The novels chronicle the lives of a young wizard Harry Potter and his friends Hermione Granger and Ron Weasley all of whom are students at Hogwarts School of Witchcraft and Wizardry&#39;, &#39;He gains the friendship of Ron Weasley a member of a large but poor wizarding family and Hermione Granger a witch of nonmagical or Muggle parentage&#39;, &#39;Lupin enters the shack and explains that Sirius was James Potters best friend&#39;], &#39;What are the major themes in the Harry Potter series?&#39;: [&#39;A series of many genres including fantasy drama comingofage fiction and the British school story which includes elements of mystery thriller adventure horror and romance the world of Harry Potter explores numerous themes and includes many cultural meanings and references&#39;, &#39;Major themes in the series include prejudice corruption madness love and death&#39;, &#39;Harry Potter is a series of seven fantasy novels written by British author J K Rowling&#39;]})</pre><h3>Part 3 — Generation</h3><ol><li>Setup the prompt assembler using the template to query the model.</li><li>Fill the prompt template with the query and the relevant context fetched from the retrieval step.</li><li>Pass the prompt to the LLM to receive an answer to your query.</li></ol><h4>3.1 Setup the prompt assembler using the template to query the model.</h4><p>Here, we define the default prompt template and use the PromptAssembler to set this template as the chat template.</p><pre>from sparknlp.base import *<br><br><br>template = (<br>    &quot;{{- bos_token }} {%- if custom_tools is defined %} {%- set tools = custom_tools %} {%- &quot;<br>    &quot;endif %} {%- if not tools_in_user_message is defined %} {%- set tools_in_user_message = true %} {%- &quot;<br>    &#39;endif %} {%- if not date_string is defined %} {%- set date_string = &quot;26 Jul 2024&quot; %} {%- endif %} &#39;<br>    &quot;{%- if not tools is defined %} {%- set tools = none %} {%- endif %} {#- This block extracts the &quot;<br>    &quot;system message, so we can slot it into the right place. #} {%- if messages[0][&#39;role&#39;] == &#39;system&#39; %}&quot;<br>    &quot; {%- set system_message = messages[0][&#39;content&#39;]|trim %} {%- set messages = messages[1:] %} {%- else&quot;<br>    &#39; %} {%- set system_message = &quot;&quot; %} {%- endif %} {#- System message + builtin tools #} {{- &#39;<br>    &#39;&quot;&lt;|start_header_id|&gt;system&lt;|end_header_id|&gt;\\n\\n&quot; }} {%- if builtin_tools is defined or tools is &#39;<br>    &#39;not none %} {{- &quot;Environment: ipython\\n&quot; }} {%- endif %} {%- if builtin_tools is defined %} {{- &#39;<br>    &#39;&quot;Tools: &quot; + builtin_tools | reject(\&#39;equalto\&#39;, \&#39;code_interpreter\&#39;) | join(&quot;, &quot;) + &quot;\\n\\n&quot;}} &#39;<br>    &#39;{%- endif %} {{- &quot;Cutting Knowledge Date: December 2023\\n&quot; }} {{- &quot;Today Date: &quot; + date_string &#39;<br>    &#39;+ &quot;\\n\\n&quot; }} {%- if tools is not none and not tools_in_user_message %} {{- &quot;You have access to &#39;<br>    &#39;the following functions. To call a function, please respond with JSON for a function call.&quot; }} {{- &#39;<br>    &#39;\&#39;Respond in the format {&quot;name&quot;: function name, &quot;parameters&quot;: dictionary of argument name and its&#39;<br>    &#39; value}.\&#39; }} {{- &quot;Do not use variables.\\n\\n&quot; }} {%- for t in tools %} {{- t | tojson(indent=4) &#39;<br>    &#39;}} {{- &quot;\\n\\n&quot; }} {%- endfor %} {%- endif %} {{- system_message }} {{- &quot;&lt;|eot_id|&gt;&quot; }} {#- &#39;<br>    &quot;Custom tools are passed in a user message with some extra guidance #} {%- if tools_in_user_message &quot;<br>    &quot;and not tools is none %} {#- Extract the first user message so we can plug it in here #} {%- if &quot;<br>    &quot;messages | length != 0 %} {%- set first_user_message = messages[0][&#39;content&#39;]|trim %} {%- set &quot;<br>    &#39;messages = messages[1:] %} {%- else %} {{- raise_exception(&quot;Cannot put tools in the first user &#39;<br>    &quot;message when there&#39;s no first user message!\&quot;) }} {%- endif %} {{- &quot;<br>    &quot;&#39;&lt;|start_header_id|&gt;user&lt;|end_header_id|&gt;\\n\\n&#39; -}} {{- \&quot;Given the following functions, please &quot;<br>    &#39;respond with a JSON for a function call &quot; }} {{- &quot;with its proper arguments that best answers the &#39;<br>    &#39;given prompt.\\n\\n&quot; }} {{- \&#39;Respond in the format {&quot;name&quot;: function name, &quot;parameters&quot;: &#39;<br>    &#39;dictionary of argument name and its value}.\&#39; }} {{- &quot;Do not use variables.\\n\\n&quot; }} {%- for t in &#39;<br>    &#39;tools %} {{- t | tojson(indent=4) }} {{- &quot;\\n\\n&quot; }} {%- endfor %} {{- first_user_message + &#39;<br>    &quot;\&quot;&lt;|eot_id|&gt;\&quot;}} {%- endif %} {%- for message in messages %} {%- if not (message.role == &#39;ipython&#39; &quot;<br>    &quot;or message.role == &#39;tool&#39; or &#39;tool_calls&#39; in message) %} {{- &#39;&lt;|start_header_id|&gt;&#39; + message[&#39;role&#39;]&quot;<br>    &quot; + &#39;&lt;|end_header_id|&gt;\\n\\n&#39;+ message[&#39;content&#39;] | trim + &#39;&lt;|eot_id|&gt;&#39; }} {%- elif &#39;tool_calls&#39; in &quot;<br>    &#39;message %} {%- if not message.tool_calls|length == 1 %} {{- raise_exception(&quot;This model only &#39;<br>    &#39;supports single tool-calls at once!&quot;) }} {%- endif %} {%- set tool_call = message.tool_calls[0]&#39;<br>    &quot;.function %} {%- if builtin_tools is defined and tool_call.name in builtin_tools %} {{- &quot;<br>    &quot;&#39;&lt;|start_header_id|&gt;assistant&lt;|end_header_id|&gt;\\n\\n&#39; -}} {{- \&quot;&lt;|python_tag|&gt;\&quot; + tool_call.name + &quot;<br>    &#39;&quot;.call(&quot; }} {%- for arg_name, arg_val in tool_call.arguments | items %} {{- arg_name + \&#39;=&quot;\&#39; + &#39;<br>    &#39;arg_val + \&#39;&quot;\&#39; }} {%- if not loop.last %} {{- &quot;, &quot; }} {%- endif %} {%- endfor %} {{- &quot;)&quot; }} {%- &#39;<br>    &quot;else %} {{- &#39;&lt;|start_header_id|&gt;assistant&lt;|end_header_id|&gt;\\n\\n&#39; -}} {{- &#39;{\&quot;name\&quot;: \&quot;&#39; + &quot;<br>    &#39;tool_call.name + \&#39;&quot;, \&#39; }} {{- \&#39;&quot;parameters&quot;: \&#39; }} {{- tool_call.arguments | tojson }} {{- &quot;}&quot; &#39;<br>    &quot;}} {%- endif %} {%- if builtin_tools is defined %} {#- This means we&#39;re in ipython mode #} {{- &quot;<br>    &#39;&quot;&lt;|eom_id|&gt;&quot; }} {%- else %} {{- &quot;&lt;|eot_id|&gt;&quot; }} {%- endif %} {%- elif message.role == &quot;tool&quot; &#39;<br>    &#39;or message.role == &quot;ipython&quot; %} {{- &quot;&lt;|start_header_id|&gt;ipython&lt;|end_header_id|&gt;\\n\\n&quot; }} {%- &#39;<br>    &quot;if message.content is mapping or message.content is iterable %} {{- message.content | tojson }} {%- &quot;<br>    &#39;else %} {{- message.content }} {%- endif %} {{- &quot;&lt;|eot_id|&gt;&quot; }} {%- endif %} {%- endfor %} {%- if &#39;<br>    &quot;add_generation_prompt %} {{- &#39;&lt;|start_header_id|&gt;assistant&lt;|end_header_id|&gt;\\n\\n&#39; }} {%- endif %} &quot;<br>)<br><br>promptAssembler = (<br>    PromptAssembler()<br>    .setInputCol(&quot;messages&quot;)<br>    .setOutputCol(&quot;prompt&quot;)<br>    .setChatTemplate(template)<br>)</pre><h4>3.2 Fill the prompt template with the query and the relevant context fetched from the retrieval step.</h4><p>Now we populate our prompt template with the query and the context as shown below. Then we convert our prompts into a DataFrame to pass it to the prompt assembler. Below we can see the output of the prompt template with the query and the context that is fetched from the vector database.</p><pre>prompts = []<br>for query, context in query_and_context.items():<br>    messages = [<br>        (&quot;system&quot;, &quot;You are a question answering system. You will be given a query and some context, you need to answer the query based on the context provided. Use your own knowledge if relevant context is not provided. Give your answer as a full sentence with minimum text.&quot;),<br>        (&quot;assistant&quot;, &quot;Hello there! What is your query today?&quot;),<br>        (&quot;user&quot;, f&quot;Query: {query} Context: {&#39;&#39;.join(context)}&quot;),<br>    ]<br>    prompts.append([messages])<br><br>promptDF = spark.createDataFrame(prompts, [&quot;messages&quot;])<br>promptDF.show()<br><br>#OUTPUT<br>+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|messages                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |<br>+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+<br>|[{system, You are a question answering system. You will be given a query and some context, you need to answer the query based on the context provided. Use your own knowledge if relevant context is not provided. Give your answer as a full sentence with minimum text.}, {assistant, Hello there! What is your query today?}, {user, Query: Who are Harry Potter&#39;s parents? Context: Harry learns that his parents Lily and James Potter also had magical powers and were murdered by the dark wizard Lord Voldemort when Harry was a babywizards of Muggle parentage are the primary targetsHe gains the friendship of Ron Weasley a member of a large but poor wizarding family and Hermione Granger a witch of nonmagical or Muggle parentage}]                                                                                                                                                                           |<br>|[{system, You are a question answering system. You will be given a query and some context, you need to answer the query based on the context provided. Use your own knowledge if relevant context is not provided. Give your answer as a full sentence with minimum text.}, {assistant, Hello there! What is your query today?}, {user, Query: Which House did Harry belong to? Context: The event made Harry famous among the community of wizards and witchesHarry becomes a student at Hogwarts and is sorted into Gryffindor HouseHarry learns that his parents Lily and James Potter also had magical powers and were murdered by the dark wizard Lord Voldemort when Harry was a babyIn the first book Harry Potter and the Philosophers Stone Harry Potter and the Sorcerers Stone in the US Harry lives in a cupboard under the stairs in the house of the Dursleys his aunt uncle and cousin who all treat him poorly}]|<br>|[{system, You are a question answering system. You will be given a query and some context, you need to answer the query based on the context provided. Use your own knowledge if relevant context is not provided. Give your answer as a full sentence with minimum text.}, {assistant, Hello there! What is your query today?}, {user, Query: Who were Harry&#39;s friends? Context: The novels chronicle the lives of a young wizard Harry Potter and his friends Hermione Granger and Ron Weasley all of whom are students at Hogwarts School of Witchcraft and WizardryHe gains the friendship of Ron Weasley a member of a large but poor wizarding family and Hermione Granger a witch of nonmagical or Muggle parentageLupin enters the shack and explains that Sirius was James Potters best friend}]                                                                                                                       |<br>|[{system, You are a question answering system. You will be given a query and some context, you need to answer the query based on the context provided. Use your own knowledge if relevant context is not provided. Give your answer as a full sentence with minimum text.}, {assistant, Hello there! What is your query today?}, {user, Query: What are the major themes in the Harry Potter series? Context: A series of many genres including fantasy drama comingofage fiction and the British school story which includes elements of mystery thriller adventure horror and romance the world of Harry Potter explores numerous themes and includes many cultural meanings and referencesMajor themes in the series include prejudice corruption madness love and deathHarry Potter is a series of seven fantasy novels written by British author J K Rowling}]                                                             |<br>+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</pre><p>Load the AutoGGUFModel which loads phi3.5_mini_4k_instruct_q4_gguf model by default. Any LLM of your choice can be used in this step.</p><pre>from sparknlp.annotator import AutoGGUFModel<br><br>autoGGUFModel = (<br>    AutoGGUFModel<br>    .pretrained()<br>    .setInputCols(&quot;prompt&quot;)<br>    .setOutputCol(&quot;completions&quot;)<br>    .setBatchSize(4)<br>    .setNGpuLayers(99)<br>    .setUseChatTemplate(True)  <br>)</pre><h4>3.3 Pass the prompt to the LLM to receive an answer to your query.</h4><p>Now we build a SparkNLP pipeline with the following stages:</p><p>PromptAssembler: Annotator to fill prompt templates with relevant text.</p><p>AutoGGUFModel: LLM which completes our prompts.</p><pre>generationPipeline = Pipeline(stages=[promptAssembler, autoGGUFModel])<br>output = generationPipeline.fit(promptDF).transform(promptDF)</pre><p>Let’s check our final results from the AutoGGUFModel.</p><pre>final_output = output.selectExpr(<br>    &quot;explode(completions.result) as output&quot;<br>)<br><br>final_output.show()<br><br>#OUTPUT<br>+--------------------------------------------------------------------------------------------------------+<br>|output                                                                                                  |<br>+--------------------------------------------------------------------------------------------------------+<br>|Harry Potter&#39;s parents are Lily and James Potter.                                                       |<br>|Harry belonged to Gryffindor House at Hogwarts School of Witchcraft and Wizardry.                       |<br>|Harry&#39;s friends were Ron Weasley and Hermione Granger.\n\n                                              |<br>|The major themes in the Harry Potter series include prejudice, corruption, madness, love, and death.\n\n|<br>+--------------------------------------------------------------------------------------------------------+<br></pre><p>With this RAG pipeline, you’re now equipped to unlock richer insights and build smarter applications — time to put your data into action! ✨</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=61921c39349d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/rag-with-spark-nlp-61921c39349d">RAG with Spark NLP</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ Spark NLP 6.0.0: A New Era for Universal Ingestion and Multimodal LLM Processing at Scale]]></title>
            <link>https://medium.com/spark-nlp/spark-nlp-6-0-0-a-new-era-for-universal-ingestion-and-multimodal-llm-processing-at-scale-9078ff5859b4?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/9078ff5859b4</guid>
            <category><![CDATA[spark]]></category>
            <category><![CDATA[spark-nlp]]></category>
            <category><![CDATA[qwen]]></category>
            <category><![CDATA[llama-cpp]]></category>
            <category><![CDATA[vision]]></category>
            <dc:creator><![CDATA[Maziyar Panahi]]></dc:creator>
            <pubDate>Mon, 28 Apr 2025 19:06:27 GMT</pubDate>
            <atom:updated>2025-04-28T19:12:55.577Z</atom:updated>
            <content:encoded><![CDATA[<h3>From Raw Documents to Multimodal Insights at Enterprise Scale</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mgxWmZirJREfZCzs90B9ZQ.png" /></figure><p>Spark NLP 6.0.0 marks a monumental shift — from being the leading NLP library to becoming the <strong>de facto platform for distributed LLM ingestion and multimodal processing</strong>.</p><p>In this release, we <strong>expand Spark NLP beyond text</strong>:</p><ul><li>Ingest <strong>PDFs, Excel files, PowerPoint presentations, and text logs</strong> natively into Spark pipelines.</li><li>Automatically extract structure, semantics, metadata — at scale, without writing a single line of parsing code.</li><li>Perform <strong>batch multimodal inference</strong> by running <strong>quantized Vision-Language Models (VLMs)</strong> like <strong>LLAVA</strong>, <strong>Phi-3 Vision</strong>, <strong>DeepSeek Janus</strong>, and <strong>Llama 3.2 Vision</strong> <strong>natively</strong> in Spark — no servers, no API bottlenecks.</li></ul><p>With Spark NLP 6.0.0, building <strong>enterprise-grade RAG</strong>, <strong>document understanding</strong>, <strong>compliance audits</strong>, and <strong>multimodal analytics pipelines</strong> becomes frictionless.</p><p>One unified framework. Text, vision, documents — at Spark scale.</p><h3>✨ Spotlight Feature: AutoGGUFVisionModel</h3><h3>Native Multimodal Inference with Llama.cpp</h3><p>Spark NLP 6.0.0 introduces the groundbreaking AutoGGUFVisionModel, enabling <strong>vision-language model inference inside DataFrames</strong> — directly through <strong>Llama.cpp</strong>.</p><p>You can now:</p><ul><li>Pass <strong>raw image bytes</strong> and <strong>captions</strong> into a multimodal LLM.</li><li>Generate <strong>descriptions</strong>, <strong>summaries</strong>, or <strong>visual Q&amp;A</strong> results.</li><li>Run fully <strong>on-premises</strong> at <strong>Spark-native scale</strong>.</li></ul><h3>Why it Matters</h3><ul><li>Unlock <strong>pure multimodal workflows</strong> with zero infrastructure setup.</li><li>Perform <strong>massive batch inference</strong> across product catalogs, document archives, compliance audits, and more.</li><li>Enjoy full control over inference parameters like topK, topP, temperature, and nPredict.</li></ul><h3>How it Works</h3><pre>documentAssembler = DocumentAssembler().setInputCol(&quot;caption&quot;).setOutputCol(&quot;caption_document&quot;)</pre><pre>imageAssembler = ImageAssembler().setInputCol(&quot;image&quot;).setOutputCol(&quot;image_assembler&quot;)</pre><pre>data = ImageAssembler.loadImagesAsBytes(spark, &quot;src/test/resources/image/&quot;)\<br>    .withColumn(&quot;caption&quot;, lit(&quot;Caption this image.&quot;))</pre><pre>model = AutoGGUFVisionModel.pretrained()\<br>    .setInputCols([&quot;caption_document&quot;, &quot;image_assembler&quot;])\<br>    .setOutputCol(&quot;completions&quot;)\<br>    .setBatchSize(4)\<br>    .setNPredict(40)\<br>    .setTopK(40)\<br>    .setTopP(0.95)\<br>    .setTemperature(0.05)</pre><pre>pipeline = Pipeline().setStages([documentAssembler, imageAssembler, model])<br>results = pipeline.fit(data).transform(data)</pre><pre>results.selectExpr(&quot;reverse(split(image.origin, &#39;/&#39;))[0] as image_name&quot;, &quot;completions.result&quot;).show(truncate=False)</pre><p>📚 <a href="https://github.com/DevinTDHa/spark-nlp/blob/feature/SPARKNLP-1079-AutoGGUFVisionModel/examples/python/llama.cpp/llama.cpp_in_Spark_NLP_AutoGGUFVisionModel.ipynb"><strong>Full notebook walkthrough available here</strong></a></p><h3>🔥 New Features and Enhancements</h3><h3>Universal Document Ingestion</h3><ul><li><strong>PDF Reader</strong>: Font-aware ingestion, page segmentation, encrypted file support.</li><li><strong>Excel Reader</strong>: Native .xls and .xlsx ingestion with multi-sheet support.</li><li><strong>PowerPoint Reader</strong>: Capture slides, speaker notes, themes, alt text.</li><li><strong>Text Reader</strong>: Load .txt, .csv, .log files at scale with encoding detection.</li></ul><h3>Multimodal &amp; Vision-Language Modeling</h3><ul><li><strong>AutoGGUFVisionModel</strong>: Native multimodal batch inference with Llama.cpp.</li><li><strong>DeepSeek Janus Integration</strong>: Advanced instruction-following across text and images.</li><li><strong>Qwen-2 Vision Language Models</strong>: Multilingual multimodal understanding.</li><li><strong>Phi-3.5 Vision</strong>: Lightweight visual reasoning models under 1 GB.</li><li><strong>LLAVA 1.5</strong>: Screenshot Q&amp;A, chart reading, UI testing in Spark.</li></ul><h3>Massive LLM Catalog Expansion</h3><ul><li><strong>Cohere Command-R Support</strong>: Up to 35B multilingual models natively.</li><li><strong>OLMo Family</strong>: Open-weight, reproducible LLMs tuned for academic and benchmark tasks.</li><li><strong>Multiple-Choice Heads</strong>: Lightweight heads for ALBERT, RoBERTa, XLM-RoBERTa.</li></ul><h3>Infrastructure Upgrades</h3><ul><li><strong>VisionEncoderDecoder Improvements</strong>: Full Scala/Python API parity.</li><li><strong>Better GGUF Error Reporting</strong>: Actionable fixes for model compatibility.</li></ul><h3>🐛 Key Bug Fixes</h3><ul><li>Clearer error messages for GGUF models.</li><li>Fixed MXBAI typo across notebooks.</li><li>Full alignment of VisionEncoderDecoder APIs between Scala and Python.</li><li>Smoothed variable naming across the entire codebase.</li></ul><h3>📝 Models, Models, and More Models</h3><p>Over <strong>110,000 new models and pipelines</strong> have been added — spanning <strong>230+ languages</strong>.<br>Explore them all on the <a href="https://sparknlp.org/models"><strong>Models Hub</strong></a>.</p><h3>❤️ Community Support</h3><ul><li><a href="https://join.slack.com/t/spark-nlp/shared_invite/zt-198dipu77-L3UWNe_AJ8xqDk0ivmih5Q">Slack</a> — Chat with us live</li><li><a href="https://github.com/JohnSnowLabs/spark-nlp">GitHub</a> — Report issues, suggest features</li><li><a href="https://medium.com/spark-nlp">Medium</a> — Articles and tutorials</li><li><a href="https://www.youtube.com/channel/UCmFOjlpYEhxf_wJUDuz6xxQ/videos">YouTube</a> — Video walkthroughs</li></ul><h3>📦 Installation</h3><p><strong>PyPI</strong></p><pre>pip install spark-nlp==6.0.0</pre><p><strong>Spark Packages</strong></p><pre># CPU<br>spark-shell --packages com.johnsnowlabs.nlp:spark-nlp_2.12:6.0.0</pre><pre># GPU<br>spark-shell --packages com.johnsnowlabs.nlp:spark-nlp-gpu_2.12:6.0.0</pre><pre># Apple Silicon (M1, M2)<br>spark-shell --packages com.johnsnowlabs.nlp:spark-nlp-silicon_2.12:6.0.0</pre><pre># AArch64<br>spark-shell --packages com.johnsnowlabs.nlp:spark-nlp-aarch64_2.12:6.0.0</pre><p><strong>FAT JARs</strong></p><ul><li><a href="https://s3.amazonaws.com/auxdata.johnsnowlabs.com/public/jars/spark-nlp-assembly-6.0.0.jar">CPU Assembly JAR</a></li><li><a href="https://s3.amazonaws.com/auxdata.johnsnowlabs.com/public/jars/spark-nlp-gpu-assembly-6.0.0.jar">GPU Assembly JAR</a></li><li><a href="https://s3.amazonaws.com/auxdata.johnsnowlabs.com/public/jars/spark-nlp-silicon-assembly-6.0.0.jar">M1 Assembly JAR</a></li><li><a href="https://s3.amazonaws.com/auxdata.johnsnowlabs.com/public/jars/spark-nlp-aarch64-assembly-6.0.0.jar">AArch64 Assembly JAR</a></li></ul><h3>🚀 Full Changelog</h3><p>Want even more detail? Dive into the <a href="https://github.com/JohnSnowLabs/spark-nlp/compare/5.5.3...6.0.0"><strong>full changelog</strong></a>.</p><h3>🌟 Final Words</h3><p>Spark NLP 6.0.0 is not just a version update — it is a <strong>complete evolution of the platform</strong>.</p><p>From <strong>text</strong>, to <strong>vision</strong>, to <strong>multimodal reasoning</strong> — <strong>at Spark scale</strong>, <strong>zero servers</strong>, <strong>maximum performance</strong>.</p><p>The future of distributed AI pipelines has arrived.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9078ff5859b4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/spark-nlp-6-0-0-a-new-era-for-universal-ingestion-and-multimodal-llm-processing-at-scale-9078ff5859b4">🚀 Spark NLP 6.0.0: A New Era for Universal Ingestion and Multimodal LLM Processing at Scale</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing NLLB: Breaking Language Barriers with Multilingual Translation in Spark NLP]]></title>
            <link>https://medium.com/spark-nlp/introducing-nllb-breaking-language-barriers-with-multilingual-translation-in-spark-nlp-d95338551fd5?source=rss----81310c8f4868---4</link>
            <guid isPermaLink="false">https://medium.com/p/d95338551fd5</guid>
            <category><![CDATA[nllb]]></category>
            <category><![CDATA[language-model]]></category>
            <category><![CDATA[translation]]></category>
            <category><![CDATA[multi-lingual-model]]></category>
            <category><![CDATA[spark-nlp]]></category>
            <dc:creator><![CDATA[Muhammad Abdullah]]></dc:creator>
            <pubDate>Thu, 14 Nov 2024 09:29:47 GMT</pubDate>
            <atom:updated>2024-11-14T09:29:47.132Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6t7Jun9FMAmI6jW4EpyN5g.png" /></figure><p>As Natural Language Processing (NLP) continues to break new ground, Spark NLP’s integration of the <em>No Language Left Behind</em> (NLLB) model marks a major step forward in multilingual machine translation. NLLB, a state-of-the-art encoder-decoder model, is designed to tackle the complexities of translating over 200 languages with remarkable accuracy, even for low-resource languages. With this latest addition, Spark NLP provides a powerful tool for developers and researchers to create highly efficient and versatile language translation pipelines.</p><h3>Key Features of NLLB</h3><p>NLLB stands out as an essential tool for multilingual translation, providing a robust and scalable solution for both common and underrepresented languages. Here’s a breakdown of its major capabilities:</p><h4>1. Unmatched Language Coverage</h4><p>NLLB directly supports translation between over 200 languages, from widely spoken ones like <strong>English</strong>, <strong>Spanish</strong>, and <strong>Chinese </strong>to low-resource languages such as <strong>Kashmiri</strong>, <strong>Tswana</strong>, and <strong>Quechua</strong>. This makes it a powerful model for organizations and developers looking to break through language barriers on a global scale.</p><h4>2. Efficient Multilingual Translation</h4><p>Unlike traditional models, NLLB excels at translating <em>many-to-many</em> language pairs without requiring an intermediary language like English. This direct approach reduces translation latency and improves the quality of translations, even for languages that lack extensive training data.</p><h4>3. Optimized for Low-Resource Languages</h4><p>One of NLLB’s core strengths is its ability to perform well with low-resource languages. By incorporating novel data mining techniques and a Sparsely Gated Mixture of Experts model architecture, NLLB narrows the performance gap between high and low-resource languages, ensuring consistent quality across the board.</p><h4>4. Customizable Translation Pipelines</h4><p>Spark NLP makes it easy to integrate and customize NLLB for various translation tasks. With parameters for setting source and target languages, output length, and decoding strategies, users have full control over the translation process, from casual content generation to high-stakes multilingual communication.</p><h3>Simplicity of Integration</h3><p>Getting started with NLLB in Spark NLP is as simple as a few lines of code. Here’s a quick example:</p><pre>nllb = NLLBTransformer.pretrained() \\<br>     .setInputCols([&quot;document&quot;]) \\<br>     .setOutputCol(&quot;generation&quot;)</pre><h3>Tailoring Translations to Your Needs</h3><p>The NLLB Transformer in Spark NLP offers several parameters that allow users to fine-tune translations according to their specific requirements. Here are a few key parameters that provide control over the output:</p><ul><li><strong>maxOutputLength(int)</strong>: Limits the length of the translated text, preventing unnecessarily long outputs.</li><li><strong>temperature(float)</strong>: Controls the randomness in the prediction, balancing between deterministic and creative translations.</li><li><strong>topK(int)</strong>: Allows for top-K sampling, where only the most probable tokens are considered, ensuring translations maintain contextual accuracy.</li><li><strong>repetitionPenalty(float)</strong>: Reduces the likelihood of repeated phrases or words, ensuring smooth and coherent translations.</li></ul><h3>NLLB in Action: A Sample Multilingual Translation Pipeline</h3><p>Building a multilingual translation pipeline with NLLB in Spark NLP is as simple as the following example, which translates a sentence from Chinese to English:</p><pre>!pip install spark-nlp pyspark<br><br>from sparknlp.base import DocumentAssembler<br>from sparknlp.annotator import NLLBTransformer<br>from pyspark.ml import Pipeline<br><br># Step 1: Initialize Document Assembler<br>documentAssembler = DocumentAssembler() \<br>    .setInputCol(&quot;text&quot;) \<br>    .setOutputCol(&quot;documents&quot;)<br><br># Step 2: Load the pretrained NLLB model<br>nllb = NLLBTransformer.pretrained(&quot;nllb_418M&quot;) \<br>    .setInputCols([&quot;documents&quot;]) \<br>    .setOutputCol(&quot;generation&quot;) \<br>    .setSrcLang(&quot;zho_Hans&quot;) \<br>    .setTgtLang(&quot;eng_Latn&quot;)<br><br># Step 3: Build the Spark NLP pipeline<br>pipeline = Pipeline().setStages([documentAssembler, nllb])<br><br># Step 4: Input DataFrame with Chinese text<br>data = spark.createDataFrame([[&quot;生活就像一盒巧克力。&quot;]]).toDF(&quot;text&quot;)<br><br># Step 5: Run the pipeline<br>result = pipeline.fit(data).transform(data)<br><br># Step 6: Display the generated translation<br>result.select(&quot;generation&quot;).show(truncate=False)</pre><p>In this example, NLLB is used to translate Chinese to English, generating the sentence “Life is like a box of chocolates.” This highlights NLLB’s ability to handle diverse languages and produce high-quality translations in real-time.</p><h3>Performance Considerations</h3><p>Given the extensive number of languages supported, NLLB models are optimized for efficient performance. However, for high-throughput or long-sequence translations, it’s recommended to deploy the model on GPU-accelerated environments. This ensures faster inference times and maintains translation quality at scale.</p><h3>Conclusion</h3><p>Adding NLLB to Spark NLP opens up new possibilities for multilingual translation, particularly for low-resource languages. With its expansive language coverage, customizable parameters, and ease of integration, NLLB enables developers and researchers to break down language barriers with unprecedented accuracy. Whether working on cross-lingual chatbots, document translation, or international content creation, NLLB offers the flexibility and power you need to succeed in a globalized world.</p><p>📄 <strong>Full Release Notes</strong>: <a href="https://github.com/JohnSnowLabs/spark-nlp/releases/tag/5.5.0">Spark NLP 5.5.0 Release Notes</a></p><h3>For further reading and resources, consider exploring the following:</h3><ul><li><a href="https://arxiv.org/abs/2207.04672"><strong>NLLB Research Paper</strong></a>: Read Meta AI’s research on NLLB models.</li><li><a href="https://github.com/gordicaleksa/Open-NLLB"><strong>GitHub — Open NLLB</strong>:</a> A community effort to provide open-source versions of NLLB checkpoints and related tools.</li><li><strong>GitHub — Spark NLP:</strong> <a href="https://github.com/JohnSnowLabs/spark-nlp">https://github.com/JohnSnowLabs/spark-nlp</a></li><li><strong>Models Hub:</strong> <a href="https://nlp.johnsnowlabs.com/models">https://nlp.johnsnowlabs.com/models</a></li><li><strong>More examples:</strong> <a href="https://github.com/JohnSnowLabs/spark-nlp-workshop">https://github.com/JohnSnowLabs/spark-nlp-workshop</a></li></ul><h3>❤️ Community Support</h3><ul><li><a href="https://join.slack.com/t/spark-nlp/shared_invite/zt-198dipu77-L3UWNe_AJ8xqDk0ivmih5Q"><strong>Slack</strong></a><strong>:</strong> Join the Spark NLP community and team for live discussions.</li><li><a href="https://github.com/JohnSnowLabs/spark-nlp/discussions"><strong>Discussions</strong></a>: Engage with community members, share ideas, and showcase how you use Spark NLP!</li><li><a href="https://medium.com/spark-nlp"><strong>Medium</strong></a>: Read Spark NLP articles on its official Medium page.</li><li><a href="https://www.youtube.com/channel/UCmFOjlpYEhxf_wJUDuz6xxQ/videos"><strong>YouTube</strong></a>: Watch Spark NLP video tutorials for in-depth guidance.</li></ul><p>Join the Spark NLP community for support, discussions, and insights into the latest advancements in NLP!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d95338551fd5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/spark-nlp/introducing-nllb-breaking-language-barriers-with-multilingual-translation-in-spark-nlp-d95338551fd5">Introducing NLLB: Breaking Language Barriers with Multilingual Translation in Spark NLP</a> was originally published in <a href="https://medium.com/spark-nlp">spark-nlp</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>