Skip to content

Update Test Framework To Handle Query Rewrites That Rely on Non-Null Searchers #129160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 10, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@

package org.elasticsearch.test;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.index.RandomIndexWriter;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.action.support.PlainActionFuture;
Expand Down Expand Up @@ -45,6 +49,7 @@
import org.elasticsearch.xcontent.json.JsonStringEncoder;
import org.elasticsearch.xcontent.json.JsonXContent;

import java.io.Closeable;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneOffset;
Expand Down Expand Up @@ -453,82 +458,87 @@ protected boolean builderGeneratesCacheableQueries() {
return true;
}

protected IndexReaderManager getIndexReaderManager() {
return NullIndexReaderManager.INSTANCE;
}

/**
* Test creates the {@link Query} from the {@link QueryBuilder} under test and delegates the
* assertions being made on the result to the implementing subclass.
*/
public void testToQuery() throws IOException {
for (int runs = 0; runs < NUMBER_OF_TESTQUERIES; runs++) {
SearchExecutionContext context = createSearchExecutionContext();
assert context.isCacheable();
context.setAllowUnmappedFields(true);
QB firstQuery = createTestQueryBuilder();
QB controlQuery = copyQuery(firstQuery);
/* we use a private rewrite context here since we want the most realistic way of asserting that we are cacheable or not.
* We do it this way in SearchService where
* we first rewrite the query with a private context, then reset the context and then build the actual lucene query*/
QueryBuilder rewritten = rewriteQuery(firstQuery, createQueryRewriteContext(), new SearchExecutionContext(context));
Query firstLuceneQuery = rewritten.toQuery(context);
assertNotNull("toQuery should not return null", firstLuceneQuery);
assertLuceneQuery(firstQuery, firstLuceneQuery, context);
// remove after assertLuceneQuery since the assertLuceneQuery impl might access the context as well
assertEquals(
"query is not equal to its copy after calling toQuery, firstQuery: " + firstQuery + ", secondQuery: " + controlQuery,
firstQuery,
controlQuery
);
assertEquals(
"equals is not symmetric after calling toQuery, firstQuery: " + firstQuery + ", secondQuery: " + controlQuery,
controlQuery,
firstQuery
);
assertThat(
"query copy's hashcode is different from original hashcode after calling toQuery, firstQuery: "
+ firstQuery
+ ", secondQuery: "
+ controlQuery,
controlQuery.hashCode(),
equalTo(firstQuery.hashCode())
);

QB secondQuery = copyQuery(firstQuery);
// query _name never should affect the result of toQuery, we randomly set it to make sure
if (randomBoolean()) {
secondQuery.queryName(
secondQuery.queryName() == null
? randomAlphaOfLengthBetween(1, 30)
: secondQuery.queryName() + randomAlphaOfLengthBetween(1, 10)
);
}
context = new SearchExecutionContext(context);
Query secondLuceneQuery = rewriteQuery(secondQuery, createQueryRewriteContext(), new SearchExecutionContext(context)).toQuery(
context
);
assertNotNull("toQuery should not return null", secondLuceneQuery);
assertLuceneQuery(secondQuery, secondLuceneQuery, context);

if (builderGeneratesCacheableQueries()) {
try (IndexReaderManager irm = getIndexReaderManager()) {
SearchExecutionContext context = createSearchExecutionContext(irm.getIndexSearcher());
assert context.isCacheable();
context.setAllowUnmappedFields(true);
QB firstQuery = createTestQueryBuilder();
QB controlQuery = copyQuery(firstQuery);
/* we use a private rewrite context here since we want the most realistic way of asserting that we are cacheable or not.
* We do it this way in SearchService where
* we first rewrite the query with a private context, then reset the context and then build the actual lucene query*/
QueryBuilder rewritten = rewriteQuery(firstQuery, createQueryRewriteContext(), new SearchExecutionContext(context));
Query firstLuceneQuery = rewritten.toQuery(context);
assertNotNull("toQuery should not return null", firstLuceneQuery);
assertLuceneQuery(firstQuery, firstLuceneQuery, context);
// remove after assertLuceneQuery since the assertLuceneQuery impl might access the context as well
assertEquals(
"two equivalent query builders lead to different lucene queries hashcode",
secondLuceneQuery.hashCode(),
firstLuceneQuery.hashCode()
"query is not equal to its copy after calling toQuery, firstQuery: " + firstQuery + ", secondQuery: " + controlQuery,
firstQuery,
controlQuery
);
assertEquals(
"two equivalent query builders lead to different lucene queries",
rewrite(secondLuceneQuery),
rewrite(firstLuceneQuery)
"equals is not symmetric after calling toQuery, firstQuery: " + firstQuery + ", secondQuery: " + controlQuery,
controlQuery,
firstQuery
);
assertThat(
"query copy's hashcode is different from original hashcode after calling toQuery, firstQuery: "
+ firstQuery
+ ", secondQuery: "
+ controlQuery,
controlQuery.hashCode(),
equalTo(firstQuery.hashCode())
);
}

if (supportsBoost() && firstLuceneQuery instanceof MatchNoDocsQuery == false) {
secondQuery.boost(firstQuery.boost() + 1f + randomFloat());
Query thirdLuceneQuery = rewriteQuery(secondQuery, createQueryRewriteContext(), new SearchExecutionContext(context))
QB secondQuery = copyQuery(firstQuery);
// query _name never should affect the result of toQuery, we randomly set it to make sure
if (randomBoolean()) {
secondQuery.queryName(
secondQuery.queryName() == null
? randomAlphaOfLengthBetween(1, 30)
: secondQuery.queryName() + randomAlphaOfLengthBetween(1, 10)
);
}
context = new SearchExecutionContext(context);
Query secondLuceneQuery = rewriteQuery(secondQuery, createQueryRewriteContext(), new SearchExecutionContext(context))
.toQuery(context);
assertNotEquals(
"modifying the boost doesn't affect the corresponding lucene query",
rewrite(firstLuceneQuery),
rewrite(thirdLuceneQuery)
);
assertNotNull("toQuery should not return null", secondLuceneQuery);
assertLuceneQuery(secondQuery, secondLuceneQuery, context);

if (builderGeneratesCacheableQueries()) {
assertEquals(
"two equivalent query builders lead to different lucene queries hashcode",
secondLuceneQuery.hashCode(),
firstLuceneQuery.hashCode()
);
assertEquals(
"two equivalent query builders lead to different lucene queries",
rewrite(secondLuceneQuery),
rewrite(firstLuceneQuery)
);
}

if (supportsBoost() && firstLuceneQuery instanceof MatchNoDocsQuery == false) {
secondQuery.boost(firstQuery.boost() + 1f + randomFloat());
Query thirdLuceneQuery = rewriteQuery(secondQuery, createQueryRewriteContext(), new SearchExecutionContext(context))
.toQuery(context);
assertNotEquals(
"modifying the boost doesn't affect the corresponding lucene query",
rewrite(firstLuceneQuery),
rewrite(thirdLuceneQuery)
);
}
}
}
}
Expand Down Expand Up @@ -938,9 +948,75 @@ public boolean isTextField(String fieldName) {
*/
public void testCacheability() throws IOException {
QB queryBuilder = createTestQueryBuilder();
SearchExecutionContext context = createSearchExecutionContext();
QueryBuilder rewriteQuery = rewriteQuery(queryBuilder, createQueryRewriteContext(), new SearchExecutionContext(context));
assertNotNull(rewriteQuery.toQuery(context));
assertTrue("query should be cacheable: " + queryBuilder.toString(), context.isCacheable());
try (IndexReaderManager irm = getIndexReaderManager()) {
SearchExecutionContext context = createSearchExecutionContext(irm.getIndexSearcher());
QueryBuilder rewriteQuery = rewriteQuery(queryBuilder, createQueryRewriteContext(), new SearchExecutionContext(context));
assertNotNull(rewriteQuery.toQuery(context));
assertTrue("query should be cacheable: " + queryBuilder.toString(), context.isCacheable());
}
}

public static class IndexReaderManager implements Closeable {
private final Directory directory;
private RandomIndexWriter indexWriter;
private IndexReader indexReader;
private IndexSearcher indexSearcher;

public IndexReaderManager() {
this.directory = newDirectory();
}

private IndexReaderManager(Directory directory) {
this.directory = directory;
}

public IndexReader getIndexReader() throws IOException {
if (indexReader == null) {
indexWriter = new RandomIndexWriter(random(), directory);
initIndexWriter(indexWriter);
indexReader = indexWriter.getReader();
}
return indexReader;
}

public IndexSearcher getIndexSearcher() throws IOException {
if (indexSearcher == null) {
indexSearcher = newSearcher(getIndexReader());
}
return indexSearcher;
}

@Override
public void close() throws IOException {
if (indexReader != null) {
indexReader.close();
}
if (indexWriter != null) {
indexWriter.close();
}
if (directory != null) {
directory.close();
}
}

protected void initIndexWriter(RandomIndexWriter indexWriter) {}
}

public static class NullIndexReaderManager extends IndexReaderManager {
public static final NullIndexReaderManager INSTANCE = new NullIndexReaderManager();

public NullIndexReaderManager() {
super(null);
}

@Override
public IndexReader getIndexReader() {
return null;
}

@Override
public IndexSearcher getIndexSearcher() {
return null;
}
}
}