Skip to content

SOLR-18187: Document enrichment with LLMs#4259

Draft
nicolo-rinaldi wants to merge 16 commits intoapache:mainfrom
SeaseLtd:llm-document-enrichment
Draft

SOLR-18187: Document enrichment with LLMs#4259
nicolo-rinaldi wants to merge 16 commits intoapache:mainfrom
SeaseLtd:llm-document-enrichment

Conversation

@nicolo-rinaldi
Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18187

Description

The goal of this PR is to add a way to integrate LLMs directly into Solr at index time to fill fields that might be useful (e.g., categories, tags, etc.)

Solution

This PR adds LLM-based document enrichment capabilities to Solr's indexing pipeline via a new DocumentEnrichmentUpdateProcessorFactory in the language-models module. The processor allows users to enrich documents at index time by calling an LLM (via https://github.com/langchain4j/langchain4j) with a configurable prompt built from one or more existing document fields (inputFields), and storing the model's response into an output field. The output field can be of different types (i.e., string, text, int, long, float, double, boolean, and date) and can be single-valued or multi-valued. The structured output has been used to adapt to the output field type.

The implementation has taken inspiration from the text-to-vector feature in the same module. This has been done to keep the implementation consistent with conventions already in the language-models module.

Note: this PR was developed with assistance from Claude Code (Anthropic).

Tests

Tests covering configuration validation (missing required params, conflicting params, invalid field types, placeholder mismatches), and processor initialization.

Tests covering single-valued and multi-valued output fields of all supported types, multi-input-field prompts, prompt file loading, error handling (model exceptions, ambiguous/malformed JSON responses, unsupported model types), and skipNullOrMissingFieldValues behaviour. All the supported models have been tested.

Checklist

Please review the following and check all that apply:

  • I have reviewed the guidelines for How to Contribute and my code conforms to the standards described there to the best of my ability.
  • I have created a Jira issue and added the issue ID to my pull request title.
  • I have given Solr maintainers access to contribute to my PR branch. (optional but recommended, not available for branches on forks living under an organisation)
  • I have developed this patch against the main branch.
  • I have run ./gradlew check.
  • I have added tests for my changes.
  • I have added documentation for the Reference Guide
  • I have added a changelog entry for my change

@github-actions github-actions bot added documentation Improvements or additions to documentation dependencies Dependency upgrades tool:build tests labels Apr 1, 2026
Copy link
Copy Markdown
Contributor

@aruggero aruggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments

restTestHarness.delete(ManagedChatModelStore.REST_END_POINT + "/model1");
}

private UpdateRequestProcessor createUpdateProcessor(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this always be generalised and used for all the tests? In some of them, you are now repeating this code with small changes...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same as createUpdateProcessor a part from the creation of the request and getInstance()
maybe we can exclude the solr request + getInstance() and use that method also here? calling it like "initializeUpdateProcessorFactory"?
what do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a function initializeUpdateProcessorFactory that is used inside createUpdateProcessor. In this way, the code inside the first one can be reused

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed tests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why some test could not use these new functions?
e.g. init_multipleInputFields_shouldInitAllFields

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept them unrelated to the model creation, just to see the proper initialization of the Factory. I can see if this can be changed if you want


@Test
public void init_promptFileWithMissingPlaceholder_shouldThrowExceptionInInform() {
NamedList<String> args = new NamedList<>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same as createUpdateProcessor a part from the creation of the request and getInstance()
maybe we can exclude the solr request + getInstance() and use that method also here? calling it like "initializeUpdateProcessorFactory"?
what do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed and fixed tests

restTestHarness.delete(ManagedChatModelStore.REST_END_POINT + "/model1");
}

private UpdateRequestProcessor createUpdateProcessor(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same as createUpdateProcessor a part from the creation of the request and getInstance()
maybe we can exclude the solr request + getInstance() and use that method also here? calling it like "initializeUpdateProcessorFactory"?
what do you think?

example above). These tokens are _mandatory_ for this module to work properly. Solr will throw an error if the
parameters are not properly defined.
For example, both the prompt and the content of the file prompt.txt, must contain the text '{string_field}', which
will be substituted with the content of the `string_field` field for each document. An example of a valid prompt with
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the part so far could be explained in a more schematic and better understandable way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

</updateRequestProcessorChain>
----

Another way of using more than one `inputField` is by using the following notation, instead of more than one parameter
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple inputField could also be defined by using the following notation:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

</arr>
----

The LLM response is mapped to the specified `outputField`. Note that this module only supports a subset of Solr's
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also specify that only one outputField is supported

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

====

=== Index first and enrich your documents on a second pass
LLM calls are usually quite slow, so, depending on your use case it could be a good idea to index first your documents
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLM calls are typically slow, so depending on your use case, it may be preferable to first index your documents and enrich them with LLM-generated fields at a later stage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


=== Models

* A model in this module is a chat model, that answers with text given a prompt.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

=== Models

* A model in this module is a chat model, that answers with text given a prompt.
* A model in this Solr module is a reference to an external API that runs the Large Language Model responsible for chat
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


Exactly one of the following parameters is required: `prompt` or `promptFile`.

Another important feature of this module is that one (or more) `inputField` needs to be injected in the prompt. This is
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

</arr>
----

The LLM response is mapped to the specified `outputField`. Note that this module only supports a subset of Solr's
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

</updateRequestProcessorChain>
----

Another way of using more than one `inputField` is by using the following notation, instead of more than one parameter
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

====

=== Index first and enrich your documents on a second pass
LLM calls are usually quite slow, so, depending on your use case it could be a good idea to index first your documents
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

boolean `enriched` field to `true`.

Faceting or querying on the boolean `enriched` field can also give you a quick idea on how many documents have been
enriched with the new generated fields.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note to link to the section of the documentation related to the use of update chains

}
----

== How to Trigger Document Enrichment during Indexing
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part has not been moved to the top near the model configuration in the solrconfig

Comment on lines +288 to +290
|===
+
One (or more) `inputField` needs to be injected in the prompt. This is done by some special tokens, that are the
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just say this is the field whose content is used as input/passed to the LLM to enrich the document. And that there could be more than one inputField defined.

I would move the other part about the special tokens to the prompt parameter explanation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At most, you could say that every inputField declared must be referred to in the prompt

module to work properly. Solr will throw an error if the parameters are not properly defined.
For example, both the prompt or the content of the file `prompt.txt`, must contain the text '{string_field}', which
will be substituted with the content of the `string_field` field for each document. An example of a valid prompt with
multiple input fields is as follows:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe here I would say:
Multiple inputField could also be defined by using one of the following notations:

and then list the two ways

These fields _can_ be multivalued. Solr uses structured output from LangChain4j to deal with LLMs' responses.


`prompt` or `promptFile`::
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here I would say that there are two ways of defining a prompt, one directly in the config and one through a file...
then I would explain how the prompt should be structured and the thing related to the inputFields

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency upgrades documentation Improvements or additions to documentation tests tool:build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants