spring-projects/spring-ai · warning
Query expansion result is null. Returning the input query…
Error message
Query expansion result is null. Returning the input query unchanged.
What it means
MultiQueryExpander.expand calls the chat model to generate query variants; when the model returns null content the expander logs this warning and falls back to returning the original query unchanged. Expansion is thus silently skipped rather than failing the retrieval pipeline.
Solutions
- Increase maxTokens / adjust the expansion prompt so the model returns text.
- Log the full ChatResponse to see why content is null (finish reason, filter).
- Treat the fallback as expected: ensure downstream retrieval handles a single-query list.
- Use a different or stronger model for expansion if nulls are frequent.
Example fix
// before
MultiQueryExpander expander = MultiQueryExpander.builder()
.chatClient(chatClient)
.numberOfQueries(3)
.build();
// after — set explicit options so the model always returns text
MultiQueryExpander expander = MultiQueryExpander.builder()
.chatClient(chatClient)
.numberOfQueries(3)
.prompt(UPDATE_PROMPT) // tuned prompt
.build();
// and configure model options: maxTokens(512) on the underlying chat model Defensive patterns
Strategy: fallback
Validate before calling
// pre-check the chat model config used for expansion
if (options.getMaxTokens() != null && options.getMaxTokens() < 256) {
logger.warn("maxTokens too low for query expansion; null content likely");
} Try / catch
List<Query> queries = expander.expand(new Query("original"));
if (queries.size() == 1 && queries.get(0).text().equals("original")) {
logger.info("Expansion fell back to original query; check model output");
} Prevention
- Check finishReason/filters when content is null instead of only the fallback.
- Raise maxTokens for the expansion model.
- Use an instruct model that reliably returns plain text.
- Design the retrieval pipeline to tolerate single-query results.
When it happens
Trigger: expand() invokes the prompt with number/query params and .call().content() returns null (model produced no content, e.g. content filter, empty completion, or a reasoning-only response).
Common situations: ChatModel returning null content due to safety filters; maxTokens too small so no content is generated; misconfigured model returning tool calls instead of text; provider outage producing empty completions.
Understand the failure class
Background: "empty response", "returned no data", "empty embeddings": what HTTP 200-with-empty-body errors mean across libraries — this error's family across 36 libraries.
Related errors
- Query compression result is null/empty. Returning the input…
- Query expansion result does not contain the requested
- Could not parse text search score
- Database product name is null or empty, defaulting to…
- Due to failure in establishing JDBC connection or parsing…
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/1919c9486679f360.
Report an issue: GitHub.
Appendix: source
Thrown at spring-ai-rag/src/main/java/org/springframework/ai/rag/preretrieval/query/expansion/MultiQueryExpander.java:112
}
@Override
public List<Query> expand(Query query) {
Assert.notNull(query, "query cannot be null");
if (logger.isDebugEnabled()) {
logger.debug("Generating " + this.numberOfQueries + " query variants");
}
var response = this.chatClient.prompt()
.user(user -> user.text(this.promptTemplate.getTemplate())
.param("number", this.numberOfQueries)
.param("query", query.text()))
.call()
.content();
if (response == null) {
logger.warn("Query expansion result is null. Returning the input query unchanged.");
return List.of(query);
}
var queryVariants = Arrays.asList(response.split("\n"));
if (CollectionUtils.isEmpty(queryVariants) || this.numberOfQueries != queryVariants.size()) {
if (logger.isWarnEnabled()) {
logger.warn("Query expansion result does not contain the requested " + this.numberOfQueries
+ " variants. Returning the input query unchanged.");
}
return List.of(query);
}
var queries = queryVariants.stream()
.filter(StringUtils::hasText)
.map(queryText -> query.mutate().text(queryText).build())
.collect(Collectors.toList());
View on GitHub (pinned to 98a7beda4f)