Skip to content

Commit 599585b

Browse files
authored
ES clear: delete indices themselves, not all the docs (#15449)
1 parent ddd9321 commit 599585b

File tree

8 files changed

+435
-29
lines changed

8 files changed

+435
-29
lines changed

metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/ElasticSearchGraphService.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,22 @@ public List<ReindexConfig> buildReindexConfigs(
334334

335335
@Override
336336
public void clear() {
337-
esBulkProcessor.deleteByQuery(
338-
QueryBuilders.matchAllQuery(), true, indexConvention.getIndexName(INDEX_NAME));
337+
// Instead of deleting all documents (inefficient), delete and recreate the index
338+
String indexName = indexConvention.getIndexName(INDEX_NAME);
339+
try {
340+
// Build a config with the correct target mappings for recreation
341+
ReindexConfig config =
342+
indexBuilder.buildReindexState(
343+
indexName, GraphRelationshipMappingsBuilder.getMappings(), Collections.emptyMap());
344+
345+
// Use clearIndex which handles deletion and recreation
346+
indexBuilder.clearIndex(indexName, config);
347+
348+
log.info("Cleared index {} by deleting and recreating it", indexName);
349+
} catch (IOException e) {
350+
log.error("Failed to clear index {}", indexName, e);
351+
throw new RuntimeException("Failed to clear index: " + indexName, e);
352+
}
339353
}
340354

341355
@Override

metadata-io/src/main/java/com/linkedin/metadata/graph/elastic/GraphRelationshipMappingsBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public static Map<String, Object> getMappings() {
2222
mappings.put(EDGE_FIELD_VIA, getMappingsForKeyword());
2323
mappings.put(EDGE_FIELD_LIFECYCLE_OWNER_STATUS, getMappingsForBoolean());
2424
mappings.put(EDGE_FIELD_VIA_STATUS, getMappingsForBoolean());
25+
// Timestamp and actor fields used for lineage filtering and sorting
26+
mappings.put("createdOn", getMappingsForLong());
27+
mappings.put("createdActor", getMappingsForKeyword());
28+
mappings.put("updatedOn", getMappingsForLong());
29+
mappings.put("updatedActor", getMappingsForKeyword());
2530
return ImmutableMap.of("properties", mappings);
2631
}
2732

@@ -33,6 +38,10 @@ private static Map<String, Object> getMappingsForBoolean() {
3338
return ImmutableMap.<String, Object>builder().put("type", "boolean").build();
3439
}
3540

41+
private static Map<String, Object> getMappingsForLong() {
42+
return ImmutableMap.<String, Object>builder().put("type", "long").build();
43+
}
44+
3645
private static Map<String, Object> getMappingsForEntity() {
3746

3847
Map<String, Object> mappings =

metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/ElasticSearchService.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,28 @@ public List<ReindexConfig> buildReindexConfigsWithNewStructProp(
107107

108108
@Override
109109
public void clear(@Nonnull OperationContext opContext) {
110-
esWriteDAO.clear(opContext);
110+
Set<String> deletedIndexNames = esWriteDAO.clear(opContext);
111+
112+
// Recreate the indices that were deleted
113+
if (!deletedIndexNames.isEmpty()) {
114+
try {
115+
List<ReindexConfig> allConfigs =
116+
indexBuilder.buildReindexConfigs(
117+
opContext, settingsBuilder, mappingsBuilder, Collections.emptySet());
118+
119+
// Filter to only recreate indices that were deleted
120+
for (ReindexConfig config : allConfigs) {
121+
if (deletedIndexNames.contains(config.name())) {
122+
indexBuilder.buildIndex(config);
123+
log.info("Recreated index {} after clearing", config.name());
124+
}
125+
}
126+
log.info("Recreated {} indices after clearing", deletedIndexNames.size());
127+
} catch (IOException e) {
128+
log.error("Failed to recreate indices after clearing", e);
129+
throw new RuntimeException("Failed to recreate indices after clearing", e);
130+
}
131+
}
111132
}
112133

113134
@Override

metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/ESIndexBuilder.java

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.opensearch.action.search.SearchRequest;
6161
import org.opensearch.action.search.SearchResponse;
6262
import org.opensearch.client.*;
63+
import org.opensearch.client.GetAliasesResponse;
6364
import org.opensearch.client.core.CountRequest;
6465
import org.opensearch.client.core.CountResponse;
6566
import org.opensearch.client.indices.CreateIndexRequest;
@@ -1231,15 +1232,85 @@ public long getCount(@Nonnull String indexName) throws IOException {
12311232
.getCount();
12321233
}
12331234

1235+
/**
1236+
* Check if an index exists.
1237+
*
1238+
* @param indexName The name of the index to check
1239+
* @return true if the index exists, false otherwise
1240+
* @throws IOException If there's an error communicating with Elasticsearch
1241+
*/
1242+
public boolean indexExists(@Nonnull String indexName) throws IOException {
1243+
return searchClient.indexExists(new GetIndexRequest(indexName), RequestOptions.DEFAULT);
1244+
}
1245+
1246+
/**
1247+
* Refresh an index to make all operations performed since the last refresh available for search.
1248+
*
1249+
* @param indexName The name of the index to refresh
1250+
* @throws IOException If there's an error communicating with Elasticsearch
1251+
*/
1252+
public void refreshIndex(@Nonnull String indexName) throws IOException {
1253+
searchClient.refreshIndex(
1254+
new org.opensearch.action.admin.indices.refresh.RefreshRequest(indexName),
1255+
RequestOptions.DEFAULT);
1256+
}
1257+
1258+
/**
1259+
* Delete an index. Handles both aliases and concrete indices.
1260+
*
1261+
* @param indexName The name of the index or alias to delete
1262+
* @throws IOException If there's an error communicating with Elasticsearch
1263+
*/
1264+
public void deleteIndex(@Nonnull String indexName) throws IOException {
1265+
IndexDeletionUtils.IndexResolutionResult resolution =
1266+
IndexDeletionUtils.resolveIndexForDeletion(searchClient, indexName);
1267+
if (resolution == null) {
1268+
log.debug("Index {} does not exist, nothing to delete", indexName);
1269+
return;
1270+
}
1271+
1272+
for (String concreteIndex : resolution.indicesToDelete()) {
1273+
try {
1274+
deleteActionWithRetry(searchClient, concreteIndex);
1275+
} catch (Exception e) {
1276+
throw new IOException("Failed to delete index: " + concreteIndex, e);
1277+
}
1278+
}
1279+
}
1280+
12341281
private void createIndex(String indexName, ReindexConfig state) throws IOException {
12351282
log.info("Index {} does not exist. Creating", indexName);
1283+
Map<String, Object> mappings = state.targetMappings();
1284+
Map<String, Object> settings = state.targetSettings();
1285+
log.info("Creating index {} with targetMappings: {}", indexName, mappings);
1286+
log.info("Creating index {} with targetSettings: {}", indexName, settings);
1287+
12361288
CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
1237-
createIndexRequest.mapping(state.targetMappings());
1238-
createIndexRequest.settings(state.targetSettings());
1289+
createIndexRequest.mapping(mappings);
1290+
createIndexRequest.settings(settings);
12391291
searchClient.createIndex(createIndexRequest, RequestOptions.DEFAULT);
12401292
log.info("Created index {}", indexName);
12411293
}
12421294

1295+
/**
1296+
* Efficiently clear an index by deleting it and recreating it with the same configuration. This
1297+
* is much more efficient than deleting all documents using deleteByQuery.
1298+
*
1299+
* @param indexName The name of the index to clear (can be an alias or concrete index)
1300+
* @param config The ReindexConfig containing mappings and settings for the index
1301+
* @throws IOException If the deletion or creation fails
1302+
*/
1303+
public void clearIndex(String indexName, ReindexConfig config) throws IOException {
1304+
deleteIndex(indexName);
1305+
log.info("Recreating index {} after clearing", indexName);
1306+
createIndex(indexName, config);
1307+
if (!indexExists(indexName)) {
1308+
throw new IOException("Index " + indexName + " was not successfully created after clearing!");
1309+
}
1310+
refreshIndex(indexName);
1311+
log.info("Successfully cleared and recreated index {}", indexName);
1312+
}
1313+
12431314
public static void cleanOrphanedIndices(
12441315
SearchClientShim<?> searchClient,
12451316
ElasticSearchConfiguration esConfig,
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package com.linkedin.metadata.search.elasticsearch.indexbuilder;
2+
3+
import com.linkedin.metadata.utils.elasticsearch.SearchClientShim;
4+
import java.io.IOException;
5+
import java.util.Collection;
6+
import java.util.List;
7+
import java.util.Set;
8+
import javax.annotation.Nonnull;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.opensearch.action.admin.indices.alias.get.GetAliasesRequest;
11+
import org.opensearch.action.admin.indices.delete.DeleteIndexRequest;
12+
import org.opensearch.client.GetAliasesResponse;
13+
import org.opensearch.client.RequestOptions;
14+
import org.opensearch.client.indices.GetIndexRequest;
15+
import org.opensearch.cluster.metadata.AliasMetadata;
16+
import org.opensearch.common.unit.TimeValue;
17+
18+
/**
19+
* Utility class for index deletion operations shared across ESWriteDAO and ESIndexBuilder. Provides
20+
* common logic for handling both aliases and concrete indices during deletion.
21+
*/
22+
@Slf4j
23+
public class IndexDeletionUtils {
24+
25+
private IndexDeletionUtils() {}
26+
27+
/** Result of resolving an index name to its concrete indices for deletion. */
28+
public static class IndexResolutionResult {
29+
private final Collection<String> indicesToDelete;
30+
private final String nameToTrack;
31+
32+
public IndexResolutionResult(Collection<String> indicesToDelete, String nameToTrack) {
33+
this.indicesToDelete = indicesToDelete;
34+
this.nameToTrack = nameToTrack;
35+
}
36+
37+
/** The concrete index names to delete */
38+
public Collection<String> indicesToDelete() {
39+
return indicesToDelete;
40+
}
41+
42+
/**
43+
* The name to track for recreation (alias name if it was an alias, otherwise the concrete index
44+
* name)
45+
*/
46+
public String nameToTrack() {
47+
return nameToTrack;
48+
}
49+
}
50+
51+
/**
52+
* Resolves an index name (which may be an alias or concrete index) to the concrete indices that
53+
* should be deleted, and determines what name to track for recreation.
54+
*
55+
* @param searchClient The search client to use for API calls
56+
* @param indexName The index name to resolve (may be an alias or concrete index)
57+
* @return IndexResolutionResult containing the concrete indices to delete and name to track, or
58+
* null if the index doesn't exist
59+
* @throws IOException If there's an error communicating with Elasticsearch
60+
*/
61+
public static IndexResolutionResult resolveIndexForDeletion(
62+
@Nonnull SearchClientShim<?> searchClient, @Nonnull String indexName) throws IOException {
63+
64+
// Check if it's an alias
65+
GetAliasesRequest getAliasesRequest = new GetAliasesRequest(indexName);
66+
GetAliasesResponse aliasesResponse;
67+
try {
68+
aliasesResponse = searchClient.getIndexAliases(getAliasesRequest, RequestOptions.DEFAULT);
69+
} catch (IOException | RuntimeException e) {
70+
// If getIndexAliases throws, check if it's because index/alias doesn't exist
71+
if (e.getMessage() != null && e.getMessage().contains("index_not_found_exception")) {
72+
// Check if it's an actual index
73+
boolean indexExists =
74+
searchClient.indexExists(new GetIndexRequest(indexName), RequestOptions.DEFAULT);
75+
if (!indexExists) {
76+
log.debug("Index {} does not exist, skipping", indexName);
77+
return null;
78+
}
79+
// Index exists but getIndexAliases failed, treat as concrete index (not an alias)
80+
aliasesResponse = null;
81+
} else {
82+
throw new IOException("Failed to get index aliases for " + indexName, e);
83+
}
84+
}
85+
86+
Collection<String> indicesToDelete;
87+
String nameToTrack;
88+
89+
if (aliasesResponse == null || aliasesResponse.getAliases().isEmpty()) {
90+
// Not an alias, must be a concrete index - verify it exists
91+
boolean indexExists =
92+
searchClient.indexExists(new GetIndexRequest(indexName), RequestOptions.DEFAULT);
93+
if (!indexExists) {
94+
log.debug("Index {} does not exist, skipping", indexName);
95+
return null;
96+
}
97+
98+
// Check if this concrete index has any aliases pointing to it
99+
// If so, track the alias name for recreation, not the concrete index name
100+
GetAliasesRequest aliasesForIndexRequest = new GetAliasesRequest();
101+
aliasesForIndexRequest.indices(indexName);
102+
GetAliasesResponse aliasesForIndex =
103+
searchClient.getIndexAliases(aliasesForIndexRequest, RequestOptions.DEFAULT);
104+
105+
nameToTrack = indexName;
106+
if (!aliasesForIndex.getAliases().isEmpty()
107+
&& aliasesForIndex.getAliases().containsKey(indexName)) {
108+
// Get the alias names that point to this concrete index
109+
Set<AliasMetadata> aliases = aliasesForIndex.getAliases().get(indexName);
110+
if (aliases != null && !aliases.isEmpty()) {
111+
// Use the first alias name (typically there's only one)
112+
nameToTrack = aliases.iterator().next().alias();
113+
log.info(
114+
"Concrete index {} has alias {}, tracking alias for recreation",
115+
indexName,
116+
nameToTrack);
117+
}
118+
}
119+
120+
indicesToDelete = List.of(indexName);
121+
log.info("Resolved concrete index {} for deletion, tracking as {}", indexName, nameToTrack);
122+
} else {
123+
// It's an alias, delete the concrete indices behind it
124+
indicesToDelete = aliasesResponse.getAliases().keySet();
125+
nameToTrack = indexName;
126+
log.info("Resolved alias {} to concrete indices {} for deletion", indexName, indicesToDelete);
127+
}
128+
129+
return new IndexResolutionResult(indicesToDelete, nameToTrack);
130+
}
131+
132+
/**
133+
* Deletes a concrete index with a timeout.
134+
*
135+
* @param searchClient The search client to use for API calls
136+
* @param concreteIndexName The concrete index name to delete (not an alias)
137+
* @throws IOException If there's an error deleting the index
138+
*/
139+
public static void deleteConcreteIndex(
140+
@Nonnull SearchClientShim<?> searchClient, @Nonnull String concreteIndexName)
141+
throws IOException {
142+
DeleteIndexRequest deleteRequest = new DeleteIndexRequest(concreteIndexName);
143+
deleteRequest.timeout(TimeValue.timeValueSeconds(30));
144+
searchClient.deleteIndex(deleteRequest, RequestOptions.DEFAULT);
145+
log.info("Successfully deleted index {}", concreteIndexName);
146+
}
147+
148+
/**
149+
* Deletes an index (handling both aliases and concrete indices).
150+
*
151+
* @param searchClient The search client to use for API calls
152+
* @param indexName The index name to delete (may be an alias or concrete index)
153+
* @return The name to track for recreation (alias name if applicable), or null if index didn't
154+
* exist
155+
* @throws IOException If there's an error deleting the index
156+
*/
157+
public static String deleteIndex(
158+
@Nonnull SearchClientShim<?> searchClient, @Nonnull String indexName) throws IOException {
159+
IndexResolutionResult resolution = resolveIndexForDeletion(searchClient, indexName);
160+
if (resolution == null) {
161+
return null;
162+
}
163+
164+
for (String concreteIndex : resolution.indicesToDelete()) {
165+
deleteConcreteIndex(searchClient, concreteIndex);
166+
}
167+
168+
return resolution.nameToTrack();
169+
}
170+
}

0 commit comments

Comments
 (0)