This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new f27b0a9fb1 TIKA-4889:  Deprecate and document still experimental in 
4.1.0 (#3174)
f27b0a9fb1 is described below

commit f27b0a9fb104ede2a13a77888de565a1e78e0702
Author: Tim Allison <[email protected]>
AuthorDate: Tue Sep 15 15:54:35 2026 -0400

    TIKA-4889:  Deprecate and document still experimental in 4.1.0 (#3174)
---
 CHANGES.txt                                        |  19 +++-
 docs/modules/ROOT/pages/configuration/index.adoc   |  26 +++---
 .../ROOT/pages/configuration/inference.adoc        |  17 ++--
 .../pages/configuration/parsers/tess4j-parser.adoc |   4 +-
 .../parsers/tesseract-ocr-parser.adoc              |   4 +-
 .../pages/configuration/parsers/vlm-parsers.adoc   |  25 +++--
 .../ROOT/pages/developers/custom-ocr-engine.adoc   |   6 +-
 .../pages/migration-to-4x/chunk-strategies.adoc    |  24 +++--
 .../pages/migration-to-4x/migrating-to-4x.adoc     |  39 +++++---
 .../tika/inference/ImageEmbeddingConfig.java       |   3 +
 .../tika/inference/OpenAIImageEmbeddingParser.java |  13 ++-
 .../config-examples/claude-vlm-basic.json          |   9 +-
 .../resources/config-examples/claude-vlm-full.json |   9 +-
 .../config-examples/gemini-vlm-basic.json          |   9 +-
 .../resources/config-examples/gemini-vlm-full.json |   9 +-
 .../config-examples/openai-vlm-basic.json          |   9 +-
 .../resources/config-examples/openai-vlm-full.json |   9 +-
 .../apache/tika/config/loader/ParserLoader.java    | 104 ++++++++++++---------
 .../config/loader/ContentEnricherLoaderTest.java   |  31 ++++--
 .../sample-configs/vision/vlm-claude.json          |  12 ++-
 .../sample-configs/vision/vlm-gemini.json          |  12 ++-
 .../sample-configs/vision/vlm-openai.json          |  11 ++-
 22 files changed, 261 insertions(+), 143 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index bf78778206..1c7aeca008 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,19 @@
 Release 4.1.0 - unreleased
 
+   * An inference engine named under "parsers" (an OCR engine, a VLM parser,
+     openai-image-embedding-parser) is deprecated: since 4.1.0 startup logs
+     one WARN per such entry saying what it does today, and in 4.2.0 the
+     entry fails config load. Configure engines under "engines" and name them
+     in "text-recognizers" or bind them with "inference". The whole-PDF VLM
+     shape (claude-vlm-parser with pdf-parser excluded) goes with it; the
+     replacement is pdf-parser "text": "OCR" with the VLM as the recognizer
+     (TIKA-4889).
+
+   * openai-image-embedding-parser is deprecated, removal in 4.2.0: the same
+     endpoint as an openai-embedding-engine with an IMAGES inference binding
+     embeds a document's images in requests of the engine's batch size
+     (TIKA-4889).
+     
    * PDF: "pdf-parser": {"rendering": {"dpi", "imageType", "imageFormat",
      "imageQuality", "maxImagePixels"}} sets the page images emitted under
      RENDER_PAGES_BEFORE_PARSE and RENDER_PAGES_AT_PAGE_END; a field left out
@@ -91,8 +105,9 @@ Release 4.1.0 - unreleased
 
    * New "inference" config section: bind a named engine to an input (IMAGES)
      and tasks (embed), and every image document Tika parses is embedded in
-     one request per document tree; vectors land on the document the image
-     belongs to, as it appears in the output. Per request, "inference" in
+     requests of the engine's batch size, per document tree, not one per
+     image; vectors land on the document the image belongs to, as it appears
+     in the output. Per request, "inference" in
      parse-context selects bindings or switches inference off.
      openai-image-embedding-parser keeps working, one request per image
      (TIKA-4889).
diff --git a/docs/modules/ROOT/pages/configuration/index.adoc 
b/docs/modules/ROOT/pages/configuration/index.adoc
index de6d6ddb2c..90b6b248fc 100644
--- a/docs/modules/ROOT/pages/configuration/index.adoc
+++ b/docs/modules/ROOT/pages/configuration/index.adoc
@@ -205,20 +205,22 @@ found on the classpath; among those the last registered 
wins (user-supplied clas
 after Tika's); an entry's `_mime-include`/`_mime-exclude` applies, and on the 
`default-parser`
 entry it applies to every engine inside it. When several engines in the 
winning tier claim a
 type, a WARN at startup names them and the winner; an engine under `parsers` 
shadows a classpath
-engine silently, by design. Name the engine in `text-recognizers` to stop 
depending on any of
-this. An `AutoDetectParser` built in code without the loader applies the same 
rules at parse
+engine (and draws the deprecation WARN below). Name the engine in 
`text-recognizers` to stop
+depending on any of this. An `AutoDetectParser` built in code without the 
loader applies the same rules at parse
 time.
 
-=== An engine under `parsers`
-
-Naming an engine under `parsers` still works, and is how 4.0 configured 
Tesseract's options, but
-it means two things at once: the entry configures the engine, and the engine 
is also a parser
-for the types no other parser there claims (`"parsers": 
[{"tesseract-ocr-parser": {}}]` alone
-sends every image straight to OCR; `claude-vlm-parser` next to a 
`default-parser` that excludes
-`pdf-parser` makes Claude the PDF parser). When every type the engine 
advertises is claimed by
-another parser it is never dispatched to: startup logs that it then acts only 
as the text
-recognizer, and warns if it never runs at all. Prefer `text-recognizers` for 
the engine and keep
-`parsers` for parsers.
+=== An engine under `parsers` (deprecated)
+
+Naming an engine under `parsers` is how 4.0 configured Tesseract's options. It 
is deprecated
+since 4.1.0 and unsupported in 4.2.0, when `parsers` holds parsers only and an 
engine there
+fails config load. Until then it keeps working, with a WARN at startup that 
says what the entry
+does today, because it means two things at once: the entry configures the 
engine, and the engine
+is also a parser for the types no other parser there claims (`"parsers":
+[{"tesseract-ocr-parser": {}}]` alone sends every image straight to OCR; 
`claude-vlm-parser`
+next to a `default-parser` that excludes `pdf-parser` makes Claude the PDF 
parser). When every
+type the engine advertises is claimed by another parser it is never dispatched 
to and acts only
+as the text recognizer, or never runs at all. Configure the engine under 
`engines`, name it in
+`text-recognizers`, and keep `parsers` for parsers.
 
 === Replacing extracted text
 
diff --git a/docs/modules/ROOT/pages/configuration/inference.adoc 
b/docs/modules/ROOT/pages/configuration/inference.adoc
index a868057578..f559595fe7 100644
--- a/docs/modules/ROOT/pages/configuration/inference.adoc
+++ b/docs/modules/ROOT/pages/configuration/inference.adoc
@@ -106,9 +106,9 @@ on a chat engine), or an unknown key fails config load.
 Parsers do not call engines. Every image document Tika parses is offered to 
one dispatcher
 after its own parser has run, whatever container it came from and however 
deep. The
 dispatcher keeps the units of the whole document tree until the top-level 
parse ends, then
-sends each binding's units to its engine in `maxBatchSize` requests: a docx 
with twelve
-pictures is one embeddings request, not twelve; if one image in a batch is 
rejected, the rest
-are retried one by one. Results land on the document a unit belongs to, as it 
appears in the
+sends each binding's units to its engine in requests of up to `maxBatchSize` 
units: a docx
+with twelve pictures is one embeddings request, not twelve, when the engine 
takes twelve at
+once; if one image in a batch is rejected, the rest are retried one by one. 
Results land on the document a unit belongs to, as it appears in the
 output. In the recursive outputs (`/rmeta`, `-J`, pipes `RMETA`), where every 
document has
 its own metadata object, a picture in the body of a docx or an email puts its 
vector on the
 docx or the email, with an `embedded` locator naming the picture, whether the 
docx is the
@@ -231,9 +231,10 @@ request" (see 
xref:configuration/inference-recipes.adoc[Recipes]). Names only, s
 xref:using-tika/server/index.adoc[preset]. A name not in the configured list 
fails the request.
 Engines cannot be defined or changed per request.
 
-== From `openai-image-embedding-parser`
+== From `openai-image-embedding-parser` (deprecated)
 
-The 4.0 `openai-image-embedding-parser` still works as a text-recognizers 
entry and embeds one
-image per request. The same endpoint as an engine plus a binding on `IMAGES` 
embeds a
-document's images in one request and puts the vectors where they belong; move 
the `baseUrl`,
-`model` and `apiKey` to the engine and drop the parser entry.
+The 4.0 `openai-image-embedding-parser` is deprecated since 4.1.0 and removed 
in 4.2.0. It
+still works as a text-recognizers entry, with a WARN at startup, and embeds 
one image per
+request. The same endpoint as an engine plus a binding on `IMAGES` embeds a 
document's images
+in requests of the engine's batch size and puts the vectors where they belong; 
move the
+`baseUrl`, `model` and `apiKey` to the engine and drop the parser entry.
diff --git a/docs/modules/ROOT/pages/configuration/parsers/tess4j-parser.adoc 
b/docs/modules/ROOT/pages/configuration/parsers/tess4j-parser.adoc
index 8c7397befe..0f0b2c05a1 100644
--- a/docs/modules/ROOT/pages/configuration/parsers/tess4j-parser.adoc
+++ b/docs/modules/ROOT/pages/configuration/parsers/tess4j-parser.adoc
@@ -87,8 +87,8 @@ JNA must be able to find `libtesseract` and `libleptonica` at 
runtime.
 The tess4j jar does *not* bundle these libraries for macOS or Linux.
 If JNA cannot find them on the default library search path, the parser
 advertises no media types: named under `text-recognizers` that fails
-config load with a message naming it; under `parsers` it is logged at
-startup and the parser does nothing.
+config load with a message naming it; under `parsers` (the deprecated
+4.0 shape) it is logged at startup and the parser does nothing.
 
 You have several options:
 
diff --git 
a/docs/modules/ROOT/pages/configuration/parsers/tesseract-ocr-parser.adoc 
b/docs/modules/ROOT/pages/configuration/parsers/tesseract-ocr-parser.adoc
index 18f6da8b2a..d5db51f8e8 100644
--- a/docs/modules/ROOT/pages/configuration/parsers/tesseract-ocr-parser.adoc
+++ b/docs/modules/ROOT/pages/configuration/parsers/tesseract-ocr-parser.adoc
@@ -38,7 +38,7 @@ binary is found: the loader finds it among the loaded parsers 
because it impleme
 `TextRecognizer` and advertises the image types it reads, and logs the choice 
at startup. When
 another classpath engine such as Tess4J claims the same types, one is used and 
a WARN at startup
 names the collision and the winner; an engine named under `parsers` (a VLM 
parser, for instance)
-takes precedence over Tesseract without a warning. To turn OCR off everywhere, 
set
+takes precedence over Tesseract, with the deprecation WARN that shape draws 
since 4.1.0. To turn OCR off everywhere, set
 `"text-recognizers": []`. See xref:configuration/index.adoc[Configuration] for 
how the list
 behaves, and xref:developers/custom-ocr-engine.adoc[Wiring in Your Own OCR 
Engine] to plug in
 an engine Tika does not ship.
@@ -55,7 +55,7 @@ icon:github[] 
https://github.com/apache/tika/blob/main/tika-parsers/tika-parsers
 
 Every option with its default value and an inline comment describing it. Named 
under
 `text-recognizers`, the engine leaves every default parser loaded; the same 
options apply to an
-entry under `parsers`, the 4.0 shape. ImageMagick is optional; it is used only 
when
+entry under `parsers`, the 4.0 shape, which is deprecated since 4.1.0 and 
unsupported in 4.2.0. ImageMagick is optional; it is used only when
 `enableImagePreprocessing` or `applyRotation` is true.
 
 [source,json]
diff --git a/docs/modules/ROOT/pages/configuration/parsers/vlm-parsers.adoc 
b/docs/modules/ROOT/pages/configuration/parsers/vlm-parsers.adoc
index 109441fbe4..56ea18c93f 100644
--- a/docs/modules/ROOT/pages/configuration/parsers/vlm-parsers.adoc
+++ b/docs/modules/ROOT/pages/configuration/parsers/vlm-parsers.adoc
@@ -29,8 +29,9 @@ for earlier shapes.
 Three implementations are provided out of the box. None is auto-loaded: each 
must be
 named explicitly in your configuration. (Changed in 4.1.0: `openai-vlm-parser` 
previously
 auto-registered via SPI.) To use a VLM as the OCR engine for embedded images 
and rendered
-PDF pages, name it in the `text-recognizers` list — see
-xref:configuration/index.adoc[Configuration].
+PDF pages, configure it under `engines` and name it in the `text-recognizers` 
list — see
+xref:configuration/index.adoc[Configuration]. Naming it under `parsers` is 
deprecated since
+4.1.0 and unsupported in 4.2.0.
 
 [cols="1,2,1"]
 |===
@@ -50,9 +51,9 @@ xref:configuration/index.adoc[Configuration].
 |===
 
 All three enrich the standard image types (`image/png`, `image/jpeg`, ...).
-`ClaudeVLMParser` and `GeminiVLMParser` additionally declare 
`application/pdf`, so they can process
-PDFs natively with the model's vision capabilities once the default 
`pdf-parser` is excluded (see
-below); an enricher never displaces the parser for a type.
+`ClaudeVLMParser` and `GeminiVLMParser` additionally declare 
`application/pdf`, so under `parsers`
+they can process PDFs natively with the model's vision capabilities once the 
default `pdf-parser`
+is excluded (see below, deprecated); an enricher never displaces the parser 
for a type.
 
 == Module dependency
 
@@ -142,12 +143,18 @@ endpoint. The API key is passed as a `key` query 
parameter.
 
 Change `baseUrl` if you are using Vertex AI or a proxy.
 
-== Using a VLM parser for PDF parsing
+== Using a VLM parser for PDF parsing (deprecated)
 
 Claude and Gemini can process entire PDFs with their vision capabilities.
 To route PDFs to a VLM parser instead of the default `PDFParser`, exclude
 the default and add the VLM parser:
 
+NOTE: This shape is deprecated since 4.1.0 and unsupported in 4.2.0, when an 
engine under
+`parsers` fails config load. The 4.2 way to put a PDF in front of a VLM is the 
PDF parser's
+`"text": "OCR"` with the VLM named in `text-recognizers`: every page is 
rendered and transcribed,
+and the PDF's own structure (attachments, bookmarks, forms) is still parsed. 
That sends page
+images, not the PDF file; there is no 4.2 shape yet for sending the file 
itself.
+
 [source,json]
 ----
 include::example$vlm-pdf-parsing.json[]
@@ -156,9 +163,9 @@ include::example$vlm-pdf-parsing.json[]
 You can substitute `gemini-vlm-parser` for `claude-vlm-parser` above.
 
 Named under `parsers`, the VLM is also the text recognizer for the image types 
it advertises, so
-embedded images in every other document type are sent to it too. Add a 
`text-recognizers` list to
-choose a different engine for those, or `"text-recognizers": []` for none; see
-xref:configuration/index.adoc[Configuration].
+embedded images in every other document type are sent to it too, and startup 
logs the deprecation
+WARN. Add a `text-recognizers` list to choose a different engine for those, or
+`"text-recognizers": []` for none; see 
xref:configuration/index.adoc[Configuration].
 
 == Configuration options reference
 
diff --git a/docs/modules/ROOT/pages/developers/custom-ocr-engine.adoc 
b/docs/modules/ROOT/pages/developers/custom-ocr-engine.adoc
index 99c3731bf1..340456b036 100644
--- a/docs/modules/ROOT/pages/developers/custom-ocr-engine.adoc
+++ b/docs/modules/ROOT/pages/developers/custom-ocr-engine.adoc
@@ -271,9 +271,9 @@ page renders, exactly as a dispatched parser would be, so 
the metadata tells you
 
 == Common mistakes
 
-* Naming the engine under `parsers`. It still works, but the entry then means 
two things at once
-  (configure the engine, and parse the types nobody else claims), and startup 
has to tell you
-  which happened. `text-recognizers` says what you mean.
+* Naming the engine under `parsers`. Deprecated since 4.1.0 and unsupported in 
4.2.0: the entry
+  means two things at once (configure the engine, and parse the types nobody 
else claims), and
+  startup has to tell you which happened. `engines` plus `text-recognizers` 
says what you mean.
 * Creating a fresh `ParseContext` inside `parse`. The caller's context carries 
the recursion guard
   that stops an enricher from enriching its own output; pass the one you were 
given.
 * Claiming types you do not enrich. Every advertised type routes images to 
you, and a type you
diff --git a/docs/modules/ROOT/pages/migration-to-4x/chunk-strategies.adoc 
b/docs/modules/ROOT/pages/migration-to-4x/chunk-strategies.adoc
index 1f0a15966e..ad8027e683 100644
--- a/docs/modules/ROOT/pages/migration-to-4x/chunk-strategies.adoc
+++ b/docs/modules/ROOT/pages/migration-to-4x/chunk-strategies.adoc
@@ -123,17 +123,22 @@ pictures. Nothing to configure:
 [source,json]
 ----
 {
-  "text-recognizers": [
-    { "openai-image-embedding-parser": { "baseUrl": "http://clip-server:8000";,
-                                         "model": "jinaai/jina-clip-v2" } }
-  ],
-  "metadata-filters": [
-    { "openai-embedding-filter": { "baseUrl": "http://embed-server:8000";,
-                                   "model": "text-embedding-3-small" } }
+  "engines": {
+    "clip": { "openai-embedding-engine": { "baseUrl": 
"http://clip-server:8000";,
+                                           "model": "jinaai/jina-clip-v2" } },
+    "text": { "openai-embedding-engine": { "baseUrl": 
"http://embed-server:8000";,
+                                           "model": "text-embedding-3-small" } 
}
+  },
+  "inference": [
+    { "id": "pictures", "engine": "clip", "input": "IMAGES", "tasks": 
["embed"] },
+    { "id": "text", "engine": "text", "input": "TEXT", "tasks": ["embed"] }
   ]
 }
 ----
 
+(The 4.0 `openai-image-embedding-parser` and `openai-embedding-filter`, 
deprecated since 4.1.0,
+place chunks by the same rule.)
+
 The rules:
 
 * `INLINE` and `RENDERING` children hand their chunks to the parent. 
Attachments keep their
@@ -143,8 +148,9 @@ The rules:
 * Every chunk written to a parent carries an `embedded` locator,
   `{"id_path": "/1", "name": "image1.png"}`, naming the child it came from, 
beside any
   locator it already had.
-* `"liftToParent": false` on the embedder keeps every picture's vector on the 
picture, for an
-  index where each picture is a hit of its own.
+* `"liftToParent": false` on the deprecated `openai-image-embedding-parser` 
keeps every
+  picture's vector on the picture, for an index where each picture is a hit of 
its own; the
+  `IMAGES` binding has no such switch.
 * In the recursive (`/rmeta`, `-J`, pipes `RMETA`) parse the wrapper keeps 
every document's
   metadata, so a chunk lands on its parent's. In the single-object parse 
(`/tika` as JSON,
   `-j`, pipes `CONCATENATE`) only the top-level document's metadata is handed 
back, so every
diff --git a/docs/modules/ROOT/pages/migration-to-4x/migrating-to-4x.adoc 
b/docs/modules/ROOT/pages/migration-to-4x/migrating-to-4x.adoc
index f74222b56a..ca04cb8671 100644
--- a/docs/modules/ROOT/pages/migration-to-4x/migrating-to-4x.adoc
+++ b/docs/modules/ROOT/pages/migration-to-4x/migrating-to-4x.adoc
@@ -249,22 +249,35 @@ What changes for you:
 * A custom engine that still advertises the pseudo-types keeps working as a 
text recognizer for the
   real types, with a WARN at startup, until 5.0. Advertise the real types and 
implement
   `TextRecognizer` to silence it.
-* Put the engine in the top-level `text-recognizers` list rather than under 
`parsers`; every
-  default parser stays loaded. `"text-recognizers": []` turns enrichment off 
everywhere. An engine
-  left under `parsers` keeps working; startup logs when it acts only as the 
enricher and warns
-  when it never runs at all. Startup also logs one line per effective engine.
+* Put the engine under `engines` and name it in the top-level 
`text-recognizers` list rather
+  than under `parsers`; every default parser stays loaded. 
`"text-recognizers": []` turns
+  enrichment off everywhere. An engine left under `parsers` keeps working in 
4.1.x with a
+  deprecation WARN at startup that says what the entry does (see below). 
Startup also logs one
+  line per effective engine.
 * VLM parsers gain `textRecognizer` (default `true`); set it to `false` for a 
captioning or tagging
   prompt so the PDF parser's `AUTO` OCR never replaces extracted text with the 
VLM's output.
 
-=== Embedding filters deprecated (4.1.0+)
-
-`openai-embedding-filter` and `jina-embedding-filter` are deprecated and will 
be removed in
-4.2.0. Configure the endpoint once under `engines` and bind it with a `TEXT`
-xref:configuration/inference.adoc#text[inference binding]; it embeds the whole 
document tree in
-batched requests. Until then they keep working from the server-side config. A 
request can no
-longer name them in a per-request `metadata-filters` list (4.0.x accepted a 
caller-supplied
-`baseUrl` there); the flat per-request `{"openai-embedding-filter": 
{"skipEmbedding": true}}`
-form is unchanged.
+=== Engines under `parsers` deprecated (4.1.0+)
+
+An inference engine named as a `parsers` entry, an OCR engine, a VLM parser or 
the image
+embedding parser, is deprecated since 4.1.0 and fails config load in 4.2.0, 
when `parsers`
+holds parsers only. Startup logs one WARN per such entry saying what it does 
today: it parses
+the types no other parser claims, it acts only as the text recognizer, or it 
never runs.
+Configure the engine under `engines` and name it in `text-recognizers` (or 
bind it with
+`inference`); every default parser stays loaded. Sending a whole PDF to Claude 
or Gemini by
+excluding `pdf-parser` goes with it: the 4.2 shape is the PDF parser's 
`"text": "OCR"` with the
+VLM as recognizer, which sends rendered pages rather than the file.
+
+=== Embedding filters and the image embedding parser deprecated (4.1.0+)
+
+`openai-embedding-filter`, `jina-embedding-filter` and 
`openai-image-embedding-parser` are
+deprecated and will be removed in 4.2.0. Configure the endpoint once under 
`engines` and bind
+it with a `TEXT` or `IMAGES` xref:configuration/inference.adoc[inference 
binding]; it embeds
+the whole document tree in batched requests and puts a picture's vector on the 
document it is
+part of. Until then they keep working from the server-side config, with a WARN 
at startup. A
+request can no longer name the filters in a per-request `metadata-filters` 
list (4.0.x
+accepted a caller-supplied `baseUrl` there); the flat per-request
+`{"openai-embedding-filter": {"skipEmbedding": true}}` form is unchanged.
 
 === PDF text policy: `ocr.strategy` becomes `text` (4.1.0+)
 
diff --git 
a/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/ImageEmbeddingConfig.java
 
b/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/ImageEmbeddingConfig.java
index 1d8da98f2f..f0589ccd7c 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/ImageEmbeddingConfig.java
+++ 
b/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/ImageEmbeddingConfig.java
@@ -25,7 +25,10 @@ import org.apache.tika.utils.StringUtils;
 /**
  * Configuration for image embedding parsers that call a CLIP-like
  * vector endpoint.
+ *
+ * @deprecated since 4.1.0, removed in 4.2.0 with {@link 
OpenAIImageEmbeddingParser}.
  */
+@Deprecated
 public class ImageEmbeddingConfig implements Serializable {
 
     private static final long serialVersionUID = 1L;
diff --git 
a/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/OpenAIImageEmbeddingParser.java
 
b/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/OpenAIImageEmbeddingParser.java
index 0dc5595dc7..af96034cca 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/OpenAIImageEmbeddingParser.java
+++ 
b/tika-parsers/tika-parsers-ml/tika-inference/src/main/java/org/apache/tika/inference/OpenAIImageEmbeddingParser.java
@@ -80,7 +80,13 @@ import org.apache.tika.utils.StringUtils;
  * {@link #parse}.
  *
  * @since Apache Tika 4.0
+ * @deprecated since 4.1.0, removed in 4.2.0. Configure the endpoint as an
+ * {@code openai-embedding-engine} under {@code "engines"} and bind it with an 
{@code IMAGES}
+ * {@code "inference"} binding and the {@code embed} task: a document tree's 
images go in
+ * requests of the engine's batch size instead of one per image, and the 
vectors land where
+ * {@link ChunkTarget} puts them.
  */
+@Deprecated
 @TikaComponent(name = "openai-image-embedding-parser", spi = false)
 public class OpenAIImageEmbeddingParser implements Parser, Initializable, 
Closeable,
         ContentEnricher {
@@ -185,9 +191,10 @@ public class OpenAIImageEmbeddingParser implements Parser, 
Initializable, Closea
 
     @Override
     public void initialize() throws TikaConfigException {
-        LOG.info("openai-image-embedding-parser runs one request per image; 
the \"engines\" + "
-                + "\"inference\" shape (openai-embedding-engine, input IMAGES, 
task embed) batches "
-                + "a document's images into one request");
+        LOG.warn("openai-image-embedding-parser is deprecated since 4.1.0 and 
will be removed in "
+                + "4.2.0: configure the endpoint as an openai-embedding-engine 
under \"engines\" "
+                + "and bind it with an IMAGES \"inference\" binding (task 
embed), which sends a "
+                + "document's images in requests of the engine's batch size");
         this.httpClient = TikaHttpClient.build(30);
     }
 
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/claude-vlm-basic.json
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/claude-vlm-basic.json
index 47d819bc2f..753912ab5e 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/claude-vlm-basic.json
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/claude-vlm-basic.json
@@ -1,10 +1,15 @@
 {
-  "text-recognizers": [
-    {
+  "engines": {
+    "claude": {
       "claude-vlm-parser": {
         "apiKey": "YOUR_ANTHROPIC_API_KEY",
         "model": "claude-sonnet-4-20250514"
       }
     }
+  },
+  "text-recognizers": [
+    {
+      "engine": "claude"
+    }
   ]
 }
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/claude-vlm-full.json
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/claude-vlm-full.json
index 3982ab9079..7a8aa21285 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/claude-vlm-full.json
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/claude-vlm-full.json
@@ -1,6 +1,6 @@
 {
-  "text-recognizers": [
-    {
+  "engines": {
+    "claude": {
       "claude-vlm-parser": {
         "baseUrl": "https://api.anthropic.com";,
         "model": "claude-sonnet-4-20250514",
@@ -17,5 +17,10 @@
         "allowRuntimePrompt": false
       }
     }
+  },
+  "text-recognizers": [
+    {
+      "engine": "claude"
+    }
   ]
 }
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/gemini-vlm-basic.json
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/gemini-vlm-basic.json
index 720336981e..8ac7472b04 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/gemini-vlm-basic.json
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/gemini-vlm-basic.json
@@ -1,10 +1,15 @@
 {
-  "text-recognizers": [
-    {
+  "engines": {
+    "gemini": {
       "gemini-vlm-parser": {
         "apiKey": "your-gemini-api-key",
         "model": "gemini-2.5-flash"
       }
     }
+  },
+  "text-recognizers": [
+    {
+      "engine": "gemini"
+    }
   ]
 }
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/gemini-vlm-full.json
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/gemini-vlm-full.json
index f5fb16c967..a6990c20a6 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/gemini-vlm-full.json
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/gemini-vlm-full.json
@@ -1,6 +1,6 @@
 {
-  "text-recognizers": [
-    {
+  "engines": {
+    "gemini": {
       "gemini-vlm-parser": {
         "baseUrl": "https://generativelanguage.googleapis.com";,
         "model": "gemini-2.5-flash",
@@ -17,5 +17,10 @@
         "allowRuntimePrompt": false
       }
     }
+  },
+  "text-recognizers": [
+    {
+      "engine": "gemini"
+    }
   ]
 }
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/openai-vlm-basic.json
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/openai-vlm-basic.json
index d719f4da62..03a348e69a 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/openai-vlm-basic.json
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/openai-vlm-basic.json
@@ -1,11 +1,16 @@
 {
-  "text-recognizers": [
-    {
+  "engines": {
+    "vlm": {
       "openai-vlm-parser": {
         "baseUrl": "http://127.0.0.1:8000";,
         "model": "jinaai/jina-vlm",
         "timeoutMillis": 300000
       }
     }
+  },
+  "text-recognizers": [
+    {
+      "engine": "vlm"
+    }
   ]
 }
diff --git 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/openai-vlm-full.json
 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/openai-vlm-full.json
index 24e0adb110..be2caef4a3 100644
--- 
a/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/openai-vlm-full.json
+++ 
b/tika-parsers/tika-parsers-ml/tika-vlm/src/test/resources/config-examples/openai-vlm-full.json
@@ -1,6 +1,6 @@
 {
-  "text-recognizers": [
-    {
+  "engines": {
+    "vlm": {
       "openai-vlm-parser": {
         "baseUrl": "http://127.0.0.1:8000";,
         "completionsPath": "/v1/chat/completions",
@@ -18,5 +18,10 @@
         "allowRuntimePrompt": false
       }
     }
+  },
+  "text-recognizers": [
+    {
+      "engine": "vlm"
+    }
   ]
 }
diff --git 
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java
 
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java
index 3d9e816920..5d939b3462 100644
--- 
a/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java
+++ 
b/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java
@@ -150,8 +150,8 @@ public class ParserLoader extends 
AbstractSpiComponentLoader<Parser> {
      * Content enrichers are settled once, over the whole tree: the configured 
list, or
      * with none, one engine per type resolved from the enrichers among the 
loaded parsers.
      * Either way every {@link EnrichingParser} gets a composite (possibly 
empty), the
-     * effective engines are logged, and an enricher under {@code "parsers"} 
that nothing
-     * dispatches to is called out.
+     * effective engines are logged, and every engine under {@code "parsers"}, 
the deprecated
+     * shape, is called out with what the entry does today.
      */
     @Override
     protected Parser finish(Parser root, LoaderContext context) throws 
TikaConfigException {
@@ -159,8 +159,8 @@ public class ParserLoader extends 
AbstractSpiComponentLoader<Parser> {
         CompositeContentEnricher enrichers =
                 configured != null ? configured : 
ContentEnrichers.resolve(root);
         logEnrichers(enrichers, configured != null);
-        for (Parser inert : undispatchedEnrichers(root)) {
-            logUndispatched(inert, enrichers, configured != null);
+        for (Parser engine : enginesUnderParsers(root)) {
+            warnEngineUnderParsers(engine, root, enrichers, configured != 
null);
         }
         if (configured != null) {
             for (Map.Entry<List<Parser>, Set<MediaType>> e
@@ -205,66 +205,80 @@ public class ParserLoader extends 
AbstractSpiComponentLoader<Parser> {
         return names.toString();
     }
 
-    /**
-     * Enrichers named directly under {@code "parsers"} that the composite 
never dispatches
-     * to: every type they advertise is claimed by another parser there, or 
they advertise
-     * none (engine unavailable, or told to skip). Both shapes look configured 
and do
-     * nothing as parsers.
-     */
-    static List<Parser> undispatchedEnrichers(Parser root) {
-        List<Parser> inert = new ArrayList<>();
+    /** Engines (enrichers) named directly under {@code "parsers"}: the 
deprecated 4.0 shape. */
+    static List<Parser> enginesUnderParsers(Parser root) {
+        List<Parser> engines = new ArrayList<>();
         if (!(root instanceof CompositeParser composite) || root instanceof 
DefaultParser) {
-            return inert;
+            return engines;
+        }
+        for (Parser member : composite.getAllComponentParsers()) {
+            if (ContentEnrichers.isEnricher(member)) {
+                engines.add(member);
+            }
+        }
+        return engines;
+    }
+
+    /** The types the composite dispatches to this member as their parser. */
+    static Set<MediaType> parsedTypes(Parser root, Parser member) {
+        Set<MediaType> parsed = new TreeSet<>();
+        if (!(root instanceof CompositeParser composite)) {
+            return parsed;
         }
         ParseContext empty = new ParseContext();
         Map<MediaType, Parser> dispatch = composite.getParsers(empty);
         MediaTypeRegistry registry = composite.getMediaTypeRegistry();
-        for (Parser member : composite.getAllComponentParsers()) {
-            if (!ContentEnrichers.isEnricher(member)) {
-                continue;
-            }
-            boolean dispatched = false;
-            for (MediaType type : member.getSupportedTypes(empty)) {
-                if (dispatch.get(registry.normalize(type)) == member) {
-                    dispatched = true;
-                    break;
-                }
-            }
-            if (!dispatched) {
-                inert.add(member);
+        for (MediaType type : member.getSupportedTypes(empty)) {
+            if (dispatch.get(registry.normalize(type)) == member) {
+                parsed.add(type);
             }
         }
-        return inert;
+        return parsed;
     }
 
-    // the 4.0 shape still works, so it is INFO; an entry that never runs at 
all is a WARN
-    private static void logUndispatched(Parser inert, CompositeContentEnricher 
enrichers,
+    /**
+     * One WARN per engine under "parsers": the deprecation, then what the 
entry does today. A
+     * deprecated engine class is left to its own WARN, which already says 
where to go.
+     */
+    private void warnEngineUnderParsers(Parser engine, Parser root,
+                                        CompositeContentEnricher enrichers,
                                         boolean listConfigured) {
-        String name = ParserUtils.getParserClassname(inert);
-        Set<MediaType> advertised = inert.getSupportedTypes(new 
ParseContext());
+        if (unwrapClass(engine).isAnnotationPresent(Deprecated.class)) {
+            return;
+        }
+        String name = ParserUtils.getParserClassname(engine);
+        boolean recognizer = ContentEnrichers.asTextRecognizer(engine) != null;
+        String role = recognizer ? "text recognizer" : "annotator";
+        String lead = name + " is named under \"parsers\", which is deprecated 
for engines since "
+                + "4.1.0 and unsupported in 4.2.0: configure it under 
\"engines\" and "
+                + (recognizer ? "name it in \"text-recognizers\". "
+                        : "bind it with \"inference\" (or name it in 
\"text-recognizers\" to run it on "
+                        + "every image). ");
+        Set<MediaType> advertised = engine.getSupportedTypes(new 
ParseContext());
         if (advertised.isEmpty()) {
-            LOG.info("{} under \"parsers\" advertises no media types (engine 
unavailable, or "
-                    + "configured to skip) and never runs. To turn enrichment 
off, set "
-                    + "\"text-recognizers\": [] instead.", name);
+            LOG.warn("{}It advertises no media types (engine unavailable, or 
configured to skip) "
+                    + "and never runs; to turn recognition off, set 
\"text-recognizers\": [].",
+                    lead);
             return;
         }
+        Set<MediaType> parsed = parsedTypes(root, engine);
         Set<MediaType> enriching = new TreeSet<>();
         for (MediaType type : enrichers.getSupportedTypes()) {
-            if (enrichers.getEnrichers(type).contains(inert)) {
+            if (enrichers.getEnrichers(type).contains(engine)) {
                 enriching.add(type);
             }
         }
-        if (enriching.isEmpty()) {
-            LOG.warn("{} under \"parsers\" is never dispatched to (every type 
it advertises is "
-                    + "claimed by another parser) and {}, so it never runs. 
Name it under "
-                    + "\"text-recognizers\" to invoke it, or exclude the 
parser that "
-                    + "claims its types to dispatch to it.", name, 
listConfigured
-                    ? "\"text-recognizers\" does not name it"
-                    : "another enricher is preferred for those types");
+        if (!parsed.isEmpty()) {
+            LOG.warn("{}Today it is the parser for {}{}; in 4.2.0 the default 
parser keeps those "
+                    + "types and the engine is invoked on their images and 
rendered pages.", lead,
+                    parsed, enriching.isEmpty() ? "" : " and the " + role + " 
for " + enriching);
+        } else if (!enriching.isEmpty()) {
+            LOG.warn("{}It is never dispatched to as a parser (every type it 
advertises is claimed "
+                    + "by another parser); it acts only as the {} for {}.", 
lead, role, enriching);
         } else {
-            LOG.info("{} under \"parsers\" is never dispatched to (every type 
it advertises is "
-                    + "claimed by another parser); it acts only as the text 
recognizer for "
-                    + "{}. Name it under \"text-recognizers\" to say so.", 
name, enriching);
+            LOG.warn("{}It never runs: every type it advertises is claimed by 
another parser and "
+                    + "{}.", lead, listConfigured ? "\"text-recognizers\" does 
not name it"
+                    : "another engine is preferred for those types");
         }
     }
 
diff --git 
a/tika-serialization/src/test/java/org/apache/tika/config/loader/ContentEnricherLoaderTest.java
 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/ContentEnricherLoaderTest.java
index 8b0c5ae05a..d963731e3e 100644
--- 
a/tika-serialization/src/test/java/org/apache/tika/config/loader/ContentEnricherLoaderTest.java
+++ 
b/tika-serialization/src/test/java/org/apache/tika/config/loader/ContentEnricherLoaderTest.java
@@ -286,28 +286,41 @@ public class ContentEnricherLoaderTest {
     }
 
     /**
-     * An enricher under "parsers" whose every type another parser claims is 
never dispatched
-     * to; the loader names it so a config that looks like a parser choice is 
not a silent
-     * enricher choice.
+     * An engine under "parsers" is the deprecated shape; the loader finds it 
whether it is
+     * dispatched to as a parser or only ever runs as an enricher, and tells 
them apart.
      */
     @Test
-    public void testUndispatchedEnricherUnderParsersIsReported() throws 
Exception {
+    public void testEngineUnderParsersIsFound() throws Exception {
         TikaLoader loader = load("""
                 {
                   "parsers": [ {"minimal-test-parser": {}}, 
{"test-spi-enricher": {}} ]
                 }
                 """);
-        java.util.List<Parser> inert = 
ParserLoader.undispatchedEnrichers(loader.loadParsers());
-        assertEquals(1, inert.size());
-        assertTrue(inert.get(0) instanceof TestSpiEnricher);
+        Parser root = loader.loadParsers();
+        java.util.List<Parser> engines = 
ParserLoader.enginesUnderParsers(root);
+        assertEquals(1, engines.size());
+        assertTrue(engines.get(0) instanceof TestSpiEnricher);
+        assertTrue(ParserLoader.parsedTypes(root, engines.get(0)).isEmpty(),
+                "the parser claims its type: never dispatched to");
 
         loader = load("""
                 {
                   "parsers": [ {"test-spi-enricher": {}} ]
                 }
                 """);
-        
assertTrue(ParserLoader.undispatchedEnrichers(loader.loadParsers()).isEmpty(),
-                "alone it is the parser for its type");
+        root = loader.loadParsers();
+        engines = ParserLoader.enginesUnderParsers(root);
+        assertEquals(1, engines.size());
+        
assertEquals(java.util.Set.of(MediaType.parse("application/test+minimal")),
+                ParserLoader.parsedTypes(root, engines.get(0)), "alone it is 
the parser for its type");
+
+        loader = load("""
+                {
+                  "parsers": [ {"default-parser": {}} ]
+                }
+                """);
+        
assertTrue(ParserLoader.enginesUnderParsers(loader.loadParsers()).isEmpty(),
+                "an engine the classpath supplies is not the deprecated 
shape");
     }
 
     /** Two text recognizers on one type both run; the loader names them at 
startup. */
diff --git a/tika-server/docker-build/sample-configs/vision/vlm-claude.json 
b/tika-server/docker-build/sample-configs/vision/vlm-claude.json
index 22b2311c2c..27c25e4a05 100644
--- a/tika-server/docker-build/sample-configs/vision/vlm-claude.json
+++ b/tika-server/docker-build/sample-configs/vision/vlm-claude.json
@@ -1,16 +1,18 @@
 {
-  // Vision-Language Model parsing via Anthropic's Claude API.
-  // Claude can handle OCR images and PDFs natively (no rasterization needed).
+  // Vision-Language Model OCR via Anthropic's Claude API: every default parser
+  // stays loaded, and embedded images and rendered PDF pages go to Claude when
+  // the PDF parser's "text" policy asks for OCR (AUTO by default).
   // Set apiKey to your Anthropic API key — DO NOT commit a real key.
   // Prefer passing it via the ANTHROPIC_API_KEY env var and substituting it
   // at container start, e.g. via an entrypoint shim or sidecar that templates
   // this file. See docs: configuration/parsers/vlm-parsers.
-  "parsers": [
-    {
+  "engines": {
+    "claude": {
       "claude-vlm-parser": {
         "apiKey": "${ANTHROPIC_API_KEY}",
         "model": "claude-sonnet-4-20250514"
       }
     }
-  ]
+  },
+  "text-recognizers": [ { "engine": "claude" } ]
 }
diff --git a/tika-server/docker-build/sample-configs/vision/vlm-gemini.json 
b/tika-server/docker-build/sample-configs/vision/vlm-gemini.json
index 54de0705aa..323257383d 100644
--- a/tika-server/docker-build/sample-configs/vision/vlm-gemini.json
+++ b/tika-server/docker-build/sample-configs/vision/vlm-gemini.json
@@ -1,15 +1,17 @@
 {
-  // Vision-Language Model parsing via Google's Gemini generateContent API.
-  // Gemini can handle OCR images and PDFs natively (no rasterization needed).
+  // Vision-Language Model OCR via Google's Gemini generateContent API: every
+  // default parser stays loaded, and embedded images and rendered PDF pages go
+  // to Gemini when the PDF parser's "text" policy asks for OCR (AUTO by 
default).
   // Set apiKey to your Google AI Studio API key — DO NOT commit a real key.
   // Prefer GEMINI_API_KEY env var + a templating entrypoint, similar to the
   // Claude config. See docs: configuration/parsers/vlm-parsers.
-  "parsers": [
-    {
+  "engines": {
+    "gemini": {
       "gemini-vlm-parser": {
         "apiKey": "${GEMINI_API_KEY}",
         "model": "gemini-2.5-flash"
       }
     }
-  ]
+  },
+  "text-recognizers": [ { "engine": "gemini" } ]
 }
diff --git a/tika-server/docker-build/sample-configs/vision/vlm-openai.json 
b/tika-server/docker-build/sample-configs/vision/vlm-openai.json
index ecb0e3e87d..d516f4bcc8 100644
--- a/tika-server/docker-build/sample-configs/vision/vlm-openai.json
+++ b/tika-server/docker-build/sample-configs/vision/vlm-openai.json
@@ -1,17 +1,20 @@
 {
-  // Vision-Language Model parsing via an OpenAI-compatible endpoint.
+  // Vision-Language Model OCR via an OpenAI-compatible endpoint: every default
+  // parser stays loaded, and embedded images and rendered PDF pages go to the
+  // model when the PDF parser's "text" policy asks for OCR (AUTO by default).
   // Works with self-hosted backends (vLLM, Ollama, a local FastAPI wrapper)
   // or against OpenAI's own chat-completions API. Set baseUrl to wherever
   // the OpenAI-compatible endpoint is reachable from the tika container.
   // If the endpoint requires authentication, also set apiKey.
   // See docs: configuration/parsers/vlm-parsers.
-  "parsers": [
-    {
+  "engines": {
+    "vlm": {
       "openai-vlm-parser": {
         "baseUrl": "http://vlm-server:8000";,
         "model": "jinaai/jina-vlm",
         "timeoutMillis": 300000
       }
     }
-  ]
+  },
+  "text-recognizers": [ { "engine": "vlm" } ]
 }

Reply via email to