Skip to content

Commit 7295189

Browse files
Prevent field caps from failing due to can match failure (#134134)
`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
1 parent 1ccb58a commit 7295189

File tree

8 files changed

+116
-83
lines changed

8 files changed

+116
-83
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 & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,12 @@
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;
25-
import java.util.Collection;
2623
import java.util.List;
2724

2825
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
@@ -46,14 +43,7 @@ protected boolean reuseClusters() {
4643
return false;
4744
}
4845

49-
@Override
50-
protected Collection<Class<? extends Plugin>> nodePlugins(String clusterAlias) {
51-
final List<Class<? extends Plugin>> plugins = new ArrayList<>(super.nodePlugins(clusterAlias));
52-
plugins.add(ExceptionOnRewriteQueryPlugin.class);
53-
return plugins;
54-
}
55-
56-
public void testFailuresFromRemote() {
46+
public void testFailuresFromRemote() throws IOException {
5747
Settings indexSettings = Settings.builder().put("index.number_of_replicas", 0).build();
5848
final Client localClient = client(LOCAL_CLUSTER);
5949
final Client remoteClient = client("remote_cluster");
@@ -71,11 +61,10 @@ public void testFailuresFromRemote() {
7161
FieldCapabilitiesResponse response = client().prepareFieldCaps("*", "remote_cluster:*").setFields("*").get();
7262
assertThat(Arrays.asList(response.getIndices()), containsInAnyOrder(localIndex, "remote_cluster:" + remoteErrorIndex));
7363

74-
// adding an index filter so remote call should fail
75-
response = client().prepareFieldCaps("*", "remote_cluster:*")
76-
.setFields("*")
77-
.setIndexFilter(new ExceptionOnRewriteQueryBuilder())
78-
.get();
64+
// Closed shards will result to index error because shards must be in readable state
65+
FieldCapabilitiesIT.closeShards(cluster("remote_cluster"), remoteErrorIndex);
66+
67+
response = client().prepareFieldCaps("*", "remote_cluster:*").setFields("*").get();
7968
assertThat(response.getIndices()[0], equalTo(localIndex));
8069
assertThat(response.getFailedIndicesCount(), equalTo(1));
8170
FieldCapabilitiesFailure failure = response.getFailures()
@@ -86,25 +75,22 @@ public void testFailuresFromRemote() {
8675
Exception ex = failure.getException();
8776
assertEquals(RemoteTransportException.class, ex.getClass());
8877
Throwable cause = ExceptionsHelper.unwrapCause(ex);
89-
assertEquals(IllegalArgumentException.class, cause.getClass());
90-
assertEquals("I throw because I choose to.", cause.getMessage());
78+
assertEquals(IllegalIndexShardStateException.class, cause.getClass());
79+
assertEquals(
80+
"CurrentState[CLOSED] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]",
81+
cause.getMessage()
82+
);
9183

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

9988
// add an index that doesn't fail to the remote
10089
assertAcked(remoteClient.admin().indices().prepareCreate("okay_remote_index"));
10190
remoteClient.prepareIndex("okay_remote_index").setId("2").setSource("foo", "bar").get();
10291
remoteClient.admin().indices().prepareRefresh("okay_remote_index").get();
10392

104-
response = client().prepareFieldCaps("*", "remote_cluster:*")
105-
.setFields("*")
106-
.setIndexFilter(new ExceptionOnRewriteQueryBuilder())
107-
.get();
93+
response = client().prepareFieldCaps("*", "remote_cluster:*").setFields("*").get();
10894
assertThat(Arrays.asList(response.getIndices()), containsInAnyOrder(localIndex, "remote_cluster:okay_remote_index"));
10995
assertThat(response.getFailedIndicesCount(), equalTo(1));
11096
failure = response.getFailures()
@@ -113,8 +99,8 @@ public void testFailuresFromRemote() {
11399
.findFirst()
114100
.get();
115101
ex = failure.getException();
116-
assertEquals(IllegalArgumentException.class, ex.getClass());
117-
assertEquals("I throw because I choose to.", ex.getMessage());
102+
assertEquals(IllegalIndexShardStateException.class, ex.getClass());
103+
assertEquals("CurrentState[CLOSED] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]", ex.getMessage());
118104
}
119105

120106
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
@@ -47,7 +47,7 @@
4747
import org.elasticsearch.index.query.QueryBuilder;
4848
import org.elasticsearch.index.query.QueryBuilders;
4949
import org.elasticsearch.index.query.QueryRewriteContext;
50-
import org.elasticsearch.index.query.SearchExecutionContext;
50+
import org.elasticsearch.index.shard.IllegalIndexShardStateException;
5151
import org.elasticsearch.index.shard.IndexShard;
5252
import org.elasticsearch.index.shard.ShardId;
5353
import org.elasticsearch.indices.IndexClosedException;
@@ -59,6 +59,7 @@
5959
import org.elasticsearch.search.DummyQueryBuilder;
6060
import org.elasticsearch.tasks.TaskInfo;
6161
import org.elasticsearch.test.ESIntegTestCase;
62+
import org.elasticsearch.test.InternalTestCluster;
6263
import org.elasticsearch.test.MockLog;
6364
import org.elasticsearch.test.junit.annotations.TestLogging;
6465
import org.elasticsearch.test.transport.MockTransportService;
@@ -78,6 +79,7 @@
7879
import java.util.HashSet;
7980
import java.util.List;
8081
import java.util.Map;
82+
import java.util.Set;
8183
import java.util.concurrent.CancellationException;
8284
import java.util.concurrent.CountDownLatch;
8385
import java.util.concurrent.TimeUnit;
@@ -87,7 +89,6 @@
8789
import java.util.function.Predicate;
8890
import java.util.stream.IntStream;
8991

90-
import static java.util.Collections.singletonList;
9192
import static org.elasticsearch.action.support.ActionTestUtils.wrapAsRestResponseListener;
9293
import static org.elasticsearch.index.shard.IndexShardTestCase.closeShardNoCheck;
9394
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
@@ -193,7 +194,7 @@ public void setUp() throws Exception {
193194

194195
@Override
195196
protected Collection<Class<? extends Plugin>> nodePlugins() {
196-
return List.of(TestMapperPlugin.class, ExceptionOnRewriteQueryPlugin.class, BlockingOnRewriteQueryPlugin.class);
197+
return List.of(TestMapperPlugin.class, BlockingOnRewriteQueryPlugin.class);
197198
}
198199

199200
@Override
@@ -445,30 +446,36 @@ public void testFieldMetricsAndDimensions() {
445446
assertThat(response.get().get("some_dimension").get("keyword").nonDimensionIndices(), array(equalTo("new_index")));
446447
}
447448

448-
public void testFailures() throws InterruptedException {
449+
public void testFailures() throws IOException {
449450
// in addition to the existing "old_index" and "new_index", create two where the test query throws an error on rewrite
450451
assertAcked(prepareCreate("index1-error"), prepareCreate("index2-error"));
451452
ensureGreen("index1-error", "index2-error");
452-
FieldCapabilitiesResponse response = client().prepareFieldCaps()
453+
454+
// Closed shards will result to index error because shards must be in readable state
455+
closeShards(internalCluster(), "index1-error", "index2-error");
456+
457+
FieldCapabilitiesResponse response = client().prepareFieldCaps("old_index", "new_index", "index1-error", "index2-error")
453458
.setFields("*")
454-
.setIndexFilter(new ExceptionOnRewriteQueryBuilder())
455459
.get();
456460
assertEquals(1, response.getFailures().size());
457461
assertEquals(2, response.getFailedIndicesCount());
458462
assertThat(response.getFailures().get(0).getIndices(), arrayContainingInAnyOrder("index1-error", "index2-error"));
459463
Exception failure = response.getFailures().get(0).getException();
460-
assertEquals(IllegalArgumentException.class, failure.getClass());
461-
assertEquals("I throw because I choose to.", failure.getMessage());
464+
assertEquals(IllegalIndexShardStateException.class, failure.getClass());
465+
assertEquals(
466+
"CurrentState[CLOSED] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]",
467+
failure.getMessage()
468+
);
462469

463470
// the "indices" section should not include failed ones
464471
assertThat(Arrays.asList(response.getIndices()), containsInAnyOrder("old_index", "new_index"));
465472

466473
// if all requested indices failed, we fail the request by throwing the exception
467-
IllegalArgumentException ex = expectThrows(
468-
IllegalArgumentException.class,
469-
client().prepareFieldCaps("index1-error", "index2-error").setFields("*").setIndexFilter(new ExceptionOnRewriteQueryBuilder())
474+
IllegalIndexShardStateException ex = expectThrows(
475+
IllegalIndexShardStateException.class,
476+
client().prepareFieldCaps("index1-error", "index2-error").setFields("*")
470477
);
471-
assertEquals("I throw because I choose to.", ex.getMessage());
478+
assertEquals("CurrentState[CLOSED] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]", ex.getMessage());
472479
}
473480

474481
private void populateTimeRangeIndices() throws Exception {
@@ -913,45 +920,17 @@ private void assertIndices(FieldCapabilitiesResponse response, String... indices
913920
assertArrayEquals(indices, response.getIndices());
914921
}
915922

916-
/**
917-
* Adds an "exception" query that throws on rewrite if the index name contains the string "error"
918-
*/
919-
public static class ExceptionOnRewriteQueryPlugin extends Plugin implements SearchPlugin {
920-
921-
public ExceptionOnRewriteQueryPlugin() {}
922-
923-
@Override
924-
public List<QuerySpec<?>> getQueries() {
925-
return singletonList(
926-
new QuerySpec<>("exception", ExceptionOnRewriteQueryBuilder::new, p -> new ExceptionOnRewriteQueryBuilder())
927-
);
928-
}
929-
}
930-
931-
static class ExceptionOnRewriteQueryBuilder extends DummyQueryBuilder {
932-
933-
public static final String NAME = "exception";
934-
935-
ExceptionOnRewriteQueryBuilder() {}
936-
937-
ExceptionOnRewriteQueryBuilder(StreamInput in) throws IOException {
938-
super(in);
939-
}
940-
941-
@Override
942-
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
943-
SearchExecutionContext searchExecutionContext = queryRewriteContext.convertToSearchExecutionContext();
944-
if (searchExecutionContext != null) {
945-
if (searchExecutionContext.indexMatches("*error*")) {
946-
throw new IllegalArgumentException("I throw because I choose to.");
923+
static void closeShards(InternalTestCluster cluster, String... indices) throws IOException {
924+
final Set<String> indicesToClose = Set.of(indices);
925+
for (String node : cluster.getNodeNames()) {
926+
final IndicesService indicesService = cluster.getInstance(IndicesService.class, node);
927+
for (IndexService indexService : indicesService) {
928+
if (indicesToClose.contains(indexService.getMetadata().getIndex().getName())) {
929+
for (IndexShard indexShard : indexService) {
930+
closeShardNoCheck(indexShard);
931+
}
947932
}
948933
}
949-
return this;
950-
}
951-
952-
@Override
953-
public String getWriteableName() {
954-
return NAME;
955934
}
956935
}
957936

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,17 @@ private static boolean canMatchShard(
249249
QueryBuilder indexFilter,
250250
long nowInMillis,
251251
SearchExecutionContext searchExecutionContext
252-
) throws IOException {
252+
) {
253253
assert alwaysMatches(indexFilter) == false : "should not be called for always matching [" + indexFilter + "]";
254254
assert nowInMillis != 0L;
255255
ShardSearchRequest searchRequest = new ShardSearchRequest(shardId, nowInMillis, AliasFilter.EMPTY);
256256
searchRequest.source(new SearchSourceBuilder().query(indexFilter));
257-
return SearchService.queryStillMatchesAfterRewrite(searchRequest, searchExecutionContext);
257+
try {
258+
return SearchService.queryStillMatchesAfterRewrite(searchRequest, searchExecutionContext);
259+
} catch (Exception e) {
260+
// treat as if shard is still a potential match
261+
return true;
262+
}
258263
}
259264

260265
private static boolean alwaysMatches(QueryBuilder indexFilter) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ public Set<NodeFeature> getTestFeatures() {
8484
SEMANTIC_TEXT_HIGHLIGHTING_FLAT,
8585
SEMANTIC_TEXT_SPARSE_VECTOR_INDEX_OPTIONS,
8686
SEMANTIC_TEXT_FIELDS_CHUNKS_FORMAT,
87-
SemanticQueryBuilder.SEMANTIC_QUERY_MULTIPLE_INFERENCE_IDS
87+
SemanticQueryBuilder.SEMANTIC_QUERY_MULTIPLE_INFERENCE_IDS,
88+
SemanticQueryBuilder.SEMANTIC_QUERY_FILTER_FIELD_CAPS_FIX
8889
)
8990
);
9091
if (RERANK_SNIPPETS.isEnabled()) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public class SemanticQueryBuilder extends AbstractQueryBuilder<SemanticQueryBuil
5656
public static final String NAME = "semantic";
5757

5858
public static final NodeFeature SEMANTIC_QUERY_MULTIPLE_INFERENCE_IDS = new NodeFeature("semantic_query.multiple_inference_ids");
59+
public static final NodeFeature SEMANTIC_QUERY_FILTER_FIELD_CAPS_FIX = new NodeFeature("semantic_query.filter_field_caps_fix");
5960

6061
private static final TransportVersion SEMANTIC_QUERY_MULTIPLE_INFERENCE_IDS_TV = TransportVersion.fromName(
6162
"semantic_query_multiple_inference_ids"

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,33 @@ setup:
548548
- not_exists: fields.dense_field.inference.chunks
549549
- not_exists: fields.dense_field.inference
550550

551+
---
552+
"Field caps with semantic query does not fail":
553+
- requires:
554+
cluster_features: "semantic_query.filter_field_caps_fix"
555+
reason: "fixed bug with semantic query filtering in field_caps (#116106)"
556+
# We need at least one document present to exercise can-match phase
557+
- do:
558+
index:
559+
index: test-index
560+
id: doc_1
561+
body:
562+
sparse_field: "This is a story about a cat and a dog."
563+
refresh: true
564+
565+
- do:
566+
field_caps:
567+
index: test-index
568+
fields: "*"
569+
body:
570+
index_filter:
571+
semantic:
572+
field: "sparse_field"
573+
query: "test"
574+
575+
- match: { indices: [ "test-index" ] }
576+
- match: { fields.sparse_field.text.searchable: true }
577+
551578
---
552579
"Users can set dense vector index options and index documents using those options":
553580
- requires:

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,34 @@ setup:
447447
- not_exists: fields.dense_field.inference.chunks.text
448448
- not_exists: fields.dense_field.inference.chunks
449449
- not_exists: fields.dense_field.inference
450+
451+
---
452+
"Field caps with semantic query does not fail":
453+
- requires:
454+
cluster_features: "semantic_query.filter_field_caps_fix"
455+
reason: "fixed bug with semantic query filtering in field_caps (#116106)"
456+
# We need at least one document present to exercise can-match phase
457+
- do:
458+
index:
459+
index: test-index
460+
id: doc_1
461+
body:
462+
sparse_field: "This is a story about a cat and a dog."
463+
refresh: true
464+
465+
- do:
466+
field_caps:
467+
index: test-index
468+
fields: "*"
469+
body:
470+
index_filter:
471+
semantic:
472+
field: "sparse_field"
473+
query: "test"
474+
475+
- match: { indices: [ "test-index" ] }
476+
- match: { fields.sparse_field.text.searchable: true }
477+
450478
---
451479
"Users can set dense vector index options and index documents using those options":
452480
- requires:

0 commit comments

Comments
 (0)