Skip to content

Commit 7fe3799

Browse files
[8.18] Prevent field caps from failing due to can match failure (#134134) (#134558)
`FieldCapabilitiesFetcher` performs a can-match in order to quickly return an empty response if no shard can match. However, if can-match fails for some reason, it can cause the field capabilities request to fail. An example of that is when a semantic query is used as filter. can-match will fail as it won't be able to expand the inference results of the query. In cases like that, it makes no sense to fail the field capabilities request. Instead, we should treat can-match as returning `true` to proceed. This change does that by following suit with other callers of can-match. Fixes #116106 (cherry picked from commit 7295189)
1 parent 922d9fd commit 7fe3799

File tree

8 files changed

+181
-82
lines changed

8 files changed

+181
-82
lines changed

docs/changelog/134134.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 134134
2+
summary: Prevent field caps from failing due to can match failure
3+
area: Search
4+
type: bug
5+
issues:
6+
- 116106

server/src/internalClusterTest/java/org/elasticsearch/search/fieldcaps/CCSFieldCapabilitiesIT.java

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@
1414
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse;
1515
import org.elasticsearch.client.internal.Client;
1616
import org.elasticsearch.common.settings.Settings;
17-
import org.elasticsearch.plugins.Plugin;
18-
import org.elasticsearch.search.fieldcaps.FieldCapabilitiesIT.ExceptionOnRewriteQueryBuilder;
19-
import org.elasticsearch.search.fieldcaps.FieldCapabilitiesIT.ExceptionOnRewriteQueryPlugin;
17+
import org.elasticsearch.index.shard.IllegalIndexShardStateException;
2018
import org.elasticsearch.test.AbstractMultiClustersTestCase;
2119
import org.elasticsearch.transport.RemoteTransportException;
2220

23-
import java.util.ArrayList;
21+
import java.io.IOException;
2422
import java.util.Arrays;
2523
import java.util.Collection;
2624
import java.util.List;
@@ -43,14 +41,7 @@ protected boolean reuseClusters() {
4341
return false;
4442
}
4543

46-
@Override
47-
protected Collection<Class<? extends Plugin>> nodePlugins(String clusterAlias) {
48-
final List<Class<? extends Plugin>> plugins = new ArrayList<>(super.nodePlugins(clusterAlias));
49-
plugins.add(ExceptionOnRewriteQueryPlugin.class);
50-
return plugins;
51-
}
52-
53-
public void testFailuresFromRemote() {
44+
public void testFailuresFromRemote() throws IOException {
5445
Settings indexSettings = Settings.builder().put("index.number_of_replicas", 0).build();
5546
final Client localClient = client(LOCAL_CLUSTER);
5647
final Client remoteClient = client("remote_cluster");
@@ -68,11 +59,10 @@ public void testFailuresFromRemote() {
6859
FieldCapabilitiesResponse response = client().prepareFieldCaps("*", "remote_cluster:*").setFields("*").get();
6960
assertThat(Arrays.asList(response.getIndices()), containsInAnyOrder(localIndex, "remote_cluster:" + remoteErrorIndex));
7061

71-
// adding an index filter so remote call should fail
72-
response = client().prepareFieldCaps("*", "remote_cluster:*")
73-
.setFields("*")
74-
.setIndexFilter(new ExceptionOnRewriteQueryBuilder())
75-
.get();
62+
// Closed shards will result to index error because shards must be in readable state
63+
FieldCapabilitiesIT.closeShards(cluster("remote_cluster"), remoteErrorIndex);
64+
65+
response = client().prepareFieldCaps("*", "remote_cluster:*").setFields("*").get();
7666
assertThat(response.getIndices()[0], equalTo(localIndex));
7767
assertThat(response.getFailedIndicesCount(), equalTo(1));
7868
FieldCapabilitiesFailure failure = response.getFailures()
@@ -83,25 +73,22 @@ public void testFailuresFromRemote() {
8373
Exception ex = failure.getException();
8474
assertEquals(RemoteTransportException.class, ex.getClass());
8575
Throwable cause = ExceptionsHelper.unwrapCause(ex);
86-
assertEquals(IllegalArgumentException.class, cause.getClass());
87-
assertEquals("I throw because I choose to.", cause.getMessage());
76+
assertEquals(IllegalIndexShardStateException.class, cause.getClass());
77+
assertEquals(
78+
"CurrentState[CLOSED] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]",
79+
cause.getMessage()
80+
);
8881

8982
// if we only query the remote we should get back an exception only
90-
ex = expectThrows(
91-
IllegalArgumentException.class,
92-
client().prepareFieldCaps("remote_cluster:*").setFields("*").setIndexFilter(new ExceptionOnRewriteQueryBuilder())
93-
);
94-
assertEquals("I throw because I choose to.", ex.getMessage());
83+
ex = expectThrows(IllegalIndexShardStateException.class, client().prepareFieldCaps("remote_cluster:*").setFields("*"));
84+
assertEquals("CurrentState[CLOSED] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]", ex.getMessage());
9585

9686
// add an index that doesn't fail to the remote
9787
assertAcked(remoteClient.admin().indices().prepareCreate("okay_remote_index"));
9888
remoteClient.prepareIndex("okay_remote_index").setId("2").setSource("foo", "bar").get();
9989
remoteClient.admin().indices().prepareRefresh("okay_remote_index").get();
10090

101-
response = client().prepareFieldCaps("*", "remote_cluster:*")
102-
.setFields("*")
103-
.setIndexFilter(new ExceptionOnRewriteQueryBuilder())
104-
.get();
91+
response = client().prepareFieldCaps("*", "remote_cluster:*").setFields("*").get();
10592
assertThat(Arrays.asList(response.getIndices()), containsInAnyOrder(localIndex, "remote_cluster:okay_remote_index"));
10693
assertThat(response.getFailedIndicesCount(), equalTo(1));
10794
failure = response.getFailures()
@@ -110,8 +97,8 @@ public void testFailuresFromRemote() {
11097
.findFirst()
11198
.get();
11299
ex = failure.getException();
113-
assertEquals(IllegalArgumentException.class, ex.getClass());
114-
assertEquals("I throw because I choose to.", ex.getMessage());
100+
assertEquals(IllegalIndexShardStateException.class, ex.getClass());
101+
assertEquals("CurrentState[CLOSED] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]", ex.getMessage());
115102
}
116103

117104
public void testFailedToConnectToRemoteCluster() throws Exception {

server/src/internalClusterTest/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java

Lines changed: 28 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
import org.elasticsearch.index.query.QueryBuilder;
4747
import org.elasticsearch.index.query.QueryBuilders;
4848
import org.elasticsearch.index.query.QueryRewriteContext;
49-
import org.elasticsearch.index.query.SearchExecutionContext;
49+
import org.elasticsearch.index.shard.IllegalIndexShardStateException;
5050
import org.elasticsearch.index.shard.IndexShard;
5151
import org.elasticsearch.index.shard.ShardId;
5252
import org.elasticsearch.indices.IndexClosedException;
@@ -58,6 +58,7 @@
5858
import org.elasticsearch.search.DummyQueryBuilder;
5959
import org.elasticsearch.tasks.TaskInfo;
6060
import org.elasticsearch.test.ESIntegTestCase;
61+
import org.elasticsearch.test.InternalTestCluster;
6162
import org.elasticsearch.test.MockLog;
6263
import org.elasticsearch.test.junit.annotations.TestLogging;
6364
import org.elasticsearch.test.transport.MockTransportService;
@@ -77,6 +78,7 @@
7778
import java.util.HashSet;
7879
import java.util.List;
7980
import java.util.Map;
81+
import java.util.Set;
8082
import java.util.concurrent.CancellationException;
8183
import java.util.concurrent.CountDownLatch;
8284
import java.util.concurrent.TimeUnit;
@@ -85,7 +87,6 @@
8587
import java.util.function.Function;
8688
import java.util.stream.IntStream;
8789

88-
import static java.util.Collections.singletonList;
8990
import static org.elasticsearch.action.support.ActionTestUtils.wrapAsRestResponseListener;
9091
import static org.elasticsearch.index.shard.IndexShardTestCase.closeShardNoCheck;
9192
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
@@ -190,7 +191,7 @@ public void setUp() throws Exception {
190191

191192
@Override
192193
protected Collection<Class<? extends Plugin>> nodePlugins() {
193-
return List.of(TestMapperPlugin.class, ExceptionOnRewriteQueryPlugin.class, BlockingOnRewriteQueryPlugin.class);
194+
return List.of(TestMapperPlugin.class, BlockingOnRewriteQueryPlugin.class);
194195
}
195196

196197
@Override
@@ -477,30 +478,36 @@ public void testFieldMetricsAndDimensions() {
477478
assertThat(response.get().get("some_dimension").get("keyword").nonDimensionIndices(), array(equalTo("new_index")));
478479
}
479480

480-
public void testFailures() throws InterruptedException {
481+
public void testFailures() throws IOException {
481482
// in addition to the existing "old_index" and "new_index", create two where the test query throws an error on rewrite
482483
assertAcked(prepareCreate("index1-error"), prepareCreate("index2-error"));
483484
ensureGreen("index1-error", "index2-error");
484-
FieldCapabilitiesResponse response = client().prepareFieldCaps()
485+
486+
// Closed shards will result to index error because shards must be in readable state
487+
closeShards(internalCluster(), "index1-error", "index2-error");
488+
489+
FieldCapabilitiesResponse response = client().prepareFieldCaps("old_index", "new_index", "index1-error", "index2-error")
485490
.setFields("*")
486-
.setIndexFilter(new ExceptionOnRewriteQueryBuilder())
487491
.get();
488492
assertEquals(1, response.getFailures().size());
489493
assertEquals(2, response.getFailedIndicesCount());
490494
assertThat(response.getFailures().get(0).getIndices(), arrayContainingInAnyOrder("index1-error", "index2-error"));
491495
Exception failure = response.getFailures().get(0).getException();
492-
assertEquals(IllegalArgumentException.class, failure.getClass());
493-
assertEquals("I throw because I choose to.", failure.getMessage());
496+
assertEquals(IllegalIndexShardStateException.class, failure.getClass());
497+
assertEquals(
498+
"CurrentState[CLOSED] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]",
499+
failure.getMessage()
500+
);
494501

495502
// the "indices" section should not include failed ones
496503
assertThat(Arrays.asList(response.getIndices()), containsInAnyOrder("old_index", "new_index"));
497504

498505
// if all requested indices failed, we fail the request by throwing the exception
499-
IllegalArgumentException ex = expectThrows(
500-
IllegalArgumentException.class,
501-
client().prepareFieldCaps("index1-error", "index2-error").setFields("*").setIndexFilter(new ExceptionOnRewriteQueryBuilder())
506+
IllegalIndexShardStateException ex = expectThrows(
507+
IllegalIndexShardStateException.class,
508+
client().prepareFieldCaps("index1-error", "index2-error").setFields("*")
502509
);
503-
assertEquals("I throw because I choose to.", ex.getMessage());
510+
assertEquals("CurrentState[CLOSED] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]", ex.getMessage());
504511
}
505512

506513
private void populateTimeRangeIndices() throws Exception {
@@ -852,45 +859,17 @@ private void assertIndices(FieldCapabilitiesResponse response, String... indices
852859
assertArrayEquals(indices, response.getIndices());
853860
}
854861

855-
/**
856-
* Adds an "exception" query that throws on rewrite if the index name contains the string "error"
857-
*/
858-
public static class ExceptionOnRewriteQueryPlugin extends Plugin implements SearchPlugin {
859-
860-
public ExceptionOnRewriteQueryPlugin() {}
861-
862-
@Override
863-
public List<QuerySpec<?>> getQueries() {
864-
return singletonList(
865-
new QuerySpec<>("exception", ExceptionOnRewriteQueryBuilder::new, p -> new ExceptionOnRewriteQueryBuilder())
866-
);
867-
}
868-
}
869-
870-
static class ExceptionOnRewriteQueryBuilder extends DummyQueryBuilder {
871-
872-
public static final String NAME = "exception";
873-
874-
ExceptionOnRewriteQueryBuilder() {}
875-
876-
ExceptionOnRewriteQueryBuilder(StreamInput in) throws IOException {
877-
super(in);
878-
}
879-
880-
@Override
881-
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
882-
SearchExecutionContext searchExecutionContext = queryRewriteContext.convertToSearchExecutionContext();
883-
if (searchExecutionContext != null) {
884-
if (searchExecutionContext.indexMatches("*error*")) {
885-
throw new IllegalArgumentException("I throw because I choose to.");
862+
static void closeShards(InternalTestCluster cluster, String... indices) throws IOException {
863+
final Set<String> indicesToClose = Set.of(indices);
864+
for (String node : cluster.getNodeNames()) {
865+
final IndicesService indicesService = cluster.getInstance(IndicesService.class, node);
866+
for (IndexService indexService : indicesService) {
867+
if (indicesToClose.contains(indexService.getMetadata().getIndex().getName())) {
868+
for (IndexShard indexShard : indexService) {
869+
closeShardNoCheck(indexShard);
870+
}
886871
}
887872
}
888-
return this;
889-
}
890-
891-
@Override
892-
public String getWriteableName() {
893-
return NAME;
894873
}
895874
}
896875

server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFetcher.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,17 @@ private static boolean canMatchShard(
237237
QueryBuilder indexFilter,
238238
long nowInMillis,
239239
SearchExecutionContext searchExecutionContext
240-
) throws IOException {
240+
) {
241241
assert alwaysMatches(indexFilter) == false : "should not be called for always matching [" + indexFilter + "]";
242242
assert nowInMillis != 0L;
243243
ShardSearchRequest searchRequest = new ShardSearchRequest(shardId, nowInMillis, AliasFilter.EMPTY);
244244
searchRequest.source(new SearchSourceBuilder().query(indexFilter));
245-
return SearchService.queryStillMatchesAfterRewrite(searchRequest, searchExecutionContext);
245+
try {
246+
return SearchService.queryStillMatchesAfterRewrite(searchRequest, searchExecutionContext);
247+
} catch (Exception e) {
248+
// treat as if shard is still a potential match
249+
return true;
250+
}
246251
}
247252

248253
private static boolean alwaysMatches(QueryBuilder indexFilter) {

x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.features.NodeFeature;
1212
import org.elasticsearch.xpack.inference.mapper.SemanticInferenceMetadataFieldsMapper;
1313
import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper;
14+
import org.elasticsearch.xpack.inference.queries.SemanticQueryBuilder;
1415
import org.elasticsearch.xpack.inference.rank.random.RandomRankRetrieverBuilder;
1516
import org.elasticsearch.xpack.inference.rank.textsimilarity.TextSimilarityRankRetrieverBuilder;
1617

@@ -68,7 +69,8 @@ public Set<NodeFeature> getTestFeatures() {
6869
SEMANTIC_KNN_FILTER_FIX,
6970
SEMANTIC_TEXT_MATCH_ALL_HIGHLIGHTER,
7071
TEST_RERANKING_SERVICE_PARSE_TEXT_AS_SCORE,
71-
SEMANTIC_QUERY_REWRITE_INTERCEPTORS_PROPAGATE_BOOST_AND_QUERY_NAME_FIX
72+
SEMANTIC_QUERY_REWRITE_INTERCEPTORS_PROPAGATE_BOOST_AND_QUERY_NAME_FIX,
73+
SemanticQueryBuilder.SEMANTIC_QUERY_FILTER_FIELD_CAPS_FIX
7274
);
7375
}
7476
}

x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.cluster.metadata.InferenceFieldMetadata;
1717
import org.elasticsearch.common.io.stream.StreamInput;
1818
import org.elasticsearch.common.io.stream.StreamOutput;
19+
import org.elasticsearch.features.NodeFeature;
1920
import org.elasticsearch.index.mapper.MappedFieldType;
2021
import org.elasticsearch.index.query.AbstractQueryBuilder;
2122
import org.elasticsearch.index.query.MatchNoneQueryBuilder;
@@ -52,6 +53,8 @@
5253
public class SemanticQueryBuilder extends AbstractQueryBuilder<SemanticQueryBuilder> {
5354
public static final String NAME = "semantic";
5455

56+
public static final NodeFeature SEMANTIC_QUERY_FILTER_FIELD_CAPS_FIX = new NodeFeature("semantic_query.filter_field_caps_fix");
57+
5558
private static final ParseField FIELD_FIELD = new ParseField("field");
5659
private static final ParseField QUERY_FIELD = new ParseField("query");
5760
private static final ParseField LENIENT_FIELD = new ParseField("lenient");

x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,38 @@ setup:
33
cluster_features: "gte_v8.15.0"
44
reason: semantic_text introduced in 8.15.0
55

6+
- do:
7+
inference.put:
8+
task_type: sparse_embedding
9+
inference_id: sparse-inference-id
10+
body: >
11+
{
12+
"service": "test_service",
13+
"service_settings": {
14+
"model": "my_model",
15+
"api_key": "abc64"
16+
},
17+
"task_settings": {
18+
}
19+
}
20+
21+
- do:
22+
inference.put:
23+
task_type: text_embedding
24+
inference_id: dense-inference-id
25+
body: >
26+
{
27+
"service": "text_embedding_test_service",
28+
"service_settings": {
29+
"model": "my_model",
30+
"dimensions": 4,
31+
"similarity": "cosine",
32+
"api_key": "abc64"
33+
},
34+
"task_settings": {
35+
}
36+
}
37+
638
- do:
739
indices.create:
840
index: test-index
@@ -359,3 +391,30 @@ setup:
359391
index: test-always-include-inference-id-index
360392

361393
- exists: test-always-include-inference-id-index.mappings.properties.semantic_field.inference_id
394+
395+
---
396+
"Field caps with semantic query does not fail":
397+
- requires:
398+
cluster_features: "semantic_query.filter_field_caps_fix"
399+
reason: "fixed bug with semantic query filtering in field_caps (#116106)"
400+
# We need at least one document present to exercise can-match phase
401+
- do:
402+
index:
403+
index: test-index
404+
id: doc_1
405+
body:
406+
sparse_field: "This is a story about a cat and a dog."
407+
refresh: true
408+
409+
- do:
410+
field_caps:
411+
index: test-index
412+
fields: "*"
413+
body:
414+
index_filter:
415+
semantic:
416+
field: "sparse_field"
417+
query: "test"
418+
419+
- match: { indices: [ "test-index" ] }
420+
- match: { fields.sparse_field.text.searchable: true }

0 commit comments

Comments
 (0)