From df01f8124dd26cb545fd1f797fd48bf492a306e3 Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Mon, 16 Dec 2024 12:35:54 +0100 Subject: [PATCH 001/119] Disable test on release builds (#118752) Fix https://github.com/elastic/elasticsearch/issues/118707 I plan to manually backport this to un-mute the test on 8.x. --- .../xpack/esql/optimizer/PhysicalPlanOptimizerTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index dc3ae0a3388cb..d0c7a1cd61010 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -2331,6 +2331,8 @@ public void testVerifierOnMissingReferences() { } public void testVerifierOnMissingReferencesWithBinaryPlans() throws Exception { + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + // Do not assert serialization: // This will have a LookupJoinExec, which is not serializable because it doesn't leave the coordinator. var plan = physicalPlan(""" From cf7edbbc0f08a0584e5690c7acaa33993078d441 Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Mon, 16 Dec 2024 12:40:35 +0100 Subject: [PATCH 002/119] Properly skip datasets with lookup indices (#118753) Forward-port of https://github.com/elastic/elasticsearch/pull/118682 to main This mostly just minimizes the diff; LOOKUP JOIN is enabled on main, so we don't need to skip the datasets here. --- .../xpack/esql/CsvTestsDataLoader.java | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java index 3b656ded94dd7..abfe90f80e372 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java @@ -41,7 +41,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import static org.elasticsearch.common.logging.LoggerMessageFormat.format; import static org.elasticsearch.xpack.esql.CsvTestUtils.COMMA_ESCAPING_REGEX; @@ -260,11 +259,22 @@ public static void main(String[] args) throws IOException { public static Set availableDatasetsForEs(RestClient client, boolean supportsIndexModeLookup) throws IOException { boolean inferenceEnabled = clusterHasInferenceEndpoint(client); - return CSV_DATASET_MAP.values() - .stream() - .filter(d -> d.requiresInferenceEndpoint == false || inferenceEnabled) - .filter(d -> supportsIndexModeLookup || d.indexName.endsWith("_lookup") == false) // TODO: use actual index settings - .collect(Collectors.toCollection(HashSet::new)); + Set testDataSets = new HashSet<>(); + + for (TestsDataset dataset : CSV_DATASET_MAP.values()) { + if ((inferenceEnabled || dataset.requiresInferenceEndpoint == false) + && (supportsIndexModeLookup || isLookupDataset(dataset) == false)) { + testDataSets.add(dataset); + } + } + + return testDataSets; + } + + public static boolean isLookupDataset(TestsDataset dataset) throws IOException { + Settings settings = dataset.readSettingsFile(); + String mode = settings.get("index.mode"); + return (mode != null && mode.equalsIgnoreCase("lookup")); } public static void loadDataSetIntoEs(RestClient client, boolean supportsIndexModeLookup) throws IOException { @@ -354,13 +364,8 @@ private static void load(RestClient client, TestsDataset dataset, Logger logger, if (data == null) { throw new IllegalArgumentException("Cannot find resource " + dataName); } - Settings indexSettings = Settings.EMPTY; - final String settingName = dataset.settingFileName != null ? "/" + dataset.settingFileName : null; - if (settingName != null) { - indexSettings = Settings.builder() - .loadFromStream(settingName, CsvTestsDataLoader.class.getResourceAsStream(settingName), false) - .build(); - } + + Settings indexSettings = dataset.readSettingsFile(); indexCreator.createIndex(client, dataset.indexName, readMappingFile(mapping, dataset.typeMapping), indexSettings); loadCsvData(client, dataset.indexName, data, dataset.allowSubFields, logger); } @@ -669,6 +674,18 @@ public TestsDataset withTypeMapping(Map typeMapping) { public TestsDataset withInferenceEndpoint(boolean needsInference) { return new TestsDataset(indexName, mappingFileName, dataFileName, settingFileName, allowSubFields, typeMapping, needsInference); } + + private Settings readSettingsFile() throws IOException { + Settings indexSettings = Settings.EMPTY; + final String settingName = settingFileName != null ? "/" + settingFileName : null; + if (settingName != null) { + indexSettings = Settings.builder() + .loadFromStream(settingName, CsvTestsDataLoader.class.getResourceAsStream(settingName), false) + .build(); + } + + return indexSettings; + } } public record EnrichConfig(String policyName, String policyFileName) {} From b461baf1965e7da69c92bca8c97f50ef62bb8c53 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Mon, 16 Dec 2024 12:46:36 +0100 Subject: [PATCH 003/119] Skip translog creation and Lucene commits when recovering searchable snapshot shards (#118606) In order to leverage Lucene N-2 version support for searchable snapshots, we'd like to avoid executing Lucene commits during searchable snapshots shards recovery. This is because Lucene commits require to open an IndexWriter, something that Lucene does not support for N-2 versions. Today when searchable snapshot shards are recovering they create a translog on disk as well as a Lucene commit: - the translog is created as an empty translog with a new UUID and an initial global checkpoint value that is the same as the LOCAL_CHECKPOINT_KEY stored in the last Lucene commit data from the snapshot. - a Lucene commit is executed to associate the translog with the Lucene index by storing new translog UUID in the Lucene commit data. - later during recovery, the replication tracker is initialized with a global checkpoint value equals to the LOCAL_CHECKPOINT_KEY stored in the Lucene commit. We can skip the creation of the translog because searchable snapshot shard do not need one, and it's only use to store the local checkpoint locally to be read later during recovery. If we don't have a translog then we don't need to associate it with the Lucene index, so we can skip the commit too. This change introduce an hasTranslog method that is used to know when it is safe to NOT create a translog, in which case the global checkpoint is read from the last Lucene commit during primary shard recovery from snapshot, peer-recovery and recovery from existing store. In case an existing translog exist on disk, it will be cleaned up. They are also few discoveries around some assertions introduced with snapshot based recoveries, as well as a cached estimation of the size of directories that was refreshed due to Lucene commit but now requires to be "marked as stale". --- .../index/engine/NoOpEngine.java | 2 +- .../index/engine/ReadOnlyEngine.java | 3 +- .../elasticsearch/index/shard/IndexShard.java | 43 ++++++++++++--- .../index/shard/StoreRecovery.java | 53 ++++++++++++------- .../index/store/ByteSizeCachingDirectory.java | 27 ++++++++-- .../org/elasticsearch/index/store/Store.java | 2 + .../index/translog/Translog.java | 4 ++ .../index/translog/TranslogConfig.java | 9 ++++ .../recovery/PeerRecoveryTargetService.java | 15 ++---- .../indices/recovery/RecoveryTarget.java | 39 ++++++++++---- .../xpack/lucene/bwc/OldLuceneVersions.java | 8 +++ .../xpack/lucene/bwc/OldSegmentInfos.java | 4 ++ .../BaseSearchableSnapshotsIntegTestCase.java | 19 ++++--- .../FrozenSearchableSnapshotsIntegTests.java | 10 +--- .../SearchableSnapshotIndexEventListener.java | 7 +++ 15 files changed, 175 insertions(+), 70 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/NoOpEngine.java b/server/src/main/java/org/elasticsearch/index/engine/NoOpEngine.java index 8dee39f7050cb..49e0ae0587085 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/NoOpEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/NoOpEngine.java @@ -48,7 +48,7 @@ public final class NoOpEngine extends ReadOnlyEngine { public NoOpEngine(EngineConfig config) { this( config, - config.isPromotableToPrimary() ? null : new TranslogStats(0, 0, 0, 0, 0), + config.isPromotableToPrimary() && config.getTranslogConfig().hasTranslog() ? null : new TranslogStats(0, 0, 0, 0, 0), config.isPromotableToPrimary() ? null : new SeqNoStats( diff --git a/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java index c1d11223fa55e..1d032c1f400ef 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java @@ -98,7 +98,7 @@ public class ReadOnlyEngine extends Engine { public ReadOnlyEngine( EngineConfig config, SeqNoStats seqNoStats, - TranslogStats translogStats, + @Nullable TranslogStats translogStats, boolean obtainLock, Function readerWrapperFunction, boolean requireCompleteHistory, @@ -251,6 +251,7 @@ private static SeqNoStats buildSeqNoStats(EngineConfig config, SegmentInfos info } private static TranslogStats translogStats(final EngineConfig config, final SegmentInfos infos) throws IOException { + assert config.getTranslogConfig().hasTranslog(); final String translogUuid = infos.getUserData().get(Translog.TRANSLOG_UUID_KEY); if (translogUuid == null) { throw new IllegalStateException("commit doesn't contain translog unique id"); diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index a76feff84e61b..966764d2797c9 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -1487,6 +1487,27 @@ public void flush(FlushRequest request, ActionListener listener) { }); } + /** + * @return true the shard has a translog. + */ + public boolean hasTranslog() { + return translogConfig.hasTranslog(); + } + + /** + * Reads the global checkpoint from the translog checkpoint file if the shard has a translog. Otherwise, reads the local checkpoint from + * the provided commit user data. + * + * @return the global checkpoint to use for recovery + * @throws IOException + */ + public long readGlobalCheckpointForRecovery(Map commitUserData) throws IOException { + if (hasTranslog()) { + return Translog.readGlobalCheckpoint(translogConfig.getTranslogPath(), commitUserData.get(Translog.TRANSLOG_UUID_KEY)); + } + return Long.parseLong(commitUserData.get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); + } + /** * checks and removes translog files that no longer need to be retained. See * {@link org.elasticsearch.index.translog.TranslogDeletionPolicy} for details @@ -1859,8 +1880,7 @@ public void recoverLocallyUpToGlobalCheckpoint(ActionListener recoveryStar } assert routingEntry().recoverySource().getType() == RecoverySource.Type.PEER : "not a peer recovery [" + routingEntry() + "]"; try { - final var translogUUID = store.readLastCommittedSegmentsInfo().getUserData().get(Translog.TRANSLOG_UUID_KEY); - final var globalCheckpoint = Translog.readGlobalCheckpoint(translogConfig.getTranslogPath(), translogUUID); + final var globalCheckpoint = readGlobalCheckpointForRecovery(store.readLastCommittedSegmentsInfo().getUserData()); final var safeCommit = store.findSafeIndexCommit(globalCheckpoint); ActionListener.run(recoveryStartingSeqNoListener.delegateResponse((l, e) -> { logger.debug(() -> format("failed to recover shard locally up to global checkpoint %s", globalCheckpoint), e); @@ -2084,8 +2104,7 @@ private void loadGlobalCheckpointToReplicationTracker() throws IOException { // we have to set it before we open an engine and recover from the translog because // acquiring a snapshot from the translog causes a sync which causes the global checkpoint to be pulled in, // and an engine can be forced to close in ctor which also causes the global checkpoint to be pulled in. - final String translogUUID = store.readLastCommittedSegmentsInfo().getUserData().get(Translog.TRANSLOG_UUID_KEY); - final long globalCheckpoint = Translog.readGlobalCheckpoint(translogConfig.getTranslogPath(), translogUUID); + final long globalCheckpoint = readGlobalCheckpointForRecovery(store.readLastCommittedSegmentsInfo().getUserData()); replicationTracker.updateGlobalCheckpointOnReplica(globalCheckpoint, "read from translog checkpoint"); } else { replicationTracker.updateGlobalCheckpointOnReplica(globalCheckPointIfUnpromotable, "from CleanFilesRequest"); @@ -2162,7 +2181,7 @@ private void innerOpenEngineAndTranslog(LongSupplier globalCheckpointSupplier) t // time elapses after the engine is created above (pulling the config settings) until we set the engine reference, during // which settings changes could possibly have happened, so here we forcefully push any config changes to the new engine. onSettingsChanged(); - assert assertSequenceNumbersInCommit(); + assert assertLastestCommitUserData(); recoveryState.validateCurrentStage(RecoveryState.Stage.TRANSLOG); checkAndCallWaitForEngineOrClosedShardListeners(); } @@ -2183,9 +2202,13 @@ private Engine createEngine(EngineConfig config) { } } - private boolean assertSequenceNumbersInCommit() throws IOException { + /** + * Asserts that the latest Lucene commit contains expected information about sequence numbers or ES version. + */ + private boolean assertLastestCommitUserData() throws IOException { final SegmentInfos segmentCommitInfos = SegmentInfos.readLatestCommit(store.directory()); final Map userData = segmentCommitInfos.getUserData(); + // Ensure sequence numbers are present in commit data assert userData.containsKey(SequenceNumbers.LOCAL_CHECKPOINT_KEY) : "commit point doesn't contains a local checkpoint"; assert userData.containsKey(SequenceNumbers.MAX_SEQ_NO) : "commit point doesn't contains a maximum sequence number"; assert userData.containsKey(Engine.HISTORY_UUID_KEY) : "commit point doesn't contains a history uuid"; @@ -2195,10 +2218,16 @@ private boolean assertSequenceNumbersInCommit() throws IOException { + "] is different than engine [" + getHistoryUUID() + "]"; + assert userData.containsKey(Engine.MAX_UNSAFE_AUTO_ID_TIMESTAMP_COMMIT_ID) : "opening index which was created post 5.5.0 but " + Engine.MAX_UNSAFE_AUTO_ID_TIMESTAMP_COMMIT_ID + " is not found in commit"; + + // From 7.16.0, the ES version is included in the Lucene commit user data as well as in the snapshot metadata in the repository. + // This is used during primary failover to detect if the latest snapshot can be used to recover the new primary, because the failed + // primary may have created new segments in a more recent Lucene version, that may have been later snapshotted, meaning that the + // snapshotted files cannot be recovered on a node with a less recent Lucene version. Note that for versions <= 7.15 this assertion + // relies in the previous minor having a different lucene version. final org.apache.lucene.util.Version commitLuceneVersion = segmentCommitInfos.getCommitLuceneVersion(); - // This relies in the previous minor having another lucene version assert commitLuceneVersion.onOrAfter(RecoverySettings.SEQ_NO_SNAPSHOT_RECOVERIES_SUPPORTED_VERSION.luceneVersion()) == false || userData.containsKey(Engine.ES_VERSION) && Engine.readIndexVersion(userData.get(Engine.ES_VERSION)).onOrBefore(IndexVersion.current()) diff --git a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java index 42f62cf86545b..06f9b3e6c8943 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java +++ b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java @@ -315,7 +315,7 @@ void recoverFromRepository(final IndexShard indexShard, Repository repository, A RecoverySource.Type recoveryType = indexShard.recoveryState().getRecoverySource().getType(); assert recoveryType == RecoverySource.Type.SNAPSHOT : "expected snapshot recovery type: " + recoveryType; SnapshotRecoverySource recoverySource = (SnapshotRecoverySource) indexShard.recoveryState().getRecoverySource(); - restore(indexShard, repository, recoverySource, recoveryListener(indexShard, listener).map(ignored -> true)); + recoverFromRepository(indexShard, repository, recoverySource, recoveryListener(indexShard, listener).map(ignored -> true)); } else { listener.onResponse(false); } @@ -459,7 +459,7 @@ private void internalRecoverFromStore(IndexShard indexShard, ActionListener outerListener ) { + assert indexShard.shardRouting.primary() : "only primary shards can recover from snapshot"; logger.debug("restoring from {} ...", indexShard.recoveryState().getRecoverySource()); record ShardAndIndexIds(IndexId indexId, ShardId shardId) {} @@ -538,13 +539,13 @@ record ShardAndIndexIds(IndexId indexId, ShardId shardId) {} .newForked(indexShard::preRecovery) .andThen(shardAndIndexIdsListener -> { - final RecoveryState.Translog translogState = indexShard.recoveryState().getTranslog(); if (restoreSource == null) { throw new IndexShardRestoreFailedException(shardId, "empty restore source"); } if (logger.isTraceEnabled()) { logger.trace("[{}] restoring shard [{}]", restoreSource.snapshot(), shardId); } + final RecoveryState.Translog translogState = indexShard.recoveryState().getTranslog(); translogState.totalOperations(0); translogState.totalOperationsOnStart(0); indexShard.prepareForIndexRecovery(); @@ -588,9 +589,7 @@ record ShardAndIndexIds(IndexId indexId, ShardId shardId) {} .andThen(l -> { indexShard.getIndexEventListener().afterFilesRestoredFromRepository(indexShard); - final Store store = indexShard.store(); - bootstrap(indexShard, store); - assert indexShard.shardRouting.primary() : "only primary shards can recover from store"; + bootstrap(indexShard); writeEmptyRetentionLeasesFile(indexShard); indexShard.openEngineAndRecoverFromTranslog(l); }) @@ -610,19 +609,37 @@ record ShardAndIndexIds(IndexId indexId, ShardId shardId) {} })); } + /** + * @deprecated use {@link #bootstrap(IndexShard)} instead + */ + @Deprecated(forRemoval = true) public static void bootstrap(final IndexShard indexShard, final Store store) throws IOException { - if (indexShard.indexSettings.getIndexMetadata().isSearchableSnapshot() == false) { - // not bootstrapping new history for searchable snapshots (which are read-only) allows sequence-number based peer recoveries + assert indexShard.store() == store; + bootstrap(indexShard); + } + + private static void bootstrap(final IndexShard indexShard) throws IOException { + assert indexShard.routingEntry().primary(); + final var store = indexShard.store(); + store.incRef(); + try { + final var translogLocation = indexShard.shardPath().resolveTranslog(); + if (indexShard.hasTranslog() == false) { + Translog.deleteAll(translogLocation); + return; + } store.bootstrapNewHistory(); + final SegmentInfos segmentInfos = store.readLastCommittedSegmentsInfo(); + final long localCheckpoint = Long.parseLong(segmentInfos.userData.get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); + final String translogUUID = Translog.createEmptyTranslog( + translogLocation, + localCheckpoint, + indexShard.shardId(), + indexShard.getPendingPrimaryTerm() + ); + store.associateIndexWithNewTranslog(translogUUID); + } finally { + store.decRef(); } - final SegmentInfos segmentInfos = store.readLastCommittedSegmentsInfo(); - final long localCheckpoint = Long.parseLong(segmentInfos.userData.get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); - final String translogUUID = Translog.createEmptyTranslog( - indexShard.shardPath().resolveTranslog(), - localCheckpoint, - indexShard.shardId(), - indexShard.getPendingPrimaryTerm() - ); - store.associateIndexWithNewTranslog(translogUUID); } } diff --git a/server/src/main/java/org/elasticsearch/index/store/ByteSizeCachingDirectory.java b/server/src/main/java/org/elasticsearch/index/store/ByteSizeCachingDirectory.java index 166f0eadc62b4..033859bd62128 100644 --- a/server/src/main/java/org/elasticsearch/index/store/ByteSizeCachingDirectory.java +++ b/server/src/main/java/org/elasticsearch/index/store/ByteSizeCachingDirectory.java @@ -10,6 +10,7 @@ package org.elasticsearch.index.store; import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FilterDirectory; import org.apache.lucene.store.IOContext; import org.apache.lucene.store.IndexOutput; import org.elasticsearch.common.lucene.store.FilterIndexOutput; @@ -19,7 +20,7 @@ import java.io.IOException; import java.io.UncheckedIOException; -final class ByteSizeCachingDirectory extends ByteSizeDirectory { +public final class ByteSizeCachingDirectory extends ByteSizeDirectory { private static class SizeAndModCount { final long size; @@ -174,9 +175,29 @@ public void deleteFile(String name) throws IOException { try { super.deleteFile(name); } finally { - synchronized (this) { - modCount++; + markEstimatedSizeAsStale(); + } + } + + /** + * Mark the cached size as stale so that it is guaranteed to be refreshed the next time. + */ + public void markEstimatedSizeAsStale() { + synchronized (this) { + modCount++; + } + } + + public static ByteSizeCachingDirectory unwrapDirectory(Directory dir) { + while (dir != null) { + if (dir instanceof ByteSizeCachingDirectory) { + return (ByteSizeCachingDirectory) dir; + } else if (dir instanceof FilterDirectory) { + dir = ((FilterDirectory) dir).getDelegate(); + } else { + dir = null; } } + return null; } } diff --git a/server/src/main/java/org/elasticsearch/index/store/Store.java b/server/src/main/java/org/elasticsearch/index/store/Store.java index e6b499c07f189..322064f09cf77 100644 --- a/server/src/main/java/org/elasticsearch/index/store/Store.java +++ b/server/src/main/java/org/elasticsearch/index/store/Store.java @@ -1449,6 +1449,7 @@ public void bootstrapNewHistory() throws IOException { * @see SequenceNumbers#MAX_SEQ_NO */ public void bootstrapNewHistory(long localCheckpoint, long maxSeqNo) throws IOException { + assert indexSettings.getIndexMetadata().isSearchableSnapshot() == false; metadataLock.writeLock().lock(); try (IndexWriter writer = newTemporaryAppendingIndexWriter(directory, null)) { final Map map = new HashMap<>(); @@ -1572,6 +1573,7 @@ private IndexWriter newTemporaryEmptyIndexWriter(final Directory dir, final Vers } private IndexWriterConfig newTemporaryIndexWriterConfig() { + assert indexSettings.getIndexMetadata().isSearchableSnapshot() == false; // this config is only used for temporary IndexWriter instances, used to initialize the index or update the commit data, // so we don't want any merges to happen var iwc = indexWriterConfigWithNoMerging(null).setSoftDeletesField(Lucene.SOFT_DELETES_FIELD).setCommitOnClose(false); diff --git a/server/src/main/java/org/elasticsearch/index/translog/Translog.java b/server/src/main/java/org/elasticsearch/index/translog/Translog.java index 2d3e2d8c20256..75f38ed5f7342 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/Translog.java +++ b/server/src/main/java/org/elasticsearch/index/translog/Translog.java @@ -220,6 +220,10 @@ public Translog( } } + public static void deleteAll(Path translogLocation) throws IOException { + IOUtils.rm(translogLocation); + } + /** recover all translog files found on disk */ private ArrayList recoverFromFiles(Checkpoint checkpoint) throws IOException { boolean success = false; diff --git a/server/src/main/java/org/elasticsearch/index/translog/TranslogConfig.java b/server/src/main/java/org/elasticsearch/index/translog/TranslogConfig.java index 66018092089ce..280e319335b12 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/TranslogConfig.java +++ b/server/src/main/java/org/elasticsearch/index/translog/TranslogConfig.java @@ -143,4 +143,13 @@ public OperationListener getOperationListener() { public boolean fsync() { return fsync; } + + /** + * @return {@code true} if the configuration allows the Translog files to exist, {@code false} otherwise. In the case there is no + * translog, the shard is not writeable. + */ + public boolean hasTranslog() { + // Expect no translog files to exist for searchable snapshots + return false == indexSettings.getIndexMetadata().isSearchableSnapshot(); + } } diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetService.java b/server/src/main/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetService.java index c8d31d2060caf..d069717a66ad0 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetService.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetService.java @@ -49,9 +49,7 @@ import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardLongFieldRange; import org.elasticsearch.index.shard.ShardNotFoundException; -import org.elasticsearch.index.shard.StoreRecovery; import org.elasticsearch.index.store.Store; -import org.elasticsearch.index.translog.Translog; import org.elasticsearch.index.translog.TranslogCorruptedException; import org.elasticsearch.indices.recovery.RecoveriesCollection.RecoveryRef; import org.elasticsearch.tasks.Task; @@ -385,15 +383,8 @@ record StartRecoveryRequestToSend(StartRecoveryRequest startRecoveryRequest, Str logger.trace("{} preparing shard for peer recovery", recoveryTarget.shardId()); indexShard.prepareForIndexRecovery(); if (indexShard.indexSettings().getIndexMetadata().isSearchableSnapshot()) { - // for searchable snapshots, peer recovery is treated similarly to recovery from snapshot + // for archives indices mounted as searchable snapshots, we need to call this indexShard.getIndexEventListener().afterFilesRestoredFromRepository(indexShard); - final Store store = indexShard.store(); - store.incRef(); - try { - StoreRecovery.bootstrap(indexShard, store); - } finally { - store.decRef(); - } } indexShard.recoverLocallyUpToGlobalCheckpoint(ActionListener.assertOnce(l)); }) @@ -488,8 +479,8 @@ public static StartRecoveryRequest getStartRecoveryRequest( // Make sure that the current translog is consistent with the Lucene index; otherwise, we have to throw away the Lucene // index. try { - final String expectedTranslogUUID = metadataSnapshot.commitUserData().get(Translog.TRANSLOG_UUID_KEY); - final long globalCheckpoint = Translog.readGlobalCheckpoint(recoveryTarget.translogLocation(), expectedTranslogUUID); + final long globalCheckpoint = recoveryTarget.indexShard() + .readGlobalCheckpointForRecovery(metadataSnapshot.commitUserData()); assert globalCheckpoint + 1 >= startingSeqNo : "invalid startingSeqNo " + startingSeqNo + " >= " + globalCheckpoint; } catch (IOException | TranslogCorruptedException e) { logGlobalCheckpointWarning(logger, startingSeqNo, e); diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java index ea485a411143e..362a62c838e3b 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java @@ -45,7 +45,6 @@ import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Path; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; @@ -516,13 +515,7 @@ public void cleanFiles( try { if (indexShard.routingEntry().isPromotableToPrimary()) { store.cleanupAndVerify("recovery CleanFilesRequestHandler", sourceMetadata); - final String translogUUID = Translog.createEmptyTranslog( - indexShard.shardPath().resolveTranslog(), - globalCheckpoint, - shardId, - indexShard.getPendingPrimaryTerm() - ); - store.associateIndexWithNewTranslog(translogUUID); + bootstrap(indexShard, globalCheckpoint); } else { indexShard.setGlobalCheckpointIfUnpromotable(globalCheckpoint); } @@ -634,7 +627,33 @@ public String getTempNameForFile(String origFile) { return multiFileWriter.getTempNameForFile(origFile); } - Path translogLocation() { - return indexShard().shardPath().resolveTranslog(); + private static void bootstrap(final IndexShard indexShard, long globalCheckpoint) throws IOException { + assert indexShard.routingEntry().isPromotableToPrimary(); + final var store = indexShard.store(); + store.incRef(); + try { + final var translogLocation = indexShard.shardPath().resolveTranslog(); + if (indexShard.hasTranslog() == false) { + if (Assertions.ENABLED) { + if (indexShard.indexSettings().getIndexMetadata().isSearchableSnapshot()) { + long localCheckpoint = Long.parseLong( + store.readLastCommittedSegmentsInfo().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY) + ); + assert localCheckpoint == globalCheckpoint : localCheckpoint + " != " + globalCheckpoint; + } + } + Translog.deleteAll(translogLocation); + return; + } + final String translogUUID = Translog.createEmptyTranslog( + indexShard.shardPath().resolveTranslog(), + globalCheckpoint, + indexShard.shardId(), + indexShard.getPendingPrimaryTerm() + ); + store.associateIndexWithNewTranslog(translogUUID); + } finally { + store.decRef(); + } } } diff --git a/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/OldLuceneVersions.java b/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/OldLuceneVersions.java index 42fe09691d249..e36ae4994c872 100644 --- a/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/OldLuceneVersions.java +++ b/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/OldLuceneVersions.java @@ -27,6 +27,7 @@ import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.ReadOnlyEngine; @@ -34,6 +35,7 @@ import org.elasticsearch.index.shard.IndexEventListener; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.translog.TranslogStats; +import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseUtils; import org.elasticsearch.license.LicensedFeature; @@ -201,6 +203,12 @@ private static SegmentInfos convertToNewerLuceneVersion(OldSegmentInfos oldSegme if (map.containsKey(Engine.MAX_UNSAFE_AUTO_ID_TIMESTAMP_COMMIT_ID) == false) { map.put(Engine.MAX_UNSAFE_AUTO_ID_TIMESTAMP_COMMIT_ID, "-1"); } + if (map.containsKey(Engine.ES_VERSION) == false) { + assert oldSegmentInfos.getLuceneVersion() + .onOrAfter(RecoverySettings.SEQ_NO_SNAPSHOT_RECOVERIES_SUPPORTED_VERSION.luceneVersion()) == false + : oldSegmentInfos.getLuceneVersion() + " should contain the ES_VERSION"; + map.put(Engine.ES_VERSION, IndexVersions.MINIMUM_COMPATIBLE.toString()); + } segmentInfos.setUserData(map, false); for (SegmentCommitInfo infoPerCommit : oldSegmentInfos.asList()) { final SegmentInfo newInfo = BWCCodec.wrap(infoPerCommit.info); diff --git a/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/OldSegmentInfos.java b/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/OldSegmentInfos.java index 18adebb145f98..b2af41653da61 100644 --- a/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/OldSegmentInfos.java +++ b/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/OldSegmentInfos.java @@ -564,6 +564,10 @@ public long getLastGeneration() { return lastGeneration; } + public Version getLuceneVersion() { + return luceneVersion; + } + /** * Prints the given message to the infoStream. Note, this method does not check for null * infoStream. It assumes this check has been performed by the caller, which is recommended to diff --git a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java index 6115bec91ad62..a3ced0bf1b607 100644 --- a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java +++ b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java @@ -74,7 +74,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @ESIntegTestCase.ClusterScope(supportsDedicatedMasters = false, numClientNodes = 0) @@ -241,7 +240,7 @@ protected void checkSoftDeletesNotEagerlyLoaded(String restoredIndexName) { } } - protected void assertShardFolders(String indexName, boolean snapshotDirectory) throws IOException { + protected void assertShardFolders(String indexName, boolean isSearchableSnapshot) throws IOException { final Index restoredIndex = resolveIndex(indexName); final String customDataPath = resolveCustomDataPath(indexName); final ShardId shardId = new ShardId(restoredIndex, 0); @@ -261,16 +260,16 @@ protected void assertShardFolders(String indexName, boolean snapshotDirectory) t translogExists ); assertThat( - snapshotDirectory ? "Index file should not exist" : "Index file should exist", + isSearchableSnapshot ? "Index file should not exist" : "Index file should exist", indexExists, - not(snapshotDirectory) + not(isSearchableSnapshot) ); - assertThat("Translog should exist", translogExists, is(true)); - try (Stream dir = Files.list(shardPath.resolveTranslog())) { - final long translogFiles = dir.filter(path -> path.getFileName().toString().contains("translog")).count(); - if (snapshotDirectory) { - assertThat("There should be 2 translog files for a snapshot directory", translogFiles, equalTo(2L)); - } else { + if (isSearchableSnapshot) { + assertThat("Translog should not exist", translogExists, equalTo(false)); + } else { + assertThat("Translog should exist", translogExists, equalTo(true)); + try (Stream dir = Files.list(shardPath.resolveTranslog())) { + final long translogFiles = dir.filter(path -> path.getFileName().toString().contains("translog")).count(); assertThat( "There should be 2+ translog files for a non-snapshot directory", translogFiles, diff --git a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/FrozenSearchableSnapshotsIntegTests.java b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/FrozenSearchableSnapshotsIntegTests.java index 67d9d7a82acf3..2797202e5f24e 100644 --- a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/FrozenSearchableSnapshotsIntegTests.java +++ b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/FrozenSearchableSnapshotsIntegTests.java @@ -59,7 +59,6 @@ import java.time.ZoneId; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -169,12 +168,9 @@ public void testCreateAndRestorePartialSearchableSnapshot() throws Exception { logger.info("--> restoring partial index [{}] with cache enabled", restoredIndexName); Settings.Builder indexSettingsBuilder = Settings.builder().put(SearchableSnapshots.SNAPSHOT_CACHE_ENABLED_SETTING.getKey(), true); - final List nonCachedExtensions; if (randomBoolean()) { - nonCachedExtensions = randomSubsetOf(Arrays.asList("fdt", "fdx", "nvd", "dvd", "tip", "cfs", "dim")); + var nonCachedExtensions = randomSubsetOf(Arrays.asList("fdt", "fdx", "nvd", "dvd", "tip", "cfs", "dim")); indexSettingsBuilder.putList(SearchableSnapshots.SNAPSHOT_CACHE_EXCLUDED_FILE_TYPES_SETTING.getKey(), nonCachedExtensions); - } else { - nonCachedExtensions = Collections.emptyList(); } if (randomBoolean()) { indexSettingsBuilder.put( @@ -264,8 +260,6 @@ public void testCreateAndRestorePartialSearchableSnapshot() throws Exception { final long originalSize = snapshotShards.get(shardRouting.getId()).getStats().getTotalSize(); totalExpectedSize += originalSize; - // an extra segments_N file is created for bootstrapping new history and associating translog. We can extract the size of this - // extra file but we have to unwrap the in-memory directory first. final Directory unwrappedDir = FilterDirectory.unwrap( internalCluster().getInstance(IndicesService.class, getDiscoveryNodes().resolveNode(shardRouting.currentNodeId()).getName()) .indexServiceSafe(shardRouting.index()) @@ -277,7 +271,7 @@ public void testCreateAndRestorePartialSearchableSnapshot() throws Exception { assertThat(shardRouting.toString(), unwrappedDir, instanceOf(ByteBuffersDirectory.class)); final ByteBuffersDirectory inMemoryDir = (ByteBuffersDirectory) unwrappedDir; - assertThat(inMemoryDir.listAll(), arrayWithSize(1)); + assertThat(inMemoryDir.listAll(), arrayWithSize(0)); assertThat(shardRouting.toString(), store.totalDataSetSizeInBytes(), equalTo(originalSize)); } diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/allocation/SearchableSnapshotIndexEventListener.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/allocation/SearchableSnapshotIndexEventListener.java index cf0306e3e6ef2..4caf932a99807 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/allocation/SearchableSnapshotIndexEventListener.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/allocation/SearchableSnapshotIndexEventListener.java @@ -21,6 +21,7 @@ import org.elasticsearch.index.shard.IndexEventListener; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.index.store.ByteSizeCachingDirectory; import org.elasticsearch.indices.cluster.IndicesClusterStateService.AllocatedIndices.IndexRemovalReason; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshots; @@ -61,6 +62,12 @@ public SearchableSnapshotIndexEventListener( public void beforeIndexShardRecovery(IndexShard indexShard, IndexSettings indexSettings, ActionListener listener) { assert ThreadPool.assertCurrentThreadPool(ThreadPool.Names.GENERIC); ensureSnapshotIsLoaded(indexShard); + var sizeCachingDirectory = ByteSizeCachingDirectory.unwrapDirectory(indexShard.store().directory()); + if (sizeCachingDirectory != null) { + // Marks the cached estimation of the directory size as stale in ByteSizeCachingDirectory since we just loaded the snapshot + // files list into the searchable snapshot directory. + sizeCachingDirectory.markEstimatedSizeAsStale(); + } listener.onResponse(null); } From caf8afcb2d412051e6c19ae9ff223d1a396f8aaf Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Mon, 16 Dec 2024 13:36:18 +0100 Subject: [PATCH 004/119] Render all datatypes in pipetables error output (#118758) Some types (such as ip or time) require custom formatting logic and were not correctly rendered in actual/expected comparison table. This change: * properly formats such data types * add data types to the column headers * removes outer pipes --- .../elasticsearch/xpack/esql/CsvAssert.java | 152 +++++++++++------- 1 file changed, 98 insertions(+), 54 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvAssert.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvAssert.java index 8a4d44a690571..692c385cef216 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvAssert.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvAssert.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.esql; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.compute.data.Page; import org.elasticsearch.logging.Logger; @@ -197,7 +198,13 @@ public static void assertData( for (int row = 0; row < expectedValues.size(); row++) { try { if (row >= actualValues.size()) { - dataFailure("Expected more data but no more entries found after [" + row + "]", dataFailures, expected, actualValues); + dataFailure( + "Expected more data but no more entries found after [" + row + "]", + dataFailures, + expected, + actualValues, + valueTransformer + ); } if (logger != null) { @@ -208,45 +215,17 @@ public static void assertData( var actualRow = actualValues.get(row); for (int column = 0; column < expectedRow.size(); column++) { - var expectedValue = expectedRow.get(column); - var actualValue = actualRow.get(column); var expectedType = expected.columnTypes().get(column); + var expectedValue = convertExpectedValue(expectedType, expectedRow.get(column)); + var actualValue = actualRow.get(column); - if (expectedValue != null) { - // convert the long from CSV back to its STRING form - if (expectedType == Type.DATETIME) { - expectedValue = rebuildExpected(expectedValue, Long.class, x -> UTC_DATE_TIME_FORMATTER.formatMillis((long) x)); - } else if (expectedType == Type.DATE_NANOS) { - expectedValue = rebuildExpected( - expectedValue, - Long.class, - x -> DateFormatter.forPattern("strict_date_optional_time_nanos").formatNanos((long) x) - ); - } else if (expectedType == Type.GEO_POINT) { - expectedValue = rebuildExpected(expectedValue, BytesRef.class, x -> GEO.wkbToWkt((BytesRef) x)); - } else if (expectedType == Type.CARTESIAN_POINT) { - expectedValue = rebuildExpected(expectedValue, BytesRef.class, x -> CARTESIAN.wkbToWkt((BytesRef) x)); - } else if (expectedType == Type.GEO_SHAPE) { - expectedValue = rebuildExpected(expectedValue, BytesRef.class, x -> GEO.wkbToWkt((BytesRef) x)); - } else if (expectedType == Type.CARTESIAN_SHAPE) { - expectedValue = rebuildExpected(expectedValue, BytesRef.class, x -> CARTESIAN.wkbToWkt((BytesRef) x)); - } else if (expectedType == Type.IP) { - // convert BytesRef-packed IP to String, allowing subsequent comparison with what's expected - expectedValue = rebuildExpected(expectedValue, BytesRef.class, x -> DocValueFormat.IP.format((BytesRef) x)); - } else if (expectedType == Type.VERSION) { - // convert BytesRef-packed Version to String - expectedValue = rebuildExpected(expectedValue, BytesRef.class, x -> new Version((BytesRef) x).toString()); - } else if (expectedType == UNSIGNED_LONG) { - expectedValue = rebuildExpected(expectedValue, Long.class, x -> unsignedLongAsNumber((long) x)); - } - } var transformedExpected = valueTransformer.apply(expectedType, expectedValue); var transformedActual = valueTransformer.apply(expectedType, actualValue); if (Objects.equals(transformedExpected, transformedActual) == false) { dataFailures.add(new DataFailure(row, column, transformedExpected, transformedActual)); } if (dataFailures.size() > 10) { - dataFailure("", dataFailures, expected, actualValues); + dataFailure("", dataFailures, expected, actualValues, valueTransformer); } } @@ -255,7 +234,8 @@ public static void assertData( "Plan has extra columns, returned [" + actualRow.size() + "], expected [" + expectedRow.size() + "]", dataFailures, expected, - actualValues + actualValues, + valueTransformer ); } } catch (AssertionError ae) { @@ -267,10 +247,16 @@ public static void assertData( } } if (dataFailures.isEmpty() == false) { - dataFailure("", dataFailures, expected, actualValues); + dataFailure("", dataFailures, expected, actualValues, valueTransformer); } if (expectedValues.size() < actualValues.size()) { - dataFailure("Elasticsearch still has data after [" + expectedValues.size() + "] entries", dataFailures, expected, actualValues); + dataFailure( + "Elasticsearch still has data after [" + expectedValues.size() + "] entries", + dataFailures, + expected, + actualValues, + valueTransformer + ); } } @@ -278,42 +264,72 @@ private static void dataFailure( String description, List dataFailures, ExpectedResults expectedValues, - List> actualValues + List> actualValues, + BiFunction valueTransformer ) { - var expected = pipeTable("Expected:", expectedValues.columnNames(), expectedValues.values(), 25); - var actual = pipeTable("Actual:", expectedValues.columnNames(), actualValues, 25); + var expected = pipeTable( + "Expected:", + expectedValues.columnNames(), + expectedValues.columnTypes(), + expectedValues.values(), + (type, value) -> valueTransformer.apply(type, convertExpectedValue(type, value)) + ); + var actual = pipeTable("Actual:", expectedValues.columnNames(), expectedValues.columnTypes(), actualValues, valueTransformer); fail(description + System.lineSeparator() + describeFailures(dataFailures) + actual + expected); } - private static String pipeTable(String description, List headers, List> values, int maxRows) { + private static final int MAX_ROWS = 25; + + private static String pipeTable( + String description, + List headers, + List types, + List> values, + BiFunction valueTransformer + ) { + int rows = Math.min(MAX_ROWS, values.size()); int[] width = new int[headers.size()]; - for (int i = 0; i < width.length; i++) { - width[i] = headers.get(i).length(); - for (List row : values) { - width[i] = Math.max(width[i], String.valueOf(row.get(i)).length()); + String[][] printableValues = new String[rows][headers.size()]; + for (int c = 0; c < headers.size(); c++) { + width[c] = header(headers.get(c), types.get(c)).length(); + } + for (int r = 0; r < rows; r++) { + for (int c = 0; c < headers.size(); c++) { + printableValues[r][c] = String.valueOf(valueTransformer.apply(types.get(c), values.get(r).get(c))); + width[c] = Math.max(width[c], printableValues[r][c].length()); } } var result = new StringBuilder().append(System.lineSeparator()).append(description).append(System.lineSeparator()); - for (int c = 0; c < width.length; c++) { - appendValue(result, headers.get(c), width[c]); + // headers + appendPaddedValue(result, header(headers.get(0), types.get(0)), width[0]); + for (int c = 1; c < width.length; c++) { + result.append(" | "); + appendPaddedValue(result, header(headers.get(c), types.get(c)), width[c]); } - result.append('|').append(System.lineSeparator()); - for (int r = 0; r < Math.min(maxRows, values.size()); r++) { - for (int c = 0; c < width.length; c++) { - appendValue(result, values.get(r).get(c), width[c]); + result.append(System.lineSeparator()); + // values + for (int r = 0; r < printableValues.length; r++) { + appendPaddedValue(result, printableValues[r][0], width[0]); + for (int c = 1; c < printableValues[r].length; c++) { + result.append(" | "); + appendPaddedValue(result, printableValues[r][c], width[c]); } - result.append('|').append(System.lineSeparator()); + result.append(System.lineSeparator()); } - if (values.size() > maxRows) { + if (values.size() > rows) { result.append("...").append(System.lineSeparator()); } return result.toString(); } - private static void appendValue(StringBuilder result, Object value, int width) { - result.append('|').append(value); - for (int i = 0; i < width - String.valueOf(value).length(); i++) { + private static String header(String name, Type type) { + return name + ':' + Strings.toLowercaseAscii(type.name()); + } + + private static void appendPaddedValue(StringBuilder result, String value, int width) { + result.append(value); + for (int i = 0; i < width - (value != null ? value.length() : 4); i++) { result.append(' '); } } @@ -369,6 +385,34 @@ private static Comparator> resultRowComparator(List types) { }; } + private static Object convertExpectedValue(Type expectedType, Object expectedValue) { + if (expectedValue == null) { + return null; + } + + // convert the long from CSV back to its STRING form + return switch (expectedType) { + case Type.DATETIME -> rebuildExpected(expectedValue, Long.class, x -> UTC_DATE_TIME_FORMATTER.formatMillis((long) x)); + case Type.DATE_NANOS -> rebuildExpected( + expectedValue, + Long.class, + x -> DateFormatter.forPattern("strict_date_optional_time_nanos").formatNanos((long) x) + ); + case Type.GEO_POINT, Type.GEO_SHAPE -> rebuildExpected(expectedValue, BytesRef.class, x -> GEO.wkbToWkt((BytesRef) x)); + case Type.CARTESIAN_POINT, Type.CARTESIAN_SHAPE -> rebuildExpected( + expectedValue, + BytesRef.class, + x -> CARTESIAN.wkbToWkt((BytesRef) x) + ); + case Type.IP -> // convert BytesRef-packed IP to String, allowing subsequent comparison with what's expected + rebuildExpected(expectedValue, BytesRef.class, x -> DocValueFormat.IP.format((BytesRef) x)); + case Type.VERSION -> // convert BytesRef-packed Version to String + rebuildExpected(expectedValue, BytesRef.class, x -> new Version((BytesRef) x).toString()); + case UNSIGNED_LONG -> rebuildExpected(expectedValue, Long.class, x -> unsignedLongAsNumber((long) x)); + default -> expectedValue; + }; + } + private static Object rebuildExpected(Object expectedValue, Class clazz, Function mapper) { if (List.class.isAssignableFrom(expectedValue.getClass())) { assertThat(((List) expectedValue).get(0), instanceOf(clazz)); From 709a87e8013503097d55787a8950da34158b6cb1 Mon Sep 17 00:00:00 2001 From: Joan Fontanals Date: Mon, 16 Dec 2024 14:05:30 +0100 Subject: [PATCH 005/119] Handle `null` text values in RankedDocsResults.asMap() (#118597) --- .../core/inference/results/RankedDocsResults.java | 6 +++++- .../inference/results/RankedDocsResultsTests.java | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/results/RankedDocsResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/results/RankedDocsResults.java index 9c764babe33fc..a5f72bd51c6c6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/results/RankedDocsResults.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/results/RankedDocsResults.java @@ -139,7 +139,11 @@ public void writeTo(StreamOutput out) throws IOException { } public Map asMap() { - return Map.of(NAME, Map.of(INDEX, index, RELEVANCE_SCORE, relevanceScore, TEXT, text)); + if (text != null) { + return Map.of(NAME, Map.of(INDEX, index, RELEVANCE_SCORE, relevanceScore, TEXT, text)); + } else { + return Map.of(NAME, Map.of(INDEX, index, RELEVANCE_SCORE, relevanceScore)); + } } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/inference/results/RankedDocsResultsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/inference/results/RankedDocsResultsTests.java index 46f10928cad08..ff6f6848f4b69 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/inference/results/RankedDocsResultsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/inference/results/RankedDocsResultsTests.java @@ -12,10 +12,12 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.AbstractChunkedBWCSerializationTestCase; +import org.hamcrest.Matchers; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class RankedDocsResultsTests extends AbstractChunkedBWCSerializationTestCase { @@ -37,6 +39,16 @@ public static RankedDocsResults.RankedDoc createRandomDoc() { return new RankedDocsResults.RankedDoc(randomIntBetween(0, 100), randomFloat(), randomBoolean() ? null : randomAlphaOfLength(10)); } + public void test_asMap() { + var index = randomIntBetween(0, 100); + var score = randomFloat(); + var mapNullText = new RankedDocsResults.RankedDoc(index, score, null).asMap(); + assertThat(mapNullText, Matchers.is(Map.of("ranked_doc", Map.of("index", index, "relevance_score", score)))); + + var mapWithText = new RankedDocsResults.RankedDoc(index, score, "Sample text").asMap(); + assertThat(mapWithText, Matchers.is(Map.of("ranked_doc", Map.of("index", index, "relevance_score", score, "text", "Sample text")))); + } + @Override protected RankedDocsResults mutateInstance(RankedDocsResults instance) throws IOException { List copy = new ArrayList<>(List.copyOf(instance.getRankedDocs())); From 0efdc4741bfb8610528c9db0b2d72030b6a0684b Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Mon, 16 Dec 2024 14:12:39 +0100 Subject: [PATCH 006/119] Disable one more test failing on pre-8.13 BWC (#118760) Prevent `stats.ByStringAndLongWithAlias` from running on pre-8.13 BWC. Related #118655. --- .../esql/qa/testFixtures/src/main/resources/dissect.csv-spec | 4 ++-- .../esql/qa/testFixtures/src/main/resources/grok.csv-spec | 4 ++-- .../esql/qa/testFixtures/src/main/resources/stats.csv-spec | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/dissect.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/dissect.csv-spec index cde5427bf37d6..2b3b0bee93471 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/dissect.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/dissect.csv-spec @@ -223,7 +223,7 @@ null | null | null ; -// the query is incorrectly physically plan (fails the verification) in pre-8.13.0 versions +// the query is incorrectly physically planned (fails the verification) in pre-8.13.0 versions overwriteName#[skip:-8.12.99] from employees | sort emp_no asc | eval full_name = concat(first_name, " ", last_name) | dissect full_name "%{emp_no} %{b}" | keep full_name, emp_no, b | limit 3; @@ -245,7 +245,7 @@ emp_no:integer | first_name:keyword | rest:keyword ; -// the query is incorrectly physically plan (fails the verification) in pre-8.13.0 versions +// the query is incorrectly physically planned (fails the verification) in pre-8.13.0 versions overwriteNameWhere#[skip:-8.12.99] from employees | sort emp_no asc | eval full_name = concat(first_name, " ", last_name) | dissect full_name "%{emp_no} %{b}" | where emp_no == "Bezalel" | keep full_name, emp_no, b | limit 3; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/grok.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/grok.csv-spec index eece1bdfbffa4..6dc9148ffc0e8 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/grok.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/grok.csv-spec @@ -199,7 +199,7 @@ null | null | null ; -// the query is incorrectly physically plan (fails the verification) in pre-8.13.0 versions +// the query is incorrectly physically planned (fails the verification) in pre-8.13.0 versions overwriteName#[skip:-8.12.99] from employees | sort emp_no asc | eval full_name = concat(first_name, " ", last_name) | grok full_name "%{WORD:emp_no} %{WORD:b}" | keep full_name, emp_no, b | limit 3; @@ -210,7 +210,7 @@ Parto Bamford | Parto | Bamford ; -// the query is incorrectly physically plan (fails the verification) in pre-8.13.0 versions +// the query is incorrectly physically planned (fails the verification) in pre-8.13.0 versions overwriteNameWhere#[skip:-8.12.99] from employees | sort emp_no asc | eval full_name = concat(first_name, " ", last_name) | grok full_name "%{WORD:emp_no} %{WORD:b}" | where emp_no == "Bezalel" | keep full_name, emp_no, b | limit 3; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec index 100c0d716d65c..80586ce9bcb09 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec @@ -564,7 +564,7 @@ c:long | gender:keyword | trunk_worked_seconds:long 0 | null | 200000000 ; -// the query is incorrectly physically plan (fails the verification) in pre-8.13.0 versions +// the query is incorrectly physically planned (fails the verification) in pre-8.13.0 versions byStringAndLongWithAlias#[skip:-8.12.99] FROM employees | EVAL trunk_worked_seconds = avg_worked_seconds / 100000000 * 100000000 @@ -720,7 +720,8 @@ c:long | d:date | gender:keyword | languages:integer 2 | 1987-01-01T00:00:00.000Z | M | 1 ; -byDateAndKeywordAndIntWithAlias +// the query is incorrectly physically planned (fails the verification) in pre-8.13.0 versions +byDateAndKeywordAndIntWithAlias#[skip:-8.12.99] from employees | eval d = date_trunc(1 year, hire_date) | rename gender as g, languages as l, emp_no as e | keep d, g, l, e | stats c = count(e) by d, g, l | sort c desc, d, l desc, g desc | limit 10; c:long | d:date | g:keyword | l:integer From 2b8c494b3b8dbca07b90dfed8d6751f15631c814 Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Mon, 16 Dec 2024 14:47:03 +0100 Subject: [PATCH 007/119] ES|QL: Fix RLIKE folding with (unsupported) case insensitive pattern (#118454) --- docs/changelog/118454.yaml | 5 +++ .../expression/predicate/regex/RLike.java | 12 +------ .../predicate/regex/RegexMatch.java | 7 +--- .../predicate/regex/WildcardLike.java | 13 +------ .../xpack/esql/core/util/TestUtils.java | 6 ---- .../src/main/resources/eval.csv-spec | 36 +++++++++++++++++++ .../xpack/esql/action/EsqlCapabilities.java | 7 +++- .../function/scalar/string/RLike.java | 7 +++- .../function/scalar/string/WildcardLike.java | 5 +++ .../rules/logical/ConstantFoldingTests.java | 4 +-- .../PushDownAndCombineFiltersTests.java | 2 +- .../rules/logical/ReplaceRegexMatchTests.java | 4 +-- 12 files changed, 66 insertions(+), 42 deletions(-) create mode 100644 docs/changelog/118454.yaml diff --git a/docs/changelog/118454.yaml b/docs/changelog/118454.yaml new file mode 100644 index 0000000000000..9a19ede64d705 --- /dev/null +++ b/docs/changelog/118454.yaml @@ -0,0 +1,5 @@ +pr: 118454 +summary: Fix RLIKE folding with (unsupported) case insensitive pattern +area: ES|QL +type: bug +issues: [] diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RLike.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RLike.java index 5f095a654fc89..b4bccf162d9e4 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RLike.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RLike.java @@ -8,12 +8,11 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; import java.io.IOException; -public class RLike extends RegexMatch { +public abstract class RLike extends RegexMatch { public RLike(Source source, Expression value, RLikePattern pattern) { super(source, value, pattern, false); @@ -33,13 +32,4 @@ public String getWriteableName() { throw new UnsupportedOperationException(); } - @Override - protected NodeInfo info() { - return NodeInfo.create(this, RLike::new, field(), pattern(), caseInsensitive()); - } - - @Override - protected RLike replaceChild(Expression newChild) { - return new RLike(source(), newChild, pattern(), caseInsensitive()); - } } diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RegexMatch.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RegexMatch.java index 32e8b04573d2d..0f9116ade5a31 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RegexMatch.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/RegexMatch.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.esql.core.expression.predicate.regex; -import org.apache.lucene.util.BytesRef; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.Nullability; import org.elasticsearch.xpack.esql.core.expression.function.scalar.UnaryScalarFunction; @@ -64,11 +63,7 @@ public boolean foldable() { @Override public Boolean fold() { - Object val = field().fold(); - if (val instanceof BytesRef br) { - val = br.utf8ToString(); - } - return RegexOperation.match(val, pattern().asJavaRegex()); + throw new UnsupportedOperationException(); } @Override diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/WildcardLike.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/WildcardLike.java index bf54744667217..05027707326bd 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/WildcardLike.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/regex/WildcardLike.java @@ -8,12 +8,11 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; import java.io.IOException; -public class WildcardLike extends RegexMatch { +public abstract class WildcardLike extends RegexMatch { public WildcardLike(Source source, Expression left, WildcardPattern pattern) { this(source, left, pattern, false); @@ -33,14 +32,4 @@ public String getWriteableName() { throw new UnsupportedOperationException(); } - @Override - protected NodeInfo info() { - return NodeInfo.create(this, WildcardLike::new, field(), pattern(), caseInsensitive()); - } - - @Override - protected WildcardLike replaceChild(Expression newLeft) { - return new WildcardLike(source(), newLeft, pattern(), caseInsensitive()); - } - } diff --git a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/util/TestUtils.java b/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/util/TestUtils.java index 9f8e23cb15a97..b37ca0431ec2d 100644 --- a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/util/TestUtils.java +++ b/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/util/TestUtils.java @@ -11,8 +11,6 @@ import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.expression.predicate.Range; -import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLike; -import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.type.EsField; @@ -46,10 +44,6 @@ public static Range rangeOf(Expression value, Expression lower, boolean includeL return new Range(EMPTY, value, lower, includeLower, upper, includeUpper, randomZone()); } - public static RLike rlike(Expression left, String exp) { - return new RLike(EMPTY, left, new RLikePattern(exp)); - } - public static FieldAttribute fieldAttribute() { return fieldAttribute(randomAlphaOfLength(10), randomFrom(DataType.types())); } diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/eval.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/eval.csv-spec index 592b06107c8b5..72660c11d8b73 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/eval.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/eval.csv-spec @@ -601,3 +601,39 @@ Mokhtar |Bernatsky |38992 |BM Parto |Bamford |61805 |BP Premal |Baek |52833 |BP ; + + +caseInsensitiveRegex +from employees | where first_name RLIKE "(?i)geor.*" | keep first_name +; + +first_name:keyword +; + + +caseInsensitiveRegex2 +from employees | where first_name RLIKE "(?i)Geor.*" | keep first_name +; + +first_name:keyword +; + + +caseInsensitiveRegexFold +required_capability: fixed_regex_fold +row foo = "Bar" | where foo rlike "(?i)ba.*" +; + +foo:keyword +; + + +caseInsensitiveRegexFold2 +required_capability: fixed_regex_fold +row foo = "Bar" | where foo rlike "(?i)Ba.*" +; + +foo:keyword +; + + diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 649ec1eba9785..e9a0f89e4f448 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -577,7 +577,12 @@ public enum Cap { /** * Additional types for match function and operator */ - MATCH_ADDITIONAL_TYPES; + MATCH_ADDITIONAL_TYPES, + + /** + * Fix for regex folding with case-insensitive pattern https://github.com/elastic/elasticsearch/issues/118371 + */ + FIXED_REGEX_FOLD; private final boolean enabled; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLike.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLike.java index cd42711177510..996c90a8e40bc 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLike.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLike.java @@ -79,7 +79,7 @@ public String getWriteableName() { } @Override - protected NodeInfo info() { + protected NodeInfo info() { return NodeInfo.create(this, RLike::new, field(), pattern(), caseInsensitive()); } @@ -93,6 +93,11 @@ protected TypeResolution resolveType() { return isString(field(), sourceText(), DEFAULT); } + @Override + public Boolean fold() { + return (Boolean) EvaluatorMapper.super.fold(); + } + @Override public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { return AutomataMatch.toEvaluator(source(), toEvaluator.apply(field()), pattern().createAutomaton()); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLike.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLike.java index c1b4f20f41795..d2edb0f92e8f2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLike.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLike.java @@ -99,6 +99,11 @@ protected TypeResolution resolveType() { return isString(field(), sourceText(), DEFAULT); } + @Override + public Boolean fold() { + return (Boolean) EvaluatorMapper.super.fold(); + } + @Override public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { return AutomataMatch.toEvaluator( diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ConstantFoldingTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ConstantFoldingTests.java index c2e85cc43284a..c4f4dac67acd3 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ConstantFoldingTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ConstantFoldingTests.java @@ -17,11 +17,11 @@ import org.elasticsearch.xpack.esql.core.expression.predicate.logical.And; import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Not; import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Or; -import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLike; import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern; -import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardLike; import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPattern; import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.scalar.string.RLike; +import org.elasticsearch.xpack.esql.expression.function.scalar.string.WildcardLike; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mod; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java index e159e5ed0bd7d..bc22fbb6bd828 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java @@ -199,7 +199,7 @@ public void testPushDownFilterOnAliasInEval() { public void testPushDownLikeRlikeFilter() { EsRelation relation = relation(); - org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLike conditionA = rlike(getFieldAttribute("a"), "foo"); + RLike conditionA = rlike(getFieldAttribute("a"), "foo"); WildcardLike conditionB = wildcardLike(getFieldAttribute("b"), "bar"); Filter fa = new Filter(EMPTY, relation, conditionA); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ReplaceRegexMatchTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ReplaceRegexMatchTests.java index 20d638a113bf2..c7206c6971bde 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ReplaceRegexMatchTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ReplaceRegexMatchTests.java @@ -11,11 +11,11 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.expression.predicate.nulls.IsNotNull; -import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLike; import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern; -import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardLike; import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPattern; import org.elasticsearch.xpack.esql.core.util.StringUtils; +import org.elasticsearch.xpack.esql.expression.function.scalar.string.RLike; +import org.elasticsearch.xpack.esql.expression.function.scalar.string.WildcardLike; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals; import static java.util.Arrays.asList; From 6d6eac2c77a94dfdbee01cc1f609354c8c016d2a Mon Sep 17 00:00:00 2001 From: kanoshiou <73424326+kanoshiou@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:20:58 +0800 Subject: [PATCH 008/119] ESQL: Drop null columns in text formats (#117643) This PR resolves the issue where, despite setting `drop_null_columns=true`, columns that are entirely null are still returned when using `format=txt`, `format=csv`, or `format=tsv`. Closes #116848 --- docs/changelog/117643.yaml | 6 +++ .../xpack/esql/qa/rest/RestEsqlTestCase.java | 6 +-- .../xpack/esql/action/EsqlQueryResponse.java | 2 +- .../xpack/esql/formatter/TextFormat.java | 22 +++++++-- .../xpack/esql/formatter/TextFormatter.java | 24 ++++++++-- .../xpack/esql/formatter/TextFormatTests.java | 47 ++++++++++++++----- .../esql/formatter/TextFormatterTests.java | 41 +++++++++++++--- 7 files changed, 117 insertions(+), 31 deletions(-) create mode 100644 docs/changelog/117643.yaml diff --git a/docs/changelog/117643.yaml b/docs/changelog/117643.yaml new file mode 100644 index 0000000000000..9105749377d2c --- /dev/null +++ b/docs/changelog/117643.yaml @@ -0,0 +1,6 @@ +pr: 117643 +summary: Drop null columns in text formats +area: ES|QL +type: bug +issues: + - 116848 diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java index 6a8779eef4efc..86f8a8c5363f6 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java @@ -1119,7 +1119,7 @@ public void testAsyncGetWithoutContentType() throws IOException { var json = entityToMap(entity, requestObject.contentType()); checkKeepOnCompletion(requestObject, json, true); String id = (String) json.get("id"); - // results won't be returned since keepOnCompletion is true + // results won't be returned because wait_for_completion is provided a very small interval assertThat(id, is(not(emptyOrNullString()))); // issue an "async get" request with no Content-Type @@ -1274,11 +1274,11 @@ static String runEsqlAsTextWithFormat(RequestObjectBuilder builder, String forma switch (format) { case "txt" -> assertThat(initialValue, emptyOrNullString()); case "csv" -> { - assertEquals(initialValue, "\r\n"); + assertEquals("\r\n", initialValue); initialValue = ""; } case "tsv" -> { - assertEquals(initialValue, "\n"); + assertEquals("\n", initialValue); initialValue = ""; } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponse.java index dc0e9fd1fb06d..4163a222b1a28 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponse.java @@ -218,7 +218,7 @@ public Iterator toXContentChunked(ToXContent.Params params }); } - private boolean[] nullColumns() { + public boolean[] nullColumns() { boolean[] nullColumns = new boolean[columns.size()]; for (int c = 0; c < nullColumns.length; c++) { nullColumns[c] = allColumnsAreNull(c); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/formatter/TextFormat.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/formatter/TextFormat.java index 5c0d6b138b326..7a7e4677b0dca 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/formatter/TextFormat.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/formatter/TextFormat.java @@ -39,7 +39,8 @@ public enum TextFormat implements MediaType { PLAIN_TEXT() { @Override public Iterator> format(RestRequest request, EsqlQueryResponse esqlResponse) { - return new TextFormatter(esqlResponse).format(hasHeader(request)); + boolean dropNullColumns = request.paramAsBoolean(DROP_NULL_COLUMNS_OPTION, false); + return new TextFormatter(esqlResponse, hasHeader(request), dropNullColumns).format(); } @Override @@ -282,15 +283,21 @@ public Set headerValues() { */ public static final String URL_PARAM_FORMAT = "format"; public static final String URL_PARAM_DELIMITER = "delimiter"; + public static final String DROP_NULL_COLUMNS_OPTION = "drop_null_columns"; public Iterator> format(RestRequest request, EsqlQueryResponse esqlResponse) { final var delimiter = delimiter(request); + boolean dropNullColumns = request.paramAsBoolean(DROP_NULL_COLUMNS_OPTION, false); + boolean[] dropColumns = dropNullColumns ? esqlResponse.nullColumns() : new boolean[esqlResponse.columns().size()]; return Iterators.concat( // if the header is requested return the info hasHeader(request) && esqlResponse.columns() != null - ? Iterators.single(writer -> row(writer, esqlResponse.columns().iterator(), ColumnInfo::name, delimiter)) + ? Iterators.single(writer -> row(writer, esqlResponse.columns().iterator(), ColumnInfo::name, delimiter, dropColumns)) : Collections.emptyIterator(), - Iterators.map(esqlResponse.values(), row -> writer -> row(writer, row, f -> Objects.toString(f, StringUtils.EMPTY), delimiter)) + Iterators.map( + esqlResponse.values(), + row -> writer -> row(writer, row, f -> Objects.toString(f, StringUtils.EMPTY), delimiter, dropColumns) + ) ); } @@ -313,9 +320,14 @@ public String contentType(RestRequest request) { } // utility method for consuming a row. - void row(Writer writer, Iterator row, Function toString, Character delimiter) throws IOException { + void row(Writer writer, Iterator row, Function toString, Character delimiter, boolean[] dropColumns) + throws IOException { boolean firstColumn = true; - while (row.hasNext()) { + for (int i = 0; row.hasNext(); i++) { + if (dropColumns[i]) { + row.next(); + continue; + } if (firstColumn) { firstColumn = false; } else { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/formatter/TextFormatter.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/formatter/TextFormatter.java index 0535e4adfe346..95b46958be351 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/formatter/TextFormatter.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/formatter/TextFormatter.java @@ -30,13 +30,17 @@ public class TextFormatter { private final EsqlQueryResponse response; private final int[] width; private final Function FORMATTER = Objects::toString; + private final boolean includeHeader; + private final boolean[] dropColumns; /** - * Create a new {@linkplain TextFormatter} for formatting responses. + * Create a new {@linkplain TextFormatter} for formatting responses */ - public TextFormatter(EsqlQueryResponse response) { + public TextFormatter(EsqlQueryResponse response, boolean includeHeader, boolean dropNullColumns) { this.response = response; var columns = response.columns(); + this.includeHeader = includeHeader; + this.dropColumns = dropNullColumns ? response.nullColumns() : new boolean[columns.size()]; // Figure out the column widths: // 1. Start with the widths of the column names width = new int[columns.size()]; @@ -58,12 +62,12 @@ public TextFormatter(EsqlQueryResponse response) { } /** - * Format the provided {@linkplain EsqlQueryResponse} optionally including the header lines. + * Format the provided {@linkplain EsqlQueryResponse} */ - public Iterator> format(boolean includeHeader) { + public Iterator> format() { return Iterators.concat( // The header lines - includeHeader && response.columns().size() > 0 ? Iterators.single(this::formatHeader) : Collections.emptyIterator(), + includeHeader && response.columns().isEmpty() == false ? Iterators.single(this::formatHeader) : Collections.emptyIterator(), // Now format the results. formatResults() ); @@ -71,6 +75,9 @@ public Iterator> format(boolean includeHead private void formatHeader(Writer writer) throws IOException { for (int i = 0; i < width.length; i++) { + if (dropColumns[i]) { + continue; + } if (i > 0) { writer.append('|'); } @@ -86,6 +93,9 @@ private void formatHeader(Writer writer) throws IOException { writer.append('\n'); for (int i = 0; i < width.length; i++) { + if (dropColumns[i]) { + continue; + } if (i > 0) { writer.append('+'); } @@ -98,6 +108,10 @@ private Iterator> formatResults() { return Iterators.map(response.values(), row -> writer -> { for (int i = 0; i < width.length; i++) { assert row.hasNext(); + if (dropColumns[i]) { + row.next(); + continue; + } if (i > 0) { writer.append('|'); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/formatter/TextFormatTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/formatter/TextFormatTests.java index fe1ac52427627..ca47e0cb329b3 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/formatter/TextFormatTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/formatter/TextFormatTests.java @@ -123,17 +123,17 @@ public void testTsvFormatWithEmptyData() { public void testCsvFormatWithRegularData() { String text = format(CSV, req(), regularData()); assertEquals(""" - string,number,location,location2\r - Along The River Bank,708,POINT (12.0 56.0),POINT (1234.0 5678.0)\r - Mind Train,280,POINT (-97.0 26.0),POINT (-9753.0 2611.0)\r + string,number,location,location2,null_field\r + Along The River Bank,708,POINT (12.0 56.0),POINT (1234.0 5678.0),\r + Mind Train,280,POINT (-97.0 26.0),POINT (-9753.0 2611.0),\r """, text); } public void testCsvFormatNoHeaderWithRegularData() { String text = format(CSV, reqWithParam("header", "absent"), regularData()); assertEquals(""" - Along The River Bank,708,POINT (12.0 56.0),POINT (1234.0 5678.0)\r - Mind Train,280,POINT (-97.0 26.0),POINT (-9753.0 2611.0)\r + Along The River Bank,708,POINT (12.0 56.0),POINT (1234.0 5678.0),\r + Mind Train,280,POINT (-97.0 26.0),POINT (-9753.0 2611.0),\r """, text); } @@ -146,14 +146,17 @@ public void testCsvFormatWithCustomDelimiterRegularData() { "number", "location", "location2", + "null_field", "Along The River Bank", "708", "POINT (12.0 56.0)", "POINT (1234.0 5678.0)", + "", "Mind Train", "280", "POINT (-97.0 26.0)", - "POINT (-9753.0 2611.0)" + "POINT (-9753.0 2611.0)", + "" ); List expectedTerms = terms.stream() .map(x -> x.contains(String.valueOf(delim)) ? '"' + x + '"' : x) @@ -167,6 +170,8 @@ public void testCsvFormatWithCustomDelimiterRegularData() { sb.append(expectedTerms.remove(0)); sb.append(delim); sb.append(expectedTerms.remove(0)); + sb.append(delim); + sb.append(expectedTerms.remove(0)); sb.append("\r\n"); } while (expectedTerms.size() > 0); assertEquals(sb.toString(), text); @@ -175,9 +180,9 @@ public void testCsvFormatWithCustomDelimiterRegularData() { public void testTsvFormatWithRegularData() { String text = format(TSV, req(), regularData()); assertEquals(""" - string\tnumber\tlocation\tlocation2 - Along The River Bank\t708\tPOINT (12.0 56.0)\tPOINT (1234.0 5678.0) - Mind Train\t280\tPOINT (-97.0 26.0)\tPOINT (-9753.0 2611.0) + string\tnumber\tlocation\tlocation2\tnull_field + Along The River Bank\t708\tPOINT (12.0 56.0)\tPOINT (1234.0 5678.0)\t + Mind Train\t280\tPOINT (-97.0 26.0)\tPOINT (-9753.0 2611.0)\t """, text); } @@ -245,6 +250,24 @@ public void testPlainTextEmptyCursorWithoutColumns() { ); } + public void testCsvFormatWithDropNullColumns() { + String text = format(CSV, reqWithParam("drop_null_columns", "true"), regularData()); + assertEquals(""" + string,number,location,location2\r + Along The River Bank,708,POINT (12.0 56.0),POINT (1234.0 5678.0)\r + Mind Train,280,POINT (-97.0 26.0),POINT (-9753.0 2611.0)\r + """, text); + } + + public void testTsvFormatWithDropNullColumns() { + String text = format(TSV, reqWithParam("drop_null_columns", "true"), regularData()); + assertEquals(""" + string\tnumber\tlocation\tlocation2 + Along The River Bank\t708\tPOINT (12.0 56.0)\tPOINT (1234.0 5678.0) + Mind Train\t280\tPOINT (-97.0 26.0)\tPOINT (-9753.0 2611.0) + """, text); + } + private static EsqlQueryResponse emptyData() { return new EsqlQueryResponse(singletonList(new ColumnInfoImpl("name", "keyword")), emptyList(), null, false, false, null); } @@ -256,7 +279,8 @@ private static EsqlQueryResponse regularData() { new ColumnInfoImpl("string", "keyword"), new ColumnInfoImpl("number", "integer"), new ColumnInfoImpl("location", "geo_point"), - new ColumnInfoImpl("location2", "cartesian_point") + new ColumnInfoImpl("location2", "cartesian_point"), + new ColumnInfoImpl("null_field", "keyword") ); BytesRefArray geoPoints = new BytesRefArray(2, BigArrays.NON_RECYCLING_INSTANCE); @@ -274,7 +298,8 @@ private static EsqlQueryResponse regularData() { blockFactory.newBytesRefBlockBuilder(2) .appendBytesRef(CARTESIAN.asWkb(new Point(1234, 5678))) .appendBytesRef(CARTESIAN.asWkb(new Point(-9753, 2611))) - .build() + .build(), + blockFactory.newConstantNullBlock(2) ) ); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/formatter/TextFormatterTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/formatter/TextFormatterTests.java index e735ba83168bb..4e90fe53d96d7 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/formatter/TextFormatterTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/formatter/TextFormatterTests.java @@ -85,8 +85,6 @@ public class TextFormatterTests extends ESTestCase { new EsqlExecutionInfo(randomBoolean()) ); - TextFormatter formatter = new TextFormatter(esqlResponse); - /** * Tests for {@link TextFormatter#format} with header, values * of exactly the minimum column size, column names of exactly @@ -95,7 +93,7 @@ public class TextFormatterTests extends ESTestCase { * column size. */ public void testFormatWithHeader() { - String[] result = getTextBodyContent(formatter.format(true)).split("\n"); + String[] result = getTextBodyContent(new TextFormatter(esqlResponse, true, false).format()).split("\n"); assertThat(result, arrayWithSize(4)); assertEquals( " foo | bar |15charwidename!| null_field1 |superduperwidename!!!| baz |" @@ -119,6 +117,35 @@ public void testFormatWithHeader() { ); } + /** + * Tests for {@link TextFormatter#format} with drop_null_columns and + * truncation of long columns. + */ + public void testFormatWithDropNullColumns() { + String[] result = getTextBodyContent(new TextFormatter(esqlResponse, true, true).format()).split("\n"); + assertThat(result, arrayWithSize(4)); + assertEquals( + " foo | bar |15charwidename!|superduperwidename!!!| baz |" + + " date | location | location2 ", + result[0] + ); + assertEquals( + "---------------+---------------+---------------+---------------------+---------------+-------" + + "-----------------+------------------+----------------------", + result[1] + ); + assertEquals( + "15charwidedata!|1 |6.888 |12.0 |rabbit |" + + "1953-09-02T00:00:00.000Z|POINT (12.0 56.0) |POINT (1234.0 5678.0) ", + result[2] + ); + assertEquals( + "dog |2 |123124.888 |9912.0 |goat |" + + "2000-03-15T21:34:37.443Z|POINT (-97.0 26.0)|POINT (-9753.0 2611.0)", + result[3] + ); + } + /** * Tests for {@link TextFormatter#format} without header and * truncation of long columns. @@ -160,7 +187,7 @@ public void testFormatWithoutHeader() { new EsqlExecutionInfo(randomBoolean()) ); - String[] result = getTextBodyContent(new TextFormatter(response).format(false)).split("\n"); + String[] result = getTextBodyContent(new TextFormatter(response, false, false).format()).split("\n"); assertThat(result, arrayWithSize(2)); assertEquals( "doggie |4 |1.0 |null |77.0 |wombat |" @@ -199,8 +226,10 @@ public void testVeryLongPadding() { randomBoolean(), randomBoolean(), new EsqlExecutionInfo(randomBoolean()) - ) - ).format(false) + ), + false, + false + ).format() ) ); } From f2e5430682a194995f0dae87612ab758db0d377e Mon Sep 17 00:00:00 2001 From: Mark Tozzi Date: Mon, 16 Dec 2024 09:24:47 -0500 Subject: [PATCH 009/119] Remove unused method (#118601) I was looking at some type conversion stuff, and noticed this method is never used. Back porting to reduce possible conflicts with the long term maintenance branch later. --- .../xpack/esql/core/planner/TranslatorHandler.java | 2 -- .../xpack/esql/planner/EsqlTranslatorHandler.java | 7 ------- 2 files changed, 9 deletions(-) diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/planner/TranslatorHandler.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/planner/TranslatorHandler.java index 1ccbb04f7a69c..b85544905595a 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/planner/TranslatorHandler.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/planner/TranslatorHandler.java @@ -12,7 +12,6 @@ import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; -import org.elasticsearch.xpack.esql.core.type.DataType; import java.util.function.Supplier; @@ -34,5 +33,4 @@ default Query wrapFunctionQuery(ScalarFunction sf, Expression field, Supplier querySupplier) { if (field instanceof FieldAttribute fa) { From b357936227df04f88c980b6cbf7cd7cde982a25b Mon Sep 17 00:00:00 2001 From: Dmitriy Burlutskiy Date: Mon, 16 Dec 2024 15:36:50 +0100 Subject: [PATCH 010/119] Revert "Support mTLS in Elastic Inference Service plugin (#116423)" (#118765) This reverts commit 74a4484101dd65a0194f4adc3bd23fe39c2f2bd7. --- docs/changelog/116423.yaml | 5 - .../xpack/core/ssl/SSLService.java | 2 - .../core/LocalStateCompositeXPackPlugin.java | 2 +- .../xpack/core/ssl/SSLServiceTests.java | 3 +- .../ShardBulkInferenceActionFilterIT.java | 3 +- .../integration/ModelRegistryIT.java | 4 +- .../inference/src/main/java/module-info.java | 1 - .../xpack/inference/InferencePlugin.java | 101 +++++------------- .../external/http/HttpClientManager.java | 44 -------- .../TextSimilarityRankRetrieverBuilder.java | 11 +- .../ElasticInferenceServiceSettings.java | 24 +---- .../SemanticTextClusterMetadataTests.java | 3 +- .../xpack/inference/InferencePluginTests.java | 65 ----------- .../inference/LocalStateInferencePlugin.java | 71 ------------ .../elasticsearch/xpack/inference/Utils.java | 15 +++ ...emanticTextNonDynamicFieldMapperTests.java | 3 +- .../TextSimilarityRankMultiNodeTests.java | 4 +- ...SimilarityRankRetrieverTelemetryTests.java | 5 +- .../TextSimilarityRankTests.java | 4 +- .../xpack/ml/LocalStateMachineLearning.java | 7 -- .../xpack/ml/support/BaseMlIntegTestCase.java | 4 +- .../security/CrossClusterShardTests.java | 2 + 22 files changed, 69 insertions(+), 314 deletions(-) delete mode 100644 docs/changelog/116423.yaml delete mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java delete mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java diff --git a/docs/changelog/116423.yaml b/docs/changelog/116423.yaml deleted file mode 100644 index d6d10eab410e4..0000000000000 --- a/docs/changelog/116423.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 116423 -summary: Support mTLS for the Elastic Inference Service integration inside the inference API -area: Machine Learning -type: feature -issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java index d0d5e463f9652..9704335776f11 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java @@ -596,8 +596,6 @@ static Map getSSLSettingsMap(Settings settings) { sslSettingsMap.put(WatcherField.EMAIL_NOTIFICATION_SSL_PREFIX, settings.getByPrefix(WatcherField.EMAIL_NOTIFICATION_SSL_PREFIX)); sslSettingsMap.put(XPackSettings.TRANSPORT_SSL_PREFIX, settings.getByPrefix(XPackSettings.TRANSPORT_SSL_PREFIX)); sslSettingsMap.putAll(getTransportProfileSSLSettings(settings)); - // Mount Elastic Inference Service (part of the Inference plugin) configuration - sslSettingsMap.put("xpack.inference.elastic.http.ssl", settings.getByPrefix("xpack.inference.elastic.http.ssl.")); // Only build remote cluster server SSL if the port is enabled if (REMOTE_CLUSTER_SERVER_ENABLED.get(settings)) { sslSettingsMap.put(XPackSettings.REMOTE_CLUSTER_SERVER_SSL_PREFIX, getRemoteClusterServerSslSettings(settings)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java index d50f7bb27a5df..1f2c89c473a62 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java @@ -623,7 +623,7 @@ public Map getSnapshotCommitSup } @SuppressWarnings("unchecked") - protected List filterPlugins(Class type) { + private List filterPlugins(Class type) { return plugins.stream().filter(x -> type.isAssignableFrom(x.getClass())).map(p -> ((T) p)).collect(Collectors.toList()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java index bfac286bc3c35..9663e41a647a8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java @@ -614,8 +614,7 @@ public void testGetConfigurationByContextName() throws Exception { "xpack.security.authc.realms.ldap.realm1.ssl", "xpack.security.authc.realms.saml.realm2.ssl", "xpack.monitoring.exporters.mon1.ssl", - "xpack.monitoring.exporters.mon2.ssl", - "xpack.inference.elastic.http.ssl" }; + "xpack.monitoring.exporters.mon2.ssl" }; assumeTrue("Not enough cipher suites are available to support this test", getCipherSuites.length >= contextNames.length); diff --git a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java index c7b3a9d42f579..3b0fc869c8124 100644 --- a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java +++ b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java @@ -22,7 +22,6 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import org.elasticsearch.xpack.inference.Utils; import org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension; import org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension; @@ -59,7 +58,7 @@ public void setup() throws Exception { @Override protected Collection> nodePlugins() { - return Arrays.asList(LocalStateInferencePlugin.class); + return Arrays.asList(Utils.TestInferencePlugin.class); } public void testBulkOperations() throws Exception { diff --git a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java index d5c156d1d4f46..be6b3725b0f35 100644 --- a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java +++ b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java @@ -31,7 +31,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.inference.chunking.ChunkingSettingsTests; import org.elasticsearch.xpack.inference.registry.ModelRegistry; import org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalModel; @@ -76,7 +76,7 @@ public void createComponents() { @Override protected Collection> getPlugins() { - return pluginList(ReindexPlugin.class, LocalStateInferencePlugin.class); + return pluginList(ReindexPlugin.class, InferencePlugin.class); } public void testStoreModel() throws Exception { diff --git a/x-pack/plugin/inference/src/main/java/module-info.java b/x-pack/plugin/inference/src/main/java/module-info.java index 1c2240e8c5217..53974657e4e23 100644 --- a/x-pack/plugin/inference/src/main/java/module-info.java +++ b/x-pack/plugin/inference/src/main/java/module-info.java @@ -34,7 +34,6 @@ requires software.amazon.awssdk.retries.api; requires org.reactivestreams; requires org.elasticsearch.logging; - requires org.elasticsearch.sslconfig; exports org.elasticsearch.xpack.inference.action; exports org.elasticsearch.xpack.inference.registry; diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java index 93743a5485c2c..ea92b7d98fe30 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java @@ -28,7 +28,6 @@ import org.elasticsearch.indices.SystemIndexDescriptor; import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.InferenceServiceRegistry; -import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.node.PluginComponentBinding; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.ExtensiblePlugin; @@ -46,7 +45,6 @@ import org.elasticsearch.threadpool.ScalingExecutorBuilder; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xpack.core.ClientHelper; -import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction; import org.elasticsearch.xpack.core.inference.action.DeleteInferenceEndpointAction; import org.elasticsearch.xpack.core.inference.action.GetInferenceDiagnosticsAction; @@ -56,7 +54,6 @@ import org.elasticsearch.xpack.core.inference.action.PutInferenceModelAction; import org.elasticsearch.xpack.core.inference.action.UnifiedCompletionAction; import org.elasticsearch.xpack.core.inference.action.UpdateInferenceModelAction; -import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.inference.action.TransportDeleteInferenceEndpointAction; import org.elasticsearch.xpack.inference.action.TransportGetInferenceDiagnosticsAction; import org.elasticsearch.xpack.inference.action.TransportGetInferenceModelAction; @@ -121,6 +118,7 @@ import java.util.Map; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Collections.singletonList; @@ -154,7 +152,6 @@ public class InferencePlugin extends Plugin implements ActionPlugin, ExtensibleP private final Settings settings; private final SetOnce httpFactory = new SetOnce<>(); private final SetOnce amazonBedrockFactory = new SetOnce<>(); - private final SetOnce elasicInferenceServiceFactory = new SetOnce<>(); private final SetOnce serviceComponents = new SetOnce<>(); private final SetOnce elasticInferenceServiceComponents = new SetOnce<>(); private final SetOnce inferenceServiceRegistry = new SetOnce<>(); @@ -237,31 +234,31 @@ public Collection createComponents(PluginServices services) { var inferenceServices = new ArrayList<>(inferenceServiceExtensions); inferenceServices.add(this::getInferenceServiceFactories); - if (isElasticInferenceServiceEnabled()) { - // Create a separate instance of HTTPClientManager with its own SSL configuration (`xpack.inference.elastic.http.ssl.*`). - var elasticInferenceServiceHttpClientManager = HttpClientManager.create( - settings, - services.threadPool(), - services.clusterService(), - throttlerManager, - getSslService() - ); + // Set elasticInferenceUrl based on feature flags to support transitioning to the new Elastic Inference Service URL without exposing + // internal names like "eis" or "gateway". + ElasticInferenceServiceSettings inferenceServiceSettings = new ElasticInferenceServiceSettings(settings); + + String elasticInferenceUrl = null; - var elasticInferenceServiceRequestSenderFactory = new HttpRequestSender.Factory( - serviceComponents.get(), - elasticInferenceServiceHttpClientManager, - services.clusterService() + if (ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { + elasticInferenceUrl = inferenceServiceSettings.getElasticInferenceServiceUrl(); + } else if (DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { + log.warn( + "Deprecated flag {} detected for enabling {}. Please use {}.", + ELASTIC_INFERENCE_SERVICE_IDENTIFIER, + DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG, + ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG ); - elasicInferenceServiceFactory.set(elasticInferenceServiceRequestSenderFactory); + elasticInferenceUrl = inferenceServiceSettings.getEisGatewayUrl(); + } - ElasticInferenceServiceSettings inferenceServiceSettings = new ElasticInferenceServiceSettings(settings); - String elasticInferenceUrl = this.getElasticInferenceServiceUrl(inferenceServiceSettings); + if (elasticInferenceUrl != null) { elasticInferenceServiceComponents.set(new ElasticInferenceServiceComponents(elasticInferenceUrl)); inferenceServices.add( () -> List.of( context -> new ElasticInferenceService( - elasicInferenceServiceFactory.get(), + httpFactory.get(), serviceComponents.get(), elasticInferenceServiceComponents.get() ) @@ -384,21 +381,16 @@ public static ExecutorBuilder inferenceUtilityExecutor(Settings settings) { @Override public List> getSettings() { - ArrayList> settings = new ArrayList<>(); - settings.addAll(HttpSettings.getSettingsDefinitions()); - settings.addAll(HttpClientManager.getSettingsDefinitions()); - settings.addAll(ThrottlerManager.getSettingsDefinitions()); - settings.addAll(RetrySettings.getSettingsDefinitions()); - settings.addAll(Truncator.getSettingsDefinitions()); - settings.addAll(RequestExecutorServiceSettings.getSettingsDefinitions()); - settings.add(SKIP_VALIDATE_AND_START); - - // Register Elastic Inference Service settings definitions if the corresponding feature flag is enabled. - if (isElasticInferenceServiceEnabled()) { - settings.addAll(ElasticInferenceServiceSettings.getSettingsDefinitions()); - } - - return settings; + return Stream.of( + HttpSettings.getSettingsDefinitions(), + HttpClientManager.getSettingsDefinitions(), + ThrottlerManager.getSettingsDefinitions(), + RetrySettings.getSettingsDefinitions(), + ElasticInferenceServiceSettings.getSettingsDefinitions(), + Truncator.getSettingsDefinitions(), + RequestExecutorServiceSettings.getSettingsDefinitions(), + List.of(SKIP_VALIDATE_AND_START) + ).flatMap(Collection::stream).collect(Collectors.toList()); } @Override @@ -446,10 +438,7 @@ public List getQueryRewriteInterceptors() { @Override public List> getRetrievers() { return List.of( - new RetrieverSpec<>( - new ParseField(TextSimilarityRankBuilder.NAME), - (parser, context) -> TextSimilarityRankRetrieverBuilder.fromXContent(parser, context, getLicenseState()) - ), + new RetrieverSpec<>(new ParseField(TextSimilarityRankBuilder.NAME), TextSimilarityRankRetrieverBuilder::fromXContent), new RetrieverSpec<>(new ParseField(RandomRankBuilder.NAME), RandomRankRetrieverBuilder::fromXContent) ); } @@ -458,36 +447,4 @@ public List> getRetrievers() { public Map getHighlighters() { return Map.of(SemanticTextHighlighter.NAME, new SemanticTextHighlighter()); } - - // Get Elastic Inference service URL based on feature flags to support transitioning - // to the new Elastic Inference Service URL. - private String getElasticInferenceServiceUrl(ElasticInferenceServiceSettings settings) { - String elasticInferenceUrl = null; - - if (ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { - elasticInferenceUrl = settings.getElasticInferenceServiceUrl(); - } else if (DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { - log.warn( - "Deprecated flag {} detected for enabling {}. Please use {}.", - ELASTIC_INFERENCE_SERVICE_IDENTIFIER, - DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG, - ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG - ); - elasticInferenceUrl = settings.getEisGatewayUrl(); - } - - return elasticInferenceUrl; - } - - protected Boolean isElasticInferenceServiceEnabled() { - return (ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled() || DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()); - } - - protected SSLService getSslService() { - return XPackPlugin.getSharedSslService(); - } - - protected XPackLicenseState getLicenseState() { - return XPackPlugin.getSharedLicenseState(); - } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java index 6d09c9e67b363..e5d76b9bb5570 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java @@ -7,14 +7,9 @@ package org.elasticsearch.xpack.inference.external.http; -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager; import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor; import org.apache.http.impl.nio.reactor.IOReactorConfig; -import org.apache.http.nio.conn.NoopIOSessionStrategy; -import org.apache.http.nio.conn.SchemeIOSessionStrategy; -import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; import org.apache.http.nio.reactor.ConnectingIOReactor; import org.apache.http.nio.reactor.IOReactorException; import org.apache.http.pool.PoolStats; @@ -26,7 +21,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import java.io.Closeable; @@ -34,13 +28,11 @@ import java.util.List; import static org.elasticsearch.core.Strings.format; -import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceServiceSettings.ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX; public class HttpClientManager implements Closeable { private static final Logger logger = LogManager.getLogger(HttpClientManager.class); /** * The maximum number of total connections the connection pool can lease to all routes. - * The configuration applies to each instance of HTTPClientManager (max_total_connections=10 and instances=5 leads to 50 connections). * From googling around the connection pools maxTotal value should be close to the number of available threads. * * https://stackoverflow.com/questions/30989637/how-to-decide-optimal-settings-for-setmaxtotal-and-setdefaultmaxperroute @@ -55,7 +47,6 @@ public class HttpClientManager implements Closeable { /** * The max number of connections a single route can lease. - * This configuration applies to each instance of HttpClientManager. */ public static final Setting MAX_ROUTE_CONNECTIONS = Setting.intSetting( "xpack.inference.http.max_route_connections", @@ -107,22 +98,6 @@ public static HttpClientManager create( return new HttpClientManager(settings, connectionManager, threadPool, clusterService, throttlerManager); } - public static HttpClientManager create( - Settings settings, - ThreadPool threadPool, - ClusterService clusterService, - ThrottlerManager throttlerManager, - SSLService sslService - ) { - // Set the sslStrategy to ensure an encrypted connection, as Elastic Inference Service requires it. - SSLIOSessionStrategy sslioSessionStrategy = sslService.sslIOSessionStrategy( - sslService.getSSLConfiguration(ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX) - ); - - PoolingNHttpClientConnectionManager connectionManager = createConnectionManager(sslioSessionStrategy); - return new HttpClientManager(settings, connectionManager, threadPool, clusterService, throttlerManager); - } - // Default for testing HttpClientManager( Settings settings, @@ -146,25 +121,6 @@ public static HttpClientManager create( this.addSettingsUpdateConsumers(clusterService); } - private static PoolingNHttpClientConnectionManager createConnectionManager(SSLIOSessionStrategy sslStrategy) { - ConnectingIOReactor ioReactor; - try { - var configBuilder = IOReactorConfig.custom().setSoKeepAlive(true); - ioReactor = new DefaultConnectingIOReactor(configBuilder.build()); - } catch (IOReactorException e) { - var message = "Failed to initialize HTTP client manager with SSL."; - logger.error(message, e); - throw new ElasticsearchException(message, e); - } - - Registry registry = RegistryBuilder.create() - .register("http", NoopIOSessionStrategy.INSTANCE) - .register("https", sslStrategy) - .build(); - - return new PoolingNHttpClientConnectionManager(ioReactor, registry); - } - private static PoolingNHttpClientConnectionManager createConnectionManager() { ConnectingIOReactor ioReactor; try { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java index f54696895a818..fd2427dc8ac6a 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java @@ -12,7 +12,6 @@ import org.elasticsearch.features.NodeFeature; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.license.LicenseUtils; -import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.rank.RankDoc; import org.elasticsearch.search.retriever.CompoundRetrieverBuilder; @@ -22,6 +21,7 @@ import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xpack.core.XPackPlugin; import java.io.IOException; import java.util.List; @@ -73,11 +73,8 @@ public class TextSimilarityRankRetrieverBuilder extends CompoundRetrieverBuilder RetrieverBuilder.declareBaseParserFields(TextSimilarityRankBuilder.NAME, PARSER); } - public static TextSimilarityRankRetrieverBuilder fromXContent( - XContentParser parser, - RetrieverParserContext context, - XPackLicenseState licenceState - ) throws IOException { + public static TextSimilarityRankRetrieverBuilder fromXContent(XContentParser parser, RetrieverParserContext context) + throws IOException { if (context.clusterSupportsFeature(TEXT_SIMILARITY_RERANKER_RETRIEVER_SUPPORTED) == false) { throw new ParsingException(parser.getTokenLocation(), "unknown retriever [" + TextSimilarityRankBuilder.NAME + "]"); } @@ -86,7 +83,7 @@ public static TextSimilarityRankRetrieverBuilder fromXContent( "[text_similarity_reranker] retriever composition feature is not supported by all nodes in the cluster" ); } - if (TextSimilarityRankBuilder.TEXT_SIMILARITY_RERANKER_FEATURE.check(licenceState) == false) { + if (TextSimilarityRankBuilder.TEXT_SIMILARITY_RERANKER_FEATURE.check(XPackPlugin.getSharedLicenseState()) == false) { throw LicenseUtils.newComplianceException(TextSimilarityRankBuilder.NAME); } return PARSER.apply(parser, context); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java index 431a3647e2879..bc2daddc2a346 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java @@ -9,9 +9,7 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; -import java.util.ArrayList; import java.util.List; public class ElasticInferenceServiceSettings { @@ -19,8 +17,6 @@ public class ElasticInferenceServiceSettings { @Deprecated static final Setting EIS_GATEWAY_URL = Setting.simpleString("xpack.inference.eis.gateway.url", Setting.Property.NodeScope); - public static final String ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX = "xpack.inference.elastic.http.ssl."; - static final Setting ELASTIC_INFERENCE_SERVICE_URL = Setting.simpleString( "xpack.inference.elastic.url", Setting.Property.NodeScope @@ -35,27 +31,11 @@ public class ElasticInferenceServiceSettings { public ElasticInferenceServiceSettings(Settings settings) { eisGatewayUrl = EIS_GATEWAY_URL.get(settings); elasticInferenceServiceUrl = ELASTIC_INFERENCE_SERVICE_URL.get(settings); - } - - public static final SSLConfigurationSettings ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_SETTINGS = SSLConfigurationSettings.withPrefix( - ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX, - false - ); - public static final Setting ELASTIC_INFERENCE_SERVICE_SSL_ENABLED = Setting.boolSetting( - ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX + "enabled", - true, - Setting.Property.NodeScope - ); + } public static List> getSettingsDefinitions() { - ArrayList> settings = new ArrayList<>(); - settings.add(EIS_GATEWAY_URL); - settings.add(ELASTIC_INFERENCE_SERVICE_URL); - settings.add(ELASTIC_INFERENCE_SERVICE_SSL_ENABLED); - settings.addAll(ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_SETTINGS.getEnabledSettings()); - - return settings; + return List.of(EIS_GATEWAY_URL, ELASTIC_INFERENCE_SERVICE_URL); } @Deprecated diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java index 61033a0211065..bfec2d5ac3484 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.index.IndexService; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; -import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.inference.InferencePlugin; import org.hamcrest.Matchers; @@ -29,7 +28,7 @@ public class SemanticTextClusterMetadataTests extends ESSingleNodeTestCase { @Override protected Collection> getPlugins() { - return List.of(XPackPlugin.class, InferencePlugin.class); + return List.of(InferencePlugin.class); } public void testCreateIndexWithSemanticTextField() { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java deleted file mode 100644 index d1db5b8b12cc6..0000000000000 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.inference; - -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceServiceSettings; -import org.junit.After; -import org.junit.Before; - -import static org.hamcrest.Matchers.is; - -public class InferencePluginTests extends ESTestCase { - private InferencePlugin inferencePlugin; - - private Boolean elasticInferenceServiceEnabled = true; - - private void setElasticInferenceServiceEnabled(Boolean elasticInferenceServiceEnabled) { - this.elasticInferenceServiceEnabled = elasticInferenceServiceEnabled; - } - - @Before - public void setUp() throws Exception { - super.setUp(); - - Settings settings = Settings.builder().build(); - inferencePlugin = new InferencePlugin(settings) { - @Override - protected Boolean isElasticInferenceServiceEnabled() { - return elasticInferenceServiceEnabled; - } - }; - } - - @After - public void tearDown() throws Exception { - super.tearDown(); - } - - public void testElasticInferenceServiceSettingsPresent() throws Exception { - setElasticInferenceServiceEnabled(true); // enable elastic inference service - boolean anyMatch = inferencePlugin.getSettings() - .stream() - .map(Setting::getKey) - .anyMatch(key -> key.startsWith(ElasticInferenceServiceSettings.ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX)); - - assertThat("xpack.inference.elastic settings are present", anyMatch, is(true)); - } - - public void testElasticInferenceServiceSettingsNotPresent() throws Exception { - setElasticInferenceServiceEnabled(false); // disable elastic inference service - boolean noneMatch = inferencePlugin.getSettings() - .stream() - .map(Setting::getKey) - .noneMatch(key -> key.startsWith(ElasticInferenceServiceSettings.ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX)); - - assertThat("xpack.inference.elastic settings are not present", noneMatch, is(true)); - } -} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java deleted file mode 100644 index 68ea175bd9870..0000000000000 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.inference; - -import org.elasticsearch.action.support.MappedActionFilter; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.mapper.Mapper; -import org.elasticsearch.inference.InferenceServiceExtension; -import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.plugins.SearchPlugin; -import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; -import org.elasticsearch.xpack.core.ssl.SSLService; -import org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension; -import org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension; - -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import static java.util.stream.Collectors.toList; - -public class LocalStateInferencePlugin extends LocalStateCompositeXPackPlugin { - private final InferencePlugin inferencePlugin; - - public LocalStateInferencePlugin(final Settings settings, final Path configPath) throws Exception { - super(settings, configPath); - LocalStateInferencePlugin thisVar = this; - this.inferencePlugin = new InferencePlugin(settings) { - @Override - protected SSLService getSslService() { - return thisVar.getSslService(); - } - - @Override - protected XPackLicenseState getLicenseState() { - return thisVar.getLicenseState(); - } - - @Override - public List getInferenceServiceFactories() { - return List.of( - TestSparseInferenceServiceExtension.TestInferenceService::new, - TestDenseInferenceServiceExtension.TestInferenceService::new - ); - } - }; - plugins.add(inferencePlugin); - } - - @Override - public List> getRetrievers() { - return this.filterPlugins(SearchPlugin.class).stream().flatMap(p -> p.getRetrievers().stream()).collect(toList()); - } - - @Override - public Map getMappers() { - return inferencePlugin.getMappers(); - } - - @Override - public Collection getMappedActionFilters() { - return inferencePlugin.getMappedActionFilters(); - } - -} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java index 0f322e64755be..9395ae222e9ba 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; +import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -142,6 +143,20 @@ private static void blockingCall( latch.await(); } + public static class TestInferencePlugin extends InferencePlugin { + public TestInferencePlugin(Settings settings) { + super(settings); + } + + @Override + public List getInferenceServiceFactories() { + return List.of( + TestSparseInferenceServiceExtension.TestInferenceService::new, + TestDenseInferenceServiceExtension.TestInferenceService::new + ); + } + } + public static Model getInvalidModel(String inferenceEntityId, String serviceName) { var mockConfigs = mock(ModelConfigurations.class); when(mockConfigs.getInferenceEntityId()).thenReturn(inferenceEntityId); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java index 24183b21f73e7..1f58c4165056d 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.index.mapper.NonDynamicFieldMapperTests; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import org.elasticsearch.xpack.inference.Utils; import org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension; import org.junit.Before; @@ -27,7 +26,7 @@ public void setup() throws Exception { @Override protected Collection> getPlugins() { - return List.of(LocalStateInferencePlugin.class); + return List.of(Utils.TestInferencePlugin.class); } @Override diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java index daed03c198e0d..6d6403b69ea11 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java @@ -10,7 +10,7 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.rank.RankBuilder; import org.elasticsearch.search.rank.rerank.AbstractRerankerIT; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; +import org.elasticsearch.xpack.inference.InferencePlugin; import java.util.Collection; import java.util.List; @@ -40,7 +40,7 @@ protected RankBuilder getThrowingRankBuilder(int rankWindowSize, String rankFeat @Override protected Collection> pluginsNeeded() { - return List.of(LocalStateInferencePlugin.class, TextSimilarityTestPlugin.class); + return List.of(InferencePlugin.class, TextSimilarityTestPlugin.class); } public void testQueryPhaseShardThrowingAllShardsFail() throws Exception { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java index ba6924ba0ff3b..084a7f3de4a53 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java @@ -24,7 +24,8 @@ import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; +import org.elasticsearch.xpack.core.XPackPlugin; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.junit.Before; import java.io.IOException; @@ -46,7 +47,7 @@ protected boolean addMockHttpTransport() { @Override protected Collection> nodePlugins() { - return List.of(LocalStateInferencePlugin.class, TextSimilarityTestPlugin.class); + return List.of(InferencePlugin.class, XPackPlugin.class, TextSimilarityTestPlugin.class); } @Override diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java index f81f2965c392e..a042fca44fdb5 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java @@ -20,7 +20,7 @@ import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.hamcrest.ElasticsearchAssertions; import org.elasticsearch.xpack.core.inference.action.InferenceAction; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.junit.Before; import java.util.Collection; @@ -108,7 +108,7 @@ protected InferenceAction.Request generateRequest(List docFeatures) { @Override protected Collection> getPlugins() { - return List.of(LocalStateInferencePlugin.class, TextSimilarityTestPlugin.class); + return List.of(InferencePlugin.class, TextSimilarityTestPlugin.class); } @Before diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java index ff1a1d19779df..bab012afc3101 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java @@ -27,7 +27,6 @@ import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.core.rollup.action.GetRollupIndexCapsAction; import org.elasticsearch.xpack.core.ssl.SSLService; -import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.monitoring.Monitoring; import org.elasticsearch.xpack.security.Security; @@ -87,12 +86,6 @@ protected XPackLicenseState getLicenseState() { } }); plugins.add(new MockedRollupPlugin()); - plugins.add(new InferencePlugin(settings) { - @Override - protected SSLService getSslService() { - return thisVar.getSslService(); - } - }); } @Override diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java index 5cf15454e47f2..aeebfabdce704 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java @@ -82,6 +82,7 @@ import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.DataCounts; import org.elasticsearch.xpack.core.ml.utils.MlTaskState; import org.elasticsearch.xpack.ilm.IndexLifecycle; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.ml.LocalStateMachineLearning; import org.elasticsearch.xpack.ml.MachineLearning; import org.elasticsearch.xpack.ml.MlSingleNodeTestCase; @@ -160,7 +161,8 @@ protected Collection> nodePlugins() { DataStreamsPlugin.class, // To remove errors from parsing build in templates that contain scaled_float MapperExtrasPlugin.class, - Wildcard.class + Wildcard.class, + InferencePlugin.class ); } diff --git a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java index 057ebdece5c61..ab5be0f48f5f3 100644 --- a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java +++ b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.xpack.frozen.FrozenIndices; import org.elasticsearch.xpack.graph.Graph; import org.elasticsearch.xpack.ilm.IndexLifecycle; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.profiling.ProfilingPlugin; import org.elasticsearch.xpack.rollup.Rollup; import org.elasticsearch.xpack.search.AsyncSearch; @@ -88,6 +89,7 @@ protected Collection> getPlugins() { FrozenIndices.class, Graph.class, IndexLifecycle.class, + InferencePlugin.class, IngestCommonPlugin.class, IngestTestPlugin.class, MustachePlugin.class, From e6a27a91eeb4b1876287ce80cffc4efa7c4f3448 Mon Sep 17 00:00:00 2001 From: Alexis Charveriat Date: Mon, 16 Dec 2024 16:34:41 +0100 Subject: [PATCH 011/119] tier_preference and creation_date fields in monitoring template (#117851) * tier_preference and creation_date fields in monitoring template * Added index version * Update docs/changelog/117851.yaml * Updated changelog * Incremented STACK_MONITORING_REGISTRY_VERSION --- docs/changelog/117851.yaml | 5 +++++ .../src/main/resources/monitoring-es-mb.json | 11 +++++++++++ .../xpack/monitoring/MonitoringTemplateRegistry.java | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/117851.yaml diff --git a/docs/changelog/117851.yaml b/docs/changelog/117851.yaml new file mode 100644 index 0000000000000..21888cd6fb80f --- /dev/null +++ b/docs/changelog/117851.yaml @@ -0,0 +1,5 @@ +pr: 117851 +summary: Addition of `tier_preference`, `creation_date` and `version` fields in Elasticsearch monitoring template +area: Monitoring +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/template-resources/src/main/resources/monitoring-es-mb.json b/x-pack/plugin/core/template-resources/src/main/resources/monitoring-es-mb.json index 2bf7607e86d32..793a8c3035d8e 100644 --- a/x-pack/plugin/core/template-resources/src/main/resources/monitoring-es-mb.json +++ b/x-pack/plugin/core/template-resources/src/main/resources/monitoring-es-mb.json @@ -1517,6 +1517,17 @@ "ignore_above": 1024, "type": "keyword" }, + "tier_preference": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, "recovery": { "properties": { "stop_time": { diff --git a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/MonitoringTemplateRegistry.java b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/MonitoringTemplateRegistry.java index e0433ea6fdd71..cfd322d04e92f 100644 --- a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/MonitoringTemplateRegistry.java +++ b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/MonitoringTemplateRegistry.java @@ -77,7 +77,7 @@ public class MonitoringTemplateRegistry extends IndexTemplateRegistry { * writes monitoring data in ECS format as of 8.0. These templates define the ECS schema as well as alias fields for the old monitoring * mappings that point to the corresponding ECS fields. */ - public static final int STACK_MONITORING_REGISTRY_VERSION = 8_00_00_99 + 18; + public static final int STACK_MONITORING_REGISTRY_VERSION = 8_00_00_99 + 19; private static final String STACK_MONITORING_REGISTRY_VERSION_VARIABLE = "xpack.stack.monitoring.template.release.version"; private static final String STACK_TEMPLATE_VERSION = "8"; private static final String STACK_TEMPLATE_VERSION_VARIABLE = "xpack.stack.monitoring.template.version"; From bded35bb39c4977c8255c2ffc3e06a7a9fa9b1dc Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Mon, 16 Dec 2024 17:14:16 +0100 Subject: [PATCH 012/119] ESQL: Add LOOKUP JOIN tests with null and mv join keys (#118761) Notable behavior: - `null` join keys never match anything, even a `null` from the left hand side. - If a lookup index document has multi-valued join keys, it will match with all left hand side rows that contain any of the multi-values. - If the left hand side has multi-values, it will match with any lookup index document whose join key contains any of the multi-values. --- .../resources/languages_non_unique_key.csv | 4 ++ .../src/main/resources/lookup-join.csv-spec | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/languages_non_unique_key.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/languages_non_unique_key.csv index 1578762f8d1cb..d6381b174d739 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/languages_non_unique_key.csv +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/languages_non_unique_key.csv @@ -8,3 +8,7 @@ language_code:integer,language_name:keyword,country:keyword 2,German, 4,Quenya, 5,,Atlantis +[6,7],Mv-Lang,Mv-Land +[7,8],Mv-Lang2,Mv-Land2 +,Null-Lang,Null-Land +,Null-Lang2,Null-Land2 diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec index c39f4ae7b4e0c..7fed4f377096f 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec @@ -178,6 +178,73 @@ language_code:integer | language_name:keyword | country:keyword 2 | [German, German, German] | [Austria, Germany, Switzerland] ; +nullJoinKeyOnTheDataNode +required_capability: join_lookup_v5 + +FROM employees +| WHERE emp_no < 10004 +| EVAL language_code = emp_no % 10, language_code = CASE(language_code == 3, null, language_code) +| LOOKUP JOIN languages_lookup_non_unique_key ON language_code +| SORT emp_no +| KEEP emp_no, language_code, language_name +; + +emp_no:integer | language_code:integer | language_name:keyword +10001 | 1 | [English, English, English] +10002 | 2 | [German, German, German] +10003 | null | null +; + + +mvJoinKeyOnTheDataNode +required_capability: join_lookup_v5 + +FROM employees +| WHERE 10003 < emp_no AND emp_no < 10008 +| EVAL language_code = emp_no % 10 +| LOOKUP JOIN languages_lookup_non_unique_key ON language_code +| SORT emp_no +| KEEP emp_no, language_code, language_name +; + +emp_no:integer | language_code:integer | language_name:keyword +10004 | 4 | Quenya +10005 | 5 | null +10006 | 6 | Mv-Lang +10007 | 7 | [Mv-Lang, Mv-Lang2] +; + +mvJoinKeyFromRow +required_capability: join_lookup_v5 + +ROW language_code = [4, 5, 6, 7] +| LOOKUP JOIN languages_lookup_non_unique_key ON language_code +| EVAL language_name = MV_SORT(language_name), country = MV_SORT(country) +| KEEP language_code, language_name, country +; + +language_code:integer | language_name:keyword | country:keyword +[4, 5, 6, 7] | [Mv-Lang, Mv-Lang2, Quenya] | [Atlantis, Mv-Land, Mv-Land2] +; + +mvJoinKeyFromRowExpanded +required_capability: join_lookup_v5 + +ROW language_code = [4, 5, 6, 7, 8] +| MV_EXPAND language_code +| LOOKUP JOIN languages_lookup_non_unique_key ON language_code +| EVAL language_name = MV_SORT(language_name), country = MV_SORT(country) +| KEEP language_code, language_name, country +; + +language_code:integer | language_name:keyword | country:keyword +4 | Quenya | null +5 | null | Atlantis +6 | Mv-Lang | Mv-Land +7 | [Mv-Lang, Mv-Lang2] | [Mv-Land, Mv-Land2] +8 | Mv-Lang2 | Mv-Land2 +; + lookupIPFromRow required_capability: join_lookup_v5 From 693bb794bce43eb20d07b08ceba5e796ded6efbd Mon Sep 17 00:00:00 2001 From: Salvatore Campagna <93581129+salvatore-campagna@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:27:45 +0100 Subject: [PATCH 013/119] Expose shard changes action as a rest api (#118608) This PR adds a new REST API `/{index}/ccr/shard_changes` to retrieve shard-level changes, including translog operations, mapping versions, and sequence numbers. It is required for a new set of Rally benchmarks, which will be used to evaluate the impact of synthetic source on recovery time. The API accepts parameters like `from_seq_no`, `max_batch_size`, `poll_timeout`, and `max_operations_count` and is exposed only in snapshot builds. --- x-pack/plugin/ccr/build.gradle | 11 + .../xpack/ccr/rest/ShardChangesRestIT.java | 281 ++++++++++++++++ .../java/org/elasticsearch/xpack/ccr/Ccr.java | 50 +-- .../ccr/rest/RestShardChangesAction.java | 300 ++++++++++++++++++ 4 files changed, 623 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugin/ccr/src/javaRestTest/java/org/elasticsearch/xpack/ccr/rest/ShardChangesRestIT.java create mode 100644 x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestShardChangesAction.java diff --git a/x-pack/plugin/ccr/build.gradle b/x-pack/plugin/ccr/build.gradle index f673513950bb4..b5e96ac2a8b34 100644 --- a/x-pack/plugin/ccr/build.gradle +++ b/x-pack/plugin/ccr/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'elasticsearch.internal-es-plugin' apply plugin: 'elasticsearch.internal-cluster-test' +apply plugin: 'elasticsearch.internal-java-rest-test' esplugin { name 'x-pack-ccr' description 'Elasticsearch Expanded Pack Plugin - CCR' @@ -33,6 +34,16 @@ tasks.named('internalClusterTestTestingConventions').configure { baseClass 'org.elasticsearch.test.ESIntegTestCase' } +tasks.named("javaRestTest").configure { + usesDefaultDistribution() +} + +restResources { + restApi { + include 'bulk', 'search', '_common', 'indices', 'index', 'cluster', 'data_stream' + } +} + addQaCheckDependencies(project) dependencies { diff --git a/x-pack/plugin/ccr/src/javaRestTest/java/org/elasticsearch/xpack/ccr/rest/ShardChangesRestIT.java b/x-pack/plugin/ccr/src/javaRestTest/java/org/elasticsearch/xpack/ccr/rest/ShardChangesRestIT.java new file mode 100644 index 0000000000000..e5dfea7b772f2 --- /dev/null +++ b/x-pack/plugin/ccr/src/javaRestTest/java/org/elasticsearch/xpack/ccr/rest/ShardChangesRestIT.java @@ -0,0 +1,281 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.ccr.rest; + +import org.apache.http.util.EntityUtils; +import org.elasticsearch.Build; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xcontent.json.JsonXContent; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.ClassRule; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class ShardChangesRestIT extends ESRestTestCase { + private static final String CCR_SHARD_CHANGES_ENDPOINT = "/%s/ccr/shard_changes"; + private static final String BULK_INDEX_ENDPOINT = "/%s/_bulk"; + + private static final String[] SHARD_RESPONSE_FIELDS = new String[] { + "took_in_millis", + "operations", + "shard_id", + "index", + "settings_version", + "max_seq_no_of_updates_or_deletes", + "number_of_operations", + "mapping_version", + "aliases_version", + "max_seq_no", + "global_checkpoint" }; + private static final String[] NAMES = { "skywalker", "leia", "obi-wan", "yoda", "chewbacca", "r2-d2", "c-3po", "darth-vader" }; + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .distribution(DistributionType.DEFAULT) + .setting("xpack.security.enabled", "false") + .setting("xpack.license.self_generated.type", "trial") + .build(); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + + @Before + public void assumeSnapshotBuild() { + assumeTrue("/{index}/ccr/shard_changes endpoint only available in snapshot builds", Build.current().isSnapshot()); + } + + public void testShardChangesNoOperation() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + createIndex( + indexName, + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexSettings.INDEX_TRANSLOG_SYNC_INTERVAL_SETTING.getKey(), "1s") + .build() + ); + assertTrue(indexExists(indexName)); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); + assertOK(client().performRequest(shardChangesRequest)); + } + + public void testShardChangesDefaultParams() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + final Settings settings = Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexSettings.INDEX_TRANSLOG_SYNC_INTERVAL_SETTING.getKey(), "1s") + .build(); + final String mappings = """ + { + "properties": { + "name": { + "type": "keyword" + } + } + } + """; + createIndex(indexName, settings, mappings); + assertTrue(indexExists(indexName)); + + assertOK(client().performRequest(bulkRequest(indexName, randomIntBetween(10, 20)))); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); + final Response response = client().performRequest(shardChangesRequest); + assertOK(response); + assertShardChangesResponse( + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false) + ); + } + + public void testShardChangesWithAllParameters() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + createIndex( + indexName, + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexSettings.INDEX_TRANSLOG_SYNC_INTERVAL_SETTING.getKey(), "1s") + .build() + ); + assertTrue(indexExists(indexName)); + + assertOK(client().performRequest(bulkRequest(indexName, randomIntBetween(100, 200)))); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); + shardChangesRequest.addParameter("from_seq_no", "0"); + shardChangesRequest.addParameter("max_operations_count", "1"); + shardChangesRequest.addParameter("poll_timeout", "10s"); + shardChangesRequest.addParameter("max_batch_size", "1MB"); + + final Response response = client().performRequest(shardChangesRequest); + assertOK(response); + assertShardChangesResponse( + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false) + ); + } + + public void testShardChangesMultipleRequests() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + createIndex( + indexName, + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexSettings.INDEX_TRANSLOG_SYNC_INTERVAL_SETTING.getKey(), "1s") + .build() + ); + assertTrue(indexExists(indexName)); + + assertOK(client().performRequest(bulkRequest(indexName, randomIntBetween(100, 200)))); + + final Request firstRequest = new Request("GET", shardChangesEndpoint(indexName)); + firstRequest.addParameter("from_seq_no", "0"); + firstRequest.addParameter("max_operations_count", "10"); + firstRequest.addParameter("poll_timeout", "10s"); + firstRequest.addParameter("max_batch_size", "1MB"); + + final Response firstResponse = client().performRequest(firstRequest); + assertOK(firstResponse); + assertShardChangesResponse( + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(firstResponse.getEntity()), false) + ); + + final Request secondRequest = new Request("GET", shardChangesEndpoint(indexName)); + secondRequest.addParameter("from_seq_no", "10"); + secondRequest.addParameter("max_operations_count", "10"); + secondRequest.addParameter("poll_timeout", "10s"); + secondRequest.addParameter("max_batch_size", "1MB"); + + final Response secondResponse = client().performRequest(secondRequest); + assertOK(secondResponse); + assertShardChangesResponse( + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(secondResponse.getEntity()), false) + ); + } + + public void testShardChangesInvalidFromSeqNo() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + createIndex(indexName); + assertTrue(indexExists(indexName)); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); + shardChangesRequest.addParameter("from_seq_no", "-1"); + final ResponseException ex = assertThrows(ResponseException.class, () -> client().performRequest(shardChangesRequest)); + assertResponseException(ex, RestStatus.BAD_REQUEST, "Validation Failed: 1: fromSeqNo [-1] cannot be lower than 0"); + } + + public void testShardChangesInvalidMaxOperationsCount() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + createIndex(indexName); + assertTrue(indexExists(indexName)); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); + shardChangesRequest.addParameter("max_operations_count", "-1"); + final ResponseException ex = assertThrows(ResponseException.class, () -> client().performRequest(shardChangesRequest)); + assertResponseException(ex, RestStatus.BAD_REQUEST, "Validation Failed: 1: maxOperationCount [-1] cannot be lower than 0"); + } + + public void testShardChangesNegativePollTimeout() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + createIndex(indexName); + assertTrue(indexExists(indexName)); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); + shardChangesRequest.addParameter("poll_timeout", "-1s"); + assertOK(client().performRequest(shardChangesRequest)); + } + + public void testShardChangesInvalidMaxBatchSize() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + createIndex(indexName); + assertTrue(indexExists(indexName)); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); + shardChangesRequest.addParameter("max_batch_size", "-1MB"); + final ResponseException ex = assertThrows(ResponseException.class, () -> client().performRequest(shardChangesRequest)); + assertResponseException( + ex, + RestStatus.BAD_REQUEST, + "failed to parse setting [max_batch_size] with value [-1MB] as a size in bytes" + ); + } + + public void testShardChangesMissingIndex() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + assertFalse(indexExists(indexName)); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); + final ResponseException ex = assertThrows(ResponseException.class, () -> client().performRequest(shardChangesRequest)); + assertResponseException(ex, RestStatus.BAD_REQUEST, "Failed to process shard changes for index [" + indexName + "]"); + } + + private static Request bulkRequest(final String indexName, int numberOfDocuments) { + final StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < numberOfDocuments; i++) { + sb.append(String.format(Locale.ROOT, "{ \"index\": { \"_id\": \"%d\" } }\n{ \"name\": \"%s\" }\n", i + 1, randomFrom(NAMES))); + } + + final Request request = new Request("POST", bulkEndpoint(indexName)); + request.setJsonEntity(sb.toString()); + request.addParameter("refresh", "true"); + return request; + } + + private static String shardChangesEndpoint(final String indexName) { + return String.format(Locale.ROOT, CCR_SHARD_CHANGES_ENDPOINT, indexName); + } + + private static String bulkEndpoint(final String indexName) { + return String.format(Locale.ROOT, BULK_INDEX_ENDPOINT, indexName); + } + + private void assertResponseException(final ResponseException ex, final RestStatus restStatus, final String error) { + assertEquals(restStatus.getStatus(), ex.getResponse().getStatusLine().getStatusCode()); + assertThat(ex.getMessage(), Matchers.containsString(error)); + } + + private void assertShardChangesResponse(final Map shardChangesResponseBody) { + for (final String fieldName : SHARD_RESPONSE_FIELDS) { + final Object fieldValue = shardChangesResponseBody.get(fieldName); + assertNotNull("Field " + fieldName + " is missing or has a null value.", fieldValue); + + if ("operations".equals(fieldName)) { + if (fieldValue instanceof List operationsList) { + assertFalse("Field 'operations' is empty.", operationsList.isEmpty()); + + for (final Object operation : operationsList) { + assertNotNull("Operation is null.", operation); + if (operation instanceof Map operationMap) { + assertNotNull("seq_no is missing in operation.", operationMap.get("seq_no")); + assertNotNull("op_type is missing in operation.", operationMap.get("op_type")); + assertNotNull("primary_term is missing in operation.", operationMap.get("primary_term")); + } + } + } + } + } + } +} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 87a4c2c7d4826..5305e179058b2 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.ccr; import org.apache.lucene.util.SetOnce; +import org.elasticsearch.Build; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; import org.elasticsearch.action.ActionRequest; @@ -91,6 +92,7 @@ import org.elasticsearch.xpack.ccr.rest.RestPutFollowAction; import org.elasticsearch.xpack.ccr.rest.RestResumeAutoFollowPatternAction; import org.elasticsearch.xpack.ccr.rest.RestResumeFollowAction; +import org.elasticsearch.xpack.ccr.rest.RestShardChangesAction; import org.elasticsearch.xpack.ccr.rest.RestUnfollowAction; import org.elasticsearch.xpack.core.XPackFeatureUsage; import org.elasticsearch.xpack.core.XPackField; @@ -112,6 +114,7 @@ import org.elasticsearch.xpack.core.ccr.action.ShardFollowTask; import org.elasticsearch.xpack.core.ccr.action.UnfollowAction; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -140,7 +143,34 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E public static final String REQUESTED_OPS_MISSING_METADATA_KEY = "es.requested_operations_missing"; public static final TransportVersion TRANSPORT_VERSION_ACTION_WITH_SHARD_ID = TransportVersions.V_8_9_X; + private static final List BASE_REST_HANDLERS = Arrays.asList( + // stats API + new RestFollowStatsAction(), + new RestCcrStatsAction(), + new RestFollowInfoAction(), + // follow APIs + new RestPutFollowAction(), + new RestResumeFollowAction(), + new RestPauseFollowAction(), + new RestUnfollowAction(), + // auto-follow APIs + new RestDeleteAutoFollowPatternAction(), + new RestPutAutoFollowPatternAction(), + new RestGetAutoFollowPatternAction(), + new RestPauseAutoFollowPatternAction(), + new RestResumeAutoFollowPatternAction(), + // forget follower API + new RestForgetFollowerAction() + ); + private static final List REST_HANDLERS = Collections.unmodifiableList(BASE_REST_HANDLERS); + + private static final List SNAPSHOT_BUILD_REST_HANDLERS; + static { + List snapshotBuildHandlers = new ArrayList<>(BASE_REST_HANDLERS); + snapshotBuildHandlers.add(new RestShardChangesAction()); + SNAPSHOT_BUILD_REST_HANDLERS = Collections.unmodifiableList(snapshotBuildHandlers); + } private final boolean enabled; private final Settings settings; private final CcrLicenseChecker ccrLicenseChecker; @@ -272,25 +302,7 @@ public List getRestHandlers( return emptyList(); } - return Arrays.asList( - // stats API - new RestFollowStatsAction(), - new RestCcrStatsAction(), - new RestFollowInfoAction(), - // follow APIs - new RestPutFollowAction(), - new RestResumeFollowAction(), - new RestPauseFollowAction(), - new RestUnfollowAction(), - // auto-follow APIs - new RestDeleteAutoFollowPatternAction(), - new RestPutAutoFollowPatternAction(), - new RestGetAutoFollowPatternAction(), - new RestPauseAutoFollowPatternAction(), - new RestResumeAutoFollowPatternAction(), - // forget follower API - new RestForgetFollowerAction() - ); + return Build.current().isSnapshot() ? SNAPSHOT_BUILD_REST_HANDLERS : REST_HANDLERS; } public List getNamedWriteables() { diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestShardChangesAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestShardChangesAction.java new file mode 100644 index 0000000000000..84171ebce162f --- /dev/null +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestShardChangesAction.java @@ -0,0 +1,300 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.ccr.rest; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.admin.indices.stats.ShardStats; +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.engine.Engine; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.index.translog.Translog; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestActionListener; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xpack.ccr.Ccr; +import org.elasticsearch.xpack.ccr.action.ShardChangesAction; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +/** + * A REST handler that retrieves shard changes in a specific index whose name is provided as a parameter. + * It handles GET requests to the "/{index}/ccr/shard_changes" endpoint retrieving shard-level changes, + * such as translog operations, mapping version, settings version, aliases version, the global checkpoint, + * maximum sequence number and maximum sequence number of updates or deletes. + *

+ * Note: This handler is only available for snapshot builds. + */ +public class RestShardChangesAction extends BaseRestHandler { + + private static final long DEFAULT_FROM_SEQ_NO = 0L; + private static final ByteSizeValue DEFAULT_MAX_BATCH_SIZE = new ByteSizeValue(32, ByteSizeUnit.MB); + private static final TimeValue DEFAULT_POLL_TIMEOUT = new TimeValue(1, TimeUnit.MINUTES); + private static final int DEFAULT_MAX_OPERATIONS_COUNT = 1024; + private static final int DEFAULT_TIMEOUT_SECONDS = 60; + private static final TimeValue GET_INDEX_UUID_TIMEOUT = new TimeValue(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + private static final TimeValue SHARD_STATS_TIMEOUT = new TimeValue(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + private static final String INDEX_PARAM_NAME = "index"; + private static final String FROM_SEQ_NO_PARAM_NAME = "from_seq_no"; + private static final String MAX_BATCH_SIZE_PARAM_NAME = "max_batch_size"; + private static final String POLL_TIMEOUT_PARAM_NAME = "poll_timeout"; + private static final String MAX_OPERATIONS_COUNT_PARAM_NAME = "max_operations_count"; + + @Override + public String getName() { + return "ccr_shard_changes_action"; + } + + @Override + public List routes() { + return List.of(new Route(GET, "/{index}/ccr/shard_changes")); + } + + /** + * Prepares the request for retrieving shard changes. + * + * @param restRequest The REST request. + * @param client The NodeClient for executing the request. + * @return A RestChannelConsumer for handling the request. + * @throws IOException If an error occurs while preparing the request. + */ + @Override + protected RestChannelConsumer prepareRequest(final RestRequest restRequest, final NodeClient client) throws IOException { + final var indexName = restRequest.param(INDEX_PARAM_NAME); + final var fromSeqNo = restRequest.paramAsLong(FROM_SEQ_NO_PARAM_NAME, DEFAULT_FROM_SEQ_NO); + final var maxBatchSize = restRequest.paramAsSize(MAX_BATCH_SIZE_PARAM_NAME, DEFAULT_MAX_BATCH_SIZE); + final var pollTimeout = restRequest.paramAsTime(POLL_TIMEOUT_PARAM_NAME, DEFAULT_POLL_TIMEOUT); + final var maxOperationsCount = restRequest.paramAsInt(MAX_OPERATIONS_COUNT_PARAM_NAME, DEFAULT_MAX_OPERATIONS_COUNT); + + final CompletableFuture indexUUIDCompletableFuture = asyncGetIndexUUID( + client, + indexName, + client.threadPool().executor(Ccr.CCR_THREAD_POOL_NAME) + ); + final CompletableFuture shardStatsCompletableFuture = asyncShardStats( + client, + indexName, + client.threadPool().executor(Ccr.CCR_THREAD_POOL_NAME) + ); + + return channel -> CompletableFuture.allOf(indexUUIDCompletableFuture, shardStatsCompletableFuture).thenRun(() -> { + try { + final String indexUUID = indexUUIDCompletableFuture.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + final ShardStats shardStats = shardStatsCompletableFuture.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + final ShardId shardId = shardStats.getShardRouting().shardId(); + final String expectedHistoryUUID = shardStats.getCommitStats().getUserData().get(Engine.HISTORY_UUID_KEY); + + final ShardChangesAction.Request shardChangesRequest = shardChangesRequest( + indexName, + indexUUID, + shardId, + expectedHistoryUUID, + fromSeqNo, + maxBatchSize, + pollTimeout, + maxOperationsCount + ); + client.execute(ShardChangesAction.INSTANCE, shardChangesRequest, new RestActionListener<>(channel) { + @Override + protected void processResponse(final ShardChangesAction.Response response) { + channel.sendResponse(new RestResponse(RestStatus.OK, shardChangesResponseToXContent(response, indexName, shardId))); + } + }); + + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Error while retrieving shard changes", e); + } catch (TimeoutException te) { + throw new IllegalStateException("Timeout while waiting for shard stats or index UUID", te); + } + }).exceptionally(ex -> { + channel.sendResponse(new RestResponse(RestStatus.BAD_REQUEST, "Failed to process shard changes for index [" + indexName + "]")); + return null; + }); + } + + /** + * Creates a ShardChangesAction.Request object with the provided parameters. + * + * @param indexName The name of the index for which to retrieve shard changes. + * @param indexUUID The UUID of the index. + * @param shardId The ShardId for which to retrieve shard changes. + * @param expectedHistoryUUID The expected history UUID of the shard. + * @param fromSeqNo The sequence number from which to start retrieving shard changes. + * @param maxBatchSize The maximum size of a batch of operations to retrieve. + * @param pollTimeout The maximum time to wait for shard changes. + * @param maxOperationsCount The maximum number of operations to retrieve in a single request. + * @return A ShardChangesAction.Request object with the provided parameters. + */ + private static ShardChangesAction.Request shardChangesRequest( + final String indexName, + final String indexUUID, + final ShardId shardId, + final String expectedHistoryUUID, + long fromSeqNo, + final ByteSizeValue maxBatchSize, + final TimeValue pollTimeout, + int maxOperationsCount + ) { + final ShardChangesAction.Request shardChangesRequest = new ShardChangesAction.Request( + new ShardId(new Index(indexName, indexUUID), shardId.id()), + expectedHistoryUUID + ); + shardChangesRequest.setFromSeqNo(fromSeqNo); + shardChangesRequest.setMaxBatchSize(maxBatchSize); + shardChangesRequest.setPollTimeout(pollTimeout); + shardChangesRequest.setMaxOperationCount(maxOperationsCount); + return shardChangesRequest; + } + + /** + * Converts the response to XContent JSOn format. + * + * @param response The ShardChangesAction response. + * @param indexName The name of the index. + * @param shardId The ShardId. + */ + private static XContentBuilder shardChangesResponseToXContent( + final ShardChangesAction.Response response, + final String indexName, + final ShardId shardId + ) { + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + builder.startObject(); + builder.field("index", indexName); + builder.field("shard_id", shardId); + builder.field("mapping_version", response.getMappingVersion()); + builder.field("settings_version", response.getSettingsVersion()); + builder.field("aliases_version", response.getAliasesVersion()); + builder.field("global_checkpoint", response.getGlobalCheckpoint()); + builder.field("max_seq_no", response.getMaxSeqNo()); + builder.field("max_seq_no_of_updates_or_deletes", response.getMaxSeqNoOfUpdatesOrDeletes()); + builder.field("took_in_millis", response.getTookInMillis()); + if (response.getOperations() != null && response.getOperations().length > 0) { + operationsToXContent(response, builder); + } + builder.endObject(); + + return builder; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Converts the operations from a ShardChangesAction response to XContent JSON format. + * + * @param response The ShardChangesAction response containing the operations to be converted. + * @param builder The XContentBuilder to which the converted operations will be added. + * @throws IOException If an error occurs while writing to the XContentBuilder. + */ + private static void operationsToXContent(final ShardChangesAction.Response response, final XContentBuilder builder) throws IOException { + builder.field("number_of_operations", response.getOperations().length); + builder.field("operations"); + builder.startArray(); + for (final Translog.Operation operation : response.getOperations()) { + builder.startObject(); + builder.field("op_type", operation.opType()); + builder.field("seq_no", operation.seqNo()); + builder.field("primary_term", operation.primaryTerm()); + builder.endObject(); + } + builder.endArray(); + } + + /** + * Execute an asynchronous task using a task supplier and an executor service. + * + * @param The type of data to be retrieved. + * @param task The supplier task that provides the data. + * @param executorService The executorService service for executing the asynchronous task. + * @param errorMessage The error message to be thrown if the task execution fails. + * @return A CompletableFuture that completes with the retrieved data. + */ + private static CompletableFuture supplyAsyncTask( + final Supplier task, + final ExecutorService executorService, + final String errorMessage + ) { + return CompletableFuture.supplyAsync(() -> { + try { + return task.get(); + } catch (Exception e) { + throw new ElasticsearchException(errorMessage, e); + } + }, executorService); + } + + /** + * Asynchronously retrieves the shard stats for a given index using an executor service. + * + * @param client The NodeClient for executing the asynchronous request. + * @param indexName The name of the index for which to retrieve shard statistics. + * @param executorService The executorService service for executing the asynchronous task. + * @return A CompletableFuture that completes with the retrieved ShardStats. + * @throws ElasticsearchException If an error occurs while retrieving shard statistics. + */ + private static CompletableFuture asyncShardStats( + final NodeClient client, + final String indexName, + final ExecutorService executorService + ) { + return supplyAsyncTask( + () -> Arrays.stream(client.admin().indices().prepareStats(indexName).clear().get(SHARD_STATS_TIMEOUT).getShards()) + .max(Comparator.comparingLong(shardStats -> shardStats.getCommitStats().getGeneration())) + .orElseThrow(() -> new ElasticsearchException("Unable to retrieve shard stats for index: " + indexName)), + executorService, + "Error while retrieving shard stats for index [" + indexName + "]" + ); + } + + /** + * Asynchronously retrieves the index UUID for a given index using an executor service. + * + * @param client The NodeClient for executing the asynchronous request. + * @param indexName The name of the index for which to retrieve the index UUID. + * @param executorService The executorService service for executing the asynchronous task. + * @return A CompletableFuture that completes with the retrieved index UUID. + * @throws ElasticsearchException If an error occurs while retrieving the index UUID. + */ + private static CompletableFuture asyncGetIndexUUID( + final NodeClient client, + final String indexName, + final ExecutorService executorService + ) { + return supplyAsyncTask( + () -> client.admin() + .indices() + .prepareGetIndex() + .setIndices(indexName) + .get(GET_INDEX_UUID_TIMEOUT) + .getSetting(indexName, IndexMetadata.SETTING_INDEX_UUID), + executorService, + "Error while retrieving index UUID for index [" + indexName + "]" + ); + } +} From 8d1f4565a6b4ef169e926d356519963fdbe78436 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Mon, 16 Dec 2024 08:45:06 -0800 Subject: [PATCH 014/119] Update IronBank hardening manifest maintainers (#118175) --- .../docker/src/docker/iron_bank/hardening_manifest.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/distribution/docker/src/docker/iron_bank/hardening_manifest.yaml b/distribution/docker/src/docker/iron_bank/hardening_manifest.yaml index f4364c5008c09..e3bdac51cc5c5 100644 --- a/distribution/docker/src/docker/iron_bank/hardening_manifest.yaml +++ b/distribution/docker/src/docker/iron_bank/hardening_manifest.yaml @@ -50,9 +50,12 @@ resources: # List of project maintainers maintainers: - - name: "Rory Hunter" - email: "rory.hunter@elastic.co" - username: "rory" + - name: "Mark Vieira" + email: "mark.vieira@elastic.co" + username: "mark-vieira" + - name: "Rene Gröschke" + email: "rene.groschke@elastic.co" + username: "breskeby" - email: "klepal_alexander@bah.com" name: "Alexander Klepal" username: "alexander.klepal" From bf1c0fe0778f1c5af90e6ad1b3da38f444ad114c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slobodan=20Adamovi=C4=87?= Date: Mon, 16 Dec 2024 19:15:28 +0100 Subject: [PATCH 015/119] Make reserved built-in roles queryable (#117581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR makes reserved [built-in roles](https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-roles.html) queryable via [Query Role API](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-query-role.html) by indexing them into the `.security` index. Currently, the built-in roles were only available via [Get Role API](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-role.html). The built-in roles are synced into the `.security` index on cluster recovery. The `.security` index will be created (if it's not existing) before built-in roles are synced. In order to avoid concurrent updates, the built-in roles will only be synced by a master node. Once the built-in roles are synced, the information about indexed roles is kept in the cluster state as part of the `.security` index's metadata. The map containing role names and their digests is persisted as part of `queryable_built_in_roles_digest` property: ``` GET /_cluster/state/metadata/.security "queryable_built_in_roles_digest": { "superuser": "lRRmA3kPO1/ztr3ESAlTetOuDjgUC3fKcGS3ZCqM+6k=", ... } ``` Important: The reserved roles stored in the `.security` index are only intended to be used for querying and retrieving. The role resolution and mapping during authentication will remain the same and give a priority to static/file role definitions. This is ensured by the [order in which role providers (built-in, file and native) are invoked](https://github.com/elastic/elasticsearch/blob/71c252c274aa967d5a66f7d081291ac5d87d27a9/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleProviders.java#L77-L81). It’s important to note this because there can be a short period of time where we have a temporary inconsistency between actual built-in role definitions and what is stored in the `.security` index. --- Note: The functionality is temporarily hidden behind the `es.queryable_built_in_roles_enabled` system property. By default, the flag is disabled and will become enabled in a followup PR. The reason for this is to keep this PR as small as possible and to avoid the need to adjust a large number of tests that don't expect `.security` index to exist. Testing: To run and test locally execute `./gradlew run -Dtests.jvm.argline="-Des.queryable_built_in_roles_enabled=true"`. To query all reserved built-in roles execute: ``` POST /_security/_query/role { "query": { "bool": { "must": { "term": { "metadata._reserved": true } } } } } ``` --- docs/changelog/117581.yaml | 5 + .../test/rest/ESRestTestCase.java | 2 +- .../security/qa/security-basic/build.gradle | 17 +- .../xpack/security/QueryRoleIT.java | 2 +- .../security/QueryableReservedRolesIT.java | 354 ++++++++++++ .../src/main/java/module-info.java | 6 + .../role/QueryableBuiltInRolesTestPlugin.java | 22 + .../security/src/main/java/module-info.java | 2 + .../xpack/security/Security.java | 23 +- .../xpack/security/SecurityFeatures.java | 8 +- .../action/role/TransportGetRolesAction.java | 12 +- .../security/authz/store/FileRolesStore.java | 9 + .../authz/store/NativeRolesStore.java | 50 +- .../rest/action/role/RestQueryRoleAction.java | 3 + .../support/FeatureNotEnabledException.java | 1 + .../support/QueryableBuiltInRoles.java | 52 ++ .../QueryableBuiltInRolesProviderFactory.java | 23 + .../QueryableBuiltInRolesSynchronizer.java | 532 ++++++++++++++++++ .../support/QueryableBuiltInRolesUtils.java | 101 ++++ .../QueryableReservedRolesProvider.java | 56 ++ .../support/SecurityIndexManager.java | 2 +- .../QueryableBuiltInRolesUtilsTests.java | 296 ++++++++++ .../QueryableReservedRolesProviderTests.java | 31 + 23 files changed, 1585 insertions(+), 24 deletions(-) create mode 100644 docs/changelog/117581.yaml create mode 100644 x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryableReservedRolesIT.java create mode 100644 x-pack/plugin/security/qa/security-basic/src/main/java/module-info.java create mode 100644 x-pack/plugin/security/qa/security-basic/src/main/java/org/elasticsearch/xpack/security/role/QueryableBuiltInRolesTestPlugin.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRoles.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesProviderFactory.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesSynchronizer.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtils.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableReservedRolesProvider.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableReservedRolesProviderTests.java diff --git a/docs/changelog/117581.yaml b/docs/changelog/117581.yaml new file mode 100644 index 0000000000000..b88017f45e9c9 --- /dev/null +++ b/docs/changelog/117581.yaml @@ -0,0 +1,5 @@ +pr: 117581 +summary: Make reserved built-in roles queryable +area: Authorization +type: enhancement +issues: [] diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 4428afaaeabe5..fa525705a9b39 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -1138,7 +1138,7 @@ protected static void wipeAllIndices(boolean preserveSecurityIndices) throws IOE } } - private static boolean ignoreSystemIndexAccessWarnings(List warnings) { + protected static boolean ignoreSystemIndexAccessWarnings(List warnings) { for (String warning : warnings) { if (warning.startsWith("this request accesses system indices:")) { SUITE_LOGGER.warn("Ignoring system index access warning during test cleanup: {}", warning); diff --git a/x-pack/plugin/security/qa/security-basic/build.gradle b/x-pack/plugin/security/qa/security-basic/build.gradle index 8740354646346..e6caf943dc023 100644 --- a/x-pack/plugin/security/qa/security-basic/build.gradle +++ b/x-pack/plugin/security/qa/security-basic/build.gradle @@ -4,20 +4,31 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +apply plugin: 'elasticsearch.base-internal-es-plugin' apply plugin: 'elasticsearch.internal-java-rest-test' + +esplugin { + name 'queryable-reserved-roles-test' + description 'A test plugin for testing that changes to reserved roles are made queryable' + classname 'org.elasticsearch.xpack.security.role.QueryableBuiltInRolesTestPlugin' + extendedPlugins = ['x-pack-core', 'x-pack-security'] +} dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('security')))) javaRestTestImplementation(testArtifact(project(xpackModule('core')))) + compileOnly project(':x-pack:plugin:core') + compileOnly project(':x-pack:plugin:security') + clusterPlugins project(':x-pack:plugin:security:qa:security-basic') } tasks.named('javaRestTest') { usesDefaultDistribution() } +tasks.named("javadoc").configure { enabled = false } -if (buildParams.inFipsJvm){ +if (buildParams.inFipsJvm) { // This test cluster is using a BASIC license and FIPS 140 mode is not supported in BASIC - tasks.named("javaRestTest").configure{enabled = false } + tasks.named("javaRestTest").configure { enabled = false } } diff --git a/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryRoleIT.java b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryRoleIT.java index 1588749b9a331..311510352d805 100644 --- a/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryRoleIT.java +++ b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryRoleIT.java @@ -496,7 +496,7 @@ private RoleDescriptor createRole( ); } - private void assertQuery(String body, int total, Consumer>> roleVerifier) throws IOException { + static void assertQuery(String body, int total, Consumer>> roleVerifier) throws IOException { assertQuery(client(), body, total, roleVerifier); } diff --git a/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryableReservedRolesIT.java b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryableReservedRolesIT.java new file mode 100644 index 0000000000000..7adff21d8df4f --- /dev/null +++ b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryableReservedRolesIT.java @@ -0,0 +1,354 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security; + +import com.carrotsearch.randomizedtesting.annotations.TestCaseOrdering; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.test.AnnotationTestOrdering; +import org.elasticsearch.test.AnnotationTestOrdering.Order; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.MutableSettingsProvider; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.cluster.local.model.User; +import org.elasticsearch.test.cluster.util.resource.Resource; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.test.rest.ObjectPath; +import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices; +import org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer; +import org.elasticsearch.xpack.security.support.SecurityMigrations; +import org.junit.BeforeClass; +import org.junit.ClassRule; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static org.elasticsearch.xpack.core.security.test.TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7; +import static org.elasticsearch.xpack.security.QueryRoleIT.assertQuery; +import static org.elasticsearch.xpack.security.QueryRoleIT.waitForMigrationCompletion; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.oneOf; + +@TestCaseOrdering(AnnotationTestOrdering.class) +public class QueryableReservedRolesIT extends ESRestTestCase { + + protected static final String REST_USER = "security_test_user"; + private static final SecureString REST_PASSWORD = new SecureString("security-test-password".toCharArray()); + private static final String ADMIN_USER = "admin_user"; + private static final SecureString ADMIN_PASSWORD = new SecureString("admin-password".toCharArray()); + protected static final String READ_SECURITY_USER = "read_security_user"; + private static final SecureString READ_SECURITY_PASSWORD = new SecureString("read-security-password".toCharArray()); + + @BeforeClass + public static void setup() { + new ReservedRolesStore(); + } + + @Override + protected boolean preserveClusterUponCompletion() { + return true; + } + + private static MutableSettingsProvider clusterSettings = new MutableSettingsProvider() { + { + put("xpack.license.self_generated.type", "basic"); + put("xpack.security.enabled", "true"); + put("xpack.security.http.ssl.enabled", "false"); + put("xpack.security.transport.ssl.enabled", "false"); + } + }; + + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .distribution(DistributionType.DEFAULT) + .nodes(2) + .settings(clusterSettings) + .rolesFile(Resource.fromClasspath("roles.yml")) + .user(ADMIN_USER, ADMIN_PASSWORD.toString(), User.ROOT_USER_ROLE, true) + .user(REST_USER, REST_PASSWORD.toString(), "security_test_role", false) + .user(READ_SECURITY_USER, READ_SECURITY_PASSWORD.toString(), "read_security_user_role", false) + .systemProperty("es.queryable_built_in_roles_enabled", "true") + .plugin("queryable-reserved-roles-test") + .build(); + + private static Set PREVIOUS_RESERVED_ROLES; + private static Set CONFIGURED_RESERVED_ROLES; + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + + @Override + protected Settings restAdminSettings() { + String token = basicAuthHeaderValue(ADMIN_USER, ADMIN_PASSWORD); + return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); + } + + @Override + protected Settings restClientSettings() { + String token = basicAuthHeaderValue(REST_USER, REST_PASSWORD); + return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); + } + + @Order(10) + public void testQueryDeleteOrUpdateReservedRoles() throws Exception { + waitForMigrationCompletion(adminClient(), SecurityMigrations.ROLE_METADATA_FLATTENED_MIGRATION_VERSION); + + final String[] allReservedRoles = ReservedRolesStore.names().toArray(new String[0]); + assertQuery(client(), """ + { "query": { "bool": { "must": { "term": { "metadata._reserved": true } } } }, "size": 100 } + """, allReservedRoles.length, roles -> { + assertThat(roles, iterableWithSize(allReservedRoles.length)); + for (var role : roles) { + assertThat((String) role.get("name"), is(oneOf(allReservedRoles))); + } + }); + + final String roleName = randomFrom(allReservedRoles); + assertQuery(client(), String.format(""" + { "query": { "bool": { "must": { "term": { "name": "%s" } } } } } + """, roleName), 1, roles -> { + assertThat(roles, iterableWithSize(1)); + assertThat((String) roles.get(0).get("name"), equalTo(roleName)); + }); + + assertCannotDeleteReservedRoles(); + assertCannotCreateOrUpdateReservedRole(roleName); + } + + @Order(11) + public void testGetReservedRoles() throws Exception { + final String[] allReservedRoles = ReservedRolesStore.names().toArray(new String[0]); + final String roleName = randomFrom(allReservedRoles); + Request request = new Request("GET", "/_security/role/" + roleName); + Response response = adminClient().performRequest(request); + assertOK(response); + var responseMap = responseAsMap(response); + assertThat(responseMap.size(), equalTo(1)); + assertThat(responseMap.containsKey(roleName), is(true)); + } + + @Order(20) + public void testRestartForConfiguringReservedRoles() throws Exception { + configureReservedRoles(List.of("editor", "viewer", "kibana_system", "apm_system", "beats_system", "logstash_system")); + cluster.restart(false); + closeClients(); + } + + @Order(30) + public void testConfiguredReservedRoles() throws Exception { + assert CONFIGURED_RESERVED_ROLES != null; + + // Test query roles API + assertBusy(() -> { + assertQuery(client(), """ + { "query": { "bool": { "must": { "term": { "metadata._reserved": true } } } }, "size": 100 } + """, CONFIGURED_RESERVED_ROLES.size(), roles -> { + assertThat(roles, iterableWithSize(CONFIGURED_RESERVED_ROLES.size())); + for (var role : roles) { + assertThat((String) role.get("name"), is(oneOf(CONFIGURED_RESERVED_ROLES.toArray(new String[0])))); + } + }); + }, 30, TimeUnit.SECONDS); + + // Test get roles API + assertBusy(() -> { + final Response response = adminClient().performRequest(new Request("GET", "/_security/role")); + assertOK(response); + final Map responseMap = responseAsMap(response); + assertThat(responseMap.keySet(), equalTo(CONFIGURED_RESERVED_ROLES)); + }); + } + + @Order(40) + public void testRestartForConfiguringReservedRolesAndClosingIndex() throws Exception { + configureReservedRoles(List.of("editor", "viewer")); + closeSecurityIndex(); + cluster.restart(false); + closeClients(); + } + + @Order(50) + public void testConfiguredReservedRolesAfterClosingAndOpeningIndex() throws Exception { + assert CONFIGURED_RESERVED_ROLES != null; + assert PREVIOUS_RESERVED_ROLES != null; + assertThat(PREVIOUS_RESERVED_ROLES, is(not(equalTo(CONFIGURED_RESERVED_ROLES)))); + + // Test configured roles did not get updated because the security index is closed + assertMetadataContainsBuiltInRoles(PREVIOUS_RESERVED_ROLES); + + // Open the security index + openSecurityIndex(); + + // Test that the roles are now updated after index got opened + assertBusy(() -> { + assertQuery(client(), """ + { "query": { "bool": { "must": { "term": { "metadata._reserved": true } } } }, "size": 100 } + """, CONFIGURED_RESERVED_ROLES.size(), roles -> { + assertThat(roles, iterableWithSize(CONFIGURED_RESERVED_ROLES.size())); + for (var role : roles) { + assertThat((String) role.get("name"), is(oneOf(CONFIGURED_RESERVED_ROLES.toArray(new String[0])))); + } + }); + }, 30, TimeUnit.SECONDS); + + } + + @Order(60) + public void testDeletingAndCreatingSecurityIndexTriggersSynchronization() throws Exception { + deleteSecurityIndex(); + + assertBusy(this::assertSecurityIndexDeleted, 30, TimeUnit.SECONDS); + + // Creating a user will trigger .security index creation + createUser("superman", "superman", "superuser"); + + // Test that the roles are now updated after index got created + assertBusy(() -> { + assertQuery(client(), """ + { "query": { "bool": { "must": { "term": { "metadata._reserved": true } } } }, "size": 100 } + """, CONFIGURED_RESERVED_ROLES.size(), roles -> { + assertThat(roles, iterableWithSize(CONFIGURED_RESERVED_ROLES.size())); + for (var role : roles) { + assertThat((String) role.get("name"), is(oneOf(CONFIGURED_RESERVED_ROLES.toArray(new String[0])))); + } + }); + }, 30, TimeUnit.SECONDS); + } + + private void createUser(String name, String password, String role) throws IOException { + Request request = new Request("PUT", "/_security/user/" + name); + request.setJsonEntity("{ \"password\": \"" + password + "\", \"roles\": [ \"" + role + "\"] }"); + assertOK(adminClient().performRequest(request)); + } + + private void deleteSecurityIndex() throws IOException { + final Request deleteRequest = new Request("DELETE", INTERNAL_SECURITY_MAIN_INDEX_7); + deleteRequest.setOptions(RequestOptions.DEFAULT.toBuilder().setWarningsHandler(ESRestTestCase::ignoreSystemIndexAccessWarnings)); + final Response response = adminClient().performRequest(deleteRequest); + try (InputStream is = response.getEntity().getContent()) { + assertTrue((boolean) XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true).get("acknowledged")); + } + } + + private void assertMetadataContainsBuiltInRoles(Set builtInRoles) throws IOException { + final Request request = new Request("GET", "_cluster/state/metadata/" + INTERNAL_SECURITY_MAIN_INDEX_7); + final Response response = adminClient().performRequest(request); + assertOK(response); + final Map builtInRolesDigests = ObjectPath.createFromResponse(response) + .evaluate("metadata.indices.\\.security-7." + QueryableBuiltInRolesSynchronizer.METADATA_QUERYABLE_BUILT_IN_ROLES_DIGEST_KEY); + assertThat(builtInRolesDigests.keySet(), equalTo(builtInRoles)); + } + + private void assertSecurityIndexDeleted() throws IOException { + final Request request = new Request("GET", "_cluster/state/metadata/" + INTERNAL_SECURITY_MAIN_INDEX_7); + final Response response = adminClient().performRequest(request); + assertOK(response); + final Map securityIndexMetadata = ObjectPath.createFromResponse(response) + .evaluate("metadata.indices.\\.security-7"); + assertThat(securityIndexMetadata, is(nullValue())); + } + + private void configureReservedRoles(List reservedRoles) throws Exception { + PREVIOUS_RESERVED_ROLES = CONFIGURED_RESERVED_ROLES; + CONFIGURED_RESERVED_ROLES = new HashSet<>(); + CONFIGURED_RESERVED_ROLES.add("superuser"); // superuser must always be included + CONFIGURED_RESERVED_ROLES.addAll(reservedRoles); + clusterSettings.put("xpack.security.reserved_roles.include", Strings.collectionToCommaDelimitedString(CONFIGURED_RESERVED_ROLES)); + } + + private void closeSecurityIndex() throws Exception { + Request request = new Request("POST", "/" + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + "/_close"); + request.setOptions( + expectWarnings( + "this request accesses system indices: [.security-7], but in a future major version, " + + "direct access to system indices will be prevented by default" + ) + ); + Response response = adminClient().performRequest(request); + assertOK(response); + } + + private void openSecurityIndex() throws Exception { + Request request = new Request("POST", "/" + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + "/_open"); + request.setOptions( + expectWarnings( + "this request accesses system indices: [.security-7], but in a future major version, " + + "direct access to system indices will be prevented by default" + ) + ); + Response response = adminClient().performRequest(request); + assertOK(response); + } + + private void assertCannotDeleteReservedRoles() throws Exception { + { + String roleName = randomFrom(ReservedRolesStore.names()); + Request request = new Request("DELETE", "/_security/role/" + roleName); + var e = expectThrows(ResponseException.class, () -> adminClient().performRequest(request)); + assertThat(e.getMessage(), containsString("role [" + roleName + "] is reserved and cannot be deleted")); + } + { + Request request = new Request("DELETE", "/_security/role/"); + request.setJsonEntity( + """ + { + "names": [%s] + } + """.formatted( + ReservedRolesStore.names().stream().map(name -> "\"" + name + "\"").reduce((a, b) -> a + ", " + b).orElse("") + ) + ); + Response response = adminClient().performRequest(request); + assertOK(response); + String responseAsString = responseAsMap(response).toString(); + for (String roleName : ReservedRolesStore.names()) { + assertThat(responseAsString, containsString("role [" + roleName + "] is reserved and cannot be deleted")); + } + } + } + + private void assertCannotCreateOrUpdateReservedRole(String roleName) throws Exception { + Request request = new Request(randomBoolean() ? "PUT" : "POST", "/_security/role/" + roleName); + request.setJsonEntity(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["all"] + } + ] + } + """); + var e = expectThrows(ResponseException.class, () -> adminClient().performRequest(request)); + assertThat(e.getMessage(), containsString("Role [" + roleName + "] is reserved and may not be used.")); + } + +} diff --git a/x-pack/plugin/security/qa/security-basic/src/main/java/module-info.java b/x-pack/plugin/security/qa/security-basic/src/main/java/module-info.java new file mode 100644 index 0000000000000..00c8e480cfbaf --- /dev/null +++ b/x-pack/plugin/security/qa/security-basic/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module org.elasticsearch.internal.security { + requires org.elasticsearch.base; + requires org.elasticsearch.server; + requires org.elasticsearch.xcore; + requires org.elasticsearch.security; +} diff --git a/x-pack/plugin/security/qa/security-basic/src/main/java/org/elasticsearch/xpack/security/role/QueryableBuiltInRolesTestPlugin.java b/x-pack/plugin/security/qa/security-basic/src/main/java/org/elasticsearch/xpack/security/role/QueryableBuiltInRolesTestPlugin.java new file mode 100644 index 0000000000000..ba5538d992cfb --- /dev/null +++ b/x-pack/plugin/security/qa/security-basic/src/main/java/org/elasticsearch/xpack/security/role/QueryableBuiltInRolesTestPlugin.java @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.role; + +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; + +import java.util.List; + +public class QueryableBuiltInRolesTestPlugin extends Plugin { + + @Override + public List> getSettings() { + return List.of(ReservedRolesStore.INCLUDED_RESERVED_ROLES_SETTING); + } +} diff --git a/x-pack/plugin/security/src/main/java/module-info.java b/x-pack/plugin/security/src/main/java/module-info.java index a072b34da7e96..947211559b0c2 100644 --- a/x-pack/plugin/security/src/main/java/module-info.java +++ b/x-pack/plugin/security/src/main/java/module-info.java @@ -70,6 +70,8 @@ exports org.elasticsearch.xpack.security.slowlog to org.elasticsearch.server; exports org.elasticsearch.xpack.security.authc.support to org.elasticsearch.internal.security; exports org.elasticsearch.xpack.security.rest.action.apikey to org.elasticsearch.internal.security; + exports org.elasticsearch.xpack.security.support to org.elasticsearch.internal.security; + exports org.elasticsearch.xpack.security.authz.store to org.elasticsearch.internal.security; provides org.elasticsearch.index.SlowLogFieldProvider with org.elasticsearch.xpack.security.slowlog.SecuritySlowLogFieldProvider; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index ef66392a87260..fd530a338b26c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -411,6 +411,8 @@ import org.elasticsearch.xpack.security.rest.action.user.RestSetEnabledAction; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.ExtensionComponents; +import org.elasticsearch.xpack.security.support.QueryableBuiltInRolesProviderFactory; +import org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer; import org.elasticsearch.xpack.security.support.ReloadableSecurityComponent; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.support.SecurityMigrationExecutor; @@ -461,6 +463,7 @@ import static org.elasticsearch.xpack.core.security.SecurityField.FIELD_LEVEL_SECURITY_FEATURE; import static org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore.INCLUDED_RESERVED_ROLES_SETTING; import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED; +import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer.QUERYABLE_BUILT_IN_ROLES_ENABLED; import static org.elasticsearch.xpack.security.transport.SSLEngineUtils.extractClientCertificates; public class Security extends Plugin @@ -631,7 +634,7 @@ public class Security extends Plugin private final SetOnce reservedRoleNameCheckerFactory = new SetOnce<>(); private final SetOnce fileRoleValidator = new SetOnce<>(); private final SetOnce secondaryAuthActions = new SetOnce<>(); - + private final SetOnce queryableRolesProviderFactory = new SetOnce<>(); private final SetOnce securityMigrationExecutor = new SetOnce<>(); // Node local retry count for migration jobs that's checked only on the master node to make sure @@ -1202,6 +1205,23 @@ Collection createComponents( reservedRoleMappingAction.set(new ReservedRoleMappingAction()); + if (QUERYABLE_BUILT_IN_ROLES_ENABLED) { + if (queryableRolesProviderFactory.get() == null) { + queryableRolesProviderFactory.set(new QueryableBuiltInRolesProviderFactory.Default()); + } + components.add( + new QueryableBuiltInRolesSynchronizer( + clusterService, + featureService, + queryableRolesProviderFactory.get(), + nativeRolesStore, + reservedRolesStore, + fileRolesStore.get(), + threadPool + ) + ); + } + cacheInvalidatorRegistry.validate(); final List reloadableComponents = new ArrayList<>(); @@ -2317,6 +2337,7 @@ public void loadExtensions(ExtensionLoader loader) { loadSingletonExtensionAndSetOnce(loader, grantApiKeyRequestTranslator, RestGrantApiKeyAction.RequestTranslator.class); loadSingletonExtensionAndSetOnce(loader, fileRoleValidator, FileRoleValidator.class); loadSingletonExtensionAndSetOnce(loader, secondaryAuthActions, SecondaryAuthActions.class); + loadSingletonExtensionAndSetOnce(loader, queryableRolesProviderFactory, QueryableBuiltInRolesProviderFactory.class); } private void loadSingletonExtensionAndSetOnce(ExtensionLoader loader, SetOnce setOnce, Class clazz) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatures.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatures.java index 53ecafa280715..84749d895a44e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatures.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatures.java @@ -12,6 +12,7 @@ import java.util.Set; +import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer.QUERYABLE_BUILT_IN_ROLES_FEATURE; import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MIGRATION_FRAMEWORK; import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_ROLES_METADATA_FLATTENED; import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_ROLE_MAPPING_CLEANUP; @@ -20,6 +21,11 @@ public class SecurityFeatures implements FeatureSpecification { @Override public Set getFeatures() { - return Set.of(SECURITY_ROLE_MAPPING_CLEANUP, SECURITY_ROLES_METADATA_FLATTENED, SECURITY_MIGRATION_FRAMEWORK); + return Set.of( + SECURITY_ROLE_MAPPING_CLEANUP, + SECURITY_ROLES_METADATA_FLATTENED, + SECURITY_MIGRATION_FRAMEWORK, + QUERYABLE_BUILT_IN_ROLES_FEATURE + ); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java index e019f168cf8c0..cdeac51e1f492 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java @@ -20,11 +20,9 @@ import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; -import java.util.List; +import java.util.LinkedHashSet; import java.util.Set; import java.util.stream.Collectors; @@ -51,8 +49,8 @@ protected void doExecute(Task task, final GetRolesRequest request, final ActionL return; } - final Set rolesToSearchFor = new HashSet<>(); - final List reservedRoles = new ArrayList<>(); + final Set rolesToSearchFor = new LinkedHashSet<>(); + final Set reservedRoles = new LinkedHashSet<>(); if (specificRolesRequested) { for (String role : requestedRoles) { if (ReservedRolesStore.isReserved(role)) { @@ -80,10 +78,10 @@ protected void doExecute(Task task, final GetRolesRequest request, final ActionL } private void getNativeRoles(Set rolesToSearchFor, ActionListener listener) { - getNativeRoles(rolesToSearchFor, new ArrayList<>(), listener); + getNativeRoles(rolesToSearchFor, new LinkedHashSet<>(), listener); } - private void getNativeRoles(Set rolesToSearchFor, List foundRoles, ActionListener listener) { + private void getNativeRoles(Set rolesToSearchFor, Set foundRoles, ActionListener listener) { nativeRolesStore.getRoleDescriptors(rolesToSearchFor, ActionListener.wrap((retrievalResult) -> { if (retrievalResult.isSuccess()) { foundRoles.addAll(retrievalResult.getDescriptors()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java index 7618135c8662f..87378ac0b9f25 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java @@ -44,6 +44,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -173,6 +174,14 @@ public Path getFile() { return file; } + /** + * @return a map of all file role definitions. The returned map is unmodifiable. + */ + public Map getAllRoleDescriptors() { + final Map localPermissions = permissions; + return Collections.unmodifiableMap(localPermissions); + } + // package private for testing Set getAllRoleNames() { return permissions.keySet(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java index 23a1fc188e4a0..0a5865ecfe9bf 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java @@ -63,13 +63,13 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges; import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.authz.support.DLSRoleQueryValidator; -import org.elasticsearch.xpack.core.security.support.NativeRealmValidationUtil; import org.elasticsearch.xpack.security.authz.ReservedRoleNameChecker; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -169,6 +169,10 @@ public NativeRolesStore( this.enabled = settings.getAsBoolean(NATIVE_ROLES_ENABLED, true); } + public boolean isEnabled() { + return enabled; + } + @Override public void accept(Set names, ActionListener listener) { getRoleDescriptors(names, listener); @@ -263,6 +267,10 @@ public boolean isMetadataSearchable() { } public void queryRoleDescriptors(SearchSourceBuilder searchSourceBuilder, ActionListener listener) { + if (enabled == false) { + listener.onResponse(QueryRoleResult.EMPTY); + return; + } SearchRequest searchRequest = new SearchRequest(new String[] { SECURITY_MAIN_ALIAS }, searchSourceBuilder); SecurityIndexManager frozenSecurityIndex = securityIndex.defensiveCopy(); if (frozenSecurityIndex.indexExists() == false) { @@ -345,6 +353,15 @@ public void deleteRoles( final List roleNames, WriteRequest.RefreshPolicy refreshPolicy, final ActionListener listener + ) { + deleteRoles(roleNames, refreshPolicy, true, listener); + } + + public void deleteRoles( + final Collection roleNames, + WriteRequest.RefreshPolicy refreshPolicy, + boolean validateRoleNames, + final ActionListener listener ) { if (enabled == false) { listener.onFailure(new IllegalStateException("Native role management is disabled")); @@ -355,7 +372,7 @@ public void deleteRoles( Map validationErrorByRoleName = new HashMap<>(); for (String roleName : roleNames) { - if (reservedRoleNameChecker.isReserved(roleName)) { + if (validateRoleNames && reservedRoleNameChecker.isReserved(roleName)) { validationErrorByRoleName.put( roleName, new IllegalArgumentException("role [" + roleName + "] is reserved and cannot be deleted") @@ -402,7 +419,7 @@ public void onFailure(Exception e) { } private void bulkResponseAndRefreshRolesCache( - List roleNames, + Collection roleNames, BulkResponse bulkResponse, Map validationErrorByRoleName, ActionListener listener @@ -430,7 +447,7 @@ private void bulkResponseAndRefreshRolesCache( } private void bulkResponseWithOnlyValidationErrors( - List roleNames, + Collection roleNames, Map validationErrorByRoleName, ActionListener listener ) { @@ -542,7 +559,16 @@ public void onFailure(Exception e) { public void putRoles( final WriteRequest.RefreshPolicy refreshPolicy, - final List roles, + final Collection roles, + final ActionListener listener + ) { + putRoles(refreshPolicy, roles, true, listener); + } + + public void putRoles( + final WriteRequest.RefreshPolicy refreshPolicy, + final Collection roles, + boolean validateRoleDescriptors, final ActionListener listener ) { if (enabled == false) { @@ -555,7 +581,7 @@ public void putRoles( for (RoleDescriptor role : roles) { Exception validationException; try { - validationException = validateRoleDescriptor(role); + validationException = validateRoleDescriptors ? validateRoleDescriptor(role) : null; } catch (Exception e) { validationException = e; } @@ -621,8 +647,6 @@ private DeleteRequest createRoleDeleteRequest(final String roleName) { // Package private for testing XContentBuilder createRoleXContentBuilder(RoleDescriptor role) throws IOException { - assert NativeRealmValidationUtil.validateRoleName(role.getName(), false) == null - : "Role name was invalid or reserved: " + role.getName(); assert false == role.hasRestriction() : "restriction is not supported for native roles"; XContentBuilder builder = jsonBuilder().startObject(); @@ -671,7 +695,11 @@ public void usageStats(ActionListener> listener) { client.prepareMultiSearch() .add( client.prepareSearch(SECURITY_MAIN_ALIAS) - .setQuery(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE)) + .setQuery( + QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE)) + .mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true)) + ) .setTrackTotalHits(true) .setSize(0) ) @@ -680,6 +708,7 @@ public void usageStats(ActionListener> listener) { .setQuery( QueryBuilders.boolQuery() .must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE)) + .mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true)) .must( QueryBuilders.boolQuery() .should(existsQuery("indices.field_security.grant")) @@ -697,6 +726,7 @@ public void usageStats(ActionListener> listener) { .setQuery( QueryBuilders.boolQuery() .must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE)) + .mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true)) .filter(existsQuery("indices.query")) ) .setTrackTotalHits(true) @@ -708,6 +738,7 @@ public void usageStats(ActionListener> listener) { .setQuery( QueryBuilders.boolQuery() .must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE)) + .mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true)) .filter(existsQuery("remote_indices")) ) .setTrackTotalHits(true) @@ -718,6 +749,7 @@ public void usageStats(ActionListener> listener) { .setQuery( QueryBuilders.boolQuery() .must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE)) + .mustNot(QueryBuilders.termQuery("metadata_flattened._reserved", true)) .filter(existsQuery("remote_cluster")) ) .setTrackTotalHits(true) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestQueryRoleAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestQueryRoleAction.java index c2dc7166bd3b6..3637159479463 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestQueryRoleAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestQueryRoleAction.java @@ -14,6 +14,8 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.Scope; +import org.elasticsearch.rest.ServerlessScope; import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.search.searchafter.SearchAfterBuilder; import org.elasticsearch.search.sort.FieldSortBuilder; @@ -32,6 +34,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; +@ServerlessScope(Scope.INTERNAL) public final class RestQueryRoleAction extends NativeRoleBaseRestHandler { @SuppressWarnings("unchecked") diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java index 87c23284c5819..8ba3ebad8a851 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java @@ -29,6 +29,7 @@ public enum Feature { } } + @SuppressWarnings("this-escape") public FeatureNotEnabledException(Feature feature, String message, Object... args) { super(message, args); addMetadata(DISABLED_FEATURE_METADATA, feature.featureName); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRoles.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRoles.java new file mode 100644 index 0000000000000..ec38e4951f45c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRoles.java @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.support; + +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.util.Collection; +import java.util.Map; + +/** + * A class that holds the built-in roles and their hash digests. + */ +public record QueryableBuiltInRoles(Map rolesDigest, Collection roleDescriptors) { + + /** + * A listener that is notified when the built-in roles change. + */ + public interface Listener { + + /** + * Called when the built-in roles change. + * + * @param roles the new built-in roles. + */ + void onRolesChanged(QueryableBuiltInRoles roles); + + } + + /** + * A provider that provides the built-in roles and can notify subscribed listeners when the built-in roles change. + */ + public interface Provider { + + /** + * @return the built-in roles. + */ + QueryableBuiltInRoles getRoles(); + + /** + * Adds a listener to be notified when the built-in roles change. + * + * @param listener the listener to add. + */ + void addListener(Listener listener); + + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesProviderFactory.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesProviderFactory.java new file mode 100644 index 0000000000000..c29b64836d1a5 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesProviderFactory.java @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.support; + +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.security.authz.store.FileRolesStore; + +public interface QueryableBuiltInRolesProviderFactory { + + QueryableBuiltInRoles.Provider createProvider(ReservedRolesStore reservedRolesStore, FileRolesStore fileRolesStore); + + class Default implements QueryableBuiltInRolesProviderFactory { + @Override + public QueryableBuiltInRoles.Provider createProvider(ReservedRolesStore reservedRolesStore, FileRolesStore fileRolesStore) { + return new QueryableReservedRolesProvider(reservedRolesStore); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesSynchronizer.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesSynchronizer.java new file mode 100644 index 0000000000000..60163434e212f --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesSynchronizer.java @@ -0,0 +1,532 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.support; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.ResourceAlreadyExistsException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.TransportActions; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.cluster.ClusterChangedEvent; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateListener; +import org.elasticsearch.cluster.ClusterStateTaskListener; +import org.elasticsearch.cluster.NotMasterException; +import org.elasticsearch.cluster.SimpleBatchedExecutor; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.cluster.service.MasterServiceTaskQueue; +import org.elasticsearch.common.Priority; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.component.LifecycleListener; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Strings; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.features.FeatureService; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.index.engine.DocumentMissingException; +import org.elasticsearch.index.engine.VersionConflictEngineException; +import org.elasticsearch.indices.IndexClosedException; +import org.elasticsearch.indices.IndexPrimaryShardNotAllocatedException; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.action.role.BulkRolesResponse; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.security.authz.store.FileRolesStore; +import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; +import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesUtils.determineRolesToDelete; +import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesUtils.determineRolesToUpsert; +import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS; + +/** + * Synchronizes built-in roles to the .security index. + * The .security index is created if it does not exist. + *

+ * The synchronization is executed only on the elected master node + * after the cluster has recovered and roles need to be synced. + * The goal is to reduce the potential for conflicting operations. + * While in most cases, there should be only a single node that’s + * attempting to create/update/delete roles, it’s still possible + * that the master node changes in the middle of the syncing process. + */ +public final class QueryableBuiltInRolesSynchronizer implements ClusterStateListener { + + private static final Logger logger = LogManager.getLogger(QueryableBuiltInRolesSynchronizer.class); + + /** + * This is a temporary feature flag to allow enabling the synchronization of built-in roles to the .security index. + * Initially, it is disabled by default due to the number of tests that need to be adjusted now that .security index + * is created earlier in the cluster lifecycle. + *

+ * Once all tests are adjusted, this flag will be set to enabled by default and later removed altogether. + */ + public static final boolean QUERYABLE_BUILT_IN_ROLES_ENABLED; + static { + final var propertyValue = System.getProperty("es.queryable_built_in_roles_enabled"); + if (propertyValue == null || propertyValue.isEmpty() || "false".equals(propertyValue)) { + QUERYABLE_BUILT_IN_ROLES_ENABLED = false; + } else if ("true".equals(propertyValue)) { + QUERYABLE_BUILT_IN_ROLES_ENABLED = true; + } else { + throw new IllegalStateException( + "system property [es.queryable_built_in_roles_enabled] may only be set to [true] or [false], but was [" + + propertyValue + + "]" + ); + } + } + + public static final NodeFeature QUERYABLE_BUILT_IN_ROLES_FEATURE = new NodeFeature("security.queryable_built_in_roles"); + + /** + * Index metadata key of the digest of built-in roles indexed in the .security index. + *

+ * The value is a map of built-in role names to their digests (calculated by sha256 of the role definition). + */ + public static final String METADATA_QUERYABLE_BUILT_IN_ROLES_DIGEST_KEY = "queryable_built_in_roles_digest"; + + private static final SimpleBatchedExecutor> MARK_ROLES_AS_SYNCED_TASK_EXECUTOR = + new SimpleBatchedExecutor<>() { + @Override + public Tuple> executeTask(MarkRolesAsSyncedTask task, ClusterState clusterState) { + return task.execute(clusterState); + } + + @Override + public void taskSucceeded(MarkRolesAsSyncedTask task, Map value) { + task.success(value); + } + }; + + private final MasterServiceTaskQueue markRolesAsSyncedTaskQueue; + + private final ClusterService clusterService; + private final FeatureService featureService; + private final QueryableBuiltInRoles.Provider rolesProvider; + private final NativeRolesStore nativeRolesStore; + private final Executor executor; + private final AtomicBoolean synchronizationInProgress = new AtomicBoolean(false); + + private volatile boolean securityIndexDeleted = false; + + /** + * Constructs a new built-in roles synchronizer. + * + * @param clusterService the cluster service to register as a listener + * @param featureService the feature service to check if the cluster has the queryable built-in roles feature + * @param rolesProviderFactory the factory to create the built-in roles provider + * @param nativeRolesStore the native roles store to sync the built-in roles to + * @param reservedRolesStore the reserved roles store to fetch the built-in roles from + * @param fileRolesStore the file roles store to fetch the built-in roles from + * @param threadPool the thread pool + */ + public QueryableBuiltInRolesSynchronizer( + ClusterService clusterService, + FeatureService featureService, + QueryableBuiltInRolesProviderFactory rolesProviderFactory, + NativeRolesStore nativeRolesStore, + ReservedRolesStore reservedRolesStore, + FileRolesStore fileRolesStore, + ThreadPool threadPool + ) { + this.clusterService = clusterService; + this.featureService = featureService; + this.rolesProvider = rolesProviderFactory.createProvider(reservedRolesStore, fileRolesStore); + this.nativeRolesStore = nativeRolesStore; + this.executor = threadPool.generic(); + this.markRolesAsSyncedTaskQueue = clusterService.createTaskQueue( + "mark-built-in-roles-as-synced-task-queue", + Priority.LOW, + MARK_ROLES_AS_SYNCED_TASK_EXECUTOR + ); + this.rolesProvider.addListener(this::builtInRolesChanged); + this.clusterService.addLifecycleListener(new LifecycleListener() { + @Override + public void beforeStop() { + clusterService.removeListener(QueryableBuiltInRolesSynchronizer.this); + } + + @Override + public void beforeStart() { + clusterService.addListener(QueryableBuiltInRolesSynchronizer.this); + } + }); + } + + private void builtInRolesChanged(QueryableBuiltInRoles roles) { + logger.debug("Built-in roles changed, attempting to sync to .security index"); + final ClusterState state = clusterService.state(); + if (shouldSyncBuiltInRoles(state)) { + syncBuiltInRoles(roles); + } + } + + @Override + public void clusterChanged(ClusterChangedEvent event) { + final ClusterState state = event.state(); + if (isSecurityIndexDeleted(event)) { + this.securityIndexDeleted = true; + logger.trace("Received security index deletion event, skipping built-in roles synchronization"); + return; + } else if (isSecurityIndexCreatedOrRecovered(event)) { + this.securityIndexDeleted = false; + logger.trace("Security index has been created/recovered, attempting to sync built-in roles"); + } + if (shouldSyncBuiltInRoles(state)) { + final QueryableBuiltInRoles roles = rolesProvider.getRoles(); + syncBuiltInRoles(roles); + } + } + + private void syncBuiltInRoles(final QueryableBuiltInRoles roles) { + if (synchronizationInProgress.compareAndSet(false, true)) { + final Map indexedRolesDigests = readIndexedBuiltInRolesDigests(clusterService.state()); + if (roles.rolesDigest().equals(indexedRolesDigests)) { + logger.debug("Security index already contains the latest built-in roles indexed, skipping synchronization"); + return; + } + executor.execute(() -> doSyncBuiltinRoles(indexedRolesDigests, roles, ActionListener.wrap(v -> { + logger.info("Successfully synced [" + roles.roleDescriptors().size() + "] built-in roles to .security index"); + synchronizationInProgress.set(false); + }, e -> { + handleException(e); + synchronizationInProgress.set(false); + }))); + } + } + + private static void handleException(Exception e) { + if (e instanceof BulkRolesResponseException bulkException) { + final boolean isBulkDeleteFailure = bulkException instanceof BulkDeleteRolesResponseException; + for (final Map.Entry bulkFailure : bulkException.getFailures().entrySet()) { + final String logMessage = Strings.format( + "Failed to [%s] built-in role [%s]", + isBulkDeleteFailure ? "delete" : "create/update", + bulkFailure.getKey() + ); + if (isExpectedFailure(bulkFailure.getValue())) { + logger.info(logMessage, bulkFailure.getValue()); + } else { + logger.warn(logMessage, bulkFailure.getValue()); + } + } + } else if (isExpectedFailure(e)) { + logger.info("Failed to sync built-in roles to .security index", e); + } else { + logger.warn("Failed to sync built-in roles to .security index due to unexpected exception", e); + } + } + + /** + * Some failures are expected and should not be logged as errors. + * These exceptions are either: + * - transient (e.g. connection errors), + * - recoverable (e.g. no longer master, index reallocating or caused by concurrent operations) + * - not recoverable but expected (e.g. index closed). + * + * @param e to check + * @return {@code true} if the exception is expected and should not be logged as an error + */ + private static boolean isExpectedFailure(final Exception e) { + final Throwable cause = ExceptionsHelper.unwrapCause(e); + return ExceptionsHelper.isNodeOrShardUnavailableTypeException(cause) + || TransportActions.isShardNotAvailableException(cause) + || cause instanceof IndexClosedException + || cause instanceof IndexPrimaryShardNotAllocatedException + || cause instanceof NotMasterException + || cause instanceof ResourceAlreadyExistsException + || cause instanceof VersionConflictEngineException + || cause instanceof DocumentMissingException + || cause instanceof FailedToMarkBuiltInRolesAsSyncedException; + } + + private boolean shouldSyncBuiltInRoles(final ClusterState state) { + if (false == state.nodes().isLocalNodeElectedMaster()) { + logger.trace("Local node is not the master, skipping built-in roles synchronization"); + return false; + } + if (false == state.clusterRecovered()) { + logger.trace("Cluster state has not recovered yet, skipping built-in roles synchronization"); + return false; + } + if (nativeRolesStore.isEnabled() == false) { + logger.trace("Native roles store is not enabled, skipping built-in roles synchronization"); + return false; + } + if (state.nodes().getDataNodes().isEmpty()) { + logger.trace("No data nodes in the cluster, skipping built-in roles synchronization"); + return false; + } + if (state.nodes().isMixedVersionCluster()) { + // To keep things simple and avoid potential overwrites with an older version of built-in roles, + // we only sync built-in roles if all nodes are on the same version. + logger.trace("Not all nodes are on the same version, skipping built-in roles synchronization"); + return false; + } + if (false == featureService.clusterHasFeature(state, QUERYABLE_BUILT_IN_ROLES_FEATURE)) { + logger.trace("Not all nodes support queryable built-in roles feature, skipping built-in roles synchronization"); + return false; + } + if (securityIndexDeleted) { + logger.trace("Security index is deleted, skipping built-in roles synchronization"); + return false; + } + if (isSecurityIndexClosed(state)) { + logger.trace("Security index is closed, skipping built-in roles synchronization"); + return false; + } + return true; + } + + private void doSyncBuiltinRoles( + final Map indexedRolesDigests, + final QueryableBuiltInRoles roles, + final ActionListener listener + ) { + final Set rolesToUpsert = determineRolesToUpsert(roles, indexedRolesDigests); + final Set rolesToDelete = determineRolesToDelete(roles, indexedRolesDigests); + + assert Sets.intersection(rolesToUpsert.stream().map(RoleDescriptor::getName).collect(toSet()), rolesToDelete).isEmpty() + : "The roles to upsert and delete should not have any common roles"; + + if (rolesToUpsert.isEmpty() && rolesToDelete.isEmpty()) { + logger.debug("No changes to built-in roles to sync to .security index"); + listener.onResponse(null); + return; + } + + indexRoles(rolesToUpsert, listener.delegateFailureAndWrap((l1, indexResponse) -> { + deleteRoles(rolesToDelete, l1.delegateFailureAndWrap((l2, deleteResponse) -> { + markRolesAsSynced(indexedRolesDigests, roles.rolesDigest(), l2); + })); + })); + } + + private void deleteRoles(final Set rolesToDelete, final ActionListener listener) { + if (rolesToDelete.isEmpty()) { + listener.onResponse(null); + return; + } + nativeRolesStore.deleteRoles(rolesToDelete, WriteRequest.RefreshPolicy.IMMEDIATE, false, ActionListener.wrap(deleteResponse -> { + final Map deleteFailure = deleteResponse.getItems() + .stream() + .filter(BulkRolesResponse.Item::isFailed) + .collect(toMap(BulkRolesResponse.Item::getRoleName, BulkRolesResponse.Item::getCause)); + if (deleteFailure.isEmpty()) { + listener.onResponse(null); + } else { + listener.onFailure(new BulkDeleteRolesResponseException(deleteFailure)); + } + }, listener::onFailure)); + } + + private void indexRoles(final Collection rolesToUpsert, final ActionListener listener) { + if (rolesToUpsert.isEmpty()) { + listener.onResponse(null); + return; + } + nativeRolesStore.putRoles(WriteRequest.RefreshPolicy.IMMEDIATE, rolesToUpsert, false, ActionListener.wrap(response -> { + final Map indexFailures = response.getItems() + .stream() + .filter(BulkRolesResponse.Item::isFailed) + .collect(toMap(BulkRolesResponse.Item::getRoleName, BulkRolesResponse.Item::getCause)); + if (indexFailures.isEmpty()) { + listener.onResponse(null); + } else { + listener.onFailure(new BulkIndexRolesResponseException(indexFailures)); + } + }, listener::onFailure)); + } + + private boolean isSecurityIndexDeleted(final ClusterChangedEvent event) { + final IndexMetadata previousSecurityIndexMetadata = resolveSecurityIndexMetadata(event.previousState().metadata()); + final IndexMetadata currentSecurityIndexMetadata = resolveSecurityIndexMetadata(event.state().metadata()); + return previousSecurityIndexMetadata != null && currentSecurityIndexMetadata == null; + } + + private boolean isSecurityIndexCreatedOrRecovered(final ClusterChangedEvent event) { + final IndexMetadata previousSecurityIndexMetadata = resolveSecurityIndexMetadata(event.previousState().metadata()); + final IndexMetadata currentSecurityIndexMetadata = resolveSecurityIndexMetadata(event.state().metadata()); + return previousSecurityIndexMetadata == null && currentSecurityIndexMetadata != null; + } + + private boolean isSecurityIndexClosed(final ClusterState state) { + final IndexMetadata indexMetadata = resolveSecurityIndexMetadata(state.metadata()); + return indexMetadata != null && indexMetadata.getState() == IndexMetadata.State.CLOSE; + } + + /** + * This method marks the built-in roles as synced in the .security index + * by setting the new roles digests in the metadata of the .security index. + *

+ * The marking is done as a compare and swap operation to ensure that the roles + * are marked as synced only when new roles are indexed. The operation is idempotent + * and will succeed if the expected roles digests are equal to the digests in the + * .security index or if they are equal to the new roles digests. + */ + private void markRolesAsSynced( + final Map expectedRolesDigests, + final Map newRolesDigests, + final ActionListener listener + ) { + final IndexMetadata securityIndexMetadata = resolveSecurityIndexMetadata(clusterService.state().metadata()); + if (securityIndexMetadata == null) { + listener.onFailure(new IndexNotFoundException(SECURITY_MAIN_ALIAS)); + return; + } + final Index concreteSecurityIndex = securityIndexMetadata.getIndex(); + markRolesAsSyncedTaskQueue.submitTask( + "mark built-in roles as synced task", + new MarkRolesAsSyncedTask(listener.delegateFailureAndWrap((l, response) -> { + if (newRolesDigests.equals(response) == false) { + logger.debug( + () -> Strings.format( + "Another master node most probably indexed a newer versions of built-in roles in the meantime. " + + "Expected: [%s], Actual: [%s]", + newRolesDigests, + response + ) + ); + l.onFailure( + new FailedToMarkBuiltInRolesAsSyncedException( + "Failed to mark built-in roles as synced. The expected role digests have changed." + ) + ); + } else { + l.onResponse(null); + } + }), concreteSecurityIndex.getName(), expectedRolesDigests, newRolesDigests), + null + ); + } + + private Map readIndexedBuiltInRolesDigests(final ClusterState state) { + final IndexMetadata indexMetadata = resolveSecurityIndexMetadata(state.metadata()); + if (indexMetadata == null) { + return null; + } + return indexMetadata.getCustomData(METADATA_QUERYABLE_BUILT_IN_ROLES_DIGEST_KEY); + } + + private static IndexMetadata resolveSecurityIndexMetadata(final Metadata metadata) { + return SecurityIndexManager.resolveConcreteIndex(SECURITY_MAIN_ALIAS, metadata); + } + + static class MarkRolesAsSyncedTask implements ClusterStateTaskListener { + + private final ActionListener> listener; + private final String concreteSecurityIndexName; + private final Map expectedRoleDigests; + private final Map newRoleDigests; + + MarkRolesAsSyncedTask( + ActionListener> listener, + String concreteSecurityIndexName, + @Nullable Map expectedRoleDigests, + @Nullable Map newRoleDigests + ) { + this.listener = listener; + this.concreteSecurityIndexName = concreteSecurityIndexName; + this.expectedRoleDigests = expectedRoleDigests; + this.newRoleDigests = newRoleDigests; + } + + Tuple> execute(ClusterState state) { + IndexMetadata indexMetadata = state.metadata().index(concreteSecurityIndexName); + if (indexMetadata == null) { + throw new IndexNotFoundException(concreteSecurityIndexName); + } + Map existingRoleDigests = indexMetadata.getCustomData(METADATA_QUERYABLE_BUILT_IN_ROLES_DIGEST_KEY); + if (Objects.equals(expectedRoleDigests, existingRoleDigests)) { + IndexMetadata.Builder indexMetadataBuilder = IndexMetadata.builder(indexMetadata); + if (newRoleDigests != null) { + indexMetadataBuilder.putCustom(METADATA_QUERYABLE_BUILT_IN_ROLES_DIGEST_KEY, newRoleDigests); + } else { + indexMetadataBuilder.removeCustom(METADATA_QUERYABLE_BUILT_IN_ROLES_DIGEST_KEY); + } + indexMetadataBuilder.version(indexMetadataBuilder.version() + 1); + ImmutableOpenMap.Builder builder = ImmutableOpenMap.builder(state.metadata().indices()); + builder.put(concreteSecurityIndexName, indexMetadataBuilder.build()); + return new Tuple<>( + ClusterState.builder(state).metadata(Metadata.builder(state.metadata()).indices(builder.build()).build()).build(), + newRoleDigests + ); + } else { + // returns existing value when expectation is not met + return new Tuple<>(state, existingRoleDigests); + } + } + + void success(Map value) { + listener.onResponse(value); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + } + + private static class BulkDeleteRolesResponseException extends BulkRolesResponseException { + + BulkDeleteRolesResponseException(Map failures) { + super("Failed to bulk delete built-in roles", failures); + } + + } + + private static class BulkIndexRolesResponseException extends BulkRolesResponseException { + + BulkIndexRolesResponseException(Map failures) { + super("Failed to bulk create/update built-in roles", failures); + } + + } + + private abstract static class BulkRolesResponseException extends RuntimeException { + + private final Map failures; + + BulkRolesResponseException(String message, Map failures) { + super(message); + assert failures != null && failures.isEmpty() == false; + this.failures = failures; + failures.values().forEach(this::addSuppressed); + } + + Map getFailures() { + return failures; + } + + } + + private static class FailedToMarkBuiltInRolesAsSyncedException extends RuntimeException { + + FailedToMarkBuiltInRolesAsSyncedException(String message) { + super(message); + } + + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtils.java new file mode 100644 index 0000000000000..2d2eb345594ed --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtils.java @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.support; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.hash.MessageDigests; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.util.Maps; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; + +/** + * Utility class which provides helper method for calculating the hash of a role descriptor, + * determining the roles to upsert and the roles to delete. + */ +public final class QueryableBuiltInRolesUtils { + + /** + * Calculates the hash of the given role descriptor by serializing it by calling {@link RoleDescriptor#writeTo(StreamOutput)} method + * and then SHA256 hashing the bytes. + * + * @param roleDescriptor the role descriptor to hash + * @return the base64 encoded SHA256 hash of the role descriptor + */ + public static String calculateHash(final RoleDescriptor roleDescriptor) { + final MessageDigest hash = MessageDigests.sha256(); + try (XContentBuilder jsonBuilder = XContentFactory.jsonBuilder()) { + roleDescriptor.toXContent(jsonBuilder, EMPTY_PARAMS); + final Map flattenMap = Maps.flatten( + XContentHelper.convertToMap(BytesReference.bytes(jsonBuilder), true, XContentType.JSON).v2(), + false, + true + ); + hash.update(flattenMap.toString().getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new IllegalStateException("failed to compute digest for [" + roleDescriptor.getName() + "] role", e); + } + // HEX vs Base64 encoding is a trade-off between readability and space efficiency + // opting for Base64 here to reduce the size of the cluster state + return Base64.getEncoder().encodeToString(hash.digest()); + } + + /** + * Determines the roles to delete by comparing the indexed roles with the roles in the built-in roles. + * @return the set of roles to delete + */ + public static Set determineRolesToDelete(final QueryableBuiltInRoles roles, final Map indexedRolesDigests) { + assert roles != null; + if (indexedRolesDigests == null) { + // nothing indexed, nothing to delete + return Set.of(); + } + final Set rolesToDelete = Sets.difference(indexedRolesDigests.keySet(), roles.rolesDigest().keySet()); + return Collections.unmodifiableSet(rolesToDelete); + } + + /** + * Determines the roles to upsert by comparing the indexed roles and their digests with the current built-in roles. + * @return the set of roles to upsert (create or update) + */ + public static Set determineRolesToUpsert( + final QueryableBuiltInRoles roles, + final Map indexedRolesDigests + ) { + assert roles != null; + final Set rolesToUpsert = new HashSet<>(); + for (RoleDescriptor role : roles.roleDescriptors()) { + final String roleDigest = roles.rolesDigest().get(role.getName()); + if (indexedRolesDigests == null || indexedRolesDigests.containsKey(role.getName()) == false) { + rolesToUpsert.add(role); // a new role to create + } else if (indexedRolesDigests.get(role.getName()).equals(roleDigest) == false) { + rolesToUpsert.add(role); // an existing role that needs to be updated + } + } + return Collections.unmodifiableSet(rolesToUpsert); + } + + private QueryableBuiltInRolesUtils() { + throw new IllegalAccessError("not allowed"); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableReservedRolesProvider.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableReservedRolesProvider.java new file mode 100644 index 0000000000000..710e94b7ac879 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/QueryableReservedRolesProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.support; + +import org.elasticsearch.common.util.CachedSupplier; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * A provider of the built-in reserved roles. + *

+ * This provider fetches all reserved roles from the {@link ReservedRolesStore} and calculates their hashes lazily. + * The reserved roles are static and do not change during runtime, hence this provider will never notify any listeners. + *

+ */ +public final class QueryableReservedRolesProvider implements QueryableBuiltInRoles.Provider { + + private final Supplier reservedRolesSupplier; + + /** + * Constructs a new reserved roles provider. + * + * @param reservedRolesStore the store to fetch the reserved roles from. + * Having a store reference here is necessary to ensure that static fields are initialized. + */ + public QueryableReservedRolesProvider(ReservedRolesStore reservedRolesStore) { + this.reservedRolesSupplier = CachedSupplier.wrap(() -> { + final Collection roleDescriptors = Collections.unmodifiableCollection(ReservedRolesStore.roleDescriptors()); + return new QueryableBuiltInRoles( + roleDescriptors.stream() + .collect(Collectors.toUnmodifiableMap(RoleDescriptor::getName, QueryableBuiltInRolesUtils::calculateHash)), + roleDescriptors + ); + }); + } + + @Override + public QueryableBuiltInRoles getRoles() { + return reservedRolesSupplier.get(); + } + + @Override + public void addListener(QueryableBuiltInRoles.Listener listener) { + // no-op: reserved roles are static and do not change + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java index f3222a74b530c..78f7209c06e3a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java @@ -586,7 +586,7 @@ private static int readMappingVersion(String indexName, MappingMetadata mappingM * Resolves a concrete index name or alias to a {@link IndexMetadata} instance. Requires * that if supplied with an alias, the alias resolves to at most one concrete index. */ - private static IndexMetadata resolveConcreteIndex(final String indexOrAliasName, final Metadata metadata) { + public static IndexMetadata resolveConcreteIndex(final String indexOrAliasName, final Metadata metadata) { final IndexAbstraction indexAbstraction = metadata.getIndicesLookup().get(indexOrAliasName); if (indexAbstraction != null) { final List indices = indexAbstraction.getIndices(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java new file mode 100644 index 0000000000000..5b4787f25ae7f --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java @@ -0,0 +1,296 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.support; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptorTestHelper; +import org.elasticsearch.xpack.core.security.authz.permission.RemoteClusterPermissionGroup; +import org.elasticsearch.xpack.core.security.authz.permission.RemoteClusterPermissions; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.junit.BeforeClass; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.xpack.core.security.support.MetadataUtils.RESERVED_METADATA_KEY; +import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesUtils.determineRolesToDelete; +import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesUtils.determineRolesToUpsert; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +public class QueryableBuiltInRolesUtilsTests extends ESTestCase { + + @BeforeClass + public static void setupReservedRolesStore() { + new ReservedRolesStore(); // initialize the store + } + + public void testCalculateHash() { + assertThat( + QueryableBuiltInRolesUtils.calculateHash(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR), + equalTo("bWEFdFo4WX229wdhdecfiz5QHMYEssh3ex8hizRgg+Q=") + ); + } + + public void testEmptyOrNullRolesToUpsertOrDelete() { + // test empty roles and index digests + final QueryableBuiltInRoles emptyRoles = new QueryableBuiltInRoles(Map.of(), Set.of()); + assertThat(determineRolesToDelete(emptyRoles, Map.of()), is(empty())); + assertThat(determineRolesToUpsert(emptyRoles, Map.of()), is(empty())); + + // test empty roles and null indexed digests + assertThat(determineRolesToDelete(emptyRoles, null), is(empty())); + assertThat(determineRolesToUpsert(emptyRoles, null), is(empty())); + } + + public void testNoRolesToUpsertOrDelete() { + { + QueryableBuiltInRoles currentBuiltInRoles = buildQueryableBuiltInRoles( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor") + ) + ); + + // no roles to delete or upsert since the built-in roles are the same as the indexed roles + assertThat(determineRolesToDelete(currentBuiltInRoles, currentBuiltInRoles.rolesDigest()), is(empty())); + assertThat(determineRolesToUpsert(currentBuiltInRoles, currentBuiltInRoles.rolesDigest()), is(empty())); + } + { + QueryableBuiltInRoles currentBuiltInRoles = buildQueryableBuiltInRoles( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor"), + supermanRole("monitor", "read") + ) + ); + + Map digests = buildDigests( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor"), + supermanRole("monitor", "read") + ) + ); + + // no roles to delete or upsert since the built-in roles are the same as the indexed roles + assertThat(determineRolesToDelete(currentBuiltInRoles, digests), is(empty())); + assertThat(determineRolesToUpsert(currentBuiltInRoles, digests), is(empty())); + } + { + final RoleDescriptor randomRole = RoleDescriptorTestHelper.randomRoleDescriptor(); + final QueryableBuiltInRoles currentBuiltInRoles = buildQueryableBuiltInRoles(Set.of(randomRole)); + final Map digests = buildDigests( + Set.of( + new RoleDescriptor( + randomRole.getName(), + randomRole.getClusterPrivileges(), + randomRole.getIndicesPrivileges(), + randomRole.getApplicationPrivileges(), + randomRole.getConditionalClusterPrivileges(), + randomRole.getRunAs(), + randomRole.getMetadata(), + randomRole.getTransientMetadata(), + randomRole.getRemoteIndicesPrivileges(), + randomRole.getRemoteClusterPermissions(), + randomRole.getRestriction(), + randomRole.getDescription() + ) + ) + ); + + assertThat(determineRolesToDelete(currentBuiltInRoles, digests), is(empty())); + assertThat(determineRolesToUpsert(currentBuiltInRoles, digests), is(empty())); + } + } + + public void testRolesToDeleteOnly() { + Map indexedDigests = buildDigests( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor"), + supermanRole("monitor", "read", "view_index_metadata", "read_cross_cluster") + ) + ); + + QueryableBuiltInRoles currentBuiltInRoles = buildQueryableBuiltInRoles( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor") + ) + ); + + // superman is the only role that needs to be deleted since it is not in a current built-in role + assertThat(determineRolesToDelete(currentBuiltInRoles, indexedDigests), containsInAnyOrder("superman")); + assertThat(determineRolesToUpsert(currentBuiltInRoles, indexedDigests), is(empty())); + + // passing empty built-in roles should result in all indexed roles needing to be deleted + QueryableBuiltInRoles emptyBuiltInRoles = new QueryableBuiltInRoles(Map.of(), Set.of()); + assertThat( + determineRolesToDelete(emptyBuiltInRoles, indexedDigests), + containsInAnyOrder("superman", "viewer", "editor", "superuser") + ); + assertThat(determineRolesToUpsert(emptyBuiltInRoles, indexedDigests), is(empty())); + } + + public void testRolesToUpdateOnly() { + Map indexedDigests = buildDigests( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor"), + supermanRole("monitor", "read", "write") + ) + ); + + RoleDescriptor updatedSupermanRole = supermanRole("monitor", "read", "view_index_metadata", "read_cross_cluster"); + QueryableBuiltInRoles currentBuiltInRoles = buildQueryableBuiltInRoles( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor"), + updatedSupermanRole + ) + ); + + // superman is the only role that needs to be updated since its definition has changed + assertThat(determineRolesToDelete(currentBuiltInRoles, indexedDigests), is(empty())); + assertThat(determineRolesToUpsert(currentBuiltInRoles, indexedDigests), containsInAnyOrder(updatedSupermanRole)); + assertThat(currentBuiltInRoles.rolesDigest().get("superman"), is(not(equalTo(indexedDigests.get("superman"))))); + } + + public void testRolesToCreateOnly() { + Map indexedDigests = buildDigests( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor") + ) + ); + + RoleDescriptor newSupermanRole = supermanRole("monitor", "read", "view_index_metadata", "read_cross_cluster"); + QueryableBuiltInRoles currentBuiltInRoles = buildQueryableBuiltInRoles( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor"), + newSupermanRole + ) + ); + + // superman is the only role that needs to be created since it is not in the indexed roles + assertThat(determineRolesToDelete(currentBuiltInRoles, indexedDigests), is(empty())); + assertThat(determineRolesToUpsert(currentBuiltInRoles, indexedDigests), containsInAnyOrder(newSupermanRole)); + + // passing empty indexed roles should result in all roles needing to be created + assertThat(determineRolesToDelete(currentBuiltInRoles, Map.of()), is(empty())); + assertThat( + determineRolesToUpsert(currentBuiltInRoles, Map.of()), + containsInAnyOrder(currentBuiltInRoles.roleDescriptors().toArray(new RoleDescriptor[0])) + ); + } + + public void testRolesToUpsertAndDelete() { + Map indexedDigests = buildDigests( + Set.of( + ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, + ReservedRolesStore.roleDescriptor("viewer"), + ReservedRolesStore.roleDescriptor("editor") + ) + ); + + RoleDescriptor newSupermanRole = supermanRole("monitor"); + QueryableBuiltInRoles currentBuiltInRoles = buildQueryableBuiltInRoles( + Set.of(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, newSupermanRole) + ); + + // superman is the only role that needs to be updated since its definition has changed + assertThat(determineRolesToDelete(currentBuiltInRoles, indexedDigests), containsInAnyOrder("viewer", "editor")); + assertThat(determineRolesToUpsert(currentBuiltInRoles, indexedDigests), containsInAnyOrder(newSupermanRole)); + } + + private static RoleDescriptor supermanRole(String... indicesPrivileges) { + return new RoleDescriptor( + "superman", + new String[] { "all" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(false).build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + .privileges(indicesPrivileges) + .allowRestrictedIndices(true) + .build() }, + new RoleDescriptor.ApplicationResourcePrivileges[] { + RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build() }, + null, + new String[] { "*" }, + randomlyOrderedSupermanMetadata(), + Collections.emptyMap(), + new RoleDescriptor.RemoteIndicesPrivileges[] { + new RoleDescriptor.RemoteIndicesPrivileges( + RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(false).build(), + "*" + ), + new RoleDescriptor.RemoteIndicesPrivileges( + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + .privileges(indicesPrivileges) + .allowRestrictedIndices(true) + .build(), + "*" + ) }, + new RemoteClusterPermissions().addGroup( + new RemoteClusterPermissionGroup( + RemoteClusterPermissions.getSupportedRemoteClusterPermissions().toArray(new String[0]), + new String[] { "*" } + ) + ), + null, + "Grants full access to cluster management and data indices." + ); + } + + private static Map randomlyOrderedSupermanMetadata() { + final LinkedHashMap metadata = new LinkedHashMap<>(); + if (randomBoolean()) { + metadata.put("foo", "bar"); + metadata.put("baz", "qux"); + metadata.put(RESERVED_METADATA_KEY, true); + } else { + metadata.put(RESERVED_METADATA_KEY, true); + metadata.put("foo", "bar"); + metadata.put("baz", "qux"); + } + return metadata; + } + + private static QueryableBuiltInRoles buildQueryableBuiltInRoles(Set roles) { + final Map digests = buildDigests(roles); + return new QueryableBuiltInRoles(digests, roles); + } + + private static Map buildDigests(Set roles) { + final Map digests = new HashMap<>(); + for (RoleDescriptor role : roles) { + digests.put(role.getName(), QueryableBuiltInRolesUtils.calculateHash(role)); + } + return digests; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableReservedRolesProviderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableReservedRolesProviderTests.java new file mode 100644 index 0000000000000..7beb078795b29 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableReservedRolesProviderTests.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.support; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; + +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.equalTo; + +public class QueryableReservedRolesProviderTests extends ESTestCase { + + public void testReservedRoleProvider() { + QueryableReservedRolesProvider provider = new QueryableReservedRolesProvider(new ReservedRolesStore()); + assertNotNull(provider.getRoles()); + assertThat(provider.getRoles(), equalTo(provider.getRoles())); + assertThat(provider.getRoles().rolesDigest().size(), equalTo(ReservedRolesStore.roleDescriptors().size())); + assertThat( + provider.getRoles().rolesDigest().keySet(), + equalTo(ReservedRolesStore.roleDescriptors().stream().map(RoleDescriptor::getName).collect(Collectors.toSet())) + ); + } + +} From cf1f2cbc34e97b8d280ee74e824ac1106c24cef0 Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Mon, 16 Dec 2024 14:08:11 -0500 Subject: [PATCH 016/119] Unmute ml/sparse_vector_search/Test sparse_vector search with query vector and pruning config (#118788) * Unmute ml/sparse_vector_search/Test sparse_vector search with query vector and pruning config * Revert "Unmute ml/sparse_vector_search/Test sparse_vector search with query vector and pruning config" This reverts commit 48e76936e622072da7c214842613d22b9830dc45. * Unmute test, this time without formatting the entire file --- muted-tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index 2689f02cc92cd..be3805d887bbe 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -40,9 +40,6 @@ tests: - class: org.elasticsearch.packaging.test.WindowsServiceTests method: test33JavaChanged issue: https://github.com/elastic/elasticsearch/issues/113177 -- class: org.elasticsearch.smoketest.MlWithSecurityIT - method: test {yaml=ml/sparse_vector_search/Test sparse_vector search with query vector and pruning config} - issue: https://github.com/elastic/elasticsearch/issues/108997 - class: org.elasticsearch.packaging.test.WindowsServiceTests method: test80JavaOptsInEnvVar issue: https://github.com/elastic/elasticsearch/issues/113219 From 8e988689438f9579dc23427efc85d320e8289ce3 Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Mon, 16 Dec 2024 21:26:45 +0100 Subject: [PATCH 017/119] Support multi-index LOOKUP JOIN and various bug fixes (#118429) While working on supporting multi-index LOOKUP JOIN; various bugs were fixed: * Previously we just used '*' for lookup-join indices, because the fieldnames were sometimes not being correctly determined. The problem was with KEEP referencing fields from the right that had previously been defined on the left as aliases, including using the ROW command. We normally don't want to ask for aliases, but if they could be shadowed by a lookup join, we need to keep them. * With both single and multi-index LOOKUP JOIN we need to mark each index as potentially wildcard fields, if the KEEP commands occur before the LOOKUP JOIN. --- .../esql/qa/mixed/MixedClusterEsqlSpecIT.java | 4 +- .../xpack/esql/ccq/MultiClusterSpecIT.java | 8 +- .../rest/RequestIndexFilteringTestCase.java | 11 + .../src/main/resources/lookup-join.csv-spec | 566 +++++++++++++++--- .../src/main/resources/message_types.csv | 2 + .../xpack/esql/action/EsqlCapabilities.java | 2 +- .../xpack/esql/analysis/Analyzer.java | 12 +- .../xpack/esql/analysis/AnalyzerContext.java | 12 +- .../xpack/esql/session/EsqlSession.java | 269 ++++----- .../elasticsearch/xpack/esql/CsvTests.java | 2 +- .../esql/analysis/AnalyzerTestUtils.java | 4 +- .../xpack/esql/analysis/AnalyzerTests.java | 6 +- .../xpack/esql/analysis/VerifierTests.java | 2 +- .../optimizer/LogicalPlanOptimizerTests.java | 37 +- .../optimizer/PhysicalPlanOptimizerTests.java | 4 +- .../session/IndexResolverFieldNamesTests.java | 226 ++++++- 16 files changed, 902 insertions(+), 265 deletions(-) diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java index 1120a69cc5166..5efe7ffc800a2 100644 --- a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java @@ -21,7 +21,7 @@ import java.util.List; import static org.elasticsearch.xpack.esql.CsvTestUtils.isEnabled; -import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V5; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V6; import static org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase.Mode.ASYNC; public class MixedClusterEsqlSpecIT extends EsqlSpecTestCase { @@ -96,7 +96,7 @@ protected boolean supportsInferenceTestService() { @Override protected boolean supportsIndexModeLookup() throws IOException { - return hasCapabilities(List.of(JOIN_LOOKUP_V5.capabilityName())); + return hasCapabilities(List.of(JOIN_LOOKUP_V6.capabilityName())); } @Override diff --git a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java index 5c7f981c93a97..dd75776973c3d 100644 --- a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java +++ b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java @@ -48,7 +48,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.classpathResources; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS_V2; -import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V5; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V6; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_PLANNING_V1; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.METADATA_FIELDS_REMOTE_TEST; import static org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase.Mode.SYNC; @@ -124,7 +124,7 @@ protected void shouldSkipTest(String testName) throws IOException { assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS.capabilityName())); assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS_V2.capabilityName())); assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_PLANNING_V1.capabilityName())); - assumeFalse("LOOKUP JOIN not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_LOOKUP_V5.capabilityName())); + assumeFalse("LOOKUP JOIN not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_LOOKUP_V6.capabilityName())); } private TestFeatureService remoteFeaturesService() throws IOException { @@ -283,8 +283,8 @@ protected boolean supportsInferenceTestService() { @Override protected boolean supportsIndexModeLookup() throws IOException { - // CCS does not yet support JOIN_LOOKUP_V5 and clusters falsely report they have this capability - // return hasCapabilities(List.of(JOIN_LOOKUP_V5.capabilityName())); + // CCS does not yet support JOIN_LOOKUP_V6 and clusters falsely report they have this capability + // return hasCapabilities(List.of(JOIN_LOOKUP_V6.capabilityName())); return false; } } diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java index 406997b66dbf0..2aae4c94c33fe 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java @@ -14,6 +14,7 @@ import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.esql.AssertWarnings; +import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.junit.After; import org.junit.Assert; @@ -219,6 +220,16 @@ public void testIndicesDontExist() throws IOException { assertEquals(404, e.getResponse().getStatusLine().getStatusCode()); assertThat(e.getMessage(), containsString("index_not_found_exception")); assertThat(e.getMessage(), containsString("no such index [foo]")); + + if (EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()) { + e = expectThrows( + ResponseException.class, + () -> runEsql(timestampFilter("gte", "2020-01-01").query("FROM test1 | LOOKUP JOIN foo ON id1")) + ); + assertEquals(400, e.getResponse().getStatusLine().getStatusCode()); + assertThat(e.getMessage(), containsString("verification_exception")); + assertThat(e.getMessage(), containsString("Unknown index [foo]")); + } } private static RestEsqlTestCase.RequestObjectBuilder timestampFilter(String op, String date) throws IOException { diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec index 7fed4f377096f..8b8d24b1bb156 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec @@ -3,8 +3,12 @@ // Reuses the sample dataset and commands from enrich.csv-spec // +############################################### +# Tests with languages_lookup index +############################################### + basicOnTheDataNode -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | EVAL language_code = languages @@ -21,7 +25,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; basicRow -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 ROW language_code = 1 | LOOKUP JOIN languages_lookup ON language_code @@ -32,7 +36,7 @@ language_code:integer | language_name:keyword ; basicOnTheCoordinator -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | SORT emp_no @@ -49,7 +53,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; subsequentEvalOnTheDataNode -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | EVAL language_code = languages @@ -67,7 +71,7 @@ emp_no:integer | language_code:integer | language_name:keyword | language_code_x ; subsequentEvalOnTheCoordinator -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | SORT emp_no @@ -85,7 +89,7 @@ emp_no:integer | language_code:integer | language_name:keyword | language_code_x ; sortEvalBeforeLookup -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | SORT emp_no @@ -102,7 +106,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; nonUniqueLeftKeyOnTheDataNode -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | WHERE emp_no <= 10030 @@ -126,7 +130,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; nonUniqueRightKeyOnTheDataNode -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | EVAL language_code = emp_no % 10 @@ -146,7 +150,7 @@ emp_no:integer | language_code:integer | language_name:keyword | country:k ; nonUniqueRightKeyOnTheCoordinator -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | SORT emp_no @@ -166,7 +170,7 @@ emp_no:integer | language_code:integer | language_name:keyword | country:k ; nonUniqueRightKeyFromRow -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 ROW language_code = 2 | LOOKUP JOIN languages_lookup_non_unique_key ON language_code @@ -178,8 +182,73 @@ language_code:integer | language_name:keyword | country:keyword 2 | [German, German, German] | [Austria, Germany, Switzerland] ; +############################################### +# Filtering tests with languages_lookup index +############################################### + +lookupWithFilterOnLeftSideField +required_capability: join_lookup_v6 + +FROM employees +| EVAL language_code = languages +| LOOKUP JOIN languages_lookup ON language_code +| SORT emp_no +| KEEP emp_no, language_code, language_name +| WHERE emp_no >= 10091 AND emp_no < 10094 +; + +emp_no:integer | language_code:integer | language_name:keyword +10091 | 3 | Spanish +10092 | 1 | English +10093 | 3 | Spanish +; + +lookupMessageWithFilterOnRightSideField-Ignore +required_capability: join_lookup_v6 + +FROM sample_data +| LOOKUP JOIN message_types_lookup ON message +| WHERE type == "Error" +| KEEP @timestamp, client_ip, event_duration, message, type +| SORT @timestamp DESC +; + +@timestamp:date | client_ip:ip | event_duration:long | message:keyword | type:keyword +2023-10-23T13:53:55.832Z | 172.21.3.15 | 5033755 | Connection error | Error +2023-10-23T13:52:55.015Z | 172.21.3.15 | 8268153 | Connection error | Error +2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Connection error | Error +; + +lookupWithFieldAndRightSideAfterStats +required_capability: join_lookup_v6 + +FROM sample_data +| LOOKUP JOIN message_types_lookup ON message +| STATS count = count(message) BY type +| WHERE type == "Error" +; + +count:long | type:keyword +3 | Error +; + +lookupWithFieldOnJoinKey-Ignore +required_capability: join_lookup_v6 + +FROM employees +| EVAL language_code = languages +| LOOKUP JOIN languages_lookup ON language_code +| WHERE language_code > 1 AND language_name IS NOT NULL +| KEEP emp_no, language_code, language_name +; + +emp_no:integer | language_code:integer | language_name:keyword +10001 | 2 | French +10003 | 4 | German +; + nullJoinKeyOnTheDataNode -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | WHERE emp_no < 10004 @@ -197,7 +266,7 @@ emp_no:integer | language_code:integer | language_name:keyword mvJoinKeyOnTheDataNode -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM employees | WHERE 10003 < emp_no AND emp_no < 10008 @@ -215,7 +284,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; mvJoinKeyFromRow -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 ROW language_code = [4, 5, 6, 7] | LOOKUP JOIN languages_lookup_non_unique_key ON language_code @@ -228,7 +297,7 @@ language_code:integer | language_name:keyword | country:keyword ; mvJoinKeyFromRowExpanded -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 ROW language_code = [4, 5, 6, 7, 8] | MV_EXPAND language_code @@ -245,10 +314,26 @@ language_code:integer | language_name:keyword | country:keyword 8 | Mv-Lang2 | Mv-Land2 ; +############################################### +# Tests with clientips_lookup index +############################################### + lookupIPFromRow -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", right = "right" +| LOOKUP JOIN clientips_lookup ON client_ip +; + +left:keyword | client_ip:keyword | right:keyword | env:keyword +left | 172.21.0.5 | right | Development +; + +lookupIPFromKeepRow +required_capability: join_lookup_v6 ROW left = "left", client_ip = "172.21.0.5", right = "right" +| KEEP left, client_ip, right | LOOKUP JOIN clientips_lookup ON client_ip ; @@ -257,7 +342,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowing -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -268,7 +353,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowingKeep -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -281,7 +366,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowingKeepReordered -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -294,7 +379,7 @@ right | Development | 172.21.0.5 ; lookupIPFromIndex -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -313,7 +398,7 @@ ignoreOrder:true ; lookupIPFromIndexKeep -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -332,8 +417,30 @@ ignoreOrder:true 2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 | QA ; +lookupIPFromIndexKeepKeep +required_capability: join_lookup_v6 + +FROM sample_data +| KEEP client_ip, event_duration, @timestamp, message +| RENAME @timestamp AS timestamp, message AS msg +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip +| KEEP timestamp, client_ip, event_duration, msg, env +; +ignoreOrder:true + +timestamp:date | client_ip:keyword | event_duration:long | msg:keyword | env:keyword +2023-10-23T13:55:01.543Z | 172.21.3.15 | 1756467 | Connected to 10.1.0.1 | Production +2023-10-23T13:53:55.832Z | 172.21.3.15 | 5033755 | Connection error | Production +2023-10-23T13:52:55.015Z | 172.21.3.15 | 8268153 | Connection error | Production +2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Connection error | Production +2023-10-23T13:33:34.937Z | 172.21.0.5 | 1232382 | Disconnected | Development +2023-10-23T12:27:28.948Z | 172.21.2.113 | 2764889 | Connected to 10.1.0.2 | QA +2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 | QA +; + lookupIPFromIndexStats -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -349,7 +456,7 @@ count:long | env:keyword ; lookupIPFromIndexStatsKeep -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -365,10 +472,43 @@ count:long | env:keyword 1 | Development ; +statsAndLookupIPFromIndex +required_capability: join_lookup_v6 + +FROM sample_data +| EVAL client_ip = client_ip::keyword +| STATS count = count(client_ip) BY client_ip +| LOOKUP JOIN clientips_lookup ON client_ip +| SORT count DESC, client_ip ASC, env ASC +; + +count:long | client_ip:keyword | env:keyword +4 | 172.21.3.15 | Production +1 | 172.21.0.5 | Development +1 | 172.21.2.113 | QA +1 | 172.21.2.162 | QA +; + +############################################### +# Tests with message_types_lookup index +############################################### + lookupMessageFromRow -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 + +ROW left = "left", message = "Connected to 10.1.0.1", right = "right" +| LOOKUP JOIN message_types_lookup ON message +; + +left:keyword | message:keyword | right:keyword | type:keyword +left | Connected to 10.1.0.1 | right | Success +; + +lookupMessageFromKeepRow +required_capability: join_lookup_v6 ROW left = "left", message = "Connected to 10.1.0.1", right = "right" +| KEEP left, message, right | LOOKUP JOIN message_types_lookup ON message ; @@ -377,7 +517,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromRowWithShadowing -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 ROW left = "left", message = "Connected to 10.1.0.1", type = "unknown", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -388,7 +528,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromRowWithShadowingKeep -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 ROW left = "left", message = "Connected to 10.1.0.1", type = "unknown", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -400,7 +540,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromIndex -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -418,7 +558,7 @@ ignoreOrder:true ; lookupMessageFromIndexKeep -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -436,8 +576,28 @@ ignoreOrder:true 2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 | Success ; +lookupMessageFromIndexKeepKeep +required_capability: join_lookup_v6 + +FROM sample_data +| KEEP client_ip, event_duration, @timestamp, message +| LOOKUP JOIN message_types_lookup ON message +| KEEP @timestamp, client_ip, event_duration, message, type +; +ignoreOrder:true + +@timestamp:date | client_ip:ip | event_duration:long | message:keyword | type:keyword +2023-10-23T13:55:01.543Z | 172.21.3.15 | 1756467 | Connected to 10.1.0.1 | Success +2023-10-23T13:53:55.832Z | 172.21.3.15 | 5033755 | Connection error | Error +2023-10-23T13:52:55.015Z | 172.21.3.15 | 8268153 | Connection error | Error +2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Connection error | Error +2023-10-23T13:33:34.937Z | 172.21.0.5 | 1232382 | Disconnected | Disconnected +2023-10-23T12:27:28.948Z | 172.21.2.113 | 2764889 | Connected to 10.1.0.2 | Success +2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 | Success +; + lookupMessageFromIndexKeepReordered -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -456,7 +616,7 @@ Success | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 ; lookupMessageFromIndexStats -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -471,7 +631,7 @@ count:long | type:keyword ; lookupMessageFromIndexStatsKeep -required_capability: join_lookup_v5 +required_capability: join_lookup_v6 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -486,67 +646,333 @@ count:long | type:keyword 1 | Disconnected ; -// -// Filtering tests -// +statsAndLookupMessageFromIndex +required_capability: join_lookup_v6 -lookupWithFilterOnLeftSideField -required_capability: join_lookup_v5 +FROM sample_data +| STATS count = count(message) BY message +| LOOKUP JOIN message_types_lookup ON message +| KEEP count, type, message +| SORT count DESC, message ASC +; -FROM employees -| EVAL language_code = languages -| LOOKUP JOIN languages_lookup ON language_code -| SORT emp_no -| KEEP emp_no, language_code, language_name -| WHERE emp_no >= 10091 AND emp_no < 10094 +count:long | type:keyword | message:keyword +3 | Error | Connection error +1 | Success | Connected to 10.1.0.1 +1 | Success | Connected to 10.1.0.2 +1 | Success | Connected to 10.1.0.3 +1 | Disconnected | Disconnected ; -emp_no:integer | language_code:integer | language_name:keyword -10091 | 3 | Spanish -10092 | 1 | English -10093 | 3 | Spanish +lookupMessageFromIndexTwice +required_capability: join_lookup_v6 + +FROM sample_data +| LOOKUP JOIN message_types_lookup ON message +| RENAME message AS message1, type AS type1 +| EVAL message = client_ip::keyword +| LOOKUP JOIN message_types_lookup ON message +| RENAME message AS message2, type AS type2 ; +ignoreOrder:true -lookupMessageWithFilterOnRightSideField-Ignore -required_capability: join_lookup_v5 +@timestamp:date | client_ip:ip | event_duration:long | message1:keyword | type1:keyword | message2:keyword | type2:keyword +2023-10-23T13:55:01.543Z | 172.21.3.15 | 1756467 | Connected to 10.1.0.1 | Success | 172.21.3.15 | null +2023-10-23T13:53:55.832Z | 172.21.3.15 | 5033755 | Connection error | Error | 172.21.3.15 | null +2023-10-23T13:52:55.015Z | 172.21.3.15 | 8268153 | Connection error | Error | 172.21.3.15 | null +2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Connection error | Error | 172.21.3.15 | null +2023-10-23T13:33:34.937Z | 172.21.0.5 | 1232382 | Disconnected | Disconnected | 172.21.0.5 | null +2023-10-23T12:27:28.948Z | 172.21.2.113 | 2764889 | Connected to 10.1.0.2 | Success | 172.21.2.113 | null +2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 | Success | 172.21.2.162 | null +; + +lookupMessageFromIndexTwiceKeep +required_capability: join_lookup_v6 FROM sample_data | LOOKUP JOIN message_types_lookup ON message -| WHERE type == "Error" -| KEEP @timestamp, client_ip, event_duration, message, type -| SORT @timestamp DESC +| RENAME message AS message1, type AS type1 +| EVAL message = client_ip::keyword +| LOOKUP JOIN message_types_lookup ON message +| RENAME message AS message2, type AS type2 +| KEEP @timestamp, client_ip, event_duration, message1, type1, message2, type2 ; +ignoreOrder:true -@timestamp:date | client_ip:ip | event_duration:long | message:keyword | type:keyword -2023-10-23T13:53:55.832Z | 172.21.3.15 | 5033755 | Connection error | Error -2023-10-23T13:52:55.015Z | 172.21.3.15 | 8268153 | Connection error | Error -2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Connection error | Error +@timestamp:date | client_ip:ip | event_duration:long | message1:keyword | type1:keyword | message2:keyword | type2:keyword +2023-10-23T13:55:01.543Z | 172.21.3.15 | 1756467 | Connected to 10.1.0.1 | Success | 172.21.3.15 | null +2023-10-23T13:53:55.832Z | 172.21.3.15 | 5033755 | Connection error | Error | 172.21.3.15 | null +2023-10-23T13:52:55.015Z | 172.21.3.15 | 8268153 | Connection error | Error | 172.21.3.15 | null +2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Connection error | Error | 172.21.3.15 | null +2023-10-23T13:33:34.937Z | 172.21.0.5 | 1232382 | Disconnected | Disconnected | 172.21.0.5 | null +2023-10-23T12:27:28.948Z | 172.21.2.113 | 2764889 | Connected to 10.1.0.2 | Success | 172.21.2.113 | null +2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 | Success | 172.21.2.162 | null ; -lookupWithFieldAndRightSideAfterStats -required_capability: join_lookup_v5 +############################################### +# Tests with clientips_lookup and message_types_lookup indexes +############################################### + +lookupIPAndMessageFromRow +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +; + +left:keyword | client_ip:keyword | message:keyword | right:keyword | env:keyword | type:keyword +left | 172.21.0.5 | Connected to 10.1.0.1 | right | Development | Success +; + +lookupIPAndMessageFromRowKeepBefore +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" +| KEEP left, client_ip, message, right +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +; + +left:keyword | client_ip:keyword | message:keyword | right:keyword | env:keyword | type:keyword +left | 172.21.0.5 | Connected to 10.1.0.1 | right | Development | Success +; + +lookupIPAndMessageFromRowKeepBetween +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" +| LOOKUP JOIN clientips_lookup ON client_ip +| KEEP left, client_ip, message, right, env +| LOOKUP JOIN message_types_lookup ON message +; + +left:keyword | client_ip:keyword | message:keyword | right:keyword | env:keyword | type:keyword +left | 172.21.0.5 | Connected to 10.1.0.1 | right | Development | Success +; + +lookupIPAndMessageFromRowKeepAfter +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +| KEEP left, client_ip, message, right, env, type +; + +left:keyword | client_ip:keyword | message:keyword | right:keyword | env:keyword | type:keyword +left | 172.21.0.5 | Connected to 10.1.0.1 | right | Development | Success +; + +lookupIPAndMessageFromRowWithShadowing +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", type = "type", right = "right" +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +; + +left:keyword | client_ip:keyword | message:keyword | right:keyword | env:keyword | type:keyword +left | 172.21.0.5 | Connected to 10.1.0.1 | right | Development | Success +; + +lookupIPAndMessageFromRowWithShadowingKeep +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +| KEEP left, client_ip, message, right, env, type +; + +left:keyword | client_ip:keyword | message:keyword | right:keyword | env:keyword | type:keyword +left | 172.21.0.5 | Connected to 10.1.0.1 | right | Development | Success +; + +lookupIPAndMessageFromRowWithShadowingKeepKeep +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip +| KEEP left, client_ip, message, right, env +| LOOKUP JOIN message_types_lookup ON message +| KEEP left, client_ip, message, right, env, type +; + +left:keyword | client_ip:keyword | message:keyword | right:keyword | env:keyword | type:keyword +left | 172.21.0.5 | Connected to 10.1.0.1 | right | Development | Success +; + +lookupIPAndMessageFromRowWithShadowingKeepKeepKeep +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" +| EVAL client_ip = client_ip::keyword +| KEEP left, client_ip, message, right, env +| LOOKUP JOIN clientips_lookup ON client_ip +| KEEP left, client_ip, message, right, env +| LOOKUP JOIN message_types_lookup ON message +| KEEP left, client_ip, message, right, env, type +; + +left:keyword | client_ip:keyword | message:keyword | right:keyword | env:keyword | type:keyword +left | 172.21.0.5 | Connected to 10.1.0.1 | right | Development | Success +; + +lookupIPAndMessageFromRowWithShadowingKeepReordered +required_capability: join_lookup_v6 + +ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +| KEEP right, env, type, client_ip +; + +right:keyword | env:keyword | type:keyword | client_ip:keyword +right | Development | Success | 172.21.0.5 +; + +lookupIPAndMessageFromIndex +required_capability: join_lookup_v6 FROM sample_data +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip | LOOKUP JOIN message_types_lookup ON message -| STATS count = count(message) BY type -| WHERE type == "Error" ; +ignoreOrder:true -count:long | type:keyword -3 | Error +@timestamp:date | event_duration:long | message:keyword | client_ip:keyword | env:keyword | type:keyword +2023-10-23T13:55:01.543Z | 1756467 | Connected to 10.1.0.1 | 172.21.3.15 | Production | Success +2023-10-23T13:53:55.832Z | 5033755 | Connection error | 172.21.3.15 | Production | Error +2023-10-23T13:52:55.015Z | 8268153 | Connection error | 172.21.3.15 | Production | Error +2023-10-23T13:51:54.732Z | 725448 | Connection error | 172.21.3.15 | Production | Error +2023-10-23T13:33:34.937Z | 1232382 | Disconnected | 172.21.0.5 | Development | Disconnected +2023-10-23T12:27:28.948Z | 2764889 | Connected to 10.1.0.2 | 172.21.2.113 | QA | Success +2023-10-23T12:15:03.360Z | 3450233 | Connected to 10.1.0.3 | 172.21.2.162 | QA | Success ; -lookupWithFieldOnJoinKey-Ignore -required_capability: join_lookup_v5 +lookupIPAndMessageFromIndexKeep +required_capability: join_lookup_v6 -FROM employees -| EVAL language_code = languages -| LOOKUP JOIN languages_lookup ON language_code -| WHERE language_code > 1 AND language_name IS NOT NULL -| KEEP emp_no, language_code, language_name +FROM sample_data +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +| KEEP @timestamp, client_ip, event_duration, message, env, type ; +ignoreOrder:true -emp_no:integer | language_code:integer | language_name:keyword -10001 | 2 | French -10003 | 4 | German +@timestamp:date | client_ip:keyword | event_duration:long | message:keyword | env:keyword | type:keyword +2023-10-23T13:55:01.543Z | 172.21.3.15 | 1756467 | Connected to 10.1.0.1 | Production | Success +2023-10-23T13:53:55.832Z | 172.21.3.15 | 5033755 | Connection error | Production | Error +2023-10-23T13:52:55.015Z | 172.21.3.15 | 8268153 | Connection error | Production | Error +2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Connection error | Production | Error +2023-10-23T13:33:34.937Z | 172.21.0.5 | 1232382 | Disconnected | Development | Disconnected +2023-10-23T12:27:28.948Z | 172.21.2.113 | 2764889 | Connected to 10.1.0.2 | QA | Success +2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 | QA | Success +; + +lookupIPAndMessageFromIndexStats +required_capability: join_lookup_v6 + +FROM sample_data +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +| STATS count = count(*) BY env, type +| SORT count DESC, env ASC, type ASC +; + +count:long | env:keyword | type:keyword +3 | Production | Error +2 | QA | Success +1 | Development | Disconnected +1 | Production | Success +; + +lookupIPAndMessageFromIndexStatsKeep +required_capability: join_lookup_v6 + +FROM sample_data +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +| KEEP client_ip, env, type +| STATS count = count(*) BY env, type +| SORT count DESC, env ASC, type ASC +; + +count:long | env:keyword | type:keyword +3 | Production | Error +2 | QA | Success +1 | Development | Disconnected +1 | Production | Success +; + +statsAndLookupIPAndMessageFromIndex +required_capability: join_lookup_v6 + +FROM sample_data +| EVAL client_ip = client_ip::keyword +| STATS count = count(*) BY client_ip, message +| LOOKUP JOIN clientips_lookup ON client_ip +| LOOKUP JOIN message_types_lookup ON message +| SORT count DESC, client_ip ASC, message ASC +; + +count:long | client_ip:keyword | message:keyword | env:keyword | type:keyword +3 | 172.21.3.15 | Connection error | Production | Error +1 | 172.21.0.5 | Disconnected | Development | Disconnected +1 | 172.21.2.113 | Connected to 10.1.0.2 | QA | Success +1 | 172.21.2.162 | Connected to 10.1.0.3 | QA | Success +1 | 172.21.3.15 | Connected to 10.1.0.1 | Production | Success +; + +lookupIPAndMessageFromIndexChainedEvalKeep +required_capability: join_lookup_v6 + +FROM sample_data +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip +| EVAL message = CONCAT(env, " environment") +| LOOKUP JOIN message_types_lookup ON message +| KEEP @timestamp, client_ip, event_duration, message, type +; +ignoreOrder:true + +@timestamp:date | client_ip:keyword | event_duration:long | message:keyword | type:keyword +2023-10-23T13:55:01.543Z | 172.21.3.15 | 1756467 | Production environment | Production +2023-10-23T13:53:55.832Z | 172.21.3.15 | 5033755 | Production environment | Production +2023-10-23T13:52:55.015Z | 172.21.3.15 | 8268153 | Production environment | Production +2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Production environment | Production +2023-10-23T13:33:34.937Z | 172.21.0.5 | 1232382 | Development environment | Development +2023-10-23T12:27:28.948Z | 172.21.2.113 | 2764889 | QA environment | null +2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | QA environment | null +; + +lookupIPAndMessageFromIndexChainedRenameKeep +required_capability: join_lookup_v6 + +FROM sample_data +| EVAL client_ip = client_ip::keyword +| LOOKUP JOIN clientips_lookup ON client_ip +| RENAME env AS message +| LOOKUP JOIN message_types_lookup ON message +| KEEP @timestamp, client_ip, event_duration, message, type ; +ignoreOrder:true + +@timestamp:date | client_ip:keyword | event_duration:long | message:keyword | type:keyword +2023-10-23T13:55:01.543Z | 172.21.3.15 | 1756467 | Production | null +2023-10-23T13:53:55.832Z | 172.21.3.15 | 5033755 | Production | null +2023-10-23T13:52:55.015Z | 172.21.3.15 | 8268153 | Production | null +2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Production | null +2023-10-23T13:33:34.937Z | 172.21.0.5 | 1232382 | Development | null +2023-10-23T12:27:28.948Z | 172.21.2.113 | 2764889 | QA | null +2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | QA | null +; + diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/message_types.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/message_types.csv index 8e00485771445..bb4b58046b843 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/message_types.csv +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/message_types.csv @@ -4,3 +4,5 @@ Disconnected,Disconnected Connected to 10.1.0.1,Success Connected to 10.1.0.2,Success Connected to 10.1.0.3,Success +Production environment,Production +Development environment,Development diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index e9a0f89e4f448..235d0dcbe4164 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -547,7 +547,7 @@ public enum Cap { /** * LOOKUP JOIN */ - JOIN_LOOKUP_V5(Build.current().isSnapshot()), + JOIN_LOOKUP_V6(Build.current().isSnapshot()), /** * Fix for https://github.com/elastic/elasticsearch/issues/117054 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java index cf91c7df9a034..d59745f03f608 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java @@ -198,12 +198,16 @@ private static class ResolveTable extends ParameterizedAnalyzerRule lookupResolution, EnrichResolution enrichResolution ) { // Currently for tests only, since most do not test lookups @@ -26,12 +28,6 @@ public AnalyzerContext( IndexResolution indexResolution, EnrichResolution enrichResolution ) { - this( - configuration, - functionRegistry, - indexResolution, - IndexResolution.invalid("AnalyzerContext constructed without any lookup join resolution"), - enrichResolution - ); + this(configuration, functionRegistry, indexResolution, Map.of(), enrichResolution); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java index 83480f6651abf..c0290fa2b1d73 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java @@ -13,7 +13,6 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.TriFunction; import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.compute.data.Block; @@ -77,10 +76,12 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import static org.elasticsearch.index.query.QueryBuilders.boolQuery; @@ -282,10 +283,10 @@ public void analyzedPlan( return; } - TriFunction analyzeAction = (indices, lookupIndices, policies) -> { + Function analyzeAction = (l) -> { planningMetrics.gatherPreAnalysisMetrics(parsed); Analyzer analyzer = new Analyzer( - new AnalyzerContext(configuration, functionRegistry, indices, lookupIndices, policies), + new AnalyzerContext(configuration, functionRegistry, l.indices, l.lookupIndices, l.enrichResolution), verifier ); LogicalPlan plan = analyzer.analyze(parsed); @@ -301,110 +302,77 @@ public void analyzedPlan( EsqlSessionCCSUtils.checkForCcsLicense(indices, indicesExpressionGrouper, verifier.licenseState()); - // TODO: make a separate call for lookup indices final Set targetClusters = enrichPolicyResolver.groupIndicesPerCluster( indices.stream().flatMap(t -> Arrays.stream(Strings.commaDelimitedListToStringArray(t.id().index()))).toArray(String[]::new) ).keySet(); - SubscribableListener.newForked(l -> enrichPolicyResolver.resolvePolicies(targetClusters, unresolvedPolicies, l)) - .andThen((l, enrichResolution) -> { - // we need the match_fields names from enrich policies and THEN, with an updated list of fields, we call field_caps API - var enrichMatchFields = enrichResolution.resolvedEnrichPolicies() - .stream() - .map(ResolvedEnrichPolicy::matchField) - .collect(Collectors.toSet()); - // get the field names from the parsed plan combined with the ENRICH match fields from the ENRICH policy - var fieldNames = fieldNames(parsed, enrichMatchFields); - ListenerResult listenerResult = new ListenerResult(null, null, enrichResolution, fieldNames); - - // first resolve the lookup indices, then the main indices - preAnalyzeLookupIndices(preAnalysis.lookupIndices, listenerResult, l); - }) - .andThen((l, listenerResult) -> { - // resolve the main indices - preAnalyzeIndices(preAnalysis.indices, executionInfo, listenerResult, requestFilter, l); - }) - .andThen((l, listenerResult) -> { - // TODO in follow-PR (for skip_unavailable handling of missing concrete indexes) add some tests for - // invalid index resolution to updateExecutionInfo - if (listenerResult.indices.isValid()) { - // CCS indices and skip_unavailable cluster values can stop the analysis right here - if (analyzeCCSIndices(executionInfo, targetClusters, unresolvedPolicies, listenerResult, logicalPlanListener, l)) - return; - } - // whatever tuple we have here (from CCS-special handling or from the original pre-analysis), pass it on to the next step - l.onResponse(listenerResult); - }) - .andThen((l, listenerResult) -> { - // first attempt (maybe the only one) at analyzing the plan - analyzeAndMaybeRetry(analyzeAction, requestFilter, listenerResult, logicalPlanListener, l); - }) - .andThen((l, listenerResult) -> { - assert requestFilter != null : "The second pre-analysis shouldn't take place when there is no index filter in the request"; - - // "reset" execution information for all ccs or non-ccs (local) clusters, since we are performing the indices - // resolving one more time (the first attempt failed and the query had a filter) - for (String clusterAlias : executionInfo.clusterAliases()) { - executionInfo.swapCluster(clusterAlias, (k, v) -> null); - } - - // here the requestFilter is set to null, performing the pre-analysis after the first step failed - preAnalyzeIndices(preAnalysis.indices, executionInfo, listenerResult, null, l); - }) - .andThen((l, listenerResult) -> { - assert requestFilter != null : "The second analysis shouldn't take place when there is no index filter in the request"; - LOGGER.debug("Analyzing the plan (second attempt, without filter)"); - LogicalPlan plan; - try { - plan = analyzeAction.apply(listenerResult.indices, listenerResult.lookupIndices, listenerResult.enrichResolution); - } catch (Exception e) { - l.onFailure(e); - return; - } - LOGGER.debug("Analyzed plan (second attempt, without filter):\n{}", plan); - l.onResponse(plan); - }) - .addListener(logicalPlanListener); - } + var listener = SubscribableListener.newForked( + l -> enrichPolicyResolver.resolvePolicies(targetClusters, unresolvedPolicies, l) + ).andThen((l, enrichResolution) -> resolveFieldNames(parsed, enrichResolution, l)); + // first resolve the lookup indices, then the main indices + for (TableInfo lookupIndex : preAnalysis.lookupIndices) { + listener = listener.andThen((l, preAnalysisResult) -> { preAnalyzeLookupIndex(lookupIndex, preAnalysisResult, l); }); + } + listener.andThen((l, result) -> { + // resolve the main indices + preAnalyzeIndices(preAnalysis.indices, executionInfo, result, requestFilter, l); + }).andThen((l, result) -> { + // TODO in follow-PR (for skip_unavailable handling of missing concrete indexes) add some tests for + // invalid index resolution to updateExecutionInfo + if (result.indices.isValid()) { + // CCS indices and skip_unavailable cluster values can stop the analysis right here + if (analyzeCCSIndices(executionInfo, targetClusters, unresolvedPolicies, result, logicalPlanListener, l)) return; + } + // whatever tuple we have here (from CCS-special handling or from the original pre-analysis), pass it on to the next step + l.onResponse(result); + }).andThen((l, result) -> { + // first attempt (maybe the only one) at analyzing the plan + analyzeAndMaybeRetry(analyzeAction, requestFilter, result, logicalPlanListener, l); + }).andThen((l, result) -> { + assert requestFilter != null : "The second pre-analysis shouldn't take place when there is no index filter in the request"; + + // "reset" execution information for all ccs or non-ccs (local) clusters, since we are performing the indices + // resolving one more time (the first attempt failed and the query had a filter) + for (String clusterAlias : executionInfo.clusterAliases()) { + executionInfo.swapCluster(clusterAlias, (k, v) -> null); + } - private void preAnalyzeLookupIndices(List indices, ListenerResult listenerResult, ActionListener listener) { - if (indices.size() > 1) { - // Note: JOINs on more than one index are not yet supported - listener.onFailure(new MappingException("More than one LOOKUP JOIN is not supported")); - } else if (indices.size() == 1) { - TableInfo tableInfo = indices.get(0); - TableIdentifier table = tableInfo.id(); - // call the EsqlResolveFieldsAction (field-caps) to resolve indices and get field types - indexResolver.resolveAsMergedMapping( - table.index(), - Set.of("*"), // TODO: for LOOKUP JOIN, this currently declares all lookup index fields relevant and might fetch too many. - null, - listener.map(indexResolution -> listenerResult.withLookupIndexResolution(indexResolution)) - ); - // TODO: Verify that the resolved index actually has indexMode: "lookup" - } else { + // here the requestFilter is set to null, performing the pre-analysis after the first step failed + preAnalyzeIndices(preAnalysis.indices, executionInfo, result, null, l); + }).andThen((l, result) -> { + assert requestFilter != null : "The second analysis shouldn't take place when there is no index filter in the request"; + LOGGER.debug("Analyzing the plan (second attempt, without filter)"); + LogicalPlan plan; try { - // No lookup indices specified - listener.onResponse( - new ListenerResult( - listenerResult.indices, - IndexResolution.invalid("[none specified]"), - listenerResult.enrichResolution, - listenerResult.fieldNames - ) - ); - } catch (Exception ex) { - listener.onFailure(ex); + plan = analyzeAction.apply(result); + } catch (Exception e) { + l.onFailure(e); + return; } - } + LOGGER.debug("Analyzed plan (second attempt, without filter):\n{}", plan); + l.onResponse(plan); + }).addListener(logicalPlanListener); + } + + private void preAnalyzeLookupIndex(TableInfo tableInfo, PreAnalysisResult result, ActionListener listener) { + TableIdentifier table = tableInfo.id(); + Set fieldNames = result.wildcardJoinIndices().contains(table.index()) ? IndexResolver.ALL_FIELDS : result.fieldNames; + // call the EsqlResolveFieldsAction (field-caps) to resolve indices and get field types + indexResolver.resolveAsMergedMapping( + table.index(), + fieldNames, + null, + listener.map(indexResolution -> result.addLookupIndexResolution(table.index(), indexResolution)) + ); + // TODO: Verify that the resolved index actually has indexMode: "lookup" } private void preAnalyzeIndices( List indices, EsqlExecutionInfo executionInfo, - ListenerResult listenerResult, + PreAnalysisResult result, QueryBuilder requestFilter, - ActionListener listener + ActionListener listener ) { // TODO we plan to support joins in the future when possible, but for now we'll just fail early if we see one if (indices.size() > 1) { @@ -412,7 +380,7 @@ private void preAnalyzeIndices( listener.onFailure(new MappingException("Queries with multiple indices are not supported")); } else if (indices.size() == 1) { // known to be unavailable from the enrich policy API call - Map unavailableClusters = listenerResult.enrichResolution.getUnavailableClusters(); + Map unavailableClusters = result.enrichResolution.getUnavailableClusters(); TableInfo tableInfo = indices.get(0); TableIdentifier table = tableInfo.id(); @@ -445,34 +413,20 @@ private void preAnalyzeIndices( String indexExpressionToResolve = EsqlSessionCCSUtils.createIndexExpressionFromAvailableClusters(executionInfo); if (indexExpressionToResolve.isEmpty()) { // if this was a pure remote CCS request (no local indices) and all remotes are offline, return an empty IndexResolution - listener.onResponse( - new ListenerResult( - IndexResolution.valid(new EsIndex(table.index(), Map.of(), Map.of())), - listenerResult.lookupIndices, - listenerResult.enrichResolution, - listenerResult.fieldNames - ) - ); + listener.onResponse(result.withIndexResolution(IndexResolution.valid(new EsIndex(table.index(), Map.of(), Map.of())))); } else { // call the EsqlResolveFieldsAction (field-caps) to resolve indices and get field types indexResolver.resolveAsMergedMapping( indexExpressionToResolve, - listenerResult.fieldNames, + result.fieldNames, requestFilter, - listener.map(indexResolution -> listenerResult.withIndexResolution(indexResolution)) + listener.map(indexResolution -> result.withIndexResolution(indexResolution)) ); } } else { try { // occurs when dealing with local relations (row a = 1) - listener.onResponse( - new ListenerResult( - IndexResolution.invalid("[none specified]"), - listenerResult.lookupIndices, - listenerResult.enrichResolution, - listenerResult.fieldNames - ) - ); + listener.onResponse(result.withIndexResolution(IndexResolution.invalid("[none specified]"))); } catch (Exception ex) { listener.onFailure(ex); } @@ -483,11 +437,11 @@ private boolean analyzeCCSIndices( EsqlExecutionInfo executionInfo, Set targetClusters, Set unresolvedPolicies, - ListenerResult listenerResult, + PreAnalysisResult result, ActionListener logicalPlanListener, - ActionListener l + ActionListener l ) { - IndexResolution indexResolution = listenerResult.indices; + IndexResolution indexResolution = result.indices; EsqlSessionCCSUtils.updateExecutionInfoWithClustersWithNoMatchingIndices(executionInfo, indexResolution); EsqlSessionCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, indexResolution.unavailableClusters()); if (executionInfo.isCrossClusterSearch() && executionInfo.getClusterStateCount(EsqlExecutionInfo.Cluster.Status.RUNNING) == 0) { @@ -509,7 +463,7 @@ private boolean analyzeCCSIndices( enrichPolicyResolver.resolvePolicies( newClusters, unresolvedPolicies, - l.map(enrichResolution -> listenerResult.withEnrichResolution(enrichResolution)) + l.map(enrichResolution -> result.withEnrichResolution(enrichResolution)) ); return true; } @@ -517,11 +471,11 @@ private boolean analyzeCCSIndices( } private static void analyzeAndMaybeRetry( - TriFunction analyzeAction, + Function analyzeAction, QueryBuilder requestFilter, - ListenerResult listenerResult, + PreAnalysisResult result, ActionListener logicalPlanListener, - ActionListener l + ActionListener l ) { LogicalPlan plan = null; var filterPresentMessage = requestFilter == null ? "without" : "with"; @@ -529,7 +483,7 @@ private static void analyzeAndMaybeRetry( LOGGER.debug("Analyzing the plan ({} attempt, {} filter)", attemptMessage, filterPresentMessage); try { - plan = analyzeAction.apply(listenerResult.indices, listenerResult.lookupIndices, listenerResult.enrichResolution); + plan = analyzeAction.apply(result); } catch (Exception e) { if (e instanceof VerificationException ve) { LOGGER.debug( @@ -544,7 +498,7 @@ private static void analyzeAndMaybeRetry( } else { // interested only in a VerificationException, but this time we are taking out the index filter // to try and make the index resolution work without any index filtering. In the next step... to be continued - l.onResponse(listenerResult); + l.onResponse(result); } } else { // if the query failed with any other type of exception, then just pass the exception back to the user @@ -557,10 +511,24 @@ private static void analyzeAndMaybeRetry( logicalPlanListener.onResponse(plan); } - static Set fieldNames(LogicalPlan parsed, Set enrichPolicyMatchFields) { + private static void resolveFieldNames(LogicalPlan parsed, EnrichResolution enrichResolution, ActionListener l) { + try { + // we need the match_fields names from enrich policies and THEN, with an updated list of fields, we call field_caps API + var enrichMatchFields = enrichResolution.resolvedEnrichPolicies() + .stream() + .map(ResolvedEnrichPolicy::matchField) + .collect(Collectors.toSet()); + // get the field names from the parsed plan combined with the ENRICH match fields from the ENRICH policy + l.onResponse(fieldNames(parsed, enrichMatchFields, new PreAnalysisResult(enrichResolution))); + } catch (Exception ex) { + l.onFailure(ex); + } + } + + static PreAnalysisResult fieldNames(LogicalPlan parsed, Set enrichPolicyMatchFields, PreAnalysisResult result) { if (false == parsed.anyMatch(plan -> plan instanceof Aggregate || plan instanceof Project)) { // no explicit columns selection, for example "from employees" - return IndexResolver.ALL_FIELDS; + return result.withFieldNames(IndexResolver.ALL_FIELDS); } Holder projectAll = new Holder<>(false); @@ -571,7 +539,7 @@ static Set fieldNames(LogicalPlan parsed, Set enrichPolicyMatchF projectAll.set(true); }); if (projectAll.get()) { - return IndexResolver.ALL_FIELDS; + return result.withFieldNames(IndexResolver.ALL_FIELDS); } AttributeSet references = new AttributeSet(); @@ -579,6 +547,7 @@ static Set fieldNames(LogicalPlan parsed, Set enrichPolicyMatchF // ie "from test | eval lang = languages + 1 | keep *l" should consider both "languages" and "*l" as valid fields to ask for AttributeSet keepCommandReferences = new AttributeSet(); AttributeSet keepJoinReferences = new AttributeSet(); + Set wildcardJoinIndices = new java.util.HashSet<>(); parsed.forEachDown(p -> {// go over each plan top-down if (p instanceof RegexExtract re) { // for Grok and Dissect @@ -596,10 +565,16 @@ static Set fieldNames(LogicalPlan parsed, Set enrichPolicyMatchF enrichRefs.removeIf(attr -> attr instanceof EmptyAttribute); references.addAll(enrichRefs); } else if (p instanceof LookupJoin join) { - keepJoinReferences.addAll(join.config().matchFields()); // TODO: why is this empty if (join.config().type() instanceof JoinTypes.UsingJoinType usingJoinType) { keepJoinReferences.addAll(usingJoinType.columns()); } + if (keepCommandReferences.isEmpty()) { + // No KEEP commands after the JOIN, so we need to mark this index for "*" field resolution + wildcardJoinIndices.add(((UnresolvedRelation) join.right()).table().index()); + } else { + // Keep commands can reference the join columns with names that shadow aliases, so we block their removal + keepJoinReferences.addAll(keepCommandReferences); + } } else { references.addAll(p.references()); if (p instanceof UnresolvedRelation ur && ur.indexMode() == IndexMode.TIME_SERIES) { @@ -634,6 +609,10 @@ static Set fieldNames(LogicalPlan parsed, Set enrichPolicyMatchF }); // Add JOIN ON column references afterward to avoid Alias removal references.addAll(keepJoinReferences); + // If any JOIN commands need wildcard field-caps calls, persist the index names + if (wildcardJoinIndices.isEmpty() == false) { + result = result.withWildcardJoinIndices(wildcardJoinIndices); + } // remove valid metadata attributes because they will be filtered out by the IndexResolver anyway // otherwise, in some edge cases, we will fail to ask for "*" (all fields) instead @@ -642,12 +621,12 @@ static Set fieldNames(LogicalPlan parsed, Set enrichPolicyMatchF if (fieldNames.isEmpty() && enrichPolicyMatchFields.isEmpty()) { // there cannot be an empty list of fields, we'll ask the simplest and lightest one instead: _index - return IndexResolver.INDEX_METADATA_FIELD; + return result.withFieldNames(IndexResolver.INDEX_METADATA_FIELD); } else { fieldNames.addAll(subfields(fieldNames)); fieldNames.addAll(enrichPolicyMatchFields); fieldNames.addAll(subfields(enrichPolicyMatchFields)); - return fieldNames; + return result.withFieldNames(fieldNames); } } @@ -706,22 +685,36 @@ public PhysicalPlan optimizedPhysicalPlan(LogicalPlan optimizedPlan) { return plan; } - private record ListenerResult( + record PreAnalysisResult( IndexResolution indices, - IndexResolution lookupIndices, + Map lookupIndices, EnrichResolution enrichResolution, - Set fieldNames + Set fieldNames, + Set wildcardJoinIndices ) { - ListenerResult withEnrichResolution(EnrichResolution newEnrichResolution) { - return new ListenerResult(indices(), lookupIndices(), newEnrichResolution, fieldNames()); + PreAnalysisResult(EnrichResolution newEnrichResolution) { + this(null, new HashMap<>(), newEnrichResolution, Set.of(), Set.of()); } - ListenerResult withIndexResolution(IndexResolution newIndexResolution) { - return new ListenerResult(newIndexResolution, lookupIndices(), enrichResolution(), fieldNames()); + PreAnalysisResult withEnrichResolution(EnrichResolution newEnrichResolution) { + return new PreAnalysisResult(indices(), lookupIndices(), newEnrichResolution, fieldNames(), wildcardJoinIndices()); } - ListenerResult withLookupIndexResolution(IndexResolution newIndexResolution) { - return new ListenerResult(indices(), newIndexResolution, enrichResolution(), fieldNames()); + PreAnalysisResult withIndexResolution(IndexResolution newIndexResolution) { + return new PreAnalysisResult(newIndexResolution, lookupIndices(), enrichResolution(), fieldNames(), wildcardJoinIndices()); } - }; + + PreAnalysisResult addLookupIndexResolution(String index, IndexResolution newIndexResolution) { + lookupIndices.put(index, newIndexResolution); + return this; + } + + PreAnalysisResult withFieldNames(Set newFields) { + return new PreAnalysisResult(indices(), lookupIndices(), enrichResolution(), newFields, wildcardJoinIndices()); + } + + public PreAnalysisResult withWildcardJoinIndices(Set wildcardJoinIndices) { + return new PreAnalysisResult(indices(), lookupIndices(), enrichResolution(), fieldNames(), wildcardJoinIndices); + } + } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index c11ef8615eb72..f553c15ef69fa 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -263,7 +263,7 @@ public final void test() throws Throwable { ); assumeFalse( "lookup join disabled for csv tests", - testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.JOIN_LOOKUP_V5.capabilityName()) + testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.JOIN_LOOKUP_V6.capabilityName()) ); assumeFalse( "can't use TERM function in csv tests", diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java index 5e79e40b7e938..85dd36ba0aaa5 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java @@ -123,8 +123,8 @@ public static IndexResolution expandedDefaultIndexResolution() { return loadMapping("mapping-default.json", "test"); } - public static IndexResolution defaultLookupResolution() { - return loadMapping("mapping-languages.json", "languages_lookup", IndexMode.LOOKUP); + public static Map defaultLookupResolution() { + return Map.of("languages_lookup", loadMapping("mapping-languages.json", "languages_lookup", IndexMode.LOOKUP)); } public static EnrichResolution defaultEnrichResolution() { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index cfff245b19244..4e02119b31744 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -2139,7 +2139,7 @@ public void testLookupMatchTypeWrong() { } public void testLookupJoinUnknownIndex() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); String errorMessage = "Unknown index [foobar]"; IndexResolution missingLookupIndex = IndexResolution.invalid(errorMessage); @@ -2149,7 +2149,7 @@ public void testLookupJoinUnknownIndex() { EsqlTestUtils.TEST_CFG, new EsqlFunctionRegistry(), analyzerDefaultMapping(), - missingLookupIndex, + Map.of("foobar", missingLookupIndex), defaultEnrichResolution() ), TEST_VERIFIER @@ -2168,7 +2168,7 @@ public void testLookupJoinUnknownIndex() { } public void testLookupJoinUnknownField() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); String query = "FROM test | LOOKUP JOIN languages_lookup ON last_name"; String errorMessage = "1:45: Unknown column [last_name] in right side of join"; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 4b916106165fb..58180aafedc0b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1964,7 +1964,7 @@ public void testSortByAggregate() { } public void testLookupJoinDataTypeMismatch() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); query("FROM test | EVAL language_code = languages | LOOKUP JOIN languages_lookup ON language_code"); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index 1d10ebab267ce..c4d7b30115c2d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -219,11 +219,6 @@ public static void init() { enrichResolution = new EnrichResolution(); AnalyzerTestUtils.loadEnrichPolicyResolution(enrichResolution, "languages_idx", "id", "languages_idx", "mapping-languages.json"); - var lookupMapping = loadMapping("mapping-languages.json"); - IndexResolution lookupResolution = IndexResolution.valid( - new EsIndex("language_code", lookupMapping, Map.of("language_code", IndexMode.LOOKUP)) - ); - // Most tests used data from the test index, so we load it here, and use it in the plan() function. mapping = loadMapping("mapping-basic.json"); EsIndex test = new EsIndex("test", mapping, Map.of("test", IndexMode.STANDARD)); @@ -4911,7 +4906,7 @@ public void testPlanSanityCheck() throws Exception { } public void testPlanSanityCheckWithBinaryPlans() throws Exception { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); var plan = optimizedPlan(""" FROM test @@ -5913,15 +5908,15 @@ public void testLookupStats() { * | \_Limit[1000[INTEGER]] * | \_Filter[languages{f}#10 > 1[INTEGER]] * | \_EsRelation[test][_meta_field{f}#13, emp_no{f}#7, first_name{f}#8, ge..] - * \_EsRelation[language_code][LOOKUP][language_code{f}#18, language_name{f}#19] + * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#18, language_name{f}#19] */ public void testLookupJoinPushDownFilterOnJoinKeyWithRename() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); String query = """ FROM test | RENAME languages AS language_code - | LOOKUP JOIN language_code ON language_code + | LOOKUP JOIN languages_lookup ON language_code | WHERE language_code > 1 """; var plan = optimizedPlan(query); @@ -5956,15 +5951,15 @@ public void testLookupJoinPushDownFilterOnJoinKeyWithRename() { * | \_Limit[1000[INTEGER]] * | \_Filter[emp_no{f}#7 > 1[INTEGER]] * | \_EsRelation[test][_meta_field{f}#13, emp_no{f}#7, first_name{f}#8, ge..] - * \_EsRelation[language_code][LOOKUP][language_code{f}#18, language_name{f}#19] + * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#18, language_name{f}#19] */ public void testLookupJoinPushDownFilterOnLeftSideField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); String query = """ FROM test | RENAME languages AS language_code - | LOOKUP JOIN language_code ON language_code + | LOOKUP JOIN languages_lookup ON language_code | WHERE emp_no > 1 """; @@ -6000,15 +5995,15 @@ public void testLookupJoinPushDownFilterOnLeftSideField() { * |_EsqlProject[[_meta_field{f}#13, emp_no{f}#7, first_name{f}#8, gender{f}#9, hire_date{f}#14, job{f}#15, job.raw{f}#16, lang * uages{f}#10 AS language_code, last_name{f}#11, long_noidx{f}#17, salary{f}#12]] * | \_EsRelation[test][_meta_field{f}#13, emp_no{f}#7, first_name{f}#8, ge..] - * \_EsRelation[language_code][LOOKUP][language_code{f}#18, language_name{f}#19] + * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#18, language_name{f}#19] */ public void testLookupJoinPushDownDisabledForLookupField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); String query = """ FROM test | RENAME languages AS language_code - | LOOKUP JOIN language_code ON language_code + | LOOKUP JOIN languages_lookup ON language_code | WHERE language_name == "English" """; @@ -6045,15 +6040,15 @@ public void testLookupJoinPushDownDisabledForLookupField() { * guages{f}#11 AS language_code, last_name{f}#12, long_noidx{f}#18, salary{f}#13]] * | \_Filter[emp_no{f}#8 > 1[INTEGER]] * | \_EsRelation[test][_meta_field{f}#14, emp_no{f}#8, first_name{f}#9, ge..] - * \_EsRelation[language_code][LOOKUP][language_code{f}#19, language_name{f}#20] + * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#19, language_name{f}#20] */ public void testLookupJoinPushDownSeparatedForConjunctionBetweenLeftAndRightField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); String query = """ FROM test | RENAME languages AS language_code - | LOOKUP JOIN language_code ON language_code + | LOOKUP JOIN languages_lookup ON language_code | WHERE language_name == "English" AND emp_no > 1 """; @@ -6098,15 +6093,15 @@ public void testLookupJoinPushDownSeparatedForConjunctionBetweenLeftAndRightFiel * |_EsqlProject[[_meta_field{f}#14, emp_no{f}#8, first_name{f}#9, gender{f}#10, hire_date{f}#15, job{f}#16, job.raw{f}#17, lan * guages{f}#11 AS language_code, last_name{f}#12, long_noidx{f}#18, salary{f}#13]] * | \_EsRelation[test][_meta_field{f}#14, emp_no{f}#8, first_name{f}#9, ge..] - * \_EsRelation[language_code][LOOKUP][language_code{f}#19, language_name{f}#20] + * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#19, language_name{f}#20] */ public void testLookupJoinPushDownDisabledForDisjunctionBetweenLeftAndRightField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); String query = """ FROM test | RENAME languages AS language_code - | LOOKUP JOIN language_code ON language_code + | LOOKUP JOIN languages_lookup ON language_code | WHERE language_name == "English" OR emp_no > 1 """; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index d0c7a1cd61010..9f6ef89008a24 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -285,7 +285,7 @@ TestDataSource makeTestDataSource( String indexName, String mappingFileName, EsqlFunctionRegistry functionRegistry, - IndexResolution lookupResolution, + Map lookupResolution, EnrichResolution enrichResolution, SearchStats stats ) { @@ -2331,7 +2331,7 @@ public void testVerifierOnMissingReferences() { } public void testVerifierOnMissingReferencesWithBinaryPlans() throws Exception { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); // Do not assert serialization: // This will have a LookupJoinExec, which is not serializable because it doesn't leave the coordinator. diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java index 0fe89b24dfc6a..e4271a0a6ddd5 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java @@ -1316,25 +1316,25 @@ public void testCountStar() { } public void testEnrichOnDefaultFieldWithKeep() { - Set fieldNames = EsqlSession.fieldNames(parser.createStatement(""" + Set fieldNames = fieldNames(""" from employees | enrich languages_policy - | keep emp_no"""), Set.of("language_name")); + | keep emp_no""", Set.of("language_name")); assertThat(fieldNames, equalTo(Set.of("emp_no", "emp_no.*", "language_name", "language_name.*"))); } public void testDissectOverwriteName() { - Set fieldNames = EsqlSession.fieldNames(parser.createStatement(""" + Set fieldNames = fieldNames(""" from employees | dissect first_name "%{first_name} %{more}" - | keep emp_no, first_name, more"""), Set.of()); + | keep emp_no, first_name, more""", Set.of()); assertThat(fieldNames, equalTo(Set.of("emp_no", "emp_no.*", "first_name", "first_name.*"))); } public void testEnrichOnDefaultField() { - Set fieldNames = EsqlSession.fieldNames(parser.createStatement(""" + Set fieldNames = fieldNames(""" from employees - | enrich languages_policy"""), Set.of("language_name")); + | enrich languages_policy""", Set.of("language_name")); assertThat(fieldNames, equalTo(ALL_FIELDS)); } @@ -1345,7 +1345,7 @@ public void testMetrics() { assertThat(e.getMessage(), containsString("line 1:1: mismatched input 'METRICS' expecting {")); return; } - Set fieldNames = EsqlSession.fieldNames(parser.createStatement(query), Set.of()); + Set fieldNames = fieldNames(query, Set.of()); assertThat( fieldNames, equalTo( @@ -1363,8 +1363,218 @@ public void testMetrics() { ); } + public void testLookupJoin() { + assertFieldNames( + "FROM employees | KEEP languages | RENAME languages AS language_code | LOOKUP JOIN languages_lookup ON language_code", + Set.of("languages", "languages.*", "language_code", "language_code.*"), + Set.of("languages_lookup") // Since we have KEEP before the LOOKUP JOIN we need to wildcard the lookup index + ); + } + + public void testLookupJoinKeep() { + assertFieldNames( + """ + FROM employees + | KEEP languages + | RENAME languages AS language_code + | LOOKUP JOIN languages_lookup ON language_code + | KEEP languages, language_code, language_name""", + Set.of("languages", "languages.*", "language_code", "language_code.*", "language_name", "language_name.*"), + Set.of() // Since we have KEEP after the LOOKUP, we can use the global field names instead of wildcarding the lookup index + ); + } + + public void testLookupJoinKeepWildcard() { + assertFieldNames( + """ + FROM employees + | KEEP languages + | RENAME languages AS language_code + | LOOKUP JOIN languages_lookup ON language_code + | KEEP language*""", + Set.of("language*", "languages", "languages.*", "language_code", "language_code.*"), + Set.of() // Since we have KEEP after the LOOKUP, we can use the global field names instead of wildcarding the lookup index + ); + } + + public void testMultiLookupJoin() { + assertFieldNames( + """ + FROM sample_data + | EVAL client_ip = client_ip::keyword + | LOOKUP JOIN clientips_lookup ON client_ip + | LOOKUP JOIN message_types_lookup ON message""", + Set.of("*"), // With no KEEP we should keep all fields + Set.of() // since global field names are wildcarded, we don't need to wildcard any indices + ); + } + + public void testMultiLookupJoinKeepBefore() { + assertFieldNames( + """ + FROM sample_data + | EVAL client_ip = client_ip::keyword + | KEEP @timestamp, client_ip, event_duration, message + | LOOKUP JOIN clientips_lookup ON client_ip + | LOOKUP JOIN message_types_lookup ON message""", + Set.of("@timestamp", "@timestamp.*", "client_ip", "client_ip.*", "event_duration", "event_duration.*", "message", "message.*"), + Set.of("clientips_lookup", "message_types_lookup") // Since the KEEP is before both JOINS we need to wildcard both indices + ); + } + + public void testMultiLookupJoinKeepBetween() { + assertFieldNames( + """ + FROM sample_data + | EVAL client_ip = client_ip::keyword + | LOOKUP JOIN clientips_lookup ON client_ip + | KEEP @timestamp, client_ip, event_duration, message, env + | LOOKUP JOIN message_types_lookup ON message""", + Set.of( + "@timestamp", + "@timestamp.*", + "client_ip", + "client_ip.*", + "event_duration", + "event_duration.*", + "message", + "message.*", + "env", + "env.*" + ), + Set.of("message_types_lookup") // Since the KEEP is before the second JOIN, we need to wildcard the second index + ); + } + + public void testMultiLookupJoinKeepAfter() { + assertFieldNames( + """ + FROM sample_data + | EVAL client_ip = client_ip::keyword + | LOOKUP JOIN clientips_lookup ON client_ip + | LOOKUP JOIN message_types_lookup ON message + | KEEP @timestamp, client_ip, event_duration, message, env, type""", + Set.of( + "@timestamp", + "@timestamp.*", + "client_ip", + "client_ip.*", + "event_duration", + "event_duration.*", + "message", + "message.*", + "env", + "env.*", + "type", + "type.*" + ), + Set.of() // Since the KEEP is after both JOINs, we can use the global field names + ); + } + + public void testMultiLookupJoinKeepAfterWildcard() { + assertFieldNames( + """ + FROM sample_data + | EVAL client_ip = client_ip::keyword + | LOOKUP JOIN clientips_lookup ON client_ip + | LOOKUP JOIN message_types_lookup ON message + | KEEP *env*, *type*""", + Set.of("*env*", "*type*", "client_ip", "client_ip.*", "message", "message.*"), + Set.of() // Since the KEEP is after both JOINs, we can use the global field names + ); + } + + public void testMultiLookupJoinSameIndex() { + assertFieldNames( + """ + FROM sample_data + | EVAL client_ip = client_ip::keyword + | LOOKUP JOIN clientips_lookup ON client_ip + | EVAL client_ip = message + | LOOKUP JOIN clientips_lookup ON client_ip""", + Set.of("*"), // With no KEEP we should keep all fields + Set.of() // since global field names are wildcarded, we don't need to wildcard any indices + ); + } + + public void testMultiLookupJoinSameIndexKeepBefore() { + assertFieldNames( + """ + FROM sample_data + | EVAL client_ip = client_ip::keyword + | KEEP @timestamp, client_ip, event_duration, message + | LOOKUP JOIN clientips_lookup ON client_ip + | EVAL client_ip = message + | LOOKUP JOIN clientips_lookup ON client_ip""", + Set.of("@timestamp", "@timestamp.*", "client_ip", "client_ip.*", "event_duration", "event_duration.*", "message", "message.*"), + Set.of("clientips_lookup") // Since there is no KEEP after the last JOIN, we need to wildcard the index + ); + } + + public void testMultiLookupJoinSameIndexKeepBetween() { + assertFieldNames( + """ + FROM sample_data + | EVAL client_ip = client_ip::keyword + | LOOKUP JOIN clientips_lookup ON client_ip + | KEEP @timestamp, client_ip, event_duration, message, env + | EVAL client_ip = message + | LOOKUP JOIN clientips_lookup ON client_ip""", + Set.of( + "@timestamp", + "@timestamp.*", + "client_ip", + "client_ip.*", + "event_duration", + "event_duration.*", + "message", + "message.*", + "env", + "env.*" + ), + Set.of("clientips_lookup") // Since there is no KEEP after the last JOIN, we need to wildcard the index + ); + } + + public void testMultiLookupJoinSameIndexKeepAfter() { + assertFieldNames( + """ + FROM sample_data + | EVAL client_ip = client_ip::keyword + | LOOKUP JOIN clientips_lookup ON client_ip + | EVAL client_ip = message + | LOOKUP JOIN clientips_lookup ON client_ip + | KEEP @timestamp, client_ip, event_duration, message, env""", + Set.of( + "@timestamp", + "@timestamp.*", + "client_ip", + "client_ip.*", + "event_duration", + "event_duration.*", + "message", + "message.*", + "env", + "env.*" + ), + Set.of() // Since the KEEP is after both JOINs, we can use the global field names + ); + } + + private Set fieldNames(String query, Set enrichPolicyMatchFields) { + var preAnalysisResult = new EsqlSession.PreAnalysisResult(null); + return EsqlSession.fieldNames(parser.createStatement(query), enrichPolicyMatchFields, preAnalysisResult).fieldNames(); + } + private void assertFieldNames(String query, Set expected) { - Set fieldNames = EsqlSession.fieldNames(parser.createStatement(query), Collections.emptySet()); + Set fieldNames = fieldNames(query, Collections.emptySet()); assertThat(fieldNames, equalTo(expected)); } + + private void assertFieldNames(String query, Set expected, Set wildCardIndices) { + var preAnalysisResult = EsqlSession.fieldNames(parser.createStatement(query), Set.of(), new EsqlSession.PreAnalysisResult(null)); + assertThat("Query-wide field names", preAnalysisResult.fieldNames(), equalTo(expected)); + assertThat("Lookup Indices that expect wildcard lookups", preAnalysisResult.wildcardJoinIndices(), equalTo(wildCardIndices)); + } } From 8deb32fb1beb686eb8a0e853b22aadb2d382f1c1 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:25:44 +1100 Subject: [PATCH 018/119] Mute org.elasticsearch.xpack.ccr.rest.ShardChangesRestIT testShardChangesNoOperation #118800 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index be3805d887bbe..0aea4100a2606 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -299,6 +299,9 @@ tests: - class: org.elasticsearch.reservedstate.service.FileSettingsServiceTests method: testInvalidJSON issue: https://github.com/elastic/elasticsearch/issues/116521 +- class: org.elasticsearch.xpack.ccr.rest.ShardChangesRestIT + method: testShardChangesNoOperation + issue: https://github.com/elastic/elasticsearch/issues/118800 # Examples: # From 5c55c39f9f320aa4defb9f4458ef87d9ac994779 Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Mon, 16 Dec 2024 17:20:33 -0500 Subject: [PATCH 019/119] Fix semantic text match failure (#118790) * Fix semantic text match failure * Remove shard settings; only check for counts and not specific doc IDs --- .../rest-api-spec/test/inference/45_semantic_text_match.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/45_semantic_text_match.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/45_semantic_text_match.yml index cdbf73d31a272..28093ba49e6cc 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/45_semantic_text_match.yml +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/45_semantic_text_match.yml @@ -210,8 +210,6 @@ setup: query: "inference test" - match: { hits.total.value: 2 } - - match: { hits.hits.0._id: "doc_1" } - - match: { hits.hits.1._id: "doc_2" } # Test querying multiple indices that either use the same inference ID or combine semantic_text with lexical search - do: @@ -246,9 +244,6 @@ setup: query: "inference test" - match: { hits.total.value: 3 } - - match: { hits.hits.0._id: "doc_1" } - - match: { hits.hits.1._id: "doc_3" } - - match: { hits.hits.2._id: "doc_2" } --- "Query a field that has no indexed inference results": From e1d06a8881577c90f57cf0e0a379df78ba22b22e Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:14:38 +1100 Subject: [PATCH 020/119] Mute org.elasticsearch.xpack.security.QueryableReservedRolesIT testDeletingAndCreatingSecurityIndexTriggersSynchronization #118806 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 0aea4100a2606..b43045d24be81 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -302,6 +302,9 @@ tests: - class: org.elasticsearch.xpack.ccr.rest.ShardChangesRestIT method: testShardChangesNoOperation issue: https://github.com/elastic/elasticsearch/issues/118800 +- class: org.elasticsearch.xpack.security.QueryableReservedRolesIT + method: testDeletingAndCreatingSecurityIndexTriggersSynchronization + issue: https://github.com/elastic/elasticsearch/issues/118806 # Examples: # From b44469736c92cfb3dcb21d3aaa177765fd643d5d Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Tue, 17 Dec 2024 08:07:56 +0200 Subject: [PATCH 021/119] Remove esql-core ParsingException (#118789) * Remove esql-core ParsingException Move fulltext related classes: querydsl, predicate and tests --- .../xpack/esql/core/ParsingException.java | 56 ------------------- .../core/planner/ExpressionTranslators.java | 14 ----- .../function/EsqlFunctionRegistry.java | 2 +- .../function/fulltext/FullTextWritables.java | 4 +- .../predicate/fulltext/FullTextPredicate.java | 2 +- .../predicate/fulltext/FullTextUtils.java | 8 +-- .../fulltext/MatchQueryPredicate.java | 2 +- .../fulltext/MultiMatchQueryPredicate.java | 2 +- .../xpack/esql/parser/EsqlParser.java | 6 +- .../xpack/esql/parser/ParserUtils.java | 1 - .../xpack/esql/parser/ParsingException.java | 2 +- .../planner/EsqlExpressionTranslators.java | 18 +++++- .../esql}/querydsl/query/MatchQuery.java | 3 +- .../esql}/querydsl/query/MultiMatchQuery.java | 5 +- .../xpack/esql/analysis/ParsingTests.java | 22 ++++---- .../function/EsqlFunctionRegistryTests.java | 2 +- .../AbstractFulltextSerializationTests.java | 3 +- .../fulltext/FullTextUtilsTests.java | 12 ++-- .../MatchQuerySerializationTests.java | 3 +- .../MultiMatchQuerySerializationTests.java | 4 +- .../esql}/querydsl/query/BoolQueryTests.java | 6 +- .../esql}/querydsl/query/MatchQueryTests.java | 4 +- .../querydsl/query/MultiMatchQueryTests.java | 4 +- .../querydsl/query/QueryStringQueryTests.java | 3 +- .../esql/tree/EsqlNodeSubclassTests.java | 2 +- 25 files changed, 66 insertions(+), 124 deletions(-) delete mode 100644 x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/ParsingException.java rename x-pack/plugin/{esql-core/src/main/java/org/elasticsearch/xpack/esql/core => esql/src/main/java/org/elasticsearch/xpack/esql}/expression/predicate/fulltext/FullTextPredicate.java (97%) rename x-pack/plugin/{esql-core/src/main/java/org/elasticsearch/xpack/esql/core => esql/src/main/java/org/elasticsearch/xpack/esql}/expression/predicate/fulltext/FullTextUtils.java (90%) rename x-pack/plugin/{esql-core/src/main/java/org/elasticsearch/xpack/esql/core => esql/src/main/java/org/elasticsearch/xpack/esql}/expression/predicate/fulltext/MatchQueryPredicate.java (96%) rename x-pack/plugin/{esql-core/src/main/java/org/elasticsearch/xpack/esql/core => esql/src/main/java/org/elasticsearch/xpack/esql}/expression/predicate/fulltext/MultiMatchQueryPredicate.java (97%) rename x-pack/plugin/{esql-core/src/main/java/org/elasticsearch/xpack/esql/core => esql/src/main/java/org/elasticsearch/xpack/esql}/querydsl/query/MatchQuery.java (97%) rename x-pack/plugin/{esql-core/src/main/java/org/elasticsearch/xpack/esql/core => esql/src/main/java/org/elasticsearch/xpack/esql}/querydsl/query/MultiMatchQuery.java (95%) rename x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/{operator => }/fulltext/AbstractFulltextSerializationTests.java (88%) rename x-pack/plugin/{esql-core/src/test/java/org/elasticsearch/xpack/esql/core => esql/src/test/java/org/elasticsearch/xpack/esql}/expression/predicate/fulltext/FullTextUtilsTests.java (79%) rename x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/{operator => }/fulltext/MatchQuerySerializationTests.java (89%) rename x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/{operator => }/fulltext/MultiMatchQuerySerializationTests.java (92%) rename x-pack/plugin/{esql-core/src/test/java/org/elasticsearch/xpack/esql/core => esql/src/test/java/org/elasticsearch/xpack/esql}/querydsl/query/BoolQueryTests.java (92%) rename x-pack/plugin/{esql-core/src/test/java/org/elasticsearch/xpack/esql/core => esql/src/test/java/org/elasticsearch/xpack/esql}/querydsl/query/MatchQueryTests.java (96%) rename x-pack/plugin/{esql-core/src/test/java/org/elasticsearch/xpack/esql/core => esql/src/test/java/org/elasticsearch/xpack/esql}/querydsl/query/MultiMatchQueryTests.java (94%) rename x-pack/plugin/{esql-core/src/test/java/org/elasticsearch/xpack/esql/core => esql/src/test/java/org/elasticsearch/xpack/esql}/querydsl/query/QueryStringQueryTests.java (94%) diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/ParsingException.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/ParsingException.java deleted file mode 100644 index bce3f848c9387..0000000000000 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/ParsingException.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.esql.core; - -import org.elasticsearch.xpack.esql.core.tree.Source; - -import static org.elasticsearch.common.logging.LoggerMessageFormat.format; - -public class ParsingException extends QlClientException { - private final int line; - private final int charPositionInLine; - - public ParsingException(String message, Exception cause, int line, int charPositionInLine) { - super(message, cause); - this.line = line; - this.charPositionInLine = charPositionInLine; - } - - public ParsingException(String message, Object... args) { - this(Source.EMPTY, message, args); - } - - public ParsingException(Source source, String message, Object... args) { - super(message, args); - this.line = source.source().getLineNumber(); - this.charPositionInLine = source.source().getColumnNumber(); - } - - public ParsingException(Exception cause, Source source, String message, Object... args) { - super(cause, message, args); - this.line = source.source().getLineNumber(); - this.charPositionInLine = source.source().getColumnNumber(); - } - - public int getLineNumber() { - return line; - } - - public int getColumnNumber() { - return charPositionInLine + 1; - } - - public String getErrorMessage() { - return super.getMessage(); - } - - @Override - public String getMessage() { - return format("line {}:{}: {}", getLineNumber(), getColumnNumber(), getErrorMessage()); - } -} diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/planner/ExpressionTranslators.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/planner/ExpressionTranslators.java index 468d076c1b7ef..e0f4f6b032662 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/planner/ExpressionTranslators.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/planner/ExpressionTranslators.java @@ -11,7 +11,6 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.expression.MetadataAttribute; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.MultiMatchQueryPredicate; import org.elasticsearch.xpack.esql.core.expression.predicate.logical.And; import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Not; import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Or; @@ -22,7 +21,6 @@ import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardLike; import org.elasticsearch.xpack.esql.core.querydsl.query.BoolQuery; import org.elasticsearch.xpack.esql.core.querydsl.query.ExistsQuery; -import org.elasticsearch.xpack.esql.core.querydsl.query.MultiMatchQuery; import org.elasticsearch.xpack.esql.core.querydsl.query.NotQuery; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.querydsl.query.RegexQuery; @@ -71,18 +69,6 @@ private static Query translateField(RegexMatch e, String targetFieldName) { } } - public static class MultiMatches extends ExpressionTranslator { - - @Override - protected Query asQuery(MultiMatchQueryPredicate q, TranslatorHandler handler) { - return doTranslate(q, handler); - } - - public static Query doTranslate(MultiMatchQueryPredicate q, TranslatorHandler handler) { - return new MultiMatchQuery(q.source(), q.query(), q.fields(), q); - } - } - public static class BinaryLogic extends ExpressionTranslator< org.elasticsearch.xpack.esql.core.expression.predicate.logical.BinaryLogic> { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index 1ccc22eb3a6a4..a59ef5bb1575d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -11,7 +11,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.FeatureFlag; -import org.elasticsearch.xpack.esql.core.ParsingException; import org.elasticsearch.xpack.esql.core.QlIllegalArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.function.Function; @@ -147,6 +146,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.ToUpper; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Trim; import org.elasticsearch.xpack.esql.expression.function.scalar.util.Delay; +import org.elasticsearch.xpack.esql.parser.ParsingException; import org.elasticsearch.xpack.esql.session.Configuration; import java.lang.reflect.Constructor; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java index d6b79d16b74f6..245aca5b7328e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java @@ -9,8 +9,8 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.xpack.esql.action.EsqlCapabilities; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.MatchQueryPredicate; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.MultiMatchQueryPredicate; +import org.elasticsearch.xpack.esql.expression.predicate.fulltext.MatchQueryPredicate; +import org.elasticsearch.xpack.esql.expression.predicate.fulltext.MultiMatchQueryPredicate; import java.util.ArrayList; import java.util.Collections; diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/FullTextPredicate.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/FullTextPredicate.java similarity index 97% rename from x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/FullTextPredicate.java rename to x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/FullTextPredicate.java index b23593804f8fe..1dd6f650828c3 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/FullTextPredicate.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/FullTextPredicate.java @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.expression.predicate.fulltext; +package org.elasticsearch.xpack.esql.expression.predicate.fulltext; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/FullTextUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/FullTextUtils.java similarity index 90% rename from x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/FullTextUtils.java rename to x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/FullTextUtils.java index 6ba2650314d04..32c8e70a0fde6 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/FullTextUtils.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/FullTextUtils.java @@ -4,13 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.expression.predicate.fulltext; +package org.elasticsearch.xpack.esql.expression.predicate.fulltext; import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.Maps; -import org.elasticsearch.xpack.esql.core.ParsingException; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.FullTextPredicate.Operator; import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.expression.predicate.fulltext.FullTextPredicate.Operator; +import org.elasticsearch.xpack.esql.parser.ParsingException; import java.util.LinkedHashMap; import java.util.Locale; @@ -86,7 +86,7 @@ private static String[] splitInTwo(String string, String delimiter) { return split; } - static FullTextPredicate.Operator operator(Map options, String key) { + static Operator operator(Map options, String key) { String value = options.get(key); return value != null ? Operator.valueOf(value.toUpperCase(Locale.ROOT)) : null; } diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/MatchQueryPredicate.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MatchQueryPredicate.java similarity index 96% rename from x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/MatchQueryPredicate.java rename to x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MatchQueryPredicate.java index f2e6088167ba5..66c6d8995b24e 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/MatchQueryPredicate.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MatchQueryPredicate.java @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.expression.predicate.fulltext; +package org.elasticsearch.xpack.esql.expression.predicate.fulltext; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/MultiMatchQueryPredicate.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MultiMatchQueryPredicate.java similarity index 97% rename from x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/MultiMatchQueryPredicate.java rename to x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MultiMatchQueryPredicate.java index 2d66023a1407d..5d165d9ea01f7 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/MultiMatchQueryPredicate.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MultiMatchQueryPredicate.java @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.expression.predicate.fulltext; +package org.elasticsearch.xpack.esql.expression.predicate.fulltext; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlParser.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlParser.java index 2e55b4df1e223..9538e3ba495db 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlParser.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlParser.java @@ -70,11 +70,7 @@ private T invokeParser( BiFunction result ) { if (query.length() > MAX_LENGTH) { - throw new org.elasticsearch.xpack.esql.core.ParsingException( - "ESQL statement is too large [{} characters > {}]", - query.length(), - MAX_LENGTH - ); + throw new ParsingException("ESQL statement is too large [{} characters > {}]", query.length(), MAX_LENGTH); } try { EsqlBaseLexer lexer = new EsqlBaseLexer(CharStreams.fromString(query)); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ParserUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ParserUtils.java index 89b1ae4e37a68..398c6c5aafbb2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ParserUtils.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ParserUtils.java @@ -14,7 +14,6 @@ import org.antlr.v4.runtime.tree.ParseTreeVisitor; import org.antlr.v4.runtime.tree.TerminalNode; import org.elasticsearch.common.util.Maps; -import org.elasticsearch.xpack.esql.core.ParsingException; import org.elasticsearch.xpack.esql.core.tree.Location; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.util.Check; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ParsingException.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ParsingException.java index 484a655fc2988..c25ab92437bfc 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ParsingException.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ParsingException.java @@ -21,7 +21,7 @@ public ParsingException(String message, Exception cause, int line, int charPosit this.charPositionInLine = charPositionInLine + 1; } - ParsingException(String message, Object... args) { + public ParsingException(String message, Object... args) { this(Source.EMPTY, message, args); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java index 43bbf9a5f4ff1..a1765977ee9c2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java @@ -25,7 +25,6 @@ import org.elasticsearch.xpack.esql.core.planner.ExpressionTranslators; import org.elasticsearch.xpack.esql.core.planner.TranslatorHandler; import org.elasticsearch.xpack.esql.core.querydsl.query.MatchAll; -import org.elasticsearch.xpack.esql.core.querydsl.query.MatchQuery; import org.elasticsearch.xpack.esql.core.querydsl.query.NotQuery; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.querydsl.query.QueryStringQuery; @@ -44,6 +43,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils; +import org.elasticsearch.xpack.esql.expression.predicate.fulltext.MultiMatchQueryPredicate; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.GreaterThan; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.GreaterThanOrEqual; @@ -53,6 +53,8 @@ import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThanOrEqual; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.NotEquals; import org.elasticsearch.xpack.esql.querydsl.query.KqlQuery; +import org.elasticsearch.xpack.esql.querydsl.query.MatchQuery; +import org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery; import org.elasticsearch.xpack.esql.querydsl.query.SpatialRelatesQuery; import org.elasticsearch.xpack.versionfield.Version; @@ -92,7 +94,7 @@ public final class EsqlExpressionTranslators { new ExpressionTranslators.IsNotNulls(), new ExpressionTranslators.Nots(), new ExpressionTranslators.Likes(), - new ExpressionTranslators.MultiMatches(), + new MultiMatches(), new MatchFunctionTranslator(), new QueryStringFunctionTranslator(), new KqlFunctionTranslator(), @@ -537,6 +539,18 @@ private static RangeQuery translate(Range r, TranslatorHandler handler) { } } + public static class MultiMatches extends ExpressionTranslator { + + @Override + protected Query asQuery(MultiMatchQueryPredicate q, TranslatorHandler handler) { + return doTranslate(q, handler); + } + + public static Query doTranslate(MultiMatchQueryPredicate q, TranslatorHandler handler) { + return new MultiMatchQuery(q.source(), q.query(), q.fields(), q); + } + } + public static class MatchFunctionTranslator extends ExpressionTranslator { @Override protected Query asQuery(Match match, TranslatorHandler handler) { diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/querydsl/query/MatchQuery.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MatchQuery.java similarity index 97% rename from x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/querydsl/query/MatchQuery.java rename to x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MatchQuery.java index e6b6dc20c951a..1614b4f455456 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/querydsl/query/MatchQuery.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MatchQuery.java @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.querydsl.query; +package org.elasticsearch.xpack.esql.querydsl.query; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.core.Booleans; @@ -12,6 +12,7 @@ import org.elasticsearch.index.query.Operator; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.tree.Source; import java.util.Collections; diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/querydsl/query/MultiMatchQuery.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java similarity index 95% rename from x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/querydsl/query/MultiMatchQuery.java rename to x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java index 71e3cb9fd494a..84524bad29e08 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/querydsl/query/MultiMatchQuery.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.querydsl.query; +package org.elasticsearch.xpack.esql.querydsl.query; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.core.Booleans; @@ -12,8 +12,9 @@ import org.elasticsearch.index.query.Operator; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.MultiMatchQueryPredicate; +import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.expression.predicate.fulltext.MultiMatchQueryPredicate; import java.util.Map; import java.util.Objects; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java index 68529e99c6b1b..205c8943d4e3c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.esql.LoadMapping; -import org.elasticsearch.xpack.esql.core.ParsingException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry; @@ -20,6 +19,7 @@ import org.elasticsearch.xpack.esql.index.EsIndex; import org.elasticsearch.xpack.esql.index.IndexResolution; import org.elasticsearch.xpack.esql.parser.EsqlParser; +import org.elasticsearch.xpack.esql.parser.ParsingException; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.plan.logical.Row; import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter; @@ -49,27 +49,27 @@ public class ParsingTests extends ESTestCase { ); public void testCaseFunctionInvalidInputs() { - assertEquals("1:23: error building [case]: expects at least two arguments", error("row a = 1 | eval x = case()")); - assertEquals("1:23: error building [case]: expects at least two arguments", error("row a = 1 | eval x = case(a)")); - assertEquals("1:23: error building [case]: expects at least two arguments", error("row a = 1 | eval x = case(1)")); + assertEquals("1:22: error building [case]: expects at least two arguments", error("row a = 1 | eval x = case()")); + assertEquals("1:22: error building [case]: expects at least two arguments", error("row a = 1 | eval x = case(a)")); + assertEquals("1:22: error building [case]: expects at least two arguments", error("row a = 1 | eval x = case(1)")); } public void testConcatFunctionInvalidInputs() { - assertEquals("1:23: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat()")); - assertEquals("1:23: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat(a)")); - assertEquals("1:23: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat(1)")); + assertEquals("1:22: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat()")); + assertEquals("1:22: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat(a)")); + assertEquals("1:22: error building [concat]: expects at least two arguments", error("row a = 1 | eval x = concat(1)")); } public void testCoalesceFunctionInvalidInputs() { - assertEquals("1:23: error building [coalesce]: expects at least one argument", error("row a = 1 | eval x = coalesce()")); + assertEquals("1:22: error building [coalesce]: expects at least one argument", error("row a = 1 | eval x = coalesce()")); } public void testGreatestFunctionInvalidInputs() { - assertEquals("1:23: error building [greatest]: expects at least one argument", error("row a = 1 | eval x = greatest()")); + assertEquals("1:22: error building [greatest]: expects at least one argument", error("row a = 1 | eval x = greatest()")); } public void testLeastFunctionInvalidInputs() { - assertEquals("1:23: error building [least]: expects at least one argument", error("row a = 1 | eval x = least()")); + assertEquals("1:22: error building [least]: expects at least one argument", error("row a = 1 | eval x = least()")); } /** @@ -108,7 +108,7 @@ public void testTooBigQuery() { while (query.length() < EsqlParser.MAX_LENGTH) { query.append(", a = CONCAT(a, a)"); } - assertEquals("-1:0: ESQL statement is too large [1000011 characters > 1000000]", error(query.toString())); + assertEquals("-1:-1: ESQL statement is too large [1000011 characters > 1000000]", error(query.toString())); } private String functionName(EsqlFunctionRegistry registry, Expression functionCall) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistryTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistryTests.java index 801bd8700d014..50cbbdf4a9338 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistryTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistryTests.java @@ -10,7 +10,6 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.esql.core.ParsingException; import org.elasticsearch.xpack.esql.core.QlIllegalArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.function.scalar.ScalarFunction; @@ -19,6 +18,7 @@ import org.elasticsearch.xpack.esql.core.tree.SourceTests; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlConfigurationFunction; +import org.elasticsearch.xpack.esql.parser.ParsingException; import org.elasticsearch.xpack.esql.session.Configuration; import java.io.IOException; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/fulltext/AbstractFulltextSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/AbstractFulltextSerializationTests.java similarity index 88% rename from x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/fulltext/AbstractFulltextSerializationTests.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/AbstractFulltextSerializationTests.java index 370cfaf67fe0f..abd46f4b2b1aa 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/fulltext/AbstractFulltextSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/AbstractFulltextSerializationTests.java @@ -5,9 +5,8 @@ * 2.0. */ -package org.elasticsearch.xpack.esql.expression.predicate.operator.fulltext; +package org.elasticsearch.xpack.esql.expression.predicate.fulltext; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.FullTextPredicate; import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests; import java.util.HashMap; diff --git a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/FullTextUtilsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/FullTextUtilsTests.java similarity index 79% rename from x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/FullTextUtilsTests.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/FullTextUtilsTests.java index c6358b4682a79..46bafe5ebae9c 100644 --- a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/predicate/fulltext/FullTextUtilsTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/FullTextUtilsTests.java @@ -4,11 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.expression.predicate.fulltext; +package org.elasticsearch.xpack.esql.expression.predicate.fulltext; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.esql.core.ParsingException; import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.parser.ParsingException; import java.util.Map; @@ -28,15 +28,15 @@ public void testColonDelimited() { public void testColonDelimitedErrorString() { ParsingException e = expectThrows(ParsingException.class, () -> FullTextUtils.parseSettings("k1=v1;k2v2", source)); - assertThat(e.getMessage(), is("line 1:3: Cannot parse entry k2v2 in options k1=v1;k2v2")); + assertThat(e.getMessage(), is("line 1:2: Cannot parse entry k2v2 in options k1=v1;k2v2")); assertThat(e.getLineNumber(), is(1)); - assertThat(e.getColumnNumber(), is(3)); + assertThat(e.getColumnNumber(), is(2)); } public void testColonDelimitedErrorDuplicate() { ParsingException e = expectThrows(ParsingException.class, () -> FullTextUtils.parseSettings("k1=v1;k1=v2", source)); - assertThat(e.getMessage(), is("line 1:3: Duplicate option k1=v2 detected in options k1=v1;k1=v2")); + assertThat(e.getMessage(), is("line 1:2: Duplicate option k1=v2 detected in options k1=v1;k1=v2")); assertThat(e.getLineNumber(), is(1)); - assertThat(e.getColumnNumber(), is(3)); + assertThat(e.getColumnNumber(), is(2)); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/fulltext/MatchQuerySerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MatchQuerySerializationTests.java similarity index 89% rename from x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/fulltext/MatchQuerySerializationTests.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MatchQuerySerializationTests.java index 80a538cf84baa..7781c804a6dfc 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/fulltext/MatchQuerySerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MatchQuerySerializationTests.java @@ -5,9 +5,8 @@ * 2.0. */ -package org.elasticsearch.xpack.esql.expression.predicate.operator.fulltext; +package org.elasticsearch.xpack.esql.expression.predicate.fulltext; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.MatchQueryPredicate; import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests; import java.io.IOException; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/fulltext/MultiMatchQuerySerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MultiMatchQuerySerializationTests.java similarity index 92% rename from x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/fulltext/MultiMatchQuerySerializationTests.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MultiMatchQuerySerializationTests.java index d4d0f2edc11b1..17843e24a8663 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/fulltext/MultiMatchQuerySerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/fulltext/MultiMatchQuerySerializationTests.java @@ -5,9 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.esql.expression.predicate.operator.fulltext; - -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.MultiMatchQueryPredicate; +package org.elasticsearch.xpack.esql.expression.predicate.fulltext; import java.io.IOException; import java.util.HashMap; diff --git a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/BoolQueryTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/BoolQueryTests.java similarity index 92% rename from x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/BoolQueryTests.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/BoolQueryTests.java index 1c9d6bc54aebf..1aa5d47ed07ea 100644 --- a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/BoolQueryTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/BoolQueryTests.java @@ -4,9 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.querydsl.query; +package org.elasticsearch.xpack.esql.querydsl.query; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.core.querydsl.query.BoolQuery; +import org.elasticsearch.xpack.esql.core.querydsl.query.ExistsQuery; +import org.elasticsearch.xpack.esql.core.querydsl.query.NotQuery; +import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.tree.SourceTests; import org.elasticsearch.xpack.esql.core.util.StringUtils; diff --git a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/MatchQueryTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MatchQueryTests.java similarity index 96% rename from x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/MatchQueryTests.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MatchQueryTests.java index 4316bd21ffe94..49d1a9ad19d09 100644 --- a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/MatchQueryTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MatchQueryTests.java @@ -4,17 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.querydsl.query; +package org.elasticsearch.xpack.esql.querydsl.query; import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.Operator; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.MatchQueryPredicate; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.tree.SourceTests; import org.elasticsearch.xpack.esql.core.type.EsField; import org.elasticsearch.xpack.esql.core.util.StringUtils; +import org.elasticsearch.xpack.esql.expression.predicate.fulltext.MatchQueryPredicate; import java.util.Arrays; import java.util.List; diff --git a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/MultiMatchQueryTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQueryTests.java similarity index 94% rename from x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/MultiMatchQueryTests.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQueryTests.java index 9ca9765ed0542..93c285f5e3ab0 100644 --- a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/MultiMatchQueryTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQueryTests.java @@ -4,14 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.querydsl.query; +package org.elasticsearch.xpack.esql.querydsl.query; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.index.query.MultiMatchQueryBuilder; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.MultiMatchQueryPredicate; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.util.StringUtils; +import org.elasticsearch.xpack.esql.expression.predicate.fulltext.MultiMatchQueryPredicate; import java.util.HashMap; import java.util.Map; diff --git a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/QueryStringQueryTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/QueryStringQueryTests.java similarity index 94% rename from x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/QueryStringQueryTests.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/QueryStringQueryTests.java index 22e7b93e84ce1..3114b852aac70 100644 --- a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/querydsl/query/QueryStringQueryTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/QueryStringQueryTests.java @@ -4,12 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.esql.core.querydsl.query; +package org.elasticsearch.xpack.esql.querydsl.query; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.index.query.Operator; import org.elasticsearch.index.query.QueryStringQueryBuilder; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.core.querydsl.query.QueryStringQuery; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.util.StringUtils; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/tree/EsqlNodeSubclassTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/tree/EsqlNodeSubclassTests.java index c1d94933537f0..f01a125bc3c23 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/tree/EsqlNodeSubclassTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/tree/EsqlNodeSubclassTests.java @@ -25,7 +25,6 @@ import org.elasticsearch.xpack.esql.core.expression.UnresolvedAttributeTests; import org.elasticsearch.xpack.esql.core.expression.UnresolvedNamedExpression; import org.elasticsearch.xpack.esql.core.expression.function.Function; -import org.elasticsearch.xpack.esql.core.expression.predicate.fulltext.FullTextPredicate; import org.elasticsearch.xpack.esql.core.tree.AbstractNodeTestCase; import org.elasticsearch.xpack.esql.core.tree.Node; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -40,6 +39,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Pow; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat; +import org.elasticsearch.xpack.esql.expression.predicate.fulltext.FullTextPredicate; import org.elasticsearch.xpack.esql.plan.logical.Dissect; import org.elasticsearch.xpack.esql.plan.logical.Grok; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; From ce7a0c86fb975f76dfbea5eff1cf51602dbed184 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:34:22 +1100 Subject: [PATCH 022/119] Mute org.elasticsearch.xpack.esql.session.IndexResolverFieldNamesTests org.elasticsearch.xpack.esql.session.IndexResolverFieldNamesTests #118814 --- muted-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index b43045d24be81..da52d585267a5 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -305,6 +305,8 @@ tests: - class: org.elasticsearch.xpack.security.QueryableReservedRolesIT method: testDeletingAndCreatingSecurityIndexTriggersSynchronization issue: https://github.com/elastic/elasticsearch/issues/118806 +- class: org.elasticsearch.xpack.esql.session.IndexResolverFieldNamesTests + issue: https://github.com/elastic/elasticsearch/issues/118814 # Examples: # From d09d57db802ecba45195abfd1b38c12818bbe537 Mon Sep 17 00:00:00 2001 From: Carlos Delgado <6339205+carlosdelest@users.noreply.github.com> Date: Tue, 17 Dec 2024 09:14:50 +0100 Subject: [PATCH 023/119] Fix BwC synonyms tests (#118691) --- muted-tests.yml | 3 -- rest-api-spec/build.gradle | 1 + .../90_synonyms_reloading_for_synset.yml | 33 +++++++++++++++---- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index da52d585267a5..fe6c77bdf9f93 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -156,9 +156,6 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/117473 - class: org.elasticsearch.repositories.s3.RepositoryS3EcsClientYamlTestSuiteIT issue: https://github.com/elastic/elasticsearch/issues/117525 -- class: org.elasticsearch.backwards.MixedClusterClientYamlTestSuiteIT - method: test {p0=synonyms/90_synonyms_reloading_for_synset/Reload analyzers for specific synonym set} - issue: https://github.com/elastic/elasticsearch/issues/116777 - class: "org.elasticsearch.xpack.esql.qa.multi_node.EsqlSpecIT" method: "test {scoring.*}" issue: https://github.com/elastic/elasticsearch/issues/117641 diff --git a/rest-api-spec/build.gradle b/rest-api-spec/build.gradle index 7347d9c1312dd..bdee32e596c4c 100644 --- a/rest-api-spec/build.gradle +++ b/rest-api-spec/build.gradle @@ -69,4 +69,5 @@ tasks.named("yamlRestCompatTestTransform").configure ({ task -> task.skipTest("search/520_fetch_fields/fetch _seq_no via fields", "error code is changed from 5xx to 400 in 9.0") task.skipTest("search.vectors/41_knn_search_bbq_hnsw/Test knn search", "Scoring has changed in latest versions") task.skipTest("search.vectors/42_knn_search_bbq_flat/Test knn search", "Scoring has changed in latest versions") + task.skipTest("synonyms/90_synonyms_reloading_for_synset/Reload analyzers for specific synonym set", "Can't work until auto-expand replicas is 0-1 for synonyms index") }) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/90_synonyms_reloading_for_synset.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/90_synonyms_reloading_for_synset.yml index d6c98673253fb..4e6bd83f07955 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/90_synonyms_reloading_for_synset.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/90_synonyms_reloading_for_synset.yml @@ -1,8 +1,8 @@ ---- -"Reload analyzers for specific synonym set": +setup: - requires: cluster_features: ["gte_v8.10.0"] reason: Reloading analyzers for specific synonym set is introduced in 8.10.0 + # Create synonyms_set1 - do: synonyms.put_synonym: @@ -100,7 +100,12 @@ - '{"index": {"_index": "my_index2", "_id": "2"}}' - '{"my_field": "goodbye"}' - # An update of synonyms_set1 must trigger auto-reloading of analyzers only for synonyms_set1 +--- +"Reload analyzers for specific synonym set": +# These specific tests can't succeed in BwC, as synonyms auto-expand replicas are 0-all. Replicas can't be associated to +# upgraded nodes, and thus we are not able to guarantee that the shards are not failed. +# This test is skipped for BwC until synonyms index has auto-exapnd replicas set to 0-1. + - do: synonyms.put_synonym: id: synonyms_set1 @@ -108,13 +113,12 @@ synonyms_set: - synonyms: "hello, salute" - synonyms: "ciao => goodbye" + - match: { result: "updated" } - gt: { reload_analyzers_details._shards.total: 0 } - gt: { reload_analyzers_details._shards.successful: 0 } - match: { reload_analyzers_details._shards.failed: 0 } - - length: { reload_analyzers_details.reload_details: 1 } # reload details contain only a single index - - match: { reload_analyzers_details.reload_details.0.index: "my_index1" } - - match: { reload_analyzers_details.reload_details.0.reloaded_analyzers.0: "my_analyzer1" } + # Confirm that the index analyzers are reloaded for my_index1 - do: @@ -127,6 +131,23 @@ query: salute - match: { hits.total.value: 1 } +--- +"Check analyzer reloaded and non failed shards for bwc tests": + + - do: + synonyms.put_synonym: + id: synonyms_set1 + body: + synonyms_set: + - synonyms: "hello, salute" + - synonyms: "ciao => goodbye" + - match: { result: "updated" } + - gt: { reload_analyzers_details._shards.total: 0 } + - gt: { reload_analyzers_details._shards.successful: 0 } + - length: { reload_analyzers_details.reload_details: 1 } # reload details contain only a single index + - match: { reload_analyzers_details.reload_details.0.index: "my_index1" } + - match: { reload_analyzers_details.reload_details.0.reloaded_analyzers.0: "my_analyzer1" } + # Confirm that the index analyzers are still the same for my_index2 - do: search: From 87c9d13ed52668fb22e2c5da07744516aa84bf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slobodan=20Adamovi=C4=87?= Date: Tue, 17 Dec 2024 10:49:25 +0100 Subject: [PATCH 024/119] Mark Query Role API as public in seerverless (#118798) This commit makes Query Role API available in Serverless. --- .../xpack/security/rest/action/role/RestQueryRoleAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestQueryRoleAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestQueryRoleAction.java index 3637159479463..862ff2552b4e3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestQueryRoleAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestQueryRoleAction.java @@ -34,7 +34,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; -@ServerlessScope(Scope.INTERNAL) +@ServerlessScope(Scope.PUBLIC) public final class RestQueryRoleAction extends NativeRoleBaseRestHandler { @SuppressWarnings("unchecked") From 8793248d6d5dd1135327d938ae7a69e91c0eed00 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Tue, 17 Dec 2024 11:22:20 +0100 Subject: [PATCH 025/119] Remove unused code from PercolateQueryBuilder (#118791) --- .../percolator/PercolateQueryBuilder.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java index 85af5b120f6fd..c150f01153d35 100644 --- a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java +++ b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java @@ -43,7 +43,6 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.index.IndexVersion; @@ -80,7 +79,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.function.BiConsumer; import java.util.function.Supplier; import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES; @@ -88,20 +86,12 @@ import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; public class PercolateQueryBuilder extends AbstractQueryBuilder { - private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(ParseField.class); - static final String DOCUMENT_TYPE_DEPRECATION_MESSAGE = "[types removal] Types are deprecated in [percolate] queries. " - + "The [document_type] should no longer be specified."; - static final String TYPE_DEPRECATION_MESSAGE = "[types removal] Types are deprecated in [percolate] queries. " - + "The [type] of the indexed document should no longer be specified."; - public static final String NAME = "percolate"; static final ParseField DOCUMENT_FIELD = new ParseField("document"); static final ParseField DOCUMENTS_FIELD = new ParseField("documents"); private static final ParseField NAME_FIELD = new ParseField("name"); private static final ParseField QUERY_FIELD = new ParseField("field"); - private static final ParseField DOCUMENT_TYPE_FIELD = new ParseField("document_type"); - private static final ParseField INDEXED_DOCUMENT_FIELD_TYPE = new ParseField("type"); private static final ParseField INDEXED_DOCUMENT_FIELD_INDEX = new ParseField("index"); private static final ParseField INDEXED_DOCUMENT_FIELD_ID = new ParseField("id"); private static final ParseField INDEXED_DOCUMENT_FIELD_ROUTING = new ParseField("routing"); @@ -368,10 +358,6 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep ); } - private static BiConsumer deprecateAndIgnoreType(String key, String message) { - return (target, type) -> deprecationLogger.compatibleCritical(key, message); - } - private static BytesReference parseDocument(XContentParser parser) throws IOException { try (XContentBuilder builder = XContentFactory.jsonBuilder()) { builder.copyCurrentStructure(parser); From 8134c79ce8a11b26ee26d88ad7975c91a127af98 Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Tue, 17 Dec 2024 13:05:20 +0100 Subject: [PATCH 026/119] ESQL: Skip lookup fields when eliminating missing fields (#118658) We do not have SearchStats for fields from lookup indices. And unfortunately, these are hard to obtain. For now, just do not apply ReplaceMissingFieldWithNull to fields coming from an index used in LOOKUP JOIN. These are identified via their indexmode. --- .../esql/qa/mixed/MixedClusterEsqlSpecIT.java | 4 +- .../xpack/esql/ccq/MultiClusterSpecIT.java | 8 +- .../rest/RequestIndexFilteringTestCase.java | 2 +- .../src/main/resources/lookup-join.csv-spec | 212 ++++++++++++------ .../xpack/esql/action/EsqlCapabilities.java | 2 +- .../local/ReplaceMissingFieldWithNull.java | 15 +- .../elasticsearch/xpack/esql/CsvTests.java | 2 +- .../xpack/esql/analysis/AnalyzerTests.java | 4 +- .../xpack/esql/analysis/VerifierTests.java | 2 +- .../optimizer/LogicalPlanOptimizerTests.java | 12 +- .../optimizer/PhysicalPlanOptimizerTests.java | 2 +- 11 files changed, 180 insertions(+), 85 deletions(-) diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java index 5efe7ffc800a2..004beaafb4009 100644 --- a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java @@ -21,7 +21,7 @@ import java.util.List; import static org.elasticsearch.xpack.esql.CsvTestUtils.isEnabled; -import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V6; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V7; import static org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase.Mode.ASYNC; public class MixedClusterEsqlSpecIT extends EsqlSpecTestCase { @@ -96,7 +96,7 @@ protected boolean supportsInferenceTestService() { @Override protected boolean supportsIndexModeLookup() throws IOException { - return hasCapabilities(List.of(JOIN_LOOKUP_V6.capabilityName())); + return hasCapabilities(List.of(JOIN_LOOKUP_V7.capabilityName())); } @Override diff --git a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java index dd75776973c3d..c75a920e16973 100644 --- a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java +++ b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java @@ -48,7 +48,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.classpathResources; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS_V2; -import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V6; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V7; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_PLANNING_V1; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.METADATA_FIELDS_REMOTE_TEST; import static org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase.Mode.SYNC; @@ -124,7 +124,7 @@ protected void shouldSkipTest(String testName) throws IOException { assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS.capabilityName())); assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS_V2.capabilityName())); assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_PLANNING_V1.capabilityName())); - assumeFalse("LOOKUP JOIN not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_LOOKUP_V6.capabilityName())); + assumeFalse("LOOKUP JOIN not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_LOOKUP_V7.capabilityName())); } private TestFeatureService remoteFeaturesService() throws IOException { @@ -283,8 +283,8 @@ protected boolean supportsInferenceTestService() { @Override protected boolean supportsIndexModeLookup() throws IOException { - // CCS does not yet support JOIN_LOOKUP_V6 and clusters falsely report they have this capability - // return hasCapabilities(List.of(JOIN_LOOKUP_V6.capabilityName())); + // CCS does not yet support JOIN_LOOKUP_V7 and clusters falsely report they have this capability + // return hasCapabilities(List.of(JOIN_LOOKUP_V7.capabilityName())); return false; } } diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java index 2aae4c94c33fe..40027249670f6 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java @@ -221,7 +221,7 @@ public void testIndicesDontExist() throws IOException { assertThat(e.getMessage(), containsString("index_not_found_exception")); assertThat(e.getMessage(), containsString("no such index [foo]")); - if (EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()) { + if (EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()) { e = expectThrows( ResponseException.class, () -> runEsql(timestampFilter("gte", "2020-01-01").query("FROM test1 | LOOKUP JOIN foo ON id1")) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec index 8b8d24b1bb156..8bcc2c2ff3502 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec @@ -8,7 +8,7 @@ ############################################### basicOnTheDataNode -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | EVAL language_code = languages @@ -25,7 +25,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; basicRow -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW language_code = 1 | LOOKUP JOIN languages_lookup ON language_code @@ -36,7 +36,7 @@ language_code:integer | language_name:keyword ; basicOnTheCoordinator -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | SORT emp_no @@ -53,7 +53,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; subsequentEvalOnTheDataNode -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | EVAL language_code = languages @@ -71,7 +71,7 @@ emp_no:integer | language_code:integer | language_name:keyword | language_code_x ; subsequentEvalOnTheCoordinator -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | SORT emp_no @@ -89,7 +89,7 @@ emp_no:integer | language_code:integer | language_name:keyword | language_code_x ; sortEvalBeforeLookup -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | SORT emp_no @@ -106,7 +106,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; nonUniqueLeftKeyOnTheDataNode -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | WHERE emp_no <= 10030 @@ -130,7 +130,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; nonUniqueRightKeyOnTheDataNode -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | EVAL language_code = emp_no % 10 @@ -150,7 +150,7 @@ emp_no:integer | language_code:integer | language_name:keyword | country:k ; nonUniqueRightKeyOnTheCoordinator -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | SORT emp_no @@ -170,7 +170,7 @@ emp_no:integer | language_code:integer | language_name:keyword | country:k ; nonUniqueRightKeyFromRow -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW language_code = 2 | LOOKUP JOIN languages_lookup_non_unique_key ON language_code @@ -186,8 +186,8 @@ language_code:integer | language_name:keyword | country:keyword # Filtering tests with languages_lookup index ############################################### -lookupWithFilterOnLeftSideField -required_capability: join_lookup_v6 +filterOnLeftSide +required_capability: join_lookup_v7 FROM employees | EVAL language_code = languages @@ -203,8 +203,8 @@ emp_no:integer | language_code:integer | language_name:keyword 10093 | 3 | Spanish ; -lookupMessageWithFilterOnRightSideField-Ignore -required_capability: join_lookup_v6 +filterOnRightSide +required_capability: join_lookup_v7 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -219,8 +219,8 @@ FROM sample_data 2023-10-23T13:51:54.732Z | 172.21.3.15 | 725448 | Connection error | Error ; -lookupWithFieldAndRightSideAfterStats -required_capability: join_lookup_v6 +filterOnRightSideAfterStats +required_capability: join_lookup_v7 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -232,23 +232,110 @@ count:long | type:keyword 3 | Error ; -lookupWithFieldOnJoinKey-Ignore -required_capability: join_lookup_v6 +filterOnJoinKey +required_capability: join_lookup_v7 FROM employees | EVAL language_code = languages +| WHERE emp_no >= 10091 AND emp_no < 10094 +| LOOKUP JOIN languages_lookup ON language_code +| WHERE language_code == 1 +| KEEP emp_no, language_code, language_name +; + +emp_no:integer | language_code:integer | language_name:keyword +10092 | 1 | English +; + +filterOnJoinKeyAndRightSide +required_capability: join_lookup_v7 + +FROM employees +| WHERE emp_no < 10006 +| EVAL language_code = languages | LOOKUP JOIN languages_lookup ON language_code | WHERE language_code > 1 AND language_name IS NOT NULL | KEEP emp_no, language_code, language_name ; +ignoreOrder:true emp_no:integer | language_code:integer | language_name:keyword 10001 | 2 | French 10003 | 4 | German ; +filterOnRightSideOnTheCoordinator +required_capability: join_lookup_v7 + +FROM employees +| SORT emp_no +| LIMIT 5 +| EVAL language_code = languages +| LOOKUP JOIN languages_lookup ON language_code +| WHERE language_name == "English" +| KEEP emp_no, language_code, language_name +; + +emp_no:integer | language_code:integer | language_name:keyword +10005 | 1 | English +; + +filterOnJoinKeyOnTheCoordinator +required_capability: join_lookup_v7 + +FROM employees +| SORT emp_no +| LIMIT 5 +| EVAL language_code = languages +| LOOKUP JOIN languages_lookup ON language_code +| WHERE language_code == 1 +| KEEP emp_no, language_code, language_name +; + +emp_no:integer | language_code:integer | language_name:keyword +10005 | 1 | English +; + +filterOnJoinKeyAndRightSideOnTheCoordinator +required_capability: join_lookup_v7 + +FROM employees +| SORT emp_no +| LIMIT 5 +| EVAL language_code = languages +| LOOKUP JOIN languages_lookup ON language_code +| WHERE language_code > 1 AND language_name IS NOT NULL +| KEEP emp_no, language_code, language_name +; + +emp_no:integer | language_code:integer | language_name:keyword +10001 | 2 | French +10003 | 4 | German +; + +filterOnTheDataNodeThenFilterOnTheCoordinator +required_capability: join_lookup_v7 + +FROM employees +| EVAL language_code = languages +| WHERE emp_no >= 10091 AND emp_no < 10094 +| LOOKUP JOIN languages_lookup ON language_code +| WHERE language_name == "English" +| KEEP emp_no, language_code, language_name +| SORT emp_no +| WHERE language_code == 1 +; + +emp_no:integer | language_code:integer | language_name:keyword +10092 | 1 | English +; + +########################################################################### +# null and multi-value behavior with languages_lookup_non_unique_key index +########################################################################### + nullJoinKeyOnTheDataNode -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | WHERE emp_no < 10004 @@ -264,9 +351,8 @@ emp_no:integer | language_code:integer | language_name:keyword 10003 | null | null ; - mvJoinKeyOnTheDataNode -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM employees | WHERE 10003 < emp_no AND emp_no < 10008 @@ -284,7 +370,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; mvJoinKeyFromRow -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW language_code = [4, 5, 6, 7] | LOOKUP JOIN languages_lookup_non_unique_key ON language_code @@ -297,7 +383,7 @@ language_code:integer | language_name:keyword | country:keyword ; mvJoinKeyFromRowExpanded -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW language_code = [4, 5, 6, 7, 8] | MV_EXPAND language_code @@ -319,7 +405,7 @@ language_code:integer | language_name:keyword | country:keyword ############################################### lookupIPFromRow -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -330,7 +416,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromKeepRow -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", right = "right" | KEEP left, client_ip, right @@ -342,7 +428,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowing -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -353,7 +439,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowingKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -366,7 +452,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowingKeepReordered -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -379,7 +465,7 @@ right | Development | 172.21.0.5 ; lookupIPFromIndex -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -398,7 +484,7 @@ ignoreOrder:true ; lookupIPFromIndexKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -418,7 +504,7 @@ ignoreOrder:true ; lookupIPFromIndexKeepKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | KEEP client_ip, event_duration, @timestamp, message @@ -440,7 +526,7 @@ timestamp:date | client_ip:keyword | event_duration:long | msg:keyword ; lookupIPFromIndexStats -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -456,7 +542,7 @@ count:long | env:keyword ; lookupIPFromIndexStatsKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -473,7 +559,7 @@ count:long | env:keyword ; statsAndLookupIPFromIndex -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -494,7 +580,7 @@ count:long | client_ip:keyword | env:keyword ############################################### lookupMessageFromRow -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", message = "Connected to 10.1.0.1", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -505,7 +591,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromKeepRow -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", message = "Connected to 10.1.0.1", right = "right" | KEEP left, message, right @@ -517,7 +603,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromRowWithShadowing -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", message = "Connected to 10.1.0.1", type = "unknown", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -528,7 +614,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromRowWithShadowingKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", message = "Connected to 10.1.0.1", type = "unknown", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -540,7 +626,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromIndex -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -558,7 +644,7 @@ ignoreOrder:true ; lookupMessageFromIndexKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -577,7 +663,7 @@ ignoreOrder:true ; lookupMessageFromIndexKeepKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | KEEP client_ip, event_duration, @timestamp, message @@ -597,7 +683,7 @@ ignoreOrder:true ; lookupMessageFromIndexKeepReordered -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -616,7 +702,7 @@ Success | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 ; lookupMessageFromIndexStats -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -631,7 +717,7 @@ count:long | type:keyword ; lookupMessageFromIndexStatsKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -647,7 +733,7 @@ count:long | type:keyword ; statsAndLookupMessageFromIndex -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | STATS count = count(message) BY message @@ -665,7 +751,7 @@ count:long | type:keyword | message:keyword ; lookupMessageFromIndexTwice -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -687,7 +773,7 @@ ignoreOrder:true ; lookupMessageFromIndexTwiceKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -714,7 +800,7 @@ ignoreOrder:true ############################################### lookupIPAndMessageFromRow -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -726,7 +812,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowKeepBefore -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" | KEEP left, client_ip, message, right @@ -739,7 +825,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowKeepBetween -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -752,7 +838,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowKeepAfter -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -765,7 +851,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowing -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", type = "type", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -777,7 +863,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowingKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -791,7 +877,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowingKeepKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -806,7 +892,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowingKeepKeepKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -822,7 +908,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowingKeepReordered -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -836,7 +922,7 @@ right | Development | Success | 172.21.0.5 ; lookupIPAndMessageFromIndex -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -856,7 +942,7 @@ ignoreOrder:true ; lookupIPAndMessageFromIndexKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -877,7 +963,7 @@ ignoreOrder:true ; lookupIPAndMessageFromIndexStats -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -895,7 +981,7 @@ count:long | env:keyword | type:keyword ; lookupIPAndMessageFromIndexStatsKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -914,7 +1000,7 @@ count:long | env:keyword | type:keyword ; statsAndLookupIPAndMessageFromIndex -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -933,7 +1019,7 @@ count:long | client_ip:keyword | message:keyword | env:keyword | type:keyw ; lookupIPAndMessageFromIndexChainedEvalKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -955,7 +1041,7 @@ ignoreOrder:true ; lookupIPAndMessageFromIndexChainedRenameKeep -required_capability: join_lookup_v6 +required_capability: join_lookup_v7 FROM sample_data | EVAL client_ip = client_ip::keyword diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 235d0dcbe4164..4fcabb02b2d4f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -547,7 +547,7 @@ public enum Cap { /** * LOOKUP JOIN */ - JOIN_LOOKUP_V6(Build.current().isSnapshot()), + JOIN_LOOKUP_V7(Build.current().isSnapshot()), /** * Fix for https://github.com/elastic/elasticsearch/issues/117054 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceMissingFieldWithNull.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceMissingFieldWithNull.java index 096f72f7694e1..f9d86ecf0f61a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceMissingFieldWithNull.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceMissingFieldWithNull.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.esql.optimizer.rules.logical.local; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.index.IndexMode; import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.AttributeSet; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; @@ -41,10 +42,17 @@ public class ReplaceMissingFieldWithNull extends ParameterizedRule missingToNull(p, localLogicalOptimizerContext.searchStats())); + AttributeSet lookupFields = new AttributeSet(); + plan.forEachUp(EsRelation.class, esRelation -> { + if (esRelation.indexMode() == IndexMode.LOOKUP) { + lookupFields.addAll(esRelation.output()); + } + }); + + return plan.transformUp(p -> missingToNull(p, localLogicalOptimizerContext.searchStats(), lookupFields)); } - private LogicalPlan missingToNull(LogicalPlan plan, SearchStats stats) { + private LogicalPlan missingToNull(LogicalPlan plan, SearchStats stats, AttributeSet lookupFields) { if (plan instanceof EsRelation || plan instanceof LocalRelation) { return plan; } @@ -95,7 +103,8 @@ else if (plan instanceof Project project) { plan = plan.transformExpressionsOnlyUp( FieldAttribute.class, // Do not use the attribute name, this can deviate from the field name for union types. - f -> stats.exists(f.fieldName()) ? f : Literal.of(f, null) + // Also skip fields from lookup indices because we do not have stats for these. + f -> stats.exists(f.fieldName()) || lookupFields.contains(f) ? f : Literal.of(f, null) ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index f553c15ef69fa..717ac7b5a62a7 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -263,7 +263,7 @@ public final void test() throws Throwable { ); assumeFalse( "lookup join disabled for csv tests", - testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.JOIN_LOOKUP_V6.capabilityName()) + testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.JOIN_LOOKUP_V7.capabilityName()) ); assumeFalse( "can't use TERM function in csv tests", diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index 4e02119b31744..9c71f20dcde0e 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -2139,7 +2139,7 @@ public void testLookupMatchTypeWrong() { } public void testLookupJoinUnknownIndex() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); String errorMessage = "Unknown index [foobar]"; IndexResolution missingLookupIndex = IndexResolution.invalid(errorMessage); @@ -2168,7 +2168,7 @@ public void testLookupJoinUnknownIndex() { } public void testLookupJoinUnknownField() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); String query = "FROM test | LOOKUP JOIN languages_lookup ON last_name"; String errorMessage = "1:45: Unknown column [last_name] in right side of join"; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 58180aafedc0b..182e87d1ab9dd 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1964,7 +1964,7 @@ public void testSortByAggregate() { } public void testLookupJoinDataTypeMismatch() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); query("FROM test | EVAL language_code = languages | LOOKUP JOIN languages_lookup ON language_code"); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index c4d7b30115c2d..cfb993a7dd73d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -4906,7 +4906,7 @@ public void testPlanSanityCheck() throws Exception { } public void testPlanSanityCheckWithBinaryPlans() throws Exception { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); var plan = optimizedPlan(""" FROM test @@ -5911,7 +5911,7 @@ public void testLookupStats() { * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#18, language_name{f}#19] */ public void testLookupJoinPushDownFilterOnJoinKeyWithRename() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); String query = """ FROM test @@ -5954,7 +5954,7 @@ public void testLookupJoinPushDownFilterOnJoinKeyWithRename() { * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#18, language_name{f}#19] */ public void testLookupJoinPushDownFilterOnLeftSideField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); String query = """ FROM test @@ -5998,7 +5998,7 @@ public void testLookupJoinPushDownFilterOnLeftSideField() { * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#18, language_name{f}#19] */ public void testLookupJoinPushDownDisabledForLookupField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); String query = """ FROM test @@ -6043,7 +6043,7 @@ public void testLookupJoinPushDownDisabledForLookupField() { * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#19, language_name{f}#20] */ public void testLookupJoinPushDownSeparatedForConjunctionBetweenLeftAndRightField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); String query = """ FROM test @@ -6096,7 +6096,7 @@ public void testLookupJoinPushDownSeparatedForConjunctionBetweenLeftAndRightFiel * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#19, language_name{f}#20] */ public void testLookupJoinPushDownDisabledForDisjunctionBetweenLeftAndRightField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); String query = """ FROM test diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index 9f6ef89008a24..964dd4642d7c2 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -2331,7 +2331,7 @@ public void testVerifierOnMissingReferences() { } public void testVerifierOnMissingReferencesWithBinaryPlans() throws Exception { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V6.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); // Do not assert serialization: // This will have a LookupJoinExec, which is not serializable because it doesn't leave the coordinator. From 6516a535ab487eabf02b7d65b7cbe928820a181b Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Tue, 17 Dec 2024 13:45:27 +0100 Subject: [PATCH 027/119] Add wolfi documentation from 8.16 branch (#118835) port from https://github.com/elastic/elasticsearch/pull/118684 --- docs/Versions.asciidoc | 1 + docs/reference/setup/install/docker.asciidoc | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/Versions.asciidoc b/docs/Versions.asciidoc index bdb0704fcd880..f2e61861bd3a6 100644 --- a/docs/Versions.asciidoc +++ b/docs/Versions.asciidoc @@ -9,6 +9,7 @@ include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] :docker-repo: docker.elastic.co/elasticsearch/elasticsearch :docker-image: {docker-repo}:{version} +:docker-wolfi-image: {docker-repo}-wolfi:{version} :kib-docker-repo: docker.elastic.co/kibana/kibana :kib-docker-image: {kib-docker-repo}:{version} :plugin_url: https://artifacts.elastic.co/downloads/elasticsearch-plugins diff --git a/docs/reference/setup/install/docker.asciidoc b/docs/reference/setup/install/docker.asciidoc index 8694d7f5b46c6..86a0e567f6eec 100644 --- a/docs/reference/setup/install/docker.asciidoc +++ b/docs/reference/setup/install/docker.asciidoc @@ -55,6 +55,12 @@ docker pull {docker-image} // REVIEWED[DEC.10.24] -- +Alternatevely, you can use the Wolfi based image. Using Wolfi based images requires Docker version 20.10.10 or superior. +[source,sh,subs="attributes"] +---- +docker pull {docker-wolfi-image} +---- + . Optional: Install https://docs.sigstore.dev/cosign/system_config/installation/[Cosign] for your environment. Then use Cosign to verify the {es} image's signature. From f5712e4875122d9f5c451ac73225725179795929 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 17 Dec 2024 13:18:42 +0000 Subject: [PATCH 028/119] Infrastructure for assuming cluster features in the next major version (#118143) Allow features to be marked as 'assumed', allowing them to be removed in the next major version. --- .../forbidden/es-server-signatures.txt | 4 +- docs/changelog/118143.yaml | 5 + .../cluster/ClusterFeatures.java | 56 ++++- .../coordination/NodeJoinExecutor.java | 81 +++++- .../org/elasticsearch/env/BuildVersion.java | 6 + .../env/DefaultBuildVersion.java | 11 + .../features/FeatureService.java | 23 +- .../elasticsearch/features/NodeFeature.java | 9 +- .../readiness/ReadinessService.java | 4 +- .../DataStreamAutoShardingServiceTests.java | 29 ++- .../coordination/NodeJoinExecutorTests.java | 232 +++++++++++++++++- .../features/FeatureServiceTests.java | 41 ++++ .../HealthNodeTaskExecutorTests.java | 2 +- .../slm/SnapshotLifecycleServiceTests.java | 3 + 14 files changed, 464 insertions(+), 42 deletions(-) create mode 100644 docs/changelog/118143.yaml diff --git a/build-tools-internal/src/main/resources/forbidden/es-server-signatures.txt b/build-tools-internal/src/main/resources/forbidden/es-server-signatures.txt index a9da7995c2b36..53480a4a27b0b 100644 --- a/build-tools-internal/src/main/resources/forbidden/es-server-signatures.txt +++ b/build-tools-internal/src/main/resources/forbidden/es-server-signatures.txt @@ -155,10 +155,8 @@ org.elasticsearch.cluster.ClusterState#compatibilityVersions() @defaultMessage ClusterFeatures#nodeFeatures is for internal use only. Use FeatureService#clusterHasFeature to determine if a feature is present on the cluster. org.elasticsearch.cluster.ClusterFeatures#nodeFeatures() -@defaultMessage ClusterFeatures#allNodeFeatures is for internal use only. Use FeatureService#clusterHasFeature to determine if a feature is present on the cluster. -org.elasticsearch.cluster.ClusterFeatures#allNodeFeatures() @defaultMessage ClusterFeatures#clusterHasFeature is for internal use only. Use FeatureService#clusterHasFeature to determine if a feature is present on the cluster. -org.elasticsearch.cluster.ClusterFeatures#clusterHasFeature(org.elasticsearch.features.NodeFeature) +org.elasticsearch.cluster.ClusterFeatures#clusterHasFeature(org.elasticsearch.cluster.node.DiscoveryNodes, org.elasticsearch.features.NodeFeature) @defaultMessage Do not construct this records outside the source files they are declared in org.elasticsearch.cluster.SnapshotsInProgress$ShardSnapshotStatus#(java.lang.String, org.elasticsearch.cluster.SnapshotsInProgress$ShardState, org.elasticsearch.repositories.ShardGeneration, java.lang.String, org.elasticsearch.repositories.ShardSnapshotResult) diff --git a/docs/changelog/118143.yaml b/docs/changelog/118143.yaml new file mode 100644 index 0000000000000..4dcbf4b4b6c2c --- /dev/null +++ b/docs/changelog/118143.yaml @@ -0,0 +1,5 @@ +pr: 118143 +summary: Infrastructure for assuming cluster features in the next major version +area: "Infra/Core" +type: feature +issues: [] diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterFeatures.java b/server/src/main/java/org/elasticsearch/cluster/ClusterFeatures.java index ad285cbd391cd..5b5a6577082d7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/ClusterFeatures.java +++ b/server/src/main/java/org/elasticsearch/cluster/ClusterFeatures.java @@ -9,11 +9,12 @@ package org.elasticsearch.cluster; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.common.xcontent.ChunkedToXContentObject; -import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.features.NodeFeature; import org.elasticsearch.xcontent.ToXContent; @@ -79,28 +80,61 @@ public Map> nodeFeatures() { return nodeFeatures; } - /** - * The features in all nodes in the cluster. - *

- * NOTE: This should not be used directly. - * Please use {@link org.elasticsearch.features.FeatureService#clusterHasFeature} instead. - */ - public Set allNodeFeatures() { + private Set allNodeFeatures() { if (allNodeFeatures == null) { allNodeFeatures = Set.copyOf(calculateAllNodeFeatures(nodeFeatures.values())); } return allNodeFeatures; } + /** + * Returns {@code true} if {@code node} can have assumed features. + * @see org.elasticsearch.env.BuildVersion#canRemoveAssumedFeatures + */ + public static boolean featuresCanBeAssumedForNode(DiscoveryNode node) { + return node.getBuildVersion().canRemoveAssumedFeatures(); + } + + /** + * Returns {@code true} if one or more nodes in {@code nodes} can have assumed features. + * @see org.elasticsearch.env.BuildVersion#canRemoveAssumedFeatures + */ + public static boolean featuresCanBeAssumedForNodes(DiscoveryNodes nodes) { + return nodes.getAllNodes().stream().anyMatch(n -> n.getBuildVersion().canRemoveAssumedFeatures()); + } + /** * {@code true} if {@code feature} is present on all nodes in the cluster. *

* NOTE: This should not be used directly. * Please use {@link org.elasticsearch.features.FeatureService#clusterHasFeature} instead. */ - @SuppressForbidden(reason = "directly reading cluster features") - public boolean clusterHasFeature(NodeFeature feature) { - return allNodeFeatures().contains(feature.id()); + public boolean clusterHasFeature(DiscoveryNodes nodes, NodeFeature feature) { + assert nodes.getNodes().keySet().equals(nodeFeatures.keySet()) + : "Cluster features nodes " + nodeFeatures.keySet() + " is different to discovery nodes " + nodes.getNodes().keySet(); + + // basic case + boolean allNodesHaveFeature = allNodeFeatures().contains(feature.id()); + if (allNodesHaveFeature) { + return true; + } + + // if the feature is assumed, check the versions more closely + // it's actually ok if the feature is assumed, and all nodes missing the feature can assume it + // TODO: do we need some kind of transient cache of this calculation? + if (feature.assumedAfterNextCompatibilityBoundary()) { + for (var nf : nodeFeatures.entrySet()) { + if (nf.getValue().contains(feature.id()) == false + && featuresCanBeAssumedForNode(nodes.getNodes().get(nf.getKey())) == false) { + return false; + } + } + + // all nodes missing the feature can assume it - so that's alright then + return true; + } + + return false; } /** diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/NodeJoinExecutor.java b/server/src/main/java/org/elasticsearch/cluster/coordination/NodeJoinExecutor.java index 5235293a54d95..74a8dc7851c89 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/NodeJoinExecutor.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/NodeJoinExecutor.java @@ -29,6 +29,7 @@ import org.elasticsearch.common.Priority; import org.elasticsearch.common.Strings; import org.elasticsearch.features.FeatureService; +import org.elasticsearch.features.NodeFeature; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.persistent.PersistentTasksCustomMetadata; @@ -39,6 +40,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -137,8 +139,8 @@ public ClusterState execute(BatchExecutionContext batchExecutionContex DiscoveryNodes.Builder nodesBuilder = DiscoveryNodes.builder(newState.nodes()); Map compatibilityVersionsMap = new HashMap<>(newState.compatibilityVersions()); - Map> nodeFeatures = new HashMap<>(newState.nodeFeatures()); - Set allNodesFeatures = ClusterFeatures.calculateAllNodeFeatures(nodeFeatures.values()); + Map> nodeFeatures = new HashMap<>(newState.nodeFeatures()); // as present in cluster state + Set effectiveClusterFeatures = calculateEffectiveClusterFeatures(newState.nodes(), nodeFeatures); assert nodesBuilder.isLocalNodeElectedMaster(); @@ -174,14 +176,17 @@ public ClusterState execute(BatchExecutionContext batchExecutionContex } blockForbiddenVersions(compatibilityVersions.transportVersion()); ensureNodesCompatibility(node.getVersion(), minClusterNodeVersion, maxClusterNodeVersion); - enforceNodeFeatureBarrier(node.getId(), allNodesFeatures, features); + Set newNodeEffectiveFeatures = enforceNodeFeatureBarrier(node, effectiveClusterFeatures, features); // we do this validation quite late to prevent race conditions between nodes joining and importing dangling indices // we have to reject nodes that don't support all indices we have in this cluster ensureIndexCompatibility(node.getMinIndexVersion(), node.getMaxIndexVersion(), initialState.getMetadata()); + nodesBuilder.add(node); compatibilityVersionsMap.put(node.getId(), compatibilityVersions); + // store the actual node features here, not including assumed features, as this is persisted in cluster state nodeFeatures.put(node.getId(), features); - allNodesFeatures.retainAll(features); + effectiveClusterFeatures.retainAll(newNodeEffectiveFeatures); + nodesChanged = true; minClusterNodeVersion = Version.min(minClusterNodeVersion, node.getVersion()); maxClusterNodeVersion = Version.max(maxClusterNodeVersion, node.getVersion()); @@ -355,6 +360,35 @@ private static void blockForbiddenVersions(TransportVersion joiningTransportVers } } + /** + * Calculate the cluster's effective features. This includes all features that are assumed on any nodes in the cluster, + * that are also present across the whole cluster as a result. + */ + private Set calculateEffectiveClusterFeatures(DiscoveryNodes nodes, Map> nodeFeatures) { + if (featureService.featuresCanBeAssumedForNodes(nodes)) { + Set assumedFeatures = featureService.getNodeFeatures() + .values() + .stream() + .filter(NodeFeature::assumedAfterNextCompatibilityBoundary) + .map(NodeFeature::id) + .collect(Collectors.toSet()); + + // add all assumed features to the featureset of all nodes of the next major version + nodeFeatures = new HashMap<>(nodeFeatures); + for (var node : nodes.getNodes().entrySet()) { + if (featureService.featuresCanBeAssumedForNode(node.getValue())) { + assert nodeFeatures.containsKey(node.getKey()) : "Node " + node.getKey() + " does not have any features"; + nodeFeatures.computeIfPresent(node.getKey(), (k, v) -> { + var newFeatures = new HashSet<>(v); + return newFeatures.addAll(assumedFeatures) ? newFeatures : v; + }); + } + } + } + + return ClusterFeatures.calculateAllNodeFeatures(nodeFeatures.values()); + } + /** * Ensures that all indices are compatible with the given index version. This will ensure that all indices in the given metadata * will not be created with a newer version of elasticsearch as well as that all indices are newer or equal to the minimum index @@ -461,13 +495,44 @@ public static void ensureVersionBarrier(Version joiningNodeVersion, Version minC } } - private void enforceNodeFeatureBarrier(String nodeId, Set existingNodesFeatures, Set newNodeFeatures) { + /** + * Enforces the feature join barrier - a joining node should have all features already present in all existing nodes in the cluster + * + * @return The set of features that this node has (including assumed features) + */ + private Set enforceNodeFeatureBarrier(DiscoveryNode node, Set effectiveClusterFeatures, Set newNodeFeatures) { // prevent join if it does not have one or more features that all other nodes have - Set missingFeatures = new HashSet<>(existingNodesFeatures); + Set missingFeatures = new HashSet<>(effectiveClusterFeatures); missingFeatures.removeAll(newNodeFeatures); - if (missingFeatures.isEmpty() == false) { - throw new IllegalStateException("Node " + nodeId + " is missing required features " + missingFeatures); + if (missingFeatures.isEmpty()) { + // nothing missing - all ok + return newNodeFeatures; + } + + if (featureService.featuresCanBeAssumedForNode(node)) { + // it might still be ok for this node to join if this node can have assumed features, + // and all the missing features are assumed + // we can get the NodeFeature object direct from this node's registered features + // as all existing nodes in the cluster have the features present in existingNodesFeatures, including this one + newNodeFeatures = new HashSet<>(newNodeFeatures); + for (Iterator it = missingFeatures.iterator(); it.hasNext();) { + String feature = it.next(); + NodeFeature nf = featureService.getNodeFeatures().get(feature); + if (nf.assumedAfterNextCompatibilityBoundary()) { + // its ok for this feature to be missing from this node + it.remove(); + // and it should be assumed to still be in the cluster + newNodeFeatures.add(feature); + } + // even if we don't remove it, still continue, so the exception message below is accurate + } + } + + if (missingFeatures.isEmpty()) { + return newNodeFeatures; + } else { + throw new IllegalStateException("Node " + node.getId() + " is missing required features " + missingFeatures); } } diff --git a/server/src/main/java/org/elasticsearch/env/BuildVersion.java b/server/src/main/java/org/elasticsearch/env/BuildVersion.java index 7a6b27eab2330..5c3602283fef3 100644 --- a/server/src/main/java/org/elasticsearch/env/BuildVersion.java +++ b/server/src/main/java/org/elasticsearch/env/BuildVersion.java @@ -37,6 +37,12 @@ */ public abstract class BuildVersion implements ToXContentFragment, Writeable { + /** + * Checks if this version can operate properly in a cluster without features + * that are assumed in the currently running Elasticsearch. + */ + public abstract boolean canRemoveAssumedFeatures(); + /** * Check whether this version is on or after a minimum threshold. * diff --git a/server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java b/server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java index a7e1a4fee341d..70aa3f6639a4d 100644 --- a/server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java +++ b/server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java @@ -47,6 +47,17 @@ final class DefaultBuildVersion extends BuildVersion { this(in.readVInt()); } + @Override + public boolean canRemoveAssumedFeatures() { + /* + * We can remove assumed features if the node version is the next major version. + * This is because the next major version can only form a cluster with the + * latest minor version of the previous major, so any features introduced before that point + * (that are marked as assumed in the running code version) are automatically met by that version. + */ + return version.major == Version.CURRENT.major + 1; + } + @Override public boolean onOrAfterMinimumCompatible() { return Version.CURRENT.minimumCompatibilityVersion().onOrBefore(version); diff --git a/server/src/main/java/org/elasticsearch/features/FeatureService.java b/server/src/main/java/org/elasticsearch/features/FeatureService.java index 9a0ac7cafc183..c04fbae05ee2c 100644 --- a/server/src/main/java/org/elasticsearch/features/FeatureService.java +++ b/server/src/main/java/org/elasticsearch/features/FeatureService.java @@ -9,7 +9,10 @@ package org.elasticsearch.features; +import org.elasticsearch.cluster.ClusterFeatures; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; @@ -38,9 +41,7 @@ public class FeatureService { * as the local node's supported feature set */ public FeatureService(List specs) { - - var featureData = FeatureData.createFromSpecifications(specs); - nodeFeatures = featureData.getNodeFeatures(); + this.nodeFeatures = FeatureData.createFromSpecifications(specs).getNodeFeatures(); logger.info("Registered local node features {}", nodeFeatures.keySet().stream().sorted().toList()); } @@ -53,11 +54,25 @@ public Map getNodeFeatures() { return nodeFeatures; } + /** + * Returns {@code true} if {@code node} can have assumed features. + */ + public boolean featuresCanBeAssumedForNode(DiscoveryNode node) { + return ClusterFeatures.featuresCanBeAssumedForNode(node); + } + + /** + * Returns {@code true} if one or more nodes in {@code nodes} can have assumed features. + */ + public boolean featuresCanBeAssumedForNodes(DiscoveryNodes nodes) { + return ClusterFeatures.featuresCanBeAssumedForNodes(nodes); + } + /** * Returns {@code true} if all nodes in {@code state} support feature {@code feature}. */ @SuppressForbidden(reason = "We need basic feature information from cluster state") public boolean clusterHasFeature(ClusterState state, NodeFeature feature) { - return state.clusterFeatures().clusterHasFeature(feature); + return state.clusterFeatures().clusterHasFeature(state.nodes(), feature); } } diff --git a/server/src/main/java/org/elasticsearch/features/NodeFeature.java b/server/src/main/java/org/elasticsearch/features/NodeFeature.java index 957308e805562..961b386d62802 100644 --- a/server/src/main/java/org/elasticsearch/features/NodeFeature.java +++ b/server/src/main/java/org/elasticsearch/features/NodeFeature.java @@ -15,10 +15,17 @@ * A feature published by a node. * * @param id The feature id. Must be unique in the node. + * @param assumedAfterNextCompatibilityBoundary + * {@code true} if this feature is removed at the next compatibility boundary (ie next major version), + * and so should be assumed to be true for all nodes after that boundary. */ -public record NodeFeature(String id) { +public record NodeFeature(String id, boolean assumedAfterNextCompatibilityBoundary) { public NodeFeature { Objects.requireNonNull(id); } + + public NodeFeature(String id) { + this(id, false); + } } diff --git a/server/src/main/java/org/elasticsearch/readiness/ReadinessService.java b/server/src/main/java/org/elasticsearch/readiness/ReadinessService.java index 15b9eacfa2118..de56ead9b5aba 100644 --- a/server/src/main/java/org/elasticsearch/readiness/ReadinessService.java +++ b/server/src/main/java/org/elasticsearch/readiness/ReadinessService.java @@ -294,8 +294,8 @@ protected boolean areFileSettingsApplied(ClusterState clusterState) { } @SuppressForbidden(reason = "need to check file settings support on exact cluster state") - private static boolean supportsFileSettings(ClusterState clusterState) { - return clusterState.clusterFeatures().clusterHasFeature(FileSettingsFeatures.FILE_SETTINGS_SUPPORTED); + private boolean supportsFileSettings(ClusterState clusterState) { + return clusterState.clusterFeatures().clusterHasFeature(clusterState.nodes(), FileSettingsFeatures.FILE_SETTINGS_SUPPORTED); } private void setReady(boolean ready) { diff --git a/server/src/test/java/org/elasticsearch/action/datastreams/autosharding/DataStreamAutoShardingServiceTests.java b/server/src/test/java/org/elasticsearch/action/datastreams/autosharding/DataStreamAutoShardingServiceTests.java index 2c6e273bb6e23..ba0f04d174f43 100644 --- a/server/src/test/java/org/elasticsearch/action/datastreams/autosharding/DataStreamAutoShardingServiceTests.java +++ b/server/src/test/java/org/elasticsearch/action/datastreams/autosharding/DataStreamAutoShardingServiceTests.java @@ -19,6 +19,8 @@ import org.elasticsearch.cluster.metadata.IndexMetadataStats; import org.elasticsearch.cluster.metadata.IndexWriteLoad; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.node.DiscoveryNodeUtils; +import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; @@ -110,6 +112,7 @@ public void testCalculateValidations() { ); builder.put(dataStream); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("n1")).add(DiscoveryNodeUtils.create("n2"))) .nodeFeatures( Map.of( "n1", @@ -143,8 +146,9 @@ public Set getFeatures() { // cluster doesn't have feature ClusterState stateNoFeature = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder()).build(); + Settings settings = Settings.builder().put(DataStreamAutoShardingService.DATA_STREAMS_AUTO_SHARDING_ENABLED, true).build(); DataStreamAutoShardingService noFeatureService = new DataStreamAutoShardingService( - Settings.builder().put(DataStreamAutoShardingService.DATA_STREAMS_AUTO_SHARDING_ENABLED, true).build(), + settings, clusterService, new FeatureService(List.of()), () -> now @@ -155,15 +159,16 @@ public Set getFeatures() { } { + Settings settings = Settings.builder() + .put(DataStreamAutoShardingService.DATA_STREAMS_AUTO_SHARDING_ENABLED, true) + .putList( + DataStreamAutoShardingService.DATA_STREAMS_AUTO_SHARDING_EXCLUDES_SETTING.getKey(), + List.of("foo", dataStreamName + "*") + ) + .build(); // patterns are configured to exclude the current data stream DataStreamAutoShardingService noFeatureService = new DataStreamAutoShardingService( - Settings.builder() - .put(DataStreamAutoShardingService.DATA_STREAMS_AUTO_SHARDING_ENABLED, true) - .putList( - DataStreamAutoShardingService.DATA_STREAMS_AUTO_SHARDING_EXCLUDES_SETTING.getKey(), - List.of("foo", dataStreamName + "*") - ) - .build(), + settings, clusterService, new FeatureService(List.of()), () -> now @@ -199,6 +204,7 @@ public void testCalculateIncreaseShardingRecommendations() { DataStream dataStream = dataStreamSupplier.apply(null); builder.put(dataStream); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("n1")).add(DiscoveryNodeUtils.create("n2"))) .nodeFeatures( Map.of( "n1", @@ -237,6 +243,7 @@ public void testCalculateIncreaseShardingRecommendations() { ); builder.put(dataStream); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("n1")).add(DiscoveryNodeUtils.create("n2"))) .nodeFeatures( Map.of( "n1", @@ -275,6 +282,7 @@ public void testCalculateIncreaseShardingRecommendations() { ); builder.put(dataStream); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("n1")).add(DiscoveryNodeUtils.create("n2"))) .nodeFeatures( Map.of( "n1", @@ -313,6 +321,7 @@ public void testCalculateDecreaseShardingRecommendations() { DataStream dataStream = dataStreamSupplier.apply(null); builder.put(dataStream); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("n1")).add(DiscoveryNodeUtils.create("n2"))) .nodeFeatures( Map.of( "n1", @@ -353,6 +362,7 @@ public void testCalculateDecreaseShardingRecommendations() { DataStream dataStream = dataStreamSupplier.apply(null); builder.put(dataStream); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("n1")).add(DiscoveryNodeUtils.create("n2"))) .nodeFeatures( Map.of( "n1", @@ -401,6 +411,7 @@ public void testCalculateDecreaseShardingRecommendations() { ); builder.put(dataStream); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("n1")).add(DiscoveryNodeUtils.create("n2"))) .nodeFeatures( Map.of( "n1", @@ -447,6 +458,7 @@ public void testCalculateDecreaseShardingRecommendations() { ); builder.put(dataStream); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("n1")).add(DiscoveryNodeUtils.create("n2"))) .nodeFeatures( Map.of( "n1", @@ -487,6 +499,7 @@ public void testCalculateDecreaseShardingRecommendations() { DataStream dataStream = dataStreamSupplier.apply(null); builder.put(dataStream); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("n1")).add(DiscoveryNodeUtils.create("n2"))) .nodeFeatures( Map.of( "n1", diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/NodeJoinExecutorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/NodeJoinExecutorTests.java index 27775270a83eb..492a142492e18 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/NodeJoinExecutorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/NodeJoinExecutorTests.java @@ -33,6 +33,7 @@ import org.elasticsearch.common.ReferenceDocs; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.features.FeatureService; import org.elasticsearch.features.FeatureSpecification; import org.elasticsearch.features.NodeFeature; @@ -46,11 +47,13 @@ import org.elasticsearch.threadpool.ThreadPool; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static org.elasticsearch.cluster.metadata.DesiredNodesTestCase.assertDesiredNodesStatusIsCorrect; @@ -227,6 +230,227 @@ public Set getFeatures() { ); } + @SuppressForbidden(reason = "we need to actually check what is in cluster state") + private static Map> getRecordedNodeFeatures(ClusterState state) { + return state.clusterFeatures().nodeFeatures(); + } + + private static Version nextMajor() { + return Version.fromId((Version.CURRENT.major + 1) * 1_000_000 + 99); + } + + public void testCanJoinClusterWithAssumedFeatures() throws Exception { + AllocationService allocationService = createAllocationService(); + RerouteService rerouteService = (reason, priority, listener) -> listener.onResponse(null); + FeatureService featureService = new FeatureService(List.of(new FeatureSpecification() { + @Override + public Set getFeatures() { + return Set.of(new NodeFeature("f1"), new NodeFeature("af1", true), new NodeFeature("af2", true)); + } + })); + + NodeJoinExecutor executor = new NodeJoinExecutor(allocationService, rerouteService, featureService); + + DiscoveryNode masterNode = DiscoveryNodeUtils.create(UUIDs.base64UUID()); + DiscoveryNode otherNode = DiscoveryNodeUtils.create(UUIDs.base64UUID()); + Map> features = new HashMap<>(); + features.put(masterNode.getId(), Set.of("f1", "af1", "af2")); + features.put(otherNode.getId(), Set.of("f1", "af1", "af2")); + ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(masterNode).localNodeId(masterNode.getId()).masterNodeId(masterNode.getId()).add(otherNode)) + .nodeFeatures(features) + .build(); + + // it is valid for major+1 versions to join clusters assumed features still present + // this can happen in the process of marking, then removing, assumed features + // they should still be recorded appropriately + DiscoveryNode newNode = DiscoveryNodeUtils.builder(UUIDs.base64UUID()) + .version(nextMajor(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()) + .build(); + clusterState = ClusterStateTaskExecutorUtils.executeAndAssertSuccessful( + clusterState, + executor, + List.of( + JoinTask.singleNode( + newNode, + CompatibilityVersionsUtils.staticCurrent(), + Set.of("f1", "af2"), + TEST_REASON, + NO_FAILURE_LISTENER, + 0L + ) + ) + ); + features.put(newNode.getId(), Set.of("f1", "af2")); + + // extra final check that the recorded cluster features are as they should be + assertThat(getRecordedNodeFeatures(clusterState), equalTo(features)); + } + + public void testJoinClusterWithAssumedFeaturesDoesntAllowNonAssumed() throws Exception { + AllocationService allocationService = createAllocationService(); + RerouteService rerouteService = (reason, priority, listener) -> listener.onResponse(null); + FeatureService featureService = new FeatureService(List.of(new FeatureSpecification() { + @Override + public Set getFeatures() { + return Set.of(new NodeFeature("f1"), new NodeFeature("af1", true)); + } + })); + + NodeJoinExecutor executor = new NodeJoinExecutor(allocationService, rerouteService, featureService); + + DiscoveryNode masterNode = DiscoveryNodeUtils.create(UUIDs.base64UUID()); + DiscoveryNode otherNode = DiscoveryNodeUtils.create(UUIDs.base64UUID()); + Map> features = new HashMap<>(); + features.put(masterNode.getId(), Set.of("f1", "af1")); + features.put(otherNode.getId(), Set.of("f1", "af1")); + + ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(masterNode).localNodeId(masterNode.getId()).masterNodeId(masterNode.getId()).add(otherNode)) + .nodeFeatures(features) + .build(); + + DiscoveryNode newNodeNextMajor = DiscoveryNodeUtils.builder(UUIDs.base64UUID()) + .version(nextMajor(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()) + .build(); + clusterState = ClusterStateTaskExecutorUtils.executeAndAssertSuccessful( + clusterState, + executor, + List.of( + JoinTask.singleNode( + newNodeNextMajor, + CompatibilityVersionsUtils.staticCurrent(), + Set.of("f1"), + TEST_REASON, + NO_FAILURE_LISTENER, + 0L + ) + ) + ); + features.put(newNodeNextMajor.getId(), Set.of("f1")); + + // even though a next major has joined without af1, this doesnt allow the current major to join with af1 missing features + DiscoveryNode newNodeCurMajor = DiscoveryNodeUtils.create(UUIDs.base64UUID()); + AtomicReference ex = new AtomicReference<>(); + clusterState = ClusterStateTaskExecutorUtils.executeAndAssertSuccessful( + clusterState, + executor, + List.of( + JoinTask.singleNode( + newNodeCurMajor, + CompatibilityVersionsUtils.staticCurrent(), + Set.of("f1"), + TEST_REASON, + ActionTestUtils.assertNoSuccessListener(ex::set), + 0L + ) + ) + ); + assertThat(ex.get().getMessage(), containsString("missing required features [af1]")); + + // a next major can't join missing non-assumed features + DiscoveryNode newNodeNextMajorMissing = DiscoveryNodeUtils.builder(UUIDs.base64UUID()) + .version(nextMajor(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()) + .build(); + ex.set(null); + clusterState = ClusterStateTaskExecutorUtils.executeAndAssertSuccessful( + clusterState, + executor, + List.of( + JoinTask.singleNode( + newNodeNextMajorMissing, + CompatibilityVersionsUtils.staticCurrent(), + Set.of(), + TEST_REASON, + ActionTestUtils.assertNoSuccessListener(ex::set), + 0L + ) + ) + ); + assertThat(ex.get().getMessage(), containsString("missing required features [f1]")); + + // extra final check that the recorded cluster features are as they should be, and newNodeNextMajor hasn't gained af1 + assertThat(getRecordedNodeFeatures(clusterState), equalTo(features)); + } + + /* + * Same as above but the current major missing features is processed in the same execution + */ + public void testJoinClusterWithAssumedFeaturesDoesntAllowNonAssumedSameExecute() throws Exception { + AllocationService allocationService = createAllocationService(); + RerouteService rerouteService = (reason, priority, listener) -> listener.onResponse(null); + FeatureService featureService = new FeatureService(List.of(new FeatureSpecification() { + @Override + public Set getFeatures() { + return Set.of(new NodeFeature("f1"), new NodeFeature("af1", true)); + } + })); + + NodeJoinExecutor executor = new NodeJoinExecutor(allocationService, rerouteService, featureService); + + DiscoveryNode masterNode = DiscoveryNodeUtils.create(UUIDs.base64UUID()); + DiscoveryNode otherNode = DiscoveryNodeUtils.create(UUIDs.base64UUID()); + Map> features = new HashMap<>(); + features.put(masterNode.getId(), Set.of("f1", "af1")); + features.put(otherNode.getId(), Set.of("f1", "af1")); + + ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT) + .nodes(DiscoveryNodes.builder().add(masterNode).localNodeId(masterNode.getId()).masterNodeId(masterNode.getId()).add(otherNode)) + .nodeFeatures(features) + .build(); + + DiscoveryNode newNodeNextMajor = DiscoveryNodeUtils.builder(UUIDs.base64UUID()) + .version(nextMajor(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()) + .build(); + DiscoveryNode newNodeCurMajor = DiscoveryNodeUtils.create(UUIDs.base64UUID()); + DiscoveryNode newNodeNextMajorMissing = DiscoveryNodeUtils.builder(UUIDs.base64UUID()) + .version(nextMajor(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()) + .build(); + // even though a next major could join, this doesnt allow the current major to join with missing features + // nor a next major missing non-assumed features + AtomicReference thisMajorEx = new AtomicReference<>(); + AtomicReference nextMajorEx = new AtomicReference<>(); + List tasks = List.of( + JoinTask.singleNode( + newNodeNextMajor, + CompatibilityVersionsUtils.staticCurrent(), + Set.of("f1"), + TEST_REASON, + NO_FAILURE_LISTENER, + 0L + ), + JoinTask.singleNode( + newNodeCurMajor, + CompatibilityVersionsUtils.staticCurrent(), + Set.of("f1"), + TEST_REASON, + ActionTestUtils.assertNoSuccessListener(thisMajorEx::set), + 0L + ), + JoinTask.singleNode( + newNodeNextMajorMissing, + CompatibilityVersionsUtils.staticCurrent(), + Set.of(), + TEST_REASON, + ActionTestUtils.assertNoSuccessListener(nextMajorEx::set), + 0L + ) + ); + if (randomBoolean()) { + // sometimes combine them together into a single task for completeness + tasks = List.of(new JoinTask(tasks.stream().flatMap(t -> t.nodeJoinTasks().stream()).toList(), false, 0L, null)); + } + + clusterState = ClusterStateTaskExecutorUtils.executeAndAssertSuccessful(clusterState, executor, tasks); + features.put(newNodeNextMajor.getId(), Set.of("f1")); + + assertThat(thisMajorEx.get().getMessage(), containsString("missing required features [af1]")); + assertThat(nextMajorEx.get().getMessage(), containsString("missing required features [f1]")); + + // extra check that the recorded cluster features are as they should be, and newNodeNextMajor hasn't gained af1 + assertThat(getRecordedNodeFeatures(clusterState), equalTo(features)); + } + public void testSuccess() { Settings.builder().build(); Metadata.Builder metaBuilder = Metadata.builder(); @@ -921,8 +1145,8 @@ public void testSetsNodeFeaturesWhenRejoining() throws Exception { .nodeFeatures(Map.of(masterNode.getId(), Set.of("f1", "f2"), rejoinNode.getId(), Set.of())) .build(); - assertThat(clusterState.clusterFeatures().clusterHasFeature(new NodeFeature("f1")), is(false)); - assertThat(clusterState.clusterFeatures().clusterHasFeature(new NodeFeature("f2")), is(false)); + assertThat(clusterState.clusterFeatures().clusterHasFeature(clusterState.nodes(), new NodeFeature("f1")), is(false)); + assertThat(clusterState.clusterFeatures().clusterHasFeature(clusterState.nodes(), new NodeFeature("f2")), is(false)); final var resultingState = ClusterStateTaskExecutorUtils.executeAndAssertSuccessful( clusterState, @@ -939,8 +1163,8 @@ public void testSetsNodeFeaturesWhenRejoining() throws Exception { ) ); - assertThat(resultingState.clusterFeatures().clusterHasFeature(new NodeFeature("f1")), is(true)); - assertThat(resultingState.clusterFeatures().clusterHasFeature(new NodeFeature("f2")), is(true)); + assertThat(resultingState.clusterFeatures().clusterHasFeature(resultingState.nodes(), new NodeFeature("f1")), is(true)); + assertThat(resultingState.clusterFeatures().clusterHasFeature(resultingState.nodes(), new NodeFeature("f2")), is(true)); } private DesiredNodeWithStatus createActualizedDesiredNode() { diff --git a/server/src/test/java/org/elasticsearch/features/FeatureServiceTests.java b/server/src/test/java/org/elasticsearch/features/FeatureServiceTests.java index 874a6a96313e4..a64303f376b20 100644 --- a/server/src/test/java/org/elasticsearch/features/FeatureServiceTests.java +++ b/server/src/test/java/org/elasticsearch/features/FeatureServiceTests.java @@ -9,8 +9,14 @@ package org.elasticsearch.features; +import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.node.DiscoveryNodeUtils; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.node.VersionInformation; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.IndexVersions; import org.elasticsearch.test.ESTestCase; import java.util.List; @@ -69,6 +75,12 @@ public void testStateHasFeatures() { ); ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes( + DiscoveryNodes.builder() + .add(DiscoveryNodeUtils.create("node1")) + .add(DiscoveryNodeUtils.create("node2")) + .add(DiscoveryNodeUtils.create("node3")) + ) .nodeFeatures( Map.of("node1", Set.of("f1", "f2", "nf1"), "node2", Set.of("f1", "f2", "nf2"), "node3", Set.of("f1", "f2", "nf1")) ) @@ -81,4 +93,33 @@ public void testStateHasFeatures() { assertFalse(service.clusterHasFeature(state, new NodeFeature("nf2"))); assertFalse(service.clusterHasFeature(state, new NodeFeature("nf3"))); } + + private static Version nextMajor() { + return Version.fromId((Version.CURRENT.major + 1) * 1_000_000 + 99); + } + + public void testStateHasAssumedFeatures() { + List specs = List.of( + new TestFeatureSpecification(Set.of(new NodeFeature("f1"), new NodeFeature("f2"), new NodeFeature("af1", true))) + ); + + ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .nodes( + DiscoveryNodes.builder() + .add(DiscoveryNodeUtils.create("node1")) + .add(DiscoveryNodeUtils.create("node2")) + .add( + DiscoveryNodeUtils.builder("node3") + .version(new VersionInformation(nextMajor(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current())) + .build() + ) + ) + .nodeFeatures(Map.of("node1", Set.of("f1", "af1"), "node2", Set.of("f1", "f2", "af1"), "node3", Set.of("f1", "f2"))) + .build(); + + FeatureService service = new FeatureService(specs); + assertTrue(service.clusterHasFeature(state, new NodeFeature("f1"))); + assertFalse(service.clusterHasFeature(state, new NodeFeature("f2"))); + assertTrue(service.clusterHasFeature(state, new NodeFeature("af1", true))); + } } diff --git a/server/src/test/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutorTests.java b/server/src/test/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutorTests.java index 97f44f7480a72..92bfabf6f1972 100644 --- a/server/src/test/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutorTests.java +++ b/server/src/test/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutorTests.java @@ -77,8 +77,8 @@ public void setUp() throws Exception { clusterService = createClusterService(threadPool); localNodeId = clusterService.localNode().getId(); persistentTasksService = mock(PersistentTasksService.class); - featureService = new FeatureService(List.of(new HealthFeatures())); settings = Settings.builder().build(); + featureService = new FeatureService(List.of(new HealthFeatures())); clusterSettings = new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); } diff --git a/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleServiceTests.java b/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleServiceTests.java index 36887681f5575..9955fe4cf0f95 100644 --- a/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleServiceTests.java +++ b/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleServiceTests.java @@ -529,6 +529,7 @@ public void testValidateIntervalScheduleSupport() { var featureService = new FeatureService(List.of(new SnapshotLifecycleFeatures())); { ClusterState state = ClusterState.builder(new ClusterName("cluster")) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("a")).add(DiscoveryNodeUtils.create("b"))) .nodeFeatures(Map.of("a", Set.of(), "b", Set.of(SnapshotLifecycleService.INTERVAL_SCHEDULE.id()))) .build(); @@ -540,6 +541,7 @@ public void testValidateIntervalScheduleSupport() { } { ClusterState state = ClusterState.builder(new ClusterName("cluster")) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("a"))) .nodeFeatures(Map.of("a", Set.of(SnapshotLifecycleService.INTERVAL_SCHEDULE.id()))) .build(); try { @@ -550,6 +552,7 @@ public void testValidateIntervalScheduleSupport() { } { ClusterState state = ClusterState.builder(new ClusterName("cluster")) + .nodes(DiscoveryNodes.builder().add(DiscoveryNodeUtils.create("a")).add(DiscoveryNodeUtils.create("b"))) .nodeFeatures(Map.of("a", Set.of(), "b", Set.of(SnapshotLifecycleService.INTERVAL_SCHEDULE.id()))) .build(); try { From 312c21a3240339477c70fb512b5643b23952d572 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Wed, 18 Dec 2024 00:58:11 +1100 Subject: [PATCH 029/119] Mute org.elasticsearch.index.engine.RecoverySourcePruneMergePolicyTests testPruneSome #118728 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index fe6c77bdf9f93..cdf3007ee0027 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -304,6 +304,9 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118806 - class: org.elasticsearch.xpack.esql.session.IndexResolverFieldNamesTests issue: https://github.com/elastic/elasticsearch/issues/118814 +- class: org.elasticsearch.index.engine.RecoverySourcePruneMergePolicyTests + method: testPruneSome + issue: https://github.com/elastic/elasticsearch/issues/118728 # Examples: # From e0763c25ae9600611fe93b8d4133b5106ff280fd Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Tue, 17 Dec 2024 17:16:40 +0200 Subject: [PATCH 030/119] Mark the lookup join tests in IndexResolverFieldNamesTests as snapshot-only (#118815) --- muted-tests.yml | 2 -- .../esql/session/IndexResolverFieldNamesTests.java | 13 +++++++++++++ .../qa/server/src/main/resources/docs/docs.csv-spec | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index cdf3007ee0027..42845fda82180 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -302,8 +302,6 @@ tests: - class: org.elasticsearch.xpack.security.QueryableReservedRolesIT method: testDeletingAndCreatingSecurityIndexTriggersSynchronization issue: https://github.com/elastic/elasticsearch/issues/118806 -- class: org.elasticsearch.xpack.esql.session.IndexResolverFieldNamesTests - issue: https://github.com/elastic/elasticsearch/issues/118814 - class: org.elasticsearch.index.engine.RecoverySourcePruneMergePolicyTests method: testPruneSome issue: https://github.com/elastic/elasticsearch/issues/118728 diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java index e4271a0a6ddd5..31ec4663738f7 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.Build; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.elasticsearch.xpack.esql.parser.EsqlParser; import org.elasticsearch.xpack.esql.parser.ParsingException; @@ -1364,6 +1365,7 @@ public void testMetrics() { } public void testLookupJoin() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( "FROM employees | KEEP languages | RENAME languages AS language_code | LOOKUP JOIN languages_lookup ON language_code", Set.of("languages", "languages.*", "language_code", "language_code.*"), @@ -1372,6 +1374,7 @@ public void testLookupJoin() { } public void testLookupJoinKeep() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM employees @@ -1385,6 +1388,7 @@ public void testLookupJoinKeep() { } public void testLookupJoinKeepWildcard() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM employees @@ -1398,6 +1402,7 @@ public void testLookupJoinKeepWildcard() { } public void testMultiLookupJoin() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1410,6 +1415,7 @@ public void testMultiLookupJoin() { } public void testMultiLookupJoinKeepBefore() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1423,6 +1429,7 @@ public void testMultiLookupJoinKeepBefore() { } public void testMultiLookupJoinKeepBetween() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1447,6 +1454,7 @@ public void testMultiLookupJoinKeepBetween() { } public void testMultiLookupJoinKeepAfter() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1473,6 +1481,7 @@ public void testMultiLookupJoinKeepAfter() { } public void testMultiLookupJoinKeepAfterWildcard() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1486,6 +1495,7 @@ public void testMultiLookupJoinKeepAfterWildcard() { } public void testMultiLookupJoinSameIndex() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1499,6 +1509,7 @@ public void testMultiLookupJoinSameIndex() { } public void testMultiLookupJoinSameIndexKeepBefore() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1513,6 +1524,7 @@ public void testMultiLookupJoinSameIndexKeepBefore() { } public void testMultiLookupJoinSameIndexKeepBetween() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1538,6 +1550,7 @@ public void testMultiLookupJoinSameIndexKeepBetween() { } public void testMultiLookupJoinSameIndexKeepAfter() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); assertFieldNames( """ FROM sample_data diff --git a/x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec b/x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec index 60e81be43cc96..2fa82c05cc1aa 100644 --- a/x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec +++ b/x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec @@ -3353,7 +3353,7 @@ Alejandro Amabile Anoosh Basil -Bojan +Brendon // end::filterToday ; From 6d943e9e53576700b8417164d0e4c899c5f84e52 Mon Sep 17 00:00:00 2001 From: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:33:27 +0100 Subject: [PATCH 031/119] [ML] Ignore failures from renormalizing buckets in read-only index (#118674) In anomaly detection, score renormalization will update the anomaly score in the result indices. However, if the index in the old format was marked as read-only, the score update will fail. Since this failure is expected, this PR suppresses the error logging in this specific case. --- docs/changelog/118674.yaml | 5 ++++ .../JobRenormalizedResultsPersister.java | 26 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/118674.yaml diff --git a/docs/changelog/118674.yaml b/docs/changelog/118674.yaml new file mode 100644 index 0000000000000..eeb90a3b38f66 --- /dev/null +++ b/docs/changelog/118674.yaml @@ -0,0 +1,5 @@ +pr: 118674 +summary: Ignore failures from renormalizing buckets in read-only index +area: Machine Learning +type: enhancement +issues: [] diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobRenormalizedResultsPersister.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobRenormalizedResultsPersister.java index 3c0d2aca4deda..3c82841f1b99e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobRenormalizedResultsPersister.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobRenormalizedResultsPersister.java @@ -8,10 +8,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; @@ -102,7 +104,29 @@ public void executeRequest() { try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(ML_ORIGIN)) { BulkResponse addRecordsResponse = client.bulk(bulkRequest).actionGet(); if (addRecordsResponse.hasFailures()) { - logger.error("[{}] Bulk index of results has errors: {}", jobId, addRecordsResponse.buildFailureMessage()); + // Implementation note: Ignore the failures from writing to the read-only index, as it comes + // from changing the index format version. + boolean hasNonReadOnlyFailures = false; + for (BulkItemResponse response : addRecordsResponse.getItems()) { + if (response.isFailed() == false) { + continue; + } + if (response.getFailureMessage().contains(IndexMetadata.INDEX_READ_ONLY_BLOCK.description())) { + // We expect this to happen when the old index is made read-only and being reindexed + logger.debug( + "[{}] Ignoring failure to write renormalized results to a read-only index [{}]: {}", + jobId, + response.getFailure().getIndex(), + response.getFailureMessage() + ); + } else { + hasNonReadOnlyFailures = true; + break; + } + } + if (hasNonReadOnlyFailures) { + logger.error("[{}] Bulk index of results has errors: {}", jobId, addRecordsResponse.buildFailureMessage()); + } } } From f64c05ac32ee23930cf913c7939c9eeac12f00ce Mon Sep 17 00:00:00 2001 From: Adam Szaraniec Date: Tue, 17 Dec 2024 20:09:22 +0400 Subject: [PATCH 032/119] Update alias.asciidoc (#118553) Add section about removing index --- docs/reference/alias.asciidoc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/reference/alias.asciidoc b/docs/reference/alias.asciidoc index 9d784f530d63c..f676644c4ec48 100644 --- a/docs/reference/alias.asciidoc +++ b/docs/reference/alias.asciidoc @@ -407,3 +407,24 @@ POST _aliases } ---- // TEST[s/^/PUT my-index-2099.05.06-000001\n/] + +[discrete] +[[remove-index]] +=== Remove an index + +To remove an index, use the aliases API's `remove_index` action. + +[source,console] +---- +POST _aliases +{ + "actions": [ + { + "remove_index": { + "index": "my-index-2099.05.06-000001" + } + } + ] +} +---- +// TEST[s/^/PUT my-index-2099.05.06-000001\n/] From 1d2840ece1b8a5fcc1e6836d70bb1187160f2cd2 Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Tue, 17 Dec 2024 17:13:52 +0100 Subject: [PATCH 033/119] EQL: add support for partial search results (#116388) Allow queries to succeed if some shards are failing --- docs/changelog/116388.yaml | 5 + docs/reference/eql/eql-search-api.asciidoc | 47 ++ .../rest-api-spec/api/eql.search.json | 10 + .../org/elasticsearch/TransportVersions.java | 1 + .../test/eql/BaseEqlSpecTestCase.java | 84 +- .../elasticsearch/test/eql/DataLoader.java | 6 + .../test/eql/EqlDateNanosSpecTestCase.java | 36 +- .../test/eql/EqlExtraSpecTestCase.java | 36 +- .../eql/EqlMissingEventsSpecTestCase.java | 36 +- .../eql/EqlSampleMultipleEntriesTestCase.java | 36 +- .../test/eql/EqlSampleTestCase.java | 43 +- .../org/elasticsearch/test/eql/EqlSpec.java | 51 +- .../eql/EqlSpecFailingShardsTestCase.java | 83 ++ .../elasticsearch/test/eql/EqlSpecLoader.java | 7 + .../test/eql/EqlSpecTestCase.java | 43 +- .../data/endgame-shard-failures.data | 14 + .../data/endgame-shard-failures.mapping | 105 +++ .../main/resources/test_failing_shards.toml | 173 ++++ .../xpack/eql/qa/mixed_node/EqlSearchIT.java | 11 +- .../xpack/eql/EqlDateNanosIT.java | 25 +- .../elasticsearch/xpack/eql/EqlExtraIT.java | 25 +- .../elasticsearch/xpack/eql/EqlSampleIT.java | 25 +- .../xpack/eql/EqlSampleMultipleEntriesIT.java | 18 +- .../elasticsearch/xpack/eql/EqlSpecIT.java | 25 +- .../xpack/eql/EqlDateNanosIT.java | 24 +- .../elasticsearch/xpack/eql/EqlExtraIT.java | 24 +- .../xpack/eql/EqlMissingEventsIT.java | 24 +- .../elasticsearch/xpack/eql/EqlSampleIT.java | 24 +- .../xpack/eql/EqlSampleMultipleEntriesIT.java | 17 +- .../xpack/eql/EqlSpecFailingShardsIT.java | 53 ++ .../elasticsearch/xpack/eql/EqlSpecIT.java | 24 +- .../rest-api-spec/test/eql/10_basic.yml | 143 ++++ .../xpack/eql/action/CCSPartialResultsIT.java | 613 ++++++++++++++ .../eql/action/PartialSearchResultsIT.java | 780 ++++++++++++++++++ .../xpack/eql/action/EqlSearchRequest.java | 47 +- .../xpack/eql/action/EqlSearchResponse.java | 42 +- .../xpack/eql/action/EqlSearchTask.java | 4 +- .../execution/assembler/ExecutionManager.java | 7 +- .../execution/payload/AbstractPayload.java | 10 +- .../eql/execution/payload/EventPayload.java | 2 +- .../eql/execution/sample/SampleIterator.java | 29 +- .../eql/execution/sample/SamplePayload.java | 11 +- .../execution/search/BasicQueryClient.java | 18 +- .../execution/search/PITAwareQueryClient.java | 25 +- .../eql/execution/search/RuntimeUtils.java | 31 +- .../execution/sequence/SequencePayload.java | 11 +- .../execution/sequence/TumblingWindow.java | 43 +- .../xpack/eql/plugin/EqlPlugin.java | 16 +- .../xpack/eql/plugin/RestEqlSearchAction.java | 6 + .../eql/plugin/TransportEqlSearchAction.java | 36 +- .../xpack/eql/session/EmptyPayload.java | 13 +- .../xpack/eql/session/EqlConfiguration.java | 14 + .../xpack/eql/session/Payload.java | 3 + .../xpack/eql/session/Results.java | 19 +- .../xpack/eql/util/SearchHitUtils.java | 12 + .../elasticsearch/xpack/eql/EqlTestUtils.java | 4 + .../eql/action/EqlSearchRequestTests.java | 8 + .../eql/action/EqlSearchResponseTests.java | 14 +- .../eql/action/LocalStateEQLXPackPlugin.java | 21 +- .../assembler/ImplicitTiebreakerTests.java | 10 +- .../assembler/SequenceSpecTests.java | 10 +- .../execution/sample/CircuitBreakerTests.java | 5 +- .../search/PITAwareQueryClientTests.java | 12 +- .../sequence/CircuitBreakerTests.java | 32 +- .../execution/sequence/PITFailureTests.java | 12 +- 65 files changed, 3068 insertions(+), 130 deletions(-) create mode 100644 docs/changelog/116388.yaml create mode 100644 x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecFailingShardsTestCase.java create mode 100644 x-pack/plugin/eql/qa/common/src/main/resources/data/endgame-shard-failures.data create mode 100644 x-pack/plugin/eql/qa/common/src/main/resources/data/endgame-shard-failures.mapping create mode 100644 x-pack/plugin/eql/qa/common/src/main/resources/test_failing_shards.toml create mode 100644 x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecFailingShardsIT.java create mode 100644 x-pack/plugin/eql/src/internalClusterTest/java/org/elasticsearch/xpack/eql/action/CCSPartialResultsIT.java create mode 100644 x-pack/plugin/eql/src/internalClusterTest/java/org/elasticsearch/xpack/eql/action/PartialSearchResultsIT.java diff --git a/docs/changelog/116388.yaml b/docs/changelog/116388.yaml new file mode 100644 index 0000000000000..59cdafc9ec337 --- /dev/null +++ b/docs/changelog/116388.yaml @@ -0,0 +1,5 @@ +pr: 116388 +summary: Add support for partial shard results +area: EQL +type: enhancement +issues: [] diff --git a/docs/reference/eql/eql-search-api.asciidoc b/docs/reference/eql/eql-search-api.asciidoc index d7f10f4627f6c..0fd490609277f 100644 --- a/docs/reference/eql/eql-search-api.asciidoc +++ b/docs/reference/eql/eql-search-api.asciidoc @@ -88,6 +88,53 @@ request that targets only `bar*` still returns an error. + Defaults to `true`. +`allow_partial_search_results`:: +(Optional, Boolean) + +If `false`, the request returns an error if one or more shards involved in the query are unavailable. ++ +If `true`, the query is executed only on the available shards, ignoring shard request timeouts and +<>. ++ +Defaults to `false`. ++ +To override the default for this field, set the +`xpack.eql.default_allow_partial_results` cluster setting to `true`. + + +[IMPORTANT] +==== +You can also specify this value using the `allow_partial_search_results` request body parameter. +If both parameters are specified, only the query parameter is used. +==== + + +`allow_partial_sequence_results`:: +(Optional, Boolean) + + +Used together with `allow_partial_search_results=true`, controls the behavior of sequence queries specifically +(if `allow_partial_search_results=false`, this setting has no effect). +If `true` and if some shards are unavailable, the sequences are calculated on available shards only. ++ +If `false` and if some shards are unavailable, the query only returns information about the shard failures, +but no further results. ++ +Defaults to `false`. ++ +Consider that sequences calculated with `allow_partial_search_results=true` can return incorrect results +(eg. if a <> clause matches records in unavailable shards) ++ +To override the default for this field, set the +`xpack.eql.default_allow_partial_sequence_results` cluster setting to `true`. + + +[IMPORTANT] +==== +You can also specify this value using the `allow_partial_sequence_results` request body parameter. +If both parameters are specified, only the query parameter is used. +==== + `ccs_minimize_roundtrips`:: (Optional, Boolean) If `true`, network round-trips between the local and the remote cluster are minimized when running cross-cluster search (CCS) requests. diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/eql.search.json b/rest-api-spec/src/main/resources/rest-api-spec/api/eql.search.json index c854c44d9d761..0f9af508f4c16 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/eql.search.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/eql.search.json @@ -41,6 +41,16 @@ "type": "time", "description": "Update the time interval in which the results (partial or final) for this search will be available", "default": "5d" + }, + "allow_partial_search_results": { + "type":"boolean", + "description":"Control whether the query should keep running in case of shard failures, and return partial results", + "default":false + }, + "allow_partial_sequence_results": { + "type":"boolean", + "description":"Control whether a sequence query should return partial results or no results at all in case of shard failures. This option has effect only if [allow_partial_search_results] is true.", + "default":false } }, "body":{ diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index f5e581a81a37c..371af961720cc 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -138,6 +138,7 @@ static TransportVersion def(int id) { public static final TransportVersion KNN_QUERY_RESCORE_OVERSAMPLE = def(8_806_00_0); public static final TransportVersion SEMANTIC_QUERY_LENIENT = def(8_807_00_0); public static final TransportVersion ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS = def(8_808_00_0); + public static final TransportVersion EQL_ALLOW_PARTIAL_SEARCH_RESULTS = def(8_809_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/BaseEqlSpecTestCase.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/BaseEqlSpecTestCase.java index 90244d9b2c019..3557114e2f4c7 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/BaseEqlSpecTestCase.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/BaseEqlSpecTestCase.java @@ -33,6 +33,9 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; + public abstract class BaseEqlSpecTestCase extends RemoteClusterAwareEqlRestTestCase { protected static final String PARAM_FORMATTING = "%2$s"; @@ -52,6 +55,9 @@ public abstract class BaseEqlSpecTestCase extends RemoteClusterAwareEqlRestTestC */ private final int size; private final int maxSamplesPerKey; + private final Boolean allowPartialSearchResults; + private final Boolean allowPartialSequenceResults; + private final Boolean expectShardFailures; @Before public void setup() throws Exception { @@ -104,7 +110,16 @@ protected static List asArray(List specs) { } results.add( - new Object[] { spec.query(), name, spec.expectedEventIds(), spec.joinKeys(), spec.size(), spec.maxSamplesPerKey() } + new Object[] { + spec.query(), + name, + spec.expectedEventIds(), + spec.joinKeys(), + spec.size(), + spec.maxSamplesPerKey(), + spec.allowPartialSearchResults(), + spec.allowPartialSequenceResults(), + spec.expectShardFailures() } ); } @@ -118,7 +133,10 @@ protected static List asArray(List specs) { List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { this.index = index; @@ -128,6 +146,9 @@ protected static List asArray(List specs) { this.joinKeys = joinKeys; this.size = size == null ? -1 : size; this.maxSamplesPerKey = maxSamplesPerKey == null ? -1 : maxSamplesPerKey; + this.allowPartialSearchResults = allowPartialSearchResults; + this.allowPartialSequenceResults = allowPartialSequenceResults; + this.expectShardFailures = expectShardFailures; } public void test() throws Exception { @@ -137,6 +158,7 @@ public void test() throws Exception { private void assertResponse(ObjectPath response) throws Exception { List> events = response.evaluate("hits.events"); List> sequences = response.evaluate("hits.sequences"); + Object shardFailures = response.evaluate("shard_failures"); if (events != null) { assertEvents(events); @@ -145,6 +167,7 @@ private void assertResponse(ObjectPath response) throws Exception { } else { fail("No events or sequences found"); } + assertShardFailures(shardFailures); } protected ObjectPath runQuery(String index, String query) throws Exception { @@ -163,6 +186,32 @@ protected ObjectPath runQuery(String index, String query) throws Exception { if (maxSamplesPerKey > 0) { builder.field("max_samples_per_key", maxSamplesPerKey); } + boolean allowPartialResultsInBody = randomBoolean(); + if (allowPartialSearchResults != null) { + if (allowPartialResultsInBody) { + builder.field("allow_partial_search_results", String.valueOf(allowPartialSearchResults)); + if (allowPartialSequenceResults != null) { + builder.field("allow_partial_sequence_results", String.valueOf(allowPartialSequenceResults)); + } + } else { + // these will be overwritten by the path params, that have higher priority than the query (JSON body) params + if (allowPartialSearchResults != null) { + builder.field("allow_partial_search_results", randomBoolean()); + } + if (allowPartialSequenceResults != null) { + builder.field("allow_partial_sequence_results", randomBoolean()); + } + } + } else { + // Tests that don't specify a setting for these parameters should always pass. + // These params should be irrelevant. + if (randomBoolean()) { + builder.field("allow_partial_search_results", randomBoolean()); + } + if (randomBoolean()) { + builder.field("allow_partial_sequence_results", randomBoolean()); + } + } builder.endObject(); Request request = new Request("POST", "/" + index + "/_eql/search"); @@ -170,6 +219,23 @@ protected ObjectPath runQuery(String index, String query) throws Exception { if (ccsMinimizeRoundtrips != null) { request.addParameter("ccs_minimize_roundtrips", ccsMinimizeRoundtrips.toString()); } + if (allowPartialSearchResults != null) { + if (allowPartialResultsInBody == false) { + request.addParameter("allow_partial_search_results", String.valueOf(allowPartialSearchResults)); + if (allowPartialSequenceResults != null) { + request.addParameter("allow_partial_sequence_results", String.valueOf(allowPartialSequenceResults)); + } + } + } else { + // Tests that don't specify a setting for these parameters should always pass. + // These params should be irrelevant. + if (randomBoolean()) { + request.addParameter("allow_partial_search_results", String.valueOf(randomBoolean())); + } + if (randomBoolean()) { + request.addParameter("allow_partial_sequence_results", String.valueOf(randomBoolean())); + } + } int timeout = Math.toIntExact(timeout().millis()); RequestConfig config = RequestConfig.copy(RequestConfig.DEFAULT) .setConnectionRequestTimeout(timeout) @@ -182,6 +248,20 @@ protected ObjectPath runQuery(String index, String query) throws Exception { return ObjectPath.createFromResponse(client().performRequest(request)); } + private void assertShardFailures(Object shardFailures) { + if (expectShardFailures != null) { + if (expectShardFailures) { + assertNotNull(shardFailures); + List list = (List) shardFailures; + assertThat(list.size(), is(greaterThan(0))); + } else { + assertNull(shardFailures); + } + } else { + assertNull(shardFailures); + } + } + private void assertEvents(List> events) { assertNotNull(events); logger.debug("Events {}", new Object() { diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/DataLoader.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/DataLoader.java index 1d51af574c810..4618bd8f4ff3d 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/DataLoader.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/DataLoader.java @@ -52,6 +52,7 @@ */ public class DataLoader { public static final String TEST_INDEX = "endgame-140"; + public static final String TEST_SHARD_FAILURES_INDEX = "endgame-shard-failures"; public static final String TEST_EXTRA_INDEX = "extra"; public static final String TEST_NANOS_INDEX = "endgame-140-nanos"; public static final String TEST_SAMPLE = "sample1,sample2,sample3"; @@ -103,6 +104,11 @@ public static void loadDatasetIntoEs(RestClient client, CheckedBiFunction eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - this(TEST_NANOS_INDEX, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + this( + TEST_NANOS_INDEX, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } // constructor for multi-cluster tests @@ -40,9 +54,23 @@ public EqlDateNanosSpecTestCase( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - super(index, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + super( + index, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } @Override diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlExtraSpecTestCase.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlExtraSpecTestCase.java index 292fe6c895cee..cc858ded25f37 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlExtraSpecTestCase.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlExtraSpecTestCase.java @@ -27,9 +27,23 @@ public EqlExtraSpecTestCase( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - this(TEST_EXTRA_INDEX, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + this( + TEST_EXTRA_INDEX, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } // constructor for multi-cluster tests @@ -40,9 +54,23 @@ public EqlExtraSpecTestCase( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - super(index, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + super( + index, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } @Override diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlMissingEventsSpecTestCase.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlMissingEventsSpecTestCase.java index cdda9e9e068f5..f62c2b29101db 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlMissingEventsSpecTestCase.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlMissingEventsSpecTestCase.java @@ -27,9 +27,23 @@ public EqlMissingEventsSpecTestCase( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - this(TEST_MISSING_EVENTS_INDEX, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + this( + TEST_MISSING_EVENTS_INDEX, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } // constructor for multi-cluster tests @@ -40,9 +54,23 @@ public EqlMissingEventsSpecTestCase( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - super(index, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + super( + index, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } @Override diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSampleMultipleEntriesTestCase.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSampleMultipleEntriesTestCase.java index 6471e264a92fa..a38ccacb42f5f 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSampleMultipleEntriesTestCase.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSampleMultipleEntriesTestCase.java @@ -21,9 +21,23 @@ public EqlSampleMultipleEntriesTestCase( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - this(TEST_SAMPLE_MULTI, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + this( + TEST_SAMPLE_MULTI, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } public EqlSampleMultipleEntriesTestCase( @@ -33,9 +47,23 @@ public EqlSampleMultipleEntriesTestCase( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - super(index, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + super( + index, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } @ParametersFactory(shuffle = false, argumentFormatting = PARAM_FORMATTING) diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSampleTestCase.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSampleTestCase.java index dfae73b3602a7..4748bd0e3307b 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSampleTestCase.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSampleTestCase.java @@ -15,8 +15,29 @@ public abstract class EqlSampleTestCase extends BaseEqlSpecTestCase { - public EqlSampleTestCase(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - this(TEST_SAMPLE, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlSampleTestCase( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + this( + TEST_SAMPLE, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } public EqlSampleTestCase( @@ -26,9 +47,23 @@ public EqlSampleTestCase( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - super(index, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + super( + index, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } @ParametersFactory(shuffle = false, argumentFormatting = PARAM_FORMATTING) diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpec.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpec.java index db7ee05ff2239..4dd617bac0abd 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpec.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpec.java @@ -30,6 +30,9 @@ public class EqlSpec { private Integer size; private Integer maxSamplesPerKey; + private Boolean allowPartialSearchResults; + private Boolean allowPartialSequenceResults; + private Boolean expectShardFailures; public String name() { return name; @@ -103,6 +106,30 @@ public void maxSamplesPerKey(Integer maxSamplesPerKey) { this.maxSamplesPerKey = maxSamplesPerKey; } + public Boolean allowPartialSearchResults() { + return allowPartialSearchResults; + } + + public void allowPartialSearchResults(Boolean allowPartialSearchResults) { + this.allowPartialSearchResults = allowPartialSearchResults; + } + + public Boolean allowPartialSequenceResults() { + return allowPartialSequenceResults; + } + + public void allowPartialSequenceResults(Boolean allowPartialSequenceResults) { + this.allowPartialSequenceResults = allowPartialSequenceResults; + } + + public Boolean expectShardFailures() { + return expectShardFailures; + } + + public void expectShardFailures(Boolean expectShardFailures) { + this.expectShardFailures = expectShardFailures; + } + @Override public String toString() { String str = ""; @@ -132,7 +159,15 @@ public String toString() { if (maxSamplesPerKey != null) { str = appendWithComma(str, "max_samples_per_key", "" + maxSamplesPerKey); } - + if (allowPartialSearchResults != null) { + str = appendWithComma(str, "allow_partial_search_results", String.valueOf(allowPartialSearchResults)); + } + if (allowPartialSequenceResults != null) { + str = appendWithComma(str, "allow_partial_sequence_results", String.valueOf(allowPartialSequenceResults)); + } + if (expectShardFailures != null) { + str = appendWithComma(str, "expect_shard_failures", String.valueOf(expectShardFailures)); + } return str; } @@ -150,12 +185,22 @@ public boolean equals(Object other) { return Objects.equals(this.query(), that.query()) && Objects.equals(size, that.size) - && Objects.equals(maxSamplesPerKey, that.maxSamplesPerKey); + && Objects.equals(maxSamplesPerKey, that.maxSamplesPerKey) + && Objects.equals(allowPartialSearchResults, that.allowPartialSearchResults) + && Objects.equals(allowPartialSequenceResults, that.allowPartialSequenceResults) + && Objects.equals(expectShardFailures, that.expectShardFailures); } @Override public int hashCode() { - return Objects.hash(this.query, size, maxSamplesPerKey); + return Objects.hash( + this.query, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } private static String appendWithComma(String str, String name, String append) { diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecFailingShardsTestCase.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecFailingShardsTestCase.java new file mode 100644 index 0000000000000..c490a2f703dcc --- /dev/null +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecFailingShardsTestCase.java @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.test.eql; + +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import java.util.List; + +import static org.elasticsearch.test.eql.DataLoader.TEST_INDEX; +import static org.elasticsearch.test.eql.DataLoader.TEST_SHARD_FAILURES_INDEX; + +public abstract class EqlSpecFailingShardsTestCase extends BaseEqlSpecTestCase { + + @ParametersFactory(shuffle = false, argumentFormatting = PARAM_FORMATTING) + public static List readTestSpecs() throws Exception { + + // Load EQL validation specs + return asArray(EqlSpecLoader.load("/test_failing_shards.toml")); + } + + @Override + protected String tiebreaker() { + return "serial_event_id"; + } + + // constructor for "local" rest tests + public EqlSpecFailingShardsTestCase( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + this( + TEST_INDEX + "," + TEST_SHARD_FAILURES_INDEX, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); + } + + // constructor for multi-cluster tests + public EqlSpecFailingShardsTestCase( + String index, + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearch, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + index, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearch, + allowPartialSequenceResults, + expectShardFailures + ); + } +} diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecLoader.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecLoader.java index a1f555563e29c..f86107cf3bac5 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecLoader.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecLoader.java @@ -76,6 +76,10 @@ private static Integer getInteger(TomlTable table, String key) { return null; } + private static Boolean getBoolean(TomlTable table, String key) { + return table.getBoolean(key); + } + private static List readFromStream(InputStream is, Set uniqueTestNames) throws Exception { List testSpecs = new ArrayList<>(); @@ -90,6 +94,9 @@ private static List readFromStream(InputStream is, Set uniqueTe spec.note(getTrimmedString(table, "note")); spec.description(getTrimmedString(table, "description")); spec.size(getInteger(table, "size")); + spec.allowPartialSearchResults(getBoolean(table, "allow_partial_search_results")); + spec.allowPartialSequenceResults(getBoolean(table, "allow_partial_sequence_results")); + spec.expectShardFailures(getBoolean(table, "expect_shard_failures")); List arr = table.getList("tags"); if (arr != null) { diff --git a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecTestCase.java b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecTestCase.java index 7113924f79029..62a3ea72fe51f 100644 --- a/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecTestCase.java +++ b/x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/EqlSpecTestCase.java @@ -28,8 +28,29 @@ protected String tiebreaker() { } // constructor for "local" rest tests - public EqlSpecTestCase(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - this(TEST_INDEX, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlSpecTestCase( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearch, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + this( + TEST_INDEX, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearch, + allowPartialSequenceResults, + expectShardFailures + ); } // constructor for multi-cluster tests @@ -40,8 +61,22 @@ public EqlSpecTestCase( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearch, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - super(index, query, name, eventIds, joinKeys, size, maxSamplesPerKey); + super( + index, + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearch, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/common/src/main/resources/data/endgame-shard-failures.data b/x-pack/plugin/eql/qa/common/src/main/resources/data/endgame-shard-failures.data new file mode 100644 index 0000000000000..18a1d05656d09 --- /dev/null +++ b/x-pack/plugin/eql/qa/common/src/main/resources/data/endgame-shard-failures.data @@ -0,0 +1,14 @@ +[ + { + "event_subtype_full": "already_running", + "event_type": "process", + "event_type_full": "process_event", + "opcode": 3, + "pid": 0, + "process_name": "System Idle Process", + "serial_event_id": 10000, + "subtype": "create", + "timestamp": 117444736000000000, + "unique_pid": 1 + } +] diff --git a/x-pack/plugin/eql/qa/common/src/main/resources/data/endgame-shard-failures.mapping b/x-pack/plugin/eql/qa/common/src/main/resources/data/endgame-shard-failures.mapping new file mode 100644 index 0000000000000..3b5039f4098af --- /dev/null +++ b/x-pack/plugin/eql/qa/common/src/main/resources/data/endgame-shard-failures.mapping @@ -0,0 +1,105 @@ +# Text patterns like "[runtime_random_keyword_type]" will get replaced at runtime with a random string type. +# See DataLoader class for pattern replacements. +{ + "runtime":{ + "broken":{ + "type": "long", + "script": { + "lang": "painless", + "source": "emit(doc['non_existing'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))" + } + } + }, + "properties" : { + "command_line" : { + "type" : "[runtime_random_keyword_type]" + }, + "event_type" : { + "type" : "[runtime_random_keyword_type]" + }, + "event" : { + "properties" : { + "category" : { + "type" : "alias", + "path" : "event_type" + }, + "sequence" : { + "type" : "alias", + "path" : "serial_event_id" + } + } + }, + "md5" : { + "type" : "[runtime_random_keyword_type]" + }, + "parent_process_name": { + "type" : "[runtime_random_keyword_type]" + }, + "parent_process_path": { + "type" : "[runtime_random_keyword_type]" + }, + "pid" : { + "type" : "long" + }, + "ppid" : { + "type" : "long" + }, + "process_name": { + "type" : "[runtime_random_keyword_type]" + }, + "process_path": { + "type" : "[runtime_random_keyword_type]" + }, + "subtype" : { + "type" : "[runtime_random_keyword_type]" + }, + "timestamp" : { + "type" : "date" + }, + "@timestamp" : { + "type" : "date" + }, + "user" : { + "type" : "[runtime_random_keyword_type]" + }, + "user_name" : { + "type" : "[runtime_random_keyword_type]" + }, + "user_domain": { + "type" : "[runtime_random_keyword_type]" + }, + "hostname" : { + "type" : "text", + "fields" : { + "[runtime_random_keyword_type]" : { + "type" : "[runtime_random_keyword_type]", + "ignore_above" : 256 + } + } + }, + "opcode" : { + "type" : "long" + }, + "file_name" : { + "type" : "text", + "fields" : { + "[runtime_random_keyword_type]" : { + "type" : "[runtime_random_keyword_type]", + "ignore_above" : 256 + } + } + }, + "file_path" : { + "type" : "[runtime_random_keyword_type]" + }, + "serial_event_id" : { + "type" : "long" + }, + "source_address" : { + "type" : "ip" + }, + "exit_code" : { + "type" : "long" + } + } +} diff --git a/x-pack/plugin/eql/qa/common/src/main/resources/test_failing_shards.toml b/x-pack/plugin/eql/qa/common/src/main/resources/test_failing_shards.toml new file mode 100644 index 0000000000000..a551c66fd48bd --- /dev/null +++ b/x-pack/plugin/eql/qa/common/src/main/resources/test_failing_shards.toml @@ -0,0 +1,173 @@ +# this query doesn't touch the "broken" field, so it should not fail +[[queries]] +name = "eventQueryNoShardFailures" +query = 'process where serial_event_id == 1' +allow_partial_search_results = true +expected_event_ids = [1] +expect_shard_failures = false + + +[[queries]] +name = "eventQueryShardFailures" +query = 'process where serial_event_id == 1 or broken == 1' +allow_partial_search_results = true +expected_event_ids = [1] +expect_shard_failures = true + + +[[queries]] +name = "eventQueryShardFailuresOptionalField" +query = 'process where serial_event_id == 1 and ?optional_field_default_null == null or broken == 1' +allow_partial_search_results = true +expected_event_ids = [1] +expect_shard_failures = true + + +[[queries]] +name = "eventQueryShardFailuresOptionalFieldMatching" +query = 'process where serial_event_id == 2 and ?subtype == "create" or broken == 1' +allow_partial_search_results = true +expected_event_ids = [2] +expect_shard_failures = true + + +# this query doesn't touch the "broken" field, so it should not fail +[[queries]] +name = "sequenceQueryNoShardFailures" +query = ''' +sequence + [process where serial_event_id == 1] + [process where serial_event_id == 2] +''' +expected_event_ids = [1, 2] +expect_shard_failures = false + + +# this query doesn't touch the "broken" field, so it should not fail +[[queries]] +name = "sequenceQueryNoShardFailuresAllowFalse" +query = ''' +sequence + [process where serial_event_id == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = false +expected_event_ids = [1, 2] +expect_shard_failures = false + + +# this query doesn't touch the "broken" field, so it should not fail +[[queries]] +name = "sequenceQueryNoShardFailuresAllowTrue" +query = ''' +sequence + [process where serial_event_id == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = true +expected_event_ids = [1, 2] +expect_shard_failures = false + + +[[queries]] +name = "sequenceQueryMissingShards" +query = ''' +sequence + [process where serial_event_id == 1 or broken == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = true +expected_event_ids = [] +expect_shard_failures = true + + +[[queries]] +name = "sequenceQueryMissingShardsPartialResults" +query = ''' +sequence + [process where serial_event_id == 1 or broken == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = true +allow_partial_sequence_results = true +expected_event_ids = [1, 2] +expect_shard_failures = true + + +[[queries]] +name = "sequenceQueryMissingShardsPartialResultsOptional" +query = ''' +sequence + [process where ?serial_event_id == 1 or broken == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = true +allow_partial_sequence_results = true +expected_event_ids = [1, 2] +expect_shard_failures = true + + +[[queries]] +name = "sequenceQueryMissingShardsPartialResultsOptional2" +query = ''' +sequence with maxspan=100000d + [process where serial_event_id == 1 and ?subtype == "create" or broken == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = true +allow_partial_sequence_results = true +expected_event_ids = [1, 2] +expect_shard_failures = true + + +[[queries]] +name = "sequenceQueryMissingShardsPartialResultsOptionalMissing" +query = ''' +sequence with maxspan=100000d + [process where serial_event_id == 1 and ?subtype == "create"] + ![process where broken == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = true +allow_partial_sequence_results = true +expected_event_ids = [1, -1, 2] +expect_shard_failures = true + + +[[queries]] +name = "sequenceQueryMissingShardsPartialResultsOptionalMissing2" +query = ''' +sequence with maxspan=100000d + [process where serial_event_id == 1 and ?subtype == "create" or broken == 1] + ![process where broken == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = true +allow_partial_sequence_results = true +expected_event_ids = [1, -1, 2] +expect_shard_failures = true + + +[[queries]] +name = "sampleQueryMissingShardsPartialResults" +query = ''' +sample by event_subtype_full + [process where serial_event_id == 1 or broken == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = true +expected_event_ids = [1, 2] +expect_shard_failures = true + + +[[queries]] +name = "sampleQueryMissingShardsPartialResultsOptional" +query = ''' +sample by event_subtype_full + [process where serial_event_id == 1 and ?subtype == "create" or broken == 1] + [process where serial_event_id == 2] +''' +allow_partial_search_results = true +expected_event_ids = [1, 2] +expect_shard_failures = true + diff --git a/x-pack/plugin/eql/qa/mixed-node/src/javaRestTest/java/org/elasticsearch/xpack/eql/qa/mixed_node/EqlSearchIT.java b/x-pack/plugin/eql/qa/mixed-node/src/javaRestTest/java/org/elasticsearch/xpack/eql/qa/mixed_node/EqlSearchIT.java index 2a29572374fa8..60c7fb1c7ad25 100644 --- a/x-pack/plugin/eql/qa/mixed-node/src/javaRestTest/java/org/elasticsearch/xpack/eql/qa/mixed_node/EqlSearchIT.java +++ b/x-pack/plugin/eql/qa/mixed-node/src/javaRestTest/java/org/elasticsearch/xpack/eql/qa/mixed_node/EqlSearchIT.java @@ -407,7 +407,16 @@ private void assertMultiValueFunctionQuery( for (int id : ids) { eventIds.add(String.valueOf(id)); } - request.setJsonEntity("{\"query\":\"" + query + "\"}"); + + StringBuilder payload = new StringBuilder("{\"query\":\"" + query + "\""); + if (randomBoolean()) { + payload.append(", \"allow_partial_search_results\": true"); + } + if (randomBoolean()) { + payload.append(", \"allow_partial_sequence_results\": true"); + } + payload.append("}"); + request.setJsonEntity(payload.toString()); assertResponse(query, eventIds, runEql(client, request)); testedFunctions.add(functionName); } diff --git a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlDateNanosIT.java b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlDateNanosIT.java index c20968871472f..5d6824232d80f 100644 --- a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlDateNanosIT.java +++ b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlDateNanosIT.java @@ -37,7 +37,28 @@ protected String getRemoteCluster() { return REMOTE_CLUSTER.getHttpAddresses(); } - public EqlDateNanosIT(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - super(remoteClusterIndex(TEST_NANOS_INDEX), query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlDateNanosIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + remoteClusterIndex(TEST_NANOS_INDEX), + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlExtraIT.java b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlExtraIT.java index 774c19d02adf0..79b095434814b 100644 --- a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlExtraIT.java +++ b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlExtraIT.java @@ -37,7 +37,28 @@ protected String getRemoteCluster() { return REMOTE_CLUSTER.getHttpAddresses(); } - public EqlExtraIT(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - super(remoteClusterIndex(TEST_EXTRA_INDEX), query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlExtraIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + remoteClusterIndex(TEST_EXTRA_INDEX), + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleIT.java b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleIT.java index 1502c250bd058..7673eec32ec55 100644 --- a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleIT.java +++ b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleIT.java @@ -37,7 +37,28 @@ protected String getRemoteCluster() { return REMOTE_CLUSTER.getHttpAddresses(); } - public EqlSampleIT(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - super(remoteClusterPattern(TEST_SAMPLE), query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlSampleIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + remoteClusterPattern(TEST_SAMPLE), + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleMultipleEntriesIT.java b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleMultipleEntriesIT.java index 795fe4e103a31..ac6f7fe508c99 100644 --- a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleMultipleEntriesIT.java +++ b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleMultipleEntriesIT.java @@ -43,8 +43,22 @@ public EqlSampleMultipleEntriesIT( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - super(remoteClusterPattern(TEST_SAMPLE_MULTI), query, name, eventIds, joinKeys, size, maxSamplesPerKey); + super( + remoteClusterPattern(TEST_SAMPLE_MULTI), + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecIT.java b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecIT.java index 2cddecb644a1a..db0c03e8fdb6f 100644 --- a/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecIT.java +++ b/x-pack/plugin/eql/qa/multi-cluster-with-security/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecIT.java @@ -37,7 +37,28 @@ protected String getRemoteCluster() { return REMOTE_CLUSTER.getHttpAddresses(); } - public EqlSpecIT(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - super(remoteClusterIndex(TEST_INDEX), query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlSpecIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + remoteClusterIndex(TEST_INDEX), + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlDateNanosIT.java b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlDateNanosIT.java index 1df10fde7fde5..5e1fa224de58d 100644 --- a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlDateNanosIT.java +++ b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlDateNanosIT.java @@ -27,7 +27,27 @@ protected String getTestRestCluster() { return cluster.getHttpAddresses(); } - public EqlDateNanosIT(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - super(query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlDateNanosIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlExtraIT.java b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlExtraIT.java index 8af8fcac087b5..cb92eddeb0410 100644 --- a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlExtraIT.java +++ b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlExtraIT.java @@ -27,7 +27,27 @@ protected String getTestRestCluster() { return cluster.getHttpAddresses(); } - public EqlExtraIT(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - super(query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlExtraIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlMissingEventsIT.java b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlMissingEventsIT.java index 05557fb4883b3..4f1faf3322e7f 100644 --- a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlMissingEventsIT.java +++ b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlMissingEventsIT.java @@ -27,8 +27,28 @@ protected String getTestRestCluster() { return cluster.getHttpAddresses(); } - public EqlMissingEventsIT(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - super(query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlMissingEventsIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleIT.java b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleIT.java index dc2c653fad89e..c0bce3ffc9e4f 100644 --- a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleIT.java +++ b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleIT.java @@ -27,8 +27,28 @@ protected String getTestRestCluster() { return cluster.getHttpAddresses(); } - public EqlSampleIT(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - super(query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlSampleIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleMultipleEntriesIT.java b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleMultipleEntriesIT.java index af1ade9120bbd..f50ee36095ae0 100644 --- a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleMultipleEntriesIT.java +++ b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSampleMultipleEntriesIT.java @@ -33,9 +33,22 @@ public EqlSampleMultipleEntriesIT( List eventIds, String[] joinKeys, Integer size, - Integer maxSamplesPerKey + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures ) { - super(query, name, eventIds, joinKeys, size, maxSamplesPerKey); + super( + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecFailingShardsIT.java b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecFailingShardsIT.java new file mode 100644 index 0000000000000..cf05811a77857 --- /dev/null +++ b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecFailingShardsIT.java @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.eql; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; + +import org.elasticsearch.test.TestClustersThreadFilter; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.eql.EqlSpecFailingShardsTestCase; +import org.junit.ClassRule; + +import java.util.List; + +@ThreadLeakFilters(filters = TestClustersThreadFilter.class) +public class EqlSpecFailingShardsIT extends EqlSpecFailingShardsTestCase { + + @ClassRule + public static final ElasticsearchCluster cluster = EqlTestCluster.CLUSTER; + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + + public EqlSpecFailingShardsIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); + } +} diff --git a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecIT.java b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecIT.java index 7aac0ae336c8a..0aad5cc1b73da 100644 --- a/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecIT.java +++ b/x-pack/plugin/eql/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/eql/EqlSpecIT.java @@ -27,7 +27,27 @@ protected String getTestRestCluster() { return cluster.getHttpAddresses(); } - public EqlSpecIT(String query, String name, List eventIds, String[] joinKeys, Integer size, Integer maxSamplesPerKey) { - super(query, name, eventIds, joinKeys, size, maxSamplesPerKey); + public EqlSpecIT( + String query, + String name, + List eventIds, + String[] joinKeys, + Integer size, + Integer maxSamplesPerKey, + Boolean allowPartialSearchResults, + Boolean allowPartialSequenceResults, + Boolean expectShardFailures + ) { + super( + query, + name, + eventIds, + joinKeys, + size, + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults, + expectShardFailures + ); } } diff --git a/x-pack/plugin/eql/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/eql/10_basic.yml b/x-pack/plugin/eql/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/eql/10_basic.yml index e49264d76d5e9..c7974f3b584b4 100644 --- a/x-pack/plugin/eql/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/eql/10_basic.yml +++ b/x-pack/plugin/eql/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/eql/10_basic.yml @@ -83,6 +83,34 @@ setup: id: 123 valid: true + - do: + indices.create: + index: eql_test_rebel + body: + mappings: + properties: + some_keyword: + type: keyword + runtime: + day_of_week: + type: keyword + script: + source: "throw new IllegalArgumentException(\"rebel shards\")" + - do: + bulk: + refresh: true + body: + - index: + _index: eql_test_rebel + _id: "1" + - event: + - category: process + "@timestamp": 2020-02-03T12:34:56Z + user: SYSTEM + id: 123 + valid: false + some_keyword: longer than normal + --- # Testing round-trip and the basic shape of the response "Execute some EQL.": @@ -478,3 +506,118 @@ setup: query: 'sequence with maxspan=10d [network where user == "ADMIN"] ![network where used == "SYSTEM"]' - match: { error.root_cause.0.type: "verification_exception" } - match: { error.root_cause.0.reason: "Found 1 problem\nline 1:75: Unknown column [used], did you mean [user]?" } + + +--- +"Execute query shard failures and with allow_partial_search_results": + - do: + eql.search: + index: eql_test* + body: + query: 'process where user == "SYSTEM" and day_of_week == "Monday"' + fields: [{"field":"@timestamp","format":"epoch_millis"},"id","valid","day_of_week"] + allow_partial_search_results: true + + - match: {timed_out: false} + - match: {hits.total.value: 1} + - match: {hits.total.relation: "eq"} + - match: {hits.events.0._source.user: "SYSTEM"} + - match: {hits.events.0._id: "1"} + - match: {hits.events.0.fields.@timestamp: ["1580733296000"]} + - match: {hits.events.0.fields.id: [123]} + - match: {hits.events.0.fields.valid: [false]} + - match: {hits.events.0.fields.day_of_week: ["Monday"]} + - match: {shard_failures.0.index: "eql_test_rebel"} + + +--- +"Execute query shard failures and with allow_partial_search_results as request param": + - do: + eql.search: + index: eql_test* + allow_partial_search_results: true + body: + query: 'process where user == "SYSTEM" and day_of_week == "Monday"' + fields: [{"field":"@timestamp","format":"epoch_millis"},"id","valid","day_of_week"] + + - match: {timed_out: false} + - match: {hits.total.value: 1} + - match: {hits.total.relation: "eq"} + - match: {hits.events.0._source.user: "SYSTEM"} + - match: {hits.events.0._id: "1"} + - match: {hits.events.0.fields.@timestamp: ["1580733296000"]} + - match: {hits.events.0.fields.id: [123]} + - match: {hits.events.0.fields.valid: [false]} + - match: {hits.events.0.fields.day_of_week: ["Monday"]} + - match: {shard_failures.0.index: "eql_test_rebel"} + + +--- +"Execute sequence with shard failures and allow_partial_search_results=true": + - do: + eql.search: + index: eql_test* + body: + query: 'sequence [process where user == "SYSTEM" and day_of_week == "Monday"] [process where user == "SYSTEM" and day_of_week == "Tuesday"]' + fields: [{"field":"@timestamp","format":"epoch_millis"},"id","valid","day_of_week"] + allow_partial_search_results: true + + - match: {timed_out: false} + - match: {hits.total.value: 0} + - match: {shard_failures.0.index: "eql_test_rebel"} + + +--- +"Execute sequence with shard failures, allow_partial_search_results=true and allow_partial_sequence_results=true": + - do: + eql.search: + index: eql_test* + body: + query: 'sequence [process where user == "SYSTEM" and day_of_week == "Monday"] [process where user == "SYSTEM" and day_of_week == "Tuesday"]' + fields: [{"field":"@timestamp","format":"epoch_millis"},"id","valid","day_of_week"] + allow_partial_search_results: true + allow_partial_sequence_results: true + + - match: {timed_out: false} + - match: {hits.total.value: 1} + - match: {hits.total.relation: "eq"} + - match: {hits.sequences.0.events.0._source.user: "SYSTEM"} + - match: {hits.sequences.0.events.0._id: "1"} + - match: {hits.sequences.0.events.0.fields.@timestamp: ["1580733296000"]} + - match: {hits.sequences.0.events.0.fields.id: [123]} + - match: {hits.sequences.0.events.0.fields.valid: [false]} + - match: {hits.sequences.0.events.0.fields.day_of_week: ["Monday"]} + - match: {hits.sequences.0.events.1._id: "2"} + - match: {hits.sequences.0.events.1.fields.@timestamp: ["1580819696000"]} + - match: {hits.sequences.0.events.1.fields.id: [123]} + - match: {hits.sequences.0.events.1.fields.valid: [true]} + - match: {hits.sequences.0.events.1.fields.day_of_week: ["Tuesday"]} + - match: {shard_failures.0.index: "eql_test_rebel"} + + +--- +"Execute sequence with shard failures, allow_partial_search_results=true and allow_partial_sequence_results=true as query params": + - do: + eql.search: + index: eql_test* + allow_partial_search_results: true + allow_partial_sequence_results: true + body: + query: 'sequence [process where user == "SYSTEM" and day_of_week == "Monday"] [process where user == "SYSTEM" and day_of_week == "Tuesday"]' + fields: [{"field":"@timestamp","format":"epoch_millis"},"id","valid","day_of_week"] + + - match: {timed_out: false} + - match: {hits.total.value: 1} + - match: {hits.total.relation: "eq"} + - match: {hits.sequences.0.events.0._source.user: "SYSTEM"} + - match: {hits.sequences.0.events.0._id: "1"} + - match: {hits.sequences.0.events.0.fields.@timestamp: ["1580733296000"]} + - match: {hits.sequences.0.events.0.fields.id: [123]} + - match: {hits.sequences.0.events.0.fields.valid: [false]} + - match: {hits.sequences.0.events.0.fields.day_of_week: ["Monday"]} + - match: {hits.sequences.0.events.1._id: "2"} + - match: {hits.sequences.0.events.1.fields.@timestamp: ["1580819696000"]} + - match: {hits.sequences.0.events.1.fields.id: [123]} + - match: {hits.sequences.0.events.1.fields.valid: [true]} + - match: {hits.sequences.0.events.1.fields.day_of_week: ["Tuesday"]} + - match: {shard_failures.0.index: "eql_test_rebel"} diff --git a/x-pack/plugin/eql/src/internalClusterTest/java/org/elasticsearch/xpack/eql/action/CCSPartialResultsIT.java b/x-pack/plugin/eql/src/internalClusterTest/java/org/elasticsearch/xpack/eql/action/CCSPartialResultsIT.java new file mode 100644 index 0000000000000..da6bb6180428b --- /dev/null +++ b/x-pack/plugin/eql/src/internalClusterTest/java/org/elasticsearch/xpack/eql/action/CCSPartialResultsIT.java @@ -0,0 +1,613 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.eql.action; + +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.AbstractMultiClustersTestCase; +import org.elasticsearch.xpack.eql.plugin.EqlPlugin; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class CCSPartialResultsIT extends AbstractMultiClustersTestCase { + + static String REMOTE_CLUSTER = "cluster_a"; + + protected Collection> nodePlugins(String cluster) { + return Collections.singletonList(LocalStateEQLXPackPlugin.class); + } + + protected final Client localClient() { + return client(LOCAL_CLUSTER); + } + + @Override + protected List remoteClusterAlias() { + return List.of(REMOTE_CLUSTER); + } + + @Override + protected boolean reuseClusters() { + return false; + } + + /** + * + * @return remote node name + */ + private String createSchema() { + final Client remoteClient = client(REMOTE_CLUSTER); + final String remoteNode = cluster(REMOTE_CLUSTER).startDataOnlyNode(); + final String remoteNode2 = cluster(REMOTE_CLUSTER).startDataOnlyNode(); + + assertAcked( + remoteClient.admin() + .indices() + .prepareCreate("test-1-remote") + .setSettings( + Settings.builder() + .put("index.routing.allocation.require._name", remoteNode) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .build() + ) + .setMapping("@timestamp", "type=date"), + TimeValue.timeValueSeconds(60) + ); + + assertAcked( + remoteClient.admin() + .indices() + .prepareCreate("test-2-remote") + .setSettings( + Settings.builder() + .put("index.routing.allocation.require._name", remoteNode2) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .build() + ) + .setMapping("@timestamp", "type=date"), + TimeValue.timeValueSeconds(60) + ); + + for (int i = 0; i < 5; i++) { + int val = i * 2; + remoteClient.prepareIndex("test-1-remote") + .setId(Integer.toString(i)) + .setSource("@timestamp", 100000 + val, "event.category", "process", "key", "same", "value", val) + .get(); + } + for (int i = 0; i < 5; i++) { + int val = i * 2 + 1; + remoteClient.prepareIndex("test-2-remote") + .setId(Integer.toString(i)) + .setSource("@timestamp", 100000 + val, "event.category", "process", "key", "same", "value", val) + .get(); + } + + remoteClient.admin().indices().prepareRefresh().get(); + return remoteNode; + } + + // ------------------------------------------------------------------------ + // queries with full cluster (no missing shards) + // ------------------------------------------------------------------------ + + public void testNoFailures() throws ExecutionException, InterruptedException, IOException { + createSchema(); + + // event query + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("process where true") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + EqlSearchResponse response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().events().size(), equalTo(10)); + for (int i = 0; i < 10; i++) { + assertThat(response.hits().events().get(i).toString(), containsString("\"value\" : " + i)); + } + assertThat(response.shardFailures().length, is(0)); + + // sequence query on both shards + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 1] [process where value == 2]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + EqlSearchResponse.Sequence sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 1")); + assertThat(sequence.events().get(1).toString(), containsString("\"value\" : 2")); + assertThat(response.shardFailures().length, is(0)); + + // sequence query on the available shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 1] [process where value == 3]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 1")); + assertThat(sequence.events().get(1).toString(), containsString("\"value\" : 3")); + assertThat(response.shardFailures().length, is(0)); + + // sequence query on the unavailable shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 0] [process where value == 2]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 0")); + assertThat(sequence.events().get(1).toString(), containsString("\"value\" : 2")); + assertThat(response.shardFailures().length, is(0)); + + // sequence query with missing event on unavailable shard + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(0)); + + // sample query on both shards + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 2] [process where value == 1]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + EqlSearchResponse.Sequence sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 2")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(0)); + + // sample query on the available shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 3] [process where value == 1]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 3")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(0)); + + // sample query on the unavailable shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 2] [process where value == 0]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 2")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 0")); + assertThat(response.shardFailures().length, is(0)); + + } + + // ------------------------------------------------------------------------ + // same queries, with missing shards and allow_partial_search_results=true + // and allow_partial_sequence_result=true + // ------------------------------------------------------------------------ + + public void testAllowPartialSearchAndSequence_event() throws ExecutionException, InterruptedException, IOException { + var remoteNode = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + cluster(REMOTE_CLUSTER).stopNode(remoteNode); + + // event query + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("process where true") + .allowPartialSearchResults(true); + var response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().events().size(), equalTo(5)); + for (int i = 0; i < 5; i++) { + assertThat(response.hits().events().get(i).toString(), containsString("\"value\" : " + (i * 2 + 1))); + } + assertThat(response.shardFailures().length, is(1)); + } + + public void testAllowPartialSearchAndSequence_sequence() throws ExecutionException, InterruptedException, IOException { + var remoteNode = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + cluster(REMOTE_CLUSTER).stopNode(remoteNode); + + // sequence query on both shards + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 1] [process where value == 2]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + var response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the available shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 1] [process where value == 3]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + var sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 1")); + assertThat(sequence.events().get(1).toString(), containsString("\"value\" : 3")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the unavailable shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 0] [process where value == 2]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query with missing event on unavailable shard. THIS IS A FALSE POSITIVE + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 1")); + assertThat(sequence.events().get(2).toString(), containsString("\"value\" : 3")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + public void testAllowPartialSearchAndSequence_sample() throws ExecutionException, InterruptedException, IOException { + var remoteNode = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + cluster(REMOTE_CLUSTER).stopNode(remoteNode); + + // sample query on both shards + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 2] [process where value == 1]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + var response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the available shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 3] [process where value == 1]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + var sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 3")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the unavailable shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 2] [process where value == 0]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + // ------------------------------------------------------------------------ + // same queries, with missing shards and allow_partial_search_results=true + // and default allow_partial_sequence_results (ie. false) + // ------------------------------------------------------------------------ + + public void testAllowPartialSearch_event() throws ExecutionException, InterruptedException, IOException { + var remoteNode = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + cluster(REMOTE_CLUSTER).stopNode(remoteNode); + + // event query + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("process where true") + .allowPartialSearchResults(true); + var response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().events().size(), equalTo(5)); + for (int i = 0; i < 5; i++) { + assertThat(response.hits().events().get(i).toString(), containsString("\"value\" : " + (i * 2 + 1))); + } + assertThat(response.shardFailures().length, is(1)); + + } + + public void testAllowPartialSearch_sequence() throws ExecutionException, InterruptedException, IOException { + var remoteNode = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + cluster(REMOTE_CLUSTER).stopNode(remoteNode); + + // sequence query on both shards + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 1] [process where value == 2]") + .allowPartialSearchResults(true); + var response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the available shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 1] [process where value == 3]") + .allowPartialSearchResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the unavailable shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 0] [process where value == 2]") + .allowPartialSearchResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query with missing event on unavailable shard. THIS IS A FALSE POSITIVE + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]") + .allowPartialSearchResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + public void testAllowPartialSearch_sample() throws ExecutionException, InterruptedException, IOException { + var remoteNode = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + cluster(REMOTE_CLUSTER).stopNode(remoteNode); + + // sample query on both shards + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 2] [process where value == 1]") + .allowPartialSearchResults(true); + var response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the available shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 3] [process where value == 1]") + .allowPartialSearchResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + var sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 3")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the unavailable shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 2] [process where value == 0]") + .allowPartialSearchResults(true); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + // ------------------------------------------------------------------------ + // same queries, with missing shards and with default xpack.eql.default_allow_partial_results=true + // ------------------------------------------------------------------------ + + public void testClusterSetting_event() throws ExecutionException, InterruptedException, IOException { + var remoteNode = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + cluster(REMOTE_CLUSTER).stopNode(remoteNode); + + cluster(REMOTE_CLUSTER).client() + .execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().put(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey(), true) + ) + ) + .get(); + + // event query + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*").query("process where true"); + var response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().events().size(), equalTo(5)); + for (int i = 0; i < 5; i++) { + assertThat(response.hits().events().get(i).toString(), containsString("\"value\" : " + (i * 2 + 1))); + } + assertThat(response.shardFailures().length, is(1)); + + localClient().execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().putNull(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey()) + ) + ).get(); + } + + public void testClusterSetting_sequence() throws ExecutionException, InterruptedException, IOException { + var remoteNode = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + cluster(REMOTE_CLUSTER).stopNode(remoteNode); + + cluster(REMOTE_CLUSTER).client() + .execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().put(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey(), true) + ) + ) + .get(); + // sequence query on both shards + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 1] [process where value == 2]"); + var response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the available shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 1] [process where value == 3]"); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the unavailable shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence [process where value == 0] [process where value == 2]"); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query with missing event on unavailable shard. THIS IS A FALSE POSITIVE + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]"); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + localClient().execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().putNull(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey()) + ) + ).get(); + } + + public void testClusterSetting_sample() throws ExecutionException, InterruptedException, IOException { + var remoteNode = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + cluster(REMOTE_CLUSTER).stopNode(remoteNode); + + cluster(REMOTE_CLUSTER).client() + .execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().put(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey(), true) + ) + ) + .get(); + + // sample query on both shards + var request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 2] [process where value == 1]"); + var response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the available shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 3] [process where value == 1]"); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + var sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 3")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the unavailable shard only + request = new EqlSearchRequest().indices(REMOTE_CLUSTER + ":test-*") + .query("sample by key [process where value == 2] [process where value == 0]"); + response = localClient().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1-remote")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + localClient().execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().putNull(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey()) + ) + ).get(); + } +} diff --git a/x-pack/plugin/eql/src/internalClusterTest/java/org/elasticsearch/xpack/eql/action/PartialSearchResultsIT.java b/x-pack/plugin/eql/src/internalClusterTest/java/org/elasticsearch/xpack/eql/action/PartialSearchResultsIT.java new file mode 100644 index 0000000000000..9048d11f4eddf --- /dev/null +++ b/x-pack/plugin/eql/src/internalClusterTest/java/org/elasticsearch/xpack/eql/action/PartialSearchResultsIT.java @@ -0,0 +1,780 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.eql.action; + +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.action.search.SearchPhaseExecutionException; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.SearchService; +import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.xpack.core.async.GetAsyncResultRequest; +import org.elasticsearch.xpack.eql.plugin.EqlAsyncGetResultAction; +import org.elasticsearch.xpack.eql.plugin.EqlPlugin; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +public class PartialSearchResultsIT extends AbstractEqlIntegTestCase { + + @Override + protected Collection> nodePlugins() { + return CollectionUtils.appendToCopy(super.nodePlugins(), MockTransportService.TestPlugin.class); + } + + @Override + protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal, otherSettings)) + .put(SearchService.KEEPALIVE_INTERVAL_SETTING.getKey(), TimeValue.timeValueMillis(randomIntBetween(100, 500))) + .build(); + } + + /** + * + * @return node name where the first index is + */ + private String createSchema() { + internalCluster().ensureAtLeastNumDataNodes(2); + final List dataNodes = internalCluster().clusterService() + .state() + .nodes() + .getDataNodes() + .values() + .stream() + .map(DiscoveryNode::getName) + .toList(); + final String assignedNodeForIndex1 = randomFrom(dataNodes); + + assertAcked( + indicesAdmin().prepareCreate("test-1") + .setSettings( + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put("index.routing.allocation.include._name", assignedNodeForIndex1) + .build() + ) + .setMapping("@timestamp", "type=date") + ); + assertAcked( + indicesAdmin().prepareCreate("test-2") + .setSettings( + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put("index.routing.allocation.exclude._name", assignedNodeForIndex1) + .build() + ) + .setMapping("@timestamp", "type=date") + ); + + for (int i = 0; i < 5; i++) { + int val = i * 2; + prepareIndex("test-1").setId(Integer.toString(i)) + .setSource("@timestamp", 100000 + val, "event.category", "process", "key", "same", "value", val) + .get(); + } + for (int i = 0; i < 5; i++) { + int val = i * 2 + 1; + prepareIndex("test-2").setId(Integer.toString(i)) + .setSource("@timestamp", 100000 + val, "event.category", "process", "key", "same", "value", val) + .get(); + } + refresh(); + return assignedNodeForIndex1; + } + + public void testNoFailures() throws Exception { + createSchema(); + + // ------------------------------------------------------------------------ + // queries with full cluster (no missing shards) + // ------------------------------------------------------------------------ + + // event query + var request = new EqlSearchRequest().indices("test-*") + .query("process where true") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + EqlSearchResponse response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().events().size(), equalTo(10)); + for (int i = 0; i < 10; i++) { + assertThat(response.hits().events().get(i).toString(), containsString("\"value\" : " + i)); + } + assertThat(response.shardFailures().length, is(0)); + + // sequence query on both shards + request = new EqlSearchRequest().indices("test-*") + .query("sequence [process where value == 1] [process where value == 2]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + EqlSearchResponse.Sequence sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 1")); + assertThat(sequence.events().get(1).toString(), containsString("\"value\" : 2")); + assertThat(response.shardFailures().length, is(0)); + + // sequence query on the available shard only + request = new EqlSearchRequest().indices("test-*") + .query("sequence [process where value == 1] [process where value == 3]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 1")); + assertThat(sequence.events().get(1).toString(), containsString("\"value\" : 3")); + assertThat(response.shardFailures().length, is(0)); + + // sequence query on the unavailable shard only + request = new EqlSearchRequest().indices("test-*") + .query("sequence [process where value == 0] [process where value == 2]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 0")); + assertThat(sequence.events().get(1).toString(), containsString("\"value\" : 2")); + assertThat(response.shardFailures().length, is(0)); + + // sequence query with missing event on unavailable shard + request = new EqlSearchRequest().indices("test-*") + .query("sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(0)); + + // sample query on both shards + request = new EqlSearchRequest().indices("test-*") + .query("sample by key [process where value == 2] [process where value == 1]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + EqlSearchResponse.Sequence sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 2")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(0)); + + // sample query on the available shard only + request = new EqlSearchRequest().indices("test-*") + .query("sample by key [process where value == 3] [process where value == 1]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 3")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(0)); + + // sample query on the unavailable shard only + request = new EqlSearchRequest().indices("test-*") + .query("sample by key [process where value == 2] [process where value == 0]") + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 2")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 0")); + assertThat(response.shardFailures().length, is(0)); + + } + + // ------------------------------------------------------------------------ + // same queries, with missing shards. Let them fail + // allow_partial_sequence_results has no effect if allow_partial_sequence_results is not set to true. + // ------------------------------------------------------------------------ + + public void testFailures_event() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // event query + shouldFail("process where true"); + + } + + public void testFailures_sequence() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // sequence query on both shards + shouldFail("sequence [process where value == 1] [process where value == 2]"); + + // sequence query on the available shard only + shouldFail("sequence [process where value == 1] [process where value == 3]"); + + // sequence query on the unavailable shard only + shouldFail("sequence [process where value == 0] [process where value == 2]"); + + // sequence query with missing event on unavailable shard. + shouldFail("sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]"); + } + + public void testFailures_sample() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // sample query on both shards + shouldFail("sample by key [process where value == 2] [process where value == 1]"); + + // sample query on the available shard only + shouldFail("sample by key [process where value == 3] [process where value == 1]"); + + // sample query on the unavailable shard only + shouldFail("sample by key [process where value == 2] [process where value == 0]"); + + } + + // ------------------------------------------------------------------------ + // same queries, with missing shards and allow_partial_search_results=true + // and allow_partial_sequence_result=true + // ------------------------------------------------------------------------ + + public void testAllowPartialSearchAndSequenceResults_event() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // event query + var request = new EqlSearchRequest().indices("test-*").query("process where true").allowPartialSearchResults(true); + var response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().events().size(), equalTo(5)); + for (int i = 0; i < 5; i++) { + assertThat(response.hits().events().get(i).toString(), containsString("\"value\" : " + (i * 2 + 1))); + } + assertThat(response.shardFailures().length, is(1)); + + } + + public void testAllowPartialSearchAndSequenceResults_sequence() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // sequence query on both shards + var request = new EqlSearchRequest().indices("test-*") + .query("sequence [process where value == 1] [process where value == 2]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + var response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the available shard only + request = new EqlSearchRequest().indices("test-*") + .query("sequence [process where value == 1] [process where value == 3]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + var sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 1")); + assertThat(sequence.events().get(1).toString(), containsString("\"value\" : 3")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the unavailable shard only + request = new EqlSearchRequest().indices("test-*") + .query("sequence [process where value == 0] [process where value == 2]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query with missing event on unavailable shard. THIS IS A FALSE POSITIVE + request = new EqlSearchRequest().indices("test-*") + .query("sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + sequence = response.hits().sequences().get(0); + assertThat(sequence.events().get(0).toString(), containsString("\"value\" : 1")); + assertThat(sequence.events().get(2).toString(), containsString("\"value\" : 3")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + public void testAllowPartialSearchAndSequenceResults_sample() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // sample query on both shards + var request = new EqlSearchRequest().indices("test-*") + .query("sample by key [process where value == 2] [process where value == 1]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + var response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the available shard only + request = new EqlSearchRequest().indices("test-*") + .query("sample by key [process where value == 3] [process where value == 1]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + var sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 3")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the unavailable shard only + request = new EqlSearchRequest().indices("test-*") + .query("sample by key [process where value == 2] [process where value == 0]") + .allowPartialSearchResults(true) + .allowPartialSequenceResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + // ------------------------------------------------------------------------ + // same queries, with missing shards and allow_partial_search_results=true + // and default allow_partial_sequence_results (ie. false) + // ------------------------------------------------------------------------ + + public void testAllowPartialSearchResults_event() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // event query + var request = new EqlSearchRequest().indices("test-*").query("process where true").allowPartialSearchResults(true); + var response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().events().size(), equalTo(5)); + for (int i = 0; i < 5; i++) { + assertThat(response.hits().events().get(i).toString(), containsString("\"value\" : " + (i * 2 + 1))); + } + assertThat(response.shardFailures().length, is(1)); + + } + + public void testAllowPartialSearchResults_sequence() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // sequence query on both shards + var request = new EqlSearchRequest().indices("test-*") + .query("sequence [process where value == 1] [process where value == 2]") + .allowPartialSearchResults(true); + var response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the available shard only + request = new EqlSearchRequest().indices("test-*") + .query("sequence [process where value == 1] [process where value == 3]") + .allowPartialSearchResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the unavailable shard only + request = new EqlSearchRequest().indices("test-*") + .query("sequence [process where value == 0] [process where value == 2]") + .allowPartialSearchResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query with missing event on unavailable shard. THIS IS A FALSE POSITIVE + request = new EqlSearchRequest().indices("test-*") + .query("sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]") + .allowPartialSearchResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + public void testAllowPartialSearchResults_sample() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // sample query on both shards + var request = new EqlSearchRequest().indices("test-*") + .query("sample by key [process where value == 2] [process where value == 1]") + .allowPartialSearchResults(true); + var response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the available shard only + request = new EqlSearchRequest().indices("test-*") + .query("sample by key [process where value == 3] [process where value == 1]") + .allowPartialSearchResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + var sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 3")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the unavailable shard only + request = new EqlSearchRequest().indices("test-*") + .query("sample by key [process where value == 2] [process where value == 0]") + .allowPartialSearchResults(true); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + // ------------------------------------------------------------------------ + // same queries, this time async, with missing shards and allow_partial_search_results=true + // and default allow_partial_sequence_results (ie. false) + // ------------------------------------------------------------------------ + + public void testAsyncAllowPartialSearchResults_event() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // event query + var response = runAsync("process where true", true); + assertThat(response.hits().events().size(), equalTo(5)); + for (int i = 0; i < 5; i++) { + assertThat(response.hits().events().get(i).toString(), containsString("\"value\" : " + (i * 2 + 1))); + } + assertThat(response.shardFailures().length, is(1)); + + } + + public void testAsyncAllowPartialSearchResults_sequence() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + // sequence query on both shards + var response = runAsync("sequence [process where value == 1] [process where value == 2]", true); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the available shard only + response = runAsync("sequence [process where value == 1] [process where value == 3]", true); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the unavailable shard only + response = runAsync("sequence [process where value == 0] [process where value == 2]", true); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query with missing event on unavailable shard. THIS IS A FALSE POSITIVE + response = runAsync( + "sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]", + true + ); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + public void testAsyncAllowPartialSearchResults_sample() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + // sample query on both shards + var response = runAsync("sample by key [process where value == 2] [process where value == 1]", true); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the available shard only + response = runAsync("sample by key [process where value == 3] [process where value == 1]", true); + assertThat(response.hits().sequences().size(), equalTo(1)); + var sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 3")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the unavailable shard only + response = runAsync("sample by key [process where value == 2] [process where value == 0]", true); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + } + + // ------------------------------------------------------------------------ + // same queries, with missing shards and with default xpack.eql.default_allow_partial_results=true + // ------------------------------------------------------------------------ + + public void testClusterSetting_event() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + client().execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().put(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey(), true) + ) + ).get(); + + // event query + var request = new EqlSearchRequest().indices("test-*").query("process where true"); + var response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().events().size(), equalTo(5)); + for (int i = 0; i < 5; i++) { + assertThat(response.hits().events().get(i).toString(), containsString("\"value\" : " + (i * 2 + 1))); + } + assertThat(response.shardFailures().length, is(1)); + + client().execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().putNull(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey()) + ) + ).get(); + } + + public void testClusterSetting_sequence() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + client().execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().put(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey(), true) + ) + ).get(); + // sequence query on both shards + var request = new EqlSearchRequest().indices("test-*").query("sequence [process where value == 1] [process where value == 2]"); + var response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the available shard only + request = new EqlSearchRequest().indices("test-*").query("sequence [process where value == 1] [process where value == 3]"); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query on the unavailable shard only + request = new EqlSearchRequest().indices("test-*").query("sequence [process where value == 0] [process where value == 2]"); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sequence query with missing event on unavailable shard. THIS IS A FALSE POSITIVE + request = new EqlSearchRequest().indices("test-*") + .query("sequence with maxspan=10s [process where value == 1] ![process where value == 2] [process where value == 3]"); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + client().execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().putNull(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey()) + ) + ).get(); + } + + public void testClusterSetting_sample() throws Exception { + final String assignedNodeForIndex1 = createSchema(); + // ------------------------------------------------------------------------ + // stop one of the nodes, make one of the shards unavailable + // ------------------------------------------------------------------------ + + internalCluster().stopNode(assignedNodeForIndex1); + + client().execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().put(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey(), true) + ) + ).get(); + + // sample query on both shards + var request = new EqlSearchRequest().indices("test-*").query("sample by key [process where value == 2] [process where value == 1]"); + var response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the available shard only + request = new EqlSearchRequest().indices("test-*").query("sample by key [process where value == 3] [process where value == 1]"); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(1)); + var sample = response.hits().sequences().get(0); + assertThat(sample.events().get(0).toString(), containsString("\"value\" : 3")); + assertThat(sample.events().get(1).toString(), containsString("\"value\" : 1")); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + // sample query on the unavailable shard only + request = new EqlSearchRequest().indices("test-*").query("sample by key [process where value == 2] [process where value == 0]"); + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + assertThat(response.hits().sequences().size(), equalTo(0)); + assertThat(response.shardFailures().length, is(1)); + assertThat(response.shardFailures()[0].index(), is("test-1")); + assertThat(response.shardFailures()[0].reason(), containsString("NoShardAvailableActionException")); + + client().execute( + ClusterUpdateSettingsAction.INSTANCE, + new ClusterUpdateSettingsRequest(TimeValue.THIRTY_SECONDS, TimeValue.THIRTY_SECONDS).persistentSettings( + Settings.builder().putNull(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getKey()) + ) + ).get(); + } + + private static EqlSearchResponse runAsync(String query, Boolean allowPartialSearchResults) throws InterruptedException, + ExecutionException { + EqlSearchRequest request; + EqlSearchResponse response; + request = new EqlSearchRequest().indices("test-*").query(query).waitForCompletionTimeout(TimeValue.ZERO); + if (allowPartialSearchResults != null) { + request = request.allowPartialSearchResults(allowPartialSearchResults); + } + response = client().execute(EqlSearchAction.INSTANCE, request).get(); + while (response.isRunning()) { + GetAsyncResultRequest getResultsRequest = new GetAsyncResultRequest(response.id()).setKeepAlive(TimeValue.timeValueMinutes(10)) + .setWaitForCompletionTimeout(TimeValue.timeValueMillis(10)); + response = client().execute(EqlAsyncGetResultAction.INSTANCE, getResultsRequest).get(); + } + return response; + } + + private static void shouldFail(String query) throws InterruptedException { + EqlSearchRequest request = new EqlSearchRequest().indices("test-*").query(query); + if (randomBoolean()) { + request = request.allowPartialSearchResults(false); + } + if (randomBoolean()) { + request = request.allowPartialSequenceResults(randomBoolean()); + } + try { + client().execute(EqlSearchAction.INSTANCE, request).get(); + fail(); + } catch (ExecutionException e) { + assertThat(e.getCause(), instanceOf(SearchPhaseExecutionException.class)); + } + } +} diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchRequest.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchRequest.java index 0aeddd525e317..5804e11b72ff5 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchRequest.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchRequest.java @@ -63,6 +63,8 @@ public class EqlSearchRequest extends ActionRequest implements IndicesRequest.Re private List fetchFields; private Map runtimeMappings = emptyMap(); private int maxSamplesPerKey = RequestDefaults.MAX_SAMPLES_PER_KEY; + private Boolean allowPartialSearchResults; + private Boolean allowPartialSequenceResults; // Async settings private TimeValue waitForCompletionTimeout = null; @@ -83,6 +85,8 @@ public class EqlSearchRequest extends ActionRequest implements IndicesRequest.Re static final String KEY_FETCH_FIELDS = "fields"; static final String KEY_RUNTIME_MAPPINGS = "runtime_mappings"; static final String KEY_MAX_SAMPLES_PER_KEY = "max_samples_per_key"; + static final String KEY_ALLOW_PARTIAL_SEARCH_RESULTS = "allow_partial_search_results"; + static final String KEY_ALLOW_PARTIAL_SEQUENCE_RESULTS = "allow_partial_sequence_results"; static final ParseField FILTER = new ParseField(KEY_FILTER); static final ParseField TIMESTAMP_FIELD = new ParseField(KEY_TIMESTAMP_FIELD); @@ -97,6 +101,8 @@ public class EqlSearchRequest extends ActionRequest implements IndicesRequest.Re static final ParseField RESULT_POSITION = new ParseField(KEY_RESULT_POSITION); static final ParseField FETCH_FIELDS_FIELD = SearchSourceBuilder.FETCH_FIELDS_FIELD; static final ParseField MAX_SAMPLES_PER_KEY = new ParseField(KEY_MAX_SAMPLES_PER_KEY); + static final ParseField ALLOW_PARTIAL_SEARCH_RESULTS = new ParseField(KEY_ALLOW_PARTIAL_SEARCH_RESULTS); + static final ParseField ALLOW_PARTIAL_SEQUENCE_RESULTS = new ParseField(KEY_ALLOW_PARTIAL_SEQUENCE_RESULTS); private static final ObjectParser PARSER = objectParser(EqlSearchRequest::new); @@ -135,6 +141,13 @@ public EqlSearchRequest(StreamInput in) throws IOException { if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_7_0)) { maxSamplesPerKey = in.readInt(); } + if (in.getTransportVersion().onOrAfter(TransportVersions.EQL_ALLOW_PARTIAL_SEARCH_RESULTS)) { + allowPartialSearchResults = in.readOptionalBoolean(); + allowPartialSequenceResults = in.readOptionalBoolean(); + } else { + allowPartialSearchResults = false; + allowPartialSequenceResults = false; + } } @Override @@ -245,6 +258,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(KEY_RUNTIME_MAPPINGS, runtimeMappings); } builder.field(KEY_MAX_SAMPLES_PER_KEY, maxSamplesPerKey); + builder.field(KEY_ALLOW_PARTIAL_SEARCH_RESULTS, allowPartialSearchResults); + builder.field(KEY_ALLOW_PARTIAL_SEQUENCE_RESULTS, allowPartialSequenceResults); return builder; } @@ -279,6 +294,8 @@ protected static ObjectParser objectParser parser.declareField(EqlSearchRequest::fetchFields, EqlSearchRequest::parseFetchFields, FETCH_FIELDS_FIELD, ValueType.VALUE_ARRAY); parser.declareObject(EqlSearchRequest::runtimeMappings, (p, c) -> p.map(), SearchSourceBuilder.RUNTIME_MAPPINGS_FIELD); parser.declareInt(EqlSearchRequest::maxSamplesPerKey, MAX_SAMPLES_PER_KEY); + parser.declareBoolean(EqlSearchRequest::allowPartialSearchResults, ALLOW_PARTIAL_SEARCH_RESULTS); + parser.declareBoolean(EqlSearchRequest::allowPartialSequenceResults, ALLOW_PARTIAL_SEQUENCE_RESULTS); return parser; } @@ -427,6 +444,24 @@ public EqlSearchRequest maxSamplesPerKey(int maxSamplesPerKey) { return this; } + public Boolean allowPartialSearchResults() { + return allowPartialSearchResults; + } + + public EqlSearchRequest allowPartialSearchResults(Boolean val) { + this.allowPartialSearchResults = val; + return this; + } + + public Boolean allowPartialSequenceResults() { + return allowPartialSequenceResults; + } + + public EqlSearchRequest allowPartialSequenceResults(Boolean val) { + this.allowPartialSequenceResults = val; + return this; + } + private static List parseFetchFields(XContentParser parser) throws IOException { List result = new ArrayList<>(); Token token = parser.currentToken(); @@ -470,6 +505,10 @@ public void writeTo(StreamOutput out) throws IOException { if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_7_0)) { out.writeInt(maxSamplesPerKey); } + if (out.getTransportVersion().onOrAfter(TransportVersions.EQL_ALLOW_PARTIAL_SEARCH_RESULTS)) { + out.writeOptionalBoolean(allowPartialSearchResults); + out.writeOptionalBoolean(allowPartialSequenceResults); + } } @Override @@ -496,7 +535,9 @@ public boolean equals(Object o) { && Objects.equals(resultPosition, that.resultPosition) && Objects.equals(fetchFields, that.fetchFields) && Objects.equals(runtimeMappings, that.runtimeMappings) - && Objects.equals(maxSamplesPerKey, that.maxSamplesPerKey); + && Objects.equals(maxSamplesPerKey, that.maxSamplesPerKey) + && Objects.equals(allowPartialSearchResults, that.allowPartialSearchResults) + && Objects.equals(allowPartialSequenceResults, that.allowPartialSequenceResults); } @Override @@ -517,7 +558,9 @@ public int hashCode() { resultPosition, fetchFields, runtimeMappings, - maxSamplesPerKey + maxSamplesPerKey, + allowPartialSearchResults, + allowPartialSequenceResults ); } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchResponse.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchResponse.java index 2b7b8b074fa71..a4d93b7659970 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchResponse.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchResponse.java @@ -7,8 +7,11 @@ package org.elasticsearch.xpack.eql.action; import org.apache.lucene.search.TotalHits; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.TransportVersions; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ShardOperationFailedException; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -17,6 +20,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParserUtils; import org.elasticsearch.core.Nullable; @@ -36,6 +40,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -54,6 +59,7 @@ public class EqlSearchResponse extends ActionResponse implements ToXContentObjec private final String asyncExecutionId; private final boolean isRunning; private final boolean isPartial; + private final ShardSearchFailure[] shardFailures; private static final class Fields { static final String TOOK = "took"; @@ -62,6 +68,7 @@ private static final class Fields { static final String ID = "id"; static final String IS_RUNNING = "is_running"; static final String IS_PARTIAL = "is_partial"; + static final String SHARD_FAILURES = "shard_failures"; } private static final ParseField TOOK = new ParseField(Fields.TOOK); @@ -70,8 +77,10 @@ private static final class Fields { private static final ParseField ID = new ParseField(Fields.ID); private static final ParseField IS_RUNNING = new ParseField(Fields.IS_RUNNING); private static final ParseField IS_PARTIAL = new ParseField(Fields.IS_PARTIAL); + private static final ParseField SHARD_FAILURES = new ParseField(Fields.SHARD_FAILURES); private static final InstantiatingObjectParser PARSER; + static { InstantiatingObjectParser.Builder parser = InstantiatingObjectParser.builder( "eql/search_response", @@ -84,11 +93,12 @@ private static final class Fields { parser.declareString(optionalConstructorArg(), ID); parser.declareBoolean(constructorArg(), IS_RUNNING); parser.declareBoolean(constructorArg(), IS_PARTIAL); + parser.declareObjectArray(optionalConstructorArg(), (p, c) -> ShardSearchFailure.EMPTY_ARRAY, SHARD_FAILURES); PARSER = parser.build(); } - public EqlSearchResponse(Hits hits, long tookInMillis, boolean isTimeout) { - this(hits, tookInMillis, isTimeout, null, false, false); + public EqlSearchResponse(Hits hits, long tookInMillis, boolean isTimeout, ShardSearchFailure[] shardFailures) { + this(hits, tookInMillis, isTimeout, null, false, false, shardFailures); } public EqlSearchResponse( @@ -97,7 +107,8 @@ public EqlSearchResponse( boolean isTimeout, String asyncExecutionId, boolean isRunning, - boolean isPartial + boolean isPartial, + ShardSearchFailure[] shardFailures ) { super(); this.hits = hits == null ? Hits.EMPTY : hits; @@ -106,6 +117,7 @@ public EqlSearchResponse( this.asyncExecutionId = asyncExecutionId; this.isRunning = isRunning; this.isPartial = isPartial; + this.shardFailures = shardFailures; } public EqlSearchResponse(StreamInput in) throws IOException { @@ -116,6 +128,11 @@ public EqlSearchResponse(StreamInput in) throws IOException { asyncExecutionId = in.readOptionalString(); isPartial = in.readBoolean(); isRunning = in.readBoolean(); + if (in.getTransportVersion().onOrAfter(TransportVersions.EQL_ALLOW_PARTIAL_SEARCH_RESULTS)) { + shardFailures = in.readArray(ShardSearchFailure::readShardSearchFailure, ShardSearchFailure[]::new); + } else { + shardFailures = ShardSearchFailure.EMPTY_ARRAY; + } } public static EqlSearchResponse fromXContent(XContentParser parser) { @@ -130,6 +147,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(asyncExecutionId); out.writeBoolean(isPartial); out.writeBoolean(isRunning); + if (out.getTransportVersion().onOrAfter(TransportVersions.EQL_ALLOW_PARTIAL_SEARCH_RESULTS)) { + out.writeArray(shardFailures); + } } @Override @@ -147,6 +167,13 @@ private XContentBuilder innerToXContent(XContentBuilder builder, Params params) builder.field(IS_RUNNING.getPreferredName(), isRunning); builder.field(TOOK.getPreferredName(), tookInMillis); builder.field(TIMED_OUT.getPreferredName(), isTimeout); + if (CollectionUtils.isEmpty(shardFailures) == false) { + builder.startArray(SHARD_FAILURES.getPreferredName()); + for (ShardOperationFailedException shardFailure : ExceptionsHelper.groupBy(shardFailures)) { + shardFailure.toXContent(builder, params); + } + builder.endArray(); + } hits.toXContent(builder, params); return builder; } @@ -178,6 +205,10 @@ public boolean isPartial() { return isPartial; } + public ShardSearchFailure[] shardFailures() { + return shardFailures; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -190,12 +221,13 @@ public boolean equals(Object o) { return Objects.equals(hits, that.hits) && Objects.equals(tookInMillis, that.tookInMillis) && Objects.equals(isTimeout, that.isTimeout) - && Objects.equals(asyncExecutionId, that.asyncExecutionId); + && Objects.equals(asyncExecutionId, that.asyncExecutionId) + && Arrays.equals(shardFailures, that.shardFailures); } @Override public int hashCode() { - return Objects.hash(hits, tookInMillis, isTimeout, asyncExecutionId); + return Objects.hash(hits, tookInMillis, isTimeout, asyncExecutionId, Arrays.hashCode(shardFailures)); } @Override diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchTask.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchTask.java index 2a1bc3b7adb67..0fc8e8c88d7d9 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchTask.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchTask.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.eql.action; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.core.TimeValue; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.xpack.core.async.AsyncExecutionId; @@ -39,7 +40,8 @@ public EqlSearchResponse getCurrentResult() { false, getExecutionId().getEncoded(), true, - true + true, + ShardSearchFailure.EMPTY_ARRAY ); } } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/assembler/ExecutionManager.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/assembler/ExecutionManager.java index b26c815c1a2b5..672d6b87a8dbb 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/assembler/ExecutionManager.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/assembler/ExecutionManager.java @@ -167,7 +167,9 @@ public Executable assemble( criteria.subList(0, completionStage), criteria.get(completionStage), matcher, - listOfKeys + listOfKeys, + cfg.allowPartialSearchResults(), + cfg.allowPartialSequenceResults() ); return w; @@ -235,7 +237,8 @@ public Executable assemble(List> listOfKeys, List cfg.fetchSize(), limit, session.circuitBreaker(), - cfg.maxSamplesPerKey() + cfg.maxSamplesPerKey(), + cfg.allowPartialSearchResults() ); } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/payload/AbstractPayload.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/payload/AbstractPayload.java index 823cd04d25f45..9fecf958b9714 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/payload/AbstractPayload.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/payload/AbstractPayload.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.eql.execution.payload; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.eql.session.Payload; @@ -14,10 +15,12 @@ public abstract class AbstractPayload implements Payload { private final boolean timedOut; private final TimeValue timeTook; + private ShardSearchFailure[] shardFailures; - protected AbstractPayload(boolean timedOut, TimeValue timeTook) { + protected AbstractPayload(boolean timedOut, TimeValue timeTook, ShardSearchFailure[] shardFailures) { this.timedOut = timedOut; this.timeTook = timeTook; + this.shardFailures = shardFailures; } @Override @@ -29,4 +32,9 @@ public boolean timedOut() { public TimeValue timeTook() { return timeTook; } + + @Override + public ShardSearchFailure[] shardFailures() { + return shardFailures; + } } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/payload/EventPayload.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/payload/EventPayload.java index a7845ca62dccc..6471bc0814f70 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/payload/EventPayload.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/payload/EventPayload.java @@ -20,7 +20,7 @@ public class EventPayload extends AbstractPayload { private final List values; public EventPayload(SearchResponse response) { - super(response.isTimedOut(), response.getTook()); + super(response.isTimedOut(), response.getTook(), response.getShardFailures()); SearchHits hits = response.getHits(); values = new ArrayList<>(hits.getHits().length); diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sample/SampleIterator.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sample/SampleIterator.java index 89f1c4d1eb041..b9b7cfd6b615a 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sample/SampleIterator.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sample/SampleIterator.java @@ -14,6 +14,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.search.MultiSearchResponse; import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.core.TimeValue; import org.elasticsearch.search.SearchHit; @@ -35,6 +36,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -44,6 +46,7 @@ import static org.elasticsearch.common.Strings.EMPTY_ARRAY; import static org.elasticsearch.xpack.eql.execution.assembler.SampleQueryRequest.COMPOSITE_AGG_NAME; import static org.elasticsearch.xpack.eql.execution.search.RuntimeUtils.prepareRequest; +import static org.elasticsearch.xpack.eql.util.SearchHitUtils.addShardFailures; public class SampleIterator implements Executable { @@ -58,6 +61,7 @@ public class SampleIterator implements Executable { private final Limit limit; private final int maxSamplesPerKey; private long startTime; + private Map shardFailures = new HashMap<>(); // ---------- CIRCUIT BREAKER ----------- @@ -84,13 +88,16 @@ public class SampleIterator implements Executable { */ private long previousTotalPageSize = 0; + private boolean allowPartialSearchResults; + public SampleIterator( QueryClient client, List criteria, int fetchSize, Limit limit, CircuitBreaker circuitBreaker, - int maxSamplesPerKey + int maxSamplesPerKey, + boolean allowPartialSearchResults ) { this.client = client; this.criteria = criteria; @@ -100,6 +107,7 @@ public SampleIterator( this.limit = limit; this.circuitBreaker = circuitBreaker; this.maxSamplesPerKey = maxSamplesPerKey; + this.allowPartialSearchResults = allowPartialSearchResults; } @Override @@ -147,6 +155,7 @@ private void advance(ActionListener listener) { private void queryForCompositeAggPage(ActionListener listener, final SampleQueryRequest request) { client.query(request, listener.delegateFailureAndWrap((delegate, r) -> { + addShardFailures(shardFailures, r); // either the fields values or the fields themselves are missing // or the filter applied on the eql query matches no documents if (r.hasAggregations() == false) { @@ -209,13 +218,16 @@ private void finalStep(ActionListener listener) { for (SampleCriterion criterion : criteria) { SampleQueryRequest r = criterion.finalQuery(); r.singleKeyPair(compositeKeyValues, maxCriteria, maxSamplesPerKey); - searches.add(prepareRequest(r.searchSource(), false, EMPTY_ARRAY)); + searches.add(prepareRequest(r.searchSource(), false, allowPartialSearchResults, EMPTY_ARRAY)); } sampleKeys.add(new SequenceKey(compositeKeyValues.toArray())); } int initialSize = samples.size(); client.multiQuery(searches, listener.delegateFailureAndWrap((delegate, r) -> { + for (MultiSearchResponse.Item item : r) { + addShardFailures(shardFailures, item.getResponse()); + } List> sample = new ArrayList<>(maxCriteria); MultiSearchResponse.Item[] response = r.getResponses(); int docGroupsCounter = 1; @@ -280,14 +292,23 @@ private void payload(ActionListener listener) { log.trace("Sending payload for [{}] samples", samples.size()); if (samples.isEmpty()) { - listener.onResponse(new EmptyPayload(Type.SAMPLE, timeTook())); + listener.onResponse(new EmptyPayload(Type.SAMPLE, timeTook(), shardFailures.values().toArray(new ShardSearchFailure[0]))); return; } // get results through search (to keep using PIT) client.fetchHits( hits(samples), - ActionListeners.map(listener, listOfHits -> new SamplePayload(samples, listOfHits, false, timeTook())) + ActionListeners.map( + listener, + listOfHits -> new SamplePayload( + samples, + listOfHits, + false, + timeTook(), + shardFailures.values().toArray(new ShardSearchFailure[0]) + ) + ) ); } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sample/SamplePayload.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sample/SamplePayload.java index 121f4c208273b..aee084dd88734 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sample/SamplePayload.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sample/SamplePayload.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.eql.execution.sample; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.core.TimeValue; import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.eql.action.EqlSearchResponse.Event; @@ -19,8 +20,14 @@ class SamplePayload extends AbstractPayload { private final List values; - SamplePayload(List samples, List> docs, boolean timedOut, TimeValue timeTook) { - super(timedOut, timeTook); + SamplePayload( + List samples, + List> docs, + boolean timedOut, + TimeValue timeTook, + ShardSearchFailure[] shardFailures + ) { + super(timedOut, timeTook, shardFailures); values = new ArrayList<>(samples.size()); for (int i = 0; i < samples.size(); i++) { diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/BasicQueryClient.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/BasicQueryClient.java index 6cbe5298b5950..18623c17dcffb 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/BasicQueryClient.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/BasicQueryClient.java @@ -46,12 +46,14 @@ public class BasicQueryClient implements QueryClient { final Client client; final String[] indices; final List fetchFields; + private final boolean allowPartialSearchResults; public BasicQueryClient(EqlSession eqlSession) { this.cfg = eqlSession.configuration(); this.client = eqlSession.client(); this.indices = cfg.indices(); this.fetchFields = cfg.fetchFields(); + this.allowPartialSearchResults = cfg.allowPartialSearchResults(); } @Override @@ -60,11 +62,11 @@ public void query(QueryRequest request, ActionListener listener) // set query timeout searchSource.timeout(cfg.requestTimeout()); - SearchRequest search = prepareRequest(searchSource, false, indices); - search(search, searchLogListener(listener, log)); + SearchRequest search = prepareRequest(searchSource, false, allowPartialSearchResults, indices); + search(search, allowPartialSearchResults, searchLogListener(listener, log, allowPartialSearchResults)); } - protected void search(SearchRequest search, ActionListener listener) { + protected void search(SearchRequest search, boolean allowPartialSearchResults, ActionListener listener) { if (cfg.isCancelled()) { listener.onFailure(new TaskCancelledException("cancelled")); return; @@ -77,7 +79,7 @@ protected void search(SearchRequest search, ActionListener liste client.search(search, listener); } - protected void search(MultiSearchRequest search, ActionListener listener) { + protected void search(MultiSearchRequest search, boolean allowPartialSearchResults, ActionListener listener) { if (cfg.isCancelled()) { listener.onFailure(new TaskCancelledException("cancelled")); return; @@ -91,7 +93,7 @@ protected void search(MultiSearchRequest search, ActionListener> refs, ActionListener { + search(multiSearchBuilder.request(), allowPartialSearchResults, listener.delegateFailureAndWrap((delegate, r) -> { for (MultiSearchResponse.Item item : r.getResponses()) { // check for failures if (item.isFailure()) { @@ -187,6 +189,6 @@ public void multiQuery(List searches, ActionListener listener) { + protected void search(SearchRequest search, boolean allowPartialSearchResults, ActionListener listener) { // no pitId, ask for one if (pitId == null) { - openPIT(listener, () -> searchWithPIT(search, listener)); + openPIT(listener, () -> searchWithPIT(search, listener, allowPartialSearchResults), allowPartialSearchResults); } else { - searchWithPIT(search, listener); + searchWithPIT(search, listener, allowPartialSearchResults); } } - private void searchWithPIT(SearchRequest request, ActionListener listener) { + private void searchWithPIT(SearchRequest request, ActionListener listener, boolean allowPartialSearchResults) { makeRequestPITCompatible(request); // get the pid on each response - super.search(request, pitListener(SearchResponse::pointInTimeId, listener)); + super.search(request, allowPartialSearchResults, pitListener(SearchResponse::pointInTimeId, listener)); } @Override - protected void search(MultiSearchRequest search, ActionListener listener) { + protected void search(MultiSearchRequest search, boolean allowPartialSearchResults, ActionListener listener) { // no pitId, ask for one if (pitId == null) { - openPIT(listener, () -> searchWithPIT(search, listener)); + openPIT(listener, () -> searchWithPIT(search, allowPartialSearchResults, listener), allowPartialSearchResults); } else { - searchWithPIT(search, listener); + searchWithPIT(search, allowPartialSearchResults, listener); } } - private void searchWithPIT(MultiSearchRequest search, ActionListener listener) { + private void searchWithPIT(MultiSearchRequest search, boolean allowPartialSearchResults, ActionListener listener) { for (SearchRequest request : search.requests()) { makeRequestPITCompatible(request); } // get the pid on each request - super.search(search, pitListener(r -> { + super.search(search, allowPartialSearchResults, pitListener(r -> { // get pid for (MultiSearchResponse.Item item : r.getResponses()) { // pick the first non-failing response @@ -135,9 +135,10 @@ private ActionListener pitListener( ); } - private void openPIT(ActionListener listener, Runnable runnable) { + private void openPIT(ActionListener listener, Runnable runnable, boolean allowPartialSearchResults) { OpenPointInTimeRequest request = new OpenPointInTimeRequest(indices).indicesOptions(IndexResolver.FIELD_CAPS_INDICES_OPTIONS) - .keepAlive(keepAlive); + .keepAlive(keepAlive) + .allowPartialSearchResults(allowPartialSearchResults); request.indexFilter(filter); client.execute(TransportOpenPointInTimeAction.TYPE, request, listener.delegateFailureAndWrap((l, r) -> { pitId = r.getPointInTimeId(); diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/RuntimeUtils.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/RuntimeUtils.java index 40f7f7139efa1..92af8c562f840 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/RuntimeUtils.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/RuntimeUtils.java @@ -56,10 +56,14 @@ public final class RuntimeUtils { private RuntimeUtils() {} - public static ActionListener searchLogListener(ActionListener listener, Logger log) { + public static ActionListener searchLogListener( + ActionListener listener, + Logger log, + boolean allowPartialResults + ) { return listener.delegateFailureAndWrap((delegate, response) -> { ShardSearchFailure[] failures = response.getShardFailures(); - if (CollectionUtils.isEmpty(failures) == false) { + if (CollectionUtils.isEmpty(failures) == false && allowPartialResults == false) { delegate.onFailure(new EqlIllegalArgumentException(failures[0].reason(), failures[0].getCause())); return; } @@ -70,16 +74,22 @@ public static ActionListener searchLogListener(ActionListener multiSearchLogListener(ActionListener listener, Logger log) { + public static ActionListener multiSearchLogListener( + ActionListener listener, + boolean allowPartialSearchResults, + Logger log + ) { return listener.delegateFailureAndWrap((delegate, items) -> { for (MultiSearchResponse.Item item : items) { Exception failure = item.getFailure(); SearchResponse response = item.getResponse(); if (failure == null) { - ShardSearchFailure[] failures = response.getShardFailures(); - if (CollectionUtils.isEmpty(failures) == false) { - failure = new EqlIllegalArgumentException(failures[0].reason(), failures[0].getCause()); + if (allowPartialSearchResults == false) { + ShardSearchFailure[] failures = response.getShardFailures(); + if (CollectionUtils.isEmpty(failures) == false) { + failure = new EqlIllegalArgumentException(failures[0].reason(), failures[0].getCause()); + } } } if (failure != null) { @@ -170,11 +180,16 @@ public static HitExtractor createExtractor(FieldExtraction ref, EqlConfiguration throw new EqlIllegalArgumentException("Unexpected value reference {}", ref.getClass()); } - public static SearchRequest prepareRequest(SearchSourceBuilder source, boolean includeFrozen, String... indices) { + public static SearchRequest prepareRequest( + SearchSourceBuilder source, + boolean includeFrozen, + boolean allowPartialSearchResults, + String... indices + ) { SearchRequest searchRequest = new SearchRequest(); searchRequest.indices(indices); searchRequest.source(source); - searchRequest.allowPartialSearchResults(false); + searchRequest.allowPartialSearchResults(allowPartialSearchResults); searchRequest.indicesOptions( includeFrozen ? IndexResolver.FIELD_CAPS_FROZEN_INDICES_OPTIONS : IndexResolver.FIELD_CAPS_INDICES_OPTIONS ); diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sequence/SequencePayload.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sequence/SequencePayload.java index 45083babddbb4..b4a8edc79b3ad 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sequence/SequencePayload.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sequence/SequencePayload.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.eql.execution.sequence; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.core.TimeValue; import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.eql.action.EqlSearchResponse.Event; @@ -19,8 +20,14 @@ class SequencePayload extends AbstractPayload { private final List values; - SequencePayload(List sequences, List> docs, boolean timedOut, TimeValue timeTook) { - super(timedOut, timeTook); + SequencePayload( + List sequences, + List> docs, + boolean timedOut, + TimeValue timeTook, + ShardSearchFailure[] shardFailures + ) { + super(timedOut, timeTook, shardFailures); values = new ArrayList<>(sequences.size()); for (int i = 0; i < sequences.size(); i++) { diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sequence/TumblingWindow.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sequence/TumblingWindow.java index eabf6df518ad4..fac8788db0f95 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sequence/TumblingWindow.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/sequence/TumblingWindow.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.search.MultiSearchResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.common.Strings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; @@ -41,6 +42,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -51,6 +53,7 @@ import static org.elasticsearch.action.ActionListener.runAfter; import static org.elasticsearch.xpack.eql.execution.ExecutionUtils.copySource; import static org.elasticsearch.xpack.eql.execution.search.RuntimeUtils.combineFilters; +import static org.elasticsearch.xpack.eql.util.SearchHitUtils.addShardFailures; import static org.elasticsearch.xpack.eql.util.SearchHitUtils.qualifiedIndex; /** @@ -103,6 +106,9 @@ protected boolean removeEldestEntry(Map.Entry eldest) { private final boolean hasKeys; private final List> listOfKeys; + private final boolean allowPartialSearchResults; + private final boolean allowPartialSequenceResults; + private Map shardFailures = new HashMap<>(); // flag used for DESC sequences to indicate whether // the window needs to restart (since the DESC query still has results) @@ -127,7 +133,10 @@ public TumblingWindow( List criteria, SequenceCriterion until, SequenceMatcher matcher, - List> listOfKeys + List> listOfKeys, + boolean allowPartialSearchResults, + boolean allowPartialSequenceResults + ) { this.client = client; @@ -141,6 +150,8 @@ public TumblingWindow( this.hasKeys = baseRequest.keySize() > 0; this.restartWindowFromTailQuery = baseRequest.descending(); this.listOfKeys = listOfKeys; + this.allowPartialSearchResults = allowPartialSearchResults; + this.allowPartialSequenceResults = allowPartialSequenceResults; } @Override @@ -158,6 +169,9 @@ public void execute(ActionListener listener) { * Move the window while preserving the same base. */ private void tumbleWindow(int currentStage, ActionListener listener) { + if (allowPartialSequenceResults == false && shardFailures.isEmpty() == false) { + doPayload(listener); + } if (currentStage > matcher.firstPositiveStage && matcher.hasCandidates() == false) { if (restartWindowFromTailQuery) { currentStage = matcher.firstPositiveStage; @@ -224,6 +238,9 @@ public void checkMissingEvents(Runnable next, ActionListener listener) private void doCheckMissingEvents(List batchToCheck, MultiSearchResponse p, ActionListener listener, Runnable next) { MultiSearchResponse.Item[] responses = p.getResponses(); + for (MultiSearchResponse.Item response : responses) { + addShardFailures(shardFailures, response.getResponse()); + } int nextResponse = 0; for (Sequence sequence : batchToCheck) { boolean leading = true; @@ -316,7 +333,14 @@ private List prepareQueryForMissingEvents(List toCheck) } addKeyFilter(i, sequence, builder); RuntimeUtils.combineFilters(builder, range); - result.add(RuntimeUtils.prepareRequest(builder.size(1).trackTotalHits(false), false, Strings.EMPTY_ARRAY)); + result.add( + RuntimeUtils.prepareRequest( + builder.size(1).trackTotalHits(false), + false, + allowPartialSearchResults, + Strings.EMPTY_ARRAY + ) + ); } else { leading = false; } @@ -361,6 +385,7 @@ private void advance(int stage, ActionListener listener) { * Execute the base query. */ private void baseCriterion(int baseStage, SearchResponse r, ActionListener listener) { + addShardFailures(shardFailures, r); SequenceCriterion base = criteria.get(baseStage); SearchHits hits = r.getHits(); @@ -731,8 +756,10 @@ private void doPayload(ActionListener listener) { log.trace("Sending payload for [{}] sequences", completed.size()); - if (completed.isEmpty()) { - listener.onResponse(new EmptyPayload(Type.SEQUENCE, timeTook())); + if (completed.isEmpty() || (allowPartialSequenceResults == false && shardFailures.isEmpty() == false)) { + listener.onResponse( + new EmptyPayload(Type.SEQUENCE, timeTook(), shardFailures.values().toArray(new ShardSearchFailure[shardFailures.size()])) + ); return; } @@ -741,7 +768,13 @@ private void doPayload(ActionListener listener) { if (criteria.get(matcher.firstPositiveStage).descending()) { Collections.reverse(completed); } - return new SequencePayload(completed, addMissingEventPlaceholders(listOfHits), false, timeTook()); + return new SequencePayload( + completed, + addMissingEventPlaceholders(listOfHits), + false, + timeTook(), + shardFailures.values().toArray(new ShardSearchFailure[0]) + ); })); } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlPlugin.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlPlugin.java index 084a5e74a47e8..210f88c991539 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlPlugin.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlPlugin.java @@ -60,6 +60,20 @@ public class EqlPlugin extends Plugin implements ActionPlugin, CircuitBreakerPlu Setting.Property.DeprecatedWarning ); + public static final Setting DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS = Setting.boolSetting( + "xpack.eql.default_allow_partial_results", + false, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); + + public static final Setting DEFAULT_ALLOW_PARTIAL_SEQUENCE_RESULTS = Setting.boolSetting( + "xpack.eql.default_allow_partial_sequence_results", + false, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); + public EqlPlugin() {} @Override @@ -86,7 +100,7 @@ private Collection createComponents(Client client, Settings settings, Cl */ @Override public List> getSettings() { - return List.of(EQL_ENABLED_SETTING); + return List.of(EQL_ENABLED_SETTING, DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS, DEFAULT_ALLOW_PARTIAL_SEQUENCE_RESULTS); } @Override diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java index e24a4749f45cd..65def24563e5e 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java @@ -64,6 +64,12 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } eqlRequest.keepOnCompletion(request.paramAsBoolean("keep_on_completion", eqlRequest.keepOnCompletion())); eqlRequest.ccsMinimizeRoundtrips(request.paramAsBoolean("ccs_minimize_roundtrips", eqlRequest.ccsMinimizeRoundtrips())); + eqlRequest.allowPartialSearchResults( + request.paramAsBoolean("allow_partial_search_results", eqlRequest.allowPartialSearchResults()) + ); + eqlRequest.allowPartialSequenceResults( + request.paramAsBoolean("allow_partial_sequence_results", eqlRequest.allowPartialSequenceResults()) + ); } return channel -> { diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/TransportEqlSearchAction.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/TransportEqlSearchAction.java index c0141da2432ce..582352722fc58 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/TransportEqlSearchAction.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/TransportEqlSearchAction.java @@ -10,6 +10,7 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionListenerResponseHandler; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.client.internal.Client; @@ -144,7 +145,8 @@ public EqlSearchResponse initialResponse(EqlSearchTask task) { false, task.getExecutionId().getEncoded(), true, - true + true, + ShardSearchFailure.EMPTY_ARRAY ); } @@ -231,6 +233,12 @@ public static void operation( request.indicesOptions(), request.fetchSize(), request.maxSamplesPerKey(), + request.allowPartialSearchResults() == null + ? defaultAllowPartialSearchResults(clusterService) + : request.allowPartialSearchResults(), + request.allowPartialSequenceResults() == null + ? defaultAllowPartialSequenceResults(clusterService) + : request.allowPartialSequenceResults(), clientId, new TaskId(nodeId, task.getId()), task @@ -244,12 +252,34 @@ public static void operation( } } + private static boolean defaultAllowPartialSearchResults(ClusterService clusterService) { + if (clusterService.getClusterSettings() == null) { + return EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS.getDefault(Settings.EMPTY); + } + return clusterService.getClusterSettings().get(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS); + } + + private static boolean defaultAllowPartialSequenceResults(ClusterService clusterService) { + if (clusterService.getClusterSettings() == null) { + return EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEQUENCE_RESULTS.getDefault(Settings.EMPTY); + } + return clusterService.getClusterSettings().get(EqlPlugin.DEFAULT_ALLOW_PARTIAL_SEQUENCE_RESULTS); + } + static EqlSearchResponse createResponse(Results results, AsyncExecutionId id) { EqlSearchResponse.Hits hits = new EqlSearchResponse.Hits(results.events(), results.sequences(), results.totalHits()); if (id != null) { - return new EqlSearchResponse(hits, results.tookTime().getMillis(), results.timedOut(), id.getEncoded(), false, false); + return new EqlSearchResponse( + hits, + results.tookTime().getMillis(), + results.timedOut(), + id.getEncoded(), + false, + false, + results.shardFailures() + ); } else { - return new EqlSearchResponse(hits, results.tookTime().getMillis(), results.timedOut()); + return new EqlSearchResponse(hits, results.tookTime().getMillis(), results.timedOut(), results.shardFailures()); } } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EmptyPayload.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EmptyPayload.java index 9822285465087..33ed5799cd073 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EmptyPayload.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EmptyPayload.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.eql.session; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.core.TimeValue; import java.util.List; @@ -17,14 +18,16 @@ public class EmptyPayload implements Payload { private final Type type; private final TimeValue timeTook; + private final ShardSearchFailure[] shardFailures; public EmptyPayload(Type type) { - this(type, TimeValue.ZERO); + this(type, TimeValue.ZERO, ShardSearchFailure.EMPTY_ARRAY); } - public EmptyPayload(Type type, TimeValue timeTook) { + public EmptyPayload(Type type, TimeValue timeTook, ShardSearchFailure[] shardFailures) { this.type = type; this.timeTook = timeTook; + this.shardFailures = shardFailures; } @Override @@ -46,4 +49,10 @@ public TimeValue timeTook() { public List values() { return emptyList(); } + + @Override + public ShardSearchFailure[] shardFailures() { + return shardFailures; + } + } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EqlConfiguration.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EqlConfiguration.java index 8dd8220fb63bc..8242b0b533ad3 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EqlConfiguration.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EqlConfiguration.java @@ -30,6 +30,8 @@ public class EqlConfiguration extends org.elasticsearch.xpack.ql.session.Configu private final EqlSearchTask task; private final int fetchSize; private final int maxSamplesPerKey; + private final boolean allowPartialSearchResults; + private final boolean allowPartialSequenceResults; @Nullable private final QueryBuilder filter; @@ -50,6 +52,8 @@ public EqlConfiguration( IndicesOptions indicesOptions, int fetchSize, int maxSamplesPerKey, + boolean allowPartialSearchResults, + boolean allowPartialSequenceResults, String clientId, TaskId taskId, EqlSearchTask task @@ -67,6 +71,8 @@ public EqlConfiguration( this.task = task; this.fetchSize = fetchSize; this.maxSamplesPerKey = maxSamplesPerKey; + this.allowPartialSearchResults = allowPartialSearchResults; + this.allowPartialSequenceResults = allowPartialSequenceResults; } public String[] indices() { @@ -89,6 +95,14 @@ public int maxSamplesPerKey() { return maxSamplesPerKey; } + public boolean allowPartialSearchResults() { + return allowPartialSearchResults; + } + + public boolean allowPartialSequenceResults() { + return allowPartialSequenceResults; + } + public QueryBuilder filter() { return filter; } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/Payload.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/Payload.java index 1d82478e6db26..05e614714a5aa 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/Payload.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/Payload.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.eql.session; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.core.TimeValue; import java.util.List; @@ -29,4 +30,6 @@ enum Type { TimeValue timeTook(); List values(); + + ShardSearchFailure[] shardFailures(); } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/Results.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/Results.java index bb76c08c801cb..13886470f21f5 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/Results.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/Results.java @@ -9,6 +9,7 @@ import org.apache.lucene.search.TotalHits; import org.apache.lucene.search.TotalHits.Relation; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.eql.action.EqlSearchResponse.Event; import org.elasticsearch.xpack.eql.action.EqlSearchResponse.Sequence; @@ -23,18 +24,28 @@ public class Results { private final boolean timedOut; private final TimeValue tookTime; private final Type type; + private ShardSearchFailure[] shardFailures; public static Results fromPayload(Payload payload) { List values = payload.values(); - return new Results(new TotalHits(values.size(), Relation.EQUAL_TO), payload.timeTook(), false, values, payload.resultType()); + payload.shardFailures(); + return new Results( + new TotalHits(values.size(), Relation.EQUAL_TO), + payload.timeTook(), + false, + values, + payload.resultType(), + payload.shardFailures() + ); } - Results(TotalHits totalHits, TimeValue tookTime, boolean timedOut, List results, Type type) { + Results(TotalHits totalHits, TimeValue tookTime, boolean timedOut, List results, Type type, ShardSearchFailure[] shardFailures) { this.totalHits = totalHits; this.tookTime = tookTime; this.timedOut = timedOut; this.results = results; this.type = type; + this.shardFailures = shardFailures; } public TotalHits totalHits() { @@ -51,6 +62,10 @@ public List sequences() { return (type == Type.SEQUENCE || type == Type.SAMPLE) ? (List) results : null; } + public ShardSearchFailure[] shardFailures() { + return shardFailures; + } + public TimeValue tookTime() { return tookTime; } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/util/SearchHitUtils.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/util/SearchHitUtils.java index 91795ac15b53e..2b5ec9718cfc4 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/util/SearchHitUtils.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/util/SearchHitUtils.java @@ -7,8 +7,12 @@ package org.elasticsearch.xpack.eql.util; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.search.SearchHit; +import java.util.Map; + import static org.elasticsearch.transport.RemoteClusterAware.buildRemoteIndexName; public final class SearchHitUtils { @@ -16,4 +20,12 @@ public final class SearchHitUtils { public static String qualifiedIndex(SearchHit hit) { return buildRemoteIndexName(hit.getClusterAlias(), hit.getIndex()); } + + public static void addShardFailures(Map shardFailures, SearchResponse r) { + if (r.getShardFailures() != null) { + for (ShardSearchFailure shardFailure : r.getShardFailures()) { + shardFailures.put(shardFailure.toString(), shardFailure); + } + } + } } diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/EqlTestUtils.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/EqlTestUtils.java index a1aa8e4bd98d7..75884fab4dbb3 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/EqlTestUtils.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/EqlTestUtils.java @@ -51,6 +51,8 @@ private EqlTestUtils() {} null, 123, 1, + false, + true, "", new TaskId("test", 123), null @@ -69,6 +71,8 @@ public static EqlConfiguration randomConfiguration() { randomIndicesOptions(), randomIntBetween(1, 1000), randomIntBetween(1, 1000), + randomBoolean(), + randomBoolean(), randomAlphaOfLength(16), new TaskId(randomAlphaOfLength(10), randomNonNegativeLong()), randomTask() diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/EqlSearchRequestTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/EqlSearchRequestTests.java index 0ff9fa9131b27..1a06aead910c8 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/EqlSearchRequestTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/EqlSearchRequestTests.java @@ -80,6 +80,8 @@ protected EqlSearchRequest createTestInstance() { .waitForCompletionTimeout(randomTimeValue()) .keepAlive(randomTimeValue()) .keepOnCompletion(randomBoolean()) + .allowPartialSearchResults(randomBoolean()) + .allowPartialSequenceResults(randomBoolean()) .fetchFields(randomFetchFields) .runtimeMappings(randomRuntimeMappings()) .resultPosition(randomFrom("tail", "head")) @@ -136,6 +138,12 @@ protected EqlSearchRequest mutateInstanceForVersion(EqlSearchRequest instance, T mutatedInstance.runtimeMappings(version.onOrAfter(TransportVersions.V_7_13_0) ? instance.runtimeMappings() : emptyMap()); mutatedInstance.resultPosition(version.onOrAfter(TransportVersions.V_7_17_8) ? instance.resultPosition() : "tail"); mutatedInstance.maxSamplesPerKey(version.onOrAfter(TransportVersions.V_8_7_0) ? instance.maxSamplesPerKey() : 1); + mutatedInstance.allowPartialSearchResults( + version.onOrAfter(TransportVersions.EQL_ALLOW_PARTIAL_SEARCH_RESULTS) ? instance.allowPartialSearchResults() : false + ); + mutatedInstance.allowPartialSequenceResults( + version.onOrAfter(TransportVersions.EQL_ALLOW_PARTIAL_SEARCH_RESULTS) ? instance.allowPartialSequenceResults() : false + ); return mutatedInstance; } diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/EqlSearchResponseTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/EqlSearchResponseTests.java index 6cb283d11848e..fa118a5256df1 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/EqlSearchResponseTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/EqlSearchResponseTests.java @@ -9,6 +9,7 @@ import org.apache.lucene.search.TotalHits; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; +import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.document.DocumentField; @@ -190,7 +191,7 @@ public static EqlSearchResponse createRandomEventsResponse(TotalHits totalHits, hits = new EqlSearchResponse.Hits(randomEvents(xType), null, totalHits); } if (randomBoolean()) { - return new EqlSearchResponse(hits, randomIntBetween(0, 1001), randomBoolean()); + return new EqlSearchResponse(hits, randomIntBetween(0, 1001), randomBoolean(), ShardSearchFailure.EMPTY_ARRAY); } else { return new EqlSearchResponse( hits, @@ -198,7 +199,8 @@ public static EqlSearchResponse createRandomEventsResponse(TotalHits totalHits, randomBoolean(), randomAlphaOfLength(10), randomBoolean(), - randomBoolean() + randomBoolean(), + ShardSearchFailure.EMPTY_ARRAY ); } } @@ -222,7 +224,7 @@ public static EqlSearchResponse createRandomSequencesResponse(TotalHits totalHit hits = new EqlSearchResponse.Hits(null, seq, totalHits); } if (randomBoolean()) { - return new EqlSearchResponse(hits, randomIntBetween(0, 1001), randomBoolean()); + return new EqlSearchResponse(hits, randomIntBetween(0, 1001), randomBoolean(), ShardSearchFailure.EMPTY_ARRAY); } else { return new EqlSearchResponse( hits, @@ -230,7 +232,8 @@ public static EqlSearchResponse createRandomSequencesResponse(TotalHits totalHit randomBoolean(), randomAlphaOfLength(10), randomBoolean(), - randomBoolean() + randomBoolean(), + ShardSearchFailure.EMPTY_ARRAY ); } } @@ -273,7 +276,8 @@ protected EqlSearchResponse mutateInstanceForVersion(EqlSearchResponse instance, instance.isTimeout(), instance.id(), instance.isRunning(), - instance.isPartial() + instance.isPartial(), + ShardSearchFailure.EMPTY_ARRAY ); } diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/LocalStateEQLXPackPlugin.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/LocalStateEQLXPackPlugin.java index 4d5201f544d72..33573b99546fb 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/LocalStateEQLXPackPlugin.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/LocalStateEQLXPackPlugin.java @@ -7,26 +7,41 @@ package org.elasticsearch.xpack.eql.action; +import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.indices.breaker.BreakerSettings; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.plugins.CircuitBreakerPlugin; import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.eql.plugin.EqlPlugin; import org.elasticsearch.xpack.ql.plugin.QlPlugin; import java.nio.file.Path; -public class LocalStateEQLXPackPlugin extends LocalStateCompositeXPackPlugin { +public class LocalStateEQLXPackPlugin extends LocalStateCompositeXPackPlugin implements CircuitBreakerPlugin { + + private final EqlPlugin eqlPlugin; public LocalStateEQLXPackPlugin(final Settings settings, final Path configPath) { super(settings, configPath); LocalStateEQLXPackPlugin thisVar = this; - plugins.add(new EqlPlugin() { + this.eqlPlugin = new EqlPlugin() { @Override protected XPackLicenseState getLicenseState() { return thisVar.getLicenseState(); } - }); + }; + plugins.add(eqlPlugin); plugins.add(new QlPlugin()); } + @Override + public BreakerSettings getCircuitBreaker(Settings settings) { + return eqlPlugin.getCircuitBreaker(settings); + } + + @Override + public void setCircuitBreaker(CircuitBreaker circuitBreaker) { + eqlPlugin.setCircuitBreaker(circuitBreaker); + } } diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/assembler/ImplicitTiebreakerTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/assembler/ImplicitTiebreakerTests.java index 7bb6a228f6e48..abd928b04a9c7 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/assembler/ImplicitTiebreakerTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/assembler/ImplicitTiebreakerTests.java @@ -141,7 +141,15 @@ public void testImplicitTiebreakerBeingSet() { booleanArrayOf(stages, false), NOOP_CIRCUIT_BREAKER ); - TumblingWindow window = new TumblingWindow(client, criteria, null, matcher, Collections.emptyList()); + TumblingWindow window = new TumblingWindow( + client, + criteria, + null, + matcher, + Collections.emptyList(), + randomBoolean(), + randomBoolean() + ); window.execute(wrap(p -> {}, ex -> { throw ExceptionsHelper.convertToRuntime(ex); })); } } diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/assembler/SequenceSpecTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/assembler/SequenceSpecTests.java index a8ed842e94c44..f6aa851b2fff0 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/assembler/SequenceSpecTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/assembler/SequenceSpecTests.java @@ -277,7 +277,15 @@ public void test() throws Exception { ); QueryClient testClient = new TestQueryClient(); - TumblingWindow window = new TumblingWindow(testClient, criteria, null, matcher, Collections.emptyList()); + TumblingWindow window = new TumblingWindow( + testClient, + criteria, + null, + matcher, + Collections.emptyList(), + randomBoolean(), + randomBoolean() + ); // finally make the assertion at the end of the listener window.execute(ActionTestUtils.assertNoFailureListener(this::checkResults)); diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java index dc132659417ff..80b1ff97b725d 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java @@ -89,7 +89,7 @@ public void query(QueryRequest r, ActionListener l) {} @Override public void fetchHits(Iterable> refs, ActionListener>> listener) {} - }, mockCriteria(), randomIntBetween(10, 500), new Limit(1000, 0), CIRCUIT_BREAKER, 1); + }, mockCriteria(), randomIntBetween(10, 500), new Limit(1000, 0), CIRCUIT_BREAKER, 1, randomBoolean()); CIRCUIT_BREAKER.startBreaking(); iterator.pushToStack(new SampleIterator.Page(CB_STACK_SIZE_PRECISION - 1)); @@ -142,7 +142,8 @@ public void fetchHits(Iterable> refs, ActionListener> refs, ActionListener { // do nothing, we don't care about the query results }, ex -> { fail("Shouldn't have failed"); })); diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sequence/CircuitBreakerTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sequence/CircuitBreakerTests.java index fe1fca45364e3..58448d981fcca 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sequence/CircuitBreakerTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sequence/CircuitBreakerTests.java @@ -146,7 +146,15 @@ public void testCircuitBreakerTumblingWindow() { booleanArrayOf(stages, false), CIRCUIT_BREAKER ); - TumblingWindow window = new TumblingWindow(client, criteria, null, matcher, Collections.emptyList()); + TumblingWindow window = new TumblingWindow( + client, + criteria, + null, + matcher, + Collections.emptyList(), + randomBoolean(), + randomBoolean() + ); window.execute(ActionTestUtils.assertNoFailureListener(p -> {})); CIRCUIT_BREAKER.startBreaking(); @@ -228,7 +236,15 @@ private void assertMemoryCleared( booleanArrayOf(sequenceFiltersCount, false), eqlCircuitBreaker ); - TumblingWindow window = new TumblingWindow(eqlClient, criteria, null, matcher, Collections.emptyList()); + TumblingWindow window = new TumblingWindow( + eqlClient, + criteria, + null, + matcher, + Collections.emptyList(), + randomBoolean(), + randomBoolean() + ); window.execute(ActionListener.noop()); assertTrue(esClient.searchRequestsRemainingCount() == 0); // ensure all the search requests have been asked for @@ -271,7 +287,15 @@ public void testEqlCBCleanedUp_on_ParentCBBreak() { booleanArrayOf(sequenceFiltersCount, false), eqlCircuitBreaker ); - TumblingWindow window = new TumblingWindow(eqlClient, criteria, null, matcher, Collections.emptyList()); + TumblingWindow window = new TumblingWindow( + eqlClient, + criteria, + null, + matcher, + Collections.emptyList(), + randomBoolean(), + randomBoolean() + ); window.execute(wrap(p -> fail(), ex -> assertTrue(ex instanceof CircuitBreakingException))); } assertCriticalWarnings("[indices.breaker.total.limit] setting of [0%] is below the recommended minimum of 50.0% of the heap"); @@ -329,6 +353,8 @@ private QueryClient buildQueryClient(ESMockClient esClient, CircuitBreaker eqlCi null, 123, 1, + randomBoolean(), + randomBoolean(), "", new TaskId("test", 123), new EqlSearchTask( diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sequence/PITFailureTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sequence/PITFailureTests.java index 1a2f00463b49b..2eee6a262e73c 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sequence/PITFailureTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sequence/PITFailureTests.java @@ -83,6 +83,8 @@ public void testHandlingPitFailure() { null, 123, 1, + randomBoolean(), + randomBoolean(), "", new TaskId("test", 123), new EqlSearchTask( @@ -132,7 +134,15 @@ public void testHandlingPitFailure() { ); SequenceMatcher matcher = new SequenceMatcher(1, false, TimeValue.MINUS_ONE, null, booleanArrayOf(1, false), cb); - TumblingWindow window = new TumblingWindow(eqlClient, criteria, null, matcher, Collections.emptyList()); + TumblingWindow window = new TumblingWindow( + eqlClient, + criteria, + null, + matcher, + Collections.emptyList(), + randomBoolean(), + randomBoolean() + ); window.execute( wrap( p -> { fail("Search succeeded despite PIT failure"); }, From 51eb386a70e20bf17b42f44dda3e6a9bdd37321c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20J=C3=B3zala?= <377355+jozala@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:48:57 +0100 Subject: [PATCH 034/119] [ci] Add ubuntu-2404-aarch64 to test matrix in platform-support (#118847) --- .buildkite/pipelines/periodic-platform-support.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.buildkite/pipelines/periodic-platform-support.yml b/.buildkite/pipelines/periodic-platform-support.yml index c5846a763f5e8..ea0d7b13b55b4 100644 --- a/.buildkite/pipelines/periodic-platform-support.yml +++ b/.buildkite/pipelines/periodic-platform-support.yml @@ -63,6 +63,7 @@ steps: image: - almalinux-8-aarch64 - ubuntu-2004-aarch64 + - ubuntu-2404-aarch64 GRADLE_TASK: - checkPart1 - checkPart2 From b8f4677254f7a0cd29c8ded0361f3ab201232cd5 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Tue, 17 Dec 2024 18:11:50 +0100 Subject: [PATCH 035/119] Replace build with scope parameter (#118643) This change replaces the build with scope parameter in order to make it cleaner when parameter is build and how it is used. --- .../org/elasticsearch/compute/ann/Fixed.java | 26 +++++++++++++------ .../compute/gen/EvaluatorImplementer.java | 15 ++++++++--- .../function/scalar/convert/FromBase64.java | 3 ++- .../function/scalar/convert/ToBase64.java | 3 ++- .../function/scalar/ip/IpPrefix.java | 3 ++- .../multivalue/MvPSeriesWeightedSum.java | 3 ++- .../scalar/multivalue/MvPercentile.java | 7 ++--- .../function/scalar/string/Concat.java | 3 ++- .../function/scalar/string/Left.java | 5 ++-- .../function/scalar/string/Repeat.java | 9 +++++-- .../function/scalar/string/Right.java | 5 ++-- .../function/scalar/string/Space.java | 3 ++- .../function/scalar/string/Split.java | 5 ++-- 13 files changed, 61 insertions(+), 29 deletions(-) diff --git a/x-pack/plugin/esql/compute/ann/src/main/java/org/elasticsearch/compute/ann/Fixed.java b/x-pack/plugin/esql/compute/ann/src/main/java/org/elasticsearch/compute/ann/Fixed.java index 62703fa400ff7..1f10abf3b9fb0 100644 --- a/x-pack/plugin/esql/compute/ann/src/main/java/org/elasticsearch/compute/ann/Fixed.java +++ b/x-pack/plugin/esql/compute/ann/src/main/java/org/elasticsearch/compute/ann/Fixed.java @@ -11,7 +11,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.function.Function; /** * Used on parameters on methods annotated with {@link Evaluator} to indicate @@ -27,12 +26,23 @@ boolean includeInToString() default true; /** - * Should the Evaluator's factory build this per evaluator with a - * {@code Function} or just take fixed implementation? - * This is typically set to {@code true} to use the {@link Function} - * to make "scratch" objects which have to be isolated in a single thread. - * This is typically set to {@code false} when the parameter is simply - * immutable and can be shared. + * Defines the scope of the parameter. + * - SINGLETON (default) will build a single instance and share it across all evaluators + * - THREAD_LOCAL will build a new instance for each evaluator thread */ - boolean build() default false; + Scope scope() default Scope.SINGLETON; + + /** + * Defines the parameter scope + */ + enum Scope { + /** + * Should be used for immutable parameters that can be shared across different threads + */ + SINGLETON, + /** + * Should be used for mutable or not thread safe parameters + */ + THREAD_LOCAL, + } } diff --git a/x-pack/plugin/esql/compute/gen/src/main/java/org/elasticsearch/compute/gen/EvaluatorImplementer.java b/x-pack/plugin/esql/compute/gen/src/main/java/org/elasticsearch/compute/gen/EvaluatorImplementer.java index 5869eff23a9ab..b4a0cf9127f23 100644 --- a/x-pack/plugin/esql/compute/gen/src/main/java/org/elasticsearch/compute/gen/EvaluatorImplementer.java +++ b/x-pack/plugin/esql/compute/gen/src/main/java/org/elasticsearch/compute/gen/EvaluatorImplementer.java @@ -16,6 +16,7 @@ import com.squareup.javapoet.TypeSpec; import org.elasticsearch.compute.ann.Fixed; +import org.elasticsearch.compute.ann.Fixed.Scope; import java.util.ArrayList; import java.util.Arrays; @@ -725,7 +726,7 @@ public String closeInvocation() { } } - private record FixedProcessFunctionArg(TypeName type, String name, boolean includeInToString, boolean build, boolean releasable) + private record FixedProcessFunctionArg(TypeName type, String name, boolean includeInToString, Scope scope, boolean releasable) implements ProcessFunctionArg { @Override @@ -762,12 +763,18 @@ public void implementFactoryCtor(MethodSpec.Builder builder) { } private TypeName factoryFieldType() { - return build ? ParameterizedTypeName.get(ClassName.get(Function.class), DRIVER_CONTEXT, type.box()) : type; + return switch (scope) { + case SINGLETON -> type; + case THREAD_LOCAL -> ParameterizedTypeName.get(ClassName.get(Function.class), DRIVER_CONTEXT, type.box()); + }; } @Override public String factoryInvocation(MethodSpec.Builder factoryMethodBuilder) { - return build ? name + ".apply(context)" : name; + return switch (scope) { + case SINGLETON -> name; + case THREAD_LOCAL -> name + ".apply(context)"; + }; } @Override @@ -1020,7 +1027,7 @@ private ProcessFunction( type, name, fixed.includeInToString(), - fixed.build(), + fixed.scope(), Types.extendsSuper(types, v.asType(), "org.elasticsearch.core.Releasable") ) ); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64.java index 7f9d0d3f2e647..832c511a2dc50 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64.java @@ -30,6 +30,7 @@ import java.util.Base64; import java.util.List; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; @@ -85,7 +86,7 @@ protected NodeInfo info() { } @Evaluator() - static BytesRef process(BytesRef field, @Fixed(includeInToString = false, build = true) BytesRefBuilder oScratch) { + static BytesRef process(BytesRef field, @Fixed(includeInToString = false, scope = THREAD_LOCAL) BytesRefBuilder oScratch) { byte[] bytes = new byte[field.length]; System.arraycopy(field.bytes, field.offset, bytes, 0, field.length); oScratch.grow(field.length); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64.java index c23cef31f32f5..e78968bb209b6 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64.java @@ -30,6 +30,7 @@ import java.util.Base64; import java.util.List; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; @@ -78,7 +79,7 @@ protected NodeInfo info() { } @Evaluator(warnExceptions = { ArithmeticException.class }) - static BytesRef process(BytesRef field, @Fixed(includeInToString = false, build = true) BytesRefBuilder oScratch) { + static BytesRef process(BytesRef field, @Fixed(includeInToString = false, scope = THREAD_LOCAL) BytesRefBuilder oScratch) { int outLength = Math.multiplyExact(4, (Math.addExact(field.length, 2) / 3)); byte[] bytes = new byte[field.length]; System.arraycopy(field.bytes, field.offset, bytes, 0, field.length); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ip/IpPrefix.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ip/IpPrefix.java index 26e75e752f681..5fc61c5c07b58 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ip/IpPrefix.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ip/IpPrefix.java @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; @@ -138,7 +139,7 @@ static BytesRef process( BytesRef ip, int prefixLengthV4, int prefixLengthV6, - @Fixed(includeInToString = false, build = true) BytesRef scratch + @Fixed(includeInToString = false, scope = THREAD_LOCAL) BytesRef scratch ) { if (prefixLengthV4 < 0 || prefixLengthV4 > 32) { throw new IllegalArgumentException("Prefix length v4 must be in range [0, 32], found " + prefixLengthV4); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvPSeriesWeightedSum.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvPSeriesWeightedSum.java index cf49607893aae..4dd447f938880 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvPSeriesWeightedSum.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvPSeriesWeightedSum.java @@ -33,6 +33,7 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isFoldable; @@ -144,7 +145,7 @@ static void process( DoubleBlock.Builder builder, int position, DoubleBlock block, - @Fixed(includeInToString = false, build = true) CompensatedSum sum, + @Fixed(includeInToString = false, scope = THREAD_LOCAL) CompensatedSum sum, @Fixed double p ) { sum.reset(0, 0); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvPercentile.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvPercentile.java index f3a63c835bd34..4e4aee307f1c7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvPercentile.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvPercentile.java @@ -35,6 +35,7 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; @@ -167,7 +168,7 @@ static void process( int position, DoubleBlock values, double percentile, - @Fixed(includeInToString = false, build = true) DoubleSortingScratch scratch + @Fixed(includeInToString = false, scope = THREAD_LOCAL) DoubleSortingScratch scratch ) { int valueCount = values.getValueCount(position); int firstValueIndex = values.getFirstValueIndex(position); @@ -190,7 +191,7 @@ static void process( int position, IntBlock values, double percentile, - @Fixed(includeInToString = false, build = true) IntSortingScratch scratch + @Fixed(includeInToString = false, scope = THREAD_LOCAL) IntSortingScratch scratch ) { int valueCount = values.getValueCount(position); int firstValueIndex = values.getFirstValueIndex(position); @@ -213,7 +214,7 @@ static void process( int position, LongBlock values, double percentile, - @Fixed(includeInToString = false, build = true) LongSortingScratch scratch + @Fixed(includeInToString = false, scope = THREAD_LOCAL) LongSortingScratch scratch ) { int valueCount = values.getValueCount(position); int firstValueIndex = values.getFirstValueIndex(position); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Concat.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Concat.java index 46ecc9e026d3d..eb173029876d3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Concat.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Concat.java @@ -32,6 +32,7 @@ import java.util.stream.Stream; import static org.elasticsearch.common.unit.ByteSizeUnit.MB; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; @@ -111,7 +112,7 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { } @Evaluator - static BytesRef process(@Fixed(includeInToString = false, build = true) BreakingBytesRefBuilder scratch, BytesRef[] values) { + static BytesRef process(@Fixed(includeInToString = false, scope = THREAD_LOCAL) BreakingBytesRefBuilder scratch, BytesRef[] values) { scratch.grow(checkedTotalLength(values)); scratch.clear(); for (int i = 0; i < values.length; i++) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Left.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Left.java index e7572caafd8f5..0d885e3f3c341 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Left.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Left.java @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; @@ -77,8 +78,8 @@ public String getWriteableName() { @Evaluator static BytesRef process( - @Fixed(includeInToString = false, build = true) BytesRef out, - @Fixed(includeInToString = false, build = true) UnicodeUtil.UTF8CodePoint cp, + @Fixed(includeInToString = false, scope = THREAD_LOCAL) BytesRef out, + @Fixed(includeInToString = false, scope = THREAD_LOCAL) UnicodeUtil.UTF8CodePoint cp, BytesRef str, int length ) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Repeat.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Repeat.java index 2cc14399df2ae..e91f03de3dd7e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Repeat.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Repeat.java @@ -31,6 +31,7 @@ import java.util.List; import static org.elasticsearch.common.unit.ByteSizeUnit.MB; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; @@ -101,7 +102,7 @@ public boolean foldable() { @Evaluator(extraName = "Constant", warnExceptions = { IllegalArgumentException.class }) static BytesRef processConstantNumber( - @Fixed(includeInToString = false, build = true) BreakingBytesRefBuilder scratch, + @Fixed(includeInToString = false, scope = THREAD_LOCAL) BreakingBytesRefBuilder scratch, BytesRef str, @Fixed int number ) { @@ -109,7 +110,11 @@ static BytesRef processConstantNumber( } @Evaluator(warnExceptions = { IllegalArgumentException.class }) - static BytesRef process(@Fixed(includeInToString = false, build = true) BreakingBytesRefBuilder scratch, BytesRef str, int number) { + static BytesRef process( + @Fixed(includeInToString = false, scope = THREAD_LOCAL) BreakingBytesRefBuilder scratch, + BytesRef str, + int number + ) { if (number < 0) { throw new IllegalArgumentException("Number parameter cannot be negative, found [" + number + "]"); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Right.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Right.java index b069b984ea81e..e0ebed29cca72 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Right.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Right.java @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.List; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; @@ -77,8 +78,8 @@ public String getWriteableName() { @Evaluator static BytesRef process( - @Fixed(includeInToString = false, build = true) BytesRef out, - @Fixed(includeInToString = false, build = true) UnicodeUtil.UTF8CodePoint cp, + @Fixed(includeInToString = false, scope = THREAD_LOCAL) BytesRef out, + @Fixed(includeInToString = false, scope = THREAD_LOCAL) UnicodeUtil.UTF8CodePoint cp, BytesRef str, int length ) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Space.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Space.java index 6481ce5764e1f..3b9a466966911 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Space.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Space.java @@ -31,6 +31,7 @@ import java.util.List; import static org.elasticsearch.common.unit.ByteSizeUnit.MB; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; @@ -82,7 +83,7 @@ protected TypeResolution resolveType() { } @Evaluator(warnExceptions = { IllegalArgumentException.class }) - static BytesRef process(@Fixed(includeInToString = false, build = true) BreakingBytesRefBuilder scratch, int number) { + static BytesRef process(@Fixed(includeInToString = false, scope = THREAD_LOCAL) BreakingBytesRefBuilder scratch, int number) { checkNumber(number); scratch.grow(number); scratch.setLength(number); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Split.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Split.java index b1f5da56d011b..24762122f755b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Split.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Split.java @@ -29,6 +29,7 @@ import java.io.IOException; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isStringAndExact; @@ -110,7 +111,7 @@ static void process( BytesRefBlock.Builder builder, BytesRef str, @Fixed byte delim, - @Fixed(includeInToString = false, build = true) BytesRef scratch + @Fixed(includeInToString = false, scope = THREAD_LOCAL) BytesRef scratch ) { scratch.bytes = str.bytes; scratch.offset = str.offset; @@ -140,7 +141,7 @@ static void process( BytesRefBlock.Builder builder, BytesRef str, BytesRef delim, - @Fixed(includeInToString = false, build = true) BytesRef scratch + @Fixed(includeInToString = false, scope = THREAD_LOCAL) BytesRef scratch ) { checkDelimiter(delim); process(builder, str, delim.bytes[delim.offset], scratch); From d788761ea985a74aac90c6aee15fe32dea18e2ad Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Tue, 17 Dec 2024 18:18:24 +0100 Subject: [PATCH 036/119] [test] Correctly mute MixedClusterClientYamlTestSuiteIT {cat.shards/10_basic/Help} (#116398) The `sync_id` field was removed from the `cat shards` output for 9.0 in #114246, this test shouldn't be run along with other 8.x clusters Resolve #116110 --- muted-tests.yml | 3 --- qa/mixed-cluster/build.gradle | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index 42845fda82180..93d1a6e6374b7 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -87,9 +87,6 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/115816 - class: org.elasticsearch.xpack.application.connector.ConnectorIndexServiceTests issue: https://github.com/elastic/elasticsearch/issues/116087 -- class: org.elasticsearch.backwards.MixedClusterClientYamlTestSuiteIT - method: test {p0=cat.shards/10_basic/Help} - issue: https://github.com/elastic/elasticsearch/issues/116110 - class: org.elasticsearch.xpack.ml.integration.DatafeedJobsRestIT method: testLookbackWithIndicesOptions issue: https://github.com/elastic/elasticsearch/issues/116127 diff --git a/qa/mixed-cluster/build.gradle b/qa/mixed-cluster/build.gradle index d8f906b23d523..28bcac9f0242d 100644 --- a/qa/mixed-cluster/build.gradle +++ b/qa/mixed-cluster/build.gradle @@ -67,6 +67,9 @@ excludeList.add('indices.resolve_index/20_resolve_system_index/*') // Excluded because the error has changed excludeList.add('aggregations/percentiles_hdr_metric/Negative values test') +// sync_id is removed in 9.0 +excludeList.add("cat.shards/10_basic/Help") + def clusterPath = getPath() buildParams.bwcVersions.withWireCompatible { bwcVersion, baseName -> From 70087e3ded47e905dce9be09a969768345c0fa43 Mon Sep 17 00:00:00 2001 From: "elastic-renovate-prod[bot]" <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:58:19 +0000 Subject: [PATCH 037/119] Update docker.elastic.co/wolfi/chainguard-base:latest Docker digest to bfdeddb (#118868) Co-authored-by: elastic-renovate-prod[bot] <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> --- .../main/java/org/elasticsearch/gradle/internal/DockerBase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/DockerBase.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/DockerBase.java index d54eb798ce783..985c98bcd7883 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/DockerBase.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/DockerBase.java @@ -22,7 +22,7 @@ public enum DockerBase { // Chainguard based wolfi image with latest jdk // This is usually updated via renovatebot // spotless:off - WOLFI("docker.elastic.co/wolfi/chainguard-base:latest@sha256:1b51ff6dba78c98d3e02b0cd64a8ce3238c7a40408d21e3af12a329d44db6f23", + WOLFI("docker.elastic.co/wolfi/chainguard-base:latest@sha256:bfdeddb33330a281950c2a54adef991dbbe6a42832bc505d13b11beaf50ae73f", "-wolfi", "apk" ), From f3a16649068799aabfdafa0bc0bacf71c4409687 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Tue, 17 Dec 2024 18:59:49 +0100 Subject: [PATCH 038/119] Add min. read-only index version compatible to DiscoveryNode (#118744) #118443 added a new index version for indices that can be opened in read-only mode by Lucene. This change adds this information to the discovery node's VersionInformation and the transport serialization logic. In a short future we'd like to use this information in methods like IndexMetadataVerifier#checkSupportedVersion and NodeJoineExecutor to allow opening indices in N-2 versions as read-only indices on ES V9. --- docs/reference/indices/shard-stores.asciidoc | 6 +- .../org/elasticsearch/TransportVersions.java | 1 + .../cluster/node/DiscoveryNode.java | 21 ++++++- .../cluster/node/DiscoveryNodes.java | 15 +++++ .../cluster/node/VersionInformation.java | 32 ++++++++-- .../HandshakingTransportAddressConnector.java | 1 + .../transport/ProxyConnectionStrategy.java | 1 + .../transport/SniffConnectionStrategy.java | 1 + .../reroute/ClusterRerouteResponseTests.java | 2 + .../cluster/ClusterStateTests.java | 6 ++ .../cluster/node/DiscoveryNodeTests.java | 61 +++++++++++++++++++ .../cluster/node/DiscoveryNodeUtils.java | 15 ++++- .../ClusterStatsMonitoringDocTests.java | 2 + 13 files changed, 152 insertions(+), 12 deletions(-) diff --git a/docs/reference/indices/shard-stores.asciidoc b/docs/reference/indices/shard-stores.asciidoc index 1b001a3175b8c..04b086a758f9d 100644 --- a/docs/reference/indices/shard-stores.asciidoc +++ b/docs/reference/indices/shard-stores.asciidoc @@ -172,8 +172,9 @@ The API returns the following response: "attributes": {}, "roles": [...], "version": "8.10.0", - "min_index_version": 7000099, - "max_index_version": 8100099 + "min_index_version": 8000099, + "min_read_only_index_version": 7000099, + "max_index_version": 9004000 }, "allocation_id": "2iNySv_OQVePRX-yaRH_lQ", <4> "allocation" : "primary|replica|unused" <5> @@ -193,6 +194,7 @@ The API returns the following response: // TESTRESPONSE[s/"roles": \[[^]]*\]/"roles": $body.$_path/] // TESTRESPONSE[s/"8.10.0"/\$node_version/] // TESTRESPONSE[s/"min_index_version": 7000099/"min_index_version": $body.$_path/] +// TESTRESPONSE[s/"min_index_version": 7000099/"min_index_version": $body.$_path/] // TESTRESPONSE[s/"max_index_version": 8100099/"max_index_version": $body.$_path/] diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 371af961720cc..d3e235f1cd82a 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -139,6 +139,7 @@ static TransportVersion def(int id) { public static final TransportVersion SEMANTIC_QUERY_LENIENT = def(8_807_00_0); public static final TransportVersion ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS = def(8_808_00_0); public static final TransportVersion EQL_ALLOW_PARTIAL_SEARCH_RESULTS = def(8_809_00_0); + public static final TransportVersion NODE_VERSION_INFORMATION_WITH_MIN_READ_ONLY_INDEX_VERSION = def(8_810_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNode.java b/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNode.java index 7bf367f99b929..7c757e7657853 100644 --- a/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNode.java +++ b/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNode.java @@ -37,6 +37,7 @@ import java.util.SortedSet; import java.util.TreeSet; +import static org.elasticsearch.TransportVersions.NODE_VERSION_INFORMATION_WITH_MIN_READ_ONLY_INDEX_VERSION; import static org.elasticsearch.node.NodeRoleSettings.NODE_ROLES_SETTING; /** @@ -325,7 +326,17 @@ public DiscoveryNode(StreamInput in) throws IOException { } } this.roles = Collections.unmodifiableSortedSet(roles); - versionInfo = new VersionInformation(Version.readVersion(in), IndexVersion.readVersion(in), IndexVersion.readVersion(in)); + Version version = Version.readVersion(in); + IndexVersion minIndexVersion = IndexVersion.readVersion(in); + IndexVersion minReadOnlyIndexVersion; + if (in.getTransportVersion().onOrAfter(NODE_VERSION_INFORMATION_WITH_MIN_READ_ONLY_INDEX_VERSION)) { + minReadOnlyIndexVersion = IndexVersion.readVersion(in); + } else { + minReadOnlyIndexVersion = minIndexVersion; + + } + IndexVersion maxIndexVersion = IndexVersion.readVersion(in); + versionInfo = new VersionInformation(version, minIndexVersion, minReadOnlyIndexVersion, maxIndexVersion); if (in.getTransportVersion().onOrAfter(EXTERNAL_ID_VERSION)) { this.externalId = readStringLiteral.read(in); } else { @@ -360,6 +371,9 @@ public void writeTo(StreamOutput out) throws IOException { }); Version.writeVersion(versionInfo.nodeVersion(), out); IndexVersion.writeVersion(versionInfo.minIndexVersion(), out); + if (out.getTransportVersion().onOrAfter(NODE_VERSION_INFORMATION_WITH_MIN_READ_ONLY_INDEX_VERSION)) { + IndexVersion.writeVersion(versionInfo.minReadOnlyIndexVersion(), out); + } IndexVersion.writeVersion(versionInfo.maxIndexVersion(), out); if (out.getTransportVersion().onOrAfter(EXTERNAL_ID_VERSION)) { out.writeString(externalId); @@ -478,6 +492,10 @@ public IndexVersion getMinIndexVersion() { return versionInfo.minIndexVersion(); } + public IndexVersion getMinReadOnlyIndexVersion() { + return versionInfo.minReadOnlyIndexVersion(); + } + public IndexVersion getMaxIndexVersion() { return versionInfo.maxIndexVersion(); } @@ -577,6 +595,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.endArray(); builder.field("version", versionInfo.buildVersion().toString()); builder.field("min_index_version", versionInfo.minIndexVersion()); + builder.field("min_read_only_index_version", versionInfo.minReadOnlyIndexVersion()); builder.field("max_index_version", versionInfo.maxIndexVersion()); builder.endObject(); return builder; diff --git a/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java b/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java index 5e6dec7b68062..f733ab223fdd1 100644 --- a/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java +++ b/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java @@ -69,6 +69,7 @@ public class DiscoveryNodes implements Iterable, SimpleDiffable> tiersToNodeIds; @@ -84,6 +85,7 @@ private DiscoveryNodes( Version minNodeVersion, IndexVersion maxDataNodeCompatibleIndexVersion, IndexVersion minSupportedIndexVersion, + IndexVersion minReadOnlySupportedIndexVersion, Map> tiersToNodeIds ) { this.nodeLeftGeneration = nodeLeftGeneration; @@ -100,6 +102,8 @@ private DiscoveryNodes( this.maxNodeVersion = maxNodeVersion; this.maxDataNodeCompatibleIndexVersion = maxDataNodeCompatibleIndexVersion; this.minSupportedIndexVersion = minSupportedIndexVersion; + this.minReadOnlySupportedIndexVersion = minReadOnlySupportedIndexVersion; + assert minReadOnlySupportedIndexVersion.onOrBefore(minSupportedIndexVersion); assert (localNodeId == null) == (localNode == null); this.tiersToNodeIds = tiersToNodeIds; } @@ -118,6 +122,7 @@ public DiscoveryNodes withMasterNodeId(@Nullable String masterNodeId) { minNodeVersion, maxDataNodeCompatibleIndexVersion, minSupportedIndexVersion, + minReadOnlySupportedIndexVersion, tiersToNodeIds ); } @@ -374,6 +379,13 @@ public IndexVersion getMinSupportedIndexVersion() { return minSupportedIndexVersion; } + /** + * Returns the minimum index version for read-only indices supported by all nodes in the cluster + */ + public IndexVersion getMinReadOnlySupportedIndexVersion() { + return minReadOnlySupportedIndexVersion; + } + /** * Return the node-left generation, which is the number of times the cluster membership has been updated by removing one or more nodes. *

@@ -840,6 +852,7 @@ public DiscoveryNodes build() { Version maxNodeVersion = null; IndexVersion maxDataNodeCompatibleIndexVersion = null; IndexVersion minSupportedIndexVersion = null; + IndexVersion minReadOnlySupportedIndexVersion = null; for (Map.Entry nodeEntry : nodes.entrySet()) { DiscoveryNode discoNode = nodeEntry.getValue(); Version version = discoNode.getVersion(); @@ -849,6 +862,7 @@ public DiscoveryNodes build() { minNodeVersion = min(minNodeVersion, version); maxNodeVersion = max(maxNodeVersion, version); minSupportedIndexVersion = max(minSupportedIndexVersion, discoNode.getMinIndexVersion()); + minReadOnlySupportedIndexVersion = max(minReadOnlySupportedIndexVersion, discoNode.getMinReadOnlyIndexVersion()); } final long newNodeLeftGeneration; @@ -881,6 +895,7 @@ public DiscoveryNodes build() { Objects.requireNonNullElse(minNodeVersion, Version.CURRENT.minimumCompatibilityVersion()), Objects.requireNonNullElse(maxDataNodeCompatibleIndexVersion, IndexVersion.current()), Objects.requireNonNullElse(minSupportedIndexVersion, IndexVersions.MINIMUM_COMPATIBLE), + Objects.requireNonNullElse(minReadOnlySupportedIndexVersion, IndexVersions.MINIMUM_READONLY_COMPATIBLE), computeTiersToNodesMap(dataNodes) ); } diff --git a/server/src/main/java/org/elasticsearch/cluster/node/VersionInformation.java b/server/src/main/java/org/elasticsearch/cluster/node/VersionInformation.java index a4d0ff1eb55e4..852f31db69c92 100644 --- a/server/src/main/java/org/elasticsearch/cluster/node/VersionInformation.java +++ b/server/src/main/java/org/elasticsearch/cluster/node/VersionInformation.java @@ -18,20 +18,23 @@ /** * Represents the versions of various aspects of an Elasticsearch node. - * @param buildVersion The node {@link BuildVersion} - * @param minIndexVersion The minimum {@link IndexVersion} supported by this node - * @param maxIndexVersion The maximum {@link IndexVersion} supported by this node + * @param buildVersion The node {@link BuildVersion} + * @param minIndexVersion The minimum {@link IndexVersion} supported by this node + * @param minReadOnlyIndexVersion The minimum {@link IndexVersion} for read-only indices supported by this node + * @param maxIndexVersion The maximum {@link IndexVersion} supported by this node */ public record VersionInformation( BuildVersion buildVersion, Version nodeVersion, IndexVersion minIndexVersion, + IndexVersion minReadOnlyIndexVersion, IndexVersion maxIndexVersion ) { public static final VersionInformation CURRENT = new VersionInformation( BuildVersion.current(), IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current() ); @@ -39,11 +42,18 @@ public record VersionInformation( Objects.requireNonNull(buildVersion); Objects.requireNonNull(nodeVersion); Objects.requireNonNull(minIndexVersion); + Objects.requireNonNull(minReadOnlyIndexVersion); Objects.requireNonNull(maxIndexVersion); + assert minReadOnlyIndexVersion.onOrBefore(minIndexVersion) : minReadOnlyIndexVersion + " > " + minIndexVersion; } - public VersionInformation(BuildVersion version, IndexVersion minIndexVersion, IndexVersion maxIndexVersion) { - this(version, Version.CURRENT, minIndexVersion, maxIndexVersion); + public VersionInformation( + BuildVersion version, + IndexVersion minIndexVersion, + IndexVersion minReadOnlyIndexVersion, + IndexVersion maxIndexVersion + ) { + this(version, Version.CURRENT, minIndexVersion, minReadOnlyIndexVersion, maxIndexVersion); /* * Whilst DiscoveryNode.getVersion exists, we need to be able to get a Version from VersionInfo * This needs to be consistent - on serverless, BuildVersion has an id of -1, which translates @@ -57,7 +67,17 @@ public VersionInformation(BuildVersion version, IndexVersion minIndexVersion, In @Deprecated public VersionInformation(Version version, IndexVersion minIndexVersion, IndexVersion maxIndexVersion) { - this(BuildVersion.fromVersionId(version.id()), version, minIndexVersion, maxIndexVersion); + this(version, minIndexVersion, minIndexVersion, maxIndexVersion); + } + + @Deprecated + public VersionInformation( + Version version, + IndexVersion minIndexVersion, + IndexVersion minReadOnlyIndexVersion, + IndexVersion maxIndexVersion + ) { + this(BuildVersion.fromVersionId(version.id()), version, minIndexVersion, minReadOnlyIndexVersion, maxIndexVersion); } @Deprecated diff --git a/server/src/main/java/org/elasticsearch/discovery/HandshakingTransportAddressConnector.java b/server/src/main/java/org/elasticsearch/discovery/HandshakingTransportAddressConnector.java index ce849c26ab780..98715127351aa 100644 --- a/server/src/main/java/org/elasticsearch/discovery/HandshakingTransportAddressConnector.java +++ b/server/src/main/java/org/elasticsearch/discovery/HandshakingTransportAddressConnector.java @@ -110,6 +110,7 @@ private void openProbeConnection(ActionListener listener) new VersionInformation( Version.CURRENT.minimumCompatibilityVersion(), IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current() ) ), diff --git a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java index d5047a61e4606..eb2eab75d3fe3 100644 --- a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java @@ -303,6 +303,7 @@ public void onFailure(Exception e) { new VersionInformation( Version.CURRENT.minimumCompatibilityVersion(), IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current() ) ); diff --git a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java index 2c198caf22354..854072c49e354 100644 --- a/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/SniffConnectionStrategy.java @@ -505,6 +505,7 @@ private static DiscoveryNode resolveSeedNode(String clusterAlias, String address var seedVersion = new VersionInformation( Version.CURRENT.minimumCompatibilityVersion(), IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current() ); if (proxyAddress == null || proxyAddress.isEmpty()) { diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponseTests.java index b59cc13a20ff2..69cff0fc45ac3 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponseTests.java @@ -127,6 +127,7 @@ public void testToXContentWithDeprecatedClusterState() { ], "version": "%s", "min_index_version": %s, + "min_read_only_index_version": %s, "max_index_version": %s } }, @@ -218,6 +219,7 @@ public void testToXContentWithDeprecatedClusterState() { clusterState.getNodes().get("node0").getEphemeralId(), Version.CURRENT, IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current(), IndexVersion.current(), IndexVersion.current() diff --git a/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java b/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java index 668aea70c23f2..5f4426b02ce1a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java @@ -213,6 +213,7 @@ public void testToXContent() throws IOException { ], "version": "%s", "min_index_version":%s, + "min_read_only_index_version":%s, "max_index_version":%s } }, @@ -389,6 +390,7 @@ public void testToXContent() throws IOException { ephemeralId, Version.CURRENT, IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current(), TransportVersion.current(), IndexVersion.current(), @@ -488,6 +490,7 @@ public void testToXContent_FlatSettingTrue_ReduceMappingFalse() throws IOExcepti ], "version" : "%s", "min_index_version" : %s, + "min_read_only_index_version" : %s, "max_index_version" : %s } }, @@ -663,6 +666,7 @@ public void testToXContent_FlatSettingTrue_ReduceMappingFalse() throws IOExcepti ephemeralId, Version.CURRENT, IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current(), TransportVersion.current(), IndexVersion.current(), @@ -762,6 +766,7 @@ public void testToXContent_FlatSettingFalse_ReduceMappingTrue() throws IOExcepti ], "version" : "%s", "min_index_version" : %s, + "min_read_only_index_version" : %s, "max_index_version" : %s } }, @@ -943,6 +948,7 @@ public void testToXContent_FlatSettingFalse_ReduceMappingTrue() throws IOExcepti ephemeralId, Version.CURRENT, IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current(), TransportVersion.current(), IndexVersion.current(), diff --git a/server/src/test/java/org/elasticsearch/cluster/node/DiscoveryNodeTests.java b/server/src/test/java/org/elasticsearch/cluster/node/DiscoveryNodeTests.java index 331b5d92ca94e..fa7633f0eaf75 100644 --- a/server/src/test/java/org/elasticsearch/cluster/node/DiscoveryNodeTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/node/DiscoveryNodeTests.java @@ -31,6 +31,8 @@ import static java.util.Collections.emptySet; import static org.elasticsearch.test.NodeRoles.nonRemoteClusterClientNode; import static org.elasticsearch.test.NodeRoles.remoteClusterClientNode; +import static org.elasticsearch.test.TransportVersionUtils.getPreviousVersion; +import static org.elasticsearch.test.TransportVersionUtils.randomVersionBetween; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -221,6 +223,7 @@ public void testDiscoveryNodeToXContent() { ], "version" : "%s", "min_index_version" : %s, + "min_read_only_index_version" : %s, "max_index_version" : %s } }""", @@ -228,6 +231,7 @@ public void testDiscoveryNodeToXContent() { withExternalId ? "test-external-id" : "test-name", Version.CURRENT, IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current() ) ) @@ -250,4 +254,61 @@ public void testDiscoveryNodeToString() { assertThat(toString, containsString("{" + node.getBuildVersion() + "}")); assertThat(toString, containsString("{test-attr=val}"));// attributes } + + public void testDiscoveryNodeMinReadOnlyVersionSerialization() throws Exception { + var node = DiscoveryNodeUtils.create("_id", buildNewFakeTransportAddress(), VersionInformation.CURRENT); + + { + try (var out = new BytesStreamOutput()) { + out.setTransportVersion(TransportVersion.current()); + node.writeTo(out); + + try (var in = StreamInput.wrap(out.bytes().array())) { + in.setTransportVersion(TransportVersion.current()); + + var deserialized = new DiscoveryNode(in); + assertThat(deserialized.getId(), equalTo(node.getId())); + assertThat(deserialized.getAddress(), equalTo(node.getAddress())); + assertThat(deserialized.getMinIndexVersion(), equalTo(node.getMinIndexVersion())); + assertThat(deserialized.getMaxIndexVersion(), equalTo(node.getMaxIndexVersion())); + assertThat(deserialized.getMinReadOnlyIndexVersion(), equalTo(node.getMinReadOnlyIndexVersion())); + assertThat(deserialized.getVersionInformation(), equalTo(node.getVersionInformation())); + } + } + } + + { + var oldVersion = randomVersionBetween( + random(), + TransportVersions.MINIMUM_COMPATIBLE, + getPreviousVersion(TransportVersions.NODE_VERSION_INFORMATION_WITH_MIN_READ_ONLY_INDEX_VERSION) + ); + try (var out = new BytesStreamOutput()) { + out.setTransportVersion(oldVersion); + node.writeTo(out); + + try (var in = StreamInput.wrap(out.bytes().array())) { + in.setTransportVersion(oldVersion); + + var deserialized = new DiscoveryNode(in); + assertThat(deserialized.getId(), equalTo(node.getId())); + assertThat(deserialized.getAddress(), equalTo(node.getAddress())); + assertThat(deserialized.getMinIndexVersion(), equalTo(node.getMinIndexVersion())); + assertThat(deserialized.getMaxIndexVersion(), equalTo(node.getMaxIndexVersion())); + assertThat(deserialized.getMinReadOnlyIndexVersion(), equalTo(node.getMinIndexVersion())); + assertThat( + deserialized.getVersionInformation(), + equalTo( + new VersionInformation( + node.getBuildVersion(), + node.getMinIndexVersion(), + node.getMinIndexVersion(), + node.getMaxIndexVersion() + ) + ) + ); + } + } + } + } } diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodeUtils.java b/test/framework/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodeUtils.java index 64f8fa88762b8..20368753eac1d 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodeUtils.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodeUtils.java @@ -76,6 +76,7 @@ public static class Builder { private BuildVersion buildVersion; private Version version; private IndexVersion minIndexVersion; + private IndexVersion minReadOnlyIndexVersion; private IndexVersion maxIndexVersion; private String externalId; @@ -125,16 +126,23 @@ public Builder version(Version version, IndexVersion minIndexVersion, IndexVersi this.buildVersion = BuildVersion.fromVersionId(version.id()); this.version = version; this.minIndexVersion = minIndexVersion; + this.minReadOnlyIndexVersion = minIndexVersion; this.maxIndexVersion = maxIndexVersion; return this; } - public Builder version(BuildVersion version, IndexVersion minIndexVersion, IndexVersion maxIndexVersion) { + public Builder version( + BuildVersion version, + IndexVersion minIndexVersion, + IndexVersion minReadOnlyIndexVersion, + IndexVersion maxIndexVersion + ) { // see comment in VersionInformation assert version.equals(BuildVersion.current()); this.buildVersion = version; this.version = Version.CURRENT; this.minIndexVersion = minIndexVersion; + this.minReadOnlyIndexVersion = minReadOnlyIndexVersion; this.maxIndexVersion = maxIndexVersion; return this; } @@ -143,6 +151,7 @@ public Builder version(VersionInformation versions) { this.buildVersion = versions.buildVersion(); this.version = versions.nodeVersion(); this.minIndexVersion = versions.minIndexVersion(); + this.minReadOnlyIndexVersion = versions.minReadOnlyIndexVersion(); this.maxIndexVersion = versions.maxIndexVersion(); return this; } @@ -170,10 +179,10 @@ public DiscoveryNode build() { } VersionInformation versionInfo; - if (minIndexVersion == null || maxIndexVersion == null) { + if (minIndexVersion == null || minReadOnlyIndexVersion == null || maxIndexVersion == null) { versionInfo = VersionInformation.inferVersions(version); } else { - versionInfo = new VersionInformation(buildVersion, version, minIndexVersion, maxIndexVersion); + versionInfo = new VersionInformation(buildVersion, version, minIndexVersion, minReadOnlyIndexVersion, maxIndexVersion); } return new DiscoveryNode(name, id, ephemeralId, hostName, hostAddress, address, attributes, roles, versionInfo, externalId); diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/cluster/ClusterStatsMonitoringDocTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/cluster/ClusterStatsMonitoringDocTests.java index f4d50df4ff613..35da4abec223a 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/cluster/ClusterStatsMonitoringDocTests.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/cluster/ClusterStatsMonitoringDocTests.java @@ -462,6 +462,7 @@ public void testToXContent() throws IOException { pluginEsBuildVersion, Version.CURRENT, IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current(), apmIndicesExist }; final String expectedJson = """ @@ -817,6 +818,7 @@ public void testToXContent() throws IOException { ], "version": "%s", "min_index_version":%s, + "min_read_only_index_version":%s, "max_index_version":%s } }, From a5c57ba966cfd088b8d79fd51fe3fb35163b22a2 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 17 Dec 2024 13:30:48 -0500 Subject: [PATCH 039/119] Adjust random_score default field to _seq_no field (#118671) In an effort to improve performance and continue to provide unique seeded scores for documents in the same index, we are switching from _id to _seq_no. Requiring a field that is "unique" for a field and to help with random scores is burdensome for the user. So, we should default to a unique field (per index) when the user provides a seed. Using `_seq_no` should be better as: - We don't have to grab stored fields values - Bytes used are generally smaller Additionally this removes the deprecation warning. Marking as "breaking" as it does change the scores & behavior, but the API provide is the same. --- .../src/main/resources/changelog-schema.json | 1 + docs/changelog/118671.yaml | 11 +++++++++++ .../RandomScoreFunctionBuilder.java | 17 ++--------------- .../ScoreFunctionBuilderTests.java | 1 - 4 files changed, 14 insertions(+), 16 deletions(-) create mode 100644 docs/changelog/118671.yaml diff --git a/build-tools-internal/src/main/resources/changelog-schema.json b/build-tools-internal/src/main/resources/changelog-schema.json index 451701d74d690..7d35951eaa2cf 100644 --- a/build-tools-internal/src/main/resources/changelog-schema.json +++ b/build-tools-internal/src/main/resources/changelog-schema.json @@ -295,6 +295,7 @@ "Painless", "REST API", "Rollup", + "Search", "System requirement", "Transform" ] diff --git a/docs/changelog/118671.yaml b/docs/changelog/118671.yaml new file mode 100644 index 0000000000000..3931cc4179037 --- /dev/null +++ b/docs/changelog/118671.yaml @@ -0,0 +1,11 @@ +pr: 118671 +summary: Adjust `random_score` default field to `_seq_no` field +area: Search +type: breaking +issues: [] +breaking: + title: Adjust `random_score` default field to `_seq_no` field + area: Search + details: When providing a 'seed' parameter to a 'random_score' function in the 'function_score' query but NOT providing a 'field', the default 'field' is switched from '_id' to '_seq_no'. + impact: The random scoring and ordering may change when providing a 'seed' and not providing a 'field' to a 'random_score' function. + notable: false diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/RandomScoreFunctionBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/RandomScoreFunctionBuilder.java index 6d4b2dd4ab1f5..88f1ab1ba5c2e 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/RandomScoreFunctionBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/RandomScoreFunctionBuilder.java @@ -13,12 +13,10 @@ import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.logging.DeprecationCategory; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.lucene.search.function.RandomScoreFunction; import org.elasticsearch.common.lucene.search.function.ScoreFunction; -import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -30,7 +28,6 @@ * A function that computes a random score for the matched documents */ public class RandomScoreFunctionBuilder extends ScoreFunctionBuilder { - private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RandomScoreFunctionBuilder.class); public static final String NAME = "random_score"; private String field; @@ -140,17 +137,7 @@ protected ScoreFunction doToFunction(SearchExecutionContext context) { // DocID-based random score generation return new RandomScoreFunction(hash(context.nowInMillis()), salt, null); } else { - String fieldName; - if (field == null) { - deprecationLogger.warn( - DeprecationCategory.QUERIES, - "seed_requires_field", - "As of version 7.0 Elasticsearch will require that a [field] parameter is provided when a [seed] is set" - ); - fieldName = IdFieldMapper.NAME; - } else { - fieldName = field; - } + final String fieldName = Objects.requireNonNullElse(field, SeqNoFieldMapper.NAME); if (context.isFieldMapped(fieldName) == false) { if (context.hasMappings() == false) { // no mappings: the index is empty anyway diff --git a/server/src/test/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilderTests.java index 8d060d94e4c21..b58ac513a6449 100644 --- a/server/src/test/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilderTests.java @@ -66,7 +66,6 @@ public void testRandomScoreFunctionWithSeedNoField() throws Exception { Mockito.when(context.getFieldType(IdFieldMapper.NAME)).thenReturn(new KeywordFieldMapper.KeywordFieldType(IdFieldMapper.NAME)); Mockito.when(context.isFieldMapped(IdFieldMapper.NAME)).thenReturn(true); builder.toFunction(context); - assertWarnings("As of version 7.0 Elasticsearch will require that a [field] parameter is provided when a [seed] is set"); } public void testRandomScoreFunctionWithSeed() throws Exception { From bac70647260ba0396a904b918a3dd3576c757a0d Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 17 Dec 2024 13:34:26 -0500 Subject: [PATCH 040/119] Bump versions after 8.16.2 release --- .buildkite/pipelines/intake.yml | 2 +- .buildkite/pipelines/periodic-packaging.yml | 6 +++--- .buildkite/pipelines/periodic.yml | 10 +++++----- .ci/bwcVersions | 2 +- .ci/snapshotBwcVersions | 2 +- server/src/main/java/org/elasticsearch/Version.java | 1 + .../resources/org/elasticsearch/TransportVersions.csv | 1 + .../org/elasticsearch/index/IndexVersions.csv | 1 + 8 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.buildkite/pipelines/intake.yml b/.buildkite/pipelines/intake.yml index 6e15d64154960..9efb9c8b498aa 100644 --- a/.buildkite/pipelines/intake.yml +++ b/.buildkite/pipelines/intake.yml @@ -56,7 +56,7 @@ steps: timeout_in_minutes: 300 matrix: setup: - BWC_VERSION: ["8.16.2", "8.17.1", "8.18.0", "9.0.0"] + BWC_VERSION: ["8.16.3", "8.17.1", "8.18.0", "9.0.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2004 diff --git a/.buildkite/pipelines/periodic-packaging.yml b/.buildkite/pipelines/periodic-packaging.yml index abd11068e7a65..b1e5a7bf933c9 100644 --- a/.buildkite/pipelines/periodic-packaging.yml +++ b/.buildkite/pipelines/periodic-packaging.yml @@ -287,8 +287,8 @@ steps: env: BWC_VERSION: 8.15.5 - - label: "{{matrix.image}} / 8.16.2 / packaging-tests-upgrade" - command: ./.ci/scripts/packaging-test.sh -Dbwc.checkout.align=true destructiveDistroUpgradeTest.v8.16.2 + - label: "{{matrix.image}} / 8.16.3 / packaging-tests-upgrade" + command: ./.ci/scripts/packaging-test.sh -Dbwc.checkout.align=true destructiveDistroUpgradeTest.v8.16.3 timeout_in_minutes: 300 matrix: setup: @@ -301,7 +301,7 @@ steps: machineType: custom-16-32768 buildDirectory: /dev/shm/bk env: - BWC_VERSION: 8.16.2 + BWC_VERSION: 8.16.3 - label: "{{matrix.image}} / 8.17.1 / packaging-tests-upgrade" command: ./.ci/scripts/packaging-test.sh -Dbwc.checkout.align=true destructiveDistroUpgradeTest.v8.17.1 diff --git a/.buildkite/pipelines/periodic.yml b/.buildkite/pipelines/periodic.yml index f2d169cd2b30d..4c593bae62d7a 100644 --- a/.buildkite/pipelines/periodic.yml +++ b/.buildkite/pipelines/periodic.yml @@ -306,8 +306,8 @@ steps: - signal_reason: agent_stop limit: 3 - - label: 8.16.2 / bwc - command: .ci/scripts/run-gradle.sh -Dbwc.checkout.align=true v8.16.2#bwcTest + - label: 8.16.3 / bwc + command: .ci/scripts/run-gradle.sh -Dbwc.checkout.align=true v8.16.3#bwcTest timeout_in_minutes: 300 agents: provider: gcp @@ -316,7 +316,7 @@ steps: buildDirectory: /dev/shm/bk preemptible: true env: - BWC_VERSION: 8.16.2 + BWC_VERSION: 8.16.3 retry: automatic: - exit_status: "-1" @@ -448,7 +448,7 @@ steps: setup: ES_RUNTIME_JAVA: - openjdk21 - BWC_VERSION: ["8.16.2", "8.17.1", "8.18.0", "9.0.0"] + BWC_VERSION: ["8.16.3", "8.17.1", "8.18.0", "9.0.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2004 @@ -490,7 +490,7 @@ steps: ES_RUNTIME_JAVA: - openjdk21 - openjdk23 - BWC_VERSION: ["8.16.2", "8.17.1", "8.18.0", "9.0.0"] + BWC_VERSION: ["8.16.3", "8.17.1", "8.18.0", "9.0.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2004 diff --git a/.ci/bwcVersions b/.ci/bwcVersions index 3cb983373138f..cf12ee8c15419 100644 --- a/.ci/bwcVersions +++ b/.ci/bwcVersions @@ -15,7 +15,7 @@ BWC_VERSION: - "8.13.4" - "8.14.3" - "8.15.5" - - "8.16.2" + - "8.16.3" - "8.17.1" - "8.18.0" - "9.0.0" diff --git a/.ci/snapshotBwcVersions b/.ci/snapshotBwcVersions index e05c0774c9819..68c6ad5601546 100644 --- a/.ci/snapshotBwcVersions +++ b/.ci/snapshotBwcVersions @@ -1,5 +1,5 @@ BWC_VERSION: - - "8.16.2" + - "8.16.3" - "8.17.1" - "8.18.0" - "9.0.0" diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index 47c43eadcfb03..8873c9b0e281e 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -191,6 +191,7 @@ public class Version implements VersionId, ToXContentFragment { public static final Version V_8_16_0 = new Version(8_16_00_99); public static final Version V_8_16_1 = new Version(8_16_01_99); public static final Version V_8_16_2 = new Version(8_16_02_99); + public static final Version V_8_16_3 = new Version(8_16_03_99); public static final Version V_8_17_0 = new Version(8_17_00_99); public static final Version V_8_17_1 = new Version(8_17_01_99); public static final Version V_8_18_0 = new Version(8_18_00_99); diff --git a/server/src/main/resources/org/elasticsearch/TransportVersions.csv b/server/src/main/resources/org/elasticsearch/TransportVersions.csv index 08db0822dfef5..2016f59b58a3e 100644 --- a/server/src/main/resources/org/elasticsearch/TransportVersions.csv +++ b/server/src/main/resources/org/elasticsearch/TransportVersions.csv @@ -135,4 +135,5 @@ 8.15.5,8702003 8.16.0,8772001 8.16.1,8772004 +8.16.2,8772004 8.17.0,8797002 diff --git a/server/src/main/resources/org/elasticsearch/index/IndexVersions.csv b/server/src/main/resources/org/elasticsearch/index/IndexVersions.csv index afe696f31d323..3bfeeded6494c 100644 --- a/server/src/main/resources/org/elasticsearch/index/IndexVersions.csv +++ b/server/src/main/resources/org/elasticsearch/index/IndexVersions.csv @@ -135,4 +135,5 @@ 8.15.5,8512000 8.16.0,8518000 8.16.1,8518000 +8.16.2,8518000 8.17.0,8521000 From 5cb2d280201020c4246afa428095ae2e24c20be2 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 17 Dec 2024 13:35:51 -0500 Subject: [PATCH 041/119] Prune changelogs after 8.16.2 release --- docs/changelog/116358.yaml | 5 ----- docs/changelog/117153.yaml | 5 ----- docs/changelog/118380.yaml | 5 ----- 3 files changed, 15 deletions(-) delete mode 100644 docs/changelog/116358.yaml delete mode 100644 docs/changelog/117153.yaml delete mode 100644 docs/changelog/118380.yaml diff --git a/docs/changelog/116358.yaml b/docs/changelog/116358.yaml deleted file mode 100644 index 58b44a1e9bcf5..0000000000000 --- a/docs/changelog/116358.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 116358 -summary: Update Deberta tokenizer -area: Machine Learning -type: bug -issues: [] diff --git a/docs/changelog/117153.yaml b/docs/changelog/117153.yaml deleted file mode 100644 index f7640c0a7ed6a..0000000000000 --- a/docs/changelog/117153.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 117153 -summary: "ESQL: fix the column position in errors" -area: ES|QL -type: bug -issues: [] diff --git a/docs/changelog/118380.yaml b/docs/changelog/118380.yaml deleted file mode 100644 index 8b26c871fb172..0000000000000 --- a/docs/changelog/118380.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 118380 -summary: Restore original "is within leaf" value in `SparseVectorFieldMapper` -area: Mapping -type: bug -issues: [] From bde485a84520539f98d3f08913af4b8b73434e6b Mon Sep 17 00:00:00 2001 From: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:23:00 +0100 Subject: [PATCH 042/119] [ML] Add dynamic templates for anomalies results index (#118845) This PR enables the resolution of field names containing "." after the rollover of the anomalies results index. This way, the object parts of the field name, e.g., for an influencer, will indeed have the object type and can be resolved in search and filter operations. --- .../ml/anomalydetection/results_index_mappings.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/core/template-resources/src/main/resources/ml/anomalydetection/results_index_mappings.json b/x-pack/plugin/core/template-resources/src/main/resources/ml/anomalydetection/results_index_mappings.json index 4415afe50a998..e0bde4715839e 100644 --- a/x-pack/plugin/core/template-resources/src/main/resources/ml/anomalydetection/results_index_mappings.json +++ b/x-pack/plugin/core/template-resources/src/main/resources/ml/anomalydetection/results_index_mappings.json @@ -5,7 +5,15 @@ }, "dynamic_templates" : [ { - "strings_as_keywords" : { + "map_objects": { + "match_mapping_type": "object", + "mapping": { + "type": "object" + } + } + }, + { + "non_objects_as_keywords" : { "match" : "*", "mapping" : { "type" : "keyword" From 4b90f01b49b820242608d54fae6b624fe950847d Mon Sep 17 00:00:00 2001 From: Mark Tozzi Date: Tue, 17 Dec 2024 14:36:02 -0500 Subject: [PATCH 043/119] Esql - fix some test failures due to results ordering (#118692) fix some failing multi-shard tests. The backport for the PR that added these tests hasn't landed yet, so I'm going to just cherry pick this into there rather than do two back ports with conflict resolution. --- .../src/main/resources/date_nanos.csv-spec | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec index f4b5c98d596ae..47191148e0205 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec @@ -555,7 +555,8 @@ required_capability: date_nanos_bucket FROM date_nanos | WHERE millis > "2020-01-01" -| STATS ct = count(*) BY yr = BUCKET(nanos, 1 year); +| STATS ct = count(*) BY yr = BUCKET(nanos, 1 year) +| SORT yr DESC; ct:long | yr:date_nanos 8 | 2023-01-01T00:00:00.000000000Z @@ -567,7 +568,8 @@ required_capability: date_nanos_bucket FROM date_nanos | WHERE millis > "2020-01-01" -| STATS ct = count(*) BY yr = BUCKET(nanos, 5, "1999-01-01", NOW()); +| STATS ct = count(*) BY yr = BUCKET(nanos, 5, "1999-01-01", NOW()) +| SORT yr DESC; ct:long | yr:date_nanos 8 | 2023-01-01T00:00:00.000000000Z @@ -579,7 +581,8 @@ required_capability: date_nanos_bucket FROM date_nanos | WHERE millis > "2020-01-01" -| STATS ct = count(*) BY mo = BUCKET(nanos, 1 month); +| STATS ct = count(*) BY mo = BUCKET(nanos, 1 month) +| SORT mo DESC; ct:long | mo:date_nanos 8 | 2023-10-01T00:00:00.000000000Z @@ -591,7 +594,8 @@ required_capability: date_nanos_bucket FROM date_nanos | WHERE millis > "2020-01-01" -| STATS ct = count(*) BY mo = BUCKET(nanos, 20, "2023-01-01", "2023-12-31"); +| STATS ct = count(*) BY mo = BUCKET(nanos, 20, "2023-01-01", "2023-12-31") +| SORT mo DESC; ct:long | mo:date_nanos 8 | 2023-10-01T00:00:00.000000000Z @@ -603,18 +607,21 @@ required_capability: date_nanos_bucket FROM date_nanos | WHERE millis > "2020-01-01" -| STATS ct = count(*) BY mo = BUCKET(nanos, 55, "2023-01-01", "2023-12-31"); +| STATS ct = count(*) BY mo = BUCKET(nanos, 55, "2023-01-01", "2023-12-31") +| SORT mo DESC; ct:long | mo:date_nanos 8 | 2023-10-23T00:00:00.000000000Z ; + Bucket Date nanos by 10 minutes required_capability: date_trunc_date_nanos required_capability: date_nanos_bucket FROM date_nanos | WHERE millis > "2020-01-01" -| STATS ct = count(*) BY mn = BUCKET(nanos, 10 minutes); +| STATS ct = count(*) BY mn = BUCKET(nanos, 10 minutes) +| SORT mn DESC; ct:long | mn:date_nanos 4 | 2023-10-23T13:50:00.000000000Z From 517abe4ffdceb3c7ca771207fe20b0c813181810 Mon Sep 17 00:00:00 2001 From: Dianna Hohensee Date: Tue, 17 Dec 2024 14:51:45 -0500 Subject: [PATCH 044/119] ConnectTransportException returns retryable BAD_GATEWAY (#118681) ConnectTransportException and its subclasses previous translated to a INTERNAL_SERVER_ERROR HTTP 500 code. We are changing it to 502 BAD_GATEWAY so that users may choose to retry it on connectivity issues. Related ES-10214 Closes #118320 --- docs/changelog/118681.yaml | 6 ++++++ .../transport/ConnectTransportException.java | 13 +++++++++++++ .../elasticsearch/ExceptionSerializationTests.java | 1 + 3 files changed, 20 insertions(+) create mode 100644 docs/changelog/118681.yaml diff --git a/docs/changelog/118681.yaml b/docs/changelog/118681.yaml new file mode 100644 index 0000000000000..a186c05e6cd7d --- /dev/null +++ b/docs/changelog/118681.yaml @@ -0,0 +1,6 @@ +pr: 118681 +summary: '`ConnectTransportException` returns retryable BAD_GATEWAY' +area: Network +type: enhancement +issues: + - 118320 diff --git a/server/src/main/java/org/elasticsearch/transport/ConnectTransportException.java b/server/src/main/java/org/elasticsearch/transport/ConnectTransportException.java index 648d27c885843..302175cc4f5a0 100644 --- a/server/src/main/java/org/elasticsearch/transport/ConnectTransportException.java +++ b/server/src/main/java/org/elasticsearch/transport/ConnectTransportException.java @@ -13,6 +13,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.rest.RestStatus; import java.io.IOException; @@ -41,6 +42,18 @@ public ConnectTransportException(StreamInput in) throws IOException { } } + /** + * The ES REST API is a gateway to a single or multiple clusters. If there is an error connecting to other servers, then we should + * return a 502 BAD_GATEWAY status code instead of the parent class' 500 INTERNAL_SERVER_ERROR. Clients tend to retry on a 502 but not + * on a 500, and retrying may help on a connection error. + * + * @return a {@link RestStatus#BAD_GATEWAY} code + */ + @Override + public final RestStatus status() { + return RestStatus.BAD_GATEWAY; + } + @Override protected void writeTo(StreamOutput out, Writer nestedExceptionsWriter) throws IOException { super.writeTo(out, nestedExceptionsWriter); diff --git a/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java b/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java index 2abe4157583cd..31f54f9a16359 100644 --- a/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java @@ -409,6 +409,7 @@ public void testConnectTransportException() throws IOException { ex = serialize(new ConnectTransportException(node, "msg", "action", new NullPointerException())); assertEquals("[][" + transportAddress + "][action] msg", ex.getMessage()); assertThat(ex.getCause(), instanceOf(NullPointerException.class)); + assertEquals(RestStatus.BAD_GATEWAY, ex.status()); } public void testSearchPhaseExecutionException() throws IOException { From ae3c0d703257996ac4b07dfdc4543eb625ee9a60 Mon Sep 17 00:00:00 2001 From: Mark Tozzi Date: Tue, 17 Dec 2024 14:54:19 -0500 Subject: [PATCH 045/119] Esql implicit casting for date nanos (#118697) resolves #118476 This adds an implicit cast from string to date nanos, much the same as we do for millisecond dates. In the course of working on this, I found and fixed a couple of tests that were creating pre-epoch date nanos, which are not supported in elasticsearch. I also refactored the conversion code to use the standard DateUtils functions where appropriate, which caught some of the above errors in test data. --- docs/changelog/118697.yaml | 6 + .../src/main/resources/date_nanos.csv-spec | 131 ++++++++++++++++++ .../xpack/esql/action/EsqlCapabilities.java | 5 +- .../xpack/esql/analysis/Analyzer.java | 31 +++-- .../esql/type/EsqlDataTypeConverter.java | 14 +- .../esql/action/EsqlQueryResponseTests.java | 2 +- .../esql/type/EsqlDataTypeConverterTests.java | 12 +- 7 files changed, 179 insertions(+), 22 deletions(-) create mode 100644 docs/changelog/118697.yaml diff --git a/docs/changelog/118697.yaml b/docs/changelog/118697.yaml new file mode 100644 index 0000000000000..6e24e6ae4b47f --- /dev/null +++ b/docs/changelog/118697.yaml @@ -0,0 +1,6 @@ +pr: 118697 +summary: Esql implicit casting for date nanos +area: ES|QL +type: enhancement +issues: + - 118476 diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec index 47191148e0205..4206d6b48699f 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec @@ -216,6 +216,137 @@ millis:date | nanos:date_nanos | num:long 2023-10-23T13:33:34.937Z | 2023-10-23T13:33:34.937193000Z | 1698068014937193000 ; +implicit casting to nanos, date only +required_capability: date_nanos_type +required_capability: date_nanos_implicit_casting + +FROM date_nanos +| WHERE MV_MIN(nanos) > "2023-10-23" +| SORT nanos DESC +| KEEP millis, nanos; + +millis:date | nanos:date_nanos +2023-10-23T13:55:01.543Z | 2023-10-23T13:55:01.543123456Z +2023-10-23T13:53:55.832Z | 2023-10-23T13:53:55.832987654Z +2023-10-23T13:52:55.015Z | 2023-10-23T13:52:55.015787878Z +2023-10-23T13:51:54.732Z | 2023-10-23T13:51:54.732102837Z +2023-10-23T13:33:34.937Z | 2023-10-23T13:33:34.937193000Z +2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948000000Z +2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z +2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z +; + +implicit casting to nanos, date only, equality test +required_capability: date_nanos_type +required_capability: date_nanos_implicit_casting + +FROM date_nanos +| WHERE MV_MIN(nanos) == "2023-10-23" +| SORT nanos DESC +| KEEP millis, nanos; + +millis:date | nanos:date_nanos +; + + +implicit casting to nanos, date plus time to seconds +required_capability: date_nanos_type +required_capability: date_nanos_implicit_casting + +FROM date_nanos +| WHERE MV_MIN(nanos) > "2023-10-23T00:00:00" +| SORT nanos DESC +| KEEP millis, nanos; + +millis:date | nanos:date_nanos +2023-10-23T13:55:01.543Z | 2023-10-23T13:55:01.543123456Z +2023-10-23T13:53:55.832Z | 2023-10-23T13:53:55.832987654Z +2023-10-23T13:52:55.015Z | 2023-10-23T13:52:55.015787878Z +2023-10-23T13:51:54.732Z | 2023-10-23T13:51:54.732102837Z +2023-10-23T13:33:34.937Z | 2023-10-23T13:33:34.937193000Z +2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948000000Z +2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z +2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z +; + +implicit casting to nanos, date plus time to seconds, equality test +required_capability: date_nanos_type +required_capability: date_nanos_implicit_casting + +FROM date_nanos +| WHERE MV_MIN(nanos) == "2023-10-23T12:27:28" +| SORT nanos DESC +| KEEP millis, nanos; + +millis:date | nanos:date_nanos +; + +implicit casting to nanos, date plus time to millis +required_capability: date_nanos_type +required_capability: date_nanos_implicit_casting + +FROM date_nanos +| WHERE MV_MIN(nanos) > "2023-10-23T00:00:00.000" +| SORT nanos DESC +| KEEP millis, nanos; + +millis:date | nanos:date_nanos +2023-10-23T13:55:01.543Z | 2023-10-23T13:55:01.543123456Z +2023-10-23T13:53:55.832Z | 2023-10-23T13:53:55.832987654Z +2023-10-23T13:52:55.015Z | 2023-10-23T13:52:55.015787878Z +2023-10-23T13:51:54.732Z | 2023-10-23T13:51:54.732102837Z +2023-10-23T13:33:34.937Z | 2023-10-23T13:33:34.937193000Z +2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948000000Z +2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z +2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z +; + +implicit casting to nanos, date plus time to millis, equality test +required_capability: date_nanos_type +required_capability: date_nanos_implicit_casting + +FROM date_nanos +| WHERE MV_MIN(nanos) == "2023-10-23T12:27:28.948" +| SORT nanos DESC +| KEEP millis, nanos; + +millis:date | nanos:date_nanos +2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948000000Z +; + +implicit casting to nanos, date plus time to nanos +required_capability: date_nanos_type +required_capability: date_nanos_implicit_casting + +FROM date_nanos +| WHERE MV_MIN(nanos) > "2023-10-23T00:00:00.000000000" +| SORT nanos DESC +| KEEP millis, nanos; + +millis:date | nanos:date_nanos +2023-10-23T13:55:01.543Z | 2023-10-23T13:55:01.543123456Z +2023-10-23T13:53:55.832Z | 2023-10-23T13:53:55.832987654Z +2023-10-23T13:52:55.015Z | 2023-10-23T13:52:55.015787878Z +2023-10-23T13:51:54.732Z | 2023-10-23T13:51:54.732102837Z +2023-10-23T13:33:34.937Z | 2023-10-23T13:33:34.937193000Z +2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948000000Z +2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z +2023-10-23T12:15:03.360Z | 2023-10-23T12:15:03.360103847Z +; + +implicit casting to nanos, date plus time to nanos, equality test +required_capability: date_nanos_type +required_capability: date_nanos_implicit_casting + +FROM date_nanos +| WHERE MV_MIN(nanos) == "2023-10-23T12:27:28.948000000" +| SORT nanos DESC +| KEEP millis, nanos; + +millis:date | nanos:date_nanos +2023-10-23T12:27:28.948Z | 2023-10-23T12:27:28.948000000Z +; + date nanos greater than millis required_capability: date_nanos_type required_capability: date_nanos_compare_to_millis diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 4fcabb02b2d4f..f766beb76dd3d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -352,7 +352,10 @@ public enum Cap { * Support for mixed comparisons between nanosecond and millisecond dates */ DATE_NANOS_COMPARE_TO_MILLIS(), - + /** + * Support implicit casting of strings to date nanos + */ + DATE_NANOS_IMPLICIT_CASTING(), /** * Support Least and Greatest functions on Date Nanos type */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java index d59745f03f608..e15731ca79038 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java @@ -118,6 +118,7 @@ import static org.elasticsearch.xpack.core.enrich.EnrichPolicy.GEO_MATCH_TYPE; import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN; import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; +import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS; import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_PERIOD; import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE; import static org.elasticsearch.xpack.esql.core.type.DataType.FLOAT; @@ -1050,21 +1051,23 @@ private BitSet gatherPreAnalysisMetrics(LogicalPlan plan, BitSet b) { /** * Cast string literals in ScalarFunction, EsqlArithmeticOperation, BinaryComparison, In and GroupingFunction to desired data types. * For example, the string literals in the following expressions will be cast implicitly to the field data type on the left hand side. - * date > "2024-08-21" - * date in ("2024-08-21", "2024-08-22", "2024-08-23") - * date = "2024-08-21" + 3 days - * ip == "127.0.0.1" - * version != "1.0" - * bucket(dateField, "1 month") - * date_trunc("1 minute", dateField) - * + *

    + *
  • date > "2024-08-21"
  • + *
  • date in ("2024-08-21", "2024-08-22", "2024-08-23")
  • + *
  • date = "2024-08-21" + 3 days
  • + *
  • ip == "127.0.0.1"
  • + *
  • version != "1.0"
  • + *
  • bucket(dateField, "1 month")
  • + *
  • date_trunc("1 minute", dateField)
  • + *
* If the inputs to Coalesce are mixed numeric types, cast the rest of the numeric field or value to the first numeric data type if * applicable. For example, implicit casting converts: - * Coalesce(Long, Int) to Coalesce(Long, Long) - * Coalesce(null, Long, Int) to Coalesce(null, Long, Long) - * Coalesce(Double, Long, Int) to Coalesce(Double, Double, Double) - * Coalesce(null, Double, Long, Int) to Coalesce(null, Double, Double, Double) - * + *
    + *
  • Coalesce(Long, Int) to Coalesce(Long, Long)
  • + *
  • Coalesce(null, Long, Int) to Coalesce(null, Long, Long)
  • + *
  • Coalesce(Double, Long, Int) to Coalesce(Double, Double, Double)
  • + *
  • Coalesce(null, Double, Long, Int) to Coalesce(null, Double, Double, Double)
  • + *
* Coalesce(Int, Long) will NOT be converted to Coalesce(Long, Long) or Coalesce(Int, Int). */ private static class ImplicitCasting extends ParameterizedRule { @@ -1245,7 +1248,7 @@ private static boolean supportsImplicitTemporalCasting(Expression e, BinaryOpera } private static boolean supportsStringImplicitCasting(DataType type) { - return type == DATETIME || type == IP || type == VERSION || type == BOOLEAN; + return type == DATETIME || type == DATE_NANOS || type == IP || type == VERSION || type == BOOLEAN; } private static UnresolvedAttribute unresolvedAttribute(Expression value, String type, Exception e) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java index 6ba2d8451f956..0847f71b1fb01 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java @@ -13,6 +13,8 @@ import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.common.time.DateFormatters; +import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.QlIllegalArgumentException; @@ -51,7 +53,6 @@ import java.time.Period; import java.time.ZoneId; import java.time.temporal.ChronoField; -import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAmount; import java.util.List; import java.util.Locale; @@ -200,6 +201,9 @@ public static Converter converterFor(DataType from, DataType to) { if (to == DataType.DATETIME) { return EsqlConverter.STRING_TO_DATETIME; } + if (to == DATE_NANOS) { + return EsqlConverter.STRING_TO_DATE_NANOS; + } if (to == DataType.IP) { return EsqlConverter.STRING_TO_IP; } @@ -514,13 +518,12 @@ public static long dateTimeToLong(String dateTime, DateFormatter formatter) { } public static long dateNanosToLong(String dateNano) { - return dateNanosToLong(dateNano, DateFormatter.forPattern("strict_date_optional_time_nanos")); + return dateNanosToLong(dateNano, DEFAULT_DATE_NANOS_FORMATTER); } public static long dateNanosToLong(String dateNano, DateFormatter formatter) { - TemporalAccessor parsed = formatter.parse(dateNano); - long nanos = parsed.getLong(ChronoField.INSTANT_SECONDS) * 1_000_000_000 + parsed.getLong(ChronoField.NANO_OF_SECOND); - return nanos; + Instant parsed = DateFormatters.from(formatter.parse(dateNano)).toInstant(); + return DateUtils.toLong(parsed); } public static String dateTimeToString(long dateTime) { @@ -639,6 +642,7 @@ public enum EsqlConverter implements Converter { STRING_TO_TIME_DURATION(x -> EsqlDataTypeConverter.parseTemporalAmount(x, DataType.TIME_DURATION)), STRING_TO_CHRONO_FIELD(EsqlDataTypeConverter::stringToChrono), STRING_TO_DATETIME(x -> EsqlDataTypeConverter.dateTimeToLong((String) x)), + STRING_TO_DATE_NANOS(x -> EsqlDataTypeConverter.dateNanosToLong((String) x)), STRING_TO_IP(x -> EsqlDataTypeConverter.stringToIP((String) x)), STRING_TO_VERSION(x -> EsqlDataTypeConverter.stringToVersion((String) x)), STRING_TO_DOUBLE(x -> EsqlDataTypeConverter.stringToDouble((String) x)), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseTests.java index 35364089127cc..2deedb927331d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseTests.java @@ -204,7 +204,7 @@ private Page randomPage(List columns) { case BOOLEAN -> ((BooleanBlock.Builder) builder).appendBoolean(randomBoolean()); case UNSUPPORTED -> ((BytesRefBlock.Builder) builder).appendNull(); // TODO - add a random instant thing here? - case DATE_NANOS -> ((LongBlock.Builder) builder).appendLong(randomLong()); + case DATE_NANOS -> ((LongBlock.Builder) builder).appendLong(randomNonNegativeLong()); case VERSION -> ((BytesRefBlock.Builder) builder).appendBytesRef(new Version(randomIdentifier()).toBytesRef()); case GEO_POINT -> ((BytesRefBlock.Builder) builder).appendBytesRef(GEO.asWkb(GeometryTestUtils.randomPoint())); case CARTESIAN_POINT -> ((BytesRefBlock.Builder) builder).appendBytesRef(CARTESIAN.asWkb(ShapeTestUtils.randomPoint())); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverterTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverterTests.java index 8a57dfa968ccd..9a30c2281d742 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverterTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverterTests.java @@ -7,9 +7,11 @@ package org.elasticsearch.xpack.esql.type; +import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.esql.core.type.DataType; +import java.time.Instant; import java.util.Arrays; import java.util.List; @@ -50,11 +52,19 @@ public class EsqlDataTypeConverterTests extends ESTestCase { public void testNanoTimeToString() { - long expected = randomLong(); + long expected = randomNonNegativeLong(); long actual = EsqlDataTypeConverter.dateNanosToLong(EsqlDataTypeConverter.nanoTimeToString(expected)); assertEquals(expected, actual); } + public void testStringToDateNanos() { + assertEquals( + DateUtils.toLong(Instant.parse("2023-01-01T00:00:00.000Z")), + EsqlDataTypeConverter.convert("2023-01-01T00:00:00.000000000", DATE_NANOS) + ); + assertEquals(DateUtils.toLong(Instant.parse("2023-01-01T00:00:00.000Z")), EsqlDataTypeConverter.convert("2023-01-01", DATE_NANOS)); + } + public void testCommonTypeNull() { for (DataType dataType : DataType.values()) { assertEqualsCommonType(dataType, NULL, dataType); From 5d9c3a26631fa21ae72630fd76eec2b4eaea945f Mon Sep 17 00:00:00 2001 From: Mark Tozzi Date: Tue, 17 Dec 2024 15:56:51 -0500 Subject: [PATCH 046/119] Remove date histogram boolean support (#118484) This removes support for running date aggregations over boolean fields. This has never been useful, was deprecated in 7.x, and is finally being disabled for 9.0. --- .../src/main/resources/changelog-schema.json | 1 + docs/changelog/118484.yaml | 14 ++++++ .../DateHistogramAggregatorFactory.java | 44 ------------------- .../DateHistogramAggregatorTests.java | 8 ++-- 4 files changed, 19 insertions(+), 48 deletions(-) create mode 100644 docs/changelog/118484.yaml diff --git a/build-tools-internal/src/main/resources/changelog-schema.json b/build-tools-internal/src/main/resources/changelog-schema.json index 7d35951eaa2cf..9692af7adc5e6 100644 --- a/build-tools-internal/src/main/resources/changelog-schema.json +++ b/build-tools-internal/src/main/resources/changelog-schema.json @@ -279,6 +279,7 @@ "compatibilityChangeArea": { "type": "string", "enum": [ + "Aggregations", "Analysis", "Authorization", "Cluster and node setting", diff --git a/docs/changelog/118484.yaml b/docs/changelog/118484.yaml new file mode 100644 index 0000000000000..41db476a42523 --- /dev/null +++ b/docs/changelog/118484.yaml @@ -0,0 +1,14 @@ +pr: 118484 +summary: Remove date histogram boolean support +area: Aggregations +type: breaking +issues: [] +breaking: + title: Remove date histogram boolean support + area: Aggregations + details: Elasticsearch no longer allows running Date Histogram aggregations + over boolean fields. Instead, use Terms aggregation for boolean + fields. + impact: We expect the impact to be minimal, as this never produced good + results, and has been deprecated for years. + notable: false diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregatorFactory.java index a8ccd1c76d031..4d0f58756b11c 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregatorFactory.java @@ -11,7 +11,6 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.common.Rounding; -import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.aggregations.Aggregator; @@ -42,49 +41,6 @@ public static void registerAggregators(ValuesSourceRegistry.Builder builder) { ); builder.register(DateHistogramAggregationBuilder.REGISTRY_KEY, CoreValuesSourceType.RANGE, DateRangeHistogramAggregator::new, true); - - builder.register( - DateHistogramAggregationBuilder.REGISTRY_KEY, - CoreValuesSourceType.BOOLEAN, - ( - name, - factories, - rounding, - order, - keyed, - minDocCount, - downsampledResultsOffset, - extendedBounds, - hardBounds, - valuesSourceConfig, - context, - parent, - cardinality, - metadata) -> { - DEPRECATION_LOGGER.warn( - DeprecationCategory.AGGREGATIONS, - "date-histogram-boolean", - "Running DateHistogram aggregations on [boolean] fields is deprecated" - ); - return DateHistogramAggregator.build( - name, - factories, - rounding, - order, - keyed, - minDocCount, - downsampledResultsOffset, - extendedBounds, - hardBounds, - valuesSourceConfig, - context, - parent, - cardinality, - metadata - ); - }, - true - ); } private final DateHistogramAggregationSupplier aggregatorSupplier; diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregatorTests.java index 38294fb030ed4..bf26326abafbf 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregatorTests.java @@ -83,9 +83,9 @@ public class DateHistogramAggregatorTests extends DateHistogramAggregatorTestCas "2017-12-12T22:55:46" ); - public void testBooleanFieldDeprecated() throws IOException { + public void testBooleanFieldUnsupported() throws IOException { final String fieldName = "bogusBoolean"; - testCase(iw -> { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> testCase(iw -> { Document d = new Document(); d.add(new SortedNumericDocValuesField(fieldName, 0)); iw.addDocument(d); @@ -95,8 +95,8 @@ public void testBooleanFieldDeprecated() throws IOException { new DateHistogramAggregationBuilder("name").calendarInterval(DateHistogramInterval.HOUR).field(fieldName), new BooleanFieldMapper.BooleanFieldType(fieldName) ) - ); - assertWarnings("Running DateHistogram aggregations on [boolean] fields is deprecated"); + )); + assertThat(e.getMessage(), equalTo("Field [bogusBoolean] of type [boolean] is not supported for aggregation [date_histogram]")); } public void testMatchNoDocs() throws IOException { From 1054503ba8d42b8f592c20149eaf131a129576d3 Mon Sep 17 00:00:00 2001 From: Pius Fung Date: Tue, 17 Dec 2024 13:03:28 -0800 Subject: [PATCH 047/119] Update start-trained-model-deployment.asciidoc (#118887) Updating with changes in https://github.com/elastic/elasticsearch/pull/115041 --- .../apis/start-trained-model-deployment.asciidoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc b/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc index 6f7e2a4d9f988..bf9c4d14db290 100644 --- a/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc +++ b/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc @@ -138,8 +138,8 @@ normal priority deployments. Controls how many inference requests are allowed in the queue at a time. Every machine learning node in the cluster where the model can be allocated has a queue of this size; when the number of requests exceeds the total value, -new requests are rejected with a 429 error. Defaults to 1024. Max allowed value -is 1000000. +new requests are rejected with a 429 error. Defaults to 10000. Max allowed value +is 100000. `threads_per_allocation`:: (Optional, integer) @@ -173,7 +173,7 @@ The API returns the following results: "model_bytes": 265632637, "threads_per_allocation" : 1, "number_of_allocations" : 1, - "queue_capacity" : 1024, + "queue_capacity" : 10000, "priority": "normal" }, "routing_table": { @@ -229,4 +229,4 @@ POST _ml/trained_models/my_model/deployment/_start?deployment_id=my_model_for_se } } -------------------------------------------------- -// TEST[skip:TBD] \ No newline at end of file +// TEST[skip:TBD] From 9c3d6ca9ec390b163ce8b5c6aaa46854152a87c7 Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Tue, 17 Dec 2024 17:48:37 -0500 Subject: [PATCH 048/119] Mute test (#118897) See https://github.com/elastic/elasticsearch/issues/118896 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 93d1a6e6374b7..b5712b22fe583 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -302,6 +302,9 @@ tests: - class: org.elasticsearch.index.engine.RecoverySourcePruneMergePolicyTests method: testPruneSome issue: https://github.com/elastic/elasticsearch/issues/118728 +- class: org.elasticsearch.smoketest.DocsClientYamlTestSuiteIT + method: test {yaml=reference/indices/shard-stores/line_150} + issue: https://github.com/elastic/elasticsearch/issues/118896 # Examples: # From 127fa2782a4b40cf4b47de3529df0b2308fbddc6 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Tue, 17 Dec 2024 23:53:24 +0100 Subject: [PATCH 049/119] [Build] Add 8.16 and 8.17 as base branches for wolfi updates (#118726) Furthermore add auto-merge label to PRs created by renovate --- renovate.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index c1637ae651c1c..71c6301f8e0c2 100644 --- a/renovate.json +++ b/renovate.json @@ -7,8 +7,8 @@ "schedule": [ "after 1pm on tuesday" ], - "labels": [">non-issue", ":Delivery/Packaging", "Team:Delivery"], - "baseBranches": ["main", "8.x"], + "labels": [">non-issue", ":Delivery/Packaging", "Team:Delivery", "auto-merge-without-approval"], + "baseBranches": ["main", "8.x", "8.17", "8.16"], "packageRules": [ { "groupName": "wolfi (versioned)", From 41c3dde414c35bca765be46b90b43f918381e315 Mon Sep 17 00:00:00 2001 From: Satyam Mishra Date: Wed, 18 Dec 2024 05:02:39 +0530 Subject: [PATCH 050/119] Updated ilm docs as per the issue (#118148) This PR updates the Elasticsearch ILM tutorial for the newer screenshot and the primary shard update in the text. --- .../example-index-lifecycle-policy.asciidoc | 4 ++-- .../tutorial-ilm-hotphaserollover-default.png | Bin 219715 -> 1427109 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ilm/example-index-lifecycle-policy.asciidoc b/docs/reference/ilm/example-index-lifecycle-policy.asciidoc index 6ec261fabc448..0b3c17fb2caae 100644 --- a/docs/reference/ilm/example-index-lifecycle-policy.asciidoc +++ b/docs/reference/ilm/example-index-lifecycle-policy.asciidoc @@ -24,7 +24,7 @@ and retention requirements. You want to send log files to an {es} cluster so you can visualize and analyze the data. This data has the following retention requirements: -* When the write index reaches 50GB or is 30 days old, roll over to a new index. +* When the primary shard size of the write index reaches 50GB or the index is 30 days old, roll over to a new index. * After rollover, keep indices in the hot data tier for 30 days. * 30 days after rollover: ** Move indices to the warm data tier. @@ -84,7 +84,7 @@ To save the `logs@lifecycle` policy as a new policy in {kib}: . On the **Edit policy logs** page, toggle **Save as new policy**, and then provide a new name for the policy, for example, `logs-custom`. The `logs@lifecycle` policy uses the recommended rollover defaults: Start writing to a new -index when the current write index reaches 50GB or becomes 30 days old. +index when the primary shard size of the current write index reaches 50GB or the index becomes 30 days old. To view or change the rollover settings, click **Advanced settings** for the hot phase. Then disable **Use recommended defaults** to display the rollover diff --git a/docs/reference/images/ilm/tutorial-ilm-hotphaserollover-default.png b/docs/reference/images/ilm/tutorial-ilm-hotphaserollover-default.png index 14ff66e4108354f16240ca60b7f16dce8fc6b963..d7f314cedb2612b1a9f4b534e697328ef86f8296 100644 GIT binary patch literal 1427109 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%0D4jeX~+XO(&AF&d+ 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

n5%juiXyO41NIclHzj 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 z z_NU^Q)t27+S$or%G-4qo=>9u_%a{X6s zkE^=l!*{+UL|ztJ9?kJj_)xn~yC};LSGNNiOUXhu>*{)k|+ZB#`#>nz@ZsOZ&(=`G}blXePUI%pQZ*w7>K=j57 z<{TFyG|V~Ubdiv}$%KzJZYLk=zAp~kM=dYZm1oE0_W?V|9>vO*pA*jyWSO-grzUM4 zqwl{YbJ}|d<4gV+ceS5960Dnloi;>>{amNU4bAYe@AgaE5BlT^buIPYjwLn=VBph5 zk8e5^^foc^0~UI~)NAQ!Pw1@s-9alswc~?9z)NB@f7b6q`Ang-ViD}z?TJ%ULedLn z9GzpUK-ZeZ*m4D1_HJ=PV=Q#Uo!(4LAay+GwN$5w@pzO<`1x+y<>qv;R}f&^xLaYQ zv;1fjZ%ueYK|(3L5i#cH8@}HX=$~0<=2?r_%-L>7=OEIP%H}n7bh@hgsXNIkTAs;> zAHRx*&Q>0Y0RB{fJ3$UC#?r8m!9ePrAi3~wNz1IJFnYjL$l(h!Aum*WC$6ub({>dQ z++PAHL#VtcfPkA^{0i3dIFpAIq=N%gkkrvcV(kuG-?uB z0Yy!;q{KOoVoEvv36@Lu*44D?R&~Vq8zPSHU@dZu@MgZ{*0^WhEiU5g@Xbp2eu8Vy z@Ya_u)2+n*D=xbo`)PJ4ni{J&D53N7f|q3vG#hdK zIlrn)B}2z@`n46-gX=(lKpupmtBvaJ?dKPpf9?(zl`+@vH>bvhBbWITv(wMejCG+Bdi%aQ47Z{N3gHm5xaq0>&hTm$<1|bTc@t zQP)R;&Yi)AudS^MrmW-j<)igHu=cWiD4i{$=t0;6h&D&ZpeRBkmSQ?DO5MAy6uaGS z0?FL6=1>=D2YBJ0BfNKy*J(!0W%`vfCY@1t5!2~Y_qxY!V+wX?S87vd&_#aZ;nZ|0 zzq`rU=$a#XNM4QgrJLQ;V5P^&wIJ4Wr6MKJ_-u;)P|I^Z!u0J2joJ|Cm;H4A6Rfs} ztendQ({T;KztQ$9cZ~SrR;54A_bAs(OdhZ|53sB|Zu{l3x9ZMTQM!x|3oZ}{W_O<4 zB2@ip2av8pyZvZXQNq~3Lr+;H1aLlmiYJiN>jP}G;GMVaoyM2NfTPgcS@Dm81&&Zm zMaiv|OG#BrIrb+BSgnyt2nrw`hyA;a7vq4vJpiiyd61%Fu^LQ;?BY=^%KI z&s$dV42AWh%xdJxUuSJ`2xQAV+tU~OWYzkQlHdtvbDFRx2m+s04NH+7EZ`#_a?pRh z7h}GR*u5oy)#R+c7y46UghZ8aAM91i7JKP3k%>!=5~DI4fBP2z{v>F;&#_Gy)hk+H zykjD!FGE<{_ctV5?l=g|vN&67U4$h8*oMQf3SZoa*XD-A_wGcOoUZ`l`<9m?;$6p1 zXXw*d%qd)a5Zt5i2F!nK+?VKx*saJ>CyM%IJ(Q|HX}*Y`_s)+Nc}0V~AIJyu^E>R8 zFgrc&Rhk>=L+tUOl%4hU?y#hcvyD#XZ1b7ax8E24MFRr)Dx*0iQff~jXUp+|q9T&b z1*g`0YpZ=TUTY+5zQ5tpvYm-MoX;92J0^DGK}y@$QE#nLT_P_>GBtrj^S!$C0@w;C z41!YV&Ni82r;eq%rY_^9Ng&gFc-PCTf~l6v)6f;KGsA_tvN+#bEs*QJai;5T)yy?F z@N5ucneDYz9D(cqIp4fbsd8L4kj^6nM72Uk1@Ac|cCGHO%VfjAO%(5@3%Jld>=$47 zB5Dk)ynBDJv9a&3R|arHF4*vejY5{wAULGw`8kjAax{<3)YR08pOen>m)m-cF==wG z28NPFKPl~0n0}>pkNXVC+3vN$eQa&l1r7wHGbGxEj>6mhc>Un# zR!(-p(cOJf%N{qu+8!541XkY;b44e6f4`LxJ}C&!^jL}OQ8ZbIreA*PBtEe;H>P^# z<0mO`&yi<^ji>BIp`ksDEl1ObjT2+ib1s$GCrw`D&BAD%NI6tztJJ-d|B4;nHQz21 zgOztApk?mpds#!vQ(*Y~^~a)Vq2xdc9-U@8;`(*Cvw_#8xPqc$s-!zr*H`zy~L=v5^y#efoIv*mJpYpIN@*ih^FXQn`7h1HTHk!JTZ=uOVruqbUg zs(4t*!OZ8Rn)8XqOt&eM(z31nkY~!2**28N<@V4jNXrQD483<>>H>oR3$E>>dBee( zaQZ~z_dA*DViVvf>gF5ZM_C^pu55{_pkVkhHS<~9@xG+rx$B~z=ld6kHF?FU;8}Zn z2$`X01mp1h^itYuLfin1yw}GIt>M9;BikX|dh)A&lpKnO-#lZMAiYdkvlQNUliz$V zA24<9FZvUcg6_IzQ=GT9Ob5(3lr&TH)sLh$g%~&+E_5Y#9t+vA-oFWwL2;3BSWypk z8Wgr9d_h?nxg5XaZ!5gwzJBJpA6L9k>qX@nczCzzZg0`qf;M2rYelJXs=EyD&Y|7m zhWLc#8Utia@nvsW#p(5H*=exbsqN14`UCbEx1r){tZ`5PUFi@2K7ARSaqJd8-jE6F zkuK1X@7i>@+03%1y~A4wBP82r!BV+2b&&#ypz zo(FedUex=f5BTJ?fW`rO+{ZBBaLY-E7LPPiTkBpGe%+Xh9H)I#BH*+^Ia9g(b$rwv z8B(9@+p{6%YW8CZOGuuS#^qVP&n3HytLw{%4_XxSae*O$A0~Ld)EKk71D0F{zEIZ@ zzcD~CIa)60t1d!LTEuP<5A63=G7I&kL&-SNHO$%YgP9~ve}(j zdSWtbvzooF$Lq{T`n|GbkxE(@o?&aEyeWo>PITf41Y4une2TQw$PXfRNn{9L;ykuq z8D1>T86s$V#GAFJG9LCp#|q*h>=*`SK7eR3+?U@2EID0R}!FM)7iU2i=h^G00YlY;m|omIq>x}!6t z24=8RDeG)mnZsZP%L_9jqw7c%h|e+W7IYSu0-QRns@HSC3{rGeX7%^`;aD*I7!vW% z{3Tc!dHx{RaOMs%RrR3{rEz<0_;i>Ac)Ap%5Oa{RYga%q-4Ut$Hrmteb+qd(o9rW5 ze3*0~rD@sAODW;eg(@HJ_B*&w%gtP9&+sLcb=LeqL(OA107>p+N&d?S$Y z>@teV-ty$$c4iL0p!ChyCMMTLm*QAn5!=rLaYRz$*RIYPmn9J*@5iEL_{kZqNoRoP z3SyMoVMmJZqvdy20#x{2>wF+g)&J{dlcHf^08(^+dsRxpY2d+r^6UR$$o~Ieud18@ z@xVl!ytP$pzloj3esM3r-RM|q`und*th|r7(-Y8U_G=_I)US&N*9FRAsF{;=)Asd` z0m8~N#9*-x`e1#FCN3;*C$d<8%708%O|+7avUsar4J5|AI(_&EQG|OA%*D#3NLs4! z3u7NXD>%#gs)A#XBdKd&W68UcdJH$0e9OfRgEY?zOTy#_xRvZ_JO;-1Vvc;`h!e&g zDb__H5(thX@#q{p9fKXsgmp&FqkoJ!c@O?=_s4P--oM*}msls-o}3JAH->cwUWG+S zJNjOI%h#5rJ&ytO6*ajXRk;Qe49Y%nTED#wJtf&+MCYIJ%8`D$x3q31J86CMqI-CE zPZ8z*#b~fk*E(JA?KrBt1lccZ!uc15r!{G4mOzkA@>uT#IB{Q^YxZFY2z(- zIO?pQ{n}p5K#4- zZf>@GlfWquZ$8JK4QFWf_h4Zg0)|WRWw4=1awsg)n%lz`9yZFfi)8D_K!`lfH*V#= zco=tsr@l!PGfUWk&q-IO4DWsaiK(2#9lLuLu%kPc6wn-^?dW;k$kGvQFrk;d^D+(^ z`lmT2Z?LOO8s_(CZD>BQsV+g?fCoKP7Sye5OiJJiA7WfB9517LfcWauB;xK~3s!Ee z9Xx-1pWQ{{H07L1U)frUb7wOUY)A{|yL;g77ZK$)3r3pQ{Vb9f41|69JHpIKoX81A z)tug!*I}3v2&s_w5_w;+PFg}37!+H#u0eA-^>{cmj`Kc$-8!jg_b(AuTL0dI`frKm z=u)!+?XQDvIn1z17ZVn22kTEduar+mRfTOXY8P}>cMc`EMYvxmQ&d1ZXz4uJoCNfB z#F;bpZRT&O2Eo53eTsiId8d6h~iY`&Ukr0A3jULUt~|y)@}w@ zMj3!1FZ5as_a%-i=O4@N4^*A=25}9R(}(g)oVw&vK!Gdw&P%Z6(EE*x3GFr0!obKG zJ&_uy-W_~-ty1d>QWgQxFAq8Eq5S}xTn1L6;)8RWYUS{ z-kVt0u%jCF-f0vrIYid0{g4m`utES6<78|x6|$1PKlIaSb*vr=pfIco)5- zoXxbaU{lX~(^}6L>N%1r9$a0@8<5BInqWQ<(EJA(ZS`Etd`^k`Zf|c#P$7!nSjKBe*gH}$n-{BzZv7z-W!>(%o*F$cL!fJ#e;v4f@en<_FB_CdaPsDKpx#{fG_(VTtS;FNg1l`bqd-Mg+!;luKWQEycA0B0R9%bDw*7>9q3_B8b zwdyM_q$c(b|K{Mf?7OP+My3xpY;^Hai2Q5kJ)BR}ME{Qgc8%%`{06LuIXZLY*^*44 zrCR@LU59){qhWVe^bUWiGpxWLz`(v&WomZugSls9FEeP=v7h$6jyZOo)h7x*b8)-R zT8R#)aOSsOb0pG5@}@F;iV}K5W2@KCgz8Qx0)a(>9r+c*)Svag;Z{OQS~}RwAa`^_ z_~Blfuqke5tPLXHBHrYkzCK=!WfUNj!yT@^JI{aBa=qrY@NM*$OYC<|ii) zvE?tHf96^{Ab!y|T#E|wxb~5lKnrsled4Nr9Z_DSj%79ylMDSN!qbIuEy`3gqr78E zh*Pnj8I}7X6_PC*4+socZ00Nbd0bQ^Za%6QcynC5I8c)z7U&vep-ZCrGnmkSkyPm& z9Y~Og-Sw-uETWK>%XqmitC~IsMQ3rW)Z17F(0Tb8$L^ozHkUi)x=`fP|Hf>zrNy0L-F4Y%}%?IjJ$RAZ@is{@H zE~lnWT5N5PX5Vtpl$E7l>ZW{<`-kRhy&Z6@AO(^qurU?Gg0_R`8PNlZC~1ewvmAPi zU2$ny`lG)n)2{=YHkkYCt5saHMO7Y)K;3MVNqKNwrIj8U|MnkKdLZ66d04E z=nM15)2O@Z=T}z;TyhIV5vz#3P+s^a0KQzJI)h!+j3*2(`0aXb%mytpFE-gOGZ143 z5hnKhg0U}4! z(9u;w8swbAMYEr0rONdgj;0`C7oE{QfEGmiquDI}v*vs9KJ*0aC#0uc%o9(KW-Nn- z<(FRy)hRP2F<5>3q^0G-l$fr1=AjI4LB;83%2Em7!a^SfStsb|`~0dg=~Y--`dak| zH>nUROaC%WUf6pAtZjJ|Nh26wzXoF*6El5#F@@zt{H5m3Bk zizH2&_VZcwsF8x&{H9<;Q1c}pYSfU5Z6N{E{%E641O@K#dQ4Ft(`PQj_qsNifMe!@ z9Tk3^`Svs0lM}>lJ!GRX#o&#tIaUS+nV}0!PW!ILhv?H7yL}A_T#86P6&5p|Z#za@ z?K>p+;w~a3vVtXv&!vIpmaP>R2VU#pN_OM%Z~;^GfcK{C%e!f6X^l?G(R3zSCrset z;mJkCZNn=8s;Zku=-Xx|;L;mHE$Vh&-!3r(l>Z!L9fwCZIj_>~LH$uQeF2_)SfQv^ z?@WF8>>=m=_6GMuC3Rbj>RtZN(juRYGNe@=N+DUkkMHVksG!9YxYZ*s0k4Cq4y0rF0a>| zSqK5uJ9kwwQMZ$gS;6(98s@@JaUe*n4O@N88KfbSk(M)Gbg_3Jj$Y_`Ct-iS{VUhh zCFxaq#mk5wp`BPb$Y&+*lU7o-+vvAb$cC?h&o$>W7nus(LV8Tr;$%}}U4hjs>H@E( zG!aL!er}Oyq$@uV5Miv)8ZwoiUgX$uRRScq_FkQ7%WGRQQ_b0%_4em@%v{mrjbjiC zIdr-xm`+)z{C$2TV4qjU9KLG@(xT_G1;7*Y$G)hrT0pt*by&{%g9f=Uo-EPK{lD0c~TpW0s9jbf7Ix zR~*)+1EFZ#PMFND^MPkEpI0AwDcdjffDq=NM}g<9ZT1XL zpoj&!Nx8X@;8R3@`Jh-0YWmIatg(;c90)R%EDd|@+ zb8j-I?wwVyf*O7tNKRzRY1siC0Uw8K1Fk~d)z3>lg{bF;IIQk*Lt%S8^dZr?O-+<}b0w6|X5{UdalFfGrjUlFF{+77 z1Kq3B^|Ow)?r2!&<3tvCfi&7YPQDmBxS*=dI$fx%?`1RxJ-W0o&aYy#;EX6#)UI*7W>QczxjLJViT5b+-ZdWdX>J}* z2pvL~0}1BS$EFl0e9!K$+h$sdE7G+WX7ghOlNi4EUOubW0(T8S?Q zQn$VbEY#1MxKhP!FxFcinN)+#%Mc;>*9F!%_u} z)m4{zfw%7W=wm`BUoh}1m`L!4_9R3Ok3@9Z8Xq#JrpPVc(Pt?n>>tpjDKv>Y1dk@v zN7_2yinp7>e?q^nHYK4EWRBlm5c33}1Yy*fvS#a4b1K6u_Wz=IpB^gi8*0j%`kwYSGikK9< zL)sedb)fC4(ZnRE4EhwUWW+xRNr);vr+X*782|7ZkmFUm@Q=45UWkjcV%(mMbFSY3 za#=TgnNV%wD9mv5!2R?PYs%E3!7FR}MkG!#aMsE$>1GfnzZW9!nZbMF**VTai7z3^ z5U{5+!w@?6(^+xZzDArs2s)2k$f*hEQcgSH&;Dn$(`&Z>Kd0y%x{=WBm>2@cCEFWN2w$ST!I-?;S#`uz0dS$ z!zO(0SK_^WWVrFuW~|uGNZ8*&t=ySEC_!q&$cnPg95DYhMg+wLp;9gj*qe{jz`f<2 z``;(XlB$(IgfvP1mVEAd`9wLx`8XXQcMM6G`#%Ff{`U<9djl*qSlFY~iSSCqT}kFS+)m7h5*-!8-%ml?bHopjdc>Nydgy#;I~txN=20H;^DjAk z+v*0p+{J+CpFYT()#nAUj+bV95FihUE-8ymurvDYLa{^f3UWRl7d7p4bV|Q7mm&US zvEL@v#I=p9Jez^XC0>+A>~O^Is-#K30oA-p6+zKw&MouML?*#ql=L-eo#Ma^0ON)f z(g$o~*z?(S!!pI}(v?!Ab{@wtRv*9CGfl7({(B?{A=`hYG-^wWt8Db)tVtK;cS&O`d+n?KH;TYJKne z8>1*MU`-Ug<=>y|fkUqxBzJE8Wu@~~q^@>b#oJG6pAEBp=R3jPO-!op&vEI1)H_$H z4j_SEhPaN{SW24pT`g+*(;%F0kM*)Z5xB`r`}j;g!s}zzh7b> z%65xP#^P2v*s+9@v!y1=Dw(_t{IW1G%zW8Hk8u%J5^yF`D$ViE8vM?BgiHYyI5(YT z^g>`QYA*>q^Hy*g&4;(8h7@HIQ$l{T;AyHCNn6|f=7>s2H;3j4t4K?WUG`MimLb=p z7TeC%;4oE@828=wutIlYulYc%uD3>aFMpi2x3&0&pp^4>nZR z;4y=T!dmt=?K3Hk*<}xEk13O5Jx$VX(cXX?NA6UOR9{Bxv_e2)63Nhmb!YBOmZ1R; z%LOLC`?h+*)NfJlnmjPu5ld8hV&6(*DNIgDf!_an*nNPc1d-aMK~d6sw8Zzy*Md|l zE#}CL-3WA+h}6W!f4>pJ=9kMiR$!K>{Wc@QnEjq^-oy{MXYOL(2??kYgIpD)MHr>B zQeUXMLSisneicc9jR*-Sdjdr+Vx`2)h_ESBMRJcLT=n`X+w#jRjN3&X4s$oL%pm~m z=yLh7nX_N~>?qq;o0i;d;%{(~3djZ)k(u$_z)(4`VHRWH9|$#Aj%azVK7*{L7j)W= zUp&uC!&=4rrgfU%y^X@FQnxo(BL6>hokdh!Ti31wfgr&lxD^oG-KB8XV8PwpHMm=F z*Fu83ySsak;7)=&cc1^Zzs~Mdr$&uER;@W-d76>iPI8$h_Z6EuCg`4b=5iDZV`oe| z68k^iH`A8mjX9?=O`dlbfMmnygLq5m#%Xx5woLTyd#qC2R^8N#VeFn)yGEOwQJcru ze%EF38=vPiru^e+)h!{?d;8sAUzdF*qOEbEyE)#ph-U(tf4x0yfV?0}@hK#y9QKQr za#t5Il6NGIbs^T*Wg*+;Zah+VJf?m}p@6!2X>2gW)?RAcQ1*$xf_OJ^%_#=`($A(z zO$is|BQ?or5V^gd-cPp9JkstnGlkyI?6s#l1asqOFdoURx==B&=bk?SR7!25wvU%M z(SmtXzYhly*mdz`<+FKHPP<(|K)WAN?(^>uLZ=Zzt6#fml<@0d?z!+KCG24E3jA&If3*FuoJhU)hbd#QLC6 z1pj_`x~~SU&JS*OwgaLbLny1!^>~XRE+CX%jZ)D8oxBpL8Nh6pZXBmOGLq0ggKQa1 zFE+%cvc!(u&yLILLONd(V|*}yFi-$RI82GQAdG2}q+IvYF>a2~%9*i_yugxmQs`5J z%g-FKz#sXT2zXgF79gOHmhj`#w{e8vYz`R_@?;r)7(pPf&*koFW z$J;9fuZy`|l+I=U2{0}p{gD#J3vZd2OU_UJ8{Jc)KrBg}mnh>htF$Ft&w8?e40MoA z1F#+77&4y9itu@iT6Q>ZOl3AZGf)bZp5qC$QJjbA8#VZ_I~;Ae_qUf@;is58+-1i* z=s5axUmPJ|y5(}$bbPhdm@mQMM-WQZ_%{u`zQ2Y+`-ImfAomnR5H6P+ZV=pUDuGKs z4XN`6RPmXp;F(S502D$@2zso8 z>EK=T$P1{36ZjAYDRV-FF4p!x| zB1M{AJLLET_$CFi<<`u|09PaR=}98EFbGc!hKyo94-fQ+I|pU+9nW@@72py%jt){? zhnzAVoYgCuEtG=l^p-zp(E!a*3OUb*<2DQ~^)Pv`bVM4_1Mq$yLV5V^QYnbTeH z1AB#aC(UL%%t?lrU{MjE9&0iHjtMXmbS3C#PoIEjI_ny>0O7=TO{`BO+kNZx$D(y} znGWpJ4IU|uivdIxka5{z@(se{)AwtGx$+nEAB_-6vq$4)C})wFgLpGe0^2n)#sb8L z>Q}0T0}~&1Zd08Bn9Qiw@k$G1uRxCX^#gnu zIS}j&`)>CFz18}U7jS)GH)W^r@=r?#pGs8}&_^nSy8Ma~xb?QMwjW$yV82L?Ei9UHBOGs6F#nm9JjiYLr&sOU(CiIX zld-by2I{;+H*xhr+_Xn+S66^7pSw}Ur$Wg%!-I;|51Q%hDW-Hm!tPl`?nBYZW^)7o z==q=EKP`T%_gM|=c(qKdoOCaI*wIq;twq9~T+IF~6 z%G5H(#$;rSAV@^~Y0e5fAuNR_MTI0#vDuEPPm!=cw?xB`{>+GC8vq369V|OVy-P6I80z&gY*eXL|QsH1h8EQS0=F~iwJC{)DRKK z2St#FvANw)d-1N176GgKCNf=C(~`?>==`@#R#Oa)#VePQR~VsTngIyteDF8NuciqK@UCO0G#FD^GK5yoDcU;2ixf6b-L+gMF4mKQN`uCS9G687abB>CTd!7ECt`dh&FAI=%1TZ?7W!uGu6% zRCg?0YZ%d+x z;8Bw~&6Ka%rMRpBT6rR^mj34`R%V73wQEd9{2~~x>D*dkkKaM9S!MFle)L(1Sj^z;P5)H?fCL3tN zNLoyOk-RmwaGCi=XMQ3j1g_W_W85OT#0m(Xkl6E;F;ZFakgR zCrXzvvu7JH?^QAeXUs$A6LJMI?D;KB6{T@olGU3}gK*)u;g3DYw`SF}w6cJnA)`nn zf%9`UHNXo7SZa*+ls*uy|5RuS{!P`+>$eb|57>u8TnZB9Smi|%f0N+13uH`v>m?EKmH{Tb~>Y_hZ}YR-UeZ6DNj$l*z5v& z;_bXgwzBfdA@wQ$o|LOo!HVmHxEF#bamYN8JQ_*}LWYQVq4&FT4q8Cdl0u4aNAy&c zUEd#|h#(zkOXo>3_1pzzRZKzcWk3VzrPnk*Zwz!yz#2U<7nk~UH#8<&GoxAWhI!f5 z91Ku)dwM`WB&^zw4VV92rt1(zI@>{L=lI zwqbnvIrgWa{IcisIpp{C>6VIqci1dB-1RL%*GOL>YSAyss;jnX9*W?2?>mDTliz7? z3u?*v5lHzzoaWM!P-I7x7q0?>4b!sR)}()X4x>J9qIUI5mksz z!<+4U^BL4$K3g1r@il7Dp#I^Z>&3Bh5X6j3FSyP8U}Dn)fGbI$@V(deg+-Ve0#!ya{t zE_|3|yZCme05D@m*``<(#(4v0I4rIALg=%vwDYVL0^*n9zI7AmwCO&Rn`Y#LQ#f^3 zi$}Wq1or==7aVnUV61v@#StuI3Qs3d3vJ|1w_~%m4DMSC1{~z5Vxwd@s0nh##XF94 zU$n|J+t&%Ye)|4(Zyw%`c$vG$S142deyruNqQmEn^`enUQ6bwdE0gWH8VC6qe)BP< z!|n6Ze)|*+6-=qKzMYSq_T_2U4r1u*i6U{_8yzS&^yRb*9KbSFPM%xxcKXu&Oz=ik zJ2%s=+Z}s-3aL;SFFC9a-aJ1Ddo360CV58u6|D%TEIjO=vc0Uq(;Jm&Yc0t80wg<9 z+_%OqGnoXa9`+Pemw9?qr*kzzyN>8*a$l^!1pnPYzM8vQpK2Y-=4)-+%GCJP1@8Sv z;BuD{w(8@NG#cl!e{kQptE|*QbosFuc893C9rk4M{aj_fJoXi}32g=J=OOoo&E1z^ zuDu9{w(m+JB9$4TcXPo8%nV!)PbZ~s2&ra+zFb`fe@h;(FX?==H@i;gu$oCk9^Tqs zZ|^~2ICgxGK4XWe)m8pndlT8O;nT=nmg9m$Joa9Pa5UdruMU_lA#`{ygnxX+6tBh^ zQj<;B+wYc7i`x|l|4xeD>lE@HR?ayePi(%Ob)}%n$giTT#1_9Dd>Wqhu`S((x0#id zHU3-^!aCtj1pnag{X|o4wqab{+R!R-?5o@EoyFfmz~`BO+*Kc}SHY?}xeC5R@9@>g z74ef&zF^y@89lqS=~yoY3FFK;Ni1_b8tSg6IFC9~=r2g}Zgh?DP?%1lUy=J1tu;{UwlU0-`Ww&0V*b&UbiWZ~u5Um-j>gLwblSL%VOkl_5MDbfJDp zcgb}ADuJv~v{)tvpfvBo6eg4pIhC)-V%Cf4F zTBm(=KX`R3@WM68@!3aHl5=gny^(O?Nyl79C1>H~eYjuy?mJ~IkTk-%K=$1 zJSZFdfSjybn4wG_s-mka!j2DV=M#C=J4Wl}&>TpF1&VXgK9CQWp#VF=5^BTYZcFRYBYi$x(ECL-ZtvKZSnrN#nFdMOiew#5FlKpP)>c*@}u zEVldc1L$;AQAQtT&IkC9O=hMurqs6}EP3^exjODtJuh@F-)n1N96&SrTD6ODFXE&3 z-O2}``Qfp8kbeED%sy{;o`KBH$x)_rGPF}@Z*Tn3b}}Yuz=#k4PspY7Pnip!=BBm! zl+(vC8%YWE84~H@tf>Dq(Z)ZKfvv_$XKkA{p-*Adl`t+Wg8V8V`ER_=86lDsEaFO% zWRc(0+V@Gk;dX1uahRb$eazSq zRo-^mWn~ojeAz9ed@*}YgKmE*RX4FGRRLG4?F=Z;$ih>LoO2oTj4Rf|J=^4% zh$Z5)Sg?kN8OLGxGq4+HdNAP^I71Z+X7hk_ibKPS%$pBITe^H3v)O3M4kOXCr{udM zlH%o6$P~=3xkqKI8a;VwtO8hhG*^Zk!FlaHYD z;RHyXrE0fnM78|+Wm{6naD8bqSUK@f=unHWX@Cd>C?vV&euKv_BK*OaEP91>t6#Rjle>}A7W3B zezW$ju}GGzB_%V5d^Y2tV}JaS{bCVkiHY&lZ-P7Il1vvZ|Dban_Z=6HNPJNHSfyX# zkBbmVd3ZXT+11No*AG=i-vUbekt9gGw&S;`eCih1PjH{O|Fjf(o+K)iG*@ewn=2@5 zh$^}bQ&|l-O670Y7!Nbvoz0pscsW*5Era2QhK3%;^{Y(gix~!#69W&gUi|$zuMZRy z!3&AD&T%;}R;ES~ST#PYgegq9vV?=&!iw(cBSnL;?UC&ElEXuIBn17*jBI8|EY?d% z^1(qot0Jq8bZnyNPbNQtH0XEZMNuSpS*+Eo%{)8^X|g4C^Tg~>x{a;Brm*2|2{n|B-zSx|6 zfptz76iw7^Xz-sG3N!4<`J>wsD=2Mrah6W#IiqskcT?^aSanDY9>_pb5C@zB4l9OX zbBo_7)mi%a!d5$6KP#JH!7&0+dBOCs6lUjTqk~mspplb%`OQa@=?YCo zwba%sUvyp)7o#FMZ9|Ro{ChA?6YPvDIJ}w(Laq0XqeQGV{bkfy^RZN&YNg5abLWOG zq1!_*!v%dX+Sy8N)cR#h(8&OdfUcZdhj4f)gQ#GB*5(Ayt!IKQ5C-dw;xt>adTL6ou@?EXb9h z2~E5$x0=KE!ruT{==IUw^C*9jUcVz=qjP7U(Er7-INH_M1}8=r_s+##nKtEb2iWX2 zU!TzAkWvH}Lkt~uS^QVpm+_PupMM>4;cWsZ+xIty9j*sE+JDlKfe!g%_K8y=@xsaT16o11aY z(Pe`}e(uf^Ec3#=z4@$O>pSe0F-iIQ@s-eJ8!>VaTA9#vh{J+CBRWn4eXiG0x2>Iu zE^Zw-7IHtRx)4#!sM0u%L_#LgmL#z(e?mhh#*y=fFH8uz!VBWLk-`gqtUbU3WNqUE zVJ_O(pMv(ZawPah2HM?%|4P(~^r}@VGjCnHY`zz6@X7hp1#E7zRvAkP^rL}bgI?!* z8m9MPalFpHz=ykvt;vy1w$URMx_4A>3_vZrsqSJ%jV3(a*-^rx@WxZF`x zsMWA)4BiN1dF~t##i`P{j@~4nlU44oVHKVJLdmA{IDZ%p`9s8qYcEC_d7~R(OM+Ud zHGYx6GPciXHk~yd_?V$I5NrEH^y7RtIv9*OnSC2w;`n4qRqODF-wow*4@cbd6vOb{jV1a9Cl4byQG|k9ox`YccR&-o^j~B%Pg!TI z1vLUfz9=Czc5S*kv?Kl^X{CleAdz6P=^UW5%2g2sr%!v z`s)+!PiJ;8bjD8|)#F8Uo}7kSp5K3)z1d`F8#TYrUuJ^itrAS)hYYLLxyTYpr1S8E zRBg6KLwE+0TgHp|8k=vwj*d$^S{5X_Sz1hv`}m|EcC=?9S!!}t$*rh{ru!Bh&5TT4U3f z&)eJHG-phZsb8>)-%`w4@0gCVv3aCj8IQ~R^Xb2ix{>w$^YvEg$@N!xx70R!5(VP5 z%_#>;lgnWK-zzuq4lkHOkwEe;vXl zw5_>FrZ-)}aa3~&+-9An_utK}0`QE#X26Kh-4b}y`MBBC`PIFBNC1V&4VfiES!Uq{ zN+kU=mTKRDM<13lDvFGk!L+Azqh;N*NFRS#zJxLgjwD4vmm#5N3_dPZ$|F-F1nr>h zXrBJjtZPED%v2iUcV2y<&}K{@$=mPKa||YBw^LR#O-4Dx2fBX%p|;G&ZSw+m+L#)HCFBoL7$01SQMNW$LUtsAWyr zCuv=q?aPgUPiI$U$E{wdRdb`WZ7<8bRZ)#}-C;?MZtCYeQwI{%jfuMCGg*<%Xf^TV z{K0dVCTkJM^CJv(M8XGD2uhIdHs2L@==Ah>WU^i4T?rv;G7?bqTS0`-cX#F0Pd0p% zH@UgTo9rn0IF@Dz3uECob6n&{%Stz^My?D&EJH+L`rC0PYM z9FKs24UxzzSiM+ppjQ3jS_f`A*~R^fvT_o&)sfH0)Jda|r9uEv22YIBEXaa09hbz^ z#YZm8Hoe~lWhkSvLNL;qbyxOB4{IaPlsP+g~ z6n;lD;O?KU+jWc5<$MmqV|zM2y`0CuQ>T!v0~*zUKkGY!VU|wc4SM}9!U#L2Nr^;K zoYE={W%Dnxy#*l zPW4e4rOkgaF=RKEFPJ=zJISfnc)bbv<@Hy0aWF~ke#QJBv0AcNvdM;2>e_Tv;WVv( zSdMKuQM+xSrQ#q=vJ$MOvH|4^t+j7btw*z4%Q1&X^oy3$uF-hii&n`(@Bd`fsNsEc z!OBio5yL99T7!LeSEhg37}?BGoO%LexzGNRm#MWih!;=m-)@WAthB^{^T8U5BoHS& zB+v&iTS$N~h@;RLE8nOx>39UU`3R(&`Y(b-y;6JhZw?DHy-ysLhB|S6Y?@ zt#tUp#neQc@TT|!N|e}TY7Y0 z!VS(ML@`(xF-)e{^eqb%s2wC>ODI{a%40|w2p%bN;`GVoj-*bO^#~#B+7gqAIBkJ1 zCyg?Niow|VK^++$L-q?JkONg{;H1&I(IT#(OLL{T#-bM^=epxv)E$z^9GneAn3v0| z7tc~xSQXbbOUb;v+5yyRf!4hvQCw{VzWZjoOjvq?G$l9 zHj)qd=yV}lHhDWALtM*3V2gpvZbXhg43xCM%KKZz2JRvYd5@CS}avn7`^;b zY4x|>xAB`U@FC^qAF4I`BoUK(J{FZf9$`hMrOnQw|N18Hg&yMN zXSiNZP6QKa)LU3m9(~CrPr><030hjfO_lj-Hi_T&B~YSr=&o|zf}KNT**Jt9sh}j= zkycwAhJ zXt}?5ZkTJGIW&W(aoB)fw2E=H=3-VRFn^j5Q4oBy5O`au6P_+a3(T}@3(4jSOCT%O z`AO*Y7YYf*4Re?!76hAxM3l12>yjFo<x!bt*d14Fz)_4a1pW`jDQ3Hv!YM~f?7%f)Te}Q>8XuM9UxQN(E6Pcf z_{~jXGh}&kMA4H*U>$Q|V$)%@R##P^zTkb)inILh=;T+8!4yA=uH%PTa)iPf+wThb z=Q)3ajnjB4KYv+32>N8GG;FU1{C04@>>KHo)-p6+-ri{}#%SVlol6GyIm-C*d`z5{ zEL?tUK!1Zqy}1cY^EQj^8vB#C_ia>AQOWFS#!3zAXHcm+!ONd%f5@hV3UwR3PD{Aw z^}&%6Y&a5n;w6{mli;8V9u23zkVj zR95~t9q==?DZMi}P9^efQ%?p0y|02W?R=Xx7!75)p3WOxMk@F4VPl`HD%^6xiv1K> zXE?uneWF^e&SE}psXIa?y;e&~AkHeokC}5(dlkp{Q|#`x=g+Df#6cCBVN5fkUnZ4N z>6KXgIMYF0n|6pf2a$`0nSZ~GdMy`QTa>1Db=kd`C1@b{-`iGlQ#6WX#(!*k$CKjwC$;!HMz=+hD%~^-znl33<=^0b&G9^Wp-niAD{V-j4 zrQ~b2->ve#03m_Vph3F2%2*E~Qk9$mSj@(2NM2rEy?5@|>1{@bKi#^>Lnu^0U6-e> z)#;cP%5xLnWNN+di3_-kHR;)G9pa5zll*?LoQ816Mv(W@lMD}-$>Y)WHss?ajf*Qq zO7u@pyfe^f4Rmi;*_)@r9oD?ye(14FP@XYkS3@5DiSRi=Zcw={B&zwRDq=ZCA$IT;s z40f^L%)rCDR`P%ivz&iIcf_2IIKH?#qUE+7qR7rQ5HuNXtDY?yT&k^Qo_-QY^t}f% zqEB7+GLkjE9?=rpm=ci*8{W7C)tQb(Uw2S=kMe^4_u~zRbd?V6$!5JaY~{>1h#REU zLyZF2%26>z#e_f36`UD(TfJPWoeTR&eSOP2YhKICu*7ty=1!C>fT#JD%DUV{D9!EX zQdA9)%NyZ6vN@7sdAJ1){C^sImFtk|-_wT-{8d;gBvbA~yl zcYem)i&$?@>u7&*yBTz|sW*B}j#CcH;CRsIJ25hv*sgcGRvxl11sK_n5n`x8LK+zpWBEd zw%*63hSb?S>x)tq#;#J3YE_EEptYI6+tdzd2M>*M4t??Pnse~=_qu7Xe^N|@@e4jm zMF`mtDpZ``LR`0Q^oJNIyj;~8E#VQ0W+dMbO%X(USl$Sosf)$Z$(K0LLwbgiF-Wun zm6W)_VTQLSE;=UgR53QIKFpR)|B6_(>uYq~p>tz3UJb>}pN zPXDr+rqc-RsysIyMDQ7>zMTBEc|%0vvEsatz7JI++ht^mQqPy(w|Jafp!+`5R+Uy` zIr;f~Ic>P?%@{*}$reMx`njI3XZ~T%n=X}7ZPw4_s10nsp8~#wDDE`+-)(xR%J7X9 z)BJ;z-dOyn>0YL9dZiJ^BeOghy}hI`o-vfjz?A71Kq**zi5x5~N*1^9T<^*|FC`{X zy&N9OYPjzAp`wD4Xox$LF|m~Wd`JpSHHwTf!%%|hK_64qa^)PSZWM#k*+vp;Fe6-~ zOp5M7rji0z9KGwZ5UDlbw9O+Y?6fU9MqI~vva&*}Jwl>Wx^uHfG3hb7DQU(_vo=qC3o5|h8M>)pF+$eq87(25gXKKXP3(r#RdZe$lDw1l#9`|kx)@FL!ltx0NCpT z7Wa#+qMt|L+%^wLg#E$IaS|eNOoSuZ796BxL0VLXwycw~pA$>9L>nzwJ7CaN*Q<># zIxemlImt!66svgFvYpg|?3-AbrZD2<3ygMIA1Ac0QIq8hWu8j@53EQj8p2`7(oGp> zhV}^eoM(_Qd?i|8Qq^Z{8OykJEI5G_k-X`0#nMT1+`fOk z%a+d%O{L6*jeADXJx-mh!PQc~bM%Zy^yVfBp|ALW#OnCW#=LAJHb&uZ&&P5RZYi3w z8@0cP#J6U`Z14yZ(gQLGMK|$tv&Yx`@*^M}BS>1QLXG*oU6cLR(8vfiF3Y#)U;MDF zu_2`FkMxipXV}^@A!$kbga)B=p-3`f4+oB}YP2~J3fZSmaN$J-OE(c}V)PSYENnIK zhMLm}75d#ks>qFnlQS0aYt{FW1|gzcW2|2|^lAMdIELZ-OssKOPW#^@_ucQte%B@4 z_1=nKf}@fcm0@J03p*fqqb?f_bL;R8B89WtfR-cCcrm{|Kb;UpJlN9!MK|D=Us96U zAFC-+yEpguv_iK&#j7%5DMBvA8jKU*qEmEAhLov9tHD{VN)2%+suGeZqtM6n9tj1{ z+pAP-AY`~A#!^IrcO_6u+k9ygxmHtOW^R0mJ+`5|fcU$(T&7@*_~%B}D*5>?gMPUU za}y3TYaq%vT!rq{2B%a@0q;|pg*PDPD2FBiql^?vD9(!#yQ@NdNd(!^B;lqx(8OqxX259N_#sMRC;rk!TuTAK4zPVK3~Zr=8M9wwjWx z=PpIGU}{)pEUMR1Vw?%We5LKK=VgcK&hoU+W@)r|M{= zAv@CMSzw4{ViU#fcn$Je$HL;Y;D8hj-q|VTUn&buN{fYGG*BsM2E(vrCCZE1AB7?# zjBs;tjcqz)a0l6gC~qZ#imOjslUR=>KZiia?lSZ8_B|mJSUkxGGUwTaeWjIq8hL zFOzvne{l(q7OxTVW-Tah-~S0087T-n!k};)pv#E!yG(ZifjWinv=&cLv8T+Og1tbu znTFf>@7}qPYt1ejsB1IDL#=5a+AtC)=0fuJ){_%!T#{uNKHNI`AB*M7PW>@lDzrFS zbq?rk=96Tx#v5<)861u{I_|H?YURjEkvz_3EdIS&6sY&=%gM?X>N2$+f51C=B1ds- zoSenG;QH=09u?eRH7d#*Zdo~feIaEu6SepIPec-BIt3*qVcM0Uhpj*&>%FtQ^;iB;Hz#n;kcHu)X>Lq(RK--)``OiHwK< zh8q+%Ue5eyUQvml(SFxc&N?7>mJ!42yWu;LDUf>n@fD^^(G|RGOar-io&f;RTjq;F z)~(l@-(X~vXj6F3YJ!e0FJ~`8D8p-jotY5Q2OPrAp=?BG;XyG&saa=7T_(_dLg-A9 z?VHJg3YfZpT38IUuOd>d?`&5bcq&Dhx82rKB}+IABfqh{va=Cqu+}HD@r_=Eu>~HW zZji?ICz;S(7pV7l&`agAw~7=E@Acd(*Cv!fs;q+p;=+yjr}I~B&|p@1DV=P=UIg{aI*8L}FZm?oV|!a~!4kc`bx z?&^IW!ELl(_+|KSX1u!YV(hQ$_4Q@@<2cX>)9Ss1WpIq(Unm{10_KcslUN0y#s(HG z*jw+`2pq=0@K?iCFAV0?eljfLO5pJAOi5CJTBbX?XRj5eh8w2YX(0x~|GLE|Z*M9L z4^Jk7v(-W*9PYjM#c$-T+D-GcXeyG!D`0i*zj?(fu99?=!j+U-o+UwNzP3)j>y)7x zqNMyFO-R#GAVwqbejh|?c#~We=0aE$?`3Cx5E@Ij(&db^+lW7D7S#I&ML0i0&l9%m z0V6WNB1tmzCycQ~kEo=)9pE0H0yfR@$Mr>EmbcCe_G`o+wluDB#?{rAM={+DGg&t9 z4ys|&gpk62DJS+yyUpap3}p%W_>s_W1VmXyPua{Mw}IIc2wR`V=FY3ENse^7U}gMa zpw<)4QDOFfF zhQ@Gr=N5n%ARMPCv5DP4A${Z?Cu|bly2H)N1akE!3H3t89Im`%2TsZmZ~}+5`&G{` z*98J7FgKt^ZCaeQgWUhOD)XOQ^H(TrY1)A2h{;I=7mcP+%;fG1RV2NeqOyfb)u&w{RypA$u0zU=Q*_Zxl0!gtHK2 z(>otWP1#`@_+DRCK;gkLbON$GJak&&1C@e`D`xQveyo0p9=*Y3--?o$Q<3M}zEIdn zy_{agEM>n@rs^e5-r=-OxYEt$3Bb8d<}@LZcLo*ba}?TDpqaxF)AS#$Pk!!(jpN+zNP1VTVJ#|;diN}=qsxAI5wyR1Aggp={elxG>SO!TTsGnc z8>+0H8gJvEaFC8sId3xp%lv&ZFa1sNODmk9rL9K_(l^Gf}5b&DO;LjP0J_#@$; zj$*h@2)h#S>B0H)@fhkW=AQ{do4B4zN9)}zM^Opu6_@qEGV$6Zpw?i$b;>^kte<85 z63}QUc7t7w=Q2QJQahY=E54JQj11kJB>!u)TIlcONz~J+z5Inf1%<>} zs5C6~rSqck14ltW+te|eYj~x~{TSh8T^C|RRmL`A3qFG4DfQQiT{+YKHnsz4?fk|V zDksZk2HzBHrL7F#&BQsoqRFxR(_(pzHM3v^9ehY-lBN@B1R9FL4Xzlzn+mENzj`tI zrQCs`3u16kVZ-K_CR?5vO7av|e>avKoo|v9A}4Bj3lY_ZE8(eXSLM(0q@Ugwow-}D z7pO0PC585_^}C}#Gd}PNJ^fJxK+bfAXw1r$JI@lwqDRf)#TGqT@!Hjk@GqYnvHr9& zf-RgYVJxigO+54hs;U^17RJ;Ql-ue<!<5~xDT zCh+!wO_E;Qm}x=J@E2B|S@z@Sy0`VHtbi%F$(|yjXA$ATS>ZwST9t*c`YDHcf)CX*h;=oJ6;BJxRYc(b`sSLc(uHadQetLGhgK~&vCUXUG}hG| z1_ZWWw8iPaoVfd3h6x>d3%zip>i^@jYdO|yYlCz6O}4v83!KQ>i`3Hxz@;YEg<@ZY z3)sf$LZB(;*6VDRU0kz?aE}T#9h8-Wu`N}fZA-c6e%;378-P^pazC1K`OEmm&l54WuY2*(Vq^YP!K5lIBHQG!9>sBU zz6FzZ^DEkU&#tCz{E`{-?JSNGh~mgbhACy2&ahhC-yQm=W|zDpEB6g2FkhIaRD5fRkOCP3a7uSVSh^)CyL< zTB+w6F<5ybHoMq4a*V4Y?vzycWS8?IU=-##NAB8HG2Qerwj{W8u$#543_0nVCDBF zHdfZqL*AswFF*?zO$wdVpBmeBw5US0Ri{%;z(<88N@{|N}0O*UpeK5B`7VB|5M z1o5A`3=T-^&EyV)F=HuF8h>7YEGOPOSicyGh!(Zk8~NqLZykV@t2x2Cx;=%*`M_aGOVN00!mq7uYyua5Z$=d5zk)A7b;zvz-W+rsw8K4T z`o3*t*S?DaL_z!igpois2?Z2gs>>vP*z|w@$o@*%MN(rjN){SwW>RFQzkJ(bP!VEq zFp-Y+83n#_o=CiDzgT3T_F`*?I!~RbL_1R$uXz47`P5!cMu5(~>aN^22x0zKA~{ z=bpE_nPM-k5OZEvoN89EM;O{`AV^XNlIE#bqr({1xz(Ki0`BYLpiW-_54-eq(fQ-k z(;RjM1y~4{_;8_;!@t*0xX^=U^Hg#pxWocC(rnDOd8tmd!dNhBlP}-aqk?-xWJV{O ze~)E^Ezd4*^apx6r*oNee)R2pxaD1RlrPuh3}5MADhKFRYdZntT@ z?R1Kgva=ME({MAwR>#vL-c<+R#~6+FCkHZ@l7A&V$Ih1{1Xt_K<8b1uAnpVgs-k^D zcPu&#?prWtt$rv{qBfE@!hDkE$G_8~Kz#lX>+&O<8^wYX@RAmd_f)(mc5wMBU_^SN ze<=$mwNR;Ey=^3hGum~!?MJP5_J9H1>iDlWRRZWa0#J`5Q3n{IS`91{rt2*MR1-(T zk!TNZ+*cXB4iWwKy_b;~8WtXkjy@}BF6k5Y&;8=x9q$4HH{6KioH_vlw8II_Fj@!* zlh4iZcI&+thv@|y$7dhI@A~j`wYDVGddZiwB-t|;Lp*`fK|UqVQY$(}qxB;9T@2eh zUI9l2ux=W2v7oEKN5abg;OX#2ZQk$$>S+ZBJQjy(JX-XiLS!2^O(xeGOIJ#CXaK=O z`^sshD=iU>mR=jCxh(~^Ja1el!K*(bZhQgp6v?p+>e~8nqtojlA7Lt=Nkl+E9?c=L z`QY)TL}@7T=Vp$c%sy+ZhJj5%uV&-ukys+bx?8<>5h9qiqhu(fF>{5Dvv1{r+SgY= zN>rlgdtr`{m~u}59R0q~`!yuXV<%yO%1xDEYf_$TV<1HT9eb-|FYLGP7H@E&^p`?v z2GuWvW;X1So`xj~aTVfpSUHEa0OxS2YD>P8XyO8h|RLX6g#95rP#Hs;B`8|M|52!M3`a1>((eMpbq*-tvYG$xW^a!zn^kg7#K% zsJNd&a7bdMf}tylbWBoTnX`k9Ro=!J%2txewX%8Dg!~>(#r6w>Yvpp`}QwUC~+Pyb#S{l4iA&PQJJjcxXktp_0$y8j*(M#{S?fyT) z&axq@{*Bgu5u~L-LXZZL?v`#4P>_}e0qGtXrAxX88EKJ{?(XjH9J&S=a^~#ke1LOa z@ebG%d;jiruXU~5YsYdXGzV@x-bi|C-W?`GO{XG^SyvNz#I z85Z-W%${f5W?ZdFyvuKix->xCTj+EoWq`l8y7Aa7y7kV9*Lkqw#s&?te$vICxHlSF zJb%zWQ^995XN(!I?3|-KgJ-CdrqFEKm3d%T4;~1IC<$e?(8LL3Kc@75+&k`F$^AtW zC*LMPTP7V{ifSE9s2^?~mi9ichzyt=N2wf_#nvtze6*`pR}+k^mi$!mz^?O6x&0=z z#qlBf32uA;n6-$cU)C+)kG|S5|HXY^+BxGyRWvFh*>0EO4c@iuMHYyY_z9Wve`9|5 z&A(|Nd{BKie40^LPx&jl6l~q4$BC)TOkewW(6A(3lylT6k z6dXCUf@+og4jV>OuC2OMS896971NLJJBH|wv*B5ID(Os=$@>9pjP)HYbQ56=)NP|A z&3Zydo=!GTlscH!Xo3uO?Tbj^qwz3%YhEkbMNe%Z&c0uq6bB0{JPw;9s8pBgs^x~V zMqcIBB2R{&+}JGm)IYjf&f+!i9lQAw7sVm8=EUV^wdQZntQwsp1v4fY>Urcj4d0(g z4>mjcQr&zMPntyjG$F3woeyI%C8_Hm{G0EKwZ!H0yzKI5f}ej-J)mkaV@FMgq6XEY zncvPf9h)dnaAxy?{lJMlayS}X2X{HFKZc!*bxDxX& z(=RK(zi@CT&ntI?e&@c`l!k+b9&0=OE5Gv4ff-TRnIA1i2s^%suLr%|emP$(sdhfu zzx?DC&$9r~Q?BzxXIxfy(b&^Fiok0NAob7-Iv8HxXkLGy2kw>1d6Zk2^g~&GvFsv{ zq?;J)*1>hw7_?lT4!DG6_1QabngF-*qoyY4&#M>68D|Hr$t&uH?BNw(_`yN3UHzcz zX4z{Ca<8fnY@}>H_vmLq`W>DtsV|ah{scs?32{H#kkFHw=Derr7g6lYCF@IaBK;2Y z-AmqoRk6XMip&&HXx84>ly2VGr=AF*GxSkuoy(t`Q7xAhQ|_M1liYvC%!kuae#G$l zzvpY0aGNg?-NP*aWJ`kXs+qNhsBxos+aJ2ke0^3GS^jDm*DDvV$W<;BXt|_`N&Pqt z_2|zf3cbIw$ye8e##u)Eyo^Yd9{uv;O_W6xSy}$8enZz)L_qEWk9>x%zf;k-YOdI7 zd)Eedxqrj0(fVEh-7OX!YR2~VQlD$jDD5%A+2n?Vt97g27(C4>5b$RnGV@oE;SJU@ zRCwL)o(;WxduskuPCqdfQ?8b_N=%S#qp>W`k99{XX7AOJ*Zq@HC&N}92#CD4d>&

zYlPdd34z=2YRPS9Twt#)&?O6P+~6?2{W*NQ*SiioT5k!eF!7)?Yu2c;_D1XnzZYsX*CU#yrI8JJw^yvvJt)y5l`Aj(2-)7U`jv6(G*l378;Ekf zVW)q^Z#g*Tr%>dlzY`(!6w3GUBAS|oYi$9T)Iv_39OUdNP3fZkLZM2(H?W(uPXBsr zi_m80X(c0`)8Sbf*R4n8rFS4?*Z=|Zt%(T>9tpYn;y5<_&s#&wYEQpu9JO0?zwVMsTyMgh^SI~1(cxAj z3h8Ki)dj2C>Uhr9#7t}28hHjfO9S0*L${})t?@=@hM~zXbq+~R@3XD zziHG=K?!B!a&&aqeB@PKPNctWK+=r+`lSN7pb}>wDc@5J(1s;+??L(wf9fdr!)YIZ zMY4;F1OdH9+gocEr_2a791T^%5Tb%|uKO1+h|;}m`vu*J(Ku@jn-U(bc(&a>E7`1O$R~O$lLSE?z(E(&h3S~SgYUlvXww0q68>~ zG9BD|&4|7?b8FGrEzCMe=ehsvad}#E7nOBw^`gl{p7ftRoCSGPUt%YamzwqlQjcH# z_1X|f5Ma_YBsW%vckDQeE3>ncNBNZZc!p$nz)b8jriL7$IVC#gn~%Pk5lnhB4)_dt z6B(RooXd5*+9kU0^j&ZjTHFYXykh=Y`k+^Qu*%ij4g({TD!|(o|MwE^$fUO}&z{nU z+a4_2Pk7$oNTdyZnXt1B=2 zcGZmOEjn>gvFm(POSKu<#4J-sfY`umM9$0 zf^7N|J5l&EfPl>-CpX=ILZ+=hb0TU$A1*-vANp8wtzJ0(Y>SPc(1 zSFfJzAz*B=NN9A20WlOfNvkFL_?HAkbCz9oj%yi)%KVb`RUb_P-gOQuYZF@~Wf zS4m5YA~;|*vb{y(BxjWraA6mmw=ihakh}9+NFKsv(;xRxqc43sbU4cAr9a&RIz7#Be-; zc8IXGro(5c!IjbfCdLkpuD&C+Y}t1srxv~Piw^=_c->8W{Q*fJio&J1T!goe_`<*$ zxNVQ0q})5xe9ywA2Dt;DFzcLFHCxx-v-Ms+PvdnMQ-4pAd(3^wCCCfe`feR9@^CDC zZic${dntLzMz#IvTf-?!+kNe@=vh$o?Ffu}vqFEhcR>035qW~_mIzl@rM3AN{>2zy z#%;m6lciucRW|Jo`27O^lGSqgTg&VC@VQc0v-BhO8R}f=?pzeQ(*kcFplH9>0L+Qu zkH_=itvGONDtN^O7gFztd7$%_EL|G>gVr`FP^ajxw)2D|a95Afe>XL|ieDHb5K?BK zVwTRSrgl!Hn2?=j$e>guS*Tcj;NV6?zbI)4*wgeYy}I?jwLQ_c!Goa}gX5+Hz>ARL zK8_NDN}*(>E;7d9H;BMwM8FR2;~s&&D{K|P$H%8y`Qgi2P@{?;&)G=+M^ap_0=LK_ zi95Wut2?dBQ}4EnJ9yZFJ(fY+^$FeO;<#DQuJkQXY#O&M^0JMNB=mlTlcP!pCxvHk z!2$IfHsXi3AlO2^E_@VS&V{p!W7QYc+PUXe`T4hTIbkSK&KY&?YrN_zE;^1p9`2x& zD)jVy#M`OOrX%MKxY;8g-R*=@;|8wTc2|YjkS+Hu6;uqN_ISrVufyg!ZKsG%kDkDM zVxQz@NzW{=jivg5i)`si*=lE6GJoyS&M~plB&kI#wwtqlr7x}h@{`ngXWZpvTtFgR z8YS3vf9(t}r~pO7xo?TVx7#TK&p&kKiD@&A5r(UOEB=4wb$}u2ahRdHoiCQG*6(%2 z@Aly05H>k5D&wm4ZTMC6M@6FUTdkAQ4DD}KJ#X&2Gb@GHTPm$yryh6TR+_(RzsLQ> zTVM2t3SMlbygM-bl9*FzYb;P-&8p7L)>_3nS(QGL#GksYW{1hD4b1ZJcIqD%ED!V#;6_|S@NsLs8j)k{cCn!-(6@0Y|-#XxBIX#Wm<%j1j; zR>b354ZrnJ-~&_871bH{;NpGb=te*0I{A7 zn1?zvrr2slIC(W?k;^5aZ~3xRp3m0dI&r*Aok89DYX(8F`u|S&{$iBz7K}}crj}I{ z|N0)&msIPtTCsS@O{Yku=U-F34nY+kSK0VVKXf4*oGYG$DIbowqKg4tlyxaz+FxV0 zFc^hmD8oJZs=C&)n&N3YQEstEe;hlK1qWTd;C#3qb!r2v&(Y6mFyx*jRq-vqo>+QU z>wF!)C|6#Jm-?RFay`fxAu;$vCH&=(e17s|p4y3y_EdW*s?;euo^$X1B^>!1gR(xJ zIQlc4y=kFkMco{&(;|CyUw;dQeZqA4;-~v8iKW(2O7~08&Y^;shmnW>rOZ}GkVT2i z>hj$yd4Zj``uEfn_j|4;>CZ}` zf4j-!C0AT9?E`SvK284QKF?J(?`Ue$)IsGyeeH~Kex2%pEku7&AoJgpm<5u#iRxg5 z9$;1I6d`2OAY59rVhx6||J#-}Qok`}SHsJbn~}iwL+FqFzl2Sb%|b&9>i2RAf|7&LQG6H_-V_RvFsk z3S^btp72pS{6Iej0eEK{bQ*+vH#iO+t({-*$P}b@&El(uaavtW?W#kifCG&RIV!$ehH_r&wPVpU1`aMNR;+x`)6YPN??`&F%4>1$9F724YpRGRNBWiSV5u%I>Ytgh1LyUkrObVHACw^8?qX z2+~{a5um^I_T&sTM$zsk3Kcn%1x~kVcU$^xqt@#Wlk~n_ zjSu2gxe*IqZC3z5$VgFTE)!k+i@=&J6~@jZEMUOFNDI3AJVl#ZP$^8M@s~s}hz)(y z<>9##Q(YfD|t-~;-AKB|yyjZ)t z{TEvOg#i_KBwaMPtqqyFAaMNs&!6OBiKoFL_#^>zbb0-r3aE`|8;TuH#iwq(UsZmc?N|ZNgm~K4vaiUH zjDLvBJzQK8rkEWSftM65Gd*0reD=tbg@uI>M-|wwt!AEEt`qKsc$9rl4d~1@%YUU` zh)*7cA?c#ZmNH~0vyesJmUc?@-7qibft;OgGs>6K_)S%-=>z7}%J@(kI5PE-Rzpl8#Btb{Z`sc-3;0$N8d$qap)gU@~;4_(= zC^A=s|2c>ygkB?2mj3~FRW3wZewx7NeDfN#hikU``s|L7?rx#c*yDfIp@0Ms z#<~1CaKN0#^|-R9(gFmc3C-UiOOP!xi+e%~$^ZP0Pe4Gv@$+vnZR@>>cR0Po+K$St ziHnf7L^0bVcAVr15>fVq*f&wKK{oP)`%*@##kH7DasnA&2S>xa<1V|QPpK8gAPgvK zNC9-;GVZX4wZKYv!sD^BSA_!S_ZcgHa2u=T)WlK?2hwuhlOvX!` zR9hkNtuBmWmrUzlao3LzG?r2Jdm1!>*jHOy6@M)Ct+jm; zkChim)G-*pxOv(7{&|2QNXuJThL9oT&DC=>X=c)Yl|SR+_@%U`(baH_EB@PkHUiUz zL2DofxaravbOKr5iaLAx-frfJFTCbMGH(Wc4jfrI5EY(%3*5APU5dd5fP=x{@{rz8&t7-5dYU zcO75zw>QXek145PYY%5~>_Dp8t;xR`$ZdaT`C!xrVy_Of;gOIS`Q^RZD_hAqEj)CojcIN^Y3Z7P5T|Wpjw%S%j&z$ek-=CVXUGL%9z11PF4G3RMevu z^d3)FILuMKI6pEFHoK1Z&&)};g`el=xmX?LdV{qA<%`s#NkB$ZJ>Lx-I}S~66Li|* zOXEGF{ZZ9AQv<32spsJ{WYybfq6Gqa|8Y@ZIpOoUq$3dVXtH4neg{SAf}r;b&j4Il zicVn#u|VcCh>%mxn%#%;Mz7-7m2ygdv_Z}O!y z*e#htgO5_qqY7vYEU8@c);b74$x+yH(8|cSC}0g4`1HVWw^z@#n+MAp_J!UtP=Jrz z0xze{&ZnVIG15;5T91cm?K}6>&Q_&{*8MlH0n6sw6t0&d8}FMqtdpabha0u_NwMTY z_QVoNV#Q+!z#AH%1Kcwhr*pt-T#6CP@w$_hTSQ6Y@#UF2lA?WZo*PMoYA|P>4I8tS z)XS*)m)A4}k7A25XgBBo;YJkOFF$H)RXQ}c$&2(odU`y_gfyJza)_J-abEy~e)#l18KPoO^>#!t~T>5LS53iY&&_w&%2L+j!S@506jhBD%666ZSVgpu+Gtw zO`640DE_>&kQcZ0{0xkF<~P6Wdzmzre@t!w?+AX}2R=xT@b)2FDKIv;S-%j&|&Ty(A8q%xX#-*VE5B- zlr?BVQYrO0T?S0O;gHvh{6x*7<*1w~GV|Us%&zIc{vERRoOWs%9C3fdK!&ay{-)X6;NFN>L)yr%?5X5#JVk zZnsnIlaIW-rauBZyX~A@o?eclj?UYeo;`5veLP*7Jt#iBUyCP@Mm;J|av{CU3$J+| z+R5zwF25}`2ocpsOGiHq=C#(w6+3#Sdlh*L#Tq-Fv{L7i90?_T%`0mLi|^~84mu^s z^?DJL=FUjN%RdD)F-;|sE@HtuqGeboPtX2F=dBIObd> zeNODxvPRR=((d*!1Mm<0e$v$O7&iwGgTX61mF?zd1hjGgXiHEpk*FXg9o?JRFn1o4 z1bI~TwPC{ILPoIiT#~4Nb({ez-)nFc8JYB@!v75Jbt0C^#Orii7@wEI!@w)pLt1w6 zRR@P8a11J>%ErlJUR6wQ#v{YLL)Mp}lZxK`D)J+FGkB$t-FHmJR$+~cTIs9hbOA!j zLE12T##sv2$!}Q__GDu&i|McI6YXQjmO>xHL+*qw1lZbi)ieTw2KU%^z6*KnX;X7( zBu;MiB&%$V4MCQSFzu;W(xf==GL~33)2SK55}2n8`JS=(Iu&B5vRC7-+^Shs(O$J5 zm#{I~ICPNf)>t~sSQKC=k9gABl^laA1(*4EqxRfgMr2L`kvt1v`s4LS?+RtJ8TS7j zys9IArt@D}pe6?~Ino%Yl{iI&2SFrdmDz&V(-~$_R z$*Xn51-@@k(x>;TEctWJILrNMLj@tlz#LI|H{85#wi9sIR-rzk1b00&31hHKEah~=37UaEAq3rF~X~ZpH8ZFeTBa*EaH0mY$?$)TT zWk%QKT0BdwKEtx`&zz)w#Rr$;%xtg_$TiGlCP$e7Ep*Xjn6x7o{m=nj=*>q>=_ij= zP7~GQkH7Uwbhs!EPEK-4)JPN}gs!g=zF1huLjTbyHt29Xmu-XnqKPI<1RN#WZ-1}- zAttE;Tu9%V4s1>vA80@RNtd>z<$qB)xxU(a)fw2Ns?Ze0yf+RR2CxiY03O@N5=&B5 z+NY=1L6yUkAzTb|QsOXrcNpG9=DbuIq8;Jca>vQNS)&_k^RKz1yllAvc@Mrk84er} z=0ehgAL00pOD(wUI3lvd93R*iEC>_7mJMACuf$j$MtwvZ*HCJHgeA1fC=mUiqbIx# zw0Tqiq*u!7Ml$<0Z`+cMw7}wrs!GyJ@yL`*E$CRIO9F40|e-Xa~(PD>i0yHMgYA?)b>L4d3sF0|T>( zNy^8A>&o}aq!WMUBR{FfFxUwhE(ed+=QoUCayU4$8h%N(njVa-!9xzIQ;qJpuC>sc zK5l`xpU!~tHUEbapS@{K>w_+gKkr>n*2_2DVc_iXY%o1=KoK|gJds}Hmk5K|hd$2T zGvM7NMaPSA1`@h<|Exbp#(@&!vf=FC+7I-;*1m}K*grYkOSg>O^{nv4A4k>~xo=b% zC=(HfJS*QO3L%oQQ%(Y?cxAlhFh1KcM?e2&tVgjFH|!jAbJEPpjw;lF`VMZZ5&3C$VSNf!P1$fv$VA>_Xd_DI^h-&409CCL9>|72@%FVm`MoFWY z@0d5r+!fgmHrYIz#FlofQ`zBS# zm6~j6(>r~ZLn+zsbnHmi>CxkrRtU0_k|uD$JXuWvB%M_i7qy+vgGE@0e82SRl&?oc z_YES!2(UTfJ_F3Pn1M#U*CJs&Q4!k_Jh73VM)o)%x7W^r{q=1D*wPPo+y=FN_+8h8 zs@hx>>AWHXRn}@E0GCi?dxT#EdAkh=&r-W?8E(b$orD?-cL@O)Z=ITQBiL{vZ^RDf z1074q-WPucW<6pBqMv@Pz1NmMcy=hJF&_*nEigU5rDhD?Pxw+H)Gt2jFg zE4>cH8YUkrDmms82>5RQy!Cjgag}ghM5O$=PL=hM=4lHoaL8o!>c)!%TVywxggtGE zkT_$AyjVGoBP{;SzwK_)e^yaCnHTdr!=xs=Odcu2K)9xQac1P7EM+eJO-3`?{=osp z=KduRX1{K^xsc8I{QKh-R#WH_B*Cb&kRB_Egq+ia*dAw3FkWxPU7F}aR$k> z32P$790M%~k%a3z^|t*M_J(qnOII3s8tVSOUk8CpwMNTO1=koI_D&nqzX$xb6U)mQ zuO0+sp%#tmd8V+_X26eX^vz~8Q}AO`)4^X4UU$0)QGQ!~=P^f3i39mDsRyT)Eo<(a zk1oKIthF@&7{kJJ!u~k6o3F)d1u@{r{k;}jT}@>D= zULh));-JV&nDJv-bmLy8!+-HoAngfc^?}5k%f1YU$mO}5=;K5%5ZLM#Jx$L8*i~5h zvN*j#zHnk$an4r7T0?6q4?KVead6rT&srH4F~asx7)UExzq)v0F@HrRtFmoC5xAv^ zB?C5#W8r!GiH#HozvD~MMmNJjMMD-2uyQ3)1cj>w995`7lJ<0N+w{fU!v!Lorv}9j! z{$R*!A;*Q0-u1RD0`T!PXg?3S6Gr_l@8fBqPU#1i*L&l?hE-UzN2;Muj;4q(;q$`! zmQ)PfJ>4>m4IkQafhX?c>haV67d$|0f5Qy@22yudM+yN-zb==5RmIKOj(xcl)_h9g za9$uU^RJYzDtb{(SAIglW0}3cZfS=+S`9i_BY8+!RkeCr;&6tFEKyX+9#04K{dhwM z8SAby5K`y}nmg;dS=90U+zMnO5`C)u`9hF-QtV?l7YCAJ28goL%ZFgY#H3Z$G&Z+_ z3|kFDbGd*1Ab6eL?^1)m?GEm;9Oti$7+VB&giM^WSxtmuOQkek%#DycWi}O?pTIY< zE$iCNt+vNb8>(yu16Vj08>%m0)`JWjx>9S$&a}R8@{FYgqZ)VN6r-gD7_|D|o^FBn zTX@}oVse}SYUPFR(IXsn@9W?nV<5=LIzCW$p%m3rM!AQ`+@oJaeiH-%430BK2OM-IjKf1ejXr- zw$$1-f5}usmLXD#=cVMGNp?RrG-pULBGmvAH8NlstVgB25!SD?*IklbrQ#1Mwf7Pd6P*c=sljh1JK3(U)T$d+O{g7ib_fU7 zQxC=2Wi&Uwe;Y1OY01>loJ1Z-^qLbF>A)z-i7AQHTZHhc1e!OhX&}|KIT7UOvvZ80;kJ{42mQjY!)-9ZJ6K z0;%(*eO<^O7W||P|Cybf?p)?r8b^w6PqlF8gasIl#-(MqgY*t8-ho%Kp5FkozLa9x zL!I4uj3*(dcO}_?>)k_qsu>lGiC>*3V_+II34eAp?>7*hW1?vy6mJukk~8bHw@Kt7 zVgJe>k>9I%1YRp&b#_Z~pc9%Zy%lq2fjH#*9mHXYn#4TP2BuWbdu8T#n^JM=946Mh zwRxaVwr`>oy^@(Tkd2qe#2}QNzvCf$FIPKwi=y9IH+Fva!&J)}vkAT;^MC0%0`0O$zyEtxJFtBfTb^rj@4>BD}~(X-Q5XJ%NyDM-q{lcE9921u0wVoEfD zOsv^1Xj=Y587+H)6&sw+f%xNKBtHOyl{0YN0Q~STfG|?84gk3ZJ4{uIpcI!Yntf_1b7M?2m;|w$JSUqbI3~V~c9I&pf)_X@u@ZQw086K5fA5 zPZ)s;rv>ZNbl7W&gu)~di!FzMu6*Jg!0GIN1irkVZ~LBx?AH~&`nOA{B%-rLrmFjP z`TFXb-XxMPEJokqBmQQ8+hH`{E@dAtmOk_qH?wx`$3M%l@wvMno%*jMh)7kL0bYgDl~Trk}kZ&LvbaOo}`+c1((bgSM4lN^gDe^BWeR%2Oud zbsqB=^Xa;1xwq!t;jHjycJ|+P@L7hK_EMW6_}0!3(=Iag+e7Nxu4}UbZm4}XaiOEp zX7`{}E`#^Z&IHp%T6tf#z9}qGi0XJh&Klyk8^C%<2)19J`3Sx0J17wp2mcUA~-}HW3QVVk4~Ep4*wD=@RE>_pd%Ss z>p!USiis({3dQQDkCFqe6I18#&vzgKKo}DaBZ9w;1gV5 z0E5-hiTPT&z}Vu;?>H{^%&U(FeoPZlvx#qF?GY zEyNQ(scP9p?4KQN?&!vDnw)`fp%Sowmg~`qy%FG9ey}AfaA^6siD-v8n6*Z@k3XTU z-`^mB5RE^C7&v zTJ}T#JG}u{A_74GlX%O+>e^wpG90?Q)M>5t|bwAdit%Yxrv?1BC`Lvkh8@2 z_5dS6&WHG94zd?7!W)i|DaHZa8Y`}-;1$rrubHoce*S(g`FbCJc41-mUi8224!WzB z>GVXkWR!esiuS|umTn3CMBx0}(76h<=tj<+70u|;JmvZ46FKhcoab^$^3ciGuWuWF z7sPVtYQ~lf_rP0XbONaoxBW;}F=+G|BC)AH$uEQe1Q7&Jby%3lMT4d2=7OcjQ>4~L zFE@y&B4D+|=gGme)4}xGBKsnr)uj*kJOJnT!uq0UfT8u`{$5=mbCkQxC-Yp6{fA4I zah1kX<|CC=NuPefK+7#{5%XFNbQ-se4!dn4>n>66gI|S;KCm2!E_|-2t?gis$SGEM{6s-n7?p&o%RFy z42RQwD?qL|oh=4X7uXziAg`D&`$dm8dTGET^jWU=gIO-elOnRNh*j?kV2P0RpjXb< zY40(ga*u|##wj=Q4Y-`t^MuAQt_%~h1dli)& z<0-4O-`KV4?d%f)wrbgL)sRFaVMOMHhu2vekb`g4N8aob67_$u^?`nwRVKC{`fOWd z9!A7Eup-4VEiTRr?JM!e++SUF1 zWx03uXo=|saW>G5yfw=WrgRRNcJPHQI&bsCvb!CCin;8-`$-ztTN#g(ApnJEet#u1 zji}WaHRV~X72y6YInRuT_Vrqw5%QAHZBkaE%2%Q2X3*n0B(E>qD$<<#FAq!-Au<6aRwzBK;)Ercg9@ z*yvt_+{-QC`}gw99|68dAZUs9M$I4jK0wTd#n*1c*4%HX?^b;2HZAG=Sc|gVcmOgx z`uKf)5x?a<;j}FtWf#F}`!c-_GyDPzQ)SZg0^*s8L|3A9B|Hl}I}$}b62RBI+p~e| z5DPC&AHU%wfZxH#VOeqD%;ECeXYIGUXN{;fOzf@&!i7xy&$_$aq#v|nj<8gj36zv` z*=)x5v6{V3&^*lA+J7h}&~K9w7re1kyUl@%#cl%Hhx;8OaD;X20T24Lk_NzFF-31~ zNqpzpCDZ=1)@5JRuwD$MwZ;p#ZWgqhNmjs|oCBX}fe(4{@Vs*5V;GN{3)RhSzP-Cs zZZ&0$Wz>+h=<5hT1lX*;L3fyQ+}MUn#32GU6}_Q&J@&j^KLxp~6CeiMY|>Bz9=CP*K*x1lG!pfT+*t<dmgwyA$z1cf|IOTQl`U18% zA*V5=bGm9`#L*)GNKxmagjMf^pSEtTwf_GMQwm6I4FW9T(d>F7rfS=zvl=Ws{>#Mf zd-Noq45cV{PT=fcaH>H*x}mFMARRy2XP2A1-9yr_1n>Em4^{s8<`L{W8ADmE>8N$U z0QU&Sd4LSGlHFT|{6k$K0@UsJ zCCnNb9tTh0J|VT2i9c>~5f+7YURrtBBWqO5dxH~&Lu_(&r>D?kMvl%M52K`*Qm?E5 zm>id#HIiA%ov5IA=7qtLRlg0R!(w$05cuao@2bTb!P~Ac4fk4-is)Me?7Z z;}?3?Q!B7-XVz`2zP~~Uy#~;CmEs*71v@uV9zJVFPQyr5JCd=PiES6k%hVfPXY8TZ5g(KC+baE6ew@uy6gtw)5Ma z?nTIQ>*nOWf6GM$L;sB3!F$@GCEF%S_mT(2r{s{eLV`@1>QPAy*?NoCI#q#o9fXRt zJ#;&2PH+#I{{gY-x4Kj%!oLmZC@OeHviL<-AXu@zhS?Wo1 zPy1OPU})rw5b`J4_gi06A%0IBnaQx4RP{D#Qm?)O)m;DIMVZY~VoKWKO5`_`H7{`(*%hd$2Sh?!OH&IuEy@ghV@1$b7K9KhJs8-8_ za;i!cOZyKY78XQ~=E`iXJZPBFE3MnoC3I(bhGbAKea@QKx|+uV`Om~2I3MSi*E<#gI=*o0 z!L9_#cYG^3Dc15Lw^5yL-(v4eXrsH$M#L+2Qb0*gXIbW3kfLHMIF}fov3HToW8&Za z{+mAJ%iXhKwp0nlzMBnEm$~V&omJcR*y{EBuci-~6z4HzSa)KfiH2#o<=YL3>n#q&-AL}VoMUw$B!j~ zr!6P6g<5nyt{%{NJE6sYzTyey)JytnOH!g(vb2V+VTyU0r2V8gTqKs;#m(yDgd`-l zQULGk!Z7Kp9LK|{r7bBGuwc9BbZp|Q_AD>H+uHaR|-v_f?|{tqkv67 zKmZUmXTXwG>~{bnvT};9o+xv&t(qz`zrUdBMF7F&_UPJQ<#8Z^22P$#c-_FTv8W{( z9M^Vs0@@Ea+ju(j#iT=S_}?xlNq0P~BQ&$GON(5NU6owUWjN9W-LVQ}$0W9`04twz zA;Ar=fYbLxdY6AmQA*Frj`R(B-4aCffT;0eO#tS0;2&(No98(Hb%9`gaLxM2B+ji$ z&TPYHL%Fss!Vtv}mEBDrPCG!oe(pAQcPs9DeS{3oixW%Em@c9Z9p2LrzZ*le_0P9m z2zs5;j42U(-)=xRAGk)fq*Q?6eC6$%MLA0n|h6J6NL;SveLCVB3+Zz&#);XqFi;(!3n3zh}X&Xn9Irb{g zQP6vN@3*p8`IO?_JXZ7Nt04)dY<;c~X>5lF3xs@|BkZISXzj6PkT1Pe3N0u26?=?l zU{*Th#6#oxFonyQDBVu$nLmYX)`h$tjmH}fz0xm}K?*%GJu3!sS)i?pkzf3f&Q7Tz z?9F~;%8{x0Vb+mYsmT8S0L;f{48R1_x(WZ!{}LB?!y#09%uC55tJh}pJoGZXGwnUVh7<;c?gJ9y)7OQ<_%Iw(NdOF{|fEttvEP6qMRQk+e zIb}&-{5+tzH90C%K8G{WFSSrO%5u5)8E!)x}03c#Lmmu z>qL>M5#;L#ge2YVK-vb%eI>@_#9oqIe=TXIF&^(Fd&w5MZm09>4^hZNIV-ugV+6ML zAeBOr)eeSZob2Ruh+`!~3}evy{-@8wy%V$&JIdnvYHPp1bPQ_R{@h1F3)UGpqaDJ6ktF-!S2_<^&e9 zTTaIuOtX=hP+9uFTIL`5fyuR9YAf|KFIFU^Ncu^oWz+NjN`RbaU{tg3HnrK{u<1Q0 z8?|EV{y3>LVMI)*Bymu~T4i;?VDobHcSXsvVs_-&q|7U{{2&5zwwXSyszG5%L!p2Zy<=@P@nJ{=oGeRm&cJ7!Q0718qpQz)zg_vItt^#=)n$pdGoh-Cgbn z0>H;h0*q4-;d+tO2mNkhqt8Z!p>a$;Wjk`&PRg_3$Fl8>_23=hiJ{sNnLi$ zc=hLEE6|*KYm7Mk|Lpf7?j<__Hxz)Dq-n7b9S7ZKWZm^_`>r~ejX~CKRmTGRAwH;w z^*#sFCk9hs^>vvKs7?QMEnyuLE1TZZac^`yPt`6@#swz=q}2-VyAj63Y5fD(=kv2?bYE+4m~7BG0rHR8 z{pI}Kl`nKR)8QKs9y;%WNylmZf)jB)PEp*=^-^3MXo0dQq)-M~zI`e*zWdKcp!@^o znB%*E4OZ|XCSIgVMW>Xn;`dc+T}Jw!98v*@1^`O@X2N<|6U=AzPuv?Hur#&-sHfH} zo}dH%d!MDG>;d2v(OL7nSr$RTp}TN1320i|ttgGxfQ}8p^^_4&gZv0|y1V9wDUH&G zZ)mc5_5TB5{U6{O?5lpg%NsM2!$G<1b$Uf1;LrDWvB-vu2?(0?SXLD`Ui&()M=SKT z4+S8HmYHpmDtaKki47Lnu8Y=hmz)y$1R(xG6H?{Nz$9tolN-s({d)G|8VyvUY zq6;y~o@Y)gQ8Pr!g$+_TQUGT)`bM6_C&@cIR^xxd@UR}X`qCHvSS<(ym@sso#4KudSZa{rw>dQnPbX+t!KHxG9yxP2+M4 z;EZy_KOjhr%MMxHIe<9xCu*( zK2EJFEjv%A%q`^b07Tj1k{Z;jJQ|3W zLPi2`U6mc;izR-uYnF1U>)|&$ZOpwM97*13>@|OJ_g%-`oY--DG<=5YYwFG@((1T6 zGBX5}{(ty7tDrU;E?W1)p}0$Mm*Vd3#a&yR;_gt~-MzR&@j@ZEyIXO039je;XU^?8 z7vut%WG2Zgd#|;gWi7bkPf7Y$>SS;}S8+rCfdMZ~3e!kH0b2bprjA;o`!?-e!9*hk z*KjuDEWhb_87BQDkAD1(zg};= zYMsryWJfE8W0a8qJ2rf+yfgFjIUi!3jo4VpX5_pI=-ml?^H%U!-H zFB%sMr6HFlXVdbOS_sA;`E6kN>H&h~B+`=;M^JRgJ;+y!V&yFR^jWS(s}Ie$(JGfA z{rsrVJgzr_H_mBXZt7fxA(akiOwwumymfPOHGC;Uyrry9sg{*6h;BBOH_9{xT~?a{ zRDiP{tM;k^e!xpq1NbiJAt7QvQ)cP(%o0c96-fr``M&bSSaJK+Y?Zr4TCmAU3q)k$ zHK-zsIrF!Lp$D-dx4l0TJ+5DVbT3=w%D4418r_ikUO`{(!%hWo%YeW^;Jhf+UNu>Q z-G~h5|A1*%6c)imP&?dQK&NwY9!2>hNDV`^971+0QFDb-Uc6!TK=qY7RNb+es6fB% zOLlP|di%IN_(|*CtMAJrxKNG3X;D4D_wUBFyV3K`q0=b+5E~~a@&BAdh=o5b@Tae! zO<<)kC?e@_g#?dP8MJ9S4FEftc8Pd}O0kj6@8=#A+jah&jz{}*PiGY*dRR)IA7=zr zm{57zgHAWJYh{VJKp<5fsHng{I+en7nhuQSy$&hjJHMrMo~0iGBH2)*)ZxI3Nz^<# zDC=p5-G{_RY)AO0Ur3TnJu0YqJ_GnD3GAlQCR!DC2#4AGz zI`8a&o57B3J{d>~13LH(Oc0v8ONNA`1Gj;EaN zxRyBhwanMj41s$^%e*^I0DoP99qk#ncdvhGVp5pb-zoz1EBx~wJ6D`PeVj-LJm$a$ zDdHU4uKoAc0G_!bAgyb(dtnB+D z%MhU9XK;{~_xVb~^TxVtdTk$*b0}kGb1Yd&AahPhAd)u687ds){K@42d3)7gaMlzM zg$^)1l@u$sX4UCu<_rhxnRw&>OXuDKVd^D0jzd`9k(SPJp%8iUtZT5^TgVgr7C8n z97@6gUgF{FU7a>20j&Glpzo&TP+YF>%k1S%7Efm^BNzf5WfW;Cv}^7A=y!P#>)?w+ zoA1oDj>AH*W?JGHDv99P8%fLR@=O*;iZ1!zUm4fGDRGH2Gf)4jwIll?)au8NtYQNU zd?FQ+;#^;6dSs_sgp?;2mGoX1q4JM7XM|GkAsgLsy8$ z2>=(ir&+wK5ZavnoRwf%0t^_jtkUAzemeSy-HgBuV0O=!o?)aK9gl~Hspx1757S`{ zKoy$?h=JH0m_YD*KkwzT$nqP1qs0_EBo$pOWxNEClz4ZW=8i6ofxb=WT>$;}y8D>3*}oOB)KLA1wV2d+l|_B?gZBVMEgmRL#2g}V19sv5`0#bSKT8zw*jT5P1B}sNr&=rxg$uqc z70MVpuo&*EsWH1VCw?iRi&K%`MhH}+ck0uiZ+yZWY`L&z2jx1%lZIseWzuKQ$AJfl z8XL3ddz@{R>R)Vy5otS5y`iZ%a1vy3g@eJuJFN3JvVo`8&Os&7xa=MmOeyFFKIbR~ z9z%rOC_MPY4?qmE-Mc+Qzz0|KAoO=C+1J=ZUVsrAz64xi*DeZ-Y}6dNw!_gaTqFcC zo{6NQ)-Kz_uOk`aPO1Ajz38x=s)x2%5fHM4#ZnjyE=jSGgp}ih;HZ@y#}mW1GD45H zL`mU6zjNl4ghIU_7V342Pks6Cz4`C{#A5i{dKAf>qV!)o(8I_BH*=D76bIPAmqZ?7 z(KntpaLcQur)7RfL3Gy90Ct|4pf^Erp9vS~_Vy0AC%2yWU98<0AC~?4kf?^m(WDUf z6&N1rzMWpt-C0P21UUQ9Cwd+qZ!KH$GlcyJWr$s<);n=vetS;nzRI42g@HWv1hzNiTqUsMfiRwif2X(74i%unUm~x$D zgZ5eh^N(57+LV6zhtqOi@;2-?y<01CMIeyrT}?_#WcqL+^sUz+bEi4cKvso;29>gq zl{_^6i4`)~O^o>Re&IlQPPrkWPGn?M^zj`@zlFaxMBja8buifa_FQ(a<5h z@Q_^d(*!?|@g}hO1$7H=>srrC4c`0kqwU%(H<_m zi)W(MAjs9|nJ!Hd!>EQ(QpI5d#BMEqVi^R5TY`t7d>*LUjmxss7en@44+b|j~A$Hj$RQEExZ;z(D$H343 zf^+Sj!hW6RJ9K1MM3RZhow{WNkLXeeB?!F{+qrAkpm3?r2TByw4d%3ukl|kx{s_8`0B{ z+sAy|JxL7)^h!47w>fTz<44S&DRjy4Y>wQ*&$Sx;46H24kZBj7O{&O88ZijJi=f^Ma{o3; zApm|m@aQ^Bie?d)!U%rr6nx24KK!Q~0Jsg0ggTA}rp%9%VE@EX#fTT`)ahgYbW9Lk zaiQZSVaA^=@ym>^N2Y@| z(w9c9CY%bjqBr3*NENizlXY+hJmgGlAiheehE%SvZEaz?aT5x9OCJwd`?(uM9RRsaJI4Gf%RjJ?1SjZ=Ix5G$$b@K z@?x=)S>xClF}9p<+mW-bO<&8!&9S27h|ywiSR&h$bn1Y{!Fht*sL=%Eez7{&D+!N4 z$750Ol*eWfRXx*kX7cL>(}yivDxXh%HpBTBtKDC*s!;mctNY=MgHMuh*`F5l zlpe%-YX{In$?Q~YV+nX^NLn@f9`P~}SU#R>kTgg)WoS)g-KaKXA?Q?k5Vs7i1 zL!8f3QpU{BU9ud1$SWOjQl^HN%bv_q1XoLKFnLW9 zQ8|^uWtPsvx%&=hD}fshbelj6mSb}t1GM$P0Dh{_mhIIw!#@;Ex2HJEQKbNZyH6d-;Ksu%jh`hMHa3n@!k{2o>eQTk`j zLgX>&U^9~aiq363?zb~sJlRpFA|E({H|;gQGwJ9+)vx)5FQr}1Z*QXJANS9*el}7i zh0)u_FPrrLl)@${$3Ztwv+prZ5cZ);a^PNPM#K?!%waE95O z&1JIt41-5jQuRX)NG?|`^V!xuMnBNL*4ut&haz_4&(vsEf9i0ij+lioFCt$fC-U4R zL*Ot(dKqc$A(|nj>j_7&u}}hkI~!k@NZiwXAFx*(pJl`3pb4-09c@ zJk;)Hli6v73$d8kk}DHV5@3~a^xJIi_xos_`9`Qq+L&fVONW-jscatba~`-mxbI zfO(VYX3f#VfM1t1^)^L8^-jwa;ID8f7I7=hI5~#?GB_EP2^7bWVq0IwVJKn$UaWI> z1?PTQLE~o4U`37D1IQzp&8OTHbYy0Mth2eE0{Q#$krHTzVkJka`S_L(S9DccLbee? zCP0-)oFAFEyrW^{?h#gfC^|6FJ}#3)%>77aphz2AL(+L78{E&#joRTyWD`+ z>2m+l&*pM4e&7XIN6PZuk*0hP)bX=s!l}&0RyC`AxBde#MWy6Gk{!omy)#PxvK?9k z$;mJrSmZ^lxLD9$G~5+xFwf1cM+1GGZFcZg7y9OK&lS7Tu~2)@cSOqoPYWtCMd<*> z)yLcWgccckhqw0*i8n7jDVWNBbqE$966ySLBvJc$=m}_dwO#MrLZ7nz=|c&+$x-I! zS7v6|76m&3^FLm-4Q|dA{=D67?`FsRXM)8WW#(Ec!Zc(93? znBQjPa9UyO$Tl>k#706@lZ(pJdc_rIT zY9*mJ?2VT|kE@une7R2HI83(n5_<;2ED&WBPONnDwqf5tpAUKA=SUZ?Lm%O8iK<0F zMF1%YT}v9><{Z{rZ3jsfpaJY~1GS@iQThQ48PI6Vaj@Q#vJ{Z@`N>Y(%=lf&7QS_> z!ED3?_zH^u`RBeS^m=!>^xN(QRd8v5(NmVb+2=bpb12`+I@6C9TY}v@AJR|TkLz_w&Y0JFO`k9RdvA5}m@T zs-(Sj?1>B2A_E5J+Zh>jnF1O(7mxvPSO^UkQY#!^o|0=NUTlmrFmvxI0de>J?qJkn z71zj6lesiI+l`i79d9PXh7=%^M1G8wGJ?k;FuAByvhG=1HDYkjXM~@BP-2D-6B8E) zw74GP9GxHAIoUyMd;al(oVV0UAJUF5nb{k=@V+B#ymr&FyTk%%Dia~JCOe=(b5aU8 zX!2C$^MsNx?1$Q%Rq1@YTuYlZAfYe}rBpkjy6h#JPT2zmF4n=S4LD=Y%iS>^=mv zCvCpovj8=&&}C)$1qB3(q-V5Irj6Y{&V9}w**I-zfafS^^-1V@JJ-8thrCM;?@@aR zWIPS}p;epSt;WJ6np^~#18+ozd`XT+EOv`BF1m~&u(Gc)6F}6v;El3c%mzkbjS$Lj zR)d^{yqaXSDGIMsv-o4OsiBMX0$P22gu4Y~nE${0U+$q8}@ z#qcxyX~gw<cb%48_?P9Td zjgx`)^n#{1u3-CLAN*W=%eBdMGAgmHU3l+{D#QZpT1F>BpoH+gFExUIbUl_5gft+i zyGt1^N+n7C@#kII|9Q?BE9AdW6V)y#HOvSnP-J+pE7jM{!w3nqz0^8y3<%z2>7vy% z^41`c!|p@Zix671@6VUZwrt+=|JmDfT!e3pJFZh%x{F5bS`EH7YMC#^eL?inYMlAC za;w^pP+{yW0A+%+1OJ~m0^tM=TAhDFty*Lss(=aRc6bdv7S^tY4Hr@grj&$%*-&-G ziHkxkR&PhloH?21%hER_^tfMJIKYfsv({4YGQ=D*O{Fe|jy^;_dNrj`k$?%JcPfE~ zDpmoI^>qu!7Z;Q0Vb)1&bn}v2RoWu`BDH_>hy^^xdPiCl65jLD7_CNPfN|qctCT7$Iy6KTyc2O4N9oe*}jw+2=Mm zz<>R=!;9M2*Y^q`Xf$xMppS%!jnB|qJ* z!Z^(rpW!4lQSL1aPvY37(~ipwH{qTB|EH|2E)<_9vgB4ZJCTtRIEJSri%a$>q=T0n zag5JWR&M#MWyYT)VEe@bVq;+Y**jB0bQt{pq;MKDmB^l@U=w`Qzo+&Z3>whwe8hKt z#3k?YUcO#?xGAr$)ywyC&QnV;>?y*R{CB>>hFrb0gjy-%uF_Kh%&Gd( z(+_OYnkCVp(l{pyRBMImI>4W>?kWRQr+y^d2%N=WiN8yBW|26BaL}%DDz?Az(Akh6 zt@aNs>zJFn(2a|<`wuj#t4|IWK&TS6|1!lSgN7-QNf{YeQT0D&*n2h+P>6O%9Wn56 zG=dB8O7y>+clHjW@0*fp?^@@}OcW4K&?#U388tUKeP=Z2y7#Df9;7+rn_raq zCh+|pUt`a>c(;mL=2+`0ZZ9_d-v3(%_o$;DK{Cl#BDBD_7{387!SqV3poc$$3q_Nx zE^YH=i4E=nbhLsKB90XX1PUt;8rb*MEgAxXV19!?hD~glD^6ZP8`9HBcBL`7hvLE!DNdvcb8AWmZ_=X``px$Y0UL4Z#CfEU76J(9^=|Nli z$l>p0E0*_OO-#7)pY%Z4+GOGDT;Bd{v)V<(p@!V^;k};D%aDwk_+Qlo9*M${vmD3n zS;u%BX*k3jCT?D1S|~xg!>`)pUAo$XF{GNS!$)ho|7N4(`;&jf{x;|7KrTF<;m+Fq zrW|+^&w5VNptam#yXv0jrP+|e(<^jizOKnHmEJX!E?#uSbap7M z6O<_tvb$!WEO$=JRGzbzT64688>Z8&)YuR-ha|;FQd+yh5(3eU81wwRGCg#bvOXZ;`cLmtLCux=njath5=;sCRbVmnYFeSpMo6$ z+3&UF;wuPe3QsOX>#}}za6@8$#9>pNU}o6@CYD>DIS{xl;H-JO5?2L&!%^*Y)!Xyhk-`~Kle7EP%4bOWwtsB&y^|Zsr1igL8E)%QJ%auzs^jh!#Xbw`a7OT)aqX~KG_HW)jc6Mcn& zI$nl|eqJT_g#@Lg#@A6sYkOOYe8zh7Q<8g9-XK~(lX_n9UpZ5*f4k=b?t00;ef#z^ z@eCb!dGZ|%rsg^R?n$A)PisIJN`}-gh~sby8r{VdVVlAaZBxDFT#i&OvBB74aXeEo zpDVZFGMRu-%9|wj+|Kpu8HMg4X0*U~4qNJ!@6CVS-m4?#@VqlV8Sq^n2ybtr^)_e7 z)!%TdDP@$emyDm;4<{cmqWNwWPYx#6YKto9UBPNL%?+W8_gXZ{d6E*9iD1ZE$L$Fb zNZ4CXz*{pv+;FJ};>+!@(o>&YCQFrEoaELIB4(92iX}MG)Z%MPmCqyLV8&6u_ngkG z(1@~GGMpGEm_rA5j~zb6e>}p`PTd~|wJ41bYp43r^Z9&_7|Qd_+kDLx*#ex5u;8b2 zhnYE{%mtgptv@FU*FaIM&jnNIy<+!3--E(lBcA~=jHn@NIg3|zohy-SMNUsVsZdYF zhC0Gf5pzkfeg^gk zgJ{D}2cg5|FkmcHpKprImhSyt^EY2iNnU1L#L!VGgAqU=&8>Jzib<}eno^BXHcmmw zsrkt2zXy5-O9PP6;*+F5k@)avBzygl>YUu%a| zY%z1>d-GcrmQK5f20YT1+Lk!PrxVQ6tN#?I z`t32DWa52r=6jt~C&y3`jQQ5JRi^Kzaz&=Du0G|Jfxc}j%YLn6#x)(dDWNh*HxaH2 zLP;4#Mn&C4NM~1J!U@h6u*yXsx3afCcs{KaLx{vuN38*0s^18fDrU;++azzKmk&s1 zeRdXNh&{(1Gh(g7yOA2*9sfc;j<0nmG-c^XB~gF2O&&>`gi{jxRp~r)cu2$vklp!fohD{euDCStAh?t*bm*sUS`T);hH#LVi4?ayDtl{qVsvh*W(NC^Lp zm}P0G#a6K)lofqb)?~t=PnBQo!TGnm{LSvi>EUX+uuBkm>f$n}ij@R4HDlwcURak*Asa$0ep=RLk?zfpK9QTUfO|CCjfV6@ z#WeAH-qPr(0Zc$#KFEK+(|^7Ac!Orn0a!(+j;3-ZkyHZ>NRDExL7SAWBFZP3-FB_( ziu}1-<0~$=d3-W65a3MA8;PG>ri^$1cWM-tC09&EW1y6CwtsbPghU-Na4Oq>DodrV zc$C@S@zfaH<|ViXfpu;Z6i33m=DqG8cqku{O!8EOMHl7G5%SBz8j1_bYh?}vfJyZU zK;V+R#93PV_fo^PDie;T;P>sVK?9tQEx(Xc{^HXU*@4q?g~dctlKnd#BTLrg&!Y%| zV2D{A>dypEe}YZORf!}o1AbT~hlLs0yr1xn6HdaZa}47_JHyXUsF*y++%mi@GD}3F zHJY$kW-?pShnNLHkxd>=z$w^V@#vzz=TPy|`uT+g-DUm_!wpM*ohqv10INFw1?}vx zf5f!-6my+oM&<828^dRTa$ zOBgo`8;OhC5=iob9B9Ni8N)|^*;DY{&E5n;a}O_q?|UvF0igUEVX>#hmsAiZM{+Ac zelS68c4kJ-RDm*7JTPMyGrsLaY=!!u`ubO`-M3?a?s>6VMUNvbM`N0ECN$96_4H*l zk4I^m2{7UCCPuh`9YV_T17edqEf;dKWHB3|eu;Tbq(ltKQO{8&xy_f%x_WqUc)S}B z5}y2L46EW~2m*ClWS%nP{HbKdfZpDv0XesUz+fdr0WOd++fqS6L0(w9w^{}NGko~d zHf4Mm%TJBKw){V)L-wd7%Q6z`cwha z2*zi8;yEZM>kG_DMUU%F7cNuI-}k&EaDIZyb2Ao)bFo0-npctde?NS*sa(>8PaQDi zD`(R3GibgM@lKZy*%X+D`nz8gks_bwQ{F<9lP!L2R;8(l&^42i!XAV7|9pF+^n3GF z7sWAIyPkmPpO$T+shdwgeqSA@RXRL|+%m3ygCKpbB&CCA6T7;o z*e<(iGD$3${F>x%+;0&&FDAl3Qdb!S)@rpjgoSe7R-pTjG!-kQs@wqsDT|2P>#RQ2 z!HOWv-G5q(HIGGhA6iqasPMd9PqqljLE)~2(oO0~-dSF$Drb7##FbPhwASi_SV91u zYP>bK?YxU-GX}Hj2W+(}CmhZ#h1XH*$OH?61Q(bag`mQ+Y;8!D!1fuh$U5#^5L$hy zIGev^{J?>WWajpVx$m6u4+iyH%D8d&^`Bu<)mcWAQVd=;f01Tmk!KnJShc+5iz3?o zCsl$%>xFMtQm2G`g+tGD_P=6oqX}%;1*?Cie!j1=PWm>1fNe!T zGp|l+IG<-So5GR5$mWJ@s;)4jxmSsRbIy`%H}-wySLR$WdymuOqTX%zyFjg-&Lz24 z`ZU?z_S$XJ&!fFEg!?;f9qjLZu+4`_n?T9 zeS}9%pt%-}#6}p=f6?n#cmz|3@O44n9lxFD8Zm8it|$8~s4_w3rvq$`>hVcecbM3D zd@(AV3I30j;saCk5Cc|75OdLE78yft1Kv5A%_!me5vsOY2r`tPZWo>H-{!S%ljnTB zM}5piz@~lyFSTfKlE-UXwp^0B^W-oc5*>%q#3|OE`MCz_e)_lFG4{%8lLayXSaoAi_$wJj<9ZF9B?fnRi;`mdlMSS2)*;11l&IGx zT(;9n4_np9UDtRDtEB@BW9(-So?Wc==W@TyysnVXZi+-ePngNoPUG3GslVJ=(EktwK?3l<2}|pZ;+VZe-(B z=DRjTTQhmh=XC<(j4Oi~Rqw@P8htFxa{f^XiB1s*;h)6QN$rscAt@(icfU5;(1wym zm!8>{nzL#$zY-I(igmu&?|rU|4>HAoyG0tKe?GyxcbsN2hM!M7!QV_lz({Fcl#^j- z12N*qCz2;1*6bp1Eu_hlXgKM!I>R3>8k2|ES0U^d`p_6?A9YYxtm50`d%xVuzm)UYP9!Q>4$GUV;+ipJF_crRhe_z=xojky zsakmECR-z9y{^4eizrk{!y5^QC1fM|IF;^K-3wbak^E2eN3|WF!2r#oojnmWoYvY6 z`V{{Z21R-P+;1ix0$8hS*7k~XtFy8m+C0~u$n*_DF@GJ|@Nt zWOCaP+S#204x6RPb>H2!P0N=H39D=b=vW<~&{w>Em{fckP>y+mf76(%DNC=6Df~-M(%5<10 z{33%n;!LBR?W?O4)YI*|olKFDQEhyj(d%`1zwLCXQI}d7t4AI3p%t-|$JZ z(>O{Xz=ul;j#Ms;W&U+U&D{QudM4EE0fw-4-9NEh$pqZt7;Sb0yFmA0E0_Zfjlscu zMgew=T_52ALsK6&AA2R*8;4#MtPT4{A ze?FIhn^AzMPG|4m(|_(~!ozJ4=wDplhtgq|j9TysNe(g(^A2y18-XX8-q=7&TSxp4 zjOy#W{rV|h}eBe_2Zzs(@6;ZmI`&8k}Y zI`FUk?x23%kW&g@)iwEX-g1wS$EFfPu9?-h6xw{hz)PV=-oDJ4%D2e8R-^&;sm@1! zz|1Gp&Rkw=z?w1PP^}I{n)mYbk*JJGFI|bVN7REiv>JSRg zzg}A5C#=;TDd?;Y$u2?CqR#caQ&g*Y#M-LH(c|2iUf zhm@!1|0znK(~;5L*M75qS3Ehf7;HNJ^QV8u4aDc3R_Ax_v&)~BKK3Ke^L8ZN>!3|r zcCp0C6!D)hv=n6c28Z4$}>(@dx4N^99e-2|#BH#Oh4c?3po@=40^LXUCg- zNR<79eD}cSGRUN-r}&Mwq-tm0->+eNJaD{p)di;Qutrc0C?AD-Z|IXmxZEEhc zzSg$gT4Z&*!yPJKDU6 znfZU!H?GBInxo^^N!!~P@+IseuEe!m@Rg;$5Nh+HCRP4G}ibWi~FCUdH6Nm4(i z6QQ17#PK&*xLyp;efGIB#qEP^2aC}ZIZx{4fGfH>+keBdV99Z}zPBGR^*YD}b+9J( zf(T$PIJKQ8lDtF8oQy((FIe5v2nwFwMzB(b2}4Bbp2iwEHGIC7GE{ymy~GXI7(TXq^2hB z-IFaQ?&3_+gp??f&<=QtIL94c`y37SkyLG_(q_4H#6s4)|B#+Q0|uy@=>XVqgi)O? z-Z1>@(}+6F-0G@4Sfks+G(~VEY(jsV4HI!);Xi{Esj&=rQoWKPs&mCt|Tyz|tCT1@(~66{js>86|SMc7eNqElnf zbFvJ}b?q1N16cFar*zl=iuI9uWd?Nj^^4-sSvjkm_1(nu7D7>xmm12~;#u!a66{)G zf8fI;NzXi^8!$lx;nkgmx+EJKedO}Ez;To*Q!>MZiYNNL#3zizP`IkF-NyPX&;;(5 zzlER&iMMqtu+$#TSE#!xADJCg>qfO`;MxV0PU%XIXdD#Vq zTqF|bQUCm1B}~D~k8h3<;`Nfu`9&r5k&)}U2><12`<-`U&kJu9m6Sxj2s`sE88m~s z_MGlt5k6jXX`h}S^2v-Vn_fOanZy+v+&@0IlJ3vY=22>;mu@}nMTc%-j@%{_k5i}0 z&#cZ$yQqJzm&`KLyKF>9*gk=$<;PD0vz;R}cM(p;{7m`NOP)+nSPgA^q|2YdG3{ch zVDq_GHP9kFEX?z!qHN`L@#2>vwuLZrxrH?k*yg#Wn ztcGIw3%LX(Wy}GDD#=N{BnU^a99Wk}2EKpyKm8ogj|YA%dtKup*&Dv*=o!dgP43Lz zs3`~|{89e@eAw>n7A&aGm#(y2KN$Bu0r{EHn}zN&tXj_r+F*k?ly6L2IaYK^#I}uh${hks5>G1WaX-%B>!_6NZeqPH!MV{ zM$At_0l!QeJxt_;Aos0WArp?mmIQG>z7@R6e6h~pikn+)vry-~W~v6JaV3s>6Gi#X zT}<$l!!0)dqU?7lu$6av`Shg^R)>0ttM#`tcI>{{&O#E9!g&k`m;3Un%GG*Q77}Wh zYDuUzL!Bjg)giJYX&p^o?Tlkycub>;_j@e%fj@4YkVvh|vSOpUmdUd5y2fR&pk%=~ zQ_D*)P?m#X^}9+9IG3jSczB-2u6$Y{#62ZG(AttyWIA#^pgm#R9KFV2vaj_{LSwF# zPb;qTMCYiba83WZks(V$U~`h=kbCN@qVigL-jwY5}5xwb2Hv55_eX_kk{+dJ0$wG%3>RVRZ8(~eZLwbF{ zOq*nu^WZ@YNDueBfMB*zYJuqq#-e7xc%IBTN3m*9v$VF2vIgl7zj@lyYU&Pe5qW|T zuO&qU8`2`{a)wH=S^Uxsg^GOP**mjTx3LSLWs-Ajxs$#^&*2{Kd({NylV&>GF6Zc2ac1XFLms`!oHfouw-`GNIUasik?g!L z;kgb+ne4kGlEqzBGffy@Ic`>bLvTQYwRlZagxc;7lIHlzsM-1S%B*EaJhv1$^ZzQ~ zQs;NSX1K0j_V`v4Q5X7|9Kb33+L~sV!uJcg$EoI9d^Q}JnWC|0S~fbQ~!`>qd#hiE3mFSG5yRrJ{JQ41rOH zj}>s-Xm}sQAXj%U3xhwgh3@(uFu*%sd?%KWe$ks{klHoAakz=|-MvMKDnXV$dscR_ zBQr2)c778>Ir?>5wpoR`O7yDXSHx2!Q6YjYP|Y) zAt7i=yWL$*hnG6C)GpnsfASYu%Ws|zWww~}d7d$9o+E7D<5RJu_bPWW$=G5BtDyL^ zH*(fBGs3wjTELf7;d;+PSFGxKF!F85nE`Q;IZtDx9$K~gNhHI+zHnE;O;3T3F4#u6 zP8)>YiBiNQ$Cq;+rwM58MV?yP<*71`lxDbl z-DACHwOM;+byeDre*!YqF?{K}{ouJbl1`rz`XgJ@!K}C&!cueSBJxo6`jq}ol7l@l zah?)ow6&}`t0Y15d@a>a4DAzaOou%jd+LcQoC+D0{ZO3fzSa<=v<` z@u&VhIQKgE|Nis>O+f17HkOPse4lBWAQ{ezPTlFi+~v}Tvc%@_E=Wj7xDv4SsV&Ee zFmV(n^s^rADE(-G3f-K~y&&ZZ6V$L72QZB89LtGiMGgmvsxYV0`;cVLFaa^FECpU` z4c8?1S$b^0QEHqB2?`7)e3%|-2F;;Dsn6Cnc72Qr_aNT!WaX~%oN!S-=YgHa@?ZaL z+3*Jy^e}~^78}z3A{uhhdwSZL}~1o8iKEgi(#DqHf0c=<-m(_#Fw5 zQE0U3rkCdpj^`gp5~uKFX|Sl05EJ~js9f3v55{wP!$o{~Vq@tiA9Bt625UNAZ=~Z$ zM85VUZy^<%?zn{Q)@d+69(R=?H)D0vO965BlW|D!_9aq6YtY2R=bWuAjiyUt@8=@m zsV=`eF=r|KFWB@im}pnN%EnCP{C^+LEFWsuu_3w#li34)51V5^r5s)QSWkLd=ugRU z^ScwxYMuI&ur%o#uXq-}r~F6KuIqpmw_n46-3Rdx0|q#xJD$3*z##!Pb@ z)=&@sTxd~H+yzqXkjR0Ow0>fpHWM~1ZyMmvGI7dB@bPWPb1!mfSU;Lav;aW{HGltQ zGa${u7ecO-Cn&>^I{4rx4dL@OUTH}g&70^&Wlb6Zn)*oSRjk>eiDJvR@cHsd43dj(|@Ti9AV%p^9epCN)x!d}_ z^zm(dbpdkJ97d_PE-} z4JTABvn<~)O@`Db&3CehiCFOXv~C2x6#+``Nj&gU_!0e-$6gZZ+B4_bRY;Nq(#a$1 z+lgprXK!&oPVl%71wLfs&)PrB{G7w<7AD>^uK__7zRYe<$}rBDSz{3uCgLb~sts~T zM<}AKt|idIv#{i{d$$A-32nnTimEpsaalSWR1FVh#pIq-eAjshe~|7tRz5JXbUrhL zqmoK&sibR7W*49!fYw)M#Pa|dPB>q?$Etb@)4K`g5Y9qaz2`G#KS)M*p+xgKjy$9r zLhWKPWdanqr$oVa0C%;4{?cJ^$5wV?&kVmZ^#W4Rd*>H+J4z}d!!!tGo~aD*@Z|Ir zK4Z1FyL!FE#UUIYk)bHyPFW6)ThCMPa`N!(^3#LwV=u249E4>Hj;h|`n7!i_1w8TGqM%~MQy*n54|IA2BoNHe>UJR9o_U#%mS9F^L`O?d&uk=V!1qM` z!DX#xNquv=JM=Sjj<8!E&*r~D+lC1dTFW zs22)pMm&}EHh=cmX2@ihX#-_Zd~V550Bb$9)CDJafuT)L;( zGO?J9&hQ%!;h$%YqI*a|4>=4>AkXh=Zd?yQj+Z5;$G%BNIKrBgs4yeGmJBkfmo3og zh0JSHQDu4J2xS#e{WSPLgne~TRBzm`iU@*$fRrp<($c+1HzNfMdX!?K6P(M*qNK>jTpe~Ug9ad?EIcMvBcD<<+*KrQ9m{} z)$M&7EKa`Xa(a9Duo>6fm)fx{(W!Tb^eN}aVGVv79Us7Qqe4C6-Q*k^EwRJTb0xNu ziJJoaT(Zu~ROrDr^D@&+c6%T0?)_aZ@{MP!^@$QjdBKstFJ-PossQ@{ z0e}@n4DWx*k6{g|Cg@qEdfv~CpP7{;Nz)zgmT7FV;Q=h~Uz6joFNxOe@+Ek8fn(Jc z4L3=Q&jheX7+rD;@jF#&-{x<7>r%Z7vlX>#K@`#1{iEx~w@X5DXM4o6Q(d67K11_yg~^h+$E}J-SPX64 z=*-B#DOv6f2=uY@xQc7}H@YM(LO?y~u>B>c6Nm&_&WE4x9BjGJRw@7CfNQ&Rbo83< zct(HV8fMw3OAw4hPnk4mvnI@_UUO(?n;9;8rB-_Zz+~DRcVM}Y0d)_+83Pe2OnlN!bUY>o4t?? zE?|Qr(7{2|w9$6NhHE0d>!)@1>(!pyIL}*d1k^Oe?8@TokcIxg|Le&8e_mLHb>0lm z(b#5L@x_gMiri=cpmI1$ni%ymL;j>tOmcrV)q|J-Dl{#?LsR=#ADwrJNPDWxMX;$v z`IN4#Jj5kQqlpgtS5``iAmhfLh4^|ej#Q8DI8Z4RckaTpP^2=_P5XjL?ZaA}*I$@C zz9~DXSN}oLPj>BKe{C`G(9YORv(2!%VrcO8nZ(qDG{;Lgw}d{iZNQnb=Qix(D>)rGPWk z3|k3({yZFA<1gB;fd1TVmpFue_iy7Cq7j1)lAlL|ZKCfHh&0GCV zmKNgSiIOqYhE+#3(-eerkFP_VtKaoGNX06fuu3<}ZxCjcQ&Z?!iSqGPNlFs#9TNV8 z_0~KoS1wj|NR}rmQj3+oQL_($VEHn7{o~9l#NL|{|8r3??4YTA9XLsk)tS*ND%%im zb?ud-P7)kLdBJm zBA%x5($W0L$)0juq1L}fXIZiZpUh&u^{i@9uUHYxKT=9JWZKikSJRGdRdkDr`NwHt zesd3n37aPTje{>pF7x>;b(sMx7V7k~opET-`r~N$*e~QqFk?XfO4eGdw*v$bPpPu@ zAjrH|ZLk;0 z;y3lr^@u^(x$3O+u5=>G1~Vpnt7ATp4@KRgyMT6h+^b813&ZwQOLvhx6F-= zLa$+8tG@p{bynKxuE&j@f~q~DEQbT@MiWtq2lL0QFL~ug$9jeG#_)DAFd2&1-2c3< zUcN1^vwx3a?ltHa(f8F6ClF>8SDce}A_=n3Jo> z1sE)%j995tq}uQ9A%)y>{e(HDvm+L$2J(iln!P*<+0ABYE)1Y~kQ?!jtMAfIW*$CR z?dM|s$u1aRVf95c3YVN!B*F3@2i`#mv7CMk*sn(HSnEnE!rl zC1bvUv&l;BrJI$h|LF6>J^v5B(ZHt!dZx-$HaQuEqL<7Qx%spcb?^S>VcL22j>M>r zN1{umiQYaNcMO~!Bm1UiO=(udf}-D)I=e0FRSlx$hbbozcZnm&`GGGxiurRio}mGG zFeCi-Fv`i^{`^fJ|09C&@Kuy_PUY`VOgR%=QS)J-{t8{|)DtI9E%8zMV&aUB3rB;E z^bQSWJ&q6Q_7z2m35N#we|5D7O^`{87a+q3^UVS>R{Xho2oz>nV zdb-s0xVh=)y~^CaUCop`r}BJ?d$`p0OgL;n1s+l^*NfM1k3O7&5x*KXY-gEdg(fKo ziMpwCTT0TFmbB!1$(vQWGo|Yc5FUQXqYnG6|9qkC8Vw6SFbQ){|E0cJu+jbv6Yv>$ zgQ36idRGtyG)evc_->o^;C(^7#9EKtGO}zub(Ie!PcZjir*fTjI;qXbMMxDWQ_U%{ z`l;!LdRAri7(tcl9xF^LQ=g9Bg9QwMtM%yw@g?c_)O^}735~no2PUzRAxJmq#5y># z3}x~CBP;JaggQDk;^Oy-VT{%WJ-=BrS1tHYIEs#L@EZkdae<4g^@b#4YsDu;2lbQFOEJtjHQRpM+h2?UBV zD#h_)AQuIrha8ikEy7s8&CLz~PCfXj5STyMP_L5yBC`&xW5Bt)zwLG++G=^evpRnW zF8R$U^*f(?lb5S|YKiae9>E$C(CzOE=^-(?<0=0XfKLYsPqM)~Fc zTwlCUuRhvNd%nDXxQu)9J1|KQCKYi~M^1BISufOTvK@$L`8-rKt^bO^raF90 z$r!EWYQJSr+hg0R7|pR`Xz~DarFL&U0P+=`zWDzB(yg+Fv8R^;99(<#bcU zWi5Yurqq<@`Ks#pT>W_)jnj35{M6Z-%WnH~T`s}}Ff-Nz45qKBQ~GqL9rzNg*&aPX}?9ex;Ne71VJwt9kU-`~~D)*Qc(=U&PC zDGXX*A`2aPF^!^%x1BkL<_b!TxjM- zvwu6^@@(d@+X4&2tH}A?zYWZ)+Xwk9kQpam5ZGk*Fk{2q(eX}WC->N$-ve)N{Ia$T zx6V9P!!r87gwmZj|CVGTXPWaval;nPe@y!0{nfb~z)T(n`)W}s4 z*kH4#M=gJfn{V)HnU)N;T;Do6ujHVF^kuGPXMq@Z+;O?FS{mm@ED6 z#r>~|rb%WH%P}pUO#7}=-^Ri2tw6p;d8JOAVvjtHvHV*DmP|zAgb<%?^l90FV=6Gv zk%MBZp# zJxs|da2v9M+9JkuE zRpUzR(XSP!w0DS*%_hyRQn0S;oRGj&bDNZ;lr_&yhjv*TMVELU>o$W`K>-8RLt+sc zrTiseok~vr${kGo0Ou1J@!r{slgu`;vg)99`>WY(>)rA}8i%tzogwlM!>Tu(LRxXf z)iqX1aB(~k@~p|7Be&c{4J2~~f+^O$>`VTR-PQ&C(p;mTW`pCmPg@e?8i^nA z<5^8fH8qR!oSB+~_>ZN=cDaAQt?~@|-r3;wl??HPoa9+%CRl;j;`&)9vZ)OM@IK-i z%w;jGDW9;yRWLZ%S2B4n(PgO!yNX~ic^z-&$#o{Q=mb@Hh@^L8e4$AoB@B)v82X%u zN5b$rdu^uf->B@|X}bjnjvMq{uKKE{w)@`^iW$ zF={tMO6eVW*H~y-{M#$0)J77?)RM!(knNb0!OJrqS0pZMK}0NFT-D|q$yf$0VFngy znuN|!s@Yl>WY6nO&vplbzSNSwJ$w=WV-V!Z383rZ{om4vqRbr^m)B~%(F6_kECka^ zZLL{ZU+EN4X7EJPRfUnTvdRx)dbdSwAYS{vZgMOP2QEw3yzax9=?o&wTWjH&`>J$r z#62$KJpIz{dWUU~Hr{&NoOHNf?k{f?)l@S_5I9OrPph&09{z{{wriVXL-0fF(UK8!Fd;m(2cdIZceT#hAM z5Lx*wRLRO=tMe@I1i&G0mF1c4FFC%yDDtGTZSFpzFZJA^VDo7O)+pAI6-Rv8o7 zaCBiw#r>9@Cb?Wd6+{rkFkh%mh@z5HP*D+Mj?5`8MlWO{Ab>MkK&+s92?qx!zD$)O zNz>L-s6gWT-hD5MXuUan$wy_N?DA0N;gR3N4+Wn+QaoFna;#I|p|*@X48O-=9r3js z-!%qUBA5TP9P?7>EV50b2Pgf^+ap3>X!K38YWCXfkNr)6w|Y~C`7qA^%mZ>{Rq*Je zKe<2&QH!HqL=s0)pKKsofdC#&a+`SO>;SYS7=j9e7NW9Fs}r)A*p_CUId#i5g0{h- zQojV{T1c3I^HbM!?IzsyxgYv#ONL_)fM&>TBpgkSXK3r?x6A-%Zd@J|k&A)RE z>SCrS`MLl1PvuJsSlWojIalkp=OIo4pB=YYIAkq7!1JHJ94|njKXck-S=;E@tet+~ zWP1IT;r3xAGS|g!+z3f4u+8hGXnEaAYP|P%>YqAtc3l}<4|gTcNSccloHx!|2`KnA zr(SOSgN#piL}iCx7-t*9Rg}-_ijl~Z#mQ?gtCQ-k=BkqU7W;!4n+Fu+TE4Jv?@Qso zH*+x{C z#YwhL`}VEMEqWOGn#*wvd#=bb{$G4N00*8}h-FR!f>iWh%fDv8pDIvTJ(s@x#cV%= zn4YGl-bQ<2+7UZ%B)K740*QF9JlQMHCnwl%k0Lg&Ud7xV$th%J}tGWozCgHnSNNpwg$zFiY`y z_#R4^j9I+IqE`}AbEt}?&a0^sdd)04G-$&%!uAWl?@JcS(DOct|UWCYW*c+4{mo5p`bK4}GFqQ&RwbdvrI> zd2qop%4TM&Wb_Sl=xF9-HIjNYaV4bMe02Q_k@9`EWv(uEG>gmKOpf1_n&Niig=!rN zV9ND;HcpQ_$52-1EsjW?+u zW5A7}=$ki!ND#6dCs|gWy+iI8XRxYZ?um^TPru@OyYTjZ9$~ zRL;L+OiE)gM!@5&Wr%22G0|k6t{Q#MNKrE5 z1Vp_~uIy5mEGzF*!mapgH=WDKa-F4M4Cd`C;zA^)((+#peDv+i!b{=Nt|Refm_J58 ze7y`M6ev1PhwXd9_xaoZLGyZ%58VmcbvgGafU=ivqTcLxG8taleA54u^`&R;aldr> z;41Ix2kavk+?^=8qmHlSt)lmo=;u}6k44dv2w%&%p$G7n;gzr0XD(?GCD?91c`|8o z?r4nnaKD!nCLAZczhigq;f&~*n2geRuh^BLOO%x6AF2KQQbsMGU)jppe^O{M&(4t6 zQQObWP^j3}vG6DBBhj0fvdi!3UyI0Ctnj$JAAY9)%{|amU^*-`q8%II;MbIhEJGD~ zoVmPAaSbKD9O}c0|fMMkN9W7 z_9~xW6A)mFOVSZBN|8LHwLUH`gP%~!W*kI<+uVwZsMsmxJI0F&-3}uyPdhjs$0Nh> zEqYLj9q@P`esXsg3MNwiwT5L_yp_kk8}CzQE~oto;;ud1D*@oh^0{6D=1B!=w^T=i zuid1+#5{@Xy~f%G(?_t*bp{!k!3CF|Lsm96l59r}6tPz(9Uv5zm$zMtv8698g+~DC zd*XNJ6|Z}MHd#mMYLFOYQh~T3bx#%}@(l876W}fR!nrDFO(m z^DhpLM&O7#f4>ZxJDe}da$UA&<7S&-j6NM|k|N=PB;gmq8g_%W%EcOg4bOa_7=owv z1OY&SYBxSOJvuri{Onf&e5`fUGExeBDHfpv3G9KfTdqL+_iv3Br|f9XOtsFI?f&Y( zm@_I}k><1A-fQsYfJ_4GLi>XZiUX*At1z3k97=0x9bIo~yqbH%Zobr~p5%1WM_Y2* zXSNTVLb;$=x_P+2q*3B{_dH~)FYC9da*1je0@NCx;E?{?*zw13-aGPua0nc=sflz2 zI3_Ttx-GB^r`vy0ZF0p5jPDpKn&-6JV(pPj-k6+bKN;)ace&lX73`#n=h6} zSoGxq#nyoO}`$;Sh-(4H>l){^zq*h^Mv+)o8Zb(zd#Mzg??dtTb-|FN-f$`*k zh4E}|AD_#5gIvB_B8c4PCC9QWU&|wYn(=M9U+p@l{Ktgt!IX}T-l*9seGYsM3*BWe zIP>M|lwB!i++XO3ja z`dmQzvUz9QUQ)^>wTnTWL4v28=-wwkL7qv;x8O`{3ogINl}j-)vHsN|hFWIYA)4xT zrIzZtg_jDu5^26H3bIYr&Mid$7}b&a0t94B2DD_HCy<)gHPESu|PJkw~uLG$zHDW zWDc5NJA-1RYM*lUZ}Nv>{y=d1?do{3VON>S~x|DclsD5ctXTx9o^LN z_lwn}(!Z|tQl+h~c=?#mH;3YAxr1NV&|}zfz{ANXmGeU&mCH`Ln9IfKHm7SX4mH8v z*yV!lICyApTL5_E^7{!PO1HPGDyg-f4`%rJnN*3w8Sz%_jaEW9(4zp`G`u_)F7oQ@ zBt0^4sEy6(rIpTJ1#*1}q&i)sI+CON6~2KKHUbhDR*x70hQi)A2cXXACrnzNYcD8i zc`5(9KjgF7mh_Zn6UMaC`m>$|wxIQeGou{;ImEC;dcgriG;9vdTahK?z1z`o<_YU~ zxIIr`htU9i{dU##(#FWDPvhg1@>EM*BnNv_7aEaEvKZklIxrm2g+J|3Yq^^884o2> z5e8!hch%l*CDpFpnMqJ8eMU`Z3=1jbxjtG27mOsScWi3xU%eSUbY5CIM4e%a>r{S}eHxw_T56jtrg!|b|7nf1%-B^XZVC3QsKs%oB$G`wf} zbvU>`jds`Ilery!M^VYH*Gn+ZLd0Z~^?Hh7<};;{INGi;2t@AQ$NV?A0JBuQ_k^xM_x6z;Afk)*RshwoBu}jv&JvMJhDRs$!RVIx#&j3 z@+3KY<&{XZ9y|#qAq2pKqlW8^mrw1(7^? zqQya7OqFKuH*xem_(v>-&A7H# z5UZfg@$_`Q_yuT~VrJ`%8Ru(`(^b8l=|t-y(n(U*a`|1-kRV_Ea^HIkleX>JZ9A`r z)|_UE!C;B<(2fi0#RZRZhfclqUc9BonF?b@u600ifP)yLVUe{nL-t$zc@+Y$n}jy`Dk;|{g#RVpcVFbFYM z`%efpm=Y@dHx7;=L!IO`s3tL-4_kEJ#OSTF*YfIO<&TtK4G00J-qD4CQ=ro z+Vh+%SjJlC)tA<~;In-LE!*Ha31tEF}?vL3Y-8`@>qjl|RC za?4G6qfZcEl*g&)l zE%706x*m`l4_%hIEt3#>+)zI~nk}yuXg^W{lB?rj#^dK|xGlrVc<_2c1!1Aj-`cv1trbh9sJI| z{I^jpTk6YCgxieCxnF5m9QxjQ+-~!WxrwRkME-D5M9+_eD&)%MG&d(KHaLQM2Vo7TsLVcDG%&c5$2^Ls86U%R{4j+#;Ka z@k+>|dYM?A36~j}-G-!(n%6~*1{K#uLRYQjc5b0jR5Dq0`GJS?H5FFRP*fuBf$GDC z**Z5Ow`+p6K@@dxetKLU0*!A*!&A3Pp7xG!pM3bUA{f2x@$h;}t=j~T9{3+?`W*{;45jm`pDRFLGz!IW=Qkid3TfJ=Ex`fq~!U^q4}!H>U2dY{Pf|x zgqb_B$3_o$v?}x_ZK1Bqixblnm&pZBOkSssM@v2DjLou?NtyACqi$#9w8a|jW7)xz zFgiQ^@5kF_|GNv~KS{>TDcPMy&W2ku&$67GTY)S8oBi8RkDKDugPl7sp|8Mp&bEGjtjrT_-`-Q zM~!Mu_@uIR$C-+X7`iss3I0)D@aLq_@UT7bES+|}?UqwYfIx9qfb_e?&khcF&5G_-6tF!&60_|r7o(ySlg3hG&gGY z(%)C!nJK-yAfAvAj_&uSRvC5gX!(fV(Gui6>4ugr=VTuuGE5^8&wE~607W+0@ysS1 z5k*yCRGo{K3qDOlKBUZhA1xNgc8EUn%FILmZsMUp!F!<8cdft#L3Zk;t^$jzWm)wZ z{A<}+<}t>rWXxX*x-+q%AuN`H3|qXe8ZMUG1+16htW=2-euuvEVSVd;q*$J1heDsc ztI4m@ewI2EzcM#z+T|q~D5Eaun|Z`USD)sfio8lf9dQZrGIjT})-5sX!sU9Egp&W} z(H!6n`Rj-TrIc$XmcmT)n1k{H!$hyJtUC`;C&D5Equ6pcRCuca1D+74eq!T zWwTB6bH6=m7GEe zj^wNu-6iVIi=;^$c1?$3=iB<^GKutggYO3C@Aj2b@p09po70W=8IZ`t48L#MSX-vV zcjSe2lPY>6GL*HuAejvz=?wK4_)3JmkA|gbl!Zi--t1)l_-0Zfkch2{rLgUNlA@p> zVQf}bwd5UEX`g*d{pjat8Lv3Pc8_g8FT;hLV{T1gDO`RwSZ^>Hl#IV~y(Z&U#5Ni$ z#U?RcI$o(&`U?6Qo1>Tn5A_F?f$3iifqR;&Kb?wIG;{g)0@>7N1sitgY#|6jeX`7V zV~q#edE%K=^bMCPGZuMsDkKNC0*Oa%4T_5Xc^Wi)g+vF`7q_(|?H?05JPY4NLtniS zAi`b3X0l^o)fZUVw=!@0_cx68(%?9NcJ40*&GSc(`oi7<@q(*yXhLG(y-umsQu#T!Y8Z7TS~X9ga6j< zp)hRdLnIoAGH%Yy-mf2shlWxsre~KQ=S*uP{NO?zX4o z-rb7FcNgKGM>q=2B(t({4Ty99kHmejfK+DSX;c7(M~B#@NhQE0~>Y zkM5JNBhcQI|L6y6573Zv;1LjHS2X#kTQ()c&=!*?GYX<~>obh!$mnhz zxO8GNAfOVgj%Pu+paEf8A41oIq3o$MJ7r?Q3@N3&OXU4jLPx%HR6jEEKy=gbERkZ5f`zi+KRAKAR z<}t$d#wB7)R7GhB^`DP4LqgsuFi6R!)qgoofcpE3;_VUTXj}nO)Ntye*L`jM`fOLu z)7Snx&tLV*%=g^!!ctNodunK7Az>WC<&LQGCI9Q%!mM4$yYLlpul0s3c~kjb)>JA9 zj*0QFX*t>1fR(Z4sadw=>otI+r(F7?PHo&mt;}Zz?l?Hcr{)CI#gJ>3Y%#}kWV!YpCe9UNMXs#P05TZk7M@fqr#kPAe&$5*X4cnQO~<8)d3OhyKG(}gkS^h@Fw ziK%o>0@MO&+-N8xzb>Iy!8M-EjPiVYc$13o=JU1t3ME7Z9tSDiQl~Bq$W?=P_Vx^{ z|AePMLCX_#!PNpgBmMt8(fU+!QNwW(ofzc%@`X)5Q25Ws(+tm|2_ZeBI4iCd3=`aI zU{G?In4TUV9qk(hE(W*yaF(WRXS)s~$HTe7W|-SdjT6y58f0zZ)IJWC*S%2yZ zy(+tp=QSm5KE@n(Jooscr}1+4juj31fO|XoZSS{(bQ)7i_>vuu0!T#JnWo989@o9>iy0iz=dY+G$P?^_Wq>6F0uLKd#ZeQ&$FLbH{CjT`3 z8_y3$?PlC*HnR)P5xdpVDC3(aAGwja?{ZRhm?}kTgKTEgiYQC^J6#%avlYf=pbr?F z)V9>c`+qp+5H445fE$kSbM~El4SQ-v528r3r;)MKCkqXADAcG5LWAvS84boG&6N=d zy6`Tq7u77UMfRkAL2L_AV*#Qhw2_6cxl_|Y?18ODzVAOsQB)tybX{3Ah?B3`SJ&79 z%iHMWsL`ZL%bx=l?Eiph6bPUDuj(oGVvWXMZbyxg#l`HtglRyJ-YV7Y!K;0?S5N}1 zQRwBuOljNWX%CTxBav5MhDKq-B& z<>Ry@OqbJr)TQ&vBbgKCZ=B{v__O$IBstraImzNG+773T0&Ia)+ zTH3>b>$#_mpvB(xW`xfvGAKNz3l#PVmJM_R=N4?+^5g!vbP9KZx@BFYqCCPwMDx+M zq$U!{uPzN&O2-PTdlV02+3<;BjB1QDaG8piHOV)|fz-$e-fO z+jh0+whQ9pY;B?`ROoaL_cn1*IIk^MRFpIpiUBoJ_`-;S;izqixm{;qCfX1Q{eRBYVly^EbIiP$7(3ke_-3p-gjXC%!I zKgI&d>>oLntobf?CvagOYr}+wJKt7TRoh)HxX7k*%haigN`^l@A#3dL^vtp}fzKB> zH8rI(0BnC}8{@+)zXyNI=9*gm*!oX{sDG!}*l&G-BUrk7czCeX5T{5gt8WKqrc$KS zJ<)sPQR~r!6Mu)v&7*3w8FP`!*Wb;-E+6+RYS56=?ud@3`WRRyBZfEs8C7KMk0o>3 zBmz9b3oZb44H2zGQSWq3#C@aV8YDjEU3H<)YPoL$(&knvQT=;7hZ8Ee;OW_Q`DSI=UY71xJh5~L73 zEDS$=NPF54Gc_|45S(;4BoLC>1+E`v<0A-U)pqTK#2?{~)9DG)6Lx*J)olSbEBNp70WS;T?@1@4vQEL>RrIc|epB3V^pWsp29A}D;R+JtlvD zf0w)2?U^QHBFn}wi6~0Dj`{-CQXS>@?~UHkamfIf2j&C#o9uYTGV3Ke%f{6obMxOL zZ5x@$B|6vXn)T4>2W1ZC-G1te4VPLS>#|$Sb$83CJKq--nR~e5^EeUwvC@gqOBZJT z)>FSiljFfc2e4r3jK>~=yu_X_ z&xk%gWFKrPeBXm8Sw!^Ax}vCi{xgB%1|u%ep2_xT*n8LBZ*}&MOpfH&`bvZ}#eWHe zb$U+_JthY^@g1uB^>-crA0@{U+yo2mzCg&OWw}7QMn;L}ud8Zhy*4-5t(4lJ3`B1n z6526~=U1E7@1Yen?w|fD75%GW+EPn*kS_|;tp48YB`=XpJlU7D@YkjODc(_iTadqo zNTK^gDJW=%DgzRO+o$I?hMV^WK7gHnTsB+k@l}awL+PIKKCaK72a&VEllwx)?r z|3I8kk{(8;6!Ec=Dq4mh9)p|W7kS3{%YRX?*Ar+fThS-GRf#i%ZA=?;R zVPkWlerx3eRuq;N+&e5=Ux!Hq42Q>{H@hrFW-*ya2mpmOB&_k&+s1B9GX{uLw_gj z85${)DR0oBNwn)}j?eL(d65E+l*D)(N>)HGq=?;t9!(HGfdnP3L`XmUnzrK}oU{Az z^Y^W_nZuJ~-HMeeUqsi*X{b{eR4hEQ)Uozw;ubD!#=t_&Udjir^Oa;$^55B@GEgv>#s! z-X4qIHOe={=tWC)@|NOX z>81sQ@__9eCNFR4>2ITHJ9a|-iVEI4m@@pFY_CjI|EfTfbeZ@viuwdPdFca0q>!~u zX;;f8az$PBS?)adm!u3~1u;6aL7IJ)Y$r_2%&%d(z1m(sT@1sN9Y@lzS;+jj6!aBS zq)5cO^~u+nl)CF{F497YcT0S<-w(zC{bo zN>+9ji#hu_b#>Ffa!mQTNq)%NXQSaBzp!XyF~WN)*MzFT8I6N2&DF<&s2e7;Hb}8n zbI@jgT%;t$MFtjk!5qNgrO9{s9Ad%iqhn*j3U+Y&Q(98PS#L+z{_MxI%OtVKY|_TE z!1g{!c9D{Gz-3{g+VJk}%iBBIgChoK2ji4E9?}4%MK?W%cK_wR@@XX6au)(HZiA9j z@E=1WXoyAMZ?6)3+51A$B_AkA(z8a3{eOiM%-~oe#wuc;zhrzB_zZC!zAUpDmlZ`8 zp_EN+jXB@0FyDOzVtT{Y6}gZ1Owt7rUFFa~agn9GjcTn9!}S7;>OWn+GcR^D+j~K- zmky4XpB>Iv{r$_|p`zFwwY%^RXR1aM>)Tg4j4Vybs;E$n=gR8qsUQSn4{rGoH3o;f zpAZ(SvLf`jZ_eJ~iN)DHb{L4A7k;k`0?|H1Dhvre>p%4^OXu~W5i-_S4`4kc{6mtm zvR5!|eNN9Y&QeBeVg}ObNBGjCVrXNT%&yGu5bn6_Z*c4M5;Tg%c~f(8%`grvEdB$4 zfJ`!UvLJ4B>bsnn7>gzWJs`A_!#xjtvi8TtwLK%blkQ?uh7jkDcjqAZQyAgJHh9!$8| zW$6i5vC28pd#m;PCF{l>dlhs|wos<)HQ6eYq#ZCh+?E?kA0>z%C)jNuL+dmGLB~f2v5+3o<9b?uW1`*9{LBmr`l^ z=LLMAYEW1;Hkge%o1B*0jXlp^7T3ALJM3nuHIYsf2q6l9q{Ds{y8oh?>~I*qrpYSA zNq&p7PMDb-!UEq6FCMGa%?-i2Iuw^y)pWR7-4)t;}R!T5$mXKceVmO)dHlBKV;mALssZ6~^jgeGbC z0UqCi-SwsWkuiT;QH_R_o7leLoGIt_^@X&-D3;1sx-Bl3y?0$1a%Hb~>y19*e>XKZ z$pVPg(UD`UHdiX9CJ4HSa-=^EuXXt*We|?IX_JE6wJwT3a_gYr$fFe<0z%a?m&4&wQce7_N#i?A1%m0yq=Z4+$kgz2!UK>XFhob+|4ETv4UxRVpr zApIb~BBG=z1*vRwOvrmO(fEZaS^gz+Rn%>#qWvzGGPy5v$i5TFz~1t|bH!#<6NJ@= z2?d0&NClWze^Hm>C>qpco#0?HTrAI9>|X=a{BL4 zbpe$b^lPg(trFPbn_q&X;&eVC#%!1eiYss>wjEtv>wRq3J6`kuE3-O=Vh<$^R;iS1 z#U@$)si+{ZiPU*z1d&VO_VWwI9`{^@h=pI1YT}S!R8~2T&P~WF2w}2N%IWTH1ZvdT zlUy%elR!@c@8db#?&5!vua<3e<|~*lRwcbnSzNF_w^*pOWMi8C#xd^wG3KjXLr3u< z)l97^G66xT^uexC*BNcX$Rd|zlM`~bXLz?iw{512?8Oy}=aM;L?Xj$K(Rc5Wp*ewR zwe#u`0n~bKp1cC7Q5M}-MKTVB->baA4TN@hIQXKrc=CF^RU?9peF{{0VHJLFdNq8F zx5E>D5%j?p9>Wq+LJsQt!lslh8+X5Z9D&=AZCXRZPJvsJ`R6QhDjz=xy=fLC#m+3q z^ntfCuDIHZA;+LOnFJ&M?qw>p2hHP#+)wYnIE-S|+Mgo9*uN)GyS^c&7uQnB?ij7M zyL^x|^!I1x6d$@sU6DSP7)s+A9ufB+F!5@-N3-2CV9Zwx!BfaBcN|n2NaIfu)_|8` zasA*L?pA)y>UIsu`Ff>R?);_WUqAvlI1V=JwzaZ~hjh<2dJ6%q5gRK? z5Yhjk@PK2Pas8gK-A0@ogH5hza?XlJ#{GYZzR|J5(c({?9$FZHG0jRTHZ?HgGzryM?*RS!U(f0%A6bWtQ~qOC zeosE%B<6+aQ6OeR-C1AuP4mi58Ch-Eun9wOXKsFWV8VFr_}EuDI)Duc9486Psv>Ma!g%$)t3z`JLC^?ZvoXRipu2V@X7%rk>GT zOKwCUuY4A=n34kAOetp5#cP=0sobZ8oL^FceDqBNNf0r}kJJaQ!`{iE%zPGOi!okE zOsuEY4*rgt7(2L3XVsljE>Tv>4qvE44Olz`Yi|h2F-_$Ixj*jjB>+HkdeNG%<>8#a zzuuuhJUhedbI(~|mxhsaT3@14!R~?iy5g}5aAls=G2f12>>4B%4*}ICXPTVHftJUX zF|=!tLrd=3*AeEBYr!dbLm+5ED#*_n@oH{9QO#+PrnpuQecsVrh&Y~8_f>v;N8xVN z&8GGZg#UInZ6znESZ{H%zxFB-3rJ-pfj-OI@7(BbQ~KLjO2j#PvcX_aJXnXiB z^w14lcfMQT11oM-3LQO7)St}8H`^^Ba?ojxP+s@P20KBCm|M~iy z?b=XN4TC~c;4}$vAVUGjP!o1mVp&oY=2d2+kMsNW6}QA$T*bQQ&id?GZQ2250Zlx` z1C0L18T#3|nt-@!xo}8@=T-q3KmidoksJd2lso}oquW}WNv)VgXPm7WJMf+ck zf9EYi*NKGwf%`j^K^l7b_JAkgVC~?(Ex}z6O0H|+LTn8GSXiS9Wfd=y40A@sDL6xR ziI1hGf_EMk%19OZVH^_xD6$jjF|{R(40ozy)^urqx3T8x^`$S7|Het)vH`dE>Tskz ztHvp|k?JaH?5wJ(EopusC($-d1V7__C?L~}HaM)Zk}}$gD%Mk)3;5K60ayka1!w_A z+e2(-#Xd1jQ(fpg#InN0(C52yvWUaoUL?#zlfcMB-0-ehz3Z&tKcX>ldgoPv#EIflrJ@Gx#ZoAWHG~9^64Hr= zdX=B$*B7b~-})tug{6L~JDmTT&2P+m3A5vYEa4Pig6}wi{0oyF^Ut6uPh?@=3f9CU zJd@|KhKw|Z8s`t4s#J*Gbv17okHV=PI3G$FW5^}E?6x6ZqIAl^C>yEz;INwNkh7kAAUvJ}}b6kKl&N*u|MxD_HR=>J))h z5uHo3P~gY0)K4|0RP~zJlJlDXK@iOz+H zVp-xR^{_S=#Thf5w*%UkCGWb&80YE+16K&5yu>cb@6Ga;=iN?A`M>KWaQcN@t~I+~Z$9 ziyMaH*fi{lEINldf|wxeSaCrb0U&MP~0<|+{P&SHxo zmfXfUS0C_#N3#Y$6&V?&8dT#(Fn&9#rC3;2<3^Z-I8@H5rQu|taW{SUi`Hgz4T#%2 ztR9&qlvF3iZDChhnvnL$oS`|69rKGy;5bo)qavV?% zNa0m4SfBVGkcvM$yDs(==rDUi;da7t;+m#2uGxcrPk}c*`_=B_qhg>=E14#F`*}T2-ERF$rh6I377Y0 z*n(@>!qa0b+rrXP)Tt+@az9G#1;&KY_S#Rz$8-Mf$4q`)!U+()RtY51E1X{zTgg6-rklap=>OnUe8DhsZH4;yK^UgLg z0h&geQI#oBR%uz_*1nHs4RwAA390paegN+%SAl z2ikYZ1(AWm>9X1YPj5j&`E(yTxWk%<3@0%pWg=M|;GFK!6Dv&`uVFga)Y_ zZ05HVRF4XvD|}{XJfF2gRlQA@|8f%$xr15>xU>84rR$^8dW4FtLk zY1LO~?D2DC?0^u9(yv0{SmTzUcU7P!8>r&a#6J1%L<@3A1SA{&n9A)N8bLZLl`l-; z1+d*6PMSTopMW^o;3`aS-VW^o%df{5f@_Vc?FI{EF`uSeAFqgcZk2TswRuZvMxYFNc+R1)+Q*^ zQ;+=})$jLUFjXr*e+sWH9J+Vw@wr`Ietx3PdWU}96}*(R3j(rb%yUC?Bf9dx=d9{X z6!6kVumu*jxZHEE2s8Euj;87Pl+KFCh%5QmS7=_hTZ|itjMrBVkfX96dN=kL9w0X% zo;d-OcP1E-$k5G0SN%o*y zXXIlPNK`nrvUjBP3(bK8BPS;Z?N+~UJsn@B$yn;mQS#58gARwOI_o(LG&fpM)5Q

Oh7{y`iCDyY)(6 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%0Alsihvt(0YtbR8#@wUD}_o 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

S^062Bl(BZW=#rBtpZ4F8iMZmBz=_E~=M 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

w@s5-~DOR-_1mi z9-gfJ+!)codM5<(s+aAghy+L;8hr$^sEg7zDW4AaM}k+a>c9Cw%*u!oq->J&Q&A;< zJy@?gieQ1)5Fd>CI3R+1)|p8)p0YPU;!@%xb=6C(sd_Q&=$L>{vB=5r2nhQAYC zg$8YL_qC|^O6NIaj|D;YXXd|arT&wm_U&i+j^VBev`NI03hUnr3t4sD8)TdXP7dcA zGc55nDTXP86}Wn`tC^K_b(jX@NjReEVVD{)(L!J!{<`q_L*1E8?L6rY^B*ZzLxgX5=mlN!HVOh;G4*c9zauIW-d@2U^qeX z9+knC$}yifMrmrSv|g%?n&xm{YWUq)?h4>C$g*2Jhbc1s)=yu$Epq=O^P|4o>C|c( zHNM68Wg9Nt3&OoF)f)r|2tvv}moWE-eU%NKtiQPXGqy({0gu-`7ivTW7ntffnYku59;&^#OsQ)efdsu zQ~H@kq_q{G1J-a`dK`wfeZk}q6$cY znm>928%kUzz4C975&TMSI!>A4V~)o3NX{0x$WdJ+-abE%SW7&Rg_yZtdWVy{LCmN< zxuC;zCOW~m8zmy`o)>y#X(PKsh~6smve1|>(v(V{z0sHh;;J5X zue4fTN}R+4Qjhi0yJ@F>rcdi0)GahX5#Hc>i4`2NAsrRvUiUT zAVM46BVdd}Rz_!N&4l!Gf*M zUjdQ5mvX2C7P$(cOzck(#bJPQh3b2#LqcdPVILIn_WH@ed80A z)h_#`o!p=Gr$L1BNOan|icf&In! zyW9kB{ehiR3$l3EJRFjp-QiY82nZw;I?pgai%?TpJL#n9 zU#zcsK~9d8P8&oJ$UEiX?D_S5?TgIr2E%jf!-6^M2`iy-Y!h81yHpDBS8k#H*)HA> z^?PXSd9rVaMKduwQ1Y}B^Bnkhn;*;}9?IpCp99ID^i_uTbc@HUD9-(XyCp8|X9H^T z`2_L=D0I(BDYDX3U0;-s$u1i zTyaC*D8(&t$f1MEJ13NTOv*{FdigR#)=_#YI1Kec<1HST;#W`id!O^)GDuLNZ$%_ngXk)sE+=K=BYS{D}=MlO^^YHe@yipFih z$~$z#t7G%%ny`}bc_dcfY*k+*i}f1fIMsoDGz4nbA-M@21RJi(oA^zTcm{baB|C^1 z&pwA^ z)gT2A%f4aHL|}8{p2tWN}mk`PWq~uVMDSK@C5m-VP3c*fw2g+VTmBnhl5&} zsAhit#oxdEIt}^68w^Usk@-^G8i*6+)_$wZXyEyaA%Q-an2`==esm=nN+3&T`747w z5ve%8wKmqOdf?=Q;C(WO+0mkqauX^zm>!-zWlIR=&WD7=Ycc%ZAdjpP(gb1wiwf0c zz{1AJy!p@&t9siXn4F=FxUDrO8dE+k2sMugEg%aEx%&^y zrEh{gm{_yn^tjf3>$%_Ra`k@rMlcJgT)%G*_1P--yO%N36m&+Pr`bcpLdS&LQCetd z;fzW|d5+T55Ue=rYB``34Tlm%z6HHseZ^)94nzk^G5$yAOLV4;VGyF^u05|ZxDTe$ zyI=rDfJnkH5r_hpGK;L4xUewU*Z!XONUZrqZW*d2S9KMG-(vdYQkfa&d=hCwY0S^x;NDeJIwirg`5AT5Kirq?#b~lww={Kdd z4ts^1Mr$1*!M?K1)2l#j@OSvQbk)h=fwau?!LsRe*bdR8;v2A{=lGmhjjy=kp!OJB z|8D-%`R@WTER%L^^b{hG|GnhL-1jd~AP#~od4wvG`U@4VAD_kiUl-()yl|s}Y2!yM z*80O59gk7W(QT)!JGR_hKJTg6aIf)0BFxg+WQ6DgdFF{7W-h#A=Y@H}e@dMHapfmY zRi+|RHL{6vgp5m1CXHmJrW_^1CY8(I9Z+NJRi8$Ew`>QL&(bNA~P#QO^3E!10s~l zz~>$4Jg)#=LlAcg0R{>aQx64hX}KO&J%mu&IONCkUJhpJ01`GFuIZz5c2Q|(^l1Qr z<3TMZ_P~gxXckBVy$SxXg9h^dNzyRuW&#q84X;q>>0O%7?Fx6;TwmgloJAMB^YtG_ ziJas$H8##U*W%bi`O2%rr5!ZB!<#~w&=#&JpTBq`8p{>&h8sINjwr)QJKBs?@xVJf zKaMs-Fo1v~TS&n~bW{eU@DdCp#x}srKKM@hDXkamJCv67~Los?H#cyujy~`N;w<5OxB<-|~u_LW}q7vx3EnVt5?mh<~ zh(uJ4Z$>6w1=B~Nv>|%Z>~cS3p-sfF4EG((;a*$|jZdA4~OlyMk=$WJ;qFY8#B|uT~A{e>lEL}1{Rh|hD zNh8%*FF+{3BjRQq67?2eh~4dH;(#GV25nL@me$Ua^|y&#gSnOZx@QBO*FNP$g^Zc0 zga@htDn(O{#=tMLn&||9JMFT5s;O@aAjlR4UnJsGlba~4!x^Wv3IlUXul@smfX&!2;!2|>Fe<&+*?w@l@~g9eWyde@7Q+^cuu^LhWE4S`IJ`isvw zuNrOD7EF*;>bbwq**e{rCZ8wLJbiPl?&=6R7rNT%bs7;eKbJsc^xk9%XSuar>%F|$ z>6Bj&)@a-cHqQHO?41yu#vk{iRMD}KxEITzF^qx_NxeaCnEUu06En)y97~az@3OjaNnotq&2g*O%gq$fhxo)Y5spK-2Ytq(avFe8`vc+XJU&b5xu&a zO9q5j2!q+9{eOn$PrPr=H*Ul`gQ_%JJ`15Xn`*qy==Bpl<;DKe9qH^~VnTn3tm?_5>*?oCeL`$l3cP5N%8dVrK5G zzeVLn3R!;o{>CQRV}JmiHA-Z`uc5sRE$!g z%WJbb362H(6juXQ5PrMGz%!qTKU4V^BqbVE4E8xZvl=S@dm@3XI)f z(vZBfILXE-W=pV^q`PF%+}@XGoAjtl?wRkZG~U6Yq1Y-4>w-rhJXZjLY2zI?sJXS7bamR`EwPzlMi%BW-hj8$7X>cya~M!+l|+TM8;-F@TOs za@_+tfQ*ZKH#l%Y(b-VW%4kMV3hO#UqY0Z9cIPRtc92Iur_TQ=n-3j93*d#y zlXB>`<-9$Y@*p?Kun(=y#_KVYmY#NraZNX5w_tdK%wX=-X3`#b<4^oz9+*D0PJLts zrY5KyE7&XemenRbJv|k!ax7SM@hw^5WuT)*H>!o6p3~efToFhC*z~uU7;)03HNA6L z>fkPi3BqR)IiRF)lTAUVmDkXOWS_Ko=hp)3DL)gd`ZdYikh%Ey-X>Uxj`mUn9p99O zYep83Xo7={stzdeA?_BK=5d}*A;bkwIydDfYR%r*tq2ADhJU%44zjX&Nmnd zZnK#!Z=Vxv)0V3GfLINBwtX7whoXs>;vZQc8j6XT@g)k*@;s*rl%btE%)EL_($>|5 z7A;4-fMkA8Me&**`n4q}Ruem*{)3z2X4$xO2MN>KIM<)Bz!|698 zZfD(`ej!C=EupmR#G&!THr%PbVpkRFMM*LKQyfG09E6}KZg;3-|FLgx2$^rJ#N%nK zuJ10Bb)6lRGz8%P;gF4(52rr3xBE56sd%3yiar$f5((&#=Mnrm}Sq$)yuPW>Y*9ZF7Wt5vn607c*@+{aoiuPGTYCy9u$?uZh+#WhUafzm_*yYffqJFmf*}h z{!^6XnO!*Zzjli-oKYv`a7?Q5K%kI%-t>4`03c^n*12QnB<`BN-2&y!U&MIIi=p(5 z1jhVUyEsJFi8TNm6F99uA6-wR@uM_vx{=ktOEo`-7raznF~s*@C@dl%4D0|Iofn7uYDJ7H}1sA9#7~w>Q<$% zsbDOhJca(Q$^Zmb)-A^uFtpakDLbJ{xY}Hk!z&Ca0>M)}x}}(FkDCB{sdFvErerMp zIuiLfafg@Y?Zc!|uG08Y{sSoC=Z5CFOF~M&^tdVsEK0@!95C+~GqHGIU|C5uuzaU7 z0?=Q7OEU!5pM51TJg$Jj#*u1Yhj?V!l|9W7WSlN5HEZ4Y&{S@CP z6KBZF+KjPKRf~;zD0MJi_6AxSx`P)qff-9p3#qX`H==}?j7!@DmsjWYZ+?s1Q+Uo& zM7Rmrmy&7+%h7obEkm)MIzF7Wz^v$L9u)j(*2>L*zd|CVsx0}MfNof>06>=gNEJD1 z3W7a3qXN)sE@{fh=_3HG#U%#}<%U0gdO}{H2KM_zMl9&B7j&q!xZc411DNq2b}7Gw z@Nq*!c=wcAuBOVI+TA#fr~5#_=zaeNM53V+T>j4)Y-@Dpkr4JAf}W=^Bz{+7HnsP# z*k2XJuG0Vq?tTYe7#~2W{ko#>`eNqkrc9odrvi&64W|nf5Etlbebf1I<~($AxxMOf zL$>99t85Kj85E?Lhv>lZ>r47{pV|8GfZ=-+xdM>0eKcX?g@8|BniaEnF3TiVDCQ@L z(HMv@E)UqSu>f_;-5P29aMg=8YAgv*V1+I*4e8v zhxUt}>^#Q~UceZ@;?Eb#s5CYoSb^p{yPGTRUw#vjzK^(ZiRP1EVQ99w@$ zAW~7l@Vpb;$wqxaeZXKT5Jw8P?~RF_QvD0`BRleobUB=6^xVcRc;j1Q`BQx~N)5Xs zo@aQw_Cy~#t_%;|Nk=Hw$s1KQ5K7a%@mjwl?7%q<>K`g4g1WCK%<&KarNW0*ykjtI2C40)k+kK)$hj+VKjUQamOFNYyw}Za5F+ ztNeq_@u~Q{ISZjQrgW;x)q`pRDsIwGU=BRF{TRHe>2R8Ehjld5;8&<=wXnkSo(ZcO zTBY(Hs-o)ebFP8ntH`Cz0N#`c78FOFi6HF7V(4T5MlfSZg%@#}-YdDa+Ds=wihW&} zyGHprQ;zhIH!uW%dvyiQ{VBMbHb9~6n7@u)lH0MSpK(O1`vBRU|1Eh!qv9nD7CjdF zV?`|c0gZn3tIp%d-O4qx?^UFMS_FMw!k-^MG#i{*&&h^@ime*DSxdM~3!_D*9O zZMVLrJ&km2lD9T)Uf*vEw3<3wlO%TW?knvLGouU+Y~7t*n2Sb2@@1>WolT-+c1x~Y zf<@1osu#F>G~r~lsunhc|IG9Xr>R6tD}NgTpPf-O@&;OtB=J`WKyU-=PeD$6vc_$Q z?{`OM0!Yz(zaY>7A2vla|6!fdqB2*Z+>7Y2d|kFi$HYKdT+2T?i-1M7M-zIuCk#UU zQBc71odLNg8WrlHmF8;sykjbhz`1MrE2lxIy7;hMkX%*B8QVPo9bb={Tmek1V74TJ ziFb&NY6oekc7dYuOdNE=s&|ARM=D1R%sWM>>-cFtqx;{h@ZTA;(M*(o&h(Eo%xM@0 z_a*Q=C`W}^t7*D>$E4uHZ?(T;m@D0l%QqT;@<&MY=x67g-PRjVuK~S8-Z3U}9o7GO zoZ$DEHI@yfh{V2=06^rzd-7L0&74z?&uyH*9sjo)A*#1auO^4ZLx8BlHsrhGUcxY% zj&{s4qZOazO2akj(6D!^dX4SR|4k$R9~bwWm3rkh!`ogWi7i^$KK^Y)y;#z0fVOlq zU=x~C%>+WBcG}GS#`i|r#0q=#oY4O_Mg}9G42f+IU~||*$$rhefZWpmXOs8nmlhXy zx#gs=?fu>BKa@(TC8U51$h5HshwcNk(sIdD`;fl{u$g9!ofoPP0B4U41G~+CPk+A!gkF!#{{2R5cdoMVH9oGJ{o2Ehh3r*T~pzg zf_Lfo=ig(!qbMJ5-R2#t7^Z>n>K?Y1(cQ|iAyc0iMK>{{XlvK%nXTkhCx{|>(1QQtrpZ=td8&-YukF>gui565bP z&_DyLxxwO3j4^`i@<>1*ro{BB52WHd@4zC_rX6_(2&SypO$*Wobe7Maanx@**4Vya zQi1|B(l!;pDk$~ug7s}>JxE6cJ=1` zzU}JSVCiRTXUZ0Y8rClh8vcJY=r`-J{LS3u@Y~X_~TG~f)c{n zcqxZtSR|?KSFCC*YUbmx+%$2A&4SFTLWcNjNa|{0<5G~&^Lmg_@7XcBG-djjOdPk8a0wQfGHS`)f2uPP+ z0)!q~XbBMAn?sNVkj07_0Z@%SupSOwbCne{crBnt>f1sms5T_EQS`m9z zH2*zQz75DUDS5B75}Dt$ygC!oVkpODB@`-8xR?#yOtnyZc?qNZu-=S@A=Mmu(Pm8M zcJghtc9Z~?8z{RB5?ArNI%PhwI!H-mi)p=fkLUBAP1Ra-KWu1EqWNnJVdqI{{hEn$ z9f_v20skIxytB8DA6xjHia2_;Y!JJC%6;dZOa_o0>NyDQA@e?e{<$zQ{dM1wXPiM% z7zbNaD>Fn{hJCTzyxEqD+E1Ydm1a^Iwtpd3mM-s17ytC;U+I7UURj~G|MLBj$gY*P z28-JB>8rV+iKRNRBKD09iE6_6*Wzc>H#naz&D=g8^)qE(Egj5@9<{5=kyVgMI*A2B zcek(C@*_Td()-Z~RHFP7eM8!BeKXm8ya;!41n6TW`=cfnF-Z3lQ3@900H^yVo$&4? zfh;{=gzgUx@2J;r$hOw3rB17L*nB-;Yy->^r0ai>lqM||b3aNfxt}`tywxvB9~Fci za+_POLuGXi53>Ym<|>^^%ud9w5P4n$;_~CwgQ&*c=}r&`5ej6w8J;|Czoyp1Go%j; zd-;2Ei*ATtloe;u`jA%eX}umzq2NUzAb(W&#DEa z;do-(bnE;0ySG%=ShJ2;)Fc%Y9v6)dQd@lW)e(!{uf$A7@zb{kW?|QJ+N=OO4)1h} zhNs-Sh4P~W5VA(o$+*k%&mvM}ZzvoBcCAr+3iat-&D*gr4)br;`E-NM(VJXdC}aFz z&SAeyhc(Lr_^rI7D%VugRhk2__u|LrO*9(V(HoKV{^^EhvCQ;$^~vD>>g4!8Kgr69 zOt%JuTlH1->bg!L*yMklh=>|pZ9p2!V4KccrX+KtZ~fZ!|6Q~6->(*7>E`vpgIYT# z-uV_+;L5sPA1xyEY>n+DO?g=~h@Uuo^Fm8}lyN6qp=Sy0gn>Gy~;BSRK z6uR{&nD0o!X{)harQ6us=T-`{^%hpet;|%*@;4>J-&Q{UB0|8-fHBLdO`oNar#cf0 zfgosoY^#*rAYZ(~2}ayfG2?nXpbJu?P1EIuAsJdI;@-=vWT2^roNbQZr&=Dwv)CvmTJI*vQCSN42 ziJRBN-rK(y^w1iE3At@r8}D)a3bq!2Y>ccNB;9if>}gcM=4J!raysRHZ0p;H zdmrwzf92-ow>wNJ@wH0NYE$?#yWdp?$pGZU0*a5Z1iD*vSg6fNXGNzosfc(^VBFEi z8$fpmw}8N-OTk^^9a|sZ)~OhCSAq z_oA7}VGKPwRG8vR98HMpgL_&Dig`bW1`g;;=ktg{lMwp>9XcX4$nLSs>b z`e9TFP%8ZAbp5avSPFJS8yMSnQ4F_pvnAKSMepa!M7?r%WMI0Io$QZ^%!6|IGx7r? z@fq&oi;Q_s79ZO>*AsOH@x%2^n^j|e<3*M|Kg#@N%KVQV{K!E1lNX6+nth^y`4DvP z_ELaqM~Axd&Ze&2?pMG2rrM#N&&8`v;u`l5y!jl@pSLPzY(>oG=h!$D|LWA25=z*# z2X5kp(N?4>uuu4+t{5I*%TW$~2K{{6fA8ShGfLOa!p?kIy>51kr< zMp4jY@zbllmL9pXDOU9is71O)jVSx8FPFbCR$I%f^Lr=@a1AUvo|^Qc zPxZu(T_@+UZb0|LqS#{>%WU3=I4ffK@w-f6m8c0ZBVfA;aEJ;y+(S+i_sq*ioNM<# zcOG(3J=Ke;(c$PhHcyRDHyOGJnscFIdH%g*^_K?!ATYKb3G{pEv4Qu2@iq^?MG7bA ziG3@qZHEDL8IbjgmCUSZmRsqG)zMlE5DEnop~wL6eA4FmR5cjWrF;r^|t~pU2jvs0K9oiHmNsf8|o2mE$J^r9tZF;)uPZ z8un4MQ2Ls(wqgQMj4QvH$8IlM@Rd8#9i`tmvi2Pqhycu;zoMhC`l@M((Pg8K%Lu)?VYd49R;3+Xj4uKl6GG_nn#vUx4{^Q0js7=M=-`Pn4 z2ypBHUl{`FfQK^=|c2Jra+ARJQdLFw%|KM;AKe(?Vt+aPn4{!&8z zSQuC-_+d3Ran~36wEkr`GX&_ZE}N|QrV}GW6ZMF$?L*k1QT=;Mp!wWv%jz2}F^eRi zd^1hSj#})-lg4XCH*aJeysI*i;4pap{g(CX*FnbDEA4BncbnIpA;=-BhPyyNdenE8 z9~>6kz$97k0JzQ}6<@@9r+%@O zwLhB^;|q=#x0BDOc22*Cd(Fxg%iIF!gx4j&G#6<_Ssf*3_Q$K@Q?(!9DL@DeuepAW zf@EA2h8a`-wyX#;ltn22iHH8 zN4R^@alZT^qyMC*Fs-zfu0tw03)+6`yV=j-S<@~CDqzi3sHA<(TLt!SyN@Yfu&_P8 z8Tt}f%pB&0p7d$&?Cre$=t@)e4f-6xzoPqI-hNdT&d4j<(CYuA;`oIR$$M@MSZmj+ zFY%zcurR_sza5~+XI3Z&2PtRl?`&IqdqCHg6Cu#v0^W?e;T9|T1<@u%6aH5EmBRs$ zyILLXC$(Z>zN|>n;KlQeYsV*FjxT6qkc!wbDd+E2h%E*MN#G|l8qpq7yDz#Sc+ zmnW-dWrf3px7|HVPnWscKM5$rU}$N=NvWRn>K>NtL9>;18zRaJ6Uz1S*-rnywb%uM z(kCdduvCrQQ0(Fs9k-6NTAK4~IxQ)zg!EE%YsmezyO~p4m?}R$2c4WZI{|#pcvtHF znL{b+q;b#2`lrqhJ%9+N0A^|*SH8ipQG900Q_P?Kd+AK*1^0`eC*MCrn(mqP%e9;Z zF6`swFx%@{oxDhhNH*y=SKZly7j+K%Ww=h}hqfi5P!{Tk!}NK`z`Kd7v_M5iyI@(1 zz|yPz5Vm}GkcE-;PfibDS)vttoSsgVmG%1%f7J5zyuJ$}sIBfPvfuh8vC*l@k);O^ zodT$?ml3&H%&)d>Pg?O5a4-@ZLm~cX_EjMqizBqWuk%4`& za((L}=BMG~z{rh&?|qo!q*A~2w}Mv(MBd&uWfCwj%~^gW-DbldQ)o7QwJh# z{QT(mphJyrlg-TEkkf6WeQZnuDzW*ioP<9xvLJ2@wl=hE+Oh*e3BvZU%P!;=vV?`A z7Y!{_u~kE5oQ8lOovt#;x51~kK+XQo>bkhvq|gcLM!+MAG5ZAPiVtEUzFV8?7))Vy z+aV~kb8H9^q^m*}*2!uPD#;X^U=nDb2@WwbH+?*L>mr4$Z6?)&v^>TJwWrz5{whB6 z)GfqXm;lhuh_!Y&6rNxzUD{u|Ze(uUF<8|Df^Hb@myvg1tu44z@+H`MBZ2{WvV)A@ zJ6k7HaY4a9|ukCS*7kb%vJvslY|MRBMijMG#^(=RQ z83Mv-icWQ1BHT1<_GcX1(f+F7!IUvO`>auuS5vD{pHIP^!v$ZZ+c7xEq?4^0r!T6D zIyjY{T3Be!Wd=uQ+G{-~ND6zhZ#(Y29`%}9sEbDIxnV|cDj)uqmJe9FFIzj|Mw;^z z2i>*WX{9VK;cpFCIJD3bVK+l~UP_h7)OgTI3&Xdd1R;|?C=jGSyJHeEFu8a^5T{7k zfSpPf3&@8;3wusI)?L^BXr}4ZvWl>48Kl6gq}sZ>MJ#+V7YW16L|#G!8Mh}%ufkNq z-yK$2=z6@YpiIIg;eM9kpDQ2jt4~eMv_~~2Ecrk;xlsHrFeQo1mgwZ<3w38Wa~!S* z9NvPRN5-|_4G|Y$C>N3np?{BDi|ew)GZI3`L*Nb=afuuo$&|rs3S5HQ!3qPeE1PU< zChv1~eaO5$KW5T(ZQIYR+4o8YFS^cmlWj0lHB6HW(6l?8hI?8*PncFF;sTg|(HU+Z;9Cc63=>?mDbv6p&m>U>XdmmD!299DKZ1CGqhWHeL63RC%umz6FL11e`>-gRHc zZDoUX&SR23RIJ?Z^oBu7Dn^tvCD<_;xHGx9m=iKJ9c5(lg`#5Q0Uf2LCOc%%<@7Gj z0f~{@FU2g2MfF%nb4{?z9-@avFFj3y*PX{y>3kpDh>sEE;FdVdOuh$1HmfC1w z0-!^N%M6H(7#7wM?UZ3JekOjE4?ZpEowQxYrM^`LK(D(G4%ocC3verALraCwgPwgK z@*uJsM!=FO1HW2J8Mh^C8p501_;nGOXHDz#d+MJD0@XGMq^r4|Qz(3L{{t95E9Lx~ ztD5~qbZ*h){XH^2X;znbS>kI;UfEGuSt0uln3~wQ)Qn`=GSI`Yi zN=iz;D@zfW@JVnKda`u+_q22!0?I#@$;l?5Zm?tL7P@+&m&pwQt+!F4BmqvcD zJgZKyvXQp-@Zg=Dovni$1Kt1;9m6%a2*lEK5*AGqzeKJ^vQkpMxN-!~)qq!5M*)jm z7H7tbjEqe?y1Tv18a(Nh#smVHLx-*W>@QO6r)OiX;;zG1&~B6S8g3`QuNGUH_KY5| zHS|hBm$<;8u;UA48rW(XNt+PTLRP8sT)G`E*vwGW543kBxwPjdG}9!$e8~%5xgJuq zkr2q-DX>xKE?JwYDTWg)HW9~w@*rFVCQHrQ(vv`oxQ`(? zbHGXXE|Sh9cM6RxEF?3U&AJP-{Ak8huXWQ;l$F{5Q-1pU`@Q)5Sv=X2-UI|Nq?Vb{ zwo}iK@`%i{K~Ga*R||Esx+hMU@T$vw`3`_ldQ@8#>TG`zjpnWMUsW!(sHf_zic+I4 zZ)yrn1}+Iy_Dkk|8wWpLzHMzy5wLcCW}nWC+s^6c2M1866b%+z7E>FgVoQlpzPYYE>eanh>wDOXMhoIJfCe$}!Ki8n=tfzRwn z!Uz(YEKk~NA=SDb;}ugHZMAB>hpjFhWypK1-O$NWj4K(~QI0Y!HS6e#;d0&C;%qu1 zX!>Hpbe9NW`f0m)aj*KS6F*JieQr}ev$J#H4V-HOW*P89(RP7X3dX#qx7D{^2$#d5 zK&*a2u(d6OcI-<^!0Pu6#Xr~~;x;mTvT04I*b*FaCspEFccg@8oi9d86$e(0+&e?S z;spt9g6$emRBoE{-nA3{bD(s5T;#hLOzXCsbynWga5*$6Ir){iwn!b6c)QdbfOjQ# zsXYO0Tb>#BkoVc;!%x}FXn49?8k=NY^Xz}X1+T7;I>)SrOs}1GZUKk-So!cr;aD>* zfHwMl`!kRq%1A%US|?6uEFc&^M<*wniu-LILxGpb`vC|pBtDhIgV+I)wIO42PRLu&X>_kSaMp*EYB&3n;^4`}8^nNEorS>@&24CW!(iXzRMmz-$P4ot7hESwLG3 z!tTg>;Qg9B0axHyFw`O8dBDmYl&C6qEju5JB;O+~AjxMqb@EVIS8~4{rk!EvwL?Ru zth$MD`@x$JtNEOcX2@1x!^4Pf0H2Xk5KHjRfYVo1FPc<|uWi90wD5X(F!yyet(u6U zuz#F>V*4(X_u790fPa5w!J79;H-M+X6H!$aDk=culk_$+HE%n3L+|1{X1br{gJ951 zy6L!=Cp;OoLF9jUOcy)Hun7{RBKD3@nsVbZ?U zcU!=JUllTAii~dDs}~fF7wia5zNw6#QpXWn_OMHbVDh2c;vyxVswW-b%DNkoM0?=Q zd&u!vase{5w)xme-gTdXrPg)pj|Z-r8AeCm5ry%!?0X1yc6Zb0%y}>i1g=;$ZN`=L zpao!z3z$J4R7T6`&{AvgD@P~C*S3E2=hL|*ED@nQJCP+NjN99e%guX!V8jwHi5h`Y zBFQ4~MKF+oq2aIlc@b!5=VB5x>li$tiRV{|))4gL9#T1bMp)4$ zaS>XXNeqx7FrF6In~ejdX;BIIA6d=8$n;Gp{=GIsNSEPalxS8fzFWpoSFT)ozI9Zu zb@bqNsb%AbQe^kJGqiW{leWM%^suT$2()g^06^ZC1@OecEXeZeYWgFksTqr&K%hE# zQu##Dapx#9@JmSV4JVe-FedCm09QPJ3xlR+(bV)@=fQ#0`E4)9x%g_%K-xa*Q&U(T zCtJpY@r;%cwU8O!k*>R6!B}C5uTZA-K}4j3;h>c*o7>*J24)2<(A~H|ywY@hS9dx1 zEPkof#zEfG_2OROULk4>Hs;d3HN#^~3j_$(g~;xc6Q&wj&?2F}vd;UMr$)BMb$?ih zf*){a8v)aiLU{w%cI&a+0GVS^Y-!nZj0PN;O4mqk4*+7DH$KX~q8NQbLs8z)6kCfp zX7t@y(kO?+!=7!pPDarJr{Pa)Fv;{XKqfSD$(fpQfjC%;ngw)iQI{A6jusV!0sXlW z%fDj;JAQDfhI3_~-O$N~(gPT#ZM7d==YFa9t!9LB0 z=6)T_Paht?k$Q;4`YvN8mu5-Ja;PEbST?YTP9@fXOV+)-tT+kRNsdhS!B&2i(4@)>$L~)Bw1~qWFlq^Do&D9ASvaMPOi+b%7q;+5FqOXcV{;{r`kA#rc-+oz2Z`0lmMtE1 zB~8`2)6*KH?hpDj^OI)<2_ZNznNYhJ)3U)3MCaMIi}-YjZ0czq!F_$V{!9R}nF8<#Iq)+HP%IpEi|((< zA;jkIP+%r|xh$Um|1TthX=#a8@R4BQ18`S;C4kMhp8=Y7&FLnkF^H{_qttYp<%mJH z1s4OQg-)gh((g8)<#n2I!WNJdv9GG9R9UcH`>%>#Y?3-|i^G)SAX-oJ&b1T8^TnWx zFd|{7$A%dkIdEL`V8eBL15;ric4al=IAAXjyWHQht?5Qq!7k^ot40M0xcH-$T#-cP z9QD?!y)hboN#PR}0ly6)2ucp3LRz*Yyd_K^51^fy+GYq}0YgxB2QcEnm5BFJCT}0g z`yE^~Q}($si$?oyw4GrtTJB%;CG<|(DDJS@fE-LT78N|CiC1rf3ld>;4&=+mG5IaOFM#h3pKY=>sV25%jEM$7G zdn8wbl0vgWmXacnTCLTyA@0yl*UHQ3YW7KZ?egMM9)p~&$@2nzzm;JtaZt(PL4JRl zv&+)d2&xW^Gmn&9oT)C7_ru?W9H9ieP7eVm)(qe!%l0sa!t)574-AzXHK5rfRaaHT zt|nL6cUC)~jas0rfZOg&9|Kw0Ik5ZiH}X=lU!$46MV01hZ<9HtVb z*6hiOoePvZXTWS|DezL!a>^LRIT+H-(Z!hL60a0DyH8o|W>C{kZM zjQ<<5duHZg)1Jy6Abj&lvfJ*wjw{uFyGZt0Yh{dygS5->O>Q zVcA(^%3xw2AT2-tE_yVngs61@< zCh)x3>~yoc>&FV=&G6tU>;{$V_Af~Q#Zzk}2y_DVXwHyh@(Jz!O-sVf4Mn)Xhvpcz ze<*{|8;{7dvj0B<;otDX!F%;+uWH|uAB9=n3FJ;)R2I9>5+3h0=YymtObn46#X*1+ znjpqi`@$JBGE9MaRaxE107+Qc9F}CKvka^W78HOHyr$Ame(X!1^cvEoX^G&*nPJ?5 zXH%l(HBBG51B&@gIx-gaMU-*F>S>-op5CW0LNrJ3I_l7+jpa=>;p!0yi&G=H@~*Er0i(0uO4&pv?rF$vmIL4KEY$|k8>%FD z0?I}Zb_vvUM8(7}A0*eZIp8}xyAXd}gNcg5yFkS{Tb3|9a1)4}p_D@^hkQEODlykJ z;+Hz3*o}-Sfsh?=X7Tpmt{n9Ux4=LK-V9zNQ{%QJb&dVbq;uc8*9D;XhG#Rs7t`&s zyM`e9sjfSHwsV2o)PTr!hcK%|0;^6-o`%9s`Qf9}Ah zC;nx|-FNW4)&5jWj^(LL(^0!7fbTEM*4C6aRK#{fFfZXSD0v9(-nsc-9e-uk0tF

ZB>u@rOj;zPyviUUGvnagqn zY^7yUT@cSwYE}=3PpJ^qId$9C5#fqd@j+FW1@JFm7krk1ta#qhj>v<^M3cbKm)&uZ zkpM+o!8Di&P9PJqVcgk-6@A=>hHCh5$zo?zKzT(&Z1T9RGOjhyw`Ud^?t?(_%e$}q zML!|AV>l`>~cz_MQB{HB?sM4@#i06rcA$Ju2E zF!eAC_UP*dQh~lg1T1k%BreE=9Z^>NE~uYak~F37X#cp&OuWoVF)K5ZU-@vm5*KGl zrh^_E!Wf5C$>OqdwJgIZ(wRW33UKwaNQ`&Wu2&ggwBoItb|}snxd<3>5;MvGS~}&j zXst@X#Ucrtz9?Q~WNA{I*_*pXNI+-wb-_U4Uox^4D9f~(^Vn1I-G)yBRv|!cSJ9gb zuPS`)`(A+OJavv{#$}4uA?SVxEky`MK&n+}e?6l2jqn}q@I4W!?lf6gB;a1?p(`3N4j7IX!J6%l*RNY& zICDMjd*x`>`~gWYh4GS(vI!{prkc>PLY?mkth2L9Mi!<7X{FI^N!QTnOJFwWn0)G7bXdOEbV(z&o!c3DDS>OXL0yN z15@gQxS*?yp%&4k98)z<6{6nODg*)}G7W+VjByt5+ouE$3Of%ITmbeKqvrRXWq^)| z_np46DS9T+OIX%_5X4WCnFI;b%**EtISW;mFQFQ+6Ubly(Q`kVBynyUaHB_S23xD&>x# zBUn~Je6nTRrhcL8+HXI|mj2N4a+n98S*^j<>hkV5x^w#d-1Y-y-@7eji#c*D5in;O zqs@!SLS@5p;^%L@c6zq z28$6SeH3i#8>Qq^hESIU?u6i)hR#ZS37DP{7ngNyD}dZt4%5T|3_s1_SH(DKJ6+UF z^_>k7Kpq?yH5slq3nI9B97tjS*ycbg1t85E8UW~!aNUz~o*E$ZH1_w^nNgiljU(El)c|m@ z&!j$;|BO!kSCW81j_^Oy1#dbMJo(<(gbKm4g+2N8{6Gicf`|>%bR8c(c)jz+HC{H) z4VJ2nBbp5#e^1TpZ9D~mpWEm3e$Rf-)}f@xVTjbr`O@d7pR-j`z_Gkw_I7opdox&- z%KWylf%D0fu<-Kzz9+P-lJRBw3C=X-mMe_Hd96HvGZIL8mU+07899YDGR}I_JXcfA zc+KhFh3PcW9~@m4vg7pT5t}+QHrJgrP)vvz;sg2QA(aT@!K zuw`=CIkhgjTTjc2M%Vf8av*U1EkIpNAeqA<&I`3^GgJ3wS@w()upQc8RbWg#3H%*7 z>fU2te*D%`DvPEM6ahHfikyx-$F#)B58u%u{9y8&2ata1nrRCGe3I)XyY2OKNY

  • r1NF zgQhf&0%r)qeQ&P;Y0COxh4GjIH9O)o_Q%tgPG07zh*k5Q#`dkuO^!gMVMQr=xc>Vt z_x0}_kRl{Pw<|9xKZ;|dzbOH&ZgiZf($Y+J;r?;^|RozE3nx9tgm`fgqU$xorLV8tDKHt`l zYyM6DM#UGosrZaHG!L!5eKWTnc`~T;g_mOf%_qkOFY{8nYSTBzUv4?+N7;>fjW-<= zIV4jUy+srynp^VPR?$DI*ODCSkx_!bntHarL=4L&C&@#%xoc`<-Io4n;zBGBDG9mcJs;H8xC0Q) zrBn-ftK(S{cb7Iv%1Q>2dsA3ocl95u(Yn-{;cE6(i-n7gLdk=a?v17WkyWyy^EX@w zA5&gy@!F;Pp;rYKa(hPKhz%tT+I<;gb3tM^yqvs~RxIUc(sd+v#tMhte3G4vgllk7 z;nQ~SJY2isxhZ{X_Oncyti%sF{%6?Gsw`_QFkq4oo8GA#Q#J^!sH>H>0O;yG=jjGP z*9P8!o`jIueUN7q)>^#bJC06EK7rGzivi>j@S{mDDUr_?Juhr@-wQ08^o)2m=wX75 zlq`8sFSxm$(vrfuqVVp^?vTGz;oME4JviAeNN}uL@cO-H z;d(kh>ptRbW9Z}f-4p&2)c32|uw_zzI^^m zQGRRam8lv5e#NyXIVl2P_Xt1zHE>TZ=!{9CQCYV)mf+tsIx z&37Xc^p0H4W(N4G1Y=F_QtQ@eUJJ-zxcgwz*P*fN?N!hG!YHF*uV|%&N8NJU#m0O! zu}PQ6W8t^AM5hHzFn+a%E>SNG)T`^Cw6Fc0TFsVt5-gcM&B>GM{NzO!_h>iO10KCE z;T2c#Qx_ziuCTJO*y?}3mT_}hn}MUzf~fpx`ttQ|fw4`FJ9q8?xdMXjehF+7be@en z5A$T@WZf{^3Xnx+7(W8NN4mScz5UwHuXheU7OuVNyMbi4(LxQ^4d5+ft46|r;%m2Z z*5|#c8#3Js){2=19;#)!7N80NeUhf^XGAKa7{#gbADQ%ttOaRL2`_0jil9z#`Bhum z+$PMhydUE)5iuc8`w-q~?lmMK0~X;z=my8zW%*swh@WTvre50=Th|;FLYs;qBFPtU zce77P6^Q$byw}WqY;#K?*?&*miRSx)KHebuw_j|0VD!@iS?Z5QfATWz zlD*axpCq0+ihHE&tdKgiACJO+TVR>)ZzSdTSS3@FPcYR*FB(UU(>+DmHu~!~m9=A{f689%(JTH`@t3^=vPV%TtquXMd ze)}}~&UIf=doY=wTDoacUCjUMm_63z-Pc#5=9BMRWVk92>lO)8OapFDAFrBEKKm_# zRp^u=&At3!k;djBb~~cCY(dxa@Ip+@s!g({*29D@DF4@t#@iagf(^ypKjE`;7e>wZ zWJYEeR1^=#&ARW`a33|DrN0I;OMux*NfAlYYIt9nP<8UDdF^PGkwOc)dM`9+ zJlR&Ofhn@}hNqE|FP+t=Z6AQjwOW(dx%Lg+Gng6QUp6V>xuG_r+-oTp^BT1NzLFVh z{OzQ_I#=HgC5}((eN>uoA6q#L^(Uqh`Y%;Gn4A9O`z2$Y&}CN-9bGpU*8Y3u@|TEd zD+7;vajeb1$KFV_0h8h}Hss@;^O_wW4B>DHH5C;x903H@ue2I&YBij*B6o4WhX~rv zf%Dvwebpk-NfQ552%{Gw;;_UWGkV%Gmwt=-1f*n^7^am3z9i4)%R! z1f<04VQE~|l>xn?iO{&h%he~z-L@7#gwRI!LCNz?{&RRKp57{B{rqPs?z4jb2!@|m z2P06*@}JF1V%U${h7mo+6Kq$WV=xj?CNhc(V^{Lu^W_iMbEwefU0V7nJMriAr*gs# zk=X%EG*Tju%5t7n=-X6SRBn^Q9&2vETkm*h&K)&$#=~C+wtG{Q^K58lblzHnhGEq{@Cbze1dLr2m`*ic*^umQ5^T% zj}r+4_PwtVpF$c_^v3HqK9@Us&gdp&{E{zn67rj z4W8#Qn})Am;}nqvBPQw`cy+SmR&FVJG4<(E_;p;Mn3`#dcV+{S5)YH0V(j>NdRY73 zW)K3z+xACR?Sw^0;ja}{zN4vyA?%~4yvi2b;uEI~Dg9;>Z8~5u>}-vB|>ROuMII7ZyflUPL2E35zy@ z(ag67&ujR>V3$6J^zKfJx!wHv20X$Sk*6}0!dK_F_@HjWa z@>hDJ+{}1h&rd;{V)xI6#ig7(R+Ggl0@^Zj4gYcPq^r^mMO-P({Mx|tIm-;_KU{USb0qmCj&+_o5_Rn#Vx^rDn zt+u36;p>;2PJAJW*3;Z-F(n=&&H)m7QE z%7b4BlgL-00LZE_+#Z++c3a)@I0%DqN5+;)Q6)@Up#+4=WAF{c?gxy zzPla$@#;XCqsRu0eaVo30v5}f#48W>&&mjK{xF*MudwpqX9O?w|CXWszf@X|(UPjc za$BSA-EC#YuSsX(l)FE=V0ERxq! z9zW`jG6w%}NaV4%>u*u6aMMm|f``S~zYL&Z<4ug|bjtB<&Uti@li{#UpV2W9&esvb zk*d)DbjklxZ8JPf)X%5JHaGOM?%12OYUank$5ejDloVxKeK+8 z7)|#uzRU;qzN|EB>bu#+g;_oXEidOY$a~_h?KYoTP&@$OpzURN1oqS4I$bsR1fKq4 zUuPD~N7-8LLurwghi?Q%c{1J16aKo(TyHtGIWVyHpzQ8Tq6|{Xu;Qs2EuWEjMZ%`GcUzsiDW21VL22xt)V5qi9^8fTATg6I{2D;uSG5Pf|LkUg=& zQArzM?B?E49I6?b1HKlrSBPffYUU%3aj6leTwNl@pxE=)!BeYcVQ zx*DCl-_~OwlTH!^n8j1Ntbip2$DX^8K|3fD6uXC)YQhrt+JV@*l;Oc=C(5sWantl` zmA_2<>c@;48A`ZTx-!2iU83ny044CW}{NUN4y&%t$FDEc%LRl z%cGl?k#bEWdOl@eI-mwwF%t;M6=uf=?%)3!w$|UN`n%Qf;P^`q|7fYQWVHw(TwqF5geTSW!@1dwAK@S& zBP}-BeDewAvXaZYMCxP|P~swf_wLeYzUH~D*j0pbF$u^g%F@zTjP%fj_&&9|C*$0f zu=*R+@)3RQ{sdI}eUw`?OT+sTmpw-tJH@V)OZ|!mNu2K2ba(J=JIf_cFE1a~ap`Ytb^ED>@E@+63h^G?*Nq6sFkaPE& zowe^r^qH!f;Ijv-J~;jTFI*Y!=vD+AZQFFI+#jlESFy|Dv(-#gP} z1AfRtSPOtCO_7=+aND*r<6hm9WQtf9oX}x|rEO45tcu1bsv`X&w4*?B65iiytb%2= zVK&PnnFsK(`U^PDBN{kuX? zjIA88JjM;@2HLW-aE!qln#d|Q#=V%<6nDEJw=m&sGtIb=y8ad~va(9`zp!p6?3T|u zE?rUkhWwWPDq$Zhq#dE5hfJkW1i&?!@Bu& zBB^e|bz704ck#F6j@@>nZTUiS_^)t*I>8Bg~F zda8Q(OQ9#SPSOO?UH8H=YaUngxd@?(1UcU?HQpXSDPjc=1?1cI42{R(jY4@b(>xt< zuWgJw3laiB8^rglus$g6=E~~j|K${}JR}`A-<8%56KmtFd>0jS$IOb8VVe2m$E7@u zFNMcSMp-p8j>Au{d{;26ZTTy6PCi75upz_7n!hq~0_T0;>#cvyzLxceY{I)=NuJyr zO($<3Fjfa@?5QkZFMs;EQ>S^M@$EbCuY|~lNh^l3!IA^eK{ilAK$kG@O4FwwYtD6i zYpBA&>~zjS@yUiKmh?Ejssj6#?yVh!URrYYMB1yC@fxmTG9F=^z4vy0A$CgnY&B$# z8yYVU0x=VscH|Bx+6GQ^BmXy7$dE`nCOVVoN#$BMs=8LjyM_Co-sZ9^dT(!2)H%}s z*7ShbT>aZ9q^uQ^FAd6UFREuDjjKJlgIqIc(hYcf7;{xZXS9fFP||n z8ps@eLa7w`HTTKSw*!*ymSwrTk$wvG9?z2YChZ)&?O1snHl9q!ktMItJNzCJ4#f7Q z4Jio#V_>z0l}+fIrZH8JVzs_eceQq?i|*%&8xkN=Gb;OlE? zLiU5(kS$Wt1^EU0OgYJ=#7LjAFzpiGd)A^2@3T_ktx;DGB^&K&0?^d~Zy8;G?N(t& zbAoldgk^okdeDPb2t0)Gz+J?A-~L&;Hu#C}@n_f7SPD5$&h6&)fxOt#=F9}=xz~k_ z_4OJC#a&KCD}!$byISbFJL8NTO0!a2nROgfQ+g$@c;(BQ;u|q?tv)Amg()4)_lAa) zA$+o`E*)t9CX#cST|n*j%@kt1)eA_moD$RCkb7m|P1M_2JeeP%`| zhpjYL2!t&DT)&{wac1?2!y%wjbN5krWiw0ZY-!nNb1_hBvDNPbnIa@qvL7r|`(>7} z{hZG4>L@;QsxozG%=kn+0Pd8feKM69+X@6)^_0H)!l83fI9+dN{KNl%v$r!-k>Gml z_}dtwdOKl{@sLK;N7&}$UD@t-i|r}Zn3rpb)2B`BOOQ`QtGZQMzneL{&^+UX`87d% zryInsfRBc<*RzxFN^&utV-Dw=CF>s*wol4#2DoCP^A4%he(eZtF$c1${StULue!_+ zv8-3`5dIfb`8V`vv9`S=$h@1=-=FlD#@@?9CQ2xDn?c<8y7~v_3mvQfx&;df95}8j z-X$^c{&9s!XDZUWBUD_+#%FVWm+7-u#=H12eAnzzZ=;1=rfYS2J3N zh5{U5(Ph@15gEL=lQ^Zf`jf;7G(I&2!wwES^ch057f9mEB)cv^GM@?h`F z#>3_CpgHF?urp$0DippPuf)dan%#}K=L50cz2|&P*ybEW_g@W=t26hZIe4E}-S>80 z)4NF5B5Q8Z10~Q%394Lu<1&?%I(~RdC^XUVK0f^jh&8?(&qKO?W(Z+9UYM6g7nEE z4YV#KzFo3$f@dEpfCRSNdW2al%SkonA!$F_%~{i(y|PBQO6k8_&L;Ny3zR3Z^2GViXU!Qp#l z+o_o<6+>$l-hhobh+d7ioOZ|hRU<>`glfa#>-DBxJ14sT;rVT+aHGnf`z*A)I3fS# zoJA_>?paS8gP=O>ucobYTqk}uARFV$o>pD@#nhCXHruKi0q6AyvAxH#O)J-R%Wrzb zC_N`-7t!72vpCq8RB73^DSbT`wU_> zCeK!XmGt#LJYYcE-R-Q%{BW{dOCzzitCpR-_l0QWEMrse&?Qpa&5!y5215%GCH8qqjoaq8deG&i@}_XZ;ps_pW^qMIS|! z8oC9fd+6>`=?)ocfB|6$i2;ic7`nT=W+-Wq7`l51m5u>n$RT{$d;hYJ<2}Cn`4iT0 zueI)VUe|ekE>$ib=jxdb0kLU6AbeRSG`=^L*1pK*^(S}N+PTurpCisxH1Y24g8V0b zJipiM``p633ON6}Vbnacf9B`NU(cY1g(d`Kp7}S?-W{+1@B#jNHB|NL-zlH8#or#? zz{$ntuT7?lQ^-77_kF55SmstSG}9)>khs73QBiM|15t5dEg6Cd=4viF{tH$B^e3#0 z7KGnzOUT`|?%w}gWb4FlXIGPXK*3>pX0XKiq)^zqS5iU4yU>bX|934tJoYvOhI2e) zp8k6sT=VxLsOF|VwQG+53OdZ`Ht&9hPGL;2pS^H6ML#pc0(k@wj#W4~-%TLFwHXt|38IZPnijYZgli(LB5JdspY z7MFo$&6Ca?0X-bhPQD*1wM}(>RaF$4+!p-$W;fv39<){KQcxy{X9~ zC&m&f_egyDpF`AHf6!el9hm9**$FB*XH}~Rd8Kr8%?p0D?!h-71b_PRYgwcJTfXJp$BQIWuUWcot2;HdOZ;#3jU7u8qeUSQU6nZ1=-;COQ>Kz(? zndoh?a-PxNjqwOjz4 z*gQ6%(L2U?PapE}PWe!Ib(ISFpx5*x*Ik+4=FPX8dd^DIR>RTmI=XrM?k3CVV z87K2i4WsUfv!@&OMk^6VqrGbLGz&q3SNgV8k&~0{uz9CpH_?`6?L@{Io677YC0T0o zoPLb!`<)-nX+)EmGvN_J z!BX>tngv&C-A<#&NcP5GcmrGR?0NzdN(XZ5W)(h<1B!9de`J8o>LYWyn!c-2t065H zHHUZ5F`!HK8N0#Q_4JrG8%pHZ)Ts>qkVj|S$q@Xok@jf6{fyL*N`}}7;}_PzYH4=bCqSQ%H6^J#un3X05dDV)*IHqws zR?{{D^35Hz+cz9{wua@EF)X5)PYOYgjft|a@&0ik)is{Itk6qDY5<#s~eQ%oT&bg912a_jvo z(gi~!y2QhCcq)1z3oT?G{5DCWb@^4jPoHokB=+f0nbj~UD7N-yizs%}+Fb9UA*Wz* zBXWg#ZB%e49a;Pt_lmF}N*`O`kb60Ky5UJXW8_SpMMGcE82*4dtuN>LG(Y$FCG2W` z%bTTp3^oiNNJuC;+LkmLOCKafU}`QS1uf+T+49Xmc;l^LKEFDW)XMe1X`;UI9RzJ* zYsrORskR!G?z75cH>JAI{l2%e!YQ%tpk19&J^6-%M-o}WWytZKtx>BkK2fpZgWs_^ z?4cjE6VVkvKvLk*c4L(-Hu^(bGcGL*pGv`So9&t)%F}G8W#Eos+hjp=s;}9)=lJw` z3-Wn(3tqCsG2UO1@4NMuZY8M0J)(-#eHoy;)w5fK^|WZNJwHIaqg@}vn<}xJ%-oaF ziVC1if0)}Nr0SxoPn0(Zw(lj<=Kf(WtEQMv^!QIG`?V>kA-V|EZn>^~Rq;}e|KUPWw zooh7;w!`#>(gQapb-!zNbaHAmf= zR+N4WRm(_JPsvg7q!KTVtl8?oHZbo(xomFandfSJ=}UhJ3colLM*J){^4bre1Y7x* z6FkZ0&G^5_Ad%C)h5KNZhc}IO@E2!CW#q$TGprK0gOqQLo&TV8&EbYf-Tioh=e)zSDk6w)|NgznIz`4i(~X_ZPBFniG-Gr~B!pTVcOIUG9kR+w#xA{E>hFxKLlN{aIFGMP?&K}T(pURVI>x!@NC!zXB zOgR4<_$iE5Lli#p$kQTyJoV`dbiw&qU^hdqo42b*!%?734FNXrxn8Wq&)nveKW6K1 zyr`I&&UTu)yzpU+6Coe0w)AH((&+0@WSrJu*}}R?LcX7BqT`U~O&SNesri5|zu(Li zsTUaoeW9tNL@%ku{qc}>ZhG{Aj!tbfOho6 ze1crzUIHim?K<5^d@z#sw0<^Gn05L(v0>u-Msw-Tgn(d62g_u!dT(5`^~w5j&NGcU zjS-XY{mHI53+)84atXUZpo{scrDzUQzPVdGsJr3cnYRA@0J*Pr?eDqF798K8dshU0 z&^+a=vTV0b-BUbV_|3WlZe0<9E5H#iUc_(>-iYjN(YC<;Xc%j^$R#uF3o9RRTcQgI zE#uG9O>G9TGQ6w&pyM*^g9hHtzR`v>EOQ3jNNC~+<)DcyFW6Q8P?jB2Dp^2-C4m{XsV3U}2BnTT-h&`+3#yX<+H@$=+ zT!s6Em1i-McPSzbp$NR203GXWKRg~@o!WZoJ5wbou{Qf7G6eUWzvz>Lf+L@<|ND{l zT;cD+-?MsD-Ce};&qQl&vrN*DkEs!V6uC-?ODhN(+~gx&^y9MYhs?Q*V()#W()3m+ zD-{Ug*K_5ad8qridCHITjry)xzD>jjsQK;NXQv}{iBn8P5&lHpx=ZVM#>a!H)B!$~ zMlPa;{|LPbZvjjaKW5ZkEN|f{A;8nEu-Xiad}sJme;02nUbQF$2-yYDhFsTs+l4NL zN~YrR{?_qu;X|g2&1%OMMHvlUSFqdH|79p6++Nq^Zq8+W6$Tdj#0)>#8WOfPX3LS0=YhgY=BXt^|q%#S|YmS_m%ROWa2bUOjO?3M_O>@3xgdB|T&Y z7LdcfKBMh4L)_eWBoT>ysdV-Jd9p6P4upOlx0RwzyevO*wiwS$D;Hu+AU`fEB_Hd> zol&z9M>2j+8e!UG#Fn9;WCFz3fAGW{2UD|&cnF2rfPV#-wI&_aIoy)oX zbc30N53a z#kJ{kPx_tTj~`_%X?efv%a6yUu-FJ+FC{-X#gFxGF>&Fd6@J-FZ(O5T+X*lr@1lc( zi3fjua5i8MD{AOv%I$sQc;%XG;=qP5yR6pP(5#?m<#wygi7B#&@+1qkjr1NdUe*1{ zd3ecS5J~&#a^J1zHhaPL8-jGnF*#ahaOe12EU$c@ROnBH7i|QSCt=) zn%#j~yTB4<|G|(!pR;F|>yu7r^it$3s{IWu=-3k_op7|~;%<%>yqwsyGImkVzIz;Q zY_PfR@BbWZfMkmM+>gl=e-Z6H)w1Zon$$%1FS6D^*#f=veV}gc&-c&gxvM+)H^o>B z6OzK829FKa(Gf)|mEGp&>&u=E9RhE+0{%k`ZJ6RTUKY z>>gbo@J*VEmT5_uc1r(--8K^9m?ENT-kJj4>!64jbbkosQ|uo~nDyRw56ebe0aW)* zeW1^0u=~j@26jB*=l)H6l@JBpR3jEO;I}dgVWF zuS9@h1v==JlxP6K5L%S~Jk4jwxk*Y#JH@MTb=nkh%u6}0vV*~Ui4-p~QcT)EGE#@T z%B`83%MbwtS^W(=&;vEq_=P&`HhpoY<|>jC)#_i_a$S#eZQrUEKCRrWemc(WG!$cO9qPZ!$J{pyg1 zsW&gooFv=YqH`Cj-?y8#+~tYC?Q_1d^+l*;xF;kJ=D4R|3qc~Rc$UyjSyNJ5uf2?k zSZabsOUnzTyl8W*Q;EUJry!_5S%9y$T2_#je{B`>79W$5UrhW-SG+z-fxPY=k<0g| zZ|rqF-AC(sf64|}98bR8%{Ym|WPa--pp6x_kYi=0j47|^`YTbvKAUJtR$>{W{qp6PxHk?9P6<3cp7T8=CC3FEI$KZks1R#qK`?0N zx-uSG>aK(6Ws=VGtGSM{7SRzhC+9Ece~DIAsBE#$b9;DK;r`FnO~proC$N~5(d_PC z7m^s+zp1%flQyrN`~Es{ zM0SO_Z#1H1R&tu9@rq$xkga>cn;6+H+_@}Oxo6sw5`bG_HOoI zfSqRJQXVH;sInYY5ZCuF7BSCgOTFz=-JIwjqLRTNQkxl~WMYcBz)iTb_R+Gw_~@ZC z>5it&7g=*2bD$$?Fnl84kI>myC!o)SJc|eUV9k?3c`SxX;DN5tx=nP&4bl2?%uu}d zG)E}?Zor^=I>lF3#?~pOPy0UrePVY@;2rNWBox61+8_*{77!yTkSbT@+)+zmRnJ_e z_R6gmBg^U+;m=O`i)+;5t7ncdfPjZwXMH#Vd|hCMx}=f}yvqz6qYO)8c#y*%?7B+f zk~gXqd9rYChaZ%VRLq7bfe!XO(}zOIk(RlB-ODXLn(v>S8wh32A+fRRS4 z#jm+n!<6}H7-?EI+6LVu$rStuMb4Eb8YJJbm1s;dZlDLo=x^DDuV<2KA(brQs27O)b}B* z8_x%WVvaTj^Py{}W|GN>Q~QmTVpPqPDUR2=HP1ePB!gyk{{29fLVIXv4h^6JEy*zI zHS{=hrA=xy72dNyn?fmDGeW3PQx%oDzN-76!Z__uE52<{^L!*pI4tHl%U$&J&%V~y zyUSh4l`|Y_x6=>I0Tl<c-w*9 zM@vN4CZFy*jbWsXNhI1IgZp7o1s}7F^vsYX0V5(C-#0U31d3%z0Rp7)GgWpzYk6Hk zN=V&QuU8rDMP#jA1_E=5YUqLvTSVS#a+vH~hQ42J9q5gjM6Jh#Ea)bQi&1qGGyJ1Z z&}T)K&*~;wB?v0YsCA@kcFfMF+Dw-`qTJ`oS>?lxU87aH@`JgL2~}WJk35$64id%P z#6Wumy+JjRf&tj|j^&(H%|6?f1PzZxIDGQawR@_);mfKsi3(JtvS_|^Z~%pU<%nt# z21F9IKm}^T1r%>M82~V&UszrZTFJyMhHmyD3+IQqWV?;nkGuFed9NBEH4>YC+cP&G z%S85xGw2XxrxD@uQ8LNN9PbZ`p#No8S~*?^;Ys#v|E*?m{Fke=`7V2}FCBbL?(>7@ zK+1#2GJLx{7{^#>#G0G%J3P06L~mjo-2R2rv-YkSR5}Zo$HiDwGQpw_a_{Bpa^oXX z=!ZhtbWhbHxTYJcPWx}42Jvuphu^X&muc4EGVuL!()W0#QoZ*}pSX!ODI^&fdzJ^C zzK>-QmTUgDdV+;%Z&S+=4w*2`uT@imKxo!ljxeCH?V@c zE1x?EZU#(``Iz6DzSJZ^TpmmQm2=(>^i zpEYufDQf7go%SUSP)6My+U5L*rU^3P@AF?m^s#oggl-Ei@i&DB$?dpyihA3+$jN+v zE7=6U0T*f)3D3P3tdcXEFEIEQ>V4AB+KWYJG!4zSGZ)@fLgN-3)9>7`(2~(#gLh0z z?AkMeR6O79P)6L76xF=fy#O=Of%ja& z{%%&B#)2_&P1K-E+MR|}12hJ2pUG?s??dXb13Jqpn_$X{zF^wq>Er2k>wg6LW7@K# ztZJ2XoB8uaA%=pO?>RgWkmDaD6v4E@w!1@ZgLVz~Y1wbh8bm4G6p|6?6fDadJSxtFoCs_8v3fzO)NcY4{mV3!o7!NFjUaP zMkZb8x&7Txmdt*CGR#g`D3eHIni)Xa{=nez->0e!`&vg5?q7_5ZJ{{+^<(>3a&VJ>RCr~!fI)fl)chT2~&juaTHj0GMRwtTT6LVx&!#OfT3T)?^uQ-ICQ<)tL!VcX5yje!pE_E}q zS|vp}!2`8VJFuzD{ERMjnB-|Amzfq45?$aoXBUpIXD8X$&A&Gme6Do#EB{iSzKbTYgXLEko@ro$13MA2TQG55h8kE|@U|CJ@sO&;_6zf2I zAf3;5Ju0N^#jGx z;O0s7YMWX8C$|eh)?-8mLH@y`->d_VFCCAJy{1eI(mP~iM{1J@!u9xE#4Hh@e)cL% z{4>~`AUJvwaa(Uzqys{ljQFrehA~|d-c3ZEw54)~?Vkc<8|UP^vu)8R-2KZ5XMM9{ zBUhu>V--|OR7~DFMsDCsL$h1TJcx;-qH!L)wV|U@=Z4gK#83dnbwgNQ(_+}M z>&4V}vXFRc&}D0jln8}0KUr#)n7-;xE%$x0#SxJ!oD*zlwXDk75}Hr9<&iOuL}IpW z#~KA#)$(1$AfGbabALVASgZwFQtd3A)`j_GHtJlhUrPixckUD@3kUV?UUdNAt7w<( zZ(B^M9V?CJcSwuAz9QsH!pbUz)@+f$-9>#V&5l<~-Z>s?VTdUjF(!|6(T+!kl~orT zmKz&)0_ZBzhx3=tRtrRB@_7~&$tUt*!ac+Zw?-BmK4Us>NNff(lW-g0!7ffr3V!;P zV9kweoZhmrk_F5ljuBYFh5U;SNl*SJi2suFxlmF-8tR-r60~n&e@n+IWIw!;@4@pa zW@vE2$-HMZb;NYPIOcqBENOY(z~K}Or+;edV%4U@2;Ey^49GbPN zH0yhGbL^{5Fq_7L} zXHbMn47v*6Qr~4xfS>p~$5- z1>aj}FZ}(k3$xl87#h01+#Z^TA7^hI%hs|9VWwzl3D8v_OobAkW0+;sC0)qc8f-Li&`k$x+NNbxu~5J2bhHqC1s=267uHcjuV^prm~Y z4q^Ug9&m)Y<<-H)*-~evMGWt)McH~mhRWA^XCiMS?8r2$Cq}9m_+6aL7&A`XqhR+iwG7ZkV2p9@K>%I;4pi~Xkj`GO9Nu($T%V7&5fSM zu}S)&8kt7yxPMtk)0BUcKW|&K+?hXU%jy7qgP1$Q@ye{C_I3~{rhXM8Yv?u@V;BSa zblU$MdWe~4n{%#NM=pH9HMIDK0*UF*H04|Xy zd5}DaV|Ehs^f&aN&MBv@<6VXqzSn@d1SYTS^yA26YV@sEG%uead$nK(f$(|6R%4B( zLCN*BtWplIXZ;glRuPK;2`h`1_V4;%KZJapG`W=wjWYymSRX%-W z9&lL_?n=9E^>Z%nRB1Jt_(@m$r+Z^qZ^EB&}N;D?r6-=F5qUib}Memd1R5wtBH(t)@ZM0cO{?1!q4Zi zJmd3<8`*0UcRA+1E)dcGl?*yAS9g%DcuHT%+f%-D%sXvpBj~zERHt&5g3fx| zNsjvc2>vPG(|daEYww~vFN|9_<#otmlX?!I&Z}LgVhx5#GQH(p`CE}x`%L0|f1r4W zT(RK&a@@?8_>{!K$Wce7sG~BdgMQC>Gu?fogw5qA)(-Q`&eLWC++7e)mQ}M<3{%dF z=p?b`V=(-*HDN{n|BT9mSKs_=Ev6-O<`?O2oA5E*HOcH{o|X( zkyYrvHW5F$y8!6Vm`oDi9T{_Vok`b!=wBJu0&op*OZ4TZB($`J@DI{my5xgrtq9=n z>Z+$)`W#@aJ9Zhr1BW~M9uw6Vf3Gs^%EoQ#$|(uA7N@Coq0hIj1}ep+ud)bKKyNk| z=p(zM?f2gU0={wV2 z5oo_`S$17jtK{SV6*5KXvALwuwsZ9GzqmB>C?q1zW4=D%2_oa$f3nU0_Y=FupT(Bz zOH|n#CigQE3ubw|=fmgQnn!lZ%FmmVNs@klY;hKme|)RB3*Kl>@EfD4(tMO;UPP|U zjxtjgEuPqO_E7l%;2boaHcFxjBhXZEmaG6BZGk+4-8KR?RB`?QL__!}{5N!ydWh9$ zVh)|^?PeS*Gkcbw0r0%6-bL0<8JaeMw%*E7Zz3@+Rz*1dFC720Sht((Ab14j~XIwHlF}X6hb-n(C zq5aghBo3uBGTwTP>&iy^oPw-O=bFTDoM#pweOQvI`KAYHmNe;^ZjPwRoDXR;%_G|a zGSeKne}*`67m*j=q!4-4pZdaLpS((FpH2r7a_02MiX*eVZK5{c);|UtzJ7Hg2N_S* zBWh?5tOUin>8?PD3zaD9z*7xL%e#4d6?^g&NOw6tM>$5cgK%&*U*eVHhvwwt>*;wY z*|Gdxt!l&;vw;T)2X&@VguVLu8;h{^+Jy4j9C}cKmqZm8(^TUZoPFBho8U5=Vh&o*{?-jXUv{9e<1sx+@GJa9& z^v5HTHG}OzKl2732gOXm3+njU+6V@H&TB; zq;`^uQW)OvvDh!-mwzG(;&bJqg_W}5g>QQf*pU32XGF&-D!cU~;(uHX=WEwl#T*d2DR*+>ur*v*aE7{psZI@V&cw`e`PHg>|>=Pz^ybaQ+EThvd^RlWfjeS(un&JI6JfF{_R>Tl3<>JmqA+s5cm)T-pUQx_tr`kuGiUq7h}I-i{0SRH1O zm9{>Eu^^z9nvAjOqtA((4W+>8VncmeAfFt2MU6jA7^9;s0@x6Pqx?LZ!fSU9ni2Q3 zh_yAA0ZP)iQwR@kGIedf+S0W$b?b}6f_{8#DtL8Yig;U<(UY#QYEDN<`&RjZ%m=VG zzkw2W`p!cAK}pF*yX#7xP(!S(wjQ_A>`c(E>7gJ?O8KwplKzrDV<3>UELb^rM2#!8H-=xC2PcUi3p)J{gX{eCpkd6X-y z<&=pFNVmOlww5Y(I=00;yva0{@|PJ1eQi0dBZSA4tESBVc}MYOMh-X_yA+EXVwYwo zgsZJRQKzixvq4b`EcK{i$|jt!w5RUo3I`9tSo0a)niDagOvpe9{aE%tu|MNaPlRQ1 z?mR@#@8PT*dgN@DnR#%g|9{Zxe-&lgG{eI^xRPLIyw9Hp6sAlJtLWq}av0u3&sHXO zXc=(D^*AZZ^V>ga{w*I^ZYV>b`Rexr3<44MNjp;F(8BhGBNy1{(>nmtS~H3nTMR

    @qrrt+KmB{I@|M2Sr)xtp}-fOoSPd5g~82)2SJE9C@9(B9rA*Op!`eKZmkp>fF7%1tOlrh&fI4-<(Zao)@^t7vW*wAHn zsbDC^Q6tu#ZPd^;O~kqS?Y^zq^jKye#@yIo)&p#5n@Y(WG6Qu%tHzuhOVKoivU_t? zbX5-8yyh>fjX)6-Y1Y!r9}ip&IqR*MnaJA{2g=9)0?ID-Or)nhJ&P>&hWvPOC=9hL zsyor7=d!u#(<Vo2yi!?9~&_3jEf zQUa%43Z;%JUwVRkd?>&{_xPZUyxv_jE{{demp!zC9TL-5;*e+XYq)ywCeumVsn|ag zFbDySfZOlF>f0sSU)1|^KVDR{hQV=^ja$FL@nIdb1NpTXx^y<1DR@H^pAM2p4G@$2 z*?r7XhfhyZEUCz}w_{ROmtxtWCr)LioRrm~V4L3J-xXN$RS&Rh|1nds#S z1r!r$24?0+R~4QmtNo|@z`G5kw(FfH7K6qleCCYbt{YM|V>$h4m0CyZ&;!^d zE>0G^`Idb!`kZo7pR$SNkv30aBUWGm1Byu7oY%*#awd(edT%3SrvD+sy4aqWJdvPI z{@8}Ktgj)>m}!LFy#GQ+7bbBJlb(I0}%E>*{YF@cW86zf>JAbnKIXNQJZ8 z?`H`@n2-?gCHCFybAa-rrs!mi_)i0x6m5Z)8n5VVgkR{z0rxi!F6_ zvqa)&`J8*@>GS*Kfj97&0z8BjawYn*@9^4P@Y-K+o}4HO_=uUaI4y1OBZy-AL;C*EN_n&fdF%k_U!^xq9=EayQT{gjjJwTlbWv8;(Mj$KMl^(1+)f8p&K)m8Vh@wR z|Hyoa54k%q<+8n@MTZo26~(RuV4W>Kw0!-8xywXy@0={5_Bn}Yx`L9FY$@{FHzfmu zoE5(DgjZ`P3cDQ25B!2A-r@gl_})^9|F#*WrFl=R#n{u}1dmB>%&>KhVZ4TKlcc$c z1^@T&UzuriViFR2dwY-LUk4PRt{pwSX7{~mPp?s`TGmcNZyPyA3C670qC*)7PWzvL zA1*(^77OY{_QX>-jg6kTu>-%HNFax0p5G5F4|BvA<;v{W%`EkYm?k=Ve^qy4wW3LC z|3S7L&)+3^eIOfR9$Ob6?vFwU+|Z)fW;T6c7CRqgXuCWS*6#?!$Ez5j1Q%wF#H)mh zk*RtR>ZXjU&awAnWA+qcDPW#x*IuY~da9SD(d#VHmf`Xc{b3~$29rTQSx8Dj+@RVt zpK1yu(zTfAT2_65#S*sOLo~Amp_ri|L9b>^boO)t;)SO%HKF!)Mn;zPho-rWOMmUf z`1Xr{{^m5w=r3rj7L#9zV_dIg8;gpv^H<4J1_g-p^$E%8QaIcF1wh0ED3Q1{N+<;h z=-!H?(9dJ-QiX@a#G5Igey()(W?@V<6r>^LHH|mqiJbE%!WlKB%5KBkdw80ySYMV> zO0#9Sc{;?f1~}@mq4k`jwB(TS2Y&uD@XLx9%12IkJb)yE#j=VUk(iIoE@zK?V0%30)GeWlu`R#D^z~}Rt67& zQIVcN8(Gk$QLgDy+<;PY_mb1=!1G_tpI9jQBX-CWLjlYMd_fSn-Xr`~4nn)B2+Abe zof{XC7Ri3283Zm85CSDw=gxzo*a+#Cd$Y#51>{_zNEwz)!W`5;w5g1XdYVDr&8PEW z_BkVYf~oAm8<$T&7r`uc**1Rhw}fJ6$MDqRq=WAL`}Z51o{q!tOxfZdZxQsf3EQK` zVLER)BKShAqFh;k>CYJ<;vyqwg>v!(>wDDA+eZa@R@`I((L7@yTCj>kcVR#P1jAg{ zM*24^?!SgDB?w~1kwa2dh~D=xPSIYh-A!yqnl{=a=t$VY%$_8l%_rU}H0ur7S54rzvnJLd;*AJsO^k zca9ueYcE5SZ2o+rEmif(${@*QZ()#9y9$>>Y#?C=xp){taapQ5s&1RDhg2I^sZ5j} zYMg>>aTMlyf{wU9l_|eP=8>i$JS)ON#lQ@p#)xEYbwrl@S(mueP7JT1ry=3y#u*hQ zyS1~EoSiKz#~5KoDf8KeuPmxR?L9t+mlPFTfm-(|1G6xi8;USnBqaksLET(jISJ7H z*^{E$a5yche6BHpz|=*0UhgkVFB4pX)3L-~;7Tn&>F7h2NLtRTO+}SilHrT`CTpXL zIDbgYVek(0_IMtf3PFU(0YC7;y3f3$?&7y~V$bJsn6*X5|o`EAH; z$Xy)x2~F%JRvkbVCAzO%w{SZA3q(bWJM$I23lj3MRke{)>@hQ4C8FBlYA9D?!pZrF zr=X~7%W&?C3*lJi;%?tTrz-0^yf({G5|9(Z3^U`yV^Aw>wIZE5eyr;1#C@w?=3*ZF z;n3MY*(8|UTK?v@+b}s|F&|NO@BfwUhZPMGD%skMEy7Lv945?uNxVqMS5#;U>hTm)Tp{q}y}&dJtHLNMLLhU}P9Q;FI< zwFEj0uzV1TDx*4UJ!bzlei}oRN<~&pQ8DR2J3G75imPDv{ubEsBTtwtTnuYCgVpSy z)NM}+TJ2X^>0H=Qju57?$(Y&G;K-v2)4mr}OV>5q6CzJETkQq9qmDOFHxAEjkvjV? z7s`0Vpfq1m@(-*=BX{R86l;RBBW0K0e&dShbp&VvCc*izZCfTH{#ZauNzTpW-Jy1B ziVz&h<tzuP@M?kgZ~&ugJn3>traPhRc+ykoGltLi-67^@54% zV_~U4!OxM2AHtOZ$|#E5f{Ye4ksVd;=Oz;x+)2$a4Fk;)rxp`+ZxgX{@~zo9zF)Tz z7{>1~ZO8d12oyQ|mSvQbmER=I@3jNi*>7>E`VCf8^}cBbIYL9@v$$%g2130Z?ZIfQ zPGnZw+gu(y=pyaW)(a=`HXrFvO6koOJ~V3!`;xRrLk}}fIWHKaLM0i{w)Lt>b5y6P zamkuVPB?gsEs#0j8+mxAm*-`u^ZOcDMu4$od^O~o<&qmI0|>?_|87`uQ$bP98&kGF zip8`!^1Q6}-6P?7qf@S)NVK_~T@G${YnC=~YwYGrMVr?`!|d3325UP1NYf^2M^?G& zefGzW7xa$kOy26xXOmD0%j3oLcoNfZ&b~=g18ynv-sr>e7K?5v&9TP{Z^bBxiqKQe zX@l#qpvg*%=ID+~RM`r^8^NXXcjtuU2hFsAwABX6EwO}tGO@WqI9vCIq$tVgmJ7ds zXQMG$Uma*nL9QIaM;6Ji_K3$R>Be-|h6t&*M<3sS&xJJ2@Qb=}Yh+?>+zO+Z(CR{!dDJyks2 z7X+q>zy4OgoG+o+lG34Dd$?&?Xmb!RFRL{6iE5~DwhkH3vLQ8G%u+<#NAK4`4V#(4 zoS91AYnPO^a@|8~E57v%QvAb(5@%3bXqnyt>_@}rjGwmUsChwYH@@cWn%wl{2N_PDX|s9Nr> zzMGkuwIoEuqfg_qzkuiO>)+l|LT)KACj=rP4F2}T!jNMg&mTG>A0HR~cpynl8_D0) zCu_F|v{yoD(z5m)@tT#A3|J{6R`@Q?26F;~00enh% zKR@`{;~-~~uzlcR^pBwX8N7))J&a-0YIZ1n%gWY#sS1nt1L)8yO-0~nMN3ZJ5UVwa zoC+RTH&{}1gwoS+|J|fRyIV&(h5a{nN=@H}!qC>Q5d_>0XzTTq8l7iUR}pfsfgGH) zjt<8L?%x7I2k5)w{B7(a+k}{NG@sP^i2dc7K zaI18Hdfx_gbv!uS$=&r%DUb6wi0aTYn`S8g#Xha#NxNd?`%iw0n&=4Q$|d(&erdtepZk3h49UD!;( z>P^z1*kf9^BLR?#nPF88t{or)QInySDzcE1v6NWI>}oyNmA<2kEeq z^SM z5fii0efLH=O>D>7nFA4|-TSe<$nkeaq!t_F1_HER@}s<80kc2-r{{d-lCjk0sO)%4 zqbPrg1Ah=rtQ?KqTM=^hm$ZqTG`;2oC=Ax!0|`77UfVrt`=Z34YJ2fBYT0K_blDF& zDs8>kTu)}Jx3saYF!-Hl`tK-avdVxl75^z+!J6VV#HfeOSa;mbzS`9NB!KJBc3A(P5OquHVfdel0MZ!A`q}Md_3vjf9o@l}_&8_>5?&gaWp} zUQRbGqopXETLfQTELWlu5mhyzM#}=ZP)+MvP8wCwFo1a@jKiCHa1-J3Zh{6&%SbcQ zlT?Mf)-eYU;=QS`UtR(Wc?>3=}%M0YYXWGqA zNfXlxvWCjjvd8=LKX?vK6DjF@gOpqcEcXZp=B;lGutSqDK~BYo00s@nK}w{y4rplL zA~ZFK+?hwey4Et)m8RvA;1ZMI!WrO!%zqYdChKwQ6;MNGnk7_JsC;-a!KkC3jFgtM z8CwUOWjz^~u~sRUv%KGhbrq>I%#1$a_^f#$rBS>04 zQ086X^LCwF-1AoSAo-)5#DgaQD(hpAL(qCwM>3vfWMiXzulZJf6oYj}XO_Aw!oKPS zI^jD*>_|c?pQ1ot@01&#lo(%FVAd_a--_G!i-*J{h74LgO5;bM)MQ8A)u82JLfNM6c%Swb27R<6ysvIsB8Bx;{qPd zAL?ppTxSH%Pzf@pc8;Ot{>x!`{er#!K4s;4~2>v;-SH9DtUU}4al${+uNBI<-A!HIR8<@Br$B` z+>8)$Fj6rn=pBGNJ?Wh>uSke02X55kW3t9Z9=t>-BO{~PLPOd64?EReUmYrDnxa3{ zC@}%$w^;vyj>8$DTku6{!R6Rks-0XYT9ebvY>S2O(>yM{YF_V={&c-ldi1wpS+(_Z zU(Qi@MP^AB^0Lkv*;utiRQ|8<#>j6?u!2>k^)P4Cb-oK5R_;)Z&J%iAeEE4QpfsYa z6r;nu!(}f8r+`$z|8vvH?F>rc|L;lrmuiMkZ<=cfL#B zxkX8yIweH&xm4Nc!c*u3DRj1Ueh$f8BatH*orLmoJ#gs?4On<)t@WQ5xE0(E>CF)x z{`o&tkl=zgZ3;inSN+Y}@vbS(9Oa;bO(igxL=L*`4m~^5_3JVa-X|w`4P3Bfsihw| z{WiSf;@MGq)B0J?)K*5Q;^{4$TRf&unI%W@Deu%Pf|2?DOOs0YkN2*$w3cox%(0e+ zgzbbz$Rgq9EfdBJQ~O#KRV$Epa0WRB3!S@fFjSo^1+3c843e$jBSg-+Md9C>cqXc~ z)+Ju*@SeOKsVA`CWb0Fpb#q*4j)5_NrkAFb759Aso}bX3MEOZ^Ezl$Ksa{D!w!rqi(IGT|w=!FIgfzu#htd1EYCfl7s)v$(A@H`96AamYunMyxKUMvruRv~&1+ zA&psZT3RnU_VNyD?{I3fFoiGTy4Ja_)|UY6eS4CAL%qxrwfxv_IkoUl#zmbV!@jr; z^e@`kjF7lptTAHaENTSwj_jBW*hUHs?OrcFNmkfT`U>qC<>v7cGqU6azm33)&53qW z4kVA*=Pt({`74JOC17eiddUv%O@`rg7&884cO5QqH{nlJgy(P$Uo`Km zuS|H@QL`vtxAGIsU0aBocv2}4PCYt8=PeTKuozCp%2dhGt4+-+KlELeowD4X)FW&M zrKLpL<0u7`^e$9r;!RZGl(yxV6fjxczWq zVQDQ9TS%*1jc#4{UMRy-l{&ewx*ClNzL{pyc({LrUR6gaVr zVtZ?343&eBAmbn-W^-D8xJQVpEop}dbu0pXg8c->yD$j?;&yr(g^ zm{Y0k>0|q-Kbm@BZ1`Ixn7E670R3@jfH5L@$0}skI#}#!$6DtCy;eDjk2evi`E?v_UCsebNgN(SN=(fPd=K z@z|`04|TTow&wIChVkrl4%qyz>viK z&HlkjQOWNJfRoAbgER>0?wefuw|oEF<D|L z{lr(e!+$k4OFOE-L_pkPdS}Jga|Qoj>%$d!+)SmW6w8PHdR9+J1ZzyN^8Gm)6^97o z^Rs#I{qKtVT<|lOPWr6#qYXyYZ-*QiGYp}a?D8n5rJs8Bk=eg(u@j++19<3H@?ZOF z;__f@G1zt|puM-)(PpP%G*74OL2=;wA*{QY*T++l$0Z-Tldr9QHKb~6QBKTA{dPP< z_c;bEL^K(slrpE*(xl#B?r?iu$mkLt8ka&jJdNdrFgZy|Z~5vY43I`3zz`4GC8@YyZa5$*!`$+dDHqc)eAMu%8XJ-537Tm z-TfEXBUS~BsyWVGTN){R+JQi&8re7z_HUqBk~1o;aMZ3;dlOy?xr&X)ZGG<(;HFC4 zupWwJ0xG4I=n|kwwBDE>3e&?3V0sQtIVb?k+zt-HJsODF*@7wsLfHYQ?Fk(nq>fA8 zd4lO4EhjgxI&JvC8DVeWd3|Lj%oh#q_D(B>Dnt{%U#fR=9Bu+=>Y%)Dl{xT03Wn7b zd{Gfe^dIS`ze%CEjPHjE0h@OM|4;MDO~I|L$i0KT!i!}DP{9wF-Gcr@FAAzMJ%`6X zmW#^|WM6sZiHnj%d?dlOl*xb8Z860d9F0tkZ9KI!sRSWm9W2jz+M3R)d)PWf6dpk*2hk=D_RYcrm$+I`wX454~7z<$ePER7dlv*djlw`*d3`oe|FL?yrj zpHt0CDfuxbXqc~_B^L#z+IxMLPBu%R5#NYa zsk{Kfm1!XRMQ8bU7q70PD^m0F!j6B4WHcWPqudT^dYLxaA_jvA zz-tvp7uJ0S>2GTK9DZ z?(eN^V}M3zn5T!cr+&hV_hgFqo%Hl%r27-V4j_#Fa!3jtAfjrhID+p!TJX>hTKP!I ztf(G9!Z4)v<0XB`CQ(i)-L|}Ppt0RqL&q;H)6{6Zz66T%mlOKkW~#4|#dD>57pwU2 zByeC}_KZ5kCf=%722ZHG|Dfsee z3}?2|RCFFGD?$vJ@N)OKFw=Xbjj= zHG3Jv?%gt2j3VW^T^)08n!QWYPVL=*-!5>wo=q2sjPk!+Ljw>0h}-Ulr79HV4|1iy zc>}p%zXszf{af%^M_vgE^6e0M(Fk>uy@!9Q#UR0N>g^45)@zA&6^Ks@e`uErz8SXV zRGnRGo$;M&(9YgJAUAt9BsqI{wz9S54mfd?BI@BB9&P$%Gpcmbx%-*Nv7rGNNwkNr zi9B)v)t4{24n+)Fa#fomzK<%NrEPChmixP#Mh6QSJ=@Q;{eCACdN@?Wg1 ztZ|VsNFva9CF!vX?Ex1g4zf*3LFRbJy^?}5Svj+#O$iz=d+kp73^%GM9jFoxbH#W? z`j${;`}f+eyw7ozwII(7|K!ZfS6FGE&48RztaT1@(I@XG_|ef(yLPjOtNot_O{j_H z>w18MC@1>8+|lsc0(XVSH0vR<%?G!QWpa~%AkoJ}o(7LooUu8c&>1E+7K`fx{F9Ds znQ5)-t0`cP)_p3kpg5(%g`J44OgpnYkkt#3-rC)Q&56!uC^|W@*3>vDX%FSyDvaJ& zOD}B9P|-Cu@#7`RBq{zqi;9L{H~aN_a(Wsv9{l3*a#H=;S6^cN<%mkMoF$olk|)45 zc(B*^Zfs>@s*m3g8I ziEb@23;lE2W$8Ni7#;m#RSz87K9C{#DXHl2hx8l|0Erz=t4xiRL)tqzHh+HLw|Cyx z7=vT7MVTXFZz|+(3T?ay6cvA37`OZ4JTPEBSsVpSs1`aCNQ>Yo$zm<5`@!wzW(ls? zZ8MDdij4N8y%!W*_Iey0$r4Mp;*xSOL?hvIDQWLQsGE}( zvBwblTq!#_)FV5`gAQ4*Nm$Z*-I43v;1C1HQ3kDJNG*(B$)Iu8VU~TYcWh)7#H%`k=L4grX_<<^H1|Jj&Z; z)=L7-hXT6-wbK{`C=o`e1#=O#hglgBtQag>{p>oLnqE$`7uGELjY%4E$Sbg-qRQCC zb|E0r^=)&b>o3=>iLr!_F6nrA^Z>lEMtjbvu&%|$m49rb^u#`gej^NdYLbJMWQD)+ z^46~OQGJLVIPe>s1d)Mz@0B+#x~GDI#fLo6J!jG@LgO=Ey;VwGo3rgch6aV3*BmRJsE0yWAMkB)D*w?-AeZrIu`0}lWGG1~Oh>OLxo zy+EyWds@oVGO5_rUjQ&|6e{nN)Z8By6yW!G%nDS$1sS}JkB><-u@5H4DlXG2!l65# z!rzr!Z|V^U@0OkTtyHCtbkpYgPAJxhi!<6+bIu$&l5<*P!@6%7+^&o3=bHn==T8{H) zi0qvYAA*8#c;9=*pGK{3)ds=1%P& z&MZX3Lb^>qzp69YH0`>#?0PI&`&D;DnsNAKyEG&Tb)y9>HGf26#Rq%rItUsX15?~d z&%vkr>1=L?AMD9WW&pe+IWj!>vuR6sY*{~ikcs2)P8)&AMt|n2x6{`Xp$*~rTCH!` zZ-HmeLmBDyp(bQ^BwOhTP8^S3)nc}48?fR0a@x(w6W8(%2*Gc*0F<2fF>P>y!-k|~pGm|B6DwCUQLbG`885mNLPu$P;u8Ld=zpQ5bD z)!%1e>`qveO)BCU4_wM*U)X0hMn~w9aJK&Ce&+N6;kFSrBtt$~&$yA6x&owe?wCY! z0Im0q)^XeMBN^VXn``M%08<%rXxZc(0S$W{{L<%5Et9%T$%Kw!@m_1dI7p;L&jBdI z%XUE_4XeR&!&8Q>;X<@7jP8GKNg6RjGOC^pOxDS1SXR0y{uIx zABD}9Ff*LqVUj0G3fNril;<^idtnbDr=;ff6i0asjtr3pDR1u`+{j}+>HO>M%=Xk#={)$oj)b1u`i!LZRE`00Kbshn(I{ z%uEf%v3K^?TUJP1{xaLJIBC89Oh@UNY&Z89QEm@38nDo`a8NDzX02tF6VrLs3>6t2 z3uRxD)^$`grdmODpSxp6dknCzGdu2c{26trnKMLuT+3b?*Z<^g#OBki( zbp26%zhX)(?Vy$Bb)BbbR7^cLU6ji8DIC05$eD4Idb72qZ=j3?O7En=O`-88dwfi+ zq=HO!6E*VKR#|UvI9WQFg5HN@JyK3RQ~8@y&Hc5-f3l{pZ<-sEk#Wmqxhe-zOX^wZ za2d-0t-sf@e1ej3y zQk)3qG^AE6c&#I|ch!esEMZY$-*t$o3=g;+Hl$&K(vT|;vS|=jRz#80q_f_mRc5fSpRxkI`f9zowL_`JN8yXWBV=FL6L$C69YOB7RH2|G7x)b#?v z1r!(xS%XydNpbOqP3B8VcH?fliE?NPMyN~P6WA)>Ip82AlS@m|KI3mDe_-@R>@P|V zyG5g*DmX%W-4WQSCmA1a$jZxW;S$G{O-wMSYHq)9W_C|t2nh?}?7?$4pg>jpk&1`> zjHVG~%{7Bac*hGLWTVYpUwRQr-y@@lCB~HI21`H7VLZOzfGZ-DMLIS|fp(=K zt6}3Kx%9m_p{L4|7{$~lYi?ZSBvQKk4F6htt@2-XJ%PW{ksS>K2zIM;jbgm$YIg-t zZe8de%yK?Nn`0*o2zXuakdfu>wZ}*>I-EV=^2+BtoDDd*oKuKDBmB8NLMoCmARRO# zFLiYLXsQ+DV9X8dW2DE^km^df-l^8YExG+29j2};%nA#mt?RtU47nW_-;f{gP&M>{ zhgdRFi9tX8Zk0mcDKXyz6p`^8(STU`Vmm~jz5=!VE-yZ;8qfMNrs*Z}9=*YP^vm1e z|BtP=imLK^zjzI#8^3gSN=j|IJ5`Vl>Fx%R5`j%O8$?PFH`3kRE#2MSb(Uw0GtU3w z=Z!av!3(^5ttaOEOp3tISwSp?tel$hLOz7U0Bsq#wtV(Jn`&}r}T3k`>lIt zvB~M}WqFE;`gOAkp0BuGJQukMX8^ha@-cVNyszAB8E)7tU5kLpqHF;F4(ds6$A6Ra zA$F2ls*wZ+i*2=NZbsXL2J8k|NF{Tk>OHLkaT;+P@ltD?aY=72$HQ_^{-rY!Y?hnM zP?wcUc%qBGfB*LW+(K2;{l|;;SDv*@&_tGb9B<6Iil(yd!3fv-DEXkNv< zPyH}lVkIaiFBD4&frJG%aS#;kEpTy6uoKyz9kiA=b34U&73*R1k$V!nfxJXCUD)U9_{{Jj6g<_~6b0Dcad`}}+BbW_%FgQi z)qe>QGtmLB4=Csg{WGvT;(0d`-}CTRE}wbpL-!aSj7$>>;I7m-P^|;-WE-8)^s>bd*n1zRH1Vye;c)O2{HAYHDHdCeR`}HdgIYN zdQvxQdhQ0lB=mzV(cxY-wLd=UFP%L|_&k{wg`TE8i|&qiGkLfdt{v&A#}-P~R70|( zGMk(KELi8A&zUkGheIpry*~wrki2lPU;m9UCzsZDVj|Ze#QAcnrg*z+dy`4vX}==$I^Hf44H9o~*xk)14Bt zNg@Ak9kl!S^T;2&^rlCDR!ugkS2C=plWvuJ=`qfZS}j-K@_*gENrOblQq)I}jp2fZ zW4l!IzQ5YP>n4rf*Wl&Kp;0luG>gSXF5SmwW609O)zf!)cHk$ju6u#<>*Jhg4u9Y5 zbdh$lN6r2Zlg)*%cCQvZ7+lO%I+GK_XPK;4T1;?8q>{O6mQs&ROcoLK-=+SU@q#}C z2q1~<(`E$|G8LjnV$zV>SmNs>4v$AtR}5f*{U`Mna609wGQ&f@tu@|2y?@@UOCghA z%UoONU6_rsuz=&-Q@gr)P8@Pq-klRP@-K1KSx$!_iy@Defh9up_P(Ije#0b~N5K0U zJDtyghB(@UP{dU@I6q{h;#t}znUL#(7_vBP=DO#M{H_g&rHuoLYn%G0>t3>tjk2Yu zun3n#>NGTH31UmIsz_{lk>^~8NMlBk+c8pEs#^QHutMdnh*-}S6*tYx5@Vxu%u2Tl zp6V*hzmm$DW;)2x_1ukt-N$R3Lem;+dS+ye4>-2ExT+o72YY0iv@aH5R>6`2yAMa+52%L+{7lWvrRywjMj3QFX0Qj$%4oA9U8=&Nyb-0k?$84YP=d+BW9 zV_g0kwJ-UGP@)p8mEDmMrFZFCNJ5+dx5aU3v@jU|zj#nr#U^r6b>OQT7Y^f53XN5g09I==Q=sV+IC5DkECCfW1o%krV~EJTbn zShnTBt$w=Sbrib1`;f^%LnHP<i)FS#8GLKf%U&oG1@J0#cF&Es%TN5|%StDCO4GyR;<$n+oVXGRdoT5~w#P0coZ z5B5Zjh8NqVOyvcELcGC- znU-!JhYRbzlO};~MRLjw{6_Euw}#&S@i{VHl}DR%|Jw1x)6f6@k&5I%E<8wT%5`%P z)_y&9;_6ma?$@ot+{cbCC{pCU1ljgY5=56QDB&AmDQOS~oK!$O`ZPxBjM#)qD(gHR zHFa_`?Ds`lQEPl`zQcz7Av6l!aA0Xr6;YdI#`f*Tf=~=iGx-30dKnk{g#x1Ecx%2&)Fh2V2 zKQuy7_~-k1Tjt>pr}0nMbOpm7u#zk$wcfpd?*Y3jDzd(LY<~I{6sF3AQ}poxrI@x#Gq3p*SgJ3?BXR!`TraNmmn;LHr)wOEMW&_)ojQKoWO+1jqu zFFkRxmn=!}Sk+!=%skiETE$GuA0PYM$)4pEl5F4;WyvGKDk%0LDU0Oq8(B-55W-;W zl>9=e55b{YSGdugUpr0hg=7!{RC7-ir!cw_U&kjVhEJJdW5^a37l~Wm8tLOxyCDag zf8P@k3I%g*OS^*=?HM9lrWgs}o0ViGCi1Js$+Ddz-*-sOXThYNe3^gi)?RPJRAAig z)Ldc5gHU%UXkWJ?vxGG9eawl}zV=3@E1I2-g*$!-u7XJ>D5dJb_dF}h3~lR=^NuYY zt%+W3wJRGiKEyEaL=6lm;hykgV6Y)m9pCr{t&NCpxypvSTwI0-7^wI1HC^bxnV2-D zi}%C`8(XDz)u)&VbcW?>LaUOjsBJeY?(MOW(WjUu%UJ1-`K`CSm=f_^g)+qyD$Bo_ z>hfzb;hU#2+X8GI!Qlg3Y&R}VjhbrI^GnLk$YM4rN60f}zMjFs4J`72NTkE&WTO^U z%54VX2=$L%nee6_BNE|Hs_*6woX&p2OQRfVp;K#M^|)G!jG~x09#QoDwWm-K`JJDL zkk-ObvHjXdbX#@)#9CrMk6&ZuN9!3fz3miQ|y|Ux7 zv$YBNxu%emd}6$xPOXduMjnr}W>+1{9x+NO%W}c*hBKXq-f!h)*wEG4b2u#O-9J9V zz!)TDV=2ht23{t~tK|!XV?y!siEN(}Pa&xs6a%jVJDqxOIUW31%hz+U9VSly5m3n_ zAS!E0AoNN(+iq=gV%rCBZ=an6-W?hp?iWh6;RJI`pAOmkpO+hI&P^BJ0%6X(wPM>c z6-iST;DB)?9SJF_hHez3bBI(ZZ z*16Cw|i66))U zN}s8;VSgFR7&;|uE@R1vt3j>5Le|dF)6m=H-_vRc zn&x96=e@%)vQbX6&^t17J58A!BFq-?DEbmE~5e8TOoVmH1L zx3tDQht4?|4_?@epktlnu2=H$p5&F>v#5%mA(}A^k3p~*tmNBb`IUc@We6JrABOC8 z_0dzD!^0d_t(tDhrm$P!)KRD7T~v)9?47}luK~SMh8PF-cX(4K-&C`*g)Dnoeiw|q zJsN(uaQl6eQJ5Ude9XK*Nx!0^($o_Ov^!!I#7vOi8;4+PC!uI8PshZ^D?YV6OXs!E z%V|W*8s;;JgFdj=Atxg zIAM92OJ?g$-n#&fxIg^R?RD+`o|9f%s%sseRn#dTZ2pahPT>fd0@1+E1-^^alR= zkb2SX`YOajJyQnBPZVlnbWZoiqjt2}aJNQBChN+6h-_%v=3a~9|8`zl95v_X!ffUW z`WsJ2-FcaGgGU58XIs|Mzl1#)*Pkd5ZEm%iP7|B!`SamIE!!m}sC14-I&p029%>&y zN^{}l{Gp8An}TQmWgcew|0pFCZWIP;s}P`% zDz!sXo?1vyIY@wWyX}5r{`E%9CmR@IDQ1Yq4F=g0xa*EF9#A*f^GGy7N7pDolDS-T&JL}DXW{m1b7_z&=s zelj;VO2zN}gqw{P^M%0BJMnVJo-2q5Xm1a|_juVE zG5bqL^^ls?JxFgA8Ih8ER_Y$U31*O9cJL}#?oOru!Xwx9nq~X`Wb9@>yL=F41 z+s*hTatb`2W$)0o2`Av=A9p^TIF(6=Z$w7^G8d}xDbrBFmRR$gA*!-l`js)^+InG= zRBm!3k?TqjYWU^d47;dq=Ph6L+QpsttWug@--E~E7uWUg%TbOVW0@j$sIOjmRq*ht zJLP}598zg`PVw9@MoQvfHdC%6yz3TRdHJ=4u}M`QRI&=q&o*JUi#4f^{C&kU<=Ukt zM1f*0Q3c=P6Z%$)3JXI9b=^&7u@8<+2m8zb!5#e_ZruyULJP%*-@9%BhN4HdOZ~Nd z#9sm_Yik93mjfXuL|TTjc}n`Ke)tkbA9)M-7`oLZWI&Th%C9G5VF60F zp*}OTR8fb^<)a|8mIp0;3E==mU8Hp0BcN$RQf|o&#{+m04K9f$mZb1er_rV+^YpA5|J!iS9Nu@sj566d*gMOX`Ly5@7ZYZp+u**?>{tbjg95J z)!4eFJqNo@S82M%&(UK}A*{Om(ewMotxK9$JVN9^DkABi%J=rI8*M!AN=j$=DYI{& zUJGreoDl@K+Z{QKrozK@Y;jCpTKeU1^PkKIBrFp`#4#0c(za`=3ac=3_-f5%*T{o+ z>c%E~0Qt3r$c!UfBlBZJ`!n3V{baL+Hw!uSC<4{3H$U8a`d(g5(1n7SVb$jJlS z;wZ<=jQHd6S(ed7UuPfD^nXsCC2~x?{e#B46+3{Q@&!6Xj*S=wO;Ue9{*^r@a5$%9 z{MMVgqO#g0yDro3uFvx^WNN0#M>IS)Dx2s$to`+-#o0>O5fu&hwhn!`7|P!DOMh=4 zB!2gO1hxaR?NDdrNIG0l(khM9lB;rHW8y{q8)@Y1>b(_xx^Zb}dSoLlv~_5qSkDeS z%Kph8@Q2+|AQ;7>C5lB-9S7^+DO^50eZi)FxPtcX)70d=1u_1YzW<^JgF3C7e z-oVf8<@@(h=jUNFrCZnsjVMCdlpnZ4?Ph3|z>f1d&^ie?S7a0foY9v$Cj8A8q*o>C zwh+Bgr&me&jQv7c&(DV^ZNf`h`c;RCu?}%MAtB-Ss_fJxD40i__6+s4?44l<;{N5U z_+z?-({y*#yty&8NH5wPteu+$OvEPyPiHQX6k6Mtwfe0bS0XEpDN*Sjj>Y>tGam&$ zS(qP>yu8xTN#GNbu}mu?@bbFh;o))I93XPl-)ajX9?B}|qClsPmCE7#F-i!~pWw|0 zp`ezQw!jXn<9~Br&QtAXoQip3nPN239VZ=;D(dm~YmGiyArBt1f}#Q=GxO$pFY2u~ zqGRszcJKJKB?7nPb%TLYe|BMB)G7D-eTeUuf-J8@FvUpymjPlHn~1ZTS#Q5lh$R|1 z%1O z4_0$kqi!%^Bxu{;gko+k0p<=r4hE_ZEY^;R+vrAYFdU*@Gt_ zdIy3J^LyEF+~+M8la;P$Axkp)_?%uj<+Bw%IEpE8!^g(QWjj-uFAqgq7K6e7DL^FC zf0K^acJ^Dh4drjzv(4dk?yPJ6nVFCL8b46zlo*1=Uie^}R=L99Z_V<1O*)xmm4FRDZvi*JWcFY%5sOMT^n<|<4HUYg*78a0 zjZQn96g)NcG8k$ef^xe1rkhOpVw*(JCI3ZAW!L$`Uo5)JEQ1tY6 z1>jN&DWg?(T-@AD5dXCN%@6m1&%iqE+|`W}*uAk9e2~g5lw}nr|2AaFfyr^=OIfI? zE;umW^f3?NDyC?sDNopW+#$802j^5&d6Zi1iTI!VB48ACfqojJB}8@}hEg@=ro!jm0L0@sx{_+U9A?;*8YYsx=Y!HEtdtG_IJ>S4lliilfv zns*;(rcYciyydVh&an}s%Uk33&5b<$#(f?}^J!}{UQ6xshPIrnz^V+j@xR++dpozZ zFjdQ7u4g#YKUQvD!qQ+MS6<}^gXCdVJ?@Vt*>P z%k7`*$f$g)>7>DHr!{2r;GaMF6)kL#l6D-cZeJu4?3R*FK87G6TJBgD+z)&NH1rxiJhjBG@wWwK--*qpgd_Lk51L+lqnn1h46a5MmKw6+TU=!ZkwX0AfE=^j zVtDzlu0umnVe32L9^{Z?Z{hug%IL)=k+v0t?8s*ZC0HaRROoTNw7t3EY_DK2{dZFn zr+v3kLU(O@86}0UT;YZ;!qxLN#N7vT`KXa_8zn+R1w~y3SuCB$4lj6| z^+pRza}W}a(ibTBdrAN2;NaVB=d)I_rA=+0fM#Dwg%JD#cjAy383D1K0mw>*y zJZomlh?~g?VYp|UC$%^&E){VEkWc&@u>Je|g8{^^bJr_1J)O30ky`}5`9EX-O?zSa z8Bd7}WWG(U$y4`&r+pfbnDQfqI7hBAu!QTzK{KBdr$Gcfs*+B>q1pOafK<>+)#>`H zXChfhS=pt|om?VkI|V5sez+2vezUQE6)$G)McBE`RSIegQ^5yRYkAmc>rm45Qsx-p z1^*Z|x$4N+SXZoR!FA{O@EL{Z9mWYiu$min(fF>@M7s%e&dwV-m|3nFKCtF_MDbHwD>+aS356DkHA3!e4Ky(82DDZtEkR0Fm zeKLFHdH&J^s0g^HMHOe-s;b-#eAd`>bRbO3tlxh!X4N0o*m0-^Ljov+y^++^)Woc; zn1uZvCir;qqIG%RR+d`LxnoiYOKaLdwu*`b{C|8|@QMCowx_~tJ3Ro>3kSsl2=)0A zdTQuX`8@FX7XElkH3Ta>bJg$OyU@^@Y6&mns`K`V^#lqJ!i@G6p`8_mTkJ<3>R&g8Oe*#>G8SZ{zqek$%?pRt5pA@K|#wD2j%R*EA< zyscuUe2X&n5y7lrmD$BClyfQu2qd(5gyB&3?ae@;+r#%vr@3wlhx#0hojh-!`c*tK8 zQY^9^?W{kA-INeI<({rKF5YQ)F~2#{-Sszp=<x%qtw;BcGEY)op0iu^9q5#0S(BF6 z3!UkeGdh&{ib)-QyO;6KB@-b^L4mPo_HU1lC2JmvwMWr>ZovdQ3{5 zs87^imI|E4>-Lx!87avljCJVAOOi92p8Ma;X>ITAF}y~1K4atK^fKvPHkr8kvUwPn zoJeFcmKzQh@MGFjpY3KRczMeFy8U-a1O>Dvf#%=YAi#yD`Sv2B;pVS7?g?%7_vo=7 zzM1zy44%h9SJ=lsu~0QkDXp(*v(K?_Z;^iT>&jCz_4W;Q9-XP_WDLK&n(xE-MPLMB z&SDSS{N@ZWA(^XAdFL110$#-#`bW0Q%O|M2*69N31}bK57)V&jCtY*8o@av~6uQgY z!1K|~{b*Lw!2xS5cHVc7ZQgB*ET}#zAD<45iV!)>Yb9@Lo8oH@N^i{%iNl67%rXi8 zDQ&%#?2_m2t>>E;QoGRztfvE(IYb=30@6Tpa&w31TskH zkpA6F>D@C~XtLAxF%Bk$NMF-~>`a3j``*%QpG}FwO4Jme)3q9rU!$-kF_4kNCVzdv zMn{z4_xX8;0woh|WhrS4pW0%;-?8YLK$&;$7tCeBZ{^^`oNsU>b@O+zfq;q9_sZXB zCBusT25&qTyjZB7@-ph!&Cheo9AU`MprpbQ)Z`Z5*_CDrQ7_5(6~KQg8O6&3GcNDm zy>njgk#|23?%4bd0VZsWhKo&lFu(zRo^WZ`?^$~v*iCohq-ipGGxwNc2Xlh+MUa+9PKFkhs zspi!bS{##Ah^J6{@YfCDzA4+L?n&`VU~%?OYFe6(uZSob!5gESvd(Qd-r)Z`6!^8r zr>?K7YR)0%Z-CE|%$}Y5$0)bF`EK4o(xNR`!`g=Ar-LQkJVgN<9>TZioesN$I-hq~ zsU&6P?ROBl9|?BMIoNI_YVjVgDwa4Mx2U6Mg?(1-=8acM7YUCRxj`UbGl3+RjY@}r zkwabZ(9oO914zqGe1U-PI$^y#h0)PsknK!Wd^Mk~$zIZd?ULK)r|TW=hU--uS2uT> zU`i_;-|N`0+O?{5?6f`nUBNhHDnNf&y<58D+^}Fdz6(;y;7?_LJlRSS4XCmJ<)eI+}tC^|HXjXK2?mM>K|yaIi&#_YB)UAB8}2ouZ zZ=o-g$N#XVN2-1PSe|d3hFLi|h<+};eQ>k|-m?7#pO|kh1k)8}SfB+jx1FznS@Hy~ zVBntL5gBF-7l>ZaQq(*_#Vu&`^=jFgaog+yauZ(TuWM-Q4TKbV6BDPOY#Ue?*-V$+ zkU_KffvawkRLC<}%g`s?BGWH(sp(X(yRR=?2Dfj9E0%J8sXO?D&i5QkBxV02Rg-Wa*LWpX`gn z_xh!{Ni6cm83Ccwo(15auD2WSyl~;sP;jLx{B5@}G8%t=b-8+F9Z90qLgBZP!nREA~fW9sc#v{&bEq{IPP8}hgavv zr#5?qa4SkahOoE>Tl+>Yvb@flc2fknX+7Kq2qnfwN9FyL+48M4^fmkQ*^a0EpWo34MtK58hB*vhK*^B6e{nwguO7hk4n;pMaDp80rjCiK*1laM76Hqx*S zICPL829a|Pihxs2h3@|t@%Wh+pjkq_e-C`buVQgf<6uN4Ge5k5PHs)E+wF%R78dsl zx8eB)A#b00enY@UV;!35L$jT$3X_yoQS*9y0d}AhQ*Slge|!N=U%Azpih-9M4ZsHw zBVAEQBC{Kmgh>ejkWrq{`EjGC{#?%CkB(a&{j~!&$aMJ;wwIRxcm>O>J2moEVZjGI z@38Eq&uS0S)dEVu1!*z#0S6fYBohnSz;&HIwqluShTihKxnU&0SPPm;iL&^_xX8^` z8g_iwPh=TcdnCv80vnc|FZdZ}`rNqDB4$Tozijm5;bZC^vzznyV?6%BeSfyQOL@+7 zBZ|+{XAJ^4HV~!X6H+9n`b?&F(Z?HWuNNSjz$GgtSuxC zcHe=+3?Srerr_qc3E}p>Z-(&n0_*haFP)3-(E(!?9TrC66|*WqNA6m93cGEpZH%8< zT9l$C&Hj}GbK}$N9;1sulEw(8=ZL4L>y$>26u&%LlC@F9n*^6Icw^?=54T#fP50*l zzPfA`2{^CwLp+Ww<}MqB9tQd|;6Le8VgXa6RY?lj+YLN?-@wi|;yB}f$+w~O{ttVG zG$V{;qELV-LuytHZ_)?j0>l;61*J@$ShPyF1Q@@3Kuy*VoSc?-53bvV7cDI3Zr*j& z5kB!8y(FxtPlt{AzMucKw%Ameo#g1mUG!Xx-2wn zc{)5E+#xZ_=}Rk77bG{6-0Y>`#Ux#6$LWK9H2+eF*L}7kd)TpdQs?)0H+2Lp9c1j!Vf7`LkiQRAW% zsGouIK;5nBn;J(;g;hMjaiqhoBV)=`4Wh$Rx$})b@@^EcSlU??{Al<#Zow?zcT38k z0$UkbF$H7jFKY$No+o)zAYeG47Ye)Eu{_GIFNsE3Q}9}UtOq#6I>;jaXQ^?wVC)Xu ze>C3Q^74qXH4A=2zrea##38Mzf5K&AYm$FkvM|iVQlDXe|GTzd_uCB3>w0DwX@{~? ze(2{{+)b`R6khk_sHkW2?#{24-^*AMjMIM`w7@juaj%uB@j43+-BK&QecO$*HhP;oT=tRx{%eW#dF( z{PDg(O=2|&Hr}=_&PY2Vr9mU@HlDkLkqWxL7*~l&Ct7d1JItw(yJ*&MQ?yi&XMnJz zhp#CX{{EFyUmi3&D+ultm75tI(7LuZHQs>(ki+Mad~+k&a3i_-hV5uyNJ3Ui^u+T; z^VD)a349GYXHWfF!v!%Cn=|msz3&jLgoYHVX*kt#lG8n1=f>c3MfPLA!wQp(&2;!N zXCLAiH#m2Ba?YM*)}KFkeu1v0_O9pQY5^=f{9}wpL|KnYaGob&NC+yWXh%YmH$y+% z*3}>1xz%|KPFHwAbgK_Hdq(2{6Fl1R;a0%?Xi<_Ujh6eh1RmzEB=$8I!)x@Zl8>Qq z?#=s;yEzGQ4H>ooFCnL>NFuD?D20fNE=8U3Mzp)i4kGa3!_bOp!Oew(!D_eYBi+Wv z*72=`r_l2+2JGOtN)x?rE9mJRax8fuA2WKLmLIE>20XQ&9vfyRCVAELy~xKSXSqnH)wn*hzK+Q_mF_ZQ*cOaKNpOG`-lC)d;quCTJU%&n~@>Q+!Q)a@N& zb0~M)bMI5l`T@?dz(@ZV{!y>c@E!Nq_D0@Dg@z(3arZXVmb2sD%MBu-P%$#I(lYR+ zm*H)^)5=_HS!vG@R3C~pWhmr#3B@buo2k5eHn%v_yE!;_eNketUI~WvxQeSM*BtTk zO5CkqUX|BwWu|jR8b26S+XDIJ@m#V&L1`hZA;TA4nI`kA(|rd@mF-O15LrE_+^`|; z`^Mu*2Ms5iGXA&YhG$(QB$l?;t$P?omgGR7+64p^A2$cnL+l`yGS(UKXhwO*Ln9ATZ)`!#xH%`Yx_ot@YBQtYJ#><#W@~7#w_mHxwT+A=uP{xmP5-hbtG!2H8zb#0xP%H5EMU zcUYc~gm3U}*tC@J=_;#9+`~fz7cpwvVt$vUc!4&?Nj$1h` zqIP~hyQb|(YgITSOYlNi&*bZsZ(wXBbgo_)zCyhFFwwab?WGqt`n=?VFk;lWOK=e_ zr{wLeXGx@6wjQP57nywvl9eqhB`SsPqK^0LB$PEefNPEB=67@ zM`&v3b);4P>ghvY<*yl%rkRgrWbCmR%k2dNhEeS$1zjxy2GuN+wXWa){!TgFs+yW} zxfyh%7+J;~Acf`eI$Gpk7@bm!1LgB}H%gUQT;ZQWhJ$}$cauYNW4V7g02RM3zIZ?h z+sG4xM;Z8&y5>l#u%TW5^gx5NlS)}!t!^!(OcIW4mVn8kyS`<{?b5|sH1uKY@2tdF zg%fFSc$qVl4AOeC#sEC>RH=V((!g#H=NyeZ;GR?Db{!8SV$j3_DwYaDj=B|f4?FWe z2*OZiVb`(eNO13J$eHJMAcS#XSmsCVkYKVba$p4;y)t2UTwcGtHm}7nubP&Ae<8QU zO!X5sIOg;3=e^R1A%(RgO1ir2a#4{n6w&nBwYP#Im+N?}DSUV z)XGsy1W_kx`Jd@!|>kLczeS4Mg#v%=|pKZnbu^yikqx22Kvm-Q}kV{d)g?pAu0JoU*N>%YyFsYZ-J$71X=0w3o+mV_0abyhMkX#i7{vxKG9o)L&Z zsHnceJthK!{DGqP5xv8`q=Gy8G0ZM?a88@s*Kgh#e$R#nW-fC0HL4R^1Nq92jEB4E zGM^)V_k)??Y#Ot7?R*1RRi3cu zB(M-5BHT7!T}LUdUy+$&^7-T?2P<)Jd!4~p^6BYuyu%blh#a8M+E(dYn3xAbSQ5fZ z<@Bqoo3lVuucoOVFFIk{ zUu?ikUbtl7?% z6Skl~F*WIOe9Y;7AS7j{Ot!Gl2qc0Ugi$BxJwEd9<$S z`S`-j`tip{|B4TW$e5JU4ziH}2$rIuJJo%Yo9JkzdX2n{b^Wo{b2)ar!28F91Q>@r{GGgcTTA763Y8O*Zr1X*OMhh4$C)emED~zRbDcY=|sTDs34ckX^ z3iv(XX@8U4f5g&?7=De6L|6D0@lT3ybH63hd#$nY$3d_4M5A(p7ue=+&dl+U5x`xG@awSy;6@H`a-VIT{xgQtuo;O z#7XU^YNiT1kl(o<<&+d4%{^}?S=QTlg-IcxA}L7&5NOD-gz{H#MTU*8!g8vr0>Pe4 zQ(r$qN_M=_GEIDpN4{f~^#?@(P4SN@nHSHKE2HCO^!kIq5Fc!VwRAl$!Os zr_FgE-*F>`;B5}4wo(W^ij(^v=Zkk=u2}8XxL1LQI}F|G@!2tX1ABaP>VY`Av(&eK zeMSNb*?9=D-%y!ATb0y>@9lG2$|}#r1g6cKzTmIG{Hw57NY%{jzlWDNxf2{h{B1-jTZ>|hbHGZ-X)pY^M48O}EDH%-N1Sc#l4N)aaa4-{wH2uek+reSJ z?Jt=RzxVy?#KcO#Fk@2_ z<&BNWZ9bG3-6r%FVXdK-=jyzAE2go9MTo4xi^yw2e>b2+fOOMl*#%lEZrw#a-f#u%r%Em={jHR3`E=m}%2I3%M zzPA`2n^|bT+10V$C6?ET3r4A9c$vi)+J85VNf;yLm|JH>fX1AU+G!zAC*|SRXk+5 z#hZtIR~MW9J-3>MG8Zse?~!;Nh9o}+J7&DvhapHW`m1GBv(tqbp7;zQue7c%`rrx1 zo?(E+5^7PNDrs@PdpzPJR(luI9hq9beYkJ(y#%-up;Fyt7_$(N<7AryuS!LmjB<6m zh?kqyPLc;$`X699P;-UglT2~Sb_~5p%1HnGyBxK&Zo%pnj8%Y`x zT42D;mbftC>2hWdg|T+ipi77cZ76PXcx)!Lo)pNGs|W zn#n&17e<;XGo@evXGZ1ku%<;qdByKOFH)*nVwb1s4PG8L?$O)p)jM-EhCG`~+O7lh zg1MRUC*)dz8GzIMH&!wqR{KJsg$q#*Cl#pbjI`2B;r76s+}h(0fTNalQxf)3;+6xB zURN2q>tKZOH3~x6zqZOxoD~fmCH~FP-&G4dzDJn#Cp)gCB`P?&DLIv~#P?M%E^?$- zALP{)g&Otr{|GtzSZ;oi@*>QXD!9d+)K`QZ%&X)rWU(qI-CSKSoK_@+Wup_yoyOfS z4~08Q*BGW7oREM$IOePFwgfOTeM3XN89_SQSV1~!+dIKX^vPb`enu`y!$K1TGRxI8 zrv`4yCtcky)twGWfy^C%5tcs9T`v5?<(kT>0-agN26FC%7@>BRG8fKT)f{{)@Xnqt z0f&&+z+G1LMGQ7FLW-yZc@zf5#N@=ND0X&j9<0#^%c)`?UAPe^cTHSQ)Ib%vq+3!b z&S-4C(04ya)p1y#4;S^-s9ts|-nrAOSDib%`y1CMsFw!< zZ6|9R2{6X3{5F-H0OC=ij*qoMV$uExPE^>&;O!T>~jd( zR(xt|IMv%*pLQu~6v0`a!k_%A(w$zQoZUPxR2b5l9GilCHYOZM;S53&y{5zx^(4Tf zPhvc&XQ{x^3W76{fYJt_mP6LJHsiit;F50}8&Q6FBSedf|KMj#9ZN_ajU{=7cYN22 zd;@fXI8SG)PceH_Z^&Nr5VNpr=jiDd> z^eA}f8{cin9TAAKv8$H=GC1Io$$T))dD1l1BLd)2@}Xtv~>Z_e-0pVp^*iJ$}QmVFIx!bn?fRgV*?atk91M25O+Ad#-v0pM+<6pHs5**Wf~tC?0$Ts&l~(qw(WLmX;Z?E?Rj%@Qlj0 zZEP6ItB0VrxMY^$_E<)oTwq_NK@ZZekB$S?lYE#Z>%fVr?DoOo>g{AdiWxJN{d^5@ zLJQ5>p1=LnWmar z&;5-HxJ5)d%FJ945894v(5Q~@gx0tI!$toDEcfyfk8`@HL#;Nx+0Q%ji{+Pt$+nQ&C62}o4!^()w*!Qty28US#G*@!(CseuH1|Y2IWi%3stNFm4xxd2?+;7 zth22_qr(g$Yma*I0#pFx6x>Qo!sqsQ^i}F#m$-Q4?d?N^EW5M4Ur^4XM2#;y9rt;v ze>V~BrhV8iLsrz60+%G$S&CfQI@?i8l^qF-EE{O~2GA+M#&csOjG`?TKuZ8^dDImq z$J)B*U{8sQiFfP;#Mjp511YP5Rr<5x4+}BQf0$W&%zMZ3I(}G>{;>p1T zbxlW^Iva{CC2}-rDw2G{Q6r=nGoAqyL(23OHu$AL`Ih+*H`_<7%PH_JNbP*q{Y-6L(*KHYS_(fKiH>P$)&?jB)}1!eKWN)!ml zLc_N1Hlza63p&y$HO+A`7nhgXoB)SE5Jb@c`@R%m3Ic1;o=c$Q%ZW|7ViO}pCplI9 zNZT)_tlQ*hqK&va(vh{Y!ugsFBM&?LErNEH1_simQ$xa^K|W0jTXnJ7%9mRB+2|j+ zXo+ye?ROvEpN;);FquOU0?U%r@tUV0UBp*Bp*i-y@f(o1%To9p-bfHuc({Z7v=KL~ zM=`|Z=R2*Yj0NB32hh2=gI=$jaakV(=GObpk*D|m)tPpqElS{L3R*ax-tJA|Fk_#^^b~I?8nuy*?pQW`8IkgJu*8kFeyk|FSl{}&1!m+)LB$2E5pk^HKRT59 z&|j2yD6NaFySngOc-&sezIU6NINv`MxXOq?`!3S52-|^!e}}2C)bjUpS&~vAH?_*z zDNA2dcig7Syja}rm%hyyU@0of9yjd4+oKGwX-{ed< z?n`cA+Q~^+#uwGFZY#wKkxKFi{EbhnKv1;)tsN)z#8Qk{$!T47e!(5M z$65A6i^k(}6Wp%v312JqTs5-FKGy;}>%pyltEfs+)yt;iunNJT)`q=u|ACcys3Ry9 zq?Nkt7q#q|xOI)>wHtSKyiK>NafgB2L7+so;vQ7@#-AH!b2@-L$^c523S6#-B2QQsWf%(BiMs#$pZ~L3m&*EJZ@k334 z-a({A(Z-V=g>HSW`Lt>@jk{coS!(dXunWwUvyS4vv(O^UZ~u~~s&VB;UdUdP`bOS3 zt`$=dW8F&bA=az4^4ivWf00qu_&EON78`vGfEV$~RZXO(?EH@?d%}F0ljsO$_;a~y ztrL=O;QnwCsH9dAk-9E3f>RR^LB*$FunTAkpB=6ByW+$*&$s{bP_Tbssv3|+sAKa~ z^VKp|k-E$k$>r0&)%?|s*ItR`{t;tU>$b67_@GMmHme9kJ06*kwzN6wQYy(kVu%2< za!4|;WasAeVjO6XBKmWwVy0kEHIU@v!#|&otXyC)u_-(FxgqVt``Wnf^Xj3SiY0m} zHe8A#kJVvbJXYB(@pRjF=KU6|hz0wns;#8=!;D1e*Y|xX!^oIr?!wcS;nkakWMw@% z;4Q^|FATp3yqSzI=-aE@kk*H zihBhFK`*T8)p1;zTN~D|2^gP(i1AtN7v>tg_15TcdRhJPx~h!M_AcYSM~x?U)K&r5 z-YFWg7$?BU>`ZV~5@8xR{-&gLGv*wI-*4ep)^d3JVJ@!3mvnbP;hvwvsU5doN6pi& za4;5*?VzMNLRD9Ojt8&w0^4NB+xB6=qvZ2#(lSaDgT#wU(j>?Ni$kGnD?HfA#=y<9GC&=-16UVYz7_FVjC zwc=G6uNXRn8-AR%4`f6nsYm#MReiHjwg`$`BaY;$q+{g-brlK-z?h@X0-k97INf!( zDW`=l&7GMhL2K}ab1Pm-ZS}PMbgtHIiu1d!Jxx|l6lLh!#0)t6zH#fPlzog6u5RMY zg1nS))*)%8R?S=&foP_tEgYy32hb6G`hTZ?Jf~M^-nx<()GK()#ev}%q@9c)en)gg zGqTo%6HM*GPX4I_Mf50p+cS%@Vd;YD1Y2F|84eakZ5&azb*)&Tw>WE!Zsw8jsM@VK zFzH>$@>06~bi2D~LCEez=w3io7v8e4*XGFF>LRcRU@&^E5HHlK*$(p20&*U&Js1Q( zdoz+W{~rM}Svi;3)D)y1OjZ{QU-QR*k6K=0D~MQ;u=AtNgzl=f^M2c%$2R`yc00gn*<5Gsp?~Mbr!)}Uan`*VF*4x6R}&bRnz!0aUnc-br6mB$ctPT z)AYI0T9tjF>1Cx$cY&xa_K)>qvYf5z@=(iOXYDGmU3zPa7%ewl%5zFEZyyLNsj0*v zS&yBD-CMAgX#CR%f-O4nq{;mkOP{pAP0_X$F?TpZ2t-~&Dc+nz7H8QN>g+MLkI+p&j@zF z%TCA#@ztoJ(>+id-i=cZQw$S4^4b}?D$;--gU=36yTaTGqxWg7zCE_> zM?ur|-Zw8*hqu+QA(s6T2Xoa{X1`Z=j;^YRg>E`owh30)A_OsNUOgWMTNJ@9^hZ&X zHgRWbS>lg)>%Hfq*yS~a(Kv577r%X=ZppXPD9sI?zMSC?p4xPr@E4@1wJ%$RQamBiZQpi*F>hzv#B?@3TWWQm zKjmDC0THjJ%8B*50}D&o;g~lMzNoQ@t9=;f(Ed%WTfl!~A!OB}kR(zNiud=L{UN3R z`#spWG#_x!ZfJ0u%1(E@vyf6ZtjVtu_3a{0@~(ct8|_=kTroZ8v2@`ZiaBa)?3 zRR@=4wG6Wud6NPE)q2( z2qBTu4hI>&9dZ12QI)p!;-7kt^KEe-hhcmn=twnr?R}9}Y>9BK_uPkrpis8W2SISw~9z?(|Q8R;ZH@W${AEw{N4>*Tp9j{mC)DKvG*j+eHE_6H>l5&LMH=p96IQXupuo;Meb-T3|aX^dK z@!ZIF4?6vb4K(d>E3pKlB*3AlG69)XNv^;MNDgUih{$?PCx-7evm-k%UFWEUqmkJ; zm3}?%t(Pk5Onsj`vAW2|&#HcyY~KjX(ecNQ@Ms@wu5KQc_>qac>lVoMommPFj=M-1YMtd9`50Bmfy~g&!=%2ng^#gZ4wyv z;7by+a+3m_GWgTO?2ise>bWJ`v#f^Sf@{bpyM( zdRpr<<eG=nB>GfpwlE9yhe|SUZGWBSki+dM+qv0hvYr=j$-}N?*t!-o>}AP|9rQ zxt2%U?1Q^)PY0)#o}d1u%)YcObns5!n|k@rY)z%p=JCM~XeGFU{H*%bm-xCJ>Pv@y z<@-lg$~~o`v`1Rra^$BEp0fOGk!kgM_b|w)SWaeBlyLn*B~c|cBRTVlz4XBtNw)aq z9MuvUq*Mjttw=pTvGG3-ll(2P&8>e2V_-z7(F^B#*qnBCV$OT6#Ji?4d#!lRLp0}+ zu&wIeM9<|L=N(&YrG9-knh_Dk%zhAW{*#6JV}H{~rc7As=Cvn=FZOi*#Wt6WAn!VP zx`g%dNBEwsx#846w9BaqqNwBiIV@S&rz38oFs7&H&V9nQ=+07wtc-#vt$i3B;#3PB z%N0T7t3WsMpegz2SYPOFzoGyKP|1vUB`J*se}Y(WT6H^`FRQ@`T-a1F72UY8nBY&g z0m?nif#Lfu`vsMu|73i+(BXqJwFLa`bsi(ZGBkq54OF?5kk(VieI!u)vs8!vPQo5! zwS7H`+W~0&6$EV<-OZ0Ukje|2-6F25Aq4p$e!iQ+7lU`F<4qKFxYW>OZM%S@_kh4HcEpsR_gY|!UyxM=+sQ=%k&pMO z^n92o5SbO7!r!(e#F23H=2b67^AeMzzW|U9Op$}pMB&K_uq_z@&V=cj8XjOH_LLl9 zYTO9&|7|t-Hhe^0(4+%kk$jDKJIA4(sbC9I52d3aAQY8|HJ3uv>ZusA-y!(sBDKE3 z>5zqFwf^omH6{T&N(*48;uRL-wy4^Rc+%7gvl^J+%F0PyDBM`U{W^&ktLe?$_*a{0Bde84Ncf3pfefCV9QNQd#FdIFH)b|Z1gE&Ad+Gr;0 zeB14`Z9mDycJVLXLYS*xGS5r>CDxArH-|H$&Cp^Hrw8bjS>b{t>$bIGB6Xjns;T;w z)}g^%qv1q>?L+zevy75`QmBdB}SMgQ*+9bk)AscKzYxl23FJ|7N@3^r;d=)hB| z7oe>5Ug*TbAssG-#$^`SirmkuUvx5`A7Wk1R(z@J0DE5F;=hqkSix{hP%GX-$ZfRGC~QBCYK_~Z8e++qs!mS_oN-=#h#HHD zNvZnPFMk~xSBF?}ArRtqU8-8c(z2mgFaEDAaA?)|IMLgx`F0lAhu666%kRwp62$P8 z)F|6@0Y0wR0+M`LESrSuL3>k_zNf{{&M5oR7oN^lFCjrx0kK9J58%xfNaVT5z{j^< z>%YnX6c2AIZPix($W(W0Y~)3k^G|cp^C%ZuO(4g{{O6%_qC~tKA~wd5b9Ei#+wt7m z?o)5h>2SCTv$Nq6aYad+c*KgEu=VYkACNT#V>t3)v+kROtD(V}!_5Mmti(Rr$yJ}W zT~ol6cp(eof2{z%dX!yQ=t2CV zKi9A_aMKK#qUMm0>p;|&ttiy1kQDE9oQbuPyn8mPxa=HIuf&AzpMqn#A~z+09s(4A z+wO2hdM&hyO+nx)4#x-lPWytkV>u4jUdky6{0-fJv~t0Z-S)eqDTX!5rL`f$g8%UY ztFs#p@xXhcyLqV5M|vh6DR(XkP0a^)@VEO3bBbubg1RS)&eg8~w-~CJACkzfpaIef z_Uc%1Hqa4q_GwRRW+}h!?Czd;?T>)D!4M9WIz=R+BZT(D_F4V+Be^b3@=(eX;^LUS zY#O4x=jxp(ykVd+wyuQ9s z8*ut~-fz_kr~({f4u`>vx4QiVMlrvJi%;qVLusruAp{9R)%vf_-eD$ha?u#)11Y~M zRn0e|@Bz{iOG8@wNLF3!6mf;W{Z)b(^a3gkq>q?Fb#=)COiqG*ONh&OiQIH+kg(}o z3;!XeWMrQ;FO1{>Y+xRMKaj92jdEUTf6q(Lfe~tZqnkj(rqMXyCt3TbNG{zs&}n2z zifygGlPF*ZeZ#*Pr~@pgUjWo|-Pw3iA<42V0NaPR@$W;YPHTMnIUH~q{6Ixb)C1N- zbah*~iYAgLi0K$)(=F|Da$)hbov_LdAzn%qgLIL*v{k*QW94icoRFk*Hedv20DC}o zepyY>A#^}tUe zj%mwi8Vd5?k3^9E0-oal+MYB{QFHiEe4)rg4(Q z6!Zy%n1&z;-|{`903AI?#DS$HPRCN8ue%K3K9xaIrw2=xi1$%&z8q}7m>`Z-_Z zN4K5MiQ%TRS#}XTBr7iZJh~Y8XNOr44c(3`M1)p5d14M*W){gw02Aq;i8k4VKq~!Q zO(crb3th{erh9NzEP^P3henEkVF2AH+QIJA$^+E+-)o+aLuKXUoXWxPdjnCTAp_;PzG#_QO2KYQ?>O;+W$PW>h)Y!8bTP2=UKC;O|8T8cA)*nz}o@`cubt zDq^8mKuKim@O|}s7)TfQ8tBt**+Ukk>E69v8O|cdW@9t%8^Ii5i>nu-(G(;Yi6bdGP!9N4?@WO~(TCH_O-CxGnE{*DzHJvlr`>M2 zrI<8lAP=&rQGpn9UL~I4HKl4(zDWEQEpEgDdi*XxpgBZmDR7cTu z1@HmJ%El%eIb=V_W9~Awx2sM_UTDN;uxC7eLiZ9(UQUY;p>0{yw8X+zoa`b8Cy1$P zaQ3S9?H`kMpNvKd&li&ZZs3MO+Sm z=->BC#2)l^aSjLz(*>RecBn(z0Y{1{&2kA&jgN^C3)lJ%J^^M0xwu2ExvT>)i12A?nu zfoUAASmt3=)k5awuMjIq@L^P9%YKXSKJ`1-@@gURazVhvMdGpVi7VhR9z3gBib*sd zm;K2GuA9(hG||0w zfTkdxZ zND_Z!zSldB)>@5al z_o=EpTf_HEVuT<>vNaZGtDDfW-^3NNc%;(0^IV~~A@UGY>t1yE)M{|ez92RJ+*_p2uoN@@4YR3s9=-mqI7?mcKAy?8Rh^-IfqNI<( zuYobdE43&tC!b)>jlxJx(m3U>wB}^R{|$Lp)rWo76jD!&R(>y+!Jd|(v|h`dDAbuL zJibvaBf4Qs(&YJ>|9ydA9Qo;qckbh)34yt?fvbq>o@sCRSb`h~^>n(V_IUb``joK$ zKj1abuDR;S>*VPoMz!kpRCAGkvk8B9j!|H8*EZ^`FQ@Ul=Q|#D{k99_e8QL?U~S&P z{83qYc62j!jTOTkcq$;WE*p#ILcYhl8aDDt4(AtMhtSxAD%;X6o62M{8BN1H^H7#JR0 zO&$+k%`h46NzKnyx~b!B6?mzc08)=SMNoqVhO3c#IxR{!A3)Z8|75rurpf~a-)Floy){+1-)3+z2)RXJX}VTwXR(bn%kCxpkT!-xc3?aU+LD_4j~gTO zx!ezBAF}!yC59@_vU_zB*EA_o5v$3-r9-;he(uz{C-+qI=o^Pco5jW^>E)7or%i8eQs5)STQbyM@(f<#hCM;%i$}W;Xrg zGh|}*&=00#kj&HOaL7J$;ofb8s`eWJ$@riYWCG{P5|#0mm6H#Q9>cc|oVr^6bY?oq zjfD1azf)02&(4mfUFdS$e-ClBwLZn3^#`rHJ*M&Kd&6?ya*=v)1W&+W06_WQRP~;Z z1#+()fleJJA>H_(!>mG;po-{whzm^-9>XBMcCye*Kc7Hy5R!e^ao@1; zgMg$Mbdx7ycT%f%A4i#PbiWzAKa--<65>pW|M&qiDRlazNtvk~hMvOp>8Ck5EdcV% zFJMSkvN2uqK-~VzE+4Ml5}#sqVsxl=@6*pR{LtDWPx(2t4sU8 z4{1WCu-aNLx#P1Ws&@Z3qdF8Xz%4mhX$z3yA^^fu(($(Ceg`XYU&N(`tgym1XX?f08Ykr0 zn92{gr#5gdX`qe`T>|q?_A@9f-MAwfFN*JLv`eG!PA0o%orl9I+(yvY?uIC)R} zZ%TsSa2o&~KNoEtk+KUu_QoFx*VNPiq`2U~@UU~qYIdE0fvnuQtGAjRy2e&`fuA1D z-7D%M4rgg{);2aK7NI`W?Ej9mX@H5%HTZa5=G)Ta#gpH`NdZJ8_~a%>LT2a&KYaNc zj;sA}P#x-Z#=_zv$)zgaEOu@`lG``5pWuvMmL!t+X`H^NDDWFN^(O6t@Z_&y>7&9tp8l8odb6tk4W-lk-2%)Ya9oH^@xgt9J`xtHldr*nu?PV#pe!P*ZU$>r$kDh8%lfTJ5IBewo)o0nYONHSE}oJ1gI)aLL6 zZBX+L1SgCCcj7PMqzk%-0I7NMiJJ>%^1!#^uAGWiG+C)MS`Btc zSW49r>(Wo#LBUVB5K(G~t>?FLT#ZW^@Z9B5onukDzTugha&+XG1#-m;%_pu#HRr`u z?{wcY3KR=l;~^8lFfHnV0y@bz^h9lmAGTMxoROp;@)3)?QL*Jfn;yLbo$MVpbp_yo z);Ok>t9JUd%AbQ(g;Pv3nLDbuhaTwdjoNzdaNgtD0Bv^)cAmC1MU~!b$M}c{i}hBM z6KH_>qRqF*u_hJ<$Lq|#3?nW}V<&78u^VVV z6LruVapOtVbLsx?_QClT)Hr#q8kYLxZBO1_QK$Z24JKy%PiZgbH2+`4KwI>xz1f48 z2;SbmXEwZ243y|Igx z`%G26{XgmS_t}c&4_tCufIvb;x#8NcuJ`9ImyzYt+P_BpwUlLVYyJ7r-FA`%ktJYd zec2X$q|^1zNNL&7CByn-b1MGd%T%HOMQVC|g(v=qE|I5g@q)8G<9U|W{_69uPT#BE zKzaUFoJ`~?HK+b+{Pm@(46V4%uWpvvTotYPWxH^x!H?a5TI`2ah%~)1RaCmlZMBK3 z{$)l?*|id9z{lY}P{Q)Zp_4%p6NP<^*OX!-YIP+$XG5T-E$jU6ghZ_OWW!RZ-*%>UUC$5Uh7cZ-x zNqj5mT1`p-9bywUTho#Bbt zOVRnp3J2t2xlYwTo{rvn{KvfV|NZp{ZAaJ&b6q~6=z;S{L03V(Yr5;xG;(8=p zO~r|XGI4uU+{LQ#)VN27J&qU4O+Wr}{EJg6AG+pJeQ2FK(MDYYzq$X(E2G=;wE&Ny z2&mXI`FS0KVTCb5vW7HnoQ);@*6_!Rj2yO>^7?OaJ)Wty=t=`5kH-;c0_}3ad-(lya-DedJJ#TT&xwDXS&_#nH^)^yxyQi|9y~3x&$!LMK4^Di=+d+2(J-QF zmQGyWCD#hh;7M!E(hM=%V@pXF$(9`gOM!={*_||vVV{C+l`^n#fw2}-15)E*PNClS z1gf&kmKfo;&zwz|t*_t|qGy{waL)gR{ZUc20oV&9b^GLLk({-`GkO{8gY@^KncK#i z#xJx?pp$2{%-+qm@OBAM`t1vsTDDB#wHZI|e@>|9e*5NdqIB=q!m~iR&!_Ug&2=cA zn7LrX^fLEqrH}N33Chh)N|JCljkMdwdYx;ReeQG2+e9)qQ(CWjkK7gtt1HfZeo&RH z8ZvvF=w3~6x@sk5Yg;=o!&w`>9pE0@ecT_SJ-(y->GOK}bah5=DX8QjxY+#y#Daro zg303&@2xjmeZd*BG`i&T!zyZ>c=CvJ(w}ceS^{stS9=07B02?64rDO? zCKPZ+~39 zuR`g2nH=zx8&9=<20m^8Z+zsEYJ5%H*=SEj(CpE#6T~zU+ggs7q!gOYfGO|5@uFG{ zpQ|v>nSX%iu8Dq2tqWc=lcy8lVA(u^hVF-hlMAU$@YHHBmae3PM68A5k;GaM#R(6k zuT9-Nnmo>?b^!unE}9f0aA^sbu!lmN!eL@)$_HQiD~*fC4ib@z zw!DGMnVRpu(cdVQbvaS=Ts%@Ds^hi5<(W!u_1i|>e)rDYh7lTQbO9F zx`3|wcj9zXs2R86@qgs-|9ApznZ$!o#mGV3Qn&a=SI*Ee!3#G3ce?@KIP~=Pu;khn z6UD&tmZ(Os+M^1~WS*eQojK>_4BqEhJae(P&;f5JDt%es-!g<*12(dj&4)$?sUQxv z>k`cU%G>nax0*j^+}=dVlKj~ZBtP^P30{3N^ImhV-Qb08lZ3yZwAF`#9fOl^(*+G- z(qG|?d9*VbNtZ660??EPkN<4Ds3aEQhbgnjJN}ikxA@MgJ zH=9hYWyOi2ofIigWURn-QOG2+YWcU`|5u`{R1Dmhj^QIBlcO#GY6(zGqJ<@|k;2@nc?0l-?m$hpO1#_W&V;FhWuV%j&>W6@qN zDiYKgj;1*QDm*`OnU{JpOkr9)@bdwQc^~`xk{jz3Ro6Y3euD>>A+T1sAm&L2ek%r=uUz1GFmGZoQ zgD%#veh3P*i@WuEv+j=3jbCg3eq+F_F)C9(UVQ41tTe2+u%^y;s?Uax$%$SYbCPvQ zQug_Xv_WaFE2r)Z1g45JJIm`mJJo2szmxdehng$Fl^;K4_x{p+7vY0CwrKyjF`7r%CH`!ySjS~+#oz(nz z0Vse|%%r}zq+`DuVa5mQMQ@LSB>#>E@(F#O@e{mKV_LU!cUnACTkj>a`?Vhr29_SF zDExU<@=E^ug$F&YGai$1IOd(ij)bMZ^33i%EeWw3gK$libA)ojf zWX1*=%ln=bJ-q5-9$b1f{{v0D7MXfQ_^BZ%og}Nl_{3kiR}lBtY}UH{QLvhBnn92} z{a5eBhs_git#;Peo9-GRb>*$^_I?<+3a|sg*w=WtPc5PBFXRH^W=seH_7u(;O3|Wmf9!DyTrR7T;7|Hz_9{!f0Fy1CF zsy`MK7gY{obz=!T$p(^W3;e{3&2c zlfo*E%1`4fBzAuTT99dN(t$pdvq#wHE*mt)%Uad&^0Z{D(iHbYTt+pn_BV)lPKyy z0APWY?*9et|AYBY@{vz=nbAR&%t*k0yF=43o0KrhmGJ#O`Y zpUh!K6;dXVLP%($!={sm$93jnORKo@?S~@_;Hm2J>&(9`FK)fN#ZFm>`Sj{bsd7o- zpTS!CbOwJ0=G%c3dl|n^)|^Y9;0%u}xqB?5GFeaLnfAXo+|6V263>}+|I@R!`<8)R zrp}v`rTBP#KW&~rUF0Lmw*TakwDg(x6Q-Eu%zY3 zPt_ZiA2;s)R*UbV3YW>aw2c_^{dG1dzB+FFK9o)zF>h+}GFCn?&}u~Mtek6>?Z;Qn z`QMC`Od+?=E8f=X8sc!5sZ3w@w;U-D`n^`Vz%tVm=aW-edCO$VNN@hx!mrunac~v& znNn}6^CtzhFG3D(Mh~)DE$+;xwRVvp56V&*7)}w|7 zZr!jSA9G-MVqPWrbYr|XC<|5uA8#y{>wkN0_spBE=ESqY^~SGmSQ-3-)>)Zot~n^Y zysj$7X0KnLX4(wS(21NdQGH~;mvY%4DRWy=OM07o%;sb^_1V`dIhWtZ*KGe0&qTk2 zj72)+fa`-YA;MJW%b$fW=G6K%ZT9K94>b{gj+$xSbUp2u>U!zx-Et|t^{v$BGryUc z@}JxZ?31URzYSiG6YIJA7nxWrp;+qKeQ&Ds{XB|QTwJyFBthR+=Q&;N9Xm|7R z<%UlzfsPqW6Th=PVRyM1HB|i$9{otDZX_-~g?AY|y7yYbKzBx@o3U&vmUtgVm(MbI z#^?pvy*mS|C->#6RjN$>)l5`WZ?mNR7nQ6tgF+AL;tTzib=7h%Ba~Y+-#I>O+7Z6? z@V#>8WBUT(+wrnTiFr09{-!*FP4D{KJ#Vo}9!ZMHB+^IR|GH*cp&V~GH3r}VWI}fYcw3#b2h_hmIh{FVUGK2`6?uP+D6A)+U zDfk3|B~7*Gh=qoAaV4A-(I^|Cs6wh#JHDrSWfXP1deC2x#CR%(suIQEEWk(SLl=Kp zJKAvZ=na$g-u{Q8WI;`_B&?xsJOH_9=c&hs5TxR9jsGEw-OJIi|Gs;vSGWr31+tHQ z0Eld^&0KWEd^80@Jc7`?$qW4LiTN$2$kA2Co%tW^;3Ergsx^G|aCCp1KP@XDOdtO8 z&(upGc3BIyA`N5Nk3^##T^C$sE6|C%S`C2a8M1PaG~5%mQ4l?DI+x^~tL79OEJUFy z>jwABg-&UqQwVJ|Mqp;7*0y>#;LH(1VA;VbhYF$V(hV={E$%KAwo*2*V1ScK7(XtE z7LdscnA{@b8YowNnzw}T&^U!V|NDCWzq(uU^$3Le&GDez+KCp8v=QO}WRFBm&WEVP z=8_QcPu5ldAh_0Pc=0;tC)VqhDz66U#!5O)0ZZ*|%Fo1N5DwiKnea(@gg(kxLKapD}Jc?L%pY9wFeZ?!<& zwVT~)uVCsQMU|O$N}rR-ef1}*w=P0!^RDzG$2R@vClFiP$CE!zCf*=MIR4s-{;B6K z+Rk~=lDoNoP&QIh#WH*5I=A|h5x=0U45fPJ^4XW5MJCWXg8^BXkZdfK#`o(ZcPZR>d|_v40`N69;vXZHTQpiCc? zyO(di;BTlt68_EhiDd3AoQ02m9@tiH`gl$l^4{yyGW#84$5(mW76glc;z?ghI?0gN zYv|1W+6_~NF`M>_+ibDdzm!K6-v%7LDyi}lrJm+DI8R_t-j3b_yWV$C8aL{8LiUzW zB!0aOCbh&s=a<%$!zw;I-OfBGe`e`&@z|9|#yn%cuQ%GY%I=@M%vWZ;_aptR#NRES z3|><|85Pv}n^laZ5aJS1$!2d4VL*;m1 zu1gE%fK}i;aHtR^$s3SUIT0NPCuB+Gf1P$!u)Vm@Q#10_s@4@}CSXbofSOaAs{njU zqUZwcQ4#>GrxHT<6LjN?OgaVz?s_fO2vSG_?Yp*+G~bg+HRsiM4Fk@rI)Z9TaPm;d zGK7qQCjecplB}F7-xjIoLop3-8E9L%Xx^6g>S&tvhYugHSjn^%Q8->2F!7z5L{Oro zYyJC8{|130Y{n<{s>TFUnnZ2lu>hF%Ix@&NKVh&bYgGlUZX ztuO`z*iFERFc8pkA^M*w7_eb8WwsE!T1+s$_?5<-Vhbzh+=fNeDZ9-_6^MxYnb#b0)v9CpM^p0ml` z2aG~zTk3t0lPS510+WqLo)F|h$U!lP<<7g+_wFMQKzp`8p>^XzAz8p)R0L6g!9}Yevj9w9;cH2PJw*g1hJcZ-EEp zpZDtvk2-gB@iK3Lwx7WGO^fXO{5#cCtDh3)uP~olk{g^C)5zcdO8AM#xF`IqWT zr3Ymd>7#UXQUUIY<<&fU9Fxp97DNve%PRz3wmi%qJ-sfRbWud*ZulHjs^bv`yi->Mc`oloMa|C?Wog!QHW+;4Jqm*k|dk!fkHhOcS}L zvyrX|sRy6mj+y@0ZuNRr;K32klaR{kA5pe$z^?*_B=3b?3zjJF79bBX|5m%cW#k^1 z=wYv{{bwv6awWNyudkr5Vdd+a4E5M&pnkg2P9yUk)>3*?%}(dcP(4wTKmqd~Jh5jb z9trV5*Kdai&A!|!tpBmANnw|2N$0#%|D;fkyU-PVk2eWFFQug|+y#60^@ava+&0U{ zqkF+99beYR@-Zdea?EVY?XdK4GwMwXcVS6bs#2QzMyb>@QDxpO1CKvPzp32Zmj&-( zdQ-HrJ_op^e_zuOk|OuTl(_lne=6T>`M_qAz2~wzf1$kmQss*7Sl(hqLcv+R2kvpd zK>er>|LeBWK39fGEI&)!_HJ1D2H)n#rKd%Tf}kmp(K{DJNcB*wS*yTj3=`ws}* zo=bvcmyy)NpYeOYC4msjcw1e6cIvkMeh-LtqUHartZQKB?!cgL_oBTPE+=atRxNV; zd#&4kuqHM9Ht$xI2Aqh3zbYinKnxJppL240703f~xOKHwd1hB=SDG8>#=c=HAk0!` zX&pf4#s2b0ZDL%?oRY|p(?)83<4Q&Vws~qkS=cv})76y(942k=e!UO{Hkupr!2sNy zZa#7f@a41Wz6KHkC`9rG7>M}da&-3-u@Z4S1sX+KeRNo2R@I?vY|iIoWxYC%rtD+( ziK+V?6H|a2>O}B6>`tHC+L&Xa)?O$ZoN&^DEgOyyDE;kA(qp zyd$!Ufp2|XQAF0M8I3AQ5%!;TZ^8AdMX);%;;o%wFwKQiopXknE`b=E zfDtxY`L!g~qDvi}3?hZMM%gr>;$Xy}#1-D+9zng!eqXVrSsrU@zQUppA@ho*;)xIM zRoXZfkE{2uS7e(;I!A3=@2Z}C8g$=4L+LJu$Ovz;vuNRSjIvz*;2MK6D9lgdN8{<&$!qhvdAVCd^umWf&6^$y%G(b( zg5DOHYJ9!%oMdL6jsh_}!e%?}MhQavIgCGDTz$r7ulYEAOrS=HGHWyv$D@()Fsngv zcc9?-SEVh_AMKjgPn~9R-k<)dI^TkL^lZUK%S`h&vq}7Un#5&{H0p`2z2TFo@-f2< z`{>80tIhaomdRBA$Ql0%zDdq(UE`x70XOu^qx_uBhtuy>h8WL()=U&H?#whxj%b{| zfep&I?vpHIHQmd%_h~KjuDVY8e%O_x%n3i<&6j_!n>UM@EspqHYb<^8mDw!w)x+;+ zXKbEclKIg-a+Uw0NCU$?uMy(MbV z)qlB9jc~WeoYPwWAI9D~s_Fmz9|okA?yijvK|;D?1L=l=l;luSLFtwrqZ>w#h9N^h z6oG-#3>c*X(ke)Y-hA%+{kgw!KIi;?|LwWgIor<8^ST~aJsy|Utd4IBc&##vr-m!F z`UTa9Y`P5K@0KgmjsqkWL`-{O0~Zl%m*vi7Ib){RZ1U~&Bf4Y zT3Ea}I92@hyZ=|S-xZX~>F1B$H~Z7cx3{^Mz2;yg>ZDMvV(sVQ$G*8Q569~w2}L=T zL+St33!?iOy7h9hwD7~982wwgzvXb>PFM%;#JxF0Z{0Rsp|uzIpyuaCl)GnPUp|~m z{Jof$R9+cwx}`uokJz$%wpqnwC%I)4i(CpLIbF~FyV5^=(6yS}eUX}H*N?x{buh4b z%Usu&kSEzHjo=SuxJCKx6G5w{e>plN|8YJc9=`6alV3V`{)MjekKL=?tcc{EA7~OP zt1ldemZ;YT}>nN=M?+W?~hH-E$2u>nHg_~$?AfpDXla^EgtW()21%v`XeB_hEL8U z)ie_H+GRwKjm@04N?A363d|&uB>a^$t|{@=56xNduSZ0l%`vW-0Hfud%lRfncgPeb>2}*9t2I|NEjBy$ zxGU9~F)rY=zk5nr{)ubM-&F=1?gT^^BtB`ZGe=43q+87gryAeSat_KhkQMGZ5Nr|s zn$2^X&UOJ7>aI{iqMpvD`0aK+z`jEsdVx#Y+(cwNE*?N%=Cozow%La~!sK*nrW-?p z{}d~0^AsklHWlWH-=tWsbXr1f-s7AIZJ`Gb=^++U1CSH7%F4H#9c7&X?DbH~T94ED zL2_>*>>^~7Rm7$BCL3(Xf!1lYht&W$S7*r@7P&qd$XB*DTra+>?vnD9rO}X3a(!t1$g{l{Fii#&u5!&izK(`Hz%zllBbF8-9Y#R3(llx*$LkGI zPs_68+sdq;GDI?a|A?7^6Qk+KcP@0aQe4>*leVk*dXz)^rPT= zQNCMzJIPYv;pvHZ`CtsMwJy4mX_48uGUu3QnQ2a@PgjWYYbf|qeJJClR@#!@G~88n zDN1d}*lQd*kfb~KJcy7$o|Q(8fj|%%50LvB%d^u>dsW(BhtV-6V8Tq7gQF5(4{OQH zioYznbfWVpviQRJffy*U2EWLRD^M!Fg}fF@iFX7IntPjU0LB> z(~6=WmgQw@$nQ`*wa+$>j7}xqs-6z2EWeysNu;@@=L~z+{QUU-{VTqVxsX#Dm%+@+ z;b&*Jk_P8f+Q(OBdHMMRX~l{yw@eaqp2ya@g9;V(??H}T|H@L@6PSf~c!64#6NQRD z-cX&b2B>m|1adv8zO=TMi($^a<=Yw-z5H`=J5l!?bv*ZR?yWma)%K(H?=8o1^xr`` zlEd?Lqc86UG)eJp#XJ(~{{`J~96iD1%`o)Gg1^+L(CXsQ+Ww_M>-E-K_YqKf!>)mR zHk9xFcqU%{cBvAB+027f)m8a_?EarX?V(6Q2}*}uxi3~??c~0=g1f-2LmPY{$wA9i~KQ}eP zS&U9MTmcsvA2ycQ%_>BNq&*B^Q+?qG%$@k0&C8HJ_pBejwL*+jT1+_7)Cw5YrO;-!=)$G276$mY=$|jf zKz1%Y6hIF0pp}$$ddUxd?CzosL5+_m&a1qIkp2-5FG{71yr+YZjW392I{B@%Km&+z zrKvbC0dXC$#{h=%luISLGUm2ELr6{UZxUnbumK=G>g`A8xRPB%dLxhRHT~noi#$ljP*%g`o0dQ(C$DkzUEI7RFa8Bf?5y@I+&X3nv85`b z>QeWD)Z{aVkw!tY9_Zmn7d@9*VJkp!Q8EPSV}HcsCd06%ZOAO5iXvW3IMFQuwIy^? z^4&a?n;_PlkR}iy%v8a%)g+aEhG_=(RXm^m7+}C+v(4o1~R~vUFYF;ako2k#cQ{&x>Kc4KE9|P5#%NPH@sK{~~111Em|E@{? z`izvYjJ#Lo=GE(>^GZXq7XPDk+92f()yJt^A0FHtSCWW$CX<5x@$7%hFb@0W+`T#4 z*GyNA5}=}Z_IwH8X$e5m5o`IrJuQUU~TZCaaar}X8?@6yQHX%ISQJIhL zbM(PiRR`05{6S%@74PC`yc~BKg#MD{rnm83*abf(#-!?7MDO$ovY9(Df(x5>U%X@= z%Y=wy-T7`zwaBzrsgU<0`)17-Z!rGZ zrHe9+_S*zcg&Ho`*WB@f2iu}sW)=m_h@|E$OC4fS<89ci$GKZlLfsmp`XXVpK6W*! zCnsLhw$sFla_bvJOTIGJcZ+Ly$K=BeSs~-O{x1mxF(rbPl(8@l0S!=kF|!>N%yZ>5 zwT+D=@M1StbyPFCvLSFBvWs__Zmt#S{#Jl=@+{PJgHP9-`r9zr>yoA+8;aUv9Zw5S7cJ+q!V99V}zzGHwy$6a7e}$Cv zy;--Emru~ytpaa$d1UmbPplJ$4Ywf7M6%11o1GCPqOgYqMSxf>a{`(c_O;(yf48SIC<*C=)ecDDuZA2#vA4RE<+2KK;;H%b&<%iWS!-N)+}HcgeY-Au)fXM% z$Sm_SBQhQx8(9OFe-}_nSO)(!T-|dQjL;J|?WE zEA-Al9p{!D&9WV$N+L6I#!sL5NATSMt+SiqcXbT&3yJ3$!93+^)rJpt=4IOC_z?{g zCYyq@bw`uGWJFBk4@-ro*{39k+}CHQNRlb*AE`{c>>ugJzUGjSuX0muZiT-XA}6(+ znn^LyytDP%_ypIFO~sx;uFwX87~2)gblH_%O?B}xITC~Qb;nvuj;YYEE+tt{r1woZ ztQDub%jX=?6 zs^#DasGdJ(`v#k40^biF74`PD#I#n)_{{r6!8}rENYc56r`9#^nZKu6;y?KP$O}$F zV9#hKl_6T#6cyABfRe&VV{ol0ZFQNg+_CVM=wkx*+3ZY0-f0LuNHO|T?aw6vj_WVL zy`rKaV84*jWuUVB-7kl^2j4F3C!u-gTB%vDCOjo>Ki>D<&+ez5g#d&q7U)8=arleJ}hUDT`zyvjJL_0e_F3ekWWfTwAxO=?awW1Pc~BHR`A!lIPK zwZ>mNplm5e{5&@SM`T4m`CQod|Lw$mHvAVPqHm|Jl&`Tk7Ts_>5=XKB6#)Ey#~N<^ z82r9pb4x8jSCl?WZ8SVs(n}^I;N$4$rWr5cU*aQ~<;!Ivbdw0E-#jXV;gQek@$3lw ziu%B0ejgP7o;CK`Mb}*SudL>I6+c+bXahl(ssLm+^K4?WNuq0Iu;piB0EOA{=_j_q zd_LsS=q>Al#EH7370WJ6R|-*B(cV(coB}D3qes_ze9}-8tzj^O&p4bo)S3A=wy5dP zabhRQK5Ek9$(E7J9yr(oY45p)n_u;{h0%H?;)%>`%7-NU5D~b>s2g@u$)Dnr&uW&* zip9VW)t3gOw68w-RcD1t8y__WV<}gCPijVkA}|s-O2+y6G2=%^*2rilk&jildDN3U$o~FN5l5nDGF*y+EtknOpXpn1E zN^bd36{5enJ9`keWJ1&50Sn&kYy7x4FGNF9d6v~T zVw9ob+iaV7|G71_6C^{@qcGdx3#L9R4A(Q8Fp{uAwNYatHScwAmgH>s34f{(o1V*L zMg7AXX-dc_y5V(TfIpm-rg!o~P8^HD#IgUhj-;_Xrs*n+=tisbyO7lXA9S-uI}$3U zweOzWd}PrjEk9J7`=5oUe_XJ9XQHID-$S)pjjgU5ARy9NNzj!i9xwK_#-OGi{uz5O zp7}L6Las?rGI5*!kX}&z(ldQSyS$@(_57vrwGNQ}-3M|^&WKo5gjC6X>asPO93UcG zn@`T{nbJ5FmZjn3(q&vab~hWcNmgejl`0!W%b^!W?&XQf`m3dLP$foL=e6&iE_0H^ z34N()`KoZkjV(18|5B0UkzxVVDi_p&`%>8X?ghWeuH7iX(CxZUiznLAyUdSLS;y> zWK&=Atx(bvXv;}~X;_`XjD@2xoq6F9P(Qbc_7rbV;dTOZcK6e>=QvK$(U)k3>2kM{3`>+MMPRdxsjrVu7Zgxm{t$d<9;Kg)uS zkGl2j%tVh{ANIDhH^8{#N`7;2+&h1ZXv+jZjl>ROx&FH+8wY)xeWAATkK_CAPtw*q zSY~M~<_uf#$x2MFBnh=B!L<+T>&Rtyv`2D-ny%dN|z)wwWWPlB1Q}&RYhSAO0Qd4s8>sgBQ zh?I>9<#bsWqC==&(bs2Fd2i-PGQZag9qv7t)84KL8tJoYh7wxK=x_h4OT+rRhd3~g zJ3NmtbM6RQ>I&X`B17NRIi8a?gf`H2&6N`WWv*9d=8_T>TJ{(KoLB-zbnG*0zE%cP z6tW`HoB9?8`LP!xWbp-h8dMF%aMYD7ib!bvXclA+()Tw}=b_oq(qk|ci*mxKOa4`m z8GJ4@kiZd)g>AqMcB+D;gMxDHkjsLLSp#0U`@ROIDq!Nc&?WKyjkkFp$O^B}O7mTT zvfoSpuDiL-8-KW;D*GuX=$3Th$G$}mEXFB}`>h-1))0F8*dOw#JGKS#|D?rhO$_7Z%o0dOqAx_=1>28wVpm+V!TA zz)9PEC38l1#KsVhiamfCK^mOFDA27i49f6%)tC&$Vsy&>m|kGq_Phf6cnJ!T_^Gg6 z=^{ix#DTYD(gN&-t1KE`m333F(=kHVuE#zX1@oV{DJCI%$fa{$Q@q9#EE1UTYO^@w zBW%+p_*3rFgpe>aet_TuE&`;Yu3l5yR4@^Q@|3~m050FPbFO*G@Cl4{!ADs`qazOw zn8_aTXlAt)9<`g%7rTSU@?Z6bR(9MwhX@-q$C}`DQ*`HV z+on$oen@ueOLrH);+5$t1hO9a*qTv*ZXC^Yy2kgWJ7hpD8b2sA-sma!yUiR0Yrgr_ z)OvI}$FmCg@-XeYxxA8L0SCucdfKu{%?7=K=}A+pl)j$&)@U)Pps`IZ^EoX9{)I<=*vlHDkH znYzG>M9J!8o*g5Y1mI!0#-tfyREz)VG-||H)rKFKl;NLWpl8==DJ%OWS;FRs3vLyGm;2QqD!KN_75`Q}3u=JBQ?5Q_M zGbij<+%ql1(egmz#8O0$ea*a@XU`N>F4Z2-W4fx^x~9E?gA6HHX2@rkEvb;hheyiJ zYPG`Xzs-ERPFnB~kc-FF@c3`cJG$`y=qUfkn}&DEx96iMVavSwd6CXdh66-GFVs(k z1fxdH;L+23Xn=LpMi9iXs{D?qIWig^g{!`y-p(Exh@}^Db&ww_pJLm~4BfG_Q)T5$ zrNyGWm4#TeU~?Gbhum5g?k|7qC&>z8JOX6pbW(cY`xOK$9IU#2b4UK4PbOaPm08wFP zR6r;wpaCbq!NXOE4`&CAwThQiH1%x|J#a|oNcOF1RhTC~FdGBhqs(<*m^@&f3SUz;e0c5F->$s?Om$Xw>!2#=YQ46<<#Z4_$<<9OskNeTQ z&|hw`Ec}dJH=v%K)7vSb=!|u%Kkavd7;5WND}5ljHiY8{o2C;NE;rKM1F$;2gI4sW z?__Nh$DyWCIju_~6(1tJhrsv5DXfc}i)TIxCa9!7{JKM~C&!6M{*%hk127&!Tl=+X zjJ`{J{qt;l2;3zQC`E!tYB7Fp<@ZfR?_28Y(}uuxz8quz!O1K@t^KOrb&ZtfE&x#G zJy-qlfks!^Ljo2p~J4;i6L5%orQzHZRf zafRlpxE($zm8Rp}4eo~Kr0!5UcDZv!4Qt1DP{NcLVbmP0x&gGpr_V>^%zEoh`QW2{ZhCSJJ$x~B zphbo(NPq#{-yK1jlT+r&Ahev3O~IiK+imxuJU&vpLQF&niaab_cW#u zeJd{87d+L5BJ3~qD(X{DZCt!qQJZ0QIiuZ;ul0GzFFO($N(n*HP6?bk3$>mLJOdlT zQ@ZhY2?UP5XmOcl#~l8)g@qnu36hx(dQ2`O{Fu4488cCB5=$n)pcaV(`kmLbz8i)W zJ{u3KuJ=CLS3r%t>zwZFRKDZ3y@D;=QNOY6>@d@}6i@^uJZaNhoL5J6Ur+lVO(#jc zgUI4o*p6S0)x`D1nxNp+ij#*;C)?{Yxbl;PSK9(CAziZB4wfkMes-V2&D{d?rbL@A zG?Hhgv+)d?@3R-Q{Nm7$6QF7A@Y|;3&YlL3CVX2><+;ou*FzU$#`PlEp6Gq%IUVYk zJw7={_rEUS(WbP9K%{d(DI77DCP!o&xTzcw-29>UJHjx)QU_J;`h_e3sscHE0`}Z zpHqF|^|QxgFi6{niP#i_aLn>|l&72y`l&k^FyGj0D6BTkS!?c>;zYu^Cax9v$t*4- zOG3x1Eb9fSdOtz)T|-7QB2N9!duG_$5wPq%PKo^Cn`e#;0;QRR2>4Y@Nw>waBL|-| z$D(bBxN-GCZ|BGVt4qwY`}5D3+zoJYU`XVaNfHM}sO13wCK3vsc!J#;!ds7tBcqi7 z#=6d8uyG_j=Ha#@IHin03Bfa_YaFRBYlt--5Q;5Z3C$bZI?F8KCBI6QOvW;qhgbX0*$2 z+bSY*HCiF1V+#96iOZ{)3CAs%KQf@j>m5etf2+X`_P@1`4?6X}i#ZOXzQB^vV<|RW zP_Ee1xd8PB`_J0udvntkDrrr!X!{}Y;hNc89N3wq?kD?CQlGU{dFjy4U)MrG#rK;a zNlkzBk5FsYv>k3Z$NE=3OS82$hM}|Wh4t&2y1mC2ePwo)XZk0b71g!09z|4^k;P04 zF~8x$yjRaqH#7X4Pzy`$f7yuSfJufQ|p12P6Q>U8?o1pK^?%Rf*3_aq#(h8#WNk^ zJ+~GX;_0Ieo_}iK<;~{38J*pl($FOs!&*AGTlG|=r%5czZNZa%G`XNF) zHC-A#RlKVN=G-H;!v*sjx83o_6+kMP(nb3s;#4MU`Wnjd=w^BGF_zcFQ*4UMSm zG-UUBJ|I-{*?i)B*3u-&ey^Pk8BwC4^flps6;J=eQMxRW{&LO)@@asAR$A?)!Uk^= z=$Cp*z4{~z#gRmM^)nI%pRx!Wrs}lGFkO`&cgal5|g>Oq(SxNp9!Y)BJe%L0R#Z z(CF*Y)-)S18&UsZk9O`-{?v#M;oN7v;?0FWLrRY{Y-0>qb2$Y*?nt{8>bI^m3kp+M z-_^k~94+hc&HU;ld=ERz9y#5sbPUqCL1~;uJ779P(;GAotePjqfWn~ z1H00#gKm`*u8Rj!++ls7`?U*e9tFmVjSB6}U0J5>5Vd{1(_%cA+m1P%bh(VGh&nDw z40mGbp4*E}6ECcoe!ABvNRmWu2>Ospv*Jts$JuXqst=X9||FTh=GLA?(+QB_B&^z5co=1u;iFGkCs9J8%oA zNe-Y{{}o>_(v8}C5Y;u_3i80-G$;FH zU%T(&I;*;|rgSEgt<}HW9ygQM5oKClW#hxp>>8e-rTP6%~!HfYbg}}MqxLLi7w92Z<*^I6Ikm0Zx%d$uA&|qJt z##Hu?%#f;Id3Pq#&mS4ZrARb#)sZs*X=sR!oCgzbb*M||rZwj-#)&FQHwR8F{0$j( zrB{}J88>*P9{$gg$p0*Y{5!UQ>c>7&_wH#z7PphE~8PK18VT3Gv2fS=zlB!shL=NKt zA9VV{y*wH=)@lF;zOn-yt4%v^>8TAB!?azM0|36q_IsXLi_Vj4|5_y9n3Ry##%sdc z9)r2o3~cbTxiJ+F-tP2hJs|XyP(XZgK4@EKch?2g{Af0B7Mj!Vfr+Jtb>^_{rTlGO zhvjm%KfZ#u!>$E%-LI5#vx)O2Xk_mbwaOm9=CQHEuIy_r)l3b)tT*2irkI_@1XFVW z6G%~p$2Kb*h$h`_BbWBDpt3Mi;jZl?7-l+Rz9BZdwV8ITZe9vH2uL z2j$iuo6Yv|VYlfT>ef_pT!s-Tto+aK*KkKhkSQ5Ip1&zSrQ9}aUA4g@q$>Ea_cu8z)gxi++opK z^7OM~lesZ{j?ePpK=-b`w?*@Awo&F)yyZeO^vOt zr|jo|HAyNyX?eahe`A=p$lWA~Kn>d3j`Gm`I-}N@=RC4?H(I-nY*|1yGEHkEg(sj2 zr!Iyn6M(86Hn`1+$CU_rv=B1C;#RaQ=Cu)RUx!+@pV1 z;VeMSM^qYfhszle%LxD<(JN%!F%J9`X<9`f&DzQ+WG+-t7AGF~Fm`oHL8v7PRe;Z@ zP!{z8VfykS{bNBzok54pzBvTG|C78@&~_*;>Po-bx+?x18MO!Ji+wz5US1O5CU+%F zJJrSq{+a=uh6T%3jE{chK|fM#3fRX~UTeY9-0}#75Dld$r3=;Jsj&Q5A>C zNy3VwIO;}FQRm=!cDA8_hLt1w6Bg5_Oo0pSZ8oy993RaU#NdM01IyO4st`-G7PQUB z&f2I{<$3byCit@G3FQ9$QPo%7mSbCj5uDq5d`s11jL+n7X+!7eeL<7$2mp~b&rMdt zUFSuk=!Lq2&eMf3%jwsj6SAu~j-1RgP76S!wf1Y3pnM#5UJY!Nv|KscuI&GL#!Khy zlO(pMBNtZ9$`3bn_aYwjaVPm_Id zoN5MW+*wAX7oS20hZJD52+*|Oz7g)B3G>|uDthnq}jQo_sIMpfK2R=$i}yK zx`PV}!x!WGldSeYU7N2(c}(vv*%@t5ppqfw$$XURqbqRpa-Y0|r%2$O_e&UrFA&^| zEk1ZWQ45BB7VB_m?2%-&*`*Z#jEVTieG0R6mai4ONMj(run43a_Y}1d%N+&ori&tI zO!|cwMNPdgBnD1`5)-4|)?VE?t4yL*niU;a*T-5>3@pD{QRyd~a&Zgd$h6xzy$=my zyP|Z7!J08_OuXvn1C$bQ7Pyuj0sJeM<`uQ0oLm`t|5o4;*%KaoZInYV_a)Bbsmeyl?XnIGh-PpCxKL9I1qZ^&q z6rwm&XY+38)3cM8>Im~Edj|#C>lMkSkh*l!{GlJ-`>lweMWOa9Ta>G&l?~Y*oBEP_ z5T>V7^FjT@=KVSjl>N`8HaaJGU(Iw{_T`5k5(%5*W3^#+{Ajm72ZsbXVvF~i49=X+ zv~_+i^#v)0WFM!$AGJ~JSOx7*x=kK*XCCN3C2`Gvt*Vb`hJ`OBbXts=f0-ct^C|L@ z9WGliTXW$vB^$Zv<%@()p0F-f*q>{(F&A^qD+{;AxG{^z$#?9yuWY{b!(0LY4@WuuaTt zSIMFl_YgXJx&81hVR=4OO6I#?wTfPDJoXK^ts=p984rXj9}QxY!Z>UMXYT!O%^4<{ zjN%zisFn}*{ctzhcDkQN-bt4!cmz1u(Lo%2te*XC4bSks@b4Alk^>sYvHP|SF=5Mj zG(N?(UDuCVNVrGS>-&@-!;IqW|RQT`X;{MV5ZkDNDyo8_4r3RumZ z%I@v7F6d7hXQ?(oc1)`do7{B0Q#a{PqjoRK!5rXa7U9aCE5hsi8o;f*3sl})$R$SF z31M#%goo=l{9HW`AB}0F3`pwqo>%C}Oot6D@SZrwG>r=5fYD?;_^y;}edVY4G?J#< z{kwg1jTTD@)fbHlk6b>*Nj20IvWvTHA|kG6+sqB2<41~d331~@kDhK_R0Y+g*L?RFq5Pfy z4&xQL=IX0R8ca_i@98o0d1VV7|8_x=*s~$KLf%GJ2yT2SI+t?5)?Er1`$2X3N~*4p zAPY6)3%7W@;sk*>n}^l;nsY(i+8WmyeT8NuAS5#5pzUe1(Cl6Pz-)=!rax6p9leam z!8F)@lGJ#bY4M691^SZJV!l)C3iZj^4SWL+TnI+n3l=khhto%_yGhGfyjH8n92)c= zG#PXqu+~%o=36v}3q^b~N3Jswora1Cv_ThS#fBjJSd5@p?q&MApOT4HfL+*p~?NI{{P z<2kp04|4dC;0-+2wGBeV^z=xTbKg^MgL_oA;`@uIuON-b;P%fZ=KC)|)6k)Lf>6r`ixFXRx7< z;NrO&nb`t=y(g;574Vj>n*zbHLJ`yxL)(MU*s;4Xl>pUse%RNL$jlmOm-wpdBvwpc z@><2nqNHBlnxbO`;?lCdhx_T(&;FiNxnb`Al=&;|l~Lm*MNK-6KXROUgnu{x6eawX z`N_^-K?;$Tmg-+kh(2xLQSAbUMp{`-Yoa~Q8yU5|WLRw@=8Pv73NUt>vd##n-F;^-wKqNiy2D z%>qNkGR6(Ul)HLfO{p3!xAj|SE`U5BtZifqVlng+mlkau<9BkOEcEC_9TU#iEA#Su zxBa|KQuXqv2iMY3HtqX7GLcz7r4peIBBBcUoa4S68zF4GB-%6xlP2?IR_R|cQtuOf zj*60sO&kwY=S1j-_+09HoSBzIXd_3Z;0}xS{5i5@^KuHsHH-*;%pzM>c_WD4Zy9-O zjm8CLKX0{HM81XcxuMuO)~WdqJGVRAP|Ym{ids6O&>R1jaM~3}F#J+DTP)Wk*u&15 zr}bUZxu&cB`kb}mT~WpP1AVb-(3t z>dZfNk5j&2kPb#b$*ilgcx$9HRv$-$PRLT_T( z@=nV~QBx9IPiTY@N2KI|q;(qgb5GYtCGa&@quwntXndKZ*omo(bpFF*RrO%c`#er0 zg^U!bOX81+ym!Unv1;|fdsctxU3MdYPAsm{ILde#tfRG>;aFO1?wC*KU7T+|f$(R1 z02SJpEiWc{j)jLsKQ^~>LNo+5}_7X6Khm}ka~X`{IMZVw(4 z@r6KfKqoN2=`g_uR_EbMEIG(pcQEP3(d?Qa(_$}8lZ9&k{+t2j%I|8f2&w6r{q4Nv z0ar=Q7P(hvA|AFKW*494h7X*P;b#sDjiAR8Lkl}sMt7tJ(#9^kl416_9N3a+R28CM z3p9>1`BvBR0jqfeX`36L0f-J$iph;q4&)>w#dOuXI@NPMykn{?EMtZy_s#cuvQC9rRA`g zkK^cnn@jNFfLSz{GuE}I@2M#FL5~)10f=N0#z2v^_8_|o{iIR)4%T^z*qm##J|i$O z4&%P-q*fMzJ3FiTp=;dcYBPceqV~oMD)uq@26Mf0jfY<*aH|S8Kf<7#MOv=b=I+$LUb6I|+979H1ct@F z{>}$kNgkio*{>#>7O(6!Z}VKfzK3v*_{Ge6LvorwlKhty-9|OtcdoMtuiLKLuX*kz z0d=!?*0mfCifmJl*9*wM;y*qFpAB57=(xIAn84EBg=e@c79I0-8YM*A-u1boocmQ0 zur~G!qywKkfAK@)N8%d6`ND$)E+eIj%8J(S?g4OP9dvEwqq@c8q-XDkx21%N=Zmxb z+uSE?!ZuQl0>AFKHvE@?{8wc4-%LsAQR1~-J-r7$1KvBDJHkn^^og;Q40>}JuI$~i zL^OxZ!~7fyPVriMM6EG_gh|PnUReGp%46?0w8!s=wd!t{mWtd=d#zj9)^)k&|M>_p z5vz5*TOqKM<9KC(|1zkp;Hm{=YJI=X_@0 zUAZ7?`#A&s?5~)%%+dDfX?8QQZ0iVIWCN}3dvHQLMK{u*{p{UVruhCtey3Q42b6qu z!4^uuvT%^1WIbJ6r*b2OneW+MERgm03cIl_n6;986_`pFJDK+hNq1jf(zwt;3JLPTiY^4 zv7=u#u(&6PbxH3UR8Wa~`RMna6qXG`59XmhFTE=pyUuvGJzSO=;fBhn83G8j^C#Ms zP$YL_aUk=~1>dI|CubPv{vSY&r_Zp!bWW1T6OAwL@w{l?$u(HI<_8|D@N8nS`y*t^ z1j6Y4B(;wq2Ei%N`Tzl5{asji^i$OKpZu;nEATkua@_(GziHySc0E5lK^x+wXu)9y zw>_jGcP{nOjkT7e9jv|}KQ^i*Sot}>n2wJRztY6O?$TUv7|XfvY+R5P z{fe7sIT5G}MHd~G|gvm34Pk2 zr+{|QzTKyoJm+%OjdU^#cq4EPQ+aLebke2`TAi$&CnB=>Kc)70%=mvSjPs1~uCkX$ zzGXxb8G_#d!LLz#=CPW4$)oZww4>=`t@%h$3sw}dtcfyJH>8qBsKgBk>KUuuoqp?p zNTWwL`oD;XaweM}2yjRlLj50wp5%2rjm2wIAEL{M(_#{ojZvzKhMF=*dLeI|r-D+0 z+4ao5^xc4C7^Y3jOS$5ugOW73&Z4&W;5U^69kE%jGQplcb&y-d?DNW!2+w6~(81CX znMWfmJ}uh4JDdo@P|ityBZz38GF?G-K-i$rGP?9SKEeA2AKZ~cW6ohXxF@P$@M)!I z7c`d|a!No{sf+w5oo-p~v-f;CgEY-jyZ=~eRZEO{H`4a+s2M+wqk2W zE5=8pWvBqs*}Qx=!Om-ia7A3^6ZJbIX}k@$tcHu0 z)k~NLNpGd{s4NPvi?mRC%vqefh4jg+$uUMOP6h3Id$n=X?eItiN>= zk$NCcw!8MPvdq7Szfguz=0Et=nnyhAkz7!cY;*j8E1$1+%Lv3ZAtzP{izq;TmsB8| zXo}?m4o%$3Z~3%z;1$dP*wVN-r*JBzWy*Pkb z2c5+lL?T&S1haTb!2yi>Z}9P-Fy*%>zfM`bF!?vnS8K9=FB@(*WxtOEP^*8V0))hA zkr}5Dk`WPJ5-lGyJn6L9l)7-UBDc(Ne8`Y}-GJs5;n5ZH)cs?z01yW$pId-f1jsNF zWeIBPYoWPustboG2LcrK*Rsba`wYO|9pfmo7+BvLw6}HB~X?vg+6kS|+rT zy)>b4eK0o`xJw?BGlRCh3|h0b?AI-x)pjBmQp#}E59d2E=iQ`*!wCxMQN2UpP#goyJs zl(RLpMCW2k=zH2y+o*imHi&I-+m|_Jh3Hv|?56RNKH8b^X<`46RVsW^3-PDR>9q&c z^*&vzh9^f}rqt3p5e)#+*3ONXnx_^oERrFnC>h=pu$(lMrNsyD8 z#eE5xzxo1@F$O7g_+jzPswtBYioBcY9!R?&`ZR5W%+BfU2Ow!^a#3DQYuX#g%o)Ve zIHcXO%g1aQVF1;a7SZ+a?IeUhJyC*DKDgR{v;TXUyGB7nA*WO8H0$m2mnk0UycxUF z5th%zbALBX!@ehvUj>ePis;>bpXE6AP#+h4lkek0NLG(hDiMbzxxNr?mueUgR94S7 z_E-0esPa&*QaR~^d+vvm$*fR7SPy$q>JHPob&Z{!<~3vbvfJT7j+Zi&O9^EM)0b|J zqe!mCqHwNZfWPHmKrm9Ka{pm|;$Lv^LYU}>wJKC)-fY??G94x^^iyMxK^|JTzc(+4 z{%$k6u`>ER+a-kqoeFIp0|)1BbaFt##D*y${F|XmxT=H6#GivIOZ}^i7k&$e$&f1l zM|Be3KLHDU;DtWRLXZ!$HU?$7=o)BK;Xc%7c?as}K$iKyCf=i2!qbw~jjrH9)QFTv z6D8P3%`KItPR$4HFn0#|j0}oOo@IlAzI57OrIG%iL>k%A$g^ms6kXq{6${$;{-%$m zEYBdxt{jE#KmhK{y7YTcSq3kjf$jU9S5;pRwm^y- zZ7XdLP9*FenRAxrJN@*(OSIv>)?orr$0PRQLV$12HH{mCQL~m3uFTYTCIjJ= z#*I;z11t-=Me3@k4uO4TegyvWQ$@tZ&lFrtlEl`fl6x*e3qOy&i(?|*=!WRFlDOU( zm7Y%}eAi!*i?Ikh(io*&vTQn6L$PZ;+wh3Nq2Bl-MJCm(fj?FF{s!Q*E(&*ROQ47hJ^71-X*-KNTj!VC#}b#;*v`k*t0C1+YWt5 zZ@z-jOLoOdShe@%qNO~dJSsqovq?3nmN{=y(R7_^b!@rIExz5V@XWtZMAom2a`DgN zf@`<_DEt$jMN62UT3S3={r$1zQS%TXvR(7^yAKRk{;>*sfq#|`0>>+#t3ynS5=J_B zZ^T`!9uP3`Tq*hKU{)mCMzgpY3qS8f==poE**E~&fq%;8E!mEn z4)v@5EP@>>EFV3oO|{iKd(fJk^3KS0$p5IZB?jxMiIf4eZXC!~$ks*FmRKETM*V`t zxduByQ!>ux!k=NvIb1!stT%sJQvN_WUKiePE8+w+nl1FZCgk&lk_lTRFB-ZU8v3%{ ztJj1YRyxug7s^9r`Ffo)G4r**q&Y|2?M;rG*yVk)y>0tFGsC`RI$-!_vlUYtA z^qGN|;$6yLs}_^#)^q29a?V>TmQz4jVUves$SG2wL`s0e3WE<%kA45C$0ZGONb!>+ z`pdGwl=QP8U3(MdX+uQDsX~1uRIKr5fSj=_vO36_tPF?3@VyiDW+eiQmVa<*Y&OnO zG|_n#>^~yB^FJeNmU8dk;T+}N2j&Au!M=h*_N0KOE}TWW09bOQ8n0FIf2!N0@~Z3< z3y^G4V5x%zV+Q3x;^_x$m@zM{xRF9fgT4F3B;0wU_}(sf&I}tp9}Ej#vde+c>&C)g z2-#1ajOvdc^!m!MsDgodtlm``9K{=I-9HZBq{bE_+?(Fse=tuBD%TiH;C}ey!A;f8 z6`8M-jxjVy>iS4{PrVqmoGY{+{6+X}va5O^Xm)^8l&sm!SwmA2=B=*dtmRaC(`hE$ zIepN!=%c+DeQQb8u=f>3Q?l-71v63EHT{9Tia{u-+#pG@+3;biU#I(bC`r{GM@Drp z@|{t`mkVaQFmyXPoxS~$%)>l8gC8mFwXRM3{0Gkxo>+;hJ|<%Hnb7j`zg9~`ELW;| z!9@+jdzvdLZ`vufQFKtX4pPrzx;xYVmTmPW=);&XZyQ(M{X9T*Xw~p2m?cxgFrfe4 zbhyIy8Y8HoPrn))rU!?9Xp+UL0(vL*or8cA|iIz&Jd8q%%~U^f4H zAJ|GZAQ{JQlB|gtUXlU;m*}$%0pgWtrOsAJgn}7o+0RJOS(9Jy180Vr1&ibvB#S02 zJ>vmb(*L6aej#?8evf6Z)$fHd^d8BLk5{A{89(zjOeK7lv;UeG6@u7l+POOH=^EpoluwYp26CR` zfpY{e@itg)tH8Ex7SxM#lKxkJVCY>8gv)_AA_To)s z9cIc#9QGd5)%}dHB;f{qQp|2s9H{+JwgXgu(>zVMqR0i|NgU#@u>GqK5M39 zk!%cNxC%nST>lf?de80-{5SEPKyLn)ZNK+lZb*w6{lIFd_(^}}?5CoKGTby8IW6Lp zr&5}+ft_G>am7MiL8&c^vd#CRj~|#09xA{-W+(b}i%EoL@X88S*9xmpT0*J^$kE^> z?oG3%IdwRjLfb{atqE|?K;fGb;;5lv|7qrjUhXVVUK_c$O$WW)ZpxSdfj(*VZS&>K zs;_#-&C{BP%dt6h1B9ctgvK(_+mqZTlFoL|4R$i6Klm1seI-?6(T{>4UW*ucL!*e1 z?#`Ud{s{bWzWKSinkexI{rbMrxvWF;M|h?q3kkZd@C4X)!8=dxkhe-7LRm&g6egjf z_S%D!Iu~tD+Wlb|X2`g16QQIX;Ths;7VJbvL1(8RspC|6%(40m3#@HakN(oJr1(9n z7!DOn-&a1Si#$r}=t$Ou9LcCVgwYh$s|@2Np8s5tzX6}m!>eWRU{Gg^n7pL2E|fjP zSq-jrZtVD&F5`V|!m%Vpa~9&#ODNfvzqK0kexcE=CGaG$UO(V7#dpV8D2<=lHK@B| zILR=1c;(cyzkb5@aitMHG}0Qw-2h-czJ=t|m};%AyScSyM5_rcIz)egIXI|^XvcNf zuo&m5`?dY4`yk}GMK58CVq0weY6<|QM!eyE6Ly|z^%>Oj!v6>4(+et5-@GVU5YECoifYcDy8A20=0-Z$>i(fPaFatF;AokJ*+74|@ zkp`BltR5;gzb?z&Dn!Re1o2?nx9ZY^aBZ;^l@sfss&Yj+y?AwG-;Bt5@puDF{inFR za`~Y~>ID5<{+RYOi?bPwgX+56Q@gHL7gg>T>(x*mrkjJ=quKtA`dj5PqqpoJJ$o50 z1L&Xor7pWfw$rxHEx(MJ*<)_2a`Aqke=1Bz6)2Q~$V(ewk+|f>wXtfUD!7_HeU(pC z6X|_wpqYoJW>gv3s~@2>3pT+Xj8d~-D7635zYqGAXlGK}!Y7&rX|D@GMQh)bFHozhbjIs1G+ z^!Y5g?L&ZFV992BM}0ng#HAegSv;RnlZH4!;Z&(Z{dt4d%RQdPtW{{KuG849$DGS; z6$T-g{^x?9*fDNxqOZ4}vgheXC8_|{%$uw>S`0EZO%?34e-ruxPN{wX*+@^b7<^S@ z+@JpdYI~fjL|fMslD41TU(*i^dwdfbCg=krQ8O>y?^c^@{N1=;lqvmflOEQO@s2Kq zo?DnKZOukP>)8w5b61(hI#dIr8w^4>*uI6E5Rv2uRz=mupSd#hh|$1?D_cPh%ULMN9B9@m`oXv3D#TN}4#xCET8L!*=T#)G4H!g)sc?RZk9x;8Rq&!PX=tnzWG%Hy)B z5F(9c*om#w{XqO}$dD*n_vUP$l0OIp5jtj*;a4d3kHPoYUSq_ivp_;Hkiy$dRxQP- z5(9-9%#;6Z^RSIhuTz5BC>LWLeF;dU1B1TG{iQ|nQ4K_0_R;*KON)}?nsE(R9n7g6 z$<*)qmZOj(%F*0WD7?`t{q1=DjNrsa?}ba(p32jX(8`UKO5s6Ad2;*D%4+^G*Is5{f~%m37+X>=Ln3UPM9kyr%7S zRPjpv3w`k#=y1c0lM+XddNRVIIVu^mcpj?13&P^>Wb`jR-*8ANnsF-Ud|o{<-Oz6< zGyV@8DLA2mOKaR~U^4hgYymI|lk#ma3?M!IpJnU+9h-mKn06PQwYOLNX-6H%ilUF@ z|OnW`+JBX#7kqixGW@NnN*J9F%mQ(kq_l%>I%W>sHN*q29snN20$ zPFcTjY74YdT;uy%A@MMN1P zQZFgIOoRQGf|_D9*R|d1wrstQ=B3rhN`yPwsK&ud2}>feV~S02-q{yFnH`)(p{`Xg zi&cib5qE|e&ILW%P5Cs5TdN1%B8~UTw$aLi1}Uz(nuyZ)LA)sjYIL6kE<%@8d-xTs zkoSmAi-d^p&G=`)RJkx5GjFdrQH|S`M2{9=~_pHYyEbe-Qea6KM7a^eCJe z1LrIIka~XVifAZQ_gl6er7YaC8wsB+KJGQy5qdvntc51E@W3Sp6*JSB`>r`#tLbOi zimS0Yi@XOAtEPimhv1{ zQd_va&-QuOZS9v&!TIE+;IjwgosSi3*Rjc2x&m@GlmNL?OJT{kLu`_2kH$=r*5*!+ z^Br{+G-cjq!5Ruv!81y{jf+wyuCgm1%3|asx~Ntmk#O4>2L9A}6!N_pJBHe$AJjr< zoQp0~AVOQ$?+5#DH0xw%^f*I^f|e1*yaNbSlDQXzKlIG5Fd<9V#t;ZscB;lo7?as1 z8tdDch#Dx7UC3KWsT4PW5eQ07D*R(7=XhN}o&@q!q<6nK9veU%IT-YL`zG;qf|kz8 z_*KRgD;CIAHE~dH`h&OMJQ1tJ`txoL_O(IHd4(JE4@ttOYNxD&@w5yr!FJy;HTIEX zeuA|c7~kq@@6!4H(yQ!2s|zm#EcBrb09t;qcIhtCKYU8w1lziMgP&3#5y%sbGg7XM}GQUB>3;3>qzJ9gxozYCLhyN?z} ziyoC%1-a<=wEqq<^e(yu+J9qPvuS3WsvGfJX-V|T`j4&0zh|L?G2i`~+BD+kCf@EL z{$D<2|4Y2hvdbxsjcUt8^$xr+^K>7XYj}*{fxaN_520a=P6o@B$cnG|TQL!cxDNCuNTE69V|5&TNA;oqx8O3~RpkM%mdNY0JEby!n9WV1% z^Svs%QAeyVt=)vB>pTku@QTEIV|t)aF_HwE!E{e`vQE9s$?74fu7fa1mMq?czIXaLM25iP>e(dCm@SyS8f2 zZ8J$t-g3e3)!&?wi%Q8EWc-=!fNKwkhT`q1J*~*J%KCz>NfdXo!+LXO-3mEil}Y_~ zb$L^b_F4mUmPf0c*BS6Hg7bT;=~_pN)-Z14{~DDzk!3V2(ZJ*6CxmCPzYfV zwd5?OY}f3AqHK7ICpYVAHA%Ac&sw7NZRA9O(8BbT#i74E789zmbe>n}3&U3&m`aNP zkcwC}>hXt%c^R^u5%X~!Nv(n5aLqZ@w%KyveN37f$_*D1yKgj3=7He`2TRzu1G%qL z;tFy!r{m9%?eH@Gx_y?!^rH@EBAxg2y?;g4$o7qd_j8NtCikn+NIt0C`r@0tfz`Bq z^K?F+?2CO(r~ZbwVVgFqDoKd;X|QI09cpT|>m!go0m5TAvC-U%7mkX_F3cTnNc@XI zLicBywS;e3_%NsFV$>rOB*8dc)UN&EItOmw@gK^;|9qcMN+3+@x68lzxE*93Ff4xL zRT>sTO$`la>e9deZfRGel#{a%2Y(+; zY2d=r_r`PmRbH}Eqli??LXlQNbd~#3xR<&6beJ` zI(F3!$2v8`hcoXyW65iY*LYV`p*YZVJ(Hf)Uw2I}a_@Jq~eq zO43d?;`KoLlQoTBr9Nh*Qnjfz!kp`~?IDi(ub$W z*CBa2_6xZV{UM;>9m51hRWW{pwom;LS~YBai( zc;>O2tIxY!)y-H zh`_4-1bdo=#>K*TwiX)%9$DZkuMb=--7zDS`*B#}Q_6NV^^6Cn`|5^`HHfvfd|F^T zg&R(AO}jGj6lrVGbel{y-zm?!^iwR2UelDiGZgYC}$$qnYuvH8)SWA?hD z(?k2B1kIY_V>U~gyG^r+7@#_kTG}&zhLc{;mS}76uCHe7um<`p@8CPm;(^3c?IbeU zq6k#}l@>@u>E){Dz}7DgLY{N2S|ry`ob*EMzceM#dzC`7037A<%|A>Urt>j}rL8#m z68;kH(5DdOpoqCk`U8pqlFUP)uTyS<4Ex&=A+A=iq*Mi44SZk>sF-|JAv^RqC?SSL`51_`EMnIi zk5EdR!gw@%>QU9BbCpW$-e&~u{QlXjwz7oi;1Z*w-od~EGnLFJ`KX5hJcEob0FzKx zd@EF4Z>n`YBcOFNeK+|k^x2g1B=kf)ReTY7d$fGV%dl~#{>_^+Ip6;BQJoFeqinvd ztc9Y7RXbhV7PoIM=_Zd|jA2QF_a0|eC%MW80>_OdP^yjl77@g8owucKiQ8+chLsN7 zD__UEDy(q~<@?tdf|5Bt5u^ID3;v$&kvzlqS9jxPG}Ci9sOPD*%CbX}R13^>ZFWE7^#n@%z~k4P7``!KJ{KxLymCL`!`CVPg%V_im=0yW$GeEaI)( zVndvEL=774cCC=aTtzOL*5R5MwTymMs!^0vJrJPfHacVA2i1E%kFmwGmhR(i z9D1J3|0XNwsXqrB_*Tz#;h1 z6B+<1|G5|Uzd!j~2MkN|d&2HA&)pL6hV%9ck<FB4r1G4$MX)1++{e4nfY9JoNl?A!?Aaa&e?hjH|T@8WS)2mQLor4E<%m0+lLzc zQ{b8ERcUQ85nuQoG-NP3J&A+q{+ES@AB*R@#ES}3q9rCRz&|INN4 zK7tsGDsb-b6HaJ?+Tp&CQ7DwU{4b<~?+w$5AklYq&-4gWJ_8L*pk4YQme9ix9<6bN zJ(E~73cxX@4Q*&nIK*BIV5?i;yl>w)j?om9Va#}>NK}{gK%^R--wNEG10M_C$J|l{ z$sxcyg`A$KV1jiXec0wwkm2j34oDgLT}ZZ;GB*Qa6ONT$l*! z#V~gko)p?u>UEB=u2~NOshKK3^u4S4B0l4Z+IRX#`WOSsXnL?bQQJM7uXaHUcjtzi z#r?O-qLQw4Bep&x?Kx0*l6HsXdUouW|gWym_GW`O&fqj&PooSiytcKO}<@ zdfnWd->1atGgAm4Pq#@|Ko+Cx zmmVwpN`!tGJLyAP*JSE&+4G1uewK@@{mtK+P=pXOFNYxA^)ZKj328}c@Xvj&3G5cb z4-7p~w-9MzVL`w6Lbw`DE;eLh;dmK|vYyH_*%EAD)CdTZnPTnz1l%AQ!{sqJFMk~B zsbCa(U||T9d4JEuzD%Ru%n?6%+iVf~EFV~f&2fsSI{40VY@VLR`>T4j&N?rI`KNM@ zU~`#t-%3z-)+O`fx)s4L^$pM34S4lDHq9?B5%;;ns&|m^t-KPXf_?RO}>bs-ciNd~ezs~%JX3vh`US0=;tLG2tdMkv{G0kYUiIayR%D`}$FA+5g zUqCW&vI2(i@h%GOv!z@{5*VQh(I4mXl3Xq@N)D54(vr}%!H1nGCyvPB7FdiB;Pb6Y zyTD>k++<7$S6E#b5P?wi(RA-CCjR~psfduC)U)ua=yY!;cpvV!+LO-evJt`~`(o7f za>QfW3j@R@)m3R=$0!Qsu{)~~SypNxa+v=12amqd$oEWS+kn(SnwW^)% zWv)?Ty~KWuXN9E@q0#QDLW-{BMbP_Pp7GwITuoVNq#{Q4HlsjHmE$h{Ju3d=E~%sZ@JV7!{1}-1fyce*_uEwW*X*nBnr=f5+|Lj zHwJRbE;D-6DX3aksX;rLr}_cae!4&KR*>zIvrEf(z4=f9OrWh(F}k>QMl038_bZ!Z z8|5C8iQfFo-Kb zrk8$ZX?`m&YHHBYJ{xODg467$CQ-3D?bFqU%6lom^y@P=ctT9gbI~Dplz!rqe3yCp z{Oyqdl|M^w#SW`UbD8}$M%$a7B0VcV8B+^)c^A=`w2q^6F_z!;5!!`zn0tp}y}d>!cmmjZFD0zHZ(@1rr=^{ZHLEXYqwuBp+v` zJXo?>?;Q_HsT#TW$l6rY)}dCo318pO4q!sM1-JbPAV{#=$*>8*Ws$n9s*cDm%eAGbPP^6blYBkqrZNnmP|| zvL(PDnd=X%=>Z{7$`B&|dCmx9)w_9CwU$`pzrL)ci=Z&M$7h|gXv%(iUbT4>RG-nW z&xR!U3QALwHF4S&Lc2PMh8at=XakM)94#XBMJF|CNEg-Nmu2FhG1r7mee{fM#NvJO zHmYN4?!A2o|G4sqUS>JuEMlz+uqD(Btzw@kt9fPpCgEp?6ZzDN3EZLv)s7_=uMKzW{pe|H(77KTW#%mL`)VriVN`~=Oo^+uQf zBZYfOA*+tbP;M`}k9-BRK=eVy0R~O608wVH#|C{jcmJk5@eBZ3?Y}B5~Ht%-t z2^)qNCRUK85Z3B?GV~~88ZN#1HrRXb7fC4M$YYKSoiZ;N8w1F+@BYKW)BjiI$~8qt zwfDSG5WHYgGIsYlj(?DNUkY+HEO(E9dv6ci)1~QQ)lABt5R+$^dCV(ebvTtO&^Vdp zs?igpN1)s**`;O`Q&LpEtz;^w#e{SJ{8iTf4^8Ei$L7b^8pp!J^{tKG4eTx;TC7GG0wNzmf^CW- ze>@|*?i3-*3}FV>R7<&0hl6S+%LdllCN8tK7VcOBTT($xd<0}dyvvl|?n687;^%Ik zM!W}uy^XY``=#e({4tA-tpW=DKlj$cfBx@^a`0+cZ#*P^i6v|Rgyo?ye%#|(rosRT z4D7gvJ&Hf&O(UU3Yy&9WbeVSM;`ZiJxHHFxVy*RKZLsYIm$DLh;L6h6AFtK;tU zX4Fh>+SBG)1xw`nwNEEg$vXrOXHr#x}kx`d^|*OSyKgwX_(aMF@Q8{YUd zxmKA?73+#Dv0GR-yvNr(wG;1@-{;SY#*2!*@|mcz@}|(6Sxs_M!RX2mU)bM_qTK%^ zua!F<5A?AX92NO=iGL<3^e(&!J676WI#L$HvqjzYABp49x!+n@lSSPn$=n5fVhI!=jX1;KJWHc@mqaiIM?-^k zvH<|i6np`yuMDuzlr!WD5J^6Ha>8r)5T!}K63R~lo#h7z(_o}4hTx-rRb$CR3CbnsA7FOBabS)msNWA;~MQREOTBEpB|$BpD;hf**#py=Dii+5^HW+_|T ze{lr_?&gQLK0W9W6px(`!00B{#w2uxBDS&V(uWXf2F#GQ}$ z7F*-hZJ|XPCLKZ#_Ta%2rwx;cF{cx!K%U_3Ch_|31G-$D)fufEdE27=D;x?E;GO{7 zFJ_fB)hj+!C`UeXi3v2J`8Y1dzc4hajw2WcpH{ab^6ifjYXFXg1~%D<~NdD*L{h&*Jb9KXzz<|t&-s{ zBYZqBf*Q&~(2fcS4g=*bb$Rf~uY3w7XKF8vwciYMMJSS)oPslnr_cdSzwEYx!-*Sb z|7kIZDor(9djyg!mliMA)_Zo&@hGhaq0OwNWNHh0^m%W$<=atHG!X%>x#gciK`)a{ zT&Lf}^a#s*B0ki~9Lf%4Q%!ic82ir)5Zaek=W;TY; zb>#~gB%^}NACLhrpHzhdr0nC_m@e&LgP7~0vyNjwjBQmubFb2JerYx@yZ$tZYoqZ@ zB$gSyOba;n245lXgOR^Y@4j^=G-;3)ec$lj+}^62MCEu-CcU|RD)aMw`Li~vXAryQ zZE1;SL-O*(#A5K(X!*H#{#}7I*P+Vg$LxS)4R`whMhyB*FkwI(>d)4WZzBhJ zs*^iJpYC)M5fd0b)!Q`i$112^6QrAhNBal$<4b&7=EOcTFf7zE~h)i?hjX>Q{QzJ65_ehV#Pw=N7 zV?iFX54YB79<%>EWLh!B$;P+$$!w|*o)r6K9Y-A3nCKae9c|o42l8qgu*i`Xz&5Cw7@AoE~bO;9b}D zlg+#|>9Kp87uN?G(?w#XHQ+T7JC(}ttf??_f&-uchj{YoXewC*InlsbdVIR3HtBfcuy^mX?wXgvN%OI`WCsrRY!#i_28Bz|Wp;*tRt#z&!_Cxe!{SAug8Qb$nZgd1TC47S z^+C(gRBP&H=OJUhZMN@~+5G(p6X;PuQ({pdtu=fHKGU86rqRrikMm#HiHc1vF#qF6W`4CoU_i&w69oFyP4dYO7P^IspN5ksw`$EeJQ_zZ&WK! zPZn|Hj)UB$htH+5JRVf_iX;${h=jW7Z)R3y>Di*&hZ7t4@6{pzLd$ zy7yiEzLSlR()aV<&CAR5D-FNBEi~e~8y(=i&5S#|H1MWQmYV!(PA($O)MCx1WMp_G zwvL4OgUFN1gOB~zE}B4q`s*Xs&*YEDvSr(UN=&}kJX?A(=yJ~7aoYl|f^2uKfr~^% zT#N~<@AXbmh!=Td9yvvH&dEuWA32}geB3>#yR)VURG$h`O?S_tS7hR*w+P0kUMZ5J zfizP2cPv`~;_<{-8}mB=n`g&uV$rU9F!;0|G2#Vfh+IcQF@9SX=~5hf$1B3o{mkri zHD#hKd&o%Z>*H``W~}fWhR{y*f03EssSBg7RsE;Sv=}88A4Y~Q4;nEF1s$J0x;s6BZ_+n0vHlJYCSE^I6JQam7epnu_Zt^R~2%>B+lL8t{-m_ zWj9F)dNM`)_BW$%mE>TEWMQqI=Q%xXi4$rQpo2A8lm6kPu|)M0H{gSKs18QJ%sSy9 zkfA`G3*AE8IO^uF>6>+elRfN0)M^@FAZqu+K>(9YcN-p^`j*EJyJd!jRG1j9YuYkk zLVe=vcLqC^S<&OoC4uoKCL`YSsr3aRQo2e9p@b|bEG5mRoJLA{sd?F}&HZtL=u|{nL^7!V*6sq3=^R?MzqA+mZYbL1#Zk)^&&c z4f}$6crOOK#QVKz=8ar`Ug?XDs6E_?n&VvcyQg?NQ)Wf+-CkP9M01*U4Oq33zCO(% z8Ww#FNoswkmU8iwX6SK( zH}w<8@F~Ub^|Hn=Tmj1>FPrCQ#tXFAs~|BXSJm>+rgw`PB8*b>F(4X61=5bKNV{aZ zNVmgsKGvgDw;#gHp5osF2?!(anoWY896grZq~KxQJ`=cz)%+p$A7`y9T@XrdL_<%( zIaN>t(?n$x-@uk+Y8Llw-YYgp%0)Bi3U{FPWAHhcEMRXn>_Ev%m+YIJSy$MIponN) zcv``QHbB;xobMX>33En!I)G@d;2*TnOFlYl&vaO4=ei72_P@wF@U+CTt-z;eqd}v= z!0;NpJ3)`z0OyY23g8bT$>1oV@M4^ZMdP~<4HHoh%BhCkDuiI6JKlX<7=NwvsZ!7h zFS}^469~EdIlzNbP2ZQ%Nh$fDZ0zJNyR~};`U0+ulXBAR3Pv;3<3`Ey4o_9{Jr9V`#`s8g5cFi?UgENg>F zL99R>Yc4Cf8{Ec(BJHuA=Bj<3B9g?oDryASxHE&XJr9?=m7Sz4u%D~SYM4eH%}i+8 zO?!0V#?rLpKk|uF?NfU~CLh}9I+MXF51^ZaGx&#{p-hklybLJlT>dU(NmNZ^4j8;Q zo}Rwcvnq7SOlU6OMIm2JJ@x@uM0nf0MX|E}g4a|Pzp;oHmArEzD^u-unTsBSquEEq9O#Mn)XH-A`Pu@4H!&kUu{n{343pYRSm=-NO3EC0R3HdYq; z@|Er17~P1}M~WdXHv-7G+`j2q`I5DlME|A-aFs;R#nJ<$yjamNV zoAnUY8I;7@zXv_dTbXL1=FPC%t>&1n#>@RRItf7^iUj9sKjDWR0cn!ShzRh%Jn9B( zAE%j0pm(M0BhE(7)FJ0bGANj2vSA?u2vI4KOzR09Y{E#f9qWs*eZQd?75VX)pWM+J ze;wO6nrK@f-Qg_ax2yIFTPlCm5;g4Fzi{~082TYzy!;MEMt?|@537muasOcyRDa^K z+e-g~$h{P+aZ;k_Yh)TAgvb`7Z{ie!q(dCSzTb-R(^B?3hCh8>m=${P7?>pE%NYHO zUBWBR*yVhGrI)4M8kbU2=%YZ?$u+ioTL+Vbl<{GoU@j^B?U zoZpAZzBnsns~I<~Gf@`;K1J}=$fo?ihm^XXc-|HfG=rb=re!dvEr=;Qx-C0XTq`ph z4t73Vofb5uR{^rWtP%{4-^etalLiiVuLcB4goznyN$&4!{a1CMl5TpV-9xwx?fSi# z>48hW$2eL#8Xp5r$Pl6+HMtI8=0g6>&UqP}iz^!QNg+1oV-;qgU9l3^0~-sWJSOXy zzBt{tZR9G2UWPD+@acz_aWOF!cT&E6ow1l8Is+CUZ2+GEO(O*|yDffHk*~DuAAClv zfnCv)l~7%hdr?vU9DH{>8tQuyEz%L*+~hPIRk|;CNk`!K*3oZ94L7OF*aQOi0!9g& zUiZuj50G#^)~_nxy2-)D4yr`fc1njwZ?@Z2H}n@{|27x@m?_z`*C*mCiOmevR4KN*Tbx?z z6F#<=*U`-*Jrb*Anz5u{_>^IqQ|HD&bZv+`UV=5UfBfD{!A=C-OXs@+osiQo>mgzm zRtav6_JmH^NxdzW<$Oyv=0`EudC4l(oj7A1sEhaC93BGE;-UDOm~dvICw|}vhKz>- zSCmZ2SD%pylp@DvS%Rjfl}5LK_xOA^MHl_0tpe(?S)D5**^Fl;O|oH$3mY~;io`_Y zcjZK`Z>({KJq2bWNI6Zn6~DpqlD+UNhAp*Ju@B_bSFdv&KN)%>A+N*&jvZB9Nj z78strKO%lew9GtZOnzuViMLyn-ovmK-o@r>=azh@PyAWrtgOe)I9M9+-g|af9DDar zY~&ZLwPbKDIEgwY;Mc0nBinBk*+@V0?zbUxX4o>(I}S8`mg01d&pjT&ha)ctk@j-9 zxTO8lC59c%9^aT*O+WvLK7&1+w*8-FxB1h@2s$DnaI?**MEX9F?%gEn%hiXndIA$7 z+l#S9Yt&-g&nYntFOry5B^SwFA}|Cxd>z0ff`V8A84q@m->Afgnl~QAsEFtEUlcik zL#XI0cXAYT=6mNmA$Zv6cl6kA9*!AunYH`Uf!N@;rtP^AoKWz7sV!w8I3|2Ia-kGg1 z>9#34EA!yjQo)SWB(9g_XQegs%bk<8dx9v~#G}Q!(V{E4MJxA8rnqo>!pQ#_z*3>_ zx6*`vi9HI@P>pCi59&U6UYYYcQ-Z}8gIctpvv89dfQ!oey0&~;!RjZq$>X$yTy0oK zenX??B8fysd2KM%1j2DMeZRf1IXkm(fz0tfWqy@m^7Da?v4A3B(|K)FnQ0aTE5c)! zIIPd0BR{U$cvh5R{DhI3<6!T}>kYYzhvpgWgK{v->G7^?=pOkgMZ%^gCks4tW-(=` z&s4^HqZeo`7PM_9X+3AGBm#TD^~gzI;O~0w9+6ZNSLklCBwQa2s6*KfP{2nnN*VrX zgH*xKh85-&F^Z12^W*XN`18N5eSpO<%Z zP5I?aM;-%qy5z>E)|~N-tk%^xA!(v1vDM1r9&B@kUQT1eIG8MQH!%P9VKQ5p5N*Y{ z6#TKqPVE)MAlwo7$&+#E!Zn&mBSIq9%jPwroYb-)oZlumwG$}AbR~#qTr?T>=>K>9 z-&EHbpd{YLq$5Z5j+aNz8?-!wsalHrB7zq3S7MCTL?HO?o{+#?wNak=Hk5yu^#_u2|RUMT+q_=tu+qASHs0Y)X}UGR;Z^b70Ou zqM-k>XUL{jy!vNv2LQ2laiLjh%#(M9M$pO8yq7~@=xY{A&%zJGyX#{A6!JO|w0?$6 z9ncgRJW&^UK(2tR7Gp<6=MBVl!^c9v#yxtA8}enrB>ujeR>Ovmf9L04Noif{7Q+w> zHk{$HVuHp^bk$EYwsi@rXxny{)Ydqr!xvwI=%SN&iYXC@3P`Cr>T=JlTP(&y;xA|q z@i%tl81|wyEEglL^|NIHFw~y89l8r=SdXa=M%pGoTtm;G>-Y%;v>bY~kqc&Hyu}yB zZ4fk##wCPLODLA16=yxQ4V1a`*PAbC3;vc;XrXyNkK33BPYNIMdj>8&x90y~71;0v z1*W~^TT~JrCvO(YS_aqI(;nvvrA`ag?_D-+uI+jDi6owedrZ}Dh(QYKE+-fqCyuFK z%NYyuXp>u;oCk^F0KrNDC*zBJ8fS5%EGnj2?I#R+hHO}i{Sd1juq$^$HOQN5;m@N# zr=Db#LzIc_g?s-Ql(wu8^7N}GUxTBFMd*$$)d!s{iRuv~5G7_GRNhLRMk&D)JDBx% z!b6bber9QOvcf=%Qq#<-`y3(#PD|1iw}wD)``_cSqFk^3;}1g7qTMFK5fdXHiY$k& z-202|F5}A8(tDHjemW|N_E@Fqv|DYk;OE~Od6IhQm&y0yiyW@0W`|cH%H{#b4qYer za}%F69fwlG>&0~CyBtvwBK8Tjk|so=f-w;O0g&R53f^Kb5uwIB6)^i60;!XElhJPp z5S!?~-uf9EANejNUTWhi2w>;j$-e|P{Wr$Wf}zRxefuM%yOBmxLPBAqQ$R@p>4q;N zH5zFcDGf@Z2=r_z+2 zzUW<|u6^)mGoT>9X;MPQ3<$_^gkP3xbrrG~YR~qzIKoeg?^0=A?ubqL(2 z2(#meZI2AWB`29wyUwZ&{T+(B`ELzHqC(9ci2eI}4uLQ4oxHg?Wd5CfFH^xUk0UzD z(?ORsos3{JB!j&!)jeNN_-kU6t+kP!HKM0ctZc9>*EmO*obuc2 z$hsyy!VhTG|E&*kyTdpr50@h=M*U2C!w7hBhBY*zIq~}l5z(k3jc7XKt4R^>eJ;3jwLc~t+;xs1GAN;z+?+KdhoZ&w&-2XYzS#4!!q_tfj=7a|d)L2&Xs)d%&|o^!9=JD!=0PHlD8;zK}0T zqkaCn)lng`bX-5Qn)a=?A#SXFa!B{9ukDhbv})CMnH$J23gN!GOhp#`H{J~{7BWu_ z3oLJd=q`Zu?$Zb+@X?&}JGKhSDbBph6te_2olW1=1_<)mR)NRUzlf#-NAWSmKRG(* z`2r)S&hbk$U|}rJv(1{=Ge=pB@H!Kpf6?N^n;rBatFV{QifDMU=zy7#ex$H zE>re4Q!{+`o4RO*&tl-ZGR;qAUm%=D&bMQiH#0b_Rk=_SaizvudN3Kcy4-3{3rrG(3K2;g6vs;@zW z>7FQh9MQ;-DtArf(SE_OKi`9wKT7fO3ciiXcrJ>lcR->yC-MW=6WHd%(NqlE3{wY9 z;_~vzK9lm4k3uiTp$l8t{SZc)r&Mm5UHVH#srw=%Es(oZ>I=r|BVxr_?RQYMK1sf{ zIK47x>5_VY7Zpu0mc`gP?(f=0;0g~j9rz7CVy|v}c*0y)P8@zp3oVRVmn3%;A!F9J zi%{Nj{Q@JW6oEId)Mi25iV{6~Dm(_FD&*lvpCO5dvs|RO%Htt;{O z4kKroO^D`T{k8@pj*Y@qi5P~WwMAG4?%U_5a-;6@!?olhH6J1du(tFCXxKOfh@N1A zhQK&xrOyttVivO1B72me7eW`X(sC~tb1=}a!U#iv%JvI?eT*)mxf!!BRu{0Ve{=G* zYb&`HvD9<+t!@47b1{h7r4l{$^*|d{AtlIBh^!J?9;Nr%F-qDd@T_>YbysK#U#tF=Z zw`CUkZ@;1hRJBrXTl!}$4o!kWEP;-163?I3!8BT~a-k=$EmhU;_>5JPs-(HjLMyr5 zvr8MwGvI9HLDz6ao(eXXT&)`d?3jVF&YuvU;uoGZp%6sFCkxr$O@X)Ts|SoKz^YVtnc%^!8AwfkRSH13NYur^w36x|0Nc=u&oX z)EA4^Vqx>P-OBBaj%>?RBEj{t~5H_kQTKat4f9_Bm?E z?jf?!VqkOvQaRW;W zLe{;rx+!vE#o}trlZ0(@!4(ep*MV^}Tmi@R#@W>)S#EthrHNW06_vzAO0L|u0dSk} z?My9B>@=>}J&^0s{Dahu3E39Z{JLCC+p&b)U8Rs%H*k~WE@3C2v#Qlu<1nPF0$5c% zZwfr;HI!}tV~2IB%ElHd{&J#GX{CtBNK)yO>KQ~N9B-tLlShx(r%_pSAK>N%ct$xv zSs}%=s|+9UVxFM=4YfV+z)aEg+DynrEPP~146cT!2IWdmhUt~!f`)TCQpkNkG59PK zSn&i<)Y0AxV@?Mp$1ZErKJTcHVsWRxzs5kKqgGY95N~tJ za_MELs(Ho#R=GB*MziZv=?}!zS$6^o}j;NEpmp&BN~;PtcUsY7^E zm~pMNwHpy!eE8eK)qbz&35>U(KSWwLq{FWQj-10U8QU;5SLmOHvi0C#x`=_@1gWwy zn*5Bq*Tpaf;b*4v%9I+Ehe-F<)r9U~N>rs#0ZdZri>k312bf0Vi#X_^1wucPG6bcH zjk!ByJi3?+Bha9jw4Y+LUE2|7m4SkcYZG;~3dazDxXQ^xX3AnvWqCbO?G748)ubO| z{yeEtMQM>E{e&4`t%b5V`kXZgKU5Reh|UO#(yXMp)S!pl!)-~}rn=G^*lc|)RaBAi z_iY_}?mHNg!5_L$S|6$s!!uU@ku&E*-laA>6s7IKw&zpMC$H=0)489w_+Tlw?7S(= zaykr%@aFr3&5#6HnOZk#V?JhM>US}3f37y@p4(ZPE;S`foH)Pe47?R;R?Rpms($XS z<)o4R<3>B?2J1^T2PKnz+AN|`Rp9ISDKziYTXt@H@kayC3~=AJGS6PzTTBhVbkH)G1t8+bN|Ed{(LL@ zos$#ZA0~*ri@bE%*>gwycBzE|{X1oCMV+MASN9)ue*`vr-_gH4#hH~N!XLd2;@CXY*LQcR61F9p z@AXtXX!}dy7HTUrTWy^z^bsRxiQad1ILXbcyZ9=p-(oKC3}#&Q((c5}d1I`qMy32$ z|B8ID{s)?0E8Gmf8N?(TDO_i3)RhS)VoNE#LJsK*==vt@Mv%wsqfEf5_V2WsXNE~b zwDydaa!rx2EN+K!OxRWdE!n77aeUg8Lr&UfoUmdFwBr7kssQ{|P;8BtcCKhlm!1G$ z7&`H<0M9T_UzBaWx>j^pZJb`{WhQn*@mA99Vwb@U>>%>d=i-xTW6+aj+lXQ@JI$7C zC444TF?AdVl@}zvFp#R)aJ(aj0&z4Rq90+wNTUjULAt%o5UQd;9NT-etya1(jfYzl zbw(^dGWo=p$Om&5bWtaupreU4{`FL?a0jzkqQsQBN34K&rb(jWXpc+k88Efy%sKmz zXNg#*yV{0Q;c5gYw!rM@Vwbi0Y#TQnhxz04oV+zi-M27~2)|NvP>z_h9mmx#NR%KM zs!L>L`!)oI7F2wPPS9FU3eBJT=I-{uARqr8g6F^rk~sa&RDR~EOU9ltC-ZJo(f=s~ z(T60$G$>%&z=}7yP@=w1ftQA0^0Q)N?we8IR2WHUeIHarC6*BhCW4Kth!TW>I1LLu zg-(O;x(+?>2zrlLdXtwtI~g@nSydx(on}7NWFgf&7ssEwUNwOhVqK` z>`PRC_%PVNuPiJox{h8`ov#m-olv-9?b;c7q-Hj&Oe>)j6-CYvof>|fiB0W26>8f) zsO@WK7jc|Z@_>RC;|`Zv$O2NSY)ta2AWGr_)SDw22et-%}z9ebj3 z$LI%v-$`qRhRI#mR6r@12a~Pm+n;k1Q7c8q0^WaJ*kZH{iK5o^9wJ9B`EoakTn^f0 zVy_1O^1*5C?L)*2t*nGLdH991Gwu3=c*|9Mjp8pVtJHJdS)VC!6CDK%oKX7Z))jND zUmIpx#Un_Ne{zs`#F#Rq84JGXwU5NUl0*xSNV%Uk`TPo>)iNiBnpA6Hc{zJ$Y=O%y zZ5N1Fe{{dgNXc8w1dQSLuBXpce|fX&6&Q>_YNCHC^7nfy=pxe{3LeT%>piQXD7JPx zNe>1gx%kJ&XS8H_AdngpI(5iQlFt{jgN~oRp+J|O)q#1QD~pCRZ8gjIj#e$AJk!_h zS(xl71R7j3d9*l*N%-Z#0jrD>42i-Y#J_(_$TEH*YuXpXLwe?^N2p4XcyiK)`55el z`GqNz{El$Swn+hGT9E*WcJ0SuTQLxbjDVhshK0!iUG1#M0`2QxmP3)He4ViOs!u%I zo3jf#dl-JVkIJNr4%TJ->cGKFl0dE^-#5YhXbpLE3585QiMF)irwRL+%4*Nwl9|ar zQc4XMz+uv@?E7_dl2XWjy`$^S1!mf?9pOJieMB9hGG9^Qe9>jW{0dOK{at%YV$*f> zl+GQN-G#l^cDY+6{@n$J(x;0#)Up3s_tfi~N+%3PKPm*yeF(|{__YcC6_nrEEzzhP zi0W{lPBK~CPlfhax-o^22g~*~Sn6HA4}1e}!Yk^fZXESQI@%2uAC*c>r1dK5EbJ$G zxrijeL|e0k`tijEDt_(BsY7hOS*@$Q%Pdu8m+1EOho!bhQS6np?*pCV*tlkSQ=Tid zzg>PH#UqQuN|h0VeR6tI{g5T(V4PwlPTY=^-gf3De?Xn8Ir;9nJpI4u=x2TAiFg_m136MJ^=hN2>&5z@bvO_XskNo-r}qXU zh<@HUkQ355agunE4B8nq^F*TJl^pfWIQD(V89j#Xt_Lvd=SzpfYOXYy`7Wo7!sZm% zGS85EVV5?&go?){hr@X*moi-bY9}UG%^btP=rQ)f;?Na!K$g=Mb9WgcN&iaK(H&yRhUT*@& z?P$OTi}i_j7iws3ea!V^+@6g;oAvqFp15o{8Rc>({oa57LD8r%R&X0*>|)@NpGnCN zj~SdNT;Otw6;%79q4+ckYrd3)v@>nKj}ANvb-X#;ie0)Yxx(JT9U`WD&31vQPWZU0 zIxjx@#e2okKsNW&ZesSQ<^jj)(b6Z_5Ak+OBgk2ufS(!?^ zZ~12ateha87aL@m#IxGWCYMaq71nV{^V2kVi8_$gam&(7Ukq*}?T;!=JVAbL;rsAA zjT`CF@W4b`blH>jMu$UCQWV10@KH-IvT601;<^ML4N|aVkx6jia;yHj)O!Pvr~^_?s-DK!I7B-Oy!VSZ8oiGaCL$;kd-Y!AR49=$d}jcQ z)liHriSu{ZV0H$|_#4`q?#qihWz94cvZ*?U1vMkw=hcAl5eu4YwxNu@gN9Fp>14(zQpB#N$B@^jtm8l7x@62M`w6*IF zk17D2$6%FQ`YPTwG*Jm9vo((B#YJK3%i+12Po(O@a5kM%fvv)^q0e!cHz z+RNaa??+J6d3(R>ck`ox#pWNN-$#AFFsL=cR^S$gN*aU|#e^Btv1jj1Uhf-@9&}9J zKps@cGU$&Fnj`!V`AIY-0>Zc2$22CW@`DU=}p3R`^Y@4f8Y_{UDsIV|= z|LzR!&cq`W^Lx{8-N>-i>GAz{r(mF$ZSnt1@(9%ifPJhspQrSc=_MINc}XF0t= z(-431hA*eYL-A_G3F}H7poQd1xflLaureqc^373L=8`Fs*0r9DzZ+fY7OTpf{;!;* z(;}sVIN#6oWC7D7$;^AYQv&tT$a_bM59sPv^9D)l zWcb;h#vkXql+Iqm`IOTEcWvF?ckT@j=J?t=m>a!>lS8~ayh|_J zJs`u)sxF+{ZRG<+?MJ0--bHx|?O`Rm-e(m_ZhFY)SD*a&RVtl%I}8`RuC`w73L%*^ zVgcm4(%Hx=2%afxty@;pr}Yx^MESt{(G5yBx$40msnO&I?woc$uVu%TLmHEQ=1&Ch z{tR#p;U}bm$h26 zy;yHVLITChFCY3h2l1;I0L4wsJRfuP@j-uN-2~KsFIGf9=JJu-^1tw-9bc-L|Gtb zA}P6ARH#$DE!jE+f3d9v4r!D)fx1CI6U@I#scI`V85riDG^gi;<4SfpQGN>`P)=rj zOn47rNnAM%Y74I;wQ*d#^GOpEcl^7%DJb4oMddT3s7TPL`q`5o2iFRd&^a;BsVmTX zLBsaN2x&9C(!`%*Gy-cL`%YxyjTBWroF6M*JsexHJI zp?q*v7U>mz=`dHdVts06#qMObLST!Fy$JDJ#7nVn(L>vt4_E$E&~oB_f7!9FK`iub z^woO%S~qes=Nd9V{Cu!6)tTu3WPam6kK*lDj&~vYj?XHEpf|XW;-u%|r!@sb(d2Q3 z(H>lNQC>#9&2;u^Ed$76Dk;y!h`Fxa_kkOED-nkm2dx{~o>=DViNkA}u=DLZ`j5d| zF#_iIsvGOF6e(t9d^_dA+t3M!v%{|rW{~{XZ*&6Fa|6r2CFbw5dvgaE%kgXWgVA%2;B<^HFo>KAThhjpar~>89Ev_&t1xFAG z=GPVHB@F%B)U|t+6{S*_gWlISA|TAV*syc#-N6`Xp0eD>^Ul{Gs?Ybnrb1_^^NbUz zhqFEeo5r$ipjB*=IxDNz!_t0>9_2;5Q(Qe>9AwLWOKDs#MV9>j=!jT+(~>eZ+vPsD zB)x4?nAyu^yEoqcNtUfnBY!L?4kRlJ<}*O!|H%xVYcC0uu9Ve4dNnTq9@t8-V)=8= z_(Hlwr8{K0=g&tycVG$by|8LXIg=@p&gYl#dy(~b91)aj6fR?;^>Exz5Yo=K+0~wU zs+xp%*&zk0(a7>IrAv9yk#zmC`;E+wR-;BTiIpR8b(stgjX$a3b23NX+L(d~$ zIsb<%z7W@Yn^BUciD;!2;31-#+b)mX4--2UgduVKjuXLlD^Jcs;;z78-45!@pxvZ2 zHR}}8_g|*=FjKXcVlm3W_P9oLraPGLY^UD?=zn)6&bPSBDq#LhuB8)lL^U!vF6LM^ zljKjRX|VS0|6V_2K_a!w(6xYbbV=O?~VKAC83yzz5y=Z z@>2hWGs!H!AONd(6ZO&EBQY!#Fc(NB){LyqWybX3%oIj7Qm7Hs2v5BZnDS_a zr3#PCB#4k~G!BAE!4JhDPm*(U{c@}u@6#G)?r7`U+86(;9%lCok0&+Ah>G}=d^op3 z4eNp$nn|=7&Daa96k}$?Wtix4>B%!#?||aOg(JrV#FvrL>(yPDTQ|m9?s)0}*n3Be z#9_*%*?{gI8M7zpwJN_vbTLtaRv>=Mj2RtNz}61lnm;o6)vR!rQvWP?M^t!y3V{dq zMS%#q6-W%D9rj&rIdn2IXCGts?tN^36IHx_9W?_4iY?bUCSjWf?ca_gJL?2Y5` zN5emV8WFWbQ7*=Nx@@6{0H7Iq7i!^kqGL07dvjIi@gKqJ>9^XUlbSBv8C#Gc6WDnm z&zl_XgN^IG<5ajVO2w^%(xc%*HGPxUU-j1a8>LGsAHH+SWgaKAuC{6<$eUbD(hIpZ z+`)A2U_|&9c>!%AQ=Mm)Jhq4e5lR5YzEPk!d)b=KbK>Xsux;sqa+`QKdRX12?A_#Hz$auVQxJXx3irGl=^%DDy=Q zcac^=o7B+o$9&TpNh!czA|!Q4>Lm>bfgO^QyrHoLuf_N&M}s5)ML_$dpCu3SO}9nQ zaGL9)ElSl>-%gCkj32d)aFNt*OW)!lrppNEfF`9e;dEugYqxJ-P%;BE)j178AmQuA zsD>F8I}+BFa4O8N;72{jpB}B9%X%P9dQRy>erFaSoE6T@mIEel=6eIA8GfhO)py39 zOe%8J-jvO)_mfU&w9b(dGqJ+O#f)tAjdSwlW8>I^@G**L5POiV5c?afom;T4Y>NU` zz+bM1RD*KTu6`-bkDkP^I6~PD?=x=rz?Ch#6%EL^4 zM-{^TR=GE>MB_g%?1-H(yOmNW58bBi6<)#SAXC$l``!Kkb6cfKfEP9LKWA}8=~RbC z4KFO3>y(i(R3$xaxx}~$Fe7T}?Yd)U=e=~g6=I1dVkN=*It6{ORvZTa5`ovkQYH2n_S3%+-CTX>@l1bAyZD@WcOh_Q&SLZmzUV?FR1NKf)6+oxR8|ek%|qU`hJ3Spjj6|C$Ivo8(0?T)qHeKE`?Is6PyX z-w&%2bXvI_KfMZl=ag`FadkN4dHeg&9!Zak3dF#xe8{a|z1WPM>@uWU16}hmqM^qmN$$5_Am;(R%*c2D)WE zj-o_T!{QnXLAa$JBMV+Knn;~-ZNr;U?wNutz3BHl&zyev$*@n$gDK<=j!xxsS!>yZ zSDQYL|9OU#{qCoE_Q}80>1j%C3mfGhu*(}Gwkump*$<~l+|5-LDe3MNmgcw4dJ`^s zjODJ%SxfIAvby-}wH0!2Go3VkSY|(P4tU-BQ+}+r9JaNu3QIPyzU6!#)6=5)3;Gbu zymaEY!<6l7!Frg(KP^p2U1S5SlYEWZIYN2@B23S1PmVqUc=h$k>>WBM>)E^H--A=m z+w6De z8yf>#E=|=-ogx9%D3ESAm8S%bGt3j531WqxNV`qE%X!sW?_V5E8_JVgnHg&-*}hHG z*9ZA3dR{kQ_4-X*8XMQ#j|jL8Wx>4aL*BNrsAsg9LZ*T8u_4@y+pM-EAPVDN(mmm7 z-u;zEQcTDh`*ODvX^Ub9d1t`kuu1@RVxkqRf-ljp81~t3)f!b|;Gk0~3O1Se?4Erm zY_rXs9yT_+8($-Z{oY&i-Xh%K^Zg+Wg=2$|qj4KVPw3V6!}E}00*TMn%HKZ@erVqF zI$v}YoNx^3THzc;5qXh`d>=(?xBK+=f(?`WoW1=f8<(br@SK7==VU3|*6 z9k}$1_+tM;e~*=wnd<$HKX7-%Qc2s{?4z}j^pc!i?e!0UYqc$Z_-032yU^lH4DuwQ zVaIlRL(hKihSK)%qdeGmhl+w-F2bWVQMooQYz8?vh(B9nmDud+Jmt~~#L{Mgf5xq& zn)hby>zAA{bCd3tfhlXi4?O7?Pv&NfV4^K(20QOPVUV5*BTU?;CNh%&fld3&e7Cpj z_eS%v-K#gD%i(t~-Q+g3<+j@=2cBDd%hv>dZw8-KCx3>k8!xGyf$+C9K}D7oFQ zwF#1_O98&`FvRGFoqhR|Q{k*aZ)2siwa~Z){lW8^uj}ZW=g`l}3R_1_)_xz(TEYoR zrgB7^OfIQn_(_7H@U?@`@wrT3rwYd%>zJLsu|Oh&)P#14;qL;A{K{?Waf4;(0J(JO z`~TH&EAaOCEWVi`E|oRXk9W4(id>b8m{3jbUMy``G&aGM15cQV#GOX+l;S5*oIOl=9HRZMqRCMLC zMS7#3v>>_iexU#f$NnbBfsCk+T>APx1v?nzO>vT_apa(QUK02{(VD1lJShq$!N<>s zP7S{^5I+B}zAccfHtbA#V|x4%tSC(^Is~qvZNkm=FzU;TRb~TzJk?|N&%d4wX1x8R z%)y+_RowO1g6;+$>ohGcHUWM>cek7xeM`}?X_JFzn5j#En_QL^l`6H1hmJ<&+;vU z(uR$9?V{ZWr42%M%k4KN8fR=y&&_W?sEVr82=ArF@r4Qkl_smy zyZw+>MZl~A0DQtoH$`{a8?KA;_8w?0y|`#vHsWV?{^%m_w9B*H0%y$}`?U35so3Xd zx&fa%Yh}sT_b0A-U-ug=S`xNes3cRqdc(=s1poN2(@(fZdNI%O3q_8s#+?Vr12}Xs zl!NJIR(9&ks)${{}#s6?65z~8jh|UhbU~Ib}wdub7-ESr&fRCiLX1 zs4#FJ6Pv3TzWS2A9K#~Y4Lqx_li${gzEG~!|4nr`>=`i4IkfxL;Rgn;IeFD;!D$i+ z40jJTmlzvqB_$xftXAVgT^{G$r2RvWX+cAR}_atKO5Q+q1ATe>!ikIb!_#iO_ z6n9tqJ0&3WHKe=!Hg-*5y@#sSB;c-_jf$@WuMS1R2n`lUlwy1)6d8e^O#xIWFa{1# zWVI9fjGjMdd)2(S13(C}jl`3o2U`lLae{(m1gXubO7&732@&FfMXSfq@-pt&OQEhW zv;2$+3Qibe{y7{AfL1Jdh5qvvmmlj?=O+(7MOYCBiV4X!#hivyRzqzJbxvhWogZc^15gGk34_J zW0IY*tMgZLNn>VfKh4i2kB80h{JHzhYX(%%J!>lvBlyTlEjetSL<{4Pn(i)Sb&C;04;s|w2^x8F8wzi#>Srs@W8HXl_bVb`U+Q7YnRk(S79(!>Zr>ETH?pn zG}q~qMSnuhsQFt#pu1BoHDx1i0u%m+qoPxS*05Nvn;bW1&M}$6$2kgbh{L?Xe=><*KCN6B$DrMQOpV)5N#2cT101nq3~Uqnjj z*xoGq<$TB)1fo*bsfQ$rYGS|mMf{*O>~|;xEbujk{WOSv(TUVaSs(#L5GwrjTgk4! zxB(6P?;846e*S1jwe8}h0H`idA-*vhN4DydNm(-b`HMl_e}v-_dk|B>)oHmGCQD@J zEa>(k;Y1cj+kM}mu5ErOyElZz-z}U>Z$FSsPwp09#xOJI? z4X-N`j{#a)b|hf+)PsA8DuwrLBa~Yy=U56`>*5_WQrG)#ZvwH1FY{+V4L@HpsGABF zFbze*$(`d@{}63cMp^Ok*j5lvCbVs`R(}bJV%x^ZUd@ePI%}z$*}o1Vw?0|)989e3 zrTF`j@XG%X@h^}nxLVc;++0I~ob~Cx1Rg;4Y~iX}6vLoNs0L|!|r&S;JAdIg>BN5AJhz)|eHG&d+{w2K-DuJ2Y7PA)hEOXZTl!ZMR;@Fto;-R5W_YI zd(Bx{pwZ_Vc6N64@r{!TEZZM_Qo;v33tl-}CyNn}-9Mg}5!96Llo|2*j(gYOqzVUCqeE7K_CWzSt)1fAPu62{M`} zk)&yk7`LuYD^g$xvWS$g5WyKy&tA)KOg2pv1ADOR!Ap zJ3iUzj-M14q1>pDFvyD3YCA2G8XeE@|He?L?!*z@#ZIdi*trgqIK^&KExAc81^&PtGkNT4g2XS-DUp; zKKpEx0MBDF1+7v&l&!rAMO)e+=hyG?2_N)5*KF59`iF`*;32BpyTMEcu2G`tP4x%- zDSG1GUktHfHZn7@-ucso9+*)@tm!UK8qh>Gdk4h=pBL0{3w=$X@&@7bcD z`y&R+fMgH=CQkBBC}J!vFZUD0z8@5dMf;l-s}n47yrD~#?;!sq&guF+Uom!NU3i3C zY(}uY@|Q~IkJv~U-~dbT0&Ev_6Gd&*qGEt8ttX(sXPIu|JDYs|dhmYC)|o}{ zEA@?cT?%*7v6+W`%uI$TIH0}uR?!5OK7VHfNNq6({70W2t-RF>J8lfL-IF>EW{43) zxM(K%yfP~Y`NUI5rXV#OfwH&HB0LBN-5uWD(kom?rmk1ycX|!SN}?-Gy6?}~gv;~s z@kfco2fv@!SJ4EsxUtF1^`A(X#jI^RM_}T%wqm8NW4BX49bg)JL+fPx& za>719Vd6P%MWEe2$E^;@TmeAk;E* zb+DyVs$-~oe=l9xENi~?5VUfI3{R0g=%T#|<52$&ls&6>=Vwq=+dC?JKX1e!q zRXx~)mRzL{9;h&RIA+UQcTTbEI5>$8e&+Ez-iRi9WF|*p)i^Q?g^PwaMZ$h$)Q8gK zs--d&j0Q=5r}raCZ{oN@-n(gnvz;+mOH4Er6!Ke9KD`u_KF<(WXIaYMRhweGBAE_V z^6nK=jy1L3kK^T$BKwkknx}M-`jcX$`T4%4-)PYutRtCg6C=Js@+Q zGN#%RzwuO7SF-%|@=tz>$|pJfJH@53uzztwankqZFICl{M1%W7Na<$$>P)V{<-Mi(J$(|nFbOrH*KXiAu0)ETx+;7 z4h4k|mLJK4d%6NNE`gE(zZhp!l_zef1lz1CJ~wS<7@fF`!gAw%Jc*dzJGKrz%xkx) z=iFLKr-EE5&o|xu)_b^blJgt1PrQCKNfU+XZvK*JqFGrAdmk#xZ+C?BE2gTqiC0oz zyP*WZU&VCs{WCqyof3i%7NWwh<9d-b{2!HS72_&b2O0r=S{wRT=tzF`%pHs1xb(Ol zq3mTjyr>UP;J7>D=uf@d3`P%2_ptBHeL)sh+I~^3ol|XHN|rJWW3VUF#@S!(Wb8iR zE?bNSaziIUl(!-;7WNB|97O-Cx#!Eh+p=2K%Fd<1lG zOvup<*_LsM1!uxdBrpjqViCt*v6(cEg6;TffyvTq$mhuF@X|kI4im;0wuxMUaa{f3 zK&IXT-{OGdnh|{0bs`q*#(@02?d5w;1c;gjG0yqGzpZo`!QeO*o*~rclQ3h|7beyN zbststo`q$OoTUb{;TzJ40aV*(^fC}aSoEBUw4KM8ICwWC^ZU!iriSb|{Bs~9f)9i7 zZ+g_t-ieigl5pD7N?{G zY*g_2|1=v$3_oq{^R>(X>^l6`AtaKhEFQ?V8FAg(M584KcOqp__Dm@zoUjf?Lf%0X zK1JAP7!#an^(BL`F#>|MrH}xkai0^?z|-$Q4!`oH?P7Qdo!E7=U41C4CDAkh%-&bK2SQc+>a431qr|_v zugW?1Zn)%WP?soAuDryp?gs~x^Ft~vwiw6z22%LtgEtXWhW+05a<`@8JYk`GvB110 z)8vp(V8i;T!Uk|+ zUc@3t&vfSEB#_G{W91!fU;SK|b9=nqF47(9Ysz~5FnKSR>3P7Hoj*NZ=ts5(ZP9tl zC4Lr|q?+EFt@y9~hZAe?hh5~B+Ld(rqHyiepRUsC{Y>B%cpWD*z|_BCd2rSaHSEV! zMz5(c6P$TE5^_v&T=m~gZS+h)+R&<*L&EW%Z`Z2zyv0~ilL;cKf_yD#KL&*a!t|rr zk%ZV_&}yxJ6ihGP01`z`Oh`4%VgC7tW=B~P#ziv=)mG*K(>z6iOr_yGxb~00t~jm( zVQ4*i?1G9N=phkpJ_X>3#j|*JF=JMPACUol9;ATmCOLdFNr~#dyP`xd_1;}!DSJ=H z11I&a@p~}8RufyhHpm0NpG_ix1(V!{?kC-ljP_`ai-}S9gjE%@DdghYd3aFN^?i8a zdeq^0$=9{XJLj+wo16oO|I4Yn8YdB$u*rZmzuG&#Q-)wM1R<#pDKIpR+XwR_Aa`cJfl_%QfZoGB_8NMqI zb~t&et?jR$g+Gw+6m>1de@v{XsgPsdRLHF*>?OXw<}eNwJV3kc z5yK>G^IPkQ?7MAtj8BdUHBDyU#UM{e{^x@47n7gMb-ss|Q~#gz3H9ADiVBCS-!h8; zSGa%0f{Q zie%JOKYmmV0L%{&<|##G7AJ zt+%I3P%^ANuEX)vgI>#gBK=3JrPZa~(}nc0C*L3QMSF;rBegpYyya;8%Pv2p`ImM$ z1=G6NJY7tGFeCUEJo9*=6t3o-*q)&d%r3q0_8a?=S~L3Aw4yVsvqd2@&427iL5dIi z#+dXdFkEbae;AF4g$t;BO+U1NR9U@(svUV`M#$8y;waqVu0FqVBqNfr9uzvdPz+HD zhbQowK8Frs>cTAF5^L@T4|A2{FA4Byh#B#tPS1%K0DAc%P;0JLcb?i@b>V85MT;wa zD9uKNI~kk|E=rq@CYrb?<$x|HpHTs=57Sf!y%>d%KeLYzGNgTW-rl&i!2_;G2ZvW- z@VkBI5=B#`fFq`$Uyy&bVt)dN?5oXM30$3LBb3C93On24V`AQz?&%v(@H?aMDb-9( zb2N<4zCX113N@cMU?L-^Yl&*6 zPZDoKU7vjS@B>ke`4bvtDH-4nW4nQX#7`gzvA)iI(0*ktEILslKs*_C9s~57xr#x) zBSHdjS}K+P;p$E102CZNwJao%#jS;Cu?yX|~ z{|VzC{o02Ei`1Lm73SX}y2=e%XSUJsV#Jo!wM=;8w?9cB;`=ICBUT_i$bw8YNYjgz z5I3+rJAD{3P}PIuc`@6H0+0yxa$)q(iTt(;AL(^GAW(2?cNVG~<0lkcbdt*-rF+$C zp5qn(g&yLBU|}4Ha>Wb5{Wq_rWxi;2oj+NP=D;Ejk@O~(Zxf7ZU%mgN=eyp|PIlt6 z*2NPIG27c_=3(hvQtD zTq8lt{nccgl^E03)L?!8YJ(xdv8s$aD zz3pMt5{RYYaD=Z7xbB8c_YHGOq!&KO3ykz57~<;r#IBem`Pk#bJGHS!?HA45oAcfu z8+2yt@ifvGv(K~Ly&A|jqpn}v59wb`-fLc-kL3N&J2I3DX;w*y`x6r)Sw}WCtBIF9XWm~8{7WA%h>xwnlcJHb3y+h_;XIp9 z*S$O1vsA~>QJ-#xXFkELeRWP^>>%waqS+a5>0q#;FIDJ;t>|SbZqNw43R8Y z{+SqzzpcU&evhRa_qzs4x1peMAev)KO-Xr}pVg1+BJw1`2#aj@TMLur5nody>Iv68 z<>U=tO~PwY0H}*FG>-3E8hjXI@6J7JMFG|T?)96`R5pUmG~zm^3_U8= zVNB^dR|9aPGC&~kE+!IBYzMREMxQw(=41dw{R2#}hME4zfjDtl>BPvt1CT;7UV_j9 zxWjAje*W{%mz>*ER+fFs*K%XU5zPz}zzF#Mi^(34lbNm9PRXy=MYvP^1@Xf&h29`U zp#WJ5Ywpkur9%A6g{M`oHY$Za0$aQx)OZWyJ&*kIG9e*>82AtoM2$Yky1Txx0>Rjb z1b-GMv;MS57eD9@fP9F4UAr)`N_hULlMK+Uc9bjy)!adS{7Oe#Y0J126N`0^)Tc>7 zYQu^HT)it$yD!u<(=vDNIys);J2C@JQ&+Li54CG^hb>mF2|~65x@j40ym8FcW8{PV z*M{>v`ma{+xI)ej2CSvd|3h#-2`cBAYDTx2H4!?-v6oZhsCUFFw#lI;Sfv@&T<{N`K+|+k+w6x!6 zekM?QQn%B@CobQ8KQogSEF~Lv(H^sAcW(j$fT9RU+-nrlrPW-)_LY`><`y#{dDAmmZN!1?VUddgnV^4&hWZ^ZyNSS_%BO_*6wxB{aI$DQqmjd zgReyGM3ll;r3IG3nbkiFSSfk$r;q%whx~F0&fNDdJ$_+gc;(%Z?MkVcUL;R5t9MYu z+mhVi(0)!}Ih?XjoZ0vQ=*}Q3iM`%dn-Fn%V>vv-ySM3*l~Kh4M5KUIc?NLZBHO#( z!D_QZ-OEaqse7fNIuaQM4PXC_(=u>)SWe~NwcPbbI@1Hq2l*MfU}<^Grl>5sOF^hA z;SiR>xR9M{nBM>oOI5A8Q}% zgA}EiAqAC?83K>bd;Cf7b82mWw4X zfosm|IQOylXOsD`@e+2u#E+wbOGrQAgoxUlUbXi|>O(u-;l}7qUg%T(HdaD z(XmW3V-zUXUoTbC(AP#Yx|;$X?4KdP;QGzNQ_)=(A(Kk_XWFBn zrn&?qjB5k;u69!%C0g%>_$jQfZ`f4<8y4N5-=_@V!7n1q+Ml&!N_10J_KUP`ytyWF z&v)}QL=j&pCo4NQuv>Q>_zv(p0?z3k3P8h*Y`kG98bX8sZ+{y=^1cDsCfyo+wmxSS zPq`JLPZJ)_=~fl@56F#lmu~@S=vr?yB#bnWTkDDBG&9>>8QkSh;m_5wx2O$_*h2~S znS|mTLVjJh+q{M`E)Un z4;sq7&dr}EIp)=De1(=3vM&R}29f9%@d$LHXR0T#bwaLXLAkv{c5;1|MMv)|jYTbQ zxQ~cS%GyK!vsYi8S&Vqx{V^h8qshpl_Xx+iaqg3gMn(8f!z-zj4nsv$gZFpVPosU8 zABxm4jI#YtGJrNA*U*!mLN7XW+vfvYFjrKHVjZ3K?q?Cnjox5>O*kW+)o9aeg}k=? zb*mK9eImWK&S06kS!W=NO$hPPJ{9yVJb1)ks;4xEmrJ)z%)aorWciA#p z;N5>E6emD^=2^=QdMBi>Zy5FZ@fA@D%{e!kgwJwTDK-d&C-P!7*KKbfc;CRu3Oype zscb&FJ5-lD5eW8Ke=wu=o(7fuV(^Ki#*fw2lJIxjgLB|y!~8tPdc#Z%u5vtS#7Cl? z^_RF9K)RbOV6MEb5C*!5o(vjH(Z>&Iwi&^wmI+3?icdfJ^DAE&GXJkNgJoyL(^?7l zwMonG*1*tQVJ6a9$$DNYTpTfBA~AJON_-+^?Pc<{y<9$p)AYleUsJOuU0%(9Qkg3J zBf4W^UY&)kM^rw-uWt{mX6%-AHLh&u}V%&(YPZebZkJ`}OXIecAMuGib?TIcd*=6WEv(_cfvc%2Q1##|c8bt9K$L?m z%u%bDCZ<)z6?4Rd8y=S>Mzd%EGBEJUV+Rm>6eAYQ7uFtT(CCT+_-CU}kAWR2NPZqM z`@`Yie=pN67r-6rS(wF`LUGF_U?MdL47s!;@gopO1+=nFm`p6%3Y*`=|9u7c5Jq2(_&?4R zz2cI(G;Qhq-4%NJYIm-ImF{#8nc|5$m`y<*9Hhc&;RhS=1SA+mxe-zCasKIY)8UX~ zxYxV1!5Ge(7~fvAyvjcD8v0y?*(Zfm*Up)OWi>U~=oEK2BNPue($QE@3jKSUkWN8) zuf?S1upP^T_2v%BT!1%VFL8x?{a~NSiWw!X8XY90!mgxxqhpDoAk1G$)+IQx=5z&2 zCV_qJ&^BB?Eu^-c{p;9s_71{jX{`$DZe==0eN`mex^tq8G=$+?J2gRXdrz{klg20_ zs)jJ{++LW$;-AP(cP;Jc0c?rCLnrccuB{cHA^3j^d~j5*DG;QHP@?1eL}}J)RDWHi z&jFZGAWQGQW-u)UxZ7?i8q6=juhc!eU%ITUem~BbogF}GnDgYpdtRM=w!H{CLm`NK zgfa4YIDtSul{-s6B?!T`U17}76Mynm@(O-2c37&F{_~4}Ccr{Mzql__AgGUvro6m7 znNi=+@DjzJJy66I1YJ&vVVqM|N%JgAq#DS?#C`1VOA6lOI)1H`tRCq;Sdeh=wJ-N* zUs3zrxmnLzgWycyElzpTs^I@TDT!X@o9+F_-tRugcxFB`E@jo2T;pf^lb7xKggttL z!(28_HLf;VTa5<2f54Wb^w8fNB7iiVj{h&POhSxn&&)x)U_K3_o};9Ps28|Hx+!Yf z{9o_~53Ok^%ipB~QwLo`);pw#eO4)ipQq#GdJCgjz>-R zIa`~IQJYTbC|cV`+Gyo&^FT#%Fbn1RvSGUBI7`f@x71OQROWE>29~H@+WN8D5A^;d z;Z|OQ>cL{T@ZM2F(UNAmmV- z;a>sB+Ze%Vs^yzRvaXrxkXL(dXx{!nOcM z=06|w*`To^1szDBw;&wvySZ-+*gJWq0Eu1#aPQsPk%0n$7QX%9Muj!q1>&1=5xy7GPhb0Rf2BS)+3*g7Ug!Bxur1HEv;$VrJbzMG z9{m6p#buwTu}33NR_|~m3Rt8dK(1aEbhCmf{C^VTv2Y6b1)RhK-w%bqCSU9zA_3Pb znFcUmeDU!i3v-qBKeD~JL-@@~e$=tfXB!KM6vYw`xdyQJ3g3OlLS32jjvj<=#|VI? z6a2GQzD0+_xcLs7X7HJ76Vx|hXjB?M{m6Hxs&}^rTTZsr|F^6-QqO1Uu^e?wB@N`A z`VT)LdhnS-qF*kQ&H6e7qCx#fu{(`*@L)<z;+eC@dR zp)vTczcexzjT0m6z>iAKn#m;OfeDmMb+%PVgb>>KV3)8YQ*D^?W=$lIE7!0kvmHHnUPV+(>S4zn20{*S!`Z* zl=ZxmxmAU}8nRkLB4cu62{Y~h7c}kvQ>#kn<_pdoWye!1P3QcmwR@eK;AdrMU#R%i z(jekq;6b7&UEp;D|?Y4bOQJ(tG&n`o? z?x#n-NiXQ%d?;{q5)K#pn7LO1jmy)sXk;xGyKIZS!>^rlW1+q}Ich0KkIP zHoy2|Xh<2DI}B!0)*$l=Q{uz*5YH^fhbdpA8nse#a4PQy?MA=6pA_~>&s`PKB5H2T zcYG?DxF|}#EM>9fLZvNwx)$f84|*futZTuIsMD9IJ8Zi1CYxILm#9ver{&-(3$)3RY9=igo|vep5oLh%KC- zz^-Pme|$Uke9~WTxit#$Wg~_$$@XoUqVPMDof{IOyg(!@j*)Ai8!#xdwRP(7D~EAW z^!#Cb-%wVWIBJ}CG62%#7#nfobD{?fUV!|!&V8kWC^jqUU(B(*zbgTJdvJ#HobB|M zzQP@H`z&r3_`bpm`1)~^WlPnKxoSsN=D5|E zu%sd^5|9i|m-75aA{r8bg-498U>Eq`J9-}&Q!WLCtx55#y`(nb%n0t|L&p>nhTzFr zh?URTc^{|;f#7Y#VBzqpNiArw=Yosh*Qnuy3l|C9@Fea^c>D*uzczXr0_GI2XE>_y zCB#T%ejZHQ0;`{ehY85lnldcW`|2(hVTts~J}!9>K;l{`cEh&gMq|^#usdCKbwO z)6L;b)!{u;&o4^?1>s!H^L|VP^R2^lp|cl~6$5O5m!67|!8n4Ebv_7sg}Yt-s%0Is zb_@}JiafkX-jS76;zbQ;CMobwr$aLtR9ljxZ*2#hJbwO2@`1C13U~k2=ryf)Hg~y7 zA+2vZ$B(RgWi9(doc-9(;44<23+zpF`xv$Dczwg^#V|_}LbGh|LxiOU6v9~N9;;~m z;hc)~Eg@}YKhCU`rT4y>InWpRdUVz<^mm`5%vc=H8Ta;QkMV&|rBfS$H|WSdoq2qs zj?Br|o!GX8FF?sR*Y33N@Vp;%2CS9i;IE|TC6noOI)Vy+*jJe;`dA#2tyUViv;3 ziwdn>7?{Y5aKro48M6q3@QKt(;Wd$Z!x7^jYXk56N{$Qvd**ki@Zoe*3+R$>;T&3#v7BT}Apt4(qioI`-zEYm~=|IBJ zb3f1u?X}l6uhDMsdzdLPLMuJiIE{nPmvz?4gS9UIedU;*VON^Ftav4Tz~T3NXnQ+(}9lSeFM*v%Xz?{0;F4)JZOA+^{z!QUrbRL zsn&4F@?+zl?Vx38 zj(h2SO_*q@X%#KSZipMVSKEQuE;N`&=LMmlanVSdniKa)^o(dEycatwF`@*ordHgd zX+HN*sAH!-9F>1FF_MZna#CKCm@Al><5}S{TKN_E&@|Jo3@t4IDvEuq5;0M`{C=D?c1Pemcs zw(7tZ0V^X2E@hNas1<=kBz%&#?ql7;%6R%jW$>kZV> zGAAyq+HEJ;{T$|l^KP-nWS6wiebBx+SlmMk0dRmrj~zF;@x!a&qWorh?~GuHbmwVd z&U^jcD$*;>C9LvqG<5BIQ?{GAW&-QyYkzUAnq```J_mm&r=PqIbC>S0Y3dP9D$K1Y zv%I4#c>P%5ZI;Q`FWuMbt|@$JDVTpCAv2u5`KYW$z=Q7eeS%(bL6QM|I7i)Ywyw_t zTvE~r&=&IBrf=3GTFkM=fz$-csXqQ_fi2i@zd2I>GEFo1@#z(zjmJvTJJ>j_-!MI=7jLdzb*$1tK>wvZdRJ6I zg32+`bfA(MEaias)BV!eP&c@ifXni59M(3^$U@U2XtUKRITKZTJF4N(mcIwKC!2PxthM0& zxEnV;lo%1{&BAujoi3izha6Gu!rWN@cI|ip0uhf%MuvsRj)u;B9Ld)(>;o64nX)V8it9r`8&^`(i1^eJRKqtho>W)k z?=xK^O;hO4VZqH&qj=`uhOPiCT$JuJReh+%19?Is7B6K*?A^eak1BJ!76#DD7q$w{ zbgst!v!d{jrcy~V1wkj5Wj;eqhuA6wx_b*v2YAf7j5YwpR~&PY;1vFlqo5Ow)^gQ| z=~do0U)k80>2ofiRNCV5dKpF~AmSYVKl&%qFZRlc%Rz56^uDqCj3BR`otD;@={vEd z^dL3j*oq}IJ0j_`#i4BK!W6pH$XrID9V*c5NZ;;~%H>$3*l z2R(LdNW}Vw3g3ZJ0~?_eM1*BWsvBzCiFt8`zfbs{?_4tdzEQ$(KcyTmWwm32zu&d> zUo})StcjA33{N>YA}3kdLNYfW((tAvFd*4T^d^U|6OY@P=2WU-5Fh++RFw`` z3F1rB+ZKaSY|7L6&3i%{x8CxyrKssM%cG4OI!>0it~>6>z5K-JWWAE>qm0K0^?q_2 z53JR9)4d;Dc7w6^Bz;~{|9*hekf@M&US{sJJM;ayJid!y^MvXrfvtD0t`}5bx?Yg^ z0V}xIwf93+28O0>sh_FPV3o z{7?dQ_q*>)wfwtt#71hg(T}QUd#>T;5H0!cl;rU&!&XX&^2Z`Fb z!ikDtR>XxJz1F2&hSeE7rxre#gbXn?&qHY6`kUL>0x&xYaT5I=sg*iXe>tP>wJV!V ze~(31(5X9ste~KdY7lmfKf1_kEUVP1m>77)otJ9h>J{gIh3Lf^G+#$kc)rX1d;6O) zFBRA7OvOLlgg*iY5PhgzQU$c&zCEc>AHZ1y%qjZD-91J1SDyaE<^Y*h`EK?f#j#A% zZESoB9@Lj%GiL4@6hH!h`FD@j$21C;$L5NO`$@u;pJioYDcOC-bhuYkx-cp}Q};&W z8`Q$KOTfK!&|_ZJMm!sYxW(s-F|egDP5?~k)=jF& z<434krQYC=1kaTZ@veMB_kKZ34!{T_j9;v)@#5k##BR?%spV_0H+6h@IZ%voY4Ba` zND+DmQ`s=mi$Y-X&(gs}`wk2Woq{EGVf$Iho2<)7*r8hRFbe&eNm>=0Z>UR{-A8NF z%Fx&)LE|-irX%g_k|_I+ZpNok%mgvZ<3;4fMA9kGU3GhQ(^Nkdh+<=lTy2EY8G2IJ zkX!-<8qa%K|4-P44nx0sLlCdWnce^ zME97_(jP_wmt;TreVxXgnj|!`2 zy~c<*U?6I+rebGPq!<0h{Jw&8*Dm<>Q?4!WhI38H5r%t2i}*x=imUgZ=%Yhl2O~qGrCO zFT9djlmStQn+bPEY~$5O^fak;sqb)Qyzl;^O8-Ex<5nNTgIG#!YVHCSpZ{pE+}M)( zDL~!KkCg*VxIoAXa2cpHc_Yc^6XZlm%jP0E+1Qc(TV`zKKY*DVm>M`LzI-{@??cu3 zZ)C_gM;71s?Un_US$u@( z-zu{Ae>vJ{`8t>& zL^>ps?m-CYsg4^dUmkyu&lAzE#aI5`EF2Yb0n9Q zovtzYT>De2tN4QcY>tNP8B}cBa(K4an{Yo0r1f$9Os;3nhsw(?U#ocr`6q|BW510X zvxg>uI(Agh3C$gQlEGsE(*1GCaz3ke3_W5S8F(H}WP`l~RWyf#_ZudAHR)GBLuF@& zzGxm|Zk-YgVKaxIxZYR!57_v4pTYl_9a`!B4++!)275NgsY{SEac+qvXRW2!G7b8A zyH}cGBX_Rde@T+;f@HsW%vS4(2N6zeJ6XvXRx&-D;0jOCZT6;O2bOK0-`>yj2hM`I zIdfnvpw@-}c2{57%QTv`11B>G9-WGa=$`)myTHzh%%KwW_b@@L{MO!`{*UGVC{X94 z=r=aaOxNN!Y{Mm`T30zDTv3~eANHj-zkSeBj(TISQ~zzAZMgB#>YoN33@$5F9rnl! zeqlfV8YV6v8|+3m^F7*%;L}&0taG26Z6M84c%(%m?Db^oc+=X;BsWWjEyb?_;%E_1BK2JolJam*B zZg^fl{l+h95Mg~VOIb^TJeBuLPQbSf@K9+*$i^h^xB~9MUL25zu2Mk1sR=s=sglkz zfTot7XB2=&MXl+;sH;A9XH);><9DJsuxboI2=G$9*uM^JILEviA77y`$NPYmv-;uvVs6{`d+yWrZf=YNTrFFrx%+&G6P2@WBqSLR4!;zR z(sXyYtUq?Y=FV*62HPUJ3*Nl|JUtbqrDK1;$&_9A_y7if!0T^r$U?=h98Ax|!~{4m z+?Hy8H$HC^lQnM|`><)}Hg)&MGbWT^D03<{m_PPr_+iqIVq0^5<(KrpxuQ+21(AF+ zTD1Jn`_Zxe6deJ_`y*Zlg=1NlHtUFIgV1p5_Ja9@dWo?Y&0}tjwJ(6K1;EvfTwN7m z=H=m9glqt)ZGb@ka`i@_S}W7^RkK^ZWTNH46u!x!c%k>W-|d@tsmz=kW`C^YY?zGC znr(FF26l{ticbO~o>kE3#*pmTPI+x{lsl&`eWgp<0=}rjwStV`PXZjw;+HTJnSczP z!T!FXHju)M$u-ieCiVPW2ME}@Ojq^&8~_aGA2i3@dV0ije0_wK^k;o~Hc~P|pGb*Z zd`z3{?`@TlFAM?7*mh2-*pnDS$6h_MzC5y)T`97=k;Ws^bTH5Fie}zuSj!hM^hX~j zY5Xr*=)9a2@ z>b08z=tBdUf$lcQGlQBwSI7~={7X!->~ee~0k(S8U9z$Ycp@RBI=+KVoac=nmqvKQ zhIbG&8#X?awfz~75@3w67Lbl9#lihtzbuQDbj;o4trKSO286lhS+%66Z3<@!ROxTZ zZP(}3!%4Suv)Bc28A)))wNI}SI?gU>hHlT4Hu{(;$Hm}T5@NdhCR3iUj9zavfXQ)@ z9t2cU7ce;wyu*A()~oHc|5sd{^!DY5Il6$N#KXH>cc7YcEysbbH(05&WLMF~4z2sU{A?%z`iR6L#SfYz8O#fmHXZ%I9A()W2!x z)a3GR!<(!IOVz*`hPSv@C)c(k`ct@Gd?Ae7c$90BF6uT8trS=H>UfuDeI4(V#5WbB zry%Z=j$x$C9JXG%_BBmNfbV&^>b>Vt>Gz-cWI~RXDPS9^>0Z7qY0N?dL$O!z^$cKZ znUP`AvibIr&-crt-+{|_t@kKo{vuX-IR({Zhb2*!@&EQR_&DW8=@bRh{Gt~5_3KRu zfNeSqtoMLx9FwBFLgFLoPM$2a4L{&rR#g99!9Y>1;6=*Hi$O(-%VG7GBJ2iDPioEfO$tS}9Maa)30)_bfwARSH69!HXy?&wLX$!!c;js6U z+2*v(d*G2xh#D~B4<#`C1gpJH4F_5hyb(C`zB}S@=ZuKPYY}S5CpLuhLNc`D6hPXN zaXW}uRuU#2fMH39DOW5Fe#8>wsQ3zAJ=P=L;bHbU-Za+#hLYK5xZms<$`2;air1|b z>5lC^GeZQbT@V?Y(ShQEublYwjf>GWVJjSp|IlQyvc7*u&fniHRiC>>d9CBDjeGfD z4%L@LJ?Zy4P(IPwB;~zNg`>-v^uTZEryOuBPzqe69ls~~{2C8D-;B`Ie z=Kh*AkTAomAmY*Z3@B0;LVV#7dd#p3@nOS~cM#tJ9g`*=BzRv%WH3AXL+|l65DraW z5hgrX@?{Rwyn%cenI*#f1O-Q9BJ+apUq^LT!K76- zR!G+w9|R5Jg-<;y4(7G9D^zl0A%W!mr-+`UmA3fB1jj63#FB?E_&T-IRd3 ztt1vwRcn_r^?e@-ig>*2g>aZCQwAuM@xiI- zJ~E7bJy-86>cGA{zMnZn3#0ze+&rr({Y*&C#v-#`|5iqHFUd-l1vc^(K-u4K6rO;J zLsOVjN(zQ9lsW`*ZV&JlzHsv{&yo-ajGQM%oB*B^Qu2rG4{|PADHDi<`V$rF0kZ@U z2N!`TjRRVv()js3U*)|tq95{WQq0M|@2%Y1!`_wMDO{~_N-9p4<=0(xt2;PW6}JfE zZEJdX|5tgP(MeFlcNsvB?A1OS#tI*=_5NJpspPPUil+zF_$XXMUDu_C=WHZRrK((@?zo35nJ?9wXLOx!b2)`|Dk2k>IAK0%uvzXyTe|rA8-NM%w^xvini>mJ zZ$T&C=Vc&hN8Y0!kmrH)16Q03&#}FJt| z{lZxr$1&df%4V`;?a1T=rkafjPZL-pm#FSLcWcCxJ)nlH0iSCOB zVKObdUhkor9NpZma_)g{sdR5|$#>yHyQrjtdyeXWi~@t=ARrZ#Maa^QDCrv+tSsUM zyEAJRSrqMXn?SAm3FZ9xdsN*Tu6@5-?G$sQun>d5=OVt3Y3Ty9Aq9_wRG{zFQDN## z_;9lw@ge{Q<)l$ro(Kmxq3K4YJ&&a2HN#XfVRw>Re3s`jZ2K-OdlR%;3YmsHTE3te zfo32U?)n79V7wXMPMuIob{e%$oLzJp<*a#L2&0 z5$$=B^Mk?2jRrJLNey^gJm1lfC9m8`YcrJopWJ5G9`vU2TsL;5p||yseG;d|xZO>W z@--0vC$S7@Q8Ha)*2N`8G72wgJ@g3zA3XpjN*}!G-|kM!zde&qd~~e8RuXL&qXbg1 z_s2eTuWBdUbD#ORq%M99s8jiOS)LCfN1sjE5(C06{0ns}rh4jHuLBXD+XsJx5PZ4v z=c@wusVOMc+ouLr%O8Z1jueX8TOx{Y9Bh`bI(pS50$^b}*ZlBj{E4q0a{y2+ir!5x)0vjI&IOj z#=On=HI*ElXT)i6r&ZQJ0MkwM>vEn=`Bs4PLmC8FjU?x0`iCPMuP-QUwS4_|MKW-d z`^s{&wGG&xq~DfP;+r^3kof@>XL&LRm^-^ zU$eO$t63Z;O!sBr?5SXRvSUg;=81NB^<)bBk<;>tTYlf)-Rp;%6e}MJaX`iy)!o^9p zja>3Gy=&(0M6I0vX%PTR&k|s!1W{@$;`99fnZH-+ho$@CPIfx^!|tU1Y4RZ&d`#1c zT*`n&5W;;bZj9ZMA)LO(_rtYg6^VxLgzpupFE=;(gA$_|qusVZhg*^&%jUE@LWNHv z71Q7SW}z7QX!!W*Ti+=9k*AyA9_EFPC;@b_%YlEx0Ak=@4aAI2g-6L<$=M&hPp>ff zAN|uoekfrdGcnVu0rwvYIro{R6Ynqqo3MpM$_>s(z+BEXurvfDJ$1=joU`~z7>}F% zZ-J#H64h*-r%Ka&BTe)COQtR#Ow}xL(&nXDsrux7Z<+2Ms!*8*FGg%IvUz2@qDG^6 z=4e#5zfI*5?X<(xsz@NDjuT88MArEuG1)nDfT6Vny18P`otIpAu~pc=x=P(fEHI*u z-j@(n#Q=Wyt)ZL$juUh^;3y&WZNBqtDUaG=_3=Sd&Ig??v|xst?d`qT_X6ptsoWV{ z^F!skj;yi00W-FACOl7~8M!!qpZS09PNP)%_$D_&4*q(T$G)mX)9Z!!TLhbUfG}Kl zowJZ4BJshqp>s^xWMTIH9F#CCFG7|PN)twWa$55D;!*7LUt(?stpbus?BZ?{SVwA7o2pFOB^T`%M+t=HGUj9mEp-n$h&w2@RcZGVBuhmQU(CO}@jJ1+DlA>$mXq_adNA*ikJD6oU zaDR1vJLNkcq4~}=%K5W%X*-mokpsD?Z<8eiuspYQLO@{EsJ+y^?hPnh&g*Q=_PYUa z;ZFdj_MfA@7SZ11l%Qn+Z}{yu#fE=`>!w%!RMZUY1Mc?as!MGDdvR;=l*GCOCC`xG;IRu)_uT)TSg!%GYsHp080xcjR860tL9nzS z9%sgx)xs<3m-o|!8*{_i^q;fCz!uz;sl9uxCuPX1)!q(Y*93uz+|;+=+)4G6&jwMB zajy+6mC06FR@QB<;h!Jt2h7p!zm{j1Y63kFS^0`M3x00UVk96rDfGC|RO_J$XY5t6 zy|>QKe}<%J-H*pSDZHyBm}>Dz!;pJcp#B1c%-s`{l$VC8w8I4RyVvvihkHSR|Gnb{Wz)*!@o2yOksKxMSLy!X6~9X6O@9H! zIsrFIQ}Oo1C@&=yP1J9mUOlp-`dx+v4o1iqr#w;Xj2{y{zvn**7}Q3e@$|ZwXDa*c zDRH1LY1P-83ivF1+Hn%z*x_x$tfqxXD70tXo1W;An+wF%2}d=8Iu|LX&9l>F95+8) zZi+UNXDg2eZBTGtJI|*A@TNKm2}2;|eEEfYP(v-9-fq z5g_1?o8NMhDedqZyIc%FBon2(c!0bY!^rG61~HL*kxA&`*<#=($1ery!+6ni5&Dmp zoGY|d#0YGc0N@YJqhaLm+Z}i<3n@pvru^|!$XCipyT0u+CkA^%;Ud~zB9A6V96b%V zNBqq`n$#sp+VTfA9V+KFWLw-zcqG-z<66+l0Ayk9{?77W5FxwWMOV#$)IQToWfNdm zGA8vW+eR%p=R{7CY`(D~=^f6wNJ{bU%W?ILYrOJV__6(Ow`P0K*qX345Wliaq5DV*o)xF^S zQ6Jk{;tzxjn2r)D!u+NZaMaV4`8^+=y343FUIlnp$?i76EL}uvY=17AK%4h!ofn|C zJ(<4nk7CX{*tvT8+^izF6GQWo6#P6l7CJ|oKg0{Y{!Zjf#gI;i?BbC}=fV8F7x9Si z9?_g#wIipW(`x%ud+<^s_G_+A!dRxLaH(5PInUqKYiggdh#E!P4 zD$EttgPe^WPPA28oCgyNF5dcD2qSNQ`5#;*8;Y)VJKZ+BvjNnmsv8{NDA>-MK~(!# zq2wX1Z5ax!RdlbLW-TSs{$q;p>v1h_DIEc!Wh-XorGs*|RXfc&#gB*SOl6f?_sV`_ zrB*Otq6i6SKjE_c3*#w$UNW)Wt(^FMQ8qg;!GO;dAdyJ*tta!U)0Be4H)my=|X3^xPb< zXm4%Z%Pzzg-R;)lwtdZwSpA&6tG^VEg@Zx6@Jm%g$v56`B>E>l4^lqs!k&W&D+n@8 zUI5Moke|L;h`uSN_ctn$AmD9@>8H^_(0FNI)hEoRjDi(YwWAXkTyKO$3hr(?ltDlR zch?iOK3R|ZK8v~A(&~1z7nV|CeF<;CqqkgWcKPH}1O1QBCIbDprv7`OG(~;)?%;^l zo0uj=Sh&L>U)ASLv%pW9uVtT{9N+Z%EgZK}vC5a+8n?$brCKFCBlVQUCqRJv=}atX zjD>}mlPB=@TfXP2JgDvJi+HQd-|`~j1Iq*`zlfM~H|Cy994b?2ZRM@Y_OpkErkr2( ztFaUIba#j*;F`&5-;29*=|{RF)%;s621GL{*FHtPb~B@ls*-j8;??8N0WZ$C#H~M1 z=qP(xTzDX7kNJdml*|YM45S#wl9%Q$*=)VzP@i%w5PO!Ug z_}AOiUba&y-su{7deGc-T8BL7NU9Ni##Pf`PXZc_c)QIhd?3ru*9uJ(DW*Y>ui6#M zQEJ7A3&Q!E!`}rmg?l< z8#PPq`3RmKs!5lnY3mg1@|pMr#7#lp57F%Oykxob9161PCMEM5&AL#CsJp zc0f)k%O;SWbX2n#VH(~y?BfBW)G@Yu%o3GKfJtdO zP7h%A`^HLiHJ;c_P2T!fSor6jyU&;?H_%>&e`xh`jK-&yhq=5KOAtt*DPmNaQsSK9 z74i-n4EgcpZ3k;KlhMO|Zwm2|s*tL12NpO^i9$-us%ovXGB*15nSMZ*K%UhUR$O;= zW2v`Q!n3xiOE3l=+{{A$S>{TzA=&uaiuo|7nn!}4pPg|hn)^ma>kQpt8mqc2wJSp* z@n`Z`0~7`ZZuMRO0GGQeFahVpv_LNGgeSIoq7LyNr;&u|Kd1hNtzHkzKG`Khl;UQ1Jd{cbSDi9KH3 z%6Lv>_UkRQQU1=ND|%W%ccZ8gzC&KT4?eN&h)0SArpR&;W z>Gj0Xy(0jUw;>I)s|wZpg~xVz2BBO4Sc_w5;OYD#aY+q7hbFe~6%xeB?l@L*AH2T+ zir$?QVxkH@@IMY4fB^pYYC zR?qX+n{Sv=hV#t!{{0Qj=yPqzusVsn*$478f#yFJ;T-HX<=R>zDGxkTZplA35SVC# zG7l%Ca}a8zx(o2z=$$l;-R*ZaA9M#waL}n#<e+@5Bm+?+*@-PV0IxvaEo?ySbHk<}#A7*p9U6zhk?>%krUrL_L^rTP2`R*%-sBhfX!2L` zH{$Lnsj`zy!s7_%{wTblj+BMSMK1J6m-20yvwa*kqtg=ABw@9*uy7IRitRX=hXi-! zH17FCF8@%qZkR~X?UiM+mR$Ekw}+q2x#Cv7ckV4gT3A_cd9qtG=VJ zvZc!W{#EJuE3hFcYof|pcbVGu20_xH*)@nel(ajt7>@UBpZyLAJ?Mp8Y--_GGmLgg z-=`Ks@RFgVdE2I(1doYuG=OghNeN&NaB^hVSiQQFOMUqo*S$+i-zdLXFrBo@E>Fz@Q*k zFz}<15ab^CbMi6#6yZ%iB(K@lfytiGWgYzjPn<4k5Nb#MS$O8UTh)xNABEt7UuZ*q z7rJTPSWAGhreS=ys=ayFuXPr{Rfpqo&0U`8i(lSz#PH^3G;Fww)L4U>={ks9P(w7d zXJZP!OW+8Lvz85Ao{q7W4V^&^6=aspkj{0%#ux5IAGz-hpi4VE!%!#K4NOBg!sf_Z zI{3VHRyR)C048v5CjByT{3;)$p`F__Qm!huv8%m{#lN@1x-f@Kr0X)6VUo9>yfxH` z3QxUuA2Ll->tENPplZ0ga52RMvudAPSdhaH)pWX!{++M6T!Ac`NoIu63&*f&{{MZ^ zX!e3*EYaV{Xzwr-Y>OM7s^%>T^A2zl3ceOi#%$@4+y^>CeL+ScJP1j~U+3OO{b?Gh zt4GE%MdpMz+tZcJE`gnG+Jq2k4$7uS4cw=vE}bZq(SkW!Vg>peoMc1SHW)$93v%{w(+lmmzbaT1mbhly1U)V!YvF9q=t%y! zM~GvlrbaL6zwMnT(`zEco^!%cXefy=(sh!HZX*9GB-o+~NgHGud?WgdW#{yiH2l-W z1vg)y7K-e`HP+09Vu6O|(@n{x`-uQ_DQg+{^c#``h`eL*U)6&_r5Vg0` zks+8@(=tV8ww_6FBUwpc66+63E*jp@6q-K6?6fZYJ~e|r^So#CG*A>teBS4~_N-9y z*k7gjY3qBiZA^LL*Dk^|&8Y(yP3ZSYzG@xcB*9Vs^Lf8@I;@^o!`R|JYdFOhdZ!W7 z3Eem=n0#j?c>c)+<&))T)s^LNf7R^<)SuMe>)L{&x1Zvu%dMY0Ji8P2iYK`7k2CQT z@o84hh zm)P)KDBxK|)tKT>fTbVqHsY}2-plfz92-Z}TjFIBDU+&qK~&tT6MeV4vr zXu~w7m?*xBx6Lj7gb;T9@w*M9o@mQ?t+KPV&*rFK7b)AteG^{QNJma|lEslGZ)mgO z>3Y+3)rHESc8^@^(B_>kR6~_{gG(nmcg}0HP_4leRrc{kvzi0?19z3o5tf9X{#9+0 z{n;~X*Sl&iaLxk(x6c~R_v9w$_AiE$)EJ>wf2Eo{gIwC@PDuXk2*l~EhbNai3nKK? zb^UZPWcY8>FH#U04|RsG7!gKyLxEd%IMLm<0USn1C$6B0M>r?}OgPchIUV4I=(yTWG43iIE@}OBhK^D-w2u5O9vPo`g388vHN}?@i9f zosy7bPFyN6i)1W)?7LMEG4{b)s^oD`l~cSsdM^atxB(n(hy!l|FL23}CVwJY!bziZ z%?P4oD1Hy^0wrOfFVJUo;09X~5iv^I1QT_4vB3NIK`7Rm{COUaS9|+NS+s=NT?zdb z*X{cM5%%3tNyp#&Ewj`z_m)aaD@SVXL20IyWme`)Qw%4XnOmST_rQ_4N7{Ca;=qv; zO%X><+_-WfqJT2|@%f(Lzu(XIKhB$TfOB}g?tSj_JojGdX)TYur;nGwhx;1=I>t5a zkoK*)60C6ZoBXYUyrRsLLuW(Xe*w~r;4}_+ac`K$rSleiyf+qCc7R~bR3FrnE=N)l0WMaR45 zWDP{U+-?W4wPxqEi+uh(hhZbxI1y^V1RIf3n~9a#*@J;6 z5Fg>SanBPuDKGFSJ+v1L-qc)I>%(OK%gW5P@<~VfI()kMu!%S!mk)IYGKYbXmUZI6 zG{p}{^`i&?Nd)%Cpk2HIJ&b>z_q|Khrg6^^b(p_Q3wn5mnUgF8SIO5$s`rcZorom;VkCe1roZ(mc zR>VUhO7UJn!1`G6^DPaumxvnqm=HS*g0n>c6R7C=PVo(t$TE5#x3ggy&pv*K(M-gl34ChuQmpB?Hyu_3LDM4Lum@n&bEeN_bSLaI23=onhhOk9 zGB}*pEUbxxXPXSBdZYI|YezNK?F3UG@&yYK&)ROQ29S?kEt~&t@G7oQv{W5$F%`k( zU2yYc*>OU7$Mu;opqD|~whL{DX7l5S(DnsqhnT`ri5 zYE*)uXy)=;y;BcgbZeydoQM?~eD}dWJB4g=(Pi?xituS&{gjI+Q-0E+I-S9qG1%Yc zq)Zdx9`H>~R)YKAf6fi~DrxCiTYlJpyG@scz0qNfe$`+i@`UErf6pF+k8 z75=;X5}VpyCm@hL({79GiQx-Z=`{=ab03@bw8dAW=LutgDDZ1@t~xxdNaL}lK&v{1 z?*4bbv@K-j<|D_E2qjp#UJpVdM7q6rr|1vN;+e*v13qKTL?3QBwL^H8GeEigBjDNw zWx7}% zTzx{Sp;54|fqXSBDxdK9%jA0|>eUyMOP8zuH7p+!*}6-+6SCc?`qbQY3}{#6q0cTb4gl->XaJhSQ6K{JH;FORmvz|;ZuafbQ3{G zJL+A}w#-9^KZsig$*utlCKesvQvo2ak$|{)%o$h56wE*nxr>SNvQFFDQiIi?un37{(@*0-!XOM%ghW7zJUSIS8o{p7nJM zqhvx4LuYG!;B-7?J_;~7OHu=Z*n!+19zTmE}F{g+>O>gm@G6eH^wy!TB#|0X?Nhi|Eebm^Xr0`28d4;nDMXG-Zt>i~ zIdZMMH~;K3Cl-apR3(ZocsXnr`YSVD;FsZQt5$ewv5h7gEn#e-BYd9go;8i9!5;}_ zUqw%xl0(7ES;#sVzl>pDKaE){MpJ(rsNiM<1_-nO@o**-!rh_hu{LtAGQ}%_80wfHAN!=`LPr)E4Ra z^u2qiCi&mxP+CaLdrj*C=q$9yS*naK9bv98=2^Q%u3YW}FQC1m7jsr{PCjmxU}28y zM-HCkHQ%q@psKq??)mOZt5iq3dsasN)Ia$7meWy~KprgKdP6R}{3%^$;=LEmuvnD8 z=w&?O!nq#?=JW(A-Fq}Jw35iK3jPN);Mt&IRrAOzN=4E4!o)#H&AI&;! zd%dQ!(wx1w6{9}dm-;nj-^yBS(fBRx=%g3n1NW|Dd8q8Oki4|s+s96+Rq1|uAcp4e zUK{q{;TN0USGG*7;HPgQDF^BnAocr0-!LlN&UNKPd(s= zEUEi+*+Q~dKFJ)Gbo+)hS9egf-#g_s6V#BlzHV9WsUT|u+=P8v?}2<)6BBbOK88Mdmj3~?+%-QO)Ze=#ZcNj zCIF6j55SCU=*QkgS%`j?2?kQ$)cVG?20m{I(|0=DTA1}wPLEUfq%5!^An9=;S*IjI zi4Hf~DmI%$-_r>koQ7Jt9T(s&D0V7k7*s<u&d|bM4}s+uE#g8tE|Xi12T8Wai+LAa7}jt^kD_a6@rF}n zW5p1=xMN>Hk9hnitRq?H74txsz1QUi%-IOl8@bBcuL+b7+lgsnQaV}V@O{K2oS9FPV`rFgO)nz) zxso(MS!2j+b}F8jhMn9Z6PxgR4N;DRQ8p&YJS8S9sO&NqR65@+;iNZ4{BKJ+&Atg+ zG9m29Rdwaq?9c0*nE+6~*D)Naue*_rxiuYf>zR84fMI$re( zxUs91XWccV8`OOd{6n&7#7y5&#l+R13^X{Vkdpl@v2^lTh`za-him`jvrSIJQ_W>B zo|`n8zKV5}*~*JwQoYdh*deD~kCSjh1A!U@9Ca>MM~K3bt_@F`L9bmip-*?Q)Z*%? z21~i5mpKqp;%9AYQ1YMC0ef?pwMN>z-pr*5k#4ai_vxMEB`!R#eXn`>=EY=i2hDi3 zKiN#YxokFjTOJ+{;K4X4MctUJ-!xmvJlsAGQz=$?wy@Zu4~w@O;drJKI*1%<)$p`S zLkdWlB}mMa%aK*8q6}c=&F|JQe^!J<+hI?~mlXjUE@2!nU%d*YIhBwM9ZDsk*h?Q+ z^d|>X?(o*{#!6_dNnoPMsYNv|tAtq7uBdPKyLisxZge89#Q4*aHQ2n=;NE-3M$8!$ zA>*-Ne90Y}h!FYz~Ud-1$Sw zw#WH<>c%%Xxz7L(7HqHh^>i7N@7hJUwr`J5@Jjpg?EL8*35FDBB`o&4SV50$7T4>4 zTEi)N_fR?;pJ9AbuAqV_wRWl7bQ4YZ9m=tjbujx>oZ6&JGJIC%R!jEY%w|I%dNwfB zw`r0IOK=9Ddl&X~9bjAS)JCg*>czw61wYZ|tUak&&#te!@XMvIT1`st3bkqPQ>+^@ zrya{38lB?ye&=Q7BpRMk`O!&5l|oq0!^q?&1llce3SHy<8E!w^#IBwX%^GcMo7Iu` zk;R$Q*%M8W03Q)FJ8Sv`_+&qaAfb4#7B4WH|5HZ~Q8DbiZ6^qJ$Clwc%StDh&3Nc6 zJMXg@*Rke_8lQj`I`_IyBm%JU100C&oCywa1;7tw=s+fbO=!WH`-iogW8PFMx{1b^ zX=d$FH;Hr{;}*UBa1Ma_kK3jyH?9l?)@OaHB4kR@D>Iu`r^~?L!0bIX{Dd2oiFWuJT>~U&dLx1 zI)pfCXFp52ktQM)MZSNV1NdKfN5fOcPV@R|1fDeI?Y@obTQSnW<^bnXovXWguOTYOWJBHK3v972n)w z-3S#Im4if=F87vfCgw~}95)a0h`iOFo^}>lna|`B`z_)SE>w5&yYtb;3SY-tQ|Tej z$`ZVBF+!Hwd3)QWR^GzlupJ#0H91XR+-~LY+z4!s zU?xnm#`0}$nC#U)-ZX~^{_4AT7GnA^eCi2vF5ugr+#Ukd_Wn*0id^S0yav0ZExS|R zWtq)eGrbe>a$mBf3&~SkBLK7(-kAdP(kgeHE`_6hU9+otckO1p=XJVah0StX%0Q-E z*+A#3R-Cb>s^L$M)fWk+Hp1S~_7=(xV3=~#1KLpt>BSJiAP3@}1eDm$SyZfZO~9Qp z_pa%JC9UYatG=&6=DW_Z;wy;|hTRm>UWg$WABtHfj>$$n1O7M-`|IDcVj#=I=KN%( z1slFZSa1V+Y9ad5%imU32B}|2`OU5sBqJGiZ&w`dUmo>>>3UmkP zqHhiH!6WERJ9D{(^Ws?vF>}%?!kyfD)}H%J;kyj^y=7b#+9u4k(IJpc#MO!@Z*7uA zls7_}CYwWj&vg{cJDl69UgqFrRQa~-OmeL>nZD(Htx1@4|AgAFvcur*)CA4v?;f`o zC1+d7IwuyG*b%`Fz)o)X8#O$YeLR&NtF{4ffo|eIR^W+stcE8`lXLxN3kJ*O`3@xw z4;*j*)=6eCXCN(G79Tpf-yO_q82%v4%mtLQbDkh1*jIghOg~Qjjc34YHq<-2sRC=z zlJy*2o_Uzi4!k(So?ecgX{l&ocSL|^>05#9U}8XG>Q1^t8MMh=s_^T=ZcCxt1=@+{P!N#o@^!LsL0wg;1a!3( zYp9rI%@A@E-0Q4iSr+c`1^+jYzhIKX)+uyb+~NO#j0bdR3wyZB{&I{W_D%|b|Gl0~ zL4wOl_nOZ~GnMFzBFSO%iXXZ7IH7-njSoy+uK6Pa+`**cpUGS2UOVs=nhZU>?9}my z8xOi&jTGR_4_^qB0_%PJPzNY^EGqrlX#_kt14=aA_*FH66E4p(#rdKAVce)r72AIy zrbG{=S}kPjrmO16v&7CBl4N-VPH$~oH=OlIPaR9|jd$s@zb;j{Z1*Y&G5HfZJw)P- z>x-aQ6gh(EnF3w-ez_Hv_t@tE=F``q69 z7C0-|2xUa;h~3C?tWI-Y`K0tZCU!*c8M!8##anFo?O>h?;=k!ATVX5fCDFC)iW3!m zjQo!6igLJ|Sq3Kjv6JXb{U*JG%aFpZb@j zqizLB^bGY$E#B-Ed45i~|D}l`K ztZF8lmD}>7g%HS6qCba^;U;m1ZMX?#*34lByEbU{9$S|-RtY%d?t>9IWKrNT`J^G8 z*n%LvlF-&T_Tev`V6%~M2E?rvY#}SN9MtN7paoB1#9iOTZG3Nfs_~pcvK_bP^@^|` z-qF^h7?vCz*2w4Vy3z7?=a^|}g#bK%$M}s95)HDJBtmQNEGbR{S1Ybdw+)W|2o@3$ z)Tpc*ZMvC2w2ON$OKrm2;byDy8FTt*G}7X4oED80`KXomzuoSO|JbT3U6D>@?aZQP z593-ed5$QfRs=R{Pl3kQb8`Q%FYv;{-#49)-RB`CchALeB-8kV@8P$gGxhix4Lk+P zNYVLw`5Tu~2aP1aUnRP}Ib(kV7|KqIb(!I@h*S}}?)EAIjR&pru5 zsnVf$g0r+QO!Prqj`61iUyK{g&tq&04r^Mlv;*`SpB+o@dpSQsQ{P z+WHL{uKRZ{->wq+5#nR_yKjZeMU`-golcfVZTm`I-jKsLZk8fbT1nDezREAWvB-nU z!mg_qqQ()a`1PNF;^3nw=rU*dt*%4L?}v#DPQDp#m7lsmbBT{7QtJ?-*C*LMxgo>~z2b%Q#rRn5o1zNYiT1={C zgkDXZ-s;pdfcmoKMA~tDIr_N<{OY>GR6G~|Zdk|+-CLhaKK}blcwi>2c^VCQDqNth z+&{RZBj&KzUr+x$qHIeoGI{s5ZWKfQSgTnMaf15ML)Gwh$UwG)SKdTc*sP;O1y9w! zwJUix>F)&&fK2~?eNZG)hqEi{2+v}N1=!th^^D-VEio<9rnOr6$ZDQ z?c$C4WsAh$sexU+0}hPf4AJ>}MW-{x|6+!cr_#=J{Ly$yJq7;a@)EfGaph9&--{<} zb>)C#1#)Cl~LGcq=zceOP}RQS61R+Ifff)uW6_{+I#rn#1Zb*hc73 zf=cu$D*2e`l0bxJW=Xo|{r7r&uy^5nk&M;U`J?>Keq#@9JY9xYrVA$)CjAHbI?_V- zgk&CDE$U)H?VO+fJrXUROE1u@XjVJlyi?SA=XBBRY^2vgHca6%r?yV%Q(qa!I%Q`3 z%YSaN%qVMNk71(~xAo+pjFtJQRtKk=lM?~2`%zXf>+vZ!)^sHQ>O!S!^Te6@CwMs) zaFG*XH(_7%SqiEJ&V9RA0Vp`Ifc(|4TXn&kNpU`_dbVK+RG9pw=v^Fwd-KXpDm>sC ztYqoech7#Ws@ZGIf-Z@+BIrEkvD1Tldr|MbmtbSB@-Leg4FgXu$mSO}o}og0Bvh|t zYjVINbMiQPc;SC|fC}Oy11xlETP-XQz~&%6%WbsQlGqHS0fNeTpEjxKVF1`IuVhy7CNiW;v^? zHvcGFWIv;thNK8sMZsUPT&v9AEjbZ{x8JSc2-4z*S67*9#{3Jgbkeu`P6uaW?JR>Z z#oE0c%*$JRXmxz)L`@)`>OHnq4y_Gx@(PPeym*qZ)M*qV7}u@t$ucKS$DJrRc1sgE zQ%VF+qyFhur`Zi?yD!BQs~~(8oBvFAxn|7<_U^qcSHzY)Zxl6w2^*{QW5c{lAtc|o z^fzv?HRN;gI8{eSL5it`rxjv7v+6}g zRzo*HNH$zh%C13WK1bJh3OaSpOtzMUqMl$B|a>x>%@pW`O5;vUTmH- zrrum&KgHs|r})CTyOO5KqCF?Uc>-@Gqs$!U7|o{l+=6&-eUZIcA~s1la#|vpCmePo z!0#&-?i27)Cc1+Qxc7yFp}XY#!6=WRLKq!evDBD9jTik0O8!D<(-b@tHf35uUZU-o+R#PD!8crg*@}GkuK86I|;bCAuZv3YBpsGw6a9u73LAhN`HeBd^8M zT5yqu{k4x-x~-*{8L{2+(crJSORr$dn1&don?9$seLRD$IqCbRN&e=MzvLecCKzO2 z^!TidCrj6$2IGlmxHH2Ov=9-k>h>>RD81ChxxV$?;_P}>j|__Uqa~@@T>2xGv+%CW zKHOm8*6Fq)sepb)^pk6V-=|W{XXdFIC#m}Yx3iwb-Pk;vFzJ0QDkuzY?Kg=rSQ;GU z9XF9M)^pt=c;J(tIKKbvNwaWNQzAbF<`gPE#8^|D`E3q%w6e{k2xqb@kUIpn`9O`i z*NC#xFF!txl>heXUafDts)R#8%QEJmL&1%ISn)K*MzodV@t66~s!SVXsV4r>?epCH z39bRKJ2zm7nn*TJ)NHp74uoi_hWWR)DpMz4-8TB)yn+8Q z4W8HV+`nI(`GyaAY4gJ5ot+~oJl`Hl{*8aHv5xb;_i$VW5`@3#E2o-sQ3Mn;{5ir* z)n+mLqsa22C?_U&Z>YSJVp_c*GFQ24KB*P*$oJ=0-oG0BR^X(NNW%+yESKnswl1zw zwSR_Xy;}N7``5L;6P}!n%xSmYXsH-%&m+B(eDR7)7^=^@wLNE)`%3>q^CZhuT9B8X z`NL|{(t9tnvjdR|e`S4HD9ce-zbdL?RtvcwZ`t?ffM+b$#63**nm^+>@LYa+#G6)x zcxEFp0-$|K*4|>C_u7-DPFWGTpP|v0m|kTnsLmOEg}r0D>0dMEPz^)9?D-J4+eP+3 zLUH2n^^^d>XyeO`7PFwa>X|gawGf)-{w<<1f%6BsnS)WWlgM_Sl{90jY>j?XY|5IY zjna(g`F2(O6+QGeI$$Hiz2bji%g^%l=S;q$M*Q-0!78|~iLbJ-IH52HBHZmc0l=RL zjGtI+63my#d8_NDQA^fN@x#)(hORgl;hAnGK-H^SL8pP&sdoU;Gb&LWu-pBC2P%dO z`I+-3xshx_(ErLA?s2Icga+`JcfLhw&VK8GbV#`lz~~xbvPY8EIN3>>$iUF7m=zK_ z#WP5%+Ak^Z&=SB~y7$Lo>&Kgo9{s!dH7!@bba{ipQb+VAeI#I=^NM4J=`zl^EnF;4 zlkI|d=vKJ=k@7Y3SW-M>Q?)P?LQhfl@vo`YLdHI}%=Y55yC%9g3 zgi_yMfbuf+n6$o`Ks?)o-P>?v;kU7TgL=N-))l4q5ixrTvO{joKaz)weso83G~ejd z`=|Ri?93V86760WzPyhK`g#+u4^av_Xr$Ir_=1<2cZr|odR=0n6LC6Sc`RwGdK%g} zvs1Uc8cjI&J+JYy98?87OSRzZ*0k*M)AYeVwo%y@y7_-m-Z5vJ7M~O(`*G_*+78K$ zvBrhYm%;I0qECNvB@9-CCK{rs;2bBlmqzg?^5)@`J2GghE)w<#T@}XlZex()4*2`VLVnv zw6l?8Cw`k`gk_k%$qDN&ORl~F$+{3Z8z8-ccyVPht}0nI;APxCeq!6umwLKGD*fbx z3o%V70tIblz^P18hsQnBa5=v??-UF#e>1g)N#+sY$&EHnYTnN7CxR)cP_GCeR zaqUYCvdo>O=|v`expvtalQ*FYLWpoZAT>1{3<~?!q&F*#{@u^XEgwZ4Slaws2>ga3 zSsRpTtk=m2aNWs?>hkcssO$O@9OsE%0)9>C>XJo9bod-3CkaxnaP}uEDQJ^qhGKE` ze2D6}vL`@V`%WLi61M*faWJP*KS0YM;t)CqX1mLsSJE)2s#Llib#x`2#9Ip zni*|kM6J&%fj6BYcs3OKkKH_=etcBIO0rw%*2A3G3|`}4o6%zc_IR7E-&PAZ=Su-67v1m*)Z@#WnU z6*DrooQiTj(Dtl*4UfzC9c`(~y2~>yaQ3wuBV013&%Xer&+p}R@7s1skJ&}OWGAOB~jka#U|efw(^_sfBjHZ>E=|bSZr|YzPnV7v z_1ASSJ8UFS;q{%a^SdRCA1iJdqY(|i4yWMDV}FF-R6eALY+eICa{)dV8Q~na+i#uP zX^$nQdH9W>cEW$c_O_SiFbfVYDoUy5fO*aX8bMrj1Tz&k^6xFE{o=m1F>Ai=pjI5d zU6hmW>&co5=oKa>Ir91n=cnaaO8gF5eZ5xFFIqH&a1+UeY7L9rWNU4;!RT z0b4jIq$3%~uWeG5tY|&6kk6d8{|rp#d3fJWciP)?UX<|b6p_#q7?`zx(Yt3mrEIa? z!SoOGJ?xT8#m`KhR6~81a~{TgX@6GoWEONQ3l~)98aaM&1o6ns4MAL1jP(zis`xa1 zZN^)eMcIllR2@y5&jxhW?@xt&Q}OJ$5GHwh3e+!yB20ivkb|=YVXTba*h+Duw&$Yo zazjTo@ympON+xK37W(=4QJcp8+K1WEgPe9dX2Bw|I(uv>r!N;a;IH}o zQ&Mkm=P}x${aK9O+exOLvlz@Q_+xch$@hqOM3F}KU+8VZ6WnpwqIF+#`MTB$vQNd@Hl&7E#M2qTZOvbp@VYvAuAoY4<;^Ah8t*>Y%GxI19;bkvzhC9~&Dh9sqR8OPpU{?$vv?B|NefTN&awC7qFnlfI?N zCRP<_1CUxMrdE5E6ndX1&oTk=;*h?huk_5TY{Epol?5|M;yYlUrTt3#mHs0QcI`%f zZnLB$31MlmVxjfVZu$oxz00v#yQ7=!ID=YqAatI?wB2}}HQfn{AR0e?^%(|x%kyj2 z5}G5@)NLpz}_?r))`8ULQV%D zdzU$Y&OSHrCD|f=cR8Inah*=X4a!aCN()W|r;#|XpxNtYp2l=^;P_(jVCdP6U45Yh z0@2I}9j;Dl5z2&+Ze#Pyyoafs%ei2iZ5b}#7t$HtT{jF?jB?A>04FAvJ#9$V1p{{_ zWmsT$-p|n%GXe+WyxwjS&Bh%DRXq4FwN!7mt;DMqSB`FCG8E~VK8-%mB8g)v^qpl< zB;S9#ZXb{|AWQ8`4|g)>$A4UzvJ06fn|Pm_K3+|}IrKQW_Q72qU)Z#w08o{5o=4_x zSLgG=8Z>VxV@)ANIteyXny|z9adsfv#^-N(YLy!#Gb7|elFSSnY?J|nFl|^}9ws_8O;`T;_;ua0i}lA=^EWz9QyzcsfK@N$ zUHUrh3`|i|OvSfBG>$Q`X9AGpH;o+kAP@D#ZyKGxuUcdEHA5sY!pCSzk=^&`NLiT* zC<#%fr=@ky#RuD)th&-u0rLuB(p>FECFxj=pyw&lnu4?!zKW@UtseR7d=Dn_S?zI; zVwxM3e5H*aUDnx;6`}JvoWDGtt!*-n+rQPTW%fV|K_`Y+yT?V!rA9c#GTvenVIBnN z6Wkz}Tp^gVk%E1Gb&rkxk*2@XSiLC$AXv^fr{x#>y6KX`muPi)c4FwAck~Rltq*0J{wuI!CiIt- zK{@|5X2r&su*(&hz(rCb`3moi6py0MU#B)svzH0fX^x<0FwpDz&GVeE&`%56d{1ft3+nyVVu4$C$96OXegDJp6FCWWy$NY+`@H`qeTf z_SgEAfDHB=6Bz?nP==Zq3pU`R6;}rS)I*^sb{rVMao59KFGUeJl7%CLcZBW|C2F(x zHBI%{4ZS@(*eW!+E`OU_V8MOl%0i=YGPfGCnLsBAKLm073HP+59PuO-70wIff*MG9 zZ~-?k%5hL@>!qMiGlfr>J^4Y!dqcjf8qa<;F;QgNA+M#fH6OIu#D2gsC~f~_d1g&? z^yT!*z+@+C{HYY^TxcBeIjtnonIF$KL#M>9zIstqVC~8;iu?c{sLLrQpB4v2QSI-Z_|_T7?jg(W41DUq zN!%Gr|L}jbI^1R*yOJVu~ zef{Wnr@-};$Us?Rb^GHZKgIL6f}hr%1rI)n^m?H0{|s^-NHc+GJNrYyXQ z+2%LJ7Ut3?H%yRxhyP#?2>}g=@Tt^J#M*tM-}U;FjP$N3UhD8Dz!%|9{X0CDOpb^& zA{z-JIGZv-iGP2=BZz@vcZCh-+*meP`sG0o(E&F z8^U}Sw5=E1d@aa8?=THe*P`!|jmTe!63V>>7=W!ew!yZwYfCrECNo0%A=majQt{Gi zhavr&*9IK?%SXQ+RIo>rRpXr>LRHG`o`(8M;)}~C`wL2QySI_IV8CDZp0U2fQx|hl zbZ*CUT~eH#T-#CtRi8jKo8=8l_jye&scN=SV)9P8+qJ{_)?f2SuQekcl8F_MC9a6O z_UFFR$hV}wIh(pv+#|14iT4hL0e(}LNYpdp0>t6IN4}kR_!3DQ+M#iUfK-{I%>tX+ zCpm8a&WIK%5Tzf>Ki-5elo6)b&Nj9r9OF~+oX9(yJps}xFioaJD6Jq?gg7ZT3r+>*Ze8*1nBp<&ar~Bom=bc zj~$2Ak1OuCBsaep&4Nf=^7x=zF^fJdtn!ytv5pUv8k0XZBtvQ)*&1bcJTz=VYnwt< z&nd9&4{uGs+}!ENsayEPuX+8r>tNpLtEP~c4S#8Vtdj1eDz8jxS|gnMU2^w($F~oC z+Yzk`W1)PSe?WgKwUxYNUwUg_64%X5tJ?+m#+_-Y=T_9<8(j3ZRfl75nZ9Xx|o@amN%Ml?p~KY9rua88b=i<_V@C6g1EYfBvp%8d4k z{#bnr8qld!(j5%<>qbQYrUK&dFdAH|yy4jjOmV+SnfRU5(7C~Bzy}8Y{^JeB_~s4| zGpsSkD*#@8Bas%{pZ`1?=dBQL*Lc;p(kN1rW3?~DwnJE&jL@$#tSvAl?85war6MCp zEXY9%NIpslcC>7H)vqcP<}(LsriwzJmT!FMKDzqLpxmX{N5P8z3$WMu3NI^ga!q@2 zhf#TUjDmz!q|{$>)bi`yxJ`X;t8k%pyyu7u2{s&Va{LUhy9bzXp+XZ+3VbW=cr^>Y zLMC7-r4!9Hv_I!&^%pYLxUD*76~%eHviDFnvyG435K#fjuT{D)a(|u1_bCMij%GP_ zP7eiVEz;2M&E1v0y=&3m*bR*S#BqHJ#u~ZI)sRucpqplh!nRn6kLsaRCn9)kYc_~U z)H0w-aoi7~wtW7u2BqGUX?OGk%1EnUhHY7N!3Zto8@kT72)1g6prmpTo~=j5wy3{6 z&l~@~g4VY5E0m_*)nKmGHv99DHCun&!+QBR+&(^g0faw)PH1=A97&Py%}y%Iz4smXx8jYZvljss34TfH zb%`?dswfSZf$*UJ82~d3$3%A}}fGkiXoH(!QVWWwLOmcKSeGf&cW*w3e1M zPPpZIqY3{hx}c%9v|mSWn@=A=#YyUlW?$LohaFc^oeg_D^upY=VyHTZHBMr-`@@&~ z+aP`y=wEOuU?w9fT4v+1A?zh>t>^kr1L);&->H&)R?O*s(iX{c=0zd*R{w;3D08T0~)! zXaXFHs#ppjh|~r9WbFG-+^Kq6{S6>YEuKEuFhS6dG`2FM!rVu9M5^t&w<777u;(u{ zcQYQV(Kh&}yOAE7@}u;0z(Ua-+`=4NBt$rT<>R!$_(>55VqUr{18DO zawmAMi1SXFb!Jj7Qo*#d#t91x;OSTIeyV^TvutZ608fF!PjYo$#~qG4SE& zOZ9I%ml;7k#mslqUquJcm?n0*Vuz|FyLzL%Ib+(E(ApoM`lhww%0*Nf*3$0l1t}ic^k}crD`Lg+X9#Z6*B*b3k@=?U*5bgnsaBn9D$vvM(F&!& zZXmSq@XLfvz_YkL#=nY4b zWxtFS>m5W3%$9}-LdOZZp7H+%QAC#6CJu{LU?~lwCj#n&RARu(X_uT6&e2iLx2oA+ z!8l#6u53MIi`=ZTiF+?y_Un7q3|2>93pKMjlr>$0Ra#cr3rehOl~QwT0fQw*;hLW4 z%;%rsh*@y1#muOr~tXCg9eMw^Z9WjL@ zRpRfs62``M#NaTVKxApK4JNTgrUQ?Hpdb(FqN-;$+YYoYfTmACFOS*L3^AFDP#T)7H z;Stp;o;d~PrpLp&Nhed z7xuzxuV;9M2OnNGR|3|7*&gDpM#i4Ybf}%&xXp=7)o*dt_759*0x5G4wpj(zWN#Z7kQLjJH3UT-%4{gk~C7gY>!)v)%y zY9Qjtc6_@IXK@40F(O1o9lGyz&h4KZNa5(1s11d2R zYi~6hILkZC*oFePK9HC#>JmHEMpP)E^PbLuy@7f_daLc{M@Byof#YJv%6)x^`^o3v z!F~+S9|e$lzhf`VMQ#?M~|?#C6Yp=H&h~`51T{H=4-{IOB9>aFWhO@ z^aL{-vNuD+@?+{AmHeh<-!Cy19D8Ne<*;L&Ap-rkb!3JUHj`D(L3x)}7p{MGy*@A6 zvkTK#=w_;^{38V(mo=KRiRRX)+$~EDKAI_-RA^1efR)^$-A>+|J~UX0fPnOT8q1HE zA7p?0ZwQ)Xmpju(X~ySW343_iVOx9W%*D^|pR5%H>f}BalNWoBPbzuAXzn(V7ycG- zXgs>QVG&so#jK*-yg+AR8(RT0#cKZknY|HDMb)55Re+(CCz9?ZiIA+ zAgF+VNOumTLAn{88w0kHzx@Tj?~^^+!_U32X8M z-UpVVNeFDlJXi3w1N~LTOZo(=s?Yoze)Mn9ic-74_iguU#g5I5C%~Ss0Z-1~%$m<< z7EnC@{?AQuj$In}Yt#JS2A^}uBdRYpB6G*#PyW>Frc|Zh0FqJ$D+2i$tFG``8xjQnA=}I*#~ZkHpyTq#ynO zau#WpazBJ>Za%8T^K=Kgsea&T?LfF%k{A1Y1cblF!;2((uG(!*!xj^p(w`mK7Cd>L z!b3H&F(DhoJq69WPmoDxt`}`Kp=WE9%T48*{%bQJ2sY4lg1o~R5GsyHf7f1A*lu1{ z9_?FQ*meV4`vIU|P*)?x$oanrBjR7ibRFH9S1~^?+GJi`%3)9N6!Jd3h@H+1Vd4R~ z>jSa@%jHaAeBQPKL-$e5zsHD1gyi~CgP9+Gd+G`|kYpY<1aSi@;mKHkxtNf$mb8V; zrQOiWGFF(BJndCkPt<*|IopycS>eeEBjYizrKnR6{oANsACC`oua-9gaZ>L>Y=oR0 z24h3^a!P9BL9um1Ep|5-2qTPYQmYD|lt8hTKBF;= zY`0%+@T@tr^#>?9=o#Mx*r53~vV_7vf)5Y$=|F?Nj4^K0bTTdoIPdq(+xiT{iK0_+ z44z$?RNj%T`poc{L*5%SSzMbU7XH~Ktp$(o%0;G7Nh%BpT*@3!AMUGm<)i2wEG`0! zy9B4QQ%m*P>G70-vT%5-<$oD(GBb!4h^fH5tLtmUtM>BMT$+2gG-cA6GLnqv#gsXc zr0DMfY&YHLQDzQYX%q>KDuOe)TC|@v%>(zv&fF>H+I_@@=^jLLt4cq~eGGSmvle;P zSf!I~yl`7y$^Oz&x&04T>EG0226_ZZA_`3M9~koohUsg59-Foj)wPN?=_Bhz4Q5F4F#9d1%z%1}tobvH$Mf&DKY-*fa1zm31Q)S=5PsSr|a%wh{ zla3}V2&_UsTdSj@^+gnQR$MV0;QZC7lP3vZnyM(xrI6vNkm5BaX??EWp!?$Xdup++ zP)O2B-0;c8g;L9Q2GCqQazl1@%=-A7Fqi3EJpda#lqFhEa(pB@_p=4PsW$A7*&`dy z3hZI*6^9WL1)WLBz?$?pgK5*sBT~?tEC7Mzr+~>8WEz(uP>Z^-x?CR47N2ITZND>% z6JFMsxTcfgR)-oyQyc69df$OXuK3XNAEZ06!!Lv(bF)@FdxrMr$Zw_5A4A8Rh>^kZniW-_neY^w_LJ>R>YpnYH9o&-(Ih7A zUB&cC>2BZIklS0#Vc`@oprQND%Mjz5YM8qD#pAa{z3!&f|w8LzSXA??R zc2@qt?UDTEwBe#fIk@7GONQYKbKX^?|ot9t$hj_Zkc z0;1lRsM^1py5(f+GP%W6G{u01Ojty|mK@TwKHho@Zend!Hz_+eI$Y5}Ng}RQ9BZ@= z*@G{|2rD;be`T%P92xK*V?oTjv6ECtm=FEssy{5HgRCs$ocg|P-T4E`aU1hD-0dg7 zjcV)n7zs4O+LD(NRf4l9dihgwi=l@5tW&}li1Q&TwYTZZ&Y6>Iq$UdS#PE!wOPh3n zmni?`Je1o3fHzl5O9Y&&+;ImG+Qs;<-x0GWzrInvS>OlT&pk)(_z+)YUMf}B|At}T zViKN`{RB<}`KPTXYUPj))za_0c70C@W(}&#hOkwwcs-m^&8WEO`Kv%n`?SbwA- zn0p$*@h6kN6^p^Mq%XM;MpdbBlmCqJ^0utj-k*demsf%B9Qjn(`_oO}A>V1sBN?2K z!)vniwWe1~iT2;&3J(4sezKjS$!*K+P4Ay>ha+drGUT|g-i>6=uthu+$n&`L|4@86 zEBFhx#i>SAI;uC8ZfGP@0g=Op+Vod!EIVgGN`Q;zGpe+9nctR%^}mB32#*+#VT z5JUcLu;|T?6ai@tz9;UliWsi{{$S6^|HFEg(4;5+)DHCcq$~S<%U8-Dbhdk=>6J_$ zuh_^Kr|QIKy=a0Dvi-RpxGRWM^z+# zT6tg4CFY4;EMNy!OYsEy$=+;=1QKFf*{4xy*(*4- z?oftBGNVrLo;t3x9Qa4F>f*#33|wp!803rVbjeFJYIjLP)&7MAf|1R4&ya?GT<#5S zc@D0;j$TZpVD2NoCqaEdQ*E2+ha5zNQu<(jlS%>jO-_!2J-IzcnYpi--j_H#iHt^aFZnQ_k%58 zP9YRk1PTEPxZo-4e*O0BU&)XrhG%Uaq{eNc_xaP~Q7z{Se2tmD?ROYg*SVJDJ=>b3 zyVS;d(S%g9YCCXCtCdE3`3#0t_)?Sg2XE{lqht7K}|lBfOG6UXf{H$s;=aOeP|W63}_Oc zwGaq5RM<`p?21rPphtH1q@O*v=#9K7Y_tdQ)KA!~?1b0C96vZP+Df?T#M}BO`I7Z_XTO*9Wv4ES}GkEy?}s6z<}3#4R{3!B{3A zoN*TV4jr;aaB51e^N=x&7_WNbD=I!9aL&0$%O%k=~O+B-a*a9DI^|7lF zQfguDm2MT|He5`tRV8gq#IDL4<6zY1-BV44btqYDL2vY_Z5Ib^|Qt%jlI~LYEs|B;(!QCmj2gUmFND z*qzK`m{Q6|=scHxR$;{b<{J~JsBq(RL|W8KVIvYzmr9aVCw?eClMZY~?(V6OrA$sR z7fQfwMd3Cgo8kk-s~o$?jD{?+>_6Tn|Pr7{tGC zOEl`pm=MgJOS``Um^sCLX8&s6S(~8e3T7MiZKh+#T4w^ngIO4)ORxmu)Uibzmmanf=zqs zQ!d?VbW4Cy$aZJNa%lLVBmdO>3t4QKvcg41!ml}Z!QW*mz%0Thy5NErZYdyA1MOK^ zd?sq^i3m}|BM22xa1>)g*+rAKuflCgqpi#hxKuW0NZy4=SE^^B$`K!Ox%-X*U!VBG z8QxEdDVBx%uT$-hubg79f<#^q!32sFb;H-;vpu4?<#f}x&Rg;|Ew}h-U+BoQeR*28 zxyxNuHK@5Nc%_WFExxO_Y_8#Ucg_hB3Xfz;a0^)ra-)vthZG(D>PIxA!@dPDQB`{= zN9Qs?@2(#5^D2=LI3g=;=TtWf4<6LPT|Ujg7}FbV>8H#bs7rW@R*tB@Y%v?6A|;A4 z6+5YFHH32tfF28|F#Du?^_Y2v8bON?Z#icS$kQiI@0jz>@FD%>ZLd~TUR}9YT)Ri$ zuZ1mXX7)Y&z=7hbVhqcf>i}f_ad9)F-=M{Cx3TD?0KI!h^V;bmCtGFFLPdArOFcBI zq~{&;|9`u%GGxAff3B$!b((lTY>m>As4;}S;Fz$y`zkdo{wBqI*saf5&w|K(b>8%1 zGTx}%16uN4!aWh9WVl{hA!DyYljG1&rz<@Y{G6=`O^KoGS;gpxA(&|xRnQ>W6k6i1 zjVKotM~K)S#BZCqXAHl`76~;3O z2_^*@I(*L|+Qxo1F%;TaxA5~RQ=gmf4%TdWJo(jo$&}l$n3xX1r&us9tMw)^M-69( zN=!cC)`G4-YuHVn8!!ZVsGRcNKdd8Xi)F2(g52eyai^SmEq_)SF-T2%MC#tGrs5*5 zib-^(Xbsp~ahw!QJLX)q^L&Y9lh%%1UD2{|oM**em7r{y9Aj}Mc3-wTfQk;2?-b>W$ zs531+DHiDiFBmP4?gZeqRR_Nd#=a5Qo5X6b7D<|cN8UME3@;r)X4t&;x&XUX&T%De zwN0lw24|z&XVHbqxRSY4Y2AK*wH)0u;59oa&PC4_ZPFSfH(qtK`Y{MpuBHsnKCgRE zwX}6*chFV$E)vEUYm?~XWX?MkK};=iPEtvU{DNE&2j)BaERDKrW>tDwm=hY*ehLU&Zj0%Tb9LxMk3E>U2`8cZJ3IUd)?>`EPeBf01nt!3VLb;GRZJHT8|uZqZhy z+3rk>m#c>LzRe@mgI;4na8{5%szE9akcUh5IUeyqhXqLEb^Um*8HnD-OjydQ3cb60 zHla;!+rud>Wy5p*Bhs!2;0+ z1)H;+w$DUf;0Q`*e|}~oWFgu?aqIOJQlN6NU4|>8U4MzUB>W9Oj;%Kt%<^%m89T&- zGI_mv%&8&4I9$kNp6lyip3MNKHbdRv%PNP%D@8sNL1$;hH~6|b>s1aMig%2J_*y~{ zRC&@j%(m2bS~Ihl*(z1FSi>Sm#(sw<-~d?`zsrGA{elkfKLq80HkSP9WTNkFHY1H! zE^>aAs!gw)-@L*EVB7^sNZ^t8m>P zk$cBdXFRCggaNVAv{}gW#rW_npGpPoil&Fxt`v`^e>ik*j`*j)%hq-RP>1Tv*a`|f z8{_UP$hp*C7Okeo?@bluQYo@q-sPAKOQhW z_2Yt=^fBO<*&nrDG*)Mae8O@((Yz%#o3^cSIJxycYpZ;+=hUzlI@-UCU8XpK^AyX&k6t=(dy$z|05A@-a=_R2p!}KS`PAJ|Og`to%tRooRxK-^UAx6cZXlXo*1jSO z8eV2EeB!R4vX~t7QTa^ewEf@aFW^O+#`8n$mB=aQEW}5u&JlwOXVHKL>(oc>kpedI(Po~{7fHNZ%`%O`2h+)+RU6rSoN;Lk9f`UDb zmIxWVV)OyLKF^LM|MAIyaIk}e&qFAkq`tye(kVx(Rg!fDr2m7BO0Fe#(xs7ZDfU>Y zvC;nKkj-{%k6V>2*o%f3a`4QE_a{4e_z9kFsFzrJQIP`t9A%&HeFQNNBbxsv(&XTX zl0AkKQcSs@lc*4a0b}-r3~akE&ux0K>``(ad7g%UL}us(ka@{4U1zwX!_~RN#kh{9rMilBt`gv?w_w zrJU$vs#kHhS~?Nodq;{kRsg3*!bjD_*)X@bL;vU01Gs+bMP=Qgu9RKe2T{vF_75NI zNUJYJP-zzKnEpuA`Rk_RHVt%?K@}l|S4zLvSMl!DoFDUWrc|z=0NA^+Ha}|GQ!p+x zcyQHs!K^=)%QCOqAOPnuIO|nuot9539aT585#)rOq8E0ksUAG!Df17>rOG|+1 zeCIT+Dm&Ig<3IGCcL=+1 z=T#0jDxwS56v~hjyGKIC`NOO#h32n3pHr?uVm)W>+5;^qPOJ80o>G5Wo&s3#uCSET znz?qMCcXV&smtiYR^BW%d6l3Xsa~(0XIDszpW=mdkP!;8AFiT;r}vN-nQJ@Cwn#+l zfqcB`)xDQDO4`K8c2YSqj8kne-fDa4wVyqwqb=wIhM;Zc1FYCRn9r*YQw--JwKcDJ0;f5{&5;F80+W*r z2n%6~mF!X!v$ZbpyG#AE*rQaLB6HSvQSiY3LH9|&!*pi6Xkb8lp%I2Lp5h5@Z7OV7)P7#KugiI3$E07B9lE~oh~BS3JmE!DAahq-S1 zl!IuB7PwSurnzZd+3PvOt~-!Tp9Jmg{$XQ#ZD0kDo!J7a9tcr5H;;XW)H#>PlRwiu z(5+`KH@c*6@W_jN*_m6oOF6!ZVafZM<%E`pEAQ}4y*NWc^+Y4=QSk)ojuQeuGOgxJ z1=uh00xYlr+?N@?bXz4oRy?)J_s#>=T7%yuN^(wrbT=f}sPs`loN1G}l z7WmMi(mwL#-%~#hr!I)eauX^1Kn=rd5O2* z_r(PU+|A4 zKR3-@2ds23*MQFWY}zna z&x6gt2hcCVa|N0&FC8p5fE zhA*Hm46qLBJ-?M#g>GNJ;7ZM~eo$U_Rn`?+&bJjVSOeyZ^`N_D*k}2E4X$&l;uX8g z71-mE?5HHMVm$ zZKiSmU>fxJQB?M&%=D9wteYo_CKvNL`xLd^{wJhPHPg~)63nWkJ%1~{IRS;%ao>ir zyqdnC4Vf)Fc$hgwZENo18CYIw8Pxk-IV%+Rz$VmYzdW{E?r6JD^fcvDKY&Zf872fi zzDd)9sH?c0#(^>$Vy03=85$$+o?g_0J!|Xmk>VfHNkMU6DX&;UIcP>x7&L*D&wxuA z7ypdpou{pYMA^Efm3qc)JO07^%QF~C(FUm&k43d6_qzps;z)C-?`ey!))=`2`Zes) zFOj(s3s2Bd)tDOdFmhnS0d>;97o>)BMIPd5#7MaXa@?v`U}zO!eXRV(iP`}5CpdkG zd(cPFH8|SUBUX{F`+Z$P?Eb>p0UOGNr3(D#ir4!5A?LNu|HBWdL?u3RT@VjUz4|2f zBR;y6IM`EA{S7ndRiH}CA@dV^*`$GTUhw*xkgcDXquN5@F%YBqS`d2qNw|-Z!dAK*t{#L4Pq6N2f zi)Gy1WThshWund*D=9`W-9(T!)V>P@=mjqOtgZdLmRKHOtIg{ZL6iR8(}1|au7n{s4x%X=d_vTBEu&EczGKyhjbt*Uq9H-ncGi=pIk_)2+TLkU0Fks69+xgvpk3*@rM_r-5Ho#I|~xx&y3(j&EMzHZ1<}sXWVvL$dSJy)bpht<^1VxK~ zgJ(KwHt>O5q?`C$kt}7}Y|Ttfw<()nZ4P%5Kh4X9=K@CkH(O05TQW`V{lCTH!Z_r; z63`s2pjr%yF5=i37+B0rE11KmgeQTcBl=)YYw_F3S>`x*#QA_$qXq?G24>blD^H;<| zw7pA5UwuaZ2zvfifnqk&K3H^NLq$|}c!M0gkJ!;Le%s9|e*N*7Z)VsC@%t@LtATx< zqcYB?tcS?&&a!#dh7upDoN{$pgKU@;pMz&i$hsuE1|@5MuY;kKUIlA}h()$@t3%K) zz3cX-L2|&y@lQ7t|JXIe7y9a~9R?2Bb)V2Y9b1AK^sQu`>}fvo zqOwOZm%2!jO3|Nq*!f5bchNK`hlv%P)#G()k4VuhbrVh}^&<1(fOHV`>x%a!YG3q> z0&U(ofp0a@)KW`Bo9*ZwI!2ABN(teLogx&KJgYf1b z_?V0|yX{7&z^eGEt_QOeM4{%G!O%f39g?~{ERk@~E&r^@=ARhCPDf9m{LzNB^~O2d zl5}Rq$C^Q@0@;9w{{Ur%+Sbg_N3&$dCPWi@Rkt`p0zS)&%rQN&@qA+*1$kGe>1mb_ zCPD}fLVrDdmwfB2H?%Jx(mvUPYc=KN6Fqq5pk@oVsv*~SJ6+Q5QMQRpUK~pjN?m^O z{4*DLENSQXC_r?$rk;4|Wk%*tS)yhraks;wK1Y)%4Dnhgn+6!4eE`hj5|nR}e~Pz+ z+IRZrP2LdXd=ay0iad`}GxVc-m5!u)RP#_S;c7J&-bmo!L?u*TOAYMul0Ow4$ojB%MW&Z?iGK9CVfg&&y0pu z7#6-fRyWg6{P?G?c|`;Mjj;_-1S{wz#bSAPl*<#Sz!w9tzI7i_w~pMe(TUvv_{+VZ z7Uxly!nDhbOO?`vS}Je{L#pU$*uz_OL65g*2FOk$Z$9jFK5HL3#`DbjVMzUqFa-^v zpF+lsC}ukm5`D6;J($CzhTfu-bII8f06nDmRrb8mN84}h+*;cADOQgmJq5}HAt56@&}lbXeJ%>Zo11u^WsLHPUbOHtS8vQ^$M_ z*lzM6*|>=k#TUPJ^WhPXSLdqQ;jT#9Bi-fzNOR=bn9G! zn)RaoC;?dF@F!!C!FHrT9QhkK&v^K1(2IcO6CAk=!!&t0jeD0B^E9^V z?Qa`(1R~ZyC=QA81YB90UhZk}{NSa${Q!4DABOob@%PfZT1eb{ZNxhE6zJ|OxDjGp zHj8=kn!CcCy5F~dpZAnhnT~vKvaA5z$^LHoi_=rO5S%rX1k#v~eirAspeHeQA}u`M z4*2v;yw%M|nc~np4Y@c#^RPYw#cZE<>B75};FRu7;SA9UV1}));D!0a#NX5@IJ8 z2hQmAoCj;B0j2(QB9E}(x@c_OFb+>j*(iP&94bhjwdaOd359O;WnQgCEZawp9?8G%C5W*J zh<_f!h=3x2C|5rWW1EvBH)_yrFv$85i%q~z3-6%@{x2oCBjdQBNqqcb93U_>GLYw9 zViQ*&nO4PTm!|lHqfK#RVKDjK`N~YBiLLO{aXFq@3*pKkQOi&p3#kCIJ<`0aln6`L z)?mk8ZOAB8K_Rmoch+J*xCX_i$b=B>dns2ZJmr%wRpQo3h?v|G$Zumt;n{9GMQi#sB!xGZis9ul9lNPR@Aj;kBQC_&)RCxiIgAT>A9o(~(EZI3^&_nZGs1PI!6o^9$*K1B~*nn0?hRx#`_ z>X+~&N3P!MPb}K(v64yxMx~V2Bb#WW*w)T~hyMYR47IbKnHSSWq(dNK5y#as@v2<_%d5N9ouF$bKRuf}U`M>( z$JOxU%E_u+HurkvHtTRZ$%P+Vng~rSTmsqC^@PWjA9(o_|NG{AVmzV{cN4huHC_DnagAoGf(|D#ROaPHe=n_IIhl z-4$A;z0cHFdD)l7*q{bEv}PFEwLfq;Y1UvqAorJ^L*+6&^$0M@auc?r29{FZ`PB9J zVtJPZyL=KZ*m=o!pq!|0+XF>6t&nxIfYcbVew+1-vszQ?SSQ}3_T5*@mS91}_UGmU^ z4BXabP~;Aq(|XF|n)#7k{(^U$GRSA1ih9C^!>- z9)2n`9QJ|Gey>b{zQV~XZ}|PNh`{?#t1V`yJD1;`0E9%P2gA*=b?@ziiZ}I~{d*CP z!Dd;Pb!rpwHG$L!u4CQLftj#Bn_8aPsQL<|)&+TnV(z$T!pWM=4bThKqBD|9f_CnG z>qb#a&|%_dw&XVb=ZC5kPzijpO=Nd z*q&@&R_$GRTrLuzRWht>4!}h%p4I*8*mO%tb@(OW>9?5uXsPug&QqeF?8q&MkBNf| zgzJu%@ZYwkuWW1m;7Ki}hwg}jh@TnGl{#TGc>LOb2i3CR5F0TfBtDyJ$wypSki0Gv zJ1CXfA{;9Us7-u9zX=U5*4cVA`~%XykXG^Nvi{(aY1%&h((X2k&~J)UcZWe-qxXkH zVm(ICI9eGpZ#{TGq}eu#)_RI9wJQ9FFNs$|)xr;Zy&EV##~~VeBqOHxRi8cl`CktG zcHN?>&lQOh!dt;rPhXHGERU>qSs7-VH38Q|;#hszX)8#s1ue(v|1msL9gECx;-z_T z&*f3;2;x>F%QHygyz<9nRy;#BrMmVq&mj4!+>=Ggr@X-o{W^uJ$0loH^;V;s^$NYl zc9U5=`E_NyH&nFZ8HdY1Y-;LAh|hh=Vz>>-63*lq#6M$-;r!>5+)U{}4`SMv?~)Bf zUA_pEu^rXQ&pLlTCiZDgo$z9td>g!`m7f#X)q;X%N}ny0n)X9MD&*j0r@uW+Rg~^> z79vR{n!E^WaEJWw#zf%O(JE&rU{6wNwBbBOl(#Zis3OHDeUPMc8sx+RX1p!e#7KEu z^i)ZYwG1_bV(0(kny@En1!8Yg4dB-XufbVNwoUaHd$2wI4$sQfK zSga|WzCUj3Kn^~d4UV5U2_-!9uf+?fg3p6@AHAdlBs8pMO7HE8Ed{Kz zToCcS#AE+p-X_uU(EY%TdqH}NR8JwV@UZ)UuAqWc4Q5lYl5?&y{A2uLJL1WJ4SJPy zdA(bPrTEu@*T+rG4R16nk%j->IjbxWC|4sZME@M2so7O7v3y}-beMSN}`z?9@PaMOzMI6JJYa=M0_cM*OCSCio8-3-r zIS&rFpLUnnaicu<hAp1UmU1d{rDc4S_OjC#ooc!oE{1Z$jT3-bSKo zHySd>cR!bpap}6%5D(nx&Zg)K(dTjw4oGo(uAw2ENEa_I&h`DxW7%Qf$-~$0EG#1} zzblAWZS)DdIjS>1P0|;`-AU!BZ?RdGI)!j4N7y2|x{rlvoQmpS(@-ut1h34%KXu4a zcniSj{b{G)kLtiw8M^6|oQVV3Kgwcco$rYGW>v2CSsYwlGVwuJQC^gz62b z(}tS!14I+Oc_U5}eO%4h8pqmV0YVq-5i`8KH~XtXn;UiTiI{uf(*qP`pr`J%>a_$B zN{c4U+AK{2nku+uubBW_+Ci%_rx1FxdqkJur_xcAU~oFPM%s}X4gr;tBUx4GvU!IMF=FacQNM>qD%0*6x%vdFn2X;P}Ij=q3x;A`qkNkDuk2V3>L@)Zc`tO|``H&{TQn7+;-~@jVo*9iZ2WoF~pp5L6B2?We2V-PCB)!_dUF9V8$5wfs@2*I(_KV-e?i>91X$KDBj@vF(ZuFR8xgAXd;9bAEQQ7ebp>30;K z53k0GgImUGGU4O&I-TCJH?@oemRnSN_78atUTcF_ zL-tm^q?7oZ5SOMFr6ODw*!>B<6tfVcRM13G&~-RVwlp#*m7aPX-pUiYFNMN9js1C4|lGOjT46=U= z?qxuL3AktPAZYTvL0)b;*nE~{m1?AAJ>as$!Rl$z0|(2C*kkL&?q~k>P4*Y^snO;6 ztERW1W>;k3tq4LD+o}0BtXm!tCd5SKIA3jEQaitC{kb4>N3%_m8M@~CJs3fW1w3h* zG?B46Ma3}zUVtrLl{&Z(qD37$i(e&pQ2yQaG?qOZ<>d3jlbr97SyE>?Yji#$lQV4x z6|PGO{HD0dIiZq0#$Z&TW>hH+2=>~fYKyR8pgqLEy9c#)<=P{7sryxc!t>WVt9^nD z8;CL@J8}q8vd62gW+BD>Ve>C`(`U(E_1s=gU)}((e**L*7TO~})>b>IrL4FSII>_Z zupZE3qhZstiOB}%>N1(#a)^1=N@0!S*hX241~tyLW;{V(SL<)&)wKWKE+nv*u*Txn z9KQ>HVW1893g{V68xddJb@+o-43yFNt>`HITMY!q=KJT$+qfVJA!tsrRMa-Bmz%=S zN@pFc5?uB1dLAz2+0LC~qj?t|_nwQ>4YSHm@tX0xM_b3Ycu@2rRL^~OFVbm1MK63;*zbUfe~vzmsjnTp6eG4G z2-k(mu&e)Q92jqU5QX@LfBBkfHNC;GswO_dG4haeG4F`IvD(RG&epqUiF%~fNhl8! zt@db_4b-(HDe|j2b*bLap(X-R1x7`j|2ljmL8WKHS(CgQC94d$Y`$O6KM!TRe>vJ8(^83z4qG**thLhLW|b*>OeNz+ z5^dstv3l@%wC&I>n?sc3EadG}*U{)S=c6D*OXg06m=%5Uc0sXjUyHf$Y~lkkLWg@P z3sgH=aa22+r;_CfPl1s`r#rmAP_`GaYw)6!?i~C^m_>rQThf~6?kS|p=Fe|q#S%>X zdt8Rk*Hr@nnkVo4$hLL0MobeyFoQEyv240EN={waT%B-ZjPLYMsT;Fw<1Xel&!<-+ zexcV^C!E|a!v%gd&Nj~(zxk>^utGa)1KVcAQ8onq+r5nfJhgRsv-G>|cG6#Z=yGni z@M)Npqw&UKw`fQRjfeAZG^cK#urp4_PjILNIHRuGl&E`$%%5EDD!&dunx79K^DC}y zW8`WzS8$WV-I!`UUug{h?Hvtz%&(x!ft3jM=k5^W4{57&KBJ0KC+&cs_#h2%Q>Jlx zHk0CJrGe7Zo@Q1yd=Q}BM4@ZO@Z}54lcVm7bj}dpuMZs#&)psxX^y?*$Sp8FecRk) zMJ`Fo0kW~5#W zs$UgfCh5vrGg2nHk)gLyj^U*HzNSOIZ_cs1TgbxPH?GJ!T-FReTTTRlaTCzhIreL5 z$-@LyJJnQMIO86uF(dm2IAhYe0vA6c8gI53FAFtZa;j>dX?bjZrM@~S#x_V}VK0|B zS~QMI+(W;T;lJ+ZFrp%K^}ctSLUozs<<|x8D=|8DJxrg1%g4C6J~?z7Jw$c%tnn`^ z<**_!gulB{T{m&4X=wsjcCAJPa1rLen_$2AsG%AE92mj3bAipCN_xo&l8L;*eWNv7 z{jAb?(UeF=6rE(+YeWo&3OW|(xusZxYL5u(mj=ns=PjE+(xM8G(svBX5oH^$G+mia z6>0A8JZGI2{~Yw1w>|CcUgv+@?$YfB_i{Fkh$&~zm+bIH9;{2EesI7mE`i5@Vt?xn zwtMuyO4Q6_62Awd7UE)LEzjaNRVA1+LZ0x2x&y6&wgI`zf)^+#9dG*aI(4;6=QUE0P`+9ey|JIOAt^VLNYURV5K=|mcJXk1OVx*6 zc0+FURUa2S$J8B%c3HE;ZxgCTYTz|@?G`h63*8QMPDQ@{UcXMN=x92Ha zu=^|)s%MJs5DABf zKgiYjRl#>mFl!&r!ZgL^)U(l0NJr^vC;#8TlZ(>9IHr={o#em=$I5P4$+ZhtwyDys z?DRrt8|7OSo5r@=;%J6zXGvQVBR+4}BTVm?YpHIn z!Q8gL_AeI9 zP3c5HdJ|DmdXXA>3(^A8OF|Pu2??PE2uZ%&v-df7?{m*S@An7n^*n^N7G%z~#vF6Z zF-QB|R2&}(7WQ~nt5LGZZt!f=H=mG;fzSImbr_QBs1S06qGG)yY^?>%5j{B4c7N5%<_GqQAaN#@G zL=+SYG1Qc$7g9!axY}Mz9<3@<8xuDqRgn+;_SbqhL!NZV>+kwGwQqholj#?{S|c2i zDz-!R{msWWuRte}2LbpPwp+j^i8kS?C$ODpLJxKjsSTrvEPHia>`jUDsg*Wch0RXw zI4j#z4^6`(eFn;pzj-1aDGSs}Ezu=^#)(?I2m3w^numvoCL*hkG}BW^uO(Q!&+f=a~0lTSfr8aR6{IrDjJ zicbc?=#(g7edJT~fu?ld=X`TvsLR+RXzxmqd#r0n&QjwS1)eI-lc4<;`=+1k-?u2o zLICv+Gb$IKeA?4i)y?tSX}}m#+`D^ia}F=?nNc){MAbHOuGGMl>zPt8w03M0Q_>)I zjZIU;^v#{L7l_B*n62w2Yd7~I_T5lkLP`ipcYTfIi_uzupFRs`K^db(y`pQdtR!FA zgER+=&g*|DDtFloey)Y{OQhXwzwvq|Fet(VEE;e7(@h!KI9=}c!ZdOig?+Y^gAq^u zZKnz#LItwVin$-V6n};`oT~8Qo-T0-w!77YjkB5wl9II!5#|E^I-TI+&G@3GC*U^3 zE*auL2`1rNF`Lxc9%Ncr>HWV2sH|#VhM83F5T?S`mq<^Ky+H12I0&iuIOO0_D%tW; z@byEjgGHdVv>KR?3bs4&g-X~akdoGbxhOF$h3XzXL3PkM<=HsX)%)4Z89`Pb0NEE* zuj}nqeSg34Q)*v<*{L`hM{`{gatXCF?|PDS1o-R~}} zKDVCj2J33Y)!*5sEowXlW3d+VKpC4iqYT{x6**xv0qdKK$rYy3+JU1VV~k?(Ugch2 zWosB?jyLl*p4#%vtz)l(_fiNAZxhxYS?N2}0qVsKZk=vR{_u^Re|2xM(Fxj;_v?$3 z8}d=Tv(3Z$zRN+JKT1a@^M0AQAql2uL4z+zSCxT1C6)}F69G|uy+ z^0zr|#!~C0R_uh^Zq)2_`woGdB?{}}+L&Ix%3r(vy6l~e{0Z1uRA5tI-~5a6lr7RI z@K5JUG2@-crhPoituk>cW4GlW6YXDj^lIiEmBGQo-YoTTRkzDtQZ=5rW5-Hocs0_i zj*SW29ouD#^K)hGG-3i6h2WYMmYf{TeF#Wl4+9>5RkHUaQ2dFd3Y>I$uPxN+>PGz= zG5^{V-Uo1UxWON-aq2qvrPDt~_)WS(`v{sHi`QM{n+XzEZc{7|VJRC{HO&5Tja`IAZF zuO@W6teM6`*7BJ{?Jr%WDz1TVtdz_)F;-3+5@)T+Yo3udktdi?fIoy(2VI^XZn+Ox zsyeQ3{11rJP{?PBOE&y7Q<+JBu{C(qNX9SDUHZ9nvJrC(2|Es$l_qB&xSP3H>=gsm zAArv(3^-#yv!^&OyQQdAr{}#7^x_ru3S?v2)JWAh4=yd^t-Zu>&FB4v9gU|j%LX^% zP*n<~=G~q}nc}mrF*&60pGhN&vk0yEq1XrjAe7-pD&V=>a%*7vWZ;9!L8NJ(A(znS z^*dgpgUl0#eD5w$WQ+NzdvJ4T3w+wl&J?+x)Gw{AsNel~s9$oB%DHmH6;soAQz2wn zB6$=Q^^4`LYpg{5hVMdFW>U?TpRVJNdHpxM$p#b1yY5=OB8GQ;dv+P)<6M=})Oyy7 z<)8J^&Kp_ERQMXa3+i9tjdN=~tf7*7+KF<@C*K61``zfw>{jOKe?J~(FCFUmr%UIje z%aHV^Gh;?yE&1=)a@;otTYY62Z`Hgn)mMC&K4sb<4USuW;!r@eRsO*fCf}vI<7#1} zM3220*fCY6JDXJIkvxrUS;tRTtX%<{Fj&+FgUf@#h_)xC8{46~pmkl^vLlJiwe7LY zw5p6Uow{aj-SDh8?W$tU4|`^1?~h+^>kA8ysN}v;YNH%cKyx$<#){2 z-vBq09Ji*2$vzZqN@kpG*kr>=SfRcAlhzt)v&+5ZVDwtEa{QBzcj7Xo5+btnXl_2I zx>6^nGz@GISipxs=b>w$M~IIR&$efujC^ohiDstiGej9 zNp2*+)TyR5`?vJ_wuD&?3z;v87fG*DO0g8x6Sri40MBw}+)6r1Ok9qq;rXGv`2i@n zW9Eq&-N&;}JS7rTf1#0F1T1w_TM}5pM#7+&^Q#NT4gm{&4UiP&HEm{5x{GCPibk+@l~D9gUYJP|0vmsuj|AOm&z z>asqe3^|Z>ukya!?4;IDHLRLg=iY98ZqZZx%EE^QCcy0OWr`)ExIrWICf{Vu5jkJCyYEkblU8$` z(aH9r$EKBjC`GxWVnKOYMX{l~_0nmG=F|(MIo_>^)W}|zRgs|;D>q_W37KkNKb|7C zPZ7-`0P8yq2*~zX*p0R4a%ERv-sx00YNNLF419Jmx;!#yXsn|7=Q&dNnv1cEtHX3~ z;l=f%lQ1=z&lBgoFQoBA$HMu5c3h1)8vSjXzT*DEGu(@E{#Io~H5j z5ZO=n9u0XK?3Hp9BEMXAU0`(XGvA86jOvCB$9Kbrrf}JxR)2;EJKn-JmCFrxx~qN! zocMpr25&m35N}qqQz<|NK)@&94Z|t0^z*xfo0Pg6{kFsX8VaOcGDJ^&Uvl@oyC114 zaBx!(TmAz|wf3}iHWpw%N2q6Ea+G?FpxEAG4GiZ-|!U8mu?n>A#c_0Cf!&}_(Z zWjEz!)HnhdZT}vwspk6Z`r&3t2D1&f5OoNxLysCh#mYTp8LcZ7z~MCEsBAqegu}{qUi23ww?b)H#Rvoy}WX zxbcjMAQiX%CL>wk3$vJC#8-XR8IOU)9p8+>ARpl7nq6C>QMfGdnwA6KUT&RW={GuY z>OYf{4hjCWYo#8I{jJJ(p0aqoF7V!sisGx++rd5H#mBDkB$bkemdlo-=8`&RXf8CK zjgl)3N~^u)4NTOR=K7OV{hM%mIjLcP=Y3nc*%6PvL%EM~XPz+yH)jWCG_MY;iZ zP7fYmm#ibh*(tKCeh>~9&d|Dg$!k)KLwopT)|^z_2*-^lT8|DT3koSbv;m?Fd$Bpn zk}jBRs})nBm9Lt=>DbLymj-@iq41{@I%P%6%hC#GH+d7(FPC{8oOAhm47@N8$G(#B@rWCt1EiyJZ(4(zg7oSfZx|DGGB)4r);Va8ng zMM&-}TSgbz;Ej6Ju+5>ipXb)*v*EH*T~blU?G5?b zd=%6Y-e*eGP(Y329{X9cG1mpJHBBkJ4Kr3%vrv%ec&@NNJ#sp}4G=$hr(pM6&6-r` zz5lTO;*i}ooByfe!J6q_@q%b!h&u?c1emWwde}E#ob5k(-+lQze+sB07S6@8Y*J0B zDV~!(-WbY_^S`?(<=d%0L@Y=**lD}qe-3;!FdI8kyR2Qo{^Fv^iD2@|aO~4W(-1a> z!tv?0uLl8nszb}U&aql4k1@~==P0tc8(lll31ypkIAGt!c=u$b&(=qkudE%C(U2rL zN6tyf*C1z6**_;08;>Th8>XU zuz+cG1Ad`Z1`-zw)F5>b#^*L#VF|khcH{V)U##-N3!5cPo>R?T^18ewcqxvZiEz>R z+igC-EWwyA4nt*1$Hcv`v#S$XxW|E;({5|}(l}Lif35BpFzTF?LBlrH=Qjd{pTEtL zKBcK#6}!!mHS(mvlsd2bfOTcihCJEkDu%eCax0E0r4%kbJ$Pa_WF478p?Ss8G~qml zd+N2@sQrnwS+Meie9le}p9pr|3IN!5GaZtC{C#k#;MdxhEUDER+pY zS12m*u_T4YnytM8R^R1qobqv}x-)nCz$`@(Bi;U3d3!dUjJ1n@JJoJ#B;}I`DYQ3Z zYdSg!esjMg;1tKnK2)!+S)CGFxF#|as<=G!=&< ze;rqAu>D+(X-sTBOvp}TqWE!7(N44vK_3Q+ZTbF^!2m69SoeFxt<9Ky;8T(civ1bt z!s)sCQcbqNJ+p)B#f2fq*`as{VD1?TUt6x06_gW6 zZ699h$#rwPtV+{!`GmvBlWd;(aMDQCclax^VlAz*F-^vj)J>6YVJ$NPS1` z@1qySH9=Q`{~q>WDR7>@io*BCOV8E$ZwO}BVG1}56Krq9e5Nl`Ej!q~`Ap*(VPuj` z^J43`nb`q50CIFQYo$-L6-IK#hJvK)0E0)#B*gzGg9Cd$H*vPu9UzRR2sjmjTz20YYlTGmc zkUEy4^3a(fg?Ckl8h1>XXeA;Xoc)VU3%@;AhcLL39K zUyPa7wdWqSn>D&Ch56OjKOVzcqTeYQ)iUj?4E`Dz=l&&06eP<`P@lj7rz(!Z3Q4~zI^#uJJsOrhFz}hmpcAX)o7lmA2&j{x7b#sS0(NB zI*mV==m72DRm^;*!?kT*l63Vvky?ud`!9e-K2WZdh zh$xB(sk_J-H{yLaeYvZ=Qr1Q{050FW!l$okWnBXg|*;(2;?1Ax@zjz(JV9vr;e#yDhGI{mR_r?4@o>f+C-O;-G_-qm_6I9K@xz$hI8?!1+okBvMa!cJ&s$Uln8@CdJx&Y5sh zsx+(LL256I?s69@(vy=MT;mtS2BhqM7@1j@u2Fn&<3o5Nt<`at2Yf z-T%5Zt$`vCRqVo8T}ITX zy+hFHAF~Q$;Gw?_`Yx%bxNJ&|CM2oH9FZZ5D@?$cwULIWWZ|>b3y1fq5>OoY!#bGT z0@@V;JM}vA}l{6lyE72a2`l4hn7M zRgYbpRmQ$bPfXZFED0HFn|}hBl51H@PG#xJHoVU&2ze;9)LDJmTb#^m9}b%e;P#Fl zr>+gi4>l*fyBaOohU4ttY}>trMiin=3(^`V>JI-fn`bKY%e_S0t|{~xF%zgjkNUoo@dHhU23# zX$`tdhztg)ckXai{Q0}k{Vm7iQ^!KG_~trZ?Q07qt|BbR;_fK8zCD)=HeG(-CFSv? zXFwlAA4wSQN3p9WoY}l)}h#E}_Gii7`N!Rf+C=(OfN0e*6y5$b_ zwep~@9=cV&Ek2h!5H8Z)pjUc4kd@~7q4N=Xof>KeK7Ma!DlNUBOjjw~;&}0q?&y-b zN$-WB$p&$bt(-p7Z`U)`_1RqyAM0O@a=q*k*S1_MADDScv)`Y2;q zAaLM=P}qPEctYToqyoc}p+qW@loeoTXZ&EMu*V-KmHme^x5%ef^;gh~Mf#F&V=LiN zt{E;FLs#+DP{isVRbx5!EgRL%Sj)4K1M4kT4D{Q$u9r^8{?GL{obdb5r2gm8-~2P= z5^tUDFvKO!a%u(!84uRUoRb6RJ`QSE z7)1_6?{Htl-pa=5798`36u8wE&q?MBY9SQ;$5cm#3>sJ$y{hQVg3g^dRh4^v>$0j? z3$L4Q@Xoc41gQWwO+?#A14eoEuhaSwQ5psBJ-?oSnj|=@Cx5AXjvv}_7U=)7HfDY8~5t#k+-n1Pm^=7*i;msyMd!VYkRv)bvof4_9fTpI&2krAm8Na z#NC^41cp`d2E9&gAGwlHOIuR@<^djVI#w;e4&NtDURa!m+9Q5 ztUc)tqz7FAF>CPQLWvl{yCP|xE9c)~sX;QPK3lgITlvowS`vjLLL!m%upL>4rMU;a zUx#DdOodB}dn6L7qmI2%XF(QO^t|0Z_~4hzMw21J)3p`W$xewlRu6HTt{v`ORhW|& z%&u`9*JW)VZ8zHMDyvg@b?&t z@GkFQ3vSxGiS56DNfZ>ML!CztU+Bc7Dr5aGKJ5bBT;(iSR+`!}6->5fIjfNgLwv4g z9$}{2a;&80IZym^Qa}$hqxUV2*>X()m278x$Tko?xGGFCPRQ@#ncXeAi2n@k7!uP} z@N2*PK!9V$vP66rTFlDOV3$#zgqapvUAvg*+`MGQcNlwBLN(s)8QY{goFu@BV;1Xv zH+?Piv(wQ@E}gPxKKvM8*65mBKaFY&K-2*6^FaTY4jseTdjgi$f8qJR)=wHzWj8wE zo4fR@g>|6pz4sM?IB*sR?KHzt0h6IwzCaw0Pj{biiuU8qEOE7EsRYCB#o@>8Iv$J8 zbpzrik%auL)=1oW3s#(%R^6YezU!11! zXtJS4cu3w7sAAy7Zl|@NO1rmB4;m&S?N^weP{%XNtGaa57{o;Ca0u5XrM0&fTMjA&Yqz79Eo32Y!XD%k4Asi!XUWN6xu^3GMM2y?XMsdvI>h~kIs#kcuWMimUe zK0_et%0Bm1!S*Mu$tq@5nZa^pIMl0MjVTXNoY(u%uykTMsz#zLkc>^axXsa#KD!oC zlsktYCyv7zulstS92$yao$`FFdkS_Z`^}h_>RU8dlCH(;$ncs?CP;r6I4s|pge!x@ za}t72u7ry|l;CC)fX&ce)SvY4HaoWjL$UW(QW>*ki4oHd&|iGwF~~!|+KE@Zn-w<% zc&=`RMMxMCu$U@H&S9!1!iVfq)b{J?4DW#pxX8;kz2h!B-~ zeRwxVARcOMU+vn+`6rR@)%-@(mTPRxiuQua+d$>eWJXINT@A_GjHwRs77R`Lkske!2uH)edGzdS=o}?%r-pK?(JbyPa$Os`^jp1=}B4oM|2Xrn1bD z^S}mDkm~QattRYoU{!IvTY0It3`~7q)pKjkEKY*WiPrfL&Hmu_|e%by!a7tCu z;0Wn=cD;bwGP!ImxcTi?@3uE+0&lY(70M76z75MgEDlp&;BAoT9ooxs`8L?fDd01{ zr+G{hr1>@@PjB51TB9o3dU2c2)jA3TymKl&x_YR&oGP-L{+8(d7x{YXu>dLe8#}GP zAEmweTJG>Fz67gS+aoaawzy5-()idNdpk6!ba#*oyI#{!mIOs#8i2QO0(N?gBH(Wi z{N~JRYDe5WerL)JSNxc&mSjnSOi7T-EXB(8RKWJB4A{eH@>Ik+!F5FKPVmq2jdVTi zm6H=Efwe=bu*Kls#+VpDVA=$!;;$SH{}&?JMo0Xy=gp6XH0}?hUS3u||6csR15W)z z&8Muai)@pn)ZeEK7Ye&SuR=fHiuhr6PyJl-FVy9KXo6L}^_}@Ba={S0qsNCY0$$0%dVG$w9{oRu9n14QjsNHFf4lMTe|q$9 z68xJ4kN!=9f0KYL8viW|{!N1a-&}A{FuEF_aQ$B#q^iY z0+Ax9q37VF9Rl2P(NB~#W5Z3bMJ3_UEG^$Qp@l8xXh0nv#d%uDhTa#x^0((XUHh^e z6Z=0N1MsZ4D~mHAe-AVd@%yU+D5x1^iIX;4oCBD#3JK_Ym=Mfw1EhHo+FJKx)vglC zK$#vyd1URmbJNh86X%G+-+QqA`Y(GB!>JugEuibcB(7o7S>%y9*93_YIt3)Dl-jaN zFAb6&;&u_IONFrgk~Ly^{O?wd|7bHsP1YaB^`GSZZ@&dTprPGZlA8Z%mgLS3JS15GH-zv2;Ag6 zWds0HIh{#LP$#jZkPHdPAr$g`f#BZ;V;_|GuLfv8oU2FV-7Df((F2GjMhb7p}V?k2SYF!i8gVMq~M;~+JnBJW|W%=9d zvz@Lvsy8i-y!jh}|NCc(+AV3uKFr(9rEEoAY)t7)5r!yn8W0zLxX)E*xxleA17t)7 zfcVkaDfrQEfqo51Kk?q)&pH@xJak&=+Z=>gsH$~Z$l*{)YLkK(_^05P5e4JYEXi;X zz;fo}e-451|1t!D-R8#c=#&Mci9>?DYDCgG$({BBBId-rtqKCqpWE$IskTx{-bPq9 z%h5`zvgY;B8SGJ7(y0!ks!1$|5Nb8FUs%GT2TnExy=C&ULlyrj zpfUSzBR4=|=O8yOk@U}#;lHj(P#T*7s@QR`#rIAnNlsfPLF)4RNAuX%-!^d%yH$E` zZ~JX{WlEELm+;xK^!+wTMKe8{F;ZxE5N?LB2s3}hHH>(O!9!fBd%x4sM&h`6NgX;- zbcxWcmJPP;&_%G+FF*%*yZQ~W64LoPp^sfR)-BUqUD5Ac^A7@bycuVZ4*?x7W+LB& zbQ6iYe`nNV85*_!#exLmq$|S%?iUZRRgn$JQ+{!zTcng)ZdHw z>vn%P(npLjhChR9Y!hIv0Bv@vtH9Uco(7EDm>Y*(C@(Wc!Dp9gMiqFH@`QD;T^u2? z)*K5;4WxIrA4xd>Y&l9>*H$$O7co{$`Ya)K<{V@nMq=6KClpQ_?wU~A@NEbb$^?*Z zqj=8g1yvo@&x=ScMPbvUEsyJX4gOA9lG){R*8k3Sf16fxKZ&b*XA)-GxAJ`Ck+V_? zI&a0OsHna;$6u22k^~ktNzqPJ0f(6dhb{Enzvl`*!>AF{!k*fQ_eU}o^kzZ+P;GyE z315bZZHV7x#%J)BsMqqtnChiL zs@K(l?jFBX1f%7dD}Is+jkIMq^EZerbqzm`AoO(`XmapTKr2|OAQa{sAMb7=sVvF# z@k`u|=o5VQtTqOD2F2Sp{%9F0A_$VhPlDMBvw5Z3iFEu>(ziL$U5mQHp^4jf$9(t_<6O?}BhqvjpKj30{vdqBUe(aAG(=$mU0c9!-g1bf4Go~8_}Rv>!bw`Nk~O+!U>b(%WQC%i8Q5g%YA zajDRR(}PKVsZ4@9mAW;`(maettz+~0w*_hu#tS7v7sW4{j)*Lsjs^t%N`S2-AMKblz!17g2KygH2{%Z45*R9E zwGR6dFIxM?CqUKaC5K}y%ybL{@DIpr>MQ&Nv=FJ?c?cVxf^dLbbeV(?5ej&qB`Enm zEk>nrDU43-Yzusf#t{>49AxbMEKyDG0bBHqS>0)GEEs~!6ObpV&ACBz5ZF5)`;xrU z2Mr8Ap|Nji(O$kD!e}y_Hw`4A%n=i$Vq(P>jfX>D1iBw((3Ck6<{@vJxt4n+VynZm z1JzIm32h5VgtYp~RV|Is;<|?GM2S}0n~5wDVg{5NCdSmAIr1QJPFu%O96V{41&gmN z`n&81E&5l>lCsqiQL_zKWO1aoj}YlxIS=cV)f-KptwKWX zln~~eTX)QnK9i*{<&lq5zW_X9Yodvjm_A+9Ckamp>lnzqy&=Ke_$LhIcwA%rQZvK; zjW1TS^?bc*W!t6F7M<7Zy6AbQ%$Ogl+0hi+Wt=Q!mAKo1!L=Af#0VI9%;1&U{lVFh zaF$&I&8EgOxY8yNVbcJiRdk^4RM5^5*45X?Zo$GLV4*9Fi@xw9-2f5!tzJ58AE+bz zMgkYravPoPra3`AG8?-d=q zdjtvVANI$+%?yOXlXePhZY&OEVc>9!!Z|=_qis$FVv|MYKnz9Tx0IVsoz1RsTTsP9K7ugd-wOD0W61|SK0lS82{%J5RZD6NgyD$h9#ibN2R4w=Ox^H3}8UCV#~ zW!(2hIBFL-BNyh&l)M=8sT+{}0FyWQstJ_rzjTjCWMN_N#r55q$Tb+REadG7A?%Ep zHSB$r03~7b&2^_c)2dfS#a-uGRN5N_z8A?AZ8y(C1Rx^`M6Y*qIC7=R(8FVu1@n!U zZJ05oA1XLWwbj$j{51Iz|Bk{{Bu`@8!tlAOs_klhGahS5N4uU)qGx z`5#fWb^9v$%*J-Y$eX=6SgmyrZF$nrx77u7%fTEDW}aLjC$b3`s$3XF?W55ee_Je4 z@aq3k5w@bIR;F}e-x$t)TAVFC_BM4sQS83V@95fSM$!TAVqnivGzDD0c}RT^QeIZY zl@miTqSX%uooKkXpE`8fRD4}Kw$}LKHDLp*AR!c;%!E1J1v;X zfYt!eVx1@w9@oK`bp}B!NiPL`y1u+Kd#UsAP(}{n$(R*w>=WMA#kK*t{wg3IZL4aoWSGd)&RVM{aFiyPrD>PeQZ&NQ*}Yn1`e`?4<~J|9uw?+f`|r zx}Dc(2Rq1YsAYhPecKW&1bx@?2;@JLtAUL%2LT-Np$MkAb(!2VdHEKLz|BeO8wEd{U z1BSX5{v-2QU(IkVlz&a^=+0y@;^^%=MUzRuCQ3YP>E+)y6wSbYrB@G7jiOx!qv8^N zmz7?p&Qfx`%lbW@S@1^+3NX&ggL2zSt8ICayOkvn84$;dYGGBo zC@8=iJuC2C4xBLKsILXO3IqXH`;XKEcBrPK`mB=rc%mw}_RL(>Ts;@Pp!-M=(Mk6L z9^Ih^0}+>g4@?BV2{7jdfE|zTczu8*7vmJ$rbu-Sr7P4J-)43ZSN) zPTH8PLOM@aVH0TUDAL1#tJ~#1doi)rvB8xn@2cJp(KM-6)kVoN6r;d-`v{gR2q2Hi zU>6-=ptZT!D+=;a01Zd`G)eSAx+Z-BHduGclw9s&?zTIM#CQcPES3f1Xsg>!G%#EN zr`rYtfe0pUwTY3e`+Imh+yR>dKgh#?U{Fan4SY%}4u;UPMpl7lfUhK8aTrN_BL#b? zd6ht79-S2>pCNKX&uJwEt<)tRv15J_?{8}=pW$;(n@QWKF>QL*RqoJO%jwsyIr}{T zLY5_I+dKaA7cpfeQU6-!1Bb!EBOFJ7g3t5S#~S%T@#CMfIe3HwA>X4!$A_hk_4fe5 z9dZ95!3U^v|Mf=5C-dtZp?;edse%CRiwP~?N?=AP4Ay=bA9gkXW6LZF@iqAzGC4^n zS{z(pyy704I+GVQS-aL_%58LAKjW<`G4kySFME<0A)`Yv>(LQvA_)@EG-Or(RH`ia zA`t0m|Imco-shVaTi|!QfQk3zF*oBQKi~8mh3|GB5KYg6``Ua8N=t9H&>-GrZ}z); zPrdd(_UA6=z640qkJ;S@KPaR#7we^TNuXH2rVmuKmD>>Zf@&vKU(@ z(b1igmmhDy-53w;h;+L#OLm>PDY>oO>nrHBYMvATjs8KP!=N@Ie?| zzLD=A^ZDjl5GGe8{zN*QrQkE~l2Jw_sOqo*PEd?t^}LH%vo|?{;ZPEtsP`>RV!j5n`3z?CAF?|5FJ^h_@iyFK{%iwfK!_-(vCM6Yq7qRM{pm2wGA z!cORKYD2BkzgE4!H@*#-&e>HIPPxHgl9M8$fS}1^l(+2bTNU6Lp=>8b0lbc;W^5~p zMuR6)D3>fL5Mbu$2RYnf$90812YN0H?VZic2xzQ!hCRg>6VnbD<+lzeHR{I`iOR5F zwI>0^y!aEOUw{oh&4Tlz?@UiFEnoF|dE=$Y^OsVwti?C%)jmASxB|aUt3#d)`JbZ>rpj3` zG=kf&wLj^c&(`awz?TWLM3)_7}tO`R=20IhAi)R;9f{Ti!JvrvJOSLSKe}R$G z3{#A&JCG_LW3>EeA6Is}wDfa%X;^HpX@Z_>hzH>+{PU-Y-F^kfU>~n(z7DSbEXB$( z#>bNpOvp{n8-gdqnq$?OY2?79;RkW0er!HNtB-HV!e#hfXqAPYjT`?)7DN4EG^+++ zT0Q66QERegP{z2^pD)hOEoT&K$Cq^Y3ExQ*kixb4B%hM+tIen+H9d}0;UCDkVeE^$ z1#1>3*U_OfULw<}a`$p7M1l&eE8sUijzeHO0S`h!_j4|hZ<=J-ly}j|x#UUvNNOox z&FkCUpU|U#4HSXM^tAK6|$;6|NTy%})JxOORR-&b+H1Gx*XgRzyp2 zRerTuy7$_9%m@Qh1HvG~o=~OiF}A59w~eoCza!~z>{FH86CEfELpHF3qVPeUlR~u1xOc3_mADy@QP&9wI#c${4P`o}cXJyhbYjHrc~O6#5D3YmS{gAJJDcsI=-KZCmRl>1xIwB<^ zx8fsNY;ao`o!&M3HIb(Y6t{D7qK|~)xnl?R`KUg= z{Inq6Y1)eWW1>}H5?>n?Ao%|BZ*JgeXe&w7!Ko$tF z23x3pWy{$US@$;vWeUB&L<_sS-}w~bP~W3iyd*j4b@lYUdy?e5cPbjIakZiH#FME% zL3s-Zg~%e11jPx(j70!{fcs)^XXiDc9*yCAxl9?In&}VrS9^Ligdyf06vzuYHhTj7 z{wOQ&w~;bWuk!O-ds;coxV3?ct^>AT(;pd)S-B?S4=+gWOndw}OJ- zv`&JoMfFgBZ-wGdT%S%{wiSaBa*+q8urMCisPeg9b|-^m?69LsCQB=cYB*O;-7sBv z+D2Vj7Rf)$4ash1j&KPJ;BSNn-!W@&FJ9PvmSIjyOPeJZzdr&8UOY2gG5IHsQ0LAg{{3yN3c+}*dt!qG3NkoK0Yf~DD!VE{elf$ zgWcssX+RGiK~hbYM_n6K+g(x(rSgVAArjna5gq z6!UUg;R&^9zVE%Xv@|cieo05)=%qKOW_M3kxfBjRXasId6ejD5jRIQ_>`dNE?ZG`j zg$AJ;OS9EpR8dhvOe`!lf#d2M$)jEK-2Byk(bsgOb4ElMInjNQG>or*Itlu0FxB~w z@Jv^jK~HA&{n30SW*Mx?vcH^udYAuFKsVV2o%9&okZJ%pM1<=Zwfell=H5BoCvSD8 zE0n&!>FN#wVpltWs`!dUI*hVN4dIk*7xqprwDgrKBn~6Fc>&N@S7HQ0Bqe}zPhYnL zO2pT8o<0zQHSagR!WJ%_GxN&1U-yu~BGSGx1ijt*Df;6!Y1JFv- z6At#`0%$NsQ21?LBi7_9y0S+K>ey5}xZ>rPi0H=V!&J)}=Ly>Rd}ms^m@H{gi^47c zOd7@vNo7T_N7z9}({^dutV>nfY%o$W15@|W(%k=+k5&xB2~j?=M(Q|4_=IAKL}WQR zduNFpT(bC;bKm9GV7%~Coz#z~j2kse?Rf9W6m9Hsnn3T564k`6u0&R4!QQ|)oD_mx zz0*okN8N@=u=A4KVy}V3*{w!|FR$E^HzNl}+GWjLv8PV5yuL~_3CE_xaiRqSHBi2u zQX#NipdCH(c4CcHr&aSt%g#wA=#+KF0fQ_vS94UiYcckCho~4pHU+xlN zFfvMeb5dW0jWY<%S#WPlvLDJk_*p`_Zvfi1yyr2>msjR_^u|?|=S=|o=*=F$as{e( zF1Ti^bd{U$%iQqQs{(>B>D1oTuUDg@qL`SOr6eT>ey+$vOxvB#Gr*g<5`vCe%M|?tN@DuE;?@Nh95&P zbfgiaDv4%#F-&P^sAh~F%ou=ZE%AeydxC;4vR%}J{xQNjf%6lj97ojy^1M7}V7k=c znO=Nzh}%-?eXps1vH{t+M54=l6T;TG=+C>*;87yR_#ZnaR~u*9dl|cJiJ`B`mrvFV zmKh9Ao^lQsav`l^4;H4oJ_3_={;pRHfBu)|)8$2@Jt~_$tD)&#KL6g^tt_rf-l8Dv ziD->GJh+kozyDD)W|@jSAp*#hZvE3I@db`1ue75IiAi{@pp~9_aSHamTpqN9w7WFx zja1C@$vYd1kygGJQ%E@JkczWzF~{aP67Q2GTg;q7O`6!q_cxjaaWR2`4bd z4gosX)lV_nT2A+KT;UCuF#I3FBg*HjI@0l8-CiuK?Tu*I4dx9? zHG;FgGWKC2hOcB{_nwS$cm0e-5Bag{LXZojzLXe17gUw#b!<^fjeNrt_vKbxz4H1G zP+umIkbn($Wo~B-*UOPH(^Fm^imxrFM_5lJ(lBz?%vd~qUF*x>gJhCcZOGrL>L?I5 zYZ%+*d|}Uk&A0NVPd3sIrN`hqAqrkc+w!!?F4|P#9w2N7v8SbaSOgw(x|Cv{@rz&S zge<2=ELGNZ0x^=Z1!L_?v`F+=QnwM$Bq1_UsXRbBs* zk+CsJkKl>iAQBw{e24Fgf;?T(cSrLJB7!5&T~paxq4U%jDrbZhG4d2u7XmP?pj~Ki zw;pk8yeBKGwI}fXY=+s&uFHm5VUI!}Wc5eumUxxz=m0D2UP1QW1CPNaz1UA&`09R& z0`gR&RXwi(lhu;Lq{QWU_9pZM>n7)k%_t5Z%B(-2}Nw-_geY*h$t**hH&%s#o~d?y<~7+QhS~ z$4XX~0{ASbH(8@~=!>_1F>0%;{E2mSQT9QOJvHWZT|xqhSqc%zd~?S5O|<`Pt)ncu zE5(p`76d9d%QRTI3JXyLid#gN-Tp1prvMdIBUH6+_DMMS`U<>U&zh~d>e^3MhERR+y zF!F^5qd@7RQU~`v1ijvqd8g&p;@gUZnTdW$7S+?soWqs_SSVm0pJ2Jq9_Dju&z;K=auxbf zrZebMbg*jP(SY`T9#S=vL;v;2XPSfR{^cihvhN*uOh%LB5Cdfv{~uv*9uD>U|Bu?2 ztWgYM5=mtn`%b(|lh7t>wy{RGQH)(=XDlUIE21pfvNmC|OvsjOVeBUB%$P9t-`)57 zd0*!`=Ukt2{%CWRhI!rh{dhjM`_QtPoX@|V58b)y?#gK(5(>eIe90<4-WpQfU6{Yv z!-HUdQF=Z76#qX6FmBNMULt>`9gjdz~dvA?B^GW`Ma^uXt*yQjC)z?4^;9%R7X0zdZ}* z`4>1XaHZw=<57h5qqqOBndW~!@GW0?NEZ6%xGa)kq&1r3%$7+D0ZX z3d?5u?eXY?o+b@XF)z-+Ji%h$WJL0V-nkAoz)qN_f43<`!+9|NIXfBkHBY951I!{K z2wKM-Kuq|xLVNn74<58#Y!qyJQ$nB?!dG(`-8LXQYx5?<^nDhe2X zkca7?xs480w8*&o0(MVMQMDbXL%yiJH_KIp49+RfR;gKek5HW>wK?4D2(M+T%n3XC zJXRNSAyQv#At^`EJ=Hlfk)2CHRrPx5;Msm;nxa@w)+B?tK8W4vPOMST}EVPmH7X6{hJsC1@ zq%LdbX?l^o@^_`oY-Ib{&>h<54tB9PB^9{#Ph{ZRvYkigypkD0129C8+^-sMfP%!vvYT z$ffWC9Tdym{uF14Z%Va&ZXK9mVdIk$?TvQz z{~SQP?xC0Ad}_*V`$1!li_U5VJdJ^%DbwDnpJra?In}+dLqcN+T13Re(mFdQ2ae|2 zxM&R@+o>tei>%0Ngu#c37A>J#EeDI|J)mb0!?z7;?C?1O=cwR z_Ve@+ld_xrj~wwX*SctkV3vLX?0TtE_W()g+O;LS%E{2x@=5RcBq{TT6n#!L2D2C8 zZ#I+jj}~04w@Z5j1O>2y`Wa4s6X`iVov(zDe+D1Ryz>`9fZ=kaJb3+UMGb0S`C5#@ z9A>I27uly3(}-NU)p#lTmfq`eS5hiEB*#50=sXzf<&-;2lcl=;!fCdpKC#|Af)Deqy@j`iTkVmf#r_|JZ-> z+pw8!Z%y8zl%RzOZY^$*#~q&dM$%AwyUT>~LiT}fS|RvuRr}^ImRVOPzM~N2!|;;5 zZ|rG)4W$Fk=dtxs_OwHK6g{VE_~5MlO`iM;(SRS~i$T-J+Qg9lpz9Q-^4XJVpRVQ+ z7}%NGz{w@nhIFfCf#nI=9+4>85P$lzS*7n{bD4Q_&S0u&(XSyX%Z8K=L;kydz_-l_91OwxLDPiViXiFq zd5u>JVIpihmfyaSXxR8<9FH)&W#$5r%HSL<9`}%U>9fYf&~uLE15r@^>g}{bw}T2U z#Jqz3zuH$LdhYm~`2Sti{P$0rG!8cpvQYL*kHfpVx7+U% zV@obwr-+7BcU^5}D`{r?Sd>@-u1eG8Cy6zH~RwAXkH^z(lIuFHF zzcUPnY-&P$d!x5f9VG)^f9GnMwrl&&=S@&5zv;6|NeRU z&_~^8a*!hsdMNMthABtjLiX_u#r9xAlFExA2n}ea? z9F@JCA&Xc}`*OX^zG+H=n*|@v#Q=o}2rRufEmftQbRoHLX(bICKs&zfQkLr`K21E* zBZ0@06^oo$ucw>IDHRhbvdM(@A3nXBaMnCH{FBmG3}XTf%VB__bvqXMYG8Woj`f-Sqbq{}ZE=h|^q=O4itrOjdhxnpwC3 zT2LV7r~XKfMX@@^8GqJ9{)hN%rl|or5lkyCxsN6G*J=_62SpQEZyF`0OQ`jhVV^P@ zexoqGJecF(QWEWy48(bJ(b5sM81&`j)Ra0#We%98_xkDugaz%ry;Gh9<){@DXm0+g zp(fz8l7~bBFL9~+DB++jS-d8ysU1P<9YAVC<;`O*z}Lzcn5Z+PzVxxEx02MajK_U1 zkLE9%XUl17w*PEruy9Z#XYvLBG#hE4vm#||86suKPwZHrLJ;%w-(RVq8!3F7MB4*@ zfLi(Gl$4TcGfGNG0Br>3H+UX0HE;DE7mj42m*d6~a7yu%tpAc6vE`so(E2N2k1;VifGwlM{N2*MBfYL@654?olL* zE~o8f(!;alRpr8KGZfu_cyOeM`v{<6tZ`sgM44FlVIg$U)f4uv0kO>afjlOGKhgu% z@^zwzjDuKWgacWk1ClWQ?d2R6Sp(mAlPzoGaoT&ZoY1{|pRcT?^NqPe#U(;BZpsR2 zit|O;o=YQEBkk#@>H1K?B;dywdVV>SVkOh`n^0&GjXWJAgUqxvA26VU|A@gU&xJLfRQe1_ggzm6 zO0_O-|HsX^W9kb)S8n}(b-Hp%K9)Wkt;@c;VUBAFSyITfW5wRxZRO1) zi6`qEnaeaUIBYZ=-mk_mHjA6L&BjzX>{dISa^lY}yoa|>!ZPCsM_b5XLeO;4+7mYp zc1owpmpBC|SuS*QD4#Nve~^M1e7&Z=J>h>I9r{zOYx5LA3(K5S0p6ec?kW8tM@T7~ z>L?06SOg~=#QqNH*ONvjm$S38ds9;lssNuJ8ZutdwVCJ{l;vF0G47%hJ^SWOXy#eC zlvn|B%%#H@!&Nq2nMx+Za!Svt)_LMPdsU{prB$Z7(V05jH zL)6e(`#uX6H%v{%;fb>Bob03oL)pb2Js$zsi4~rxuiDYR9I(lof8F%O7E0NC=4(Y* z?&DRJnFonV5MJ|;$%lD=qAvIXA^_Q5W#Id>q)h5nBo=2jFva~VBR7JO&H}BQ5 zjel~w+!vHnD*5?Q-hP>j_YCbmfYxc^RWDA`bwbp0Ir(k$Rh?Q>BWT}-@L;$ zm(BQ>El-Oz4=?5%@}n0&fgN`KKD38?q0G7BYC^-tuhkFMi6Qr#UScDd`u8VVMBzL) z%w}&G;J@vT_scBhAMe_NS!qH5;xV}4$09=-xC{LX&NSn^-5eK6N{n`87T zCnn3u(0#!ojYp)s`=wo-MrYJQx5!e{K>}BA>k5y%Df-fYNBL#&Vvp_DAbl|}-^nTi zN@DS$QrI?WNOdLCR0y>&E95r-E=s88M-uv(HF-5~nVAq5+;+K?d1VT-4pqD|<62&g z^SWU3ZV~`d0(jJ~bvMn7mfdW^-*<|4&W({>w0)+hy^l5Y&wPI6lXXiuu+etdB)r@N zRHP_?9}V1M=bbF(z7VXYkCaEPLzGZr#lN2b`Gji`szcJw`_q2Gd#|8xW)K}Bbs;ZU zv-etG*2G9Pf&jsz&H}C)j2()9-zVSp)EFf7WK31ymd~1hYv{ec8s}bNavfXrCEW}c zp}^mwW!f9&!CKL}tr{kIgy5~qw)2XxS5a;hTUm7*2*duEh#Bn zdjc4_h21cUkWFBpn&!5w2riCGJE9<2D@!a{^C=d4Wy2h)0oXo8=B%3=Ifas!x2MB& z?hB*DbDx}n(s6s^k(Azg(idVdGeHQIch~eZF?cjcb@h8pjU6_aPs+R%C>dgHV(c93 z^71a6eJ_Gnc=PAJvdKC+_Q_3oG85_0q2Cdx^-|lmlQF_rT4v$a-;*beHMjo}*=f)L z4g=~&eM|pSMhdF??ok?mpEN`vG6KSPT9ZS`4ETZJO9neTg=JU%#V&$AC{lqNst& z50IyN_~LAFK6V_fElku(zdK}i=P6qNG1IgDRZdo{7@NpVc6mpOZhX(n>VECZU=ym)SNo2~mI%T&scNzLDgv z-gcmMmm29je}au)=A6+T^<0^sErw}P-0Xd7ImmnX!SyF(ZMz`|hee~W8sI%@P_t}# zJ$LiB9gp@tn9N%A2l7>fte2uD$I)M3o{lSTJI%1vC$QLnNS;k;<62m_YrOFMuJIhI z|M(10BmKQMyDr?yIPW%c)^4oYO)Ohw3a%# zY3`L)nnJf5HN&yAJP4mYUOGpMU^GPpstM2f@vu%#+^4;B$YW8+r+v0edsg-SS_@Zk z-X=F7oQKr**9`tDpNr%Df$B&?V!QVhu?sWf?iJ6<%o|nc+c?0DMl^XxKa0unW;s1p z(|gV~JHO0Qe*q~zF=-yNxRm`W>mkl3$b{vMw#(;EZ955LyYgq;4a=QY4T~caSlzTm zyV(yWLGl&ByNyCm7Ot>!D!3S!wZj>TY=QTZu zj=N`iI+6v_eDI>eyLZ@Z$fH*Bgn@;rkl*i!gQ*1Dc6hNi#G~WwGd8t_6n)ejZ{A-z z;AGzMQ$mUz89%hncd!(HxnVB8=R*mk4M?^Ye1-7tjP;3KY}j#%xYw*Te>Ma7+bxTe zW|&wPO^PgPknJdXw0Y_DU&>~w$M%Rvhl;Nc7}c2WRLCQn3yDx4qQVnC8s$HaIYNPI6&`+ z5a#yhF0z5|D_3cc9|Tnw;&~92Xat3qiQn zT7v(Cj&SMM&?AY7^h9EgBcUC_n?8a1{tCSW)U{+Io}AK#gVC0y3@gv`KYfJQ@0nui z^=Q(^0&P_JWGufW*!&MaD#a|)po6Q>&NI;2Ox4`6=wFJ29=I5X4zT(a6_M~mu1aIo zyT%_|HvDN$JuVby3I<-X&uQZDxx$Zx27gA=#nzzilDAXKa#MMZC0bX~iW1q;@-BWX zwlCJknseXYm8%Q6tV6|e?e=HWc1 zd7Wn_j=zU$yA&1t$LZo*@!wtfMRs#~>wB^^R>$ihuZNaX3hQiGrqB1^)nn1zhN0&w z^|_wj(tBSy;=6ddWodhY_?N+%p?Awd3`PC**B)}HJcWk?6unW!j_$)+8amK&VI`ut zT}_N&YHC-r3NvZ!Do5lKRwI>GBVvPqbR$x52$j=N9aNen&LM_-70*Csnr zs1$5)ca7EJM(k>|pCnKiW1xYm{b0~hkazjSnV_3x$*E2~d-DX|?4FEGjC8iLZ|e~= zi4>FHIgoR*sV}1IgLTJPyRk=>L!#(UQJ!!quzXJFu{)}o-5$pMu&Re1+*A}Zy2}*9 zbTQ$ncomf!Ee6|q8^+IqMU=;#q(fA>#a9R+34Mz*`dm!&%Y3}27L7|vUQaCFdNQH8 zwQTa221+ZuI3huvv1sxDyEo@uz6ZaiWM24x5~N@I;-}`4i()bfIGBHbgJ1sy6QG&y z+h(hilM@*6`0Nj+c~<%D)ehc4)v=iR7**g z@^A$ZFP3ncE>rg@`1T{_vce&^^f5>|b>(GCClLAm$y>bVl&B4{qjVZs2h2WO=oJcY zxk;+6**?m2G2G5MMZFuN!Xq(3eT2@`}&1XL0w%h z;~m&HaPCqNeFqMBR?4ElPsA1$M~d;MaYXga%;>VunJ3h;I^EtT*szcV5ORfXdKvBJ zH)XTZ3t551ZpS>^F8ssxDR4Y$9>tkBj&ClCyG4Jf_o}<|v?Bber+?a8saG2}t5-;3 zsNqr@cv`qo+EGr+Z1n2I1R1yk0x}eHrW?lm)r+m%$uS$I_&&dv`5#$D=?m+`+@7t>s zAN=k6=clUenx>pR94;UYuBjd-)_Bm0}?|6iBxBxKqo*0cAQ4>4Rh5oiyVD-Yj#ZPZ&U-@y)kaz{l-YX1Xo zTIe3t05$r#53v4W7i^Dyg;k?^>h^B2E5nyRX;_E!Djm*${TcDJd-iFur`vQX&agzgGSOPrI3vlDr^fYXf9~%ze+@EtFVvYy9q&WHeYP5<;^`3j!W{3v z-AP~-kt8>ZXU031US)}xofVB{Vt!x3lWpF4;ldc;$O}_DB_ojxL|VWLn}&I&i7!|r zf4C5h<#w2(BhEhzMX)pITLgLtE4$WD+>_a;m8N++EQlUHTMM~^uA5cEf~)|rzTyRHbXD5?Jd_i*L|XF_Ir&;S5IGv0EN(>_i&7|8A?Uu%BTlX?#ZLe6ofDPEOgXkFHuV8IdF!*}- zVReW{PJh@2n_tffxtzTe@HCOn38Ji1@X}4xnJT#Tg6%0rAgzvA-edvReEot!I|tt^ zAW;8x`H}MczhsYM=4vrVrWJ&85PTx49fusu2TkK9`h(a2r)z?RlvF>Z0iS^1_7Ae@ zDs8yGOebsN1=X0^4Dl+6`zsAEZMG%ZsK6^slIizir>mWm!+-SY6fE0k0|a-iO;U(rHc!U{DP>pMt*QrRM;MO z@hU1V{$TyAD!TfT&l`U~S_Stf*`NRMbPkn?e+ob>E7T#%@u5SS7}~mn7WTTV@7Ws6!B?}2fQ0&a%K70#!4)baq{;NJAKKD!` zkRoiH z;}_Sh_BZ>1$tU%dB$(XO#MixCjP$bpoKwocMu>vw7PlPV(>O|5Sab-cc#ezgC;`7w zGyZf2Oauh27p2}W5-7iPvkJvK9pn!ClVOR8*JRToBWSQ{V9cZ>&d$>E$yy2(7om|K zeLsdKi^D$MenGAN3em3_KUa0psov1E!{?W{J#Gw=o__Z)WW*hp+S>O0s;wW5;h9$j z27(>cozoQysQxfmf8ZXHnoyJfT}*`RDniM`k@rSh@@m~j+Z#{P0k4U3jYW&jtzlS( za}Dd`R&lsg1UI3Sw&(k#JT{%Xg4n!YVOMcAC}L+!UE-da4sRNa4>3mxaCNPO>MS$r z1c&JG1`@h(l{dT{lM~^(#=9loZrBBaP91DA)+E!KfZ=)9v z{4ojs`NtxFANQ(xym~(rNX32$Z*}VDYt*%613#So|Ib8O=&|@UJxYXucQ@l5gV|Oj z*Sk`XLWn``iHTgvbIEK^QL?lSn^FoUwLzU7N+Ne6dcu&Wfh+G$(>g?r+Ec9gs*yq% zT7E+FzMBpq4NrR-hka)J`EG?E+B88NPNJ3BUwmhcTqlk-yYc!@2D4d>JhBV!c_Ps% zA((*P{lU7YTR{*cf+fj4lf;L38S@VOF7H7RT8mnSNhj zAL39RcgbI5=crvl>LrFi&bGgff+<+UY4*D=sF|9kinF4G5wUJ5;X8S6C08i|aw9X= zU>a@MQ-ZdlBolbfUb~mY!E>fRW+$&5NbIJjbNcLZ40IYe+&D;jfKTl-^Epa=rrK#p zB6d7AKxXACxVLTg0X(ZP01)>KR%UW#FrSJqujyw(UP~oF)L3Z|(z%`y74;uX*l0c& z?O#orp^vL|eEj`Q$)z0hC>=YN5kqO1fu~1_S2dK-L9ef5UXhi&P2`Rg?a#hxVl@9? zGHg7rXlPlNWejgEPtNKX+}a*YIMtqCPlO=$w;w_1KKcTKFAzN3bY^D0Brx-7Mufwd z2trH{K|#7XrK-+5&l!h?VAPXsGN&WyBNyE0bS*k=b)ij6kPrqRzh&tB*J%!&k~a?hb+Uvcod zyz_@K44N5utgKEaME_X|B1OEl`qTe5e+&OA^G53^ISb$HaP|U|0S0Z$P0#zK-qbY0 zXx03>@FwZ;t-=ybCyN=a?*hWKy!oayA4^tJ;)+YGy&mEYnN9GnbHTB1AZLy4(nT8= zxBdL7Xd#C1AgH!9u3RcAl7LrV)3izVChR>vAehN6Fi#(fOdoCUP&>}I%#BQpRw6wv zjM{{7tZp*<2is}q!T!u#Do*Ke#vq5FK#{a)ex7ddqNZlnWa_hKkOH2H()VdiY zN&Sz~@vrv!@2_00FxcR}%qE^U*t#34CoKB@^tz1UOPNJ`t}qC*NPrw`hNcm1+hR-O zQ?_R6Y2`=R3u%Tj<@)p)PC*q5S1#n}I!l|L7UKh~7X8-O%_qfeW7Gg4IH?eRtwyc& zUQoPO*s8{4qrLD?wy_#b`@cr2FFU^AOcvE7UX`7jBBJ=i_A!L2;9WdJu<)-+u=AaK zZFAO3PD189%VP|d#w~+~MU`J4K+KFqU^r(=UF8XM8f+&=ZuDvu^_~kgKr1Jp=8Rs9rZcST)b)PJtC94L_M(e&VvBrGb@-ex& zR)*v3{1r;YGIM=AC*qk{Pza}EuwLB1{ghiz2a;xPnCC*qwCi0xXb$V0BXssF6vE?a z(-g}q%HLe-QT^4VBj^4QcN7!XlGB(BQ=P|%!S8mLd}WL=N_()v=o3p;u#>odlKwPV zzobOG0U}20&|X;bzN93Kvs4~W-#c=a=40&qW8l`b#ktP8n0|EZn`|t!Nc6vF%BWjB zRrH$=mdX2C%CM-Y_;qqT!~Cn9oO7E2gq6nA1w2@0Oag7zOzECYdHpsm;c|S*IryDK z7>F8TxWwIV+|HkR_t{VAz&C-XEO7*5V_Yzc*?^ z>hs6s{_RgtrKF=oOpnYTdakXyk935&pw*Cj z+R+z+Su0fu!vUS$>*240k);kZ9cGMo)9)V6b0!Crue|;zmY(sfp-V>bJyTXD!6b+g zRyPa${uz>76D+*``$-d-Qcep+aN(b+^UQho&6e;GOOFoutrMTIqZ8#775`Zj!nS7` z<+;EVd6*3>I{Rr8xM13MxS%&<$pXA4fr)9)qqqw$&ybtn4nO?iQR`}AayR2V7{k>c z#aF-Q>bJYeuGJ8Tr*iMv^FZk$bS+N72J8d-Uj~G*ZYeN%JHa!=6|;?Keuys{y*oO* zXL_EVUYflqOuF>Ptn~rLCity?>3O(~4jbLpsrlEDoc82uClKI(W}7$f91=J2JIyaA zyfC!k*J`f1+l4P)HY2T=<@U6M%_ans6Kv1Yu`+#%-UeMYhAx9C86^^XYa2?s6RYfD-A~Zj7x;KQRV(zk-ClFv&16v@dT=)X zc#=RRLPKAUej(R>e;~&n{ERiCY!Ea>Boae+KKp50#l*nc(JEUn|JVNbEu`STS+B3l zYTsu2G837Uu%@4B5uE3EQM46r_!3m4FgpxAe>+jG56PJDYR)q`o@l8;Ox&jdCTQ_I zN{)+yyy8E%Pd|Sw`MN|1dpdyVw!k5A!M}PVS&6IwL>;GGQmrvlzftkMX)^(i3&Odab`2`c>N-LQL-i6+Uet zDd1+OU1cH~jmyo3I7Q$g!V(M~X*2q-CpKuv7~VjfD?BYq(<|k?fYac+62!UnC)saf zQxhOpUd2VxC}#CS&VML5BGeTZue9C}E_tO?@1Ml;9iy$T{{1T>+i&A~MCLh$CMU0a z1^X0k_BY9&#?-3^miZo^Ehv0{=_!{uT=5w$KC^ z9MiMv?wRy%_Y3B`Hm5>=SHeXA` z%&LG=%c(}S%aESPzRJcYudW_%#QQ^v@Ju#<_Co%_%$`V&MG?sSy4|0S33%TPS%dS) zWxSRA{c|>F?>Eg5SM&l3k4F7jN|V)KIBTd-rt~=~AaLg&uRMI{MER$Qp)v7}pF6NO z{8ka|&d;4X>RTuwp_J^)z=tWEt&ERgO?K+Z@ckNxL(aju<`ydg)QEvQyF$U=u%~jB z;fK~=mR3r4fi*q37Jm3oZV38+qwTSiD*wIT!1BAmcPR!g(7yz;O_Z8eWrb5Khb47RBf3MZqL>ZkPT*X+`A_*;H=2>E`CfV67Z%6i6B?+A#dyt^p+ z9Gppvdv91seO2~{oq;e7MJsyEy+QOWkZPFO*O%P@K8gzW=hV0B`z@ErU}j3AxS9CL zMh;5POSaN)&0iu43CF1gRA1rDZJhV2 zbiKTqk`YUaV;yHZ#6x)e`l_6fg&(bCH&6i66g&1ITi<>pN_AO$@)nKM*M}hXTK1=E zN4>`@KH9l@L!j2ei)WLib-{5e9!%WVal~y)>+0QBPc%}c&P2Z9>;n8{den={+Z|Ei z$mF+Hz~MM5ucq}3`4P$#7o_t11)^z46z*FsMR?h=cV713Ge3C~Y1>f|7NW+Rt}(A9 zW`kU59A&#%W>LdL&_v-|nGL*Yt#ouGE?gLI@DUS-hh!i~9gNTdu`O%)$J8Q8#K3tx zYuER~8SdY4^?Fb7Dz}@$$SfioZ@V1}Zf?ABo6G_7i z8Lau<{Pncw;vXW#w`wHq=BfnRwuLpEmull8^0}QwZk69pm+D`&#f<_J$6deGd2~hg zUY7D1qt;udB9;F`C4jP;1{0lh_07;Fi{WLzhCgYB@0-qR;5vW%70F2qtQ!w58%M_N zbSUI)v*&HCxeU54kwuK;LD3Qpn2=&UFf&c57{ofD#Uq7ePV0#&KeyuD2 zWE)1@Y8{Q>+#8HHE}m%(*;!n}X-Cg!;ZYu0DdJ4?mrF_<@R(g8|25dp)j$4&(g<-; z5cZXE^)kWrKYr0>Zix5r>FKSXTj}>y^ySnfoh>s0(~~&Zi9b}ab8*h)5c-n*=~z)U zw~0Cf9_>UJ6N0B&T+GP9y5N;Eb-+NoGe&3AzPm)6hC+~DdVW{rI|w3vqvX7uSj;N@ zIP--VqH}k>OWpd8Y{WG_ZdMj_WW|F4CXC;Vb-`>KXvl2^Ks|C$t@0qrmlCwBJ zStceKk%rk?jEl8^#;i(ICAHG4&V5=RxMg)#h@bCqAaw*pR#v6sh9w3(hh`AASOzHb>PEsA5s5PWxB!jZzkve>3Uj-D7U$;6ZD~Ohf|I^!>U_6 zv~HmLB+if~M#Pt8zs&rKDc>hcqq2zV5^txjE6l>*TJ6{fbW?BNmI#TqU){N?5=}$n z!K2SVg?GoM{Dg&r)_}P$Z8SqejN0uT`%~4eY9ISinWPxZZE=jc6elpVYi zG`*-Jdmv%T;)pWNdM7S%KERbjk@m0c7IaFbzm*zmM7n5RL%@_7rS9GxI^}egMT96c zBJtd594!{by=VJV^m$d44-j1~=^22r!AHg}U7U$SFOkX7;s#5E=gFPnhwVO(CYo}e z6zgYoUcb^G|Kf7jP*jPO4`_1ZBK3NOB%p(-1(S`aGfCHs#Z0;=OhdVx)5lnJ17b=Y zWKV+7wXmNsrPBpWcw8nkm=13NWvd^kB`=N65E;%INp=2e>ZHZ)2ikOmm^#6;lptr$ zAbihlXvokn_kN-JXSL7ZelGl@mG>ODTdjrpSVg&6{H6QWB)k;OYgZ)oeoHHLHISpT zcgJu#&nVH@H=yaPvn*646w3=<_tDfsAY{%-?1QVnqi}d{X>v2zA z`{KaJWRgMf(-02b9!eOibYtt;#%HId-u@c^$W{0XHltM)eAg9jdz-NP z`DOF-zd15g|7U#1qM0?kVAyeM1);h&pbt+A2qh$bpZ#56mZ~gB<@R=ZR;#9S*wx>- zSU1sCofr`zT4r>eSKky^pX$w!xGbx0LwKPo2BJ59_L5nQH=RYs;4&zuK^)*Rqdbdy ze%?!bN1Xw^LiJqQDPvJxT^DoEv!da^K;)+m6HF_DV+;cOYeU!1a|dHCY?yzJu5}-| zlkotgl7Nr5(=vfA;_&`Q#}4ft+Obb4h5D(wXvLl}H8uERPiR9s#8uf6Oc*}H!!1rd zx&A;)x}HZMN7CNai3}w3-#jc+$Pk{uW#)N;U}AJY3ohUmON3Pa^&tpXoK!k*`ec=} zbY~{SB5Nzhs@j890uFo~nyTN6G)Bp7(1X|+Y!Mhp0LWB^!&mwhX@988RWJ9I`&~9z zj2FQBn`+3&4wJ@BwWJm0JK?F8%#w&3F}^AXRLt97z94| z!Ry$bzZkal4>0n@kQa(IQ|Y-hQxpwBY|P%*H-4vuNMF|Yh{I{wAyd3IY6fI2u*}NS zjjZ9C?%aBIgg(~SqXHl3Y1WlNW=EVg4i19v+kgIiSA+SS4EXHabBGR7}N0!d>_(;_#f2?SM!Q$mLjs_=8vWh^JkeKVI=x-E0w~W6Q!Bx%@Kw&jn5UKucbf}tNegq;Z%a= z&y(@hQmA$}uK71yD3*`fCL<7;mNlNEjSg85ia27QJ{~lo_W3vCMq7?-gMZsrq zh~)3C5lGndMQOPV#sog!kymJk*{9DAGp}itgP5d1vCf=gr@u$BlIpkC8Sc>s{u~v- zy``vL_vu!UYFBGMK7@K>Tf$QPF_Z_vR52(4Fz`-+O|Mf`Eb2fQGw63)rE0nsXXU++n-cr5*$C_X=^yR?7QVv0kRZ(X4-pI=-V$oCVREF z-pp8x;&yo5u1|`H7&&WI@3Cs&lU2?K@{!7X1!`_-9fTxU=H3ygs9GCSW)c+zoH6`W z*2ni_o+LO<11K(JX1_TAKBf6#UxwwG=7Ld%>@X|TXwMGq_KEHa!-OjEa2q9KqBn>qA zeYDpm8`I0J>ghncTX1wIOeNcB6GO}0rmbur9GnB7%lVCoEOizho%wKj&e!7)XG2H=$@!O_-nq#cgqrr!L zPOp{PrR%3a&*FAXKq5FJ%C5BDQ8%a=JX=K`|JQc-|FLm+`=p}0zSJR;*C+f_9M$4r z_e|q1Ykbmp^qWy9@jS@f?f}z#6e3v*bTjuxT(Rt>y(tIU4+dRRTm+)RmF&LXlmt$I z=GqJhAFIcB@C8u6qJhdwFT4m*U5jl43!4cc*Xe^L$j;PO><4L+xS(?d(EU>Yi$1dk zj|Zj1tR*Ath`^)7WIBxYMC*-GHsAXu|LF zBXB}_FiBL}&tK~CL8%*x`_dPO~UF^bg{_uJ@Q2o7yh1=nVDH7I1g5)#9GkNOv&@CZ*}HK69;?B*6L_Y zXj^nZj`L_ZeQW!IxN;9f;FZ1u4|^qIpR;6dGKzcdOb{-F%bt#2D0ECT$7g9-QIG>P zXxcma#l}^^w70srK#v3Fl?rCIH)ns-Y%}d8CJJd2C{ux2KSSMo*+Pc}X}UN#jiL8> ztJUkQTZaziZxt{P8))4rHe^m(~o19xA(;tUKbLK2yvR zvwpnz0t3uqi%JzC^s-@#rKr^jJoY?Th0iE_RSqrsZl9Q58tH;3ovKn$Q_tw^qBATPX z(E2$I7NW-wWj%x5?GnC+k=X&PCfR+aaVG)k640I8)nE4#fp3vzIiRf3@K-_P0&fb z*O!`t=g;tO&b`cOn{FZZVs?AO`gcLIfInva5r(uYyTI5VMhm6JggFwZ#sIi7m^5VX zn)1Oa?GxW!5{`p|*wDw%8|pZ01^ooOxvJJ>sy za*2UZLhcR|p0sD?v(aPIbs0pDUt*}lfLS)%)^{xp&#%>-j=aS?zmP=Rr%t&II(~Tf zZl^yW8vTa{m^S#YJb*8U7g)jyT` zu8LVOnvqYQ=z7O?3XA-hvJ18g!jM#Gie$RnvGfs2! zy8;05#lsKf)aAdbH1FmIAFCpUx+3k?9Zid}#a2aXuf z<(cbf5&7gZ{k>CHUv_uAnay`|j_Brrus|AN`~e;G zvmeWbL^lm!LBozaN2`MK-+4@7UcLP-uQ=x5=SVxW?)I_ehzS^BUmQ?z{A5akti4C@ zQxuXPNKxIS0Z9~~8McnAc8L9TPNN~v?OgYKBy?(L`^(`qmGI6;nph@X8ix1Gtlg(1 z?_~UQLta=*^5s1zSmK(~iKXw9>U3FUln2)8p?xIeIE?S{LT)Ve#gl58b|w zcje(^+NTn?RkwPYETf7kyz zMH%0?tt?~Pbnl>>N$|{MGxCfr^(#Ng|7d&qE9eYTn{0l}MgUjL8i+I|TB&%>A>4MD36^`4kWDD?-|OmWy=|7;@xACu!^ccYhy?IOMH z-VwCDWs7()5wKsbLRu-6ot+coCoSCVpILUv>(n4xw^bZ$R?M#22C?$@>BJ}Si*$75 z@?yvhYmx{E3rSZlrr;q)FIiRjO%!tz9L~ESlMN zfC0A8wjq3ep2H^W&~y^xQ?Q^e_M+C8GGc_;{I#m;zj02+w)ttnJmNcry)dz09=Ru# z-m98iBqjtEo{y9b<|fEmw+=dgHEY@Ip@tn4AH$B&Vf$Y5UPrxQTw#k9N2d>lTaHG_ zVf!t|IBLal=`jzXKNko7MOPQAHVMl$w=!JnD}CpSG7gtreX(bW3dt_u0AGHm?9mU| z*^MdQ1ytQ-mfuV=w)?+ENx$4HW`F-?$?iSbm#c&R?7uP7$+!7Dq4Ys_MJOY`j13LVc+}EY)S!|P zwK0ER?I5XzOq`!S<~rg#>YF}9lj@KAj#-Zfj~9>Qk5^g_j%UhMrd!uF>PdU<6?2oj zl>bB6cSbeYb=zX)RRmO&Dxd-)A|TRRu+c>n3mv5Q8af12KvYWTEs!V(sPx{6(n1kJ z4vrKBiGPNkq(Ow?lm9D%IGpFy^Z?UNo*>d|K88=*Hd0cLcx6M)L zl258l8V^QI{PDcw{j+R{;0yc*%Nn*|Rt64WmNY*r)tvI`s|}gwHV@f1sSO}7H|`9|dC>NJ8h1x=^5bqo299C9(zA30k&e)V7&%Wb zFOHIC`o$@KlFY)nV9G=OmOASdMBW(`3jcMB?r$6dBYaho$61epICp~jQ8;cD9Ge@p{!)BE_#Ih#JN8n@|| zy$#+*YBz@17vo&(y?7TMvc#3CD#0`SSgYf!im#yNQ1~Woj)RzbIA_s@S0x*nBKKIP z0%>BV$bb{>%?CH!rkg(kp!6$QRBvWLgAWyacXk&u)v{L7{K>!~;FNnKQ87+$D;*uo zA`!Bk*az+aT$HcQ;~4P19ju2xS?5&gxT9;JgB88mz23F@kFC*Q4hd^*4f#5zM$(>` zX(;W6+hlE2K0J^+l)`!>N?8jf*l#f&E%LY5fl*_9xVv)5^sXU2&`0DJ$KmnApZruw z(}4A76SbbqB~4Ua!1QoqF0%d$EkX;8zktX3n2OxLSPeKilLZAt&qdOat$K$GdMUJ# z&5vG(6lLH==~0x0C<&{?H!!*BUsV7)P?g-R;WhsB)p`FpqP@p#OHP_BO1>`WowK_| zE3BmHeFI>ifZS%g-odcm{PrU14q7TUR<72cc%5MeyUXK$&@1Ob>cIIQ>|)};9m?oh zA6jf-Os^np{EBPo_?2tAY*WEBSJQ?)?x{eM?nGn2iBvwwIfLS5b)LAGh&(OXOXGb~ z=0SwZfODLXbL$Cw3=;rgKk|-iY+U@?+dNBSq;)WYb{iga7%-ut@QYYSx~JFzs9hQG zg`PGjR*pdNhq9wVwBvk`om+8Iz@)`MA#^s1b?Ca@jOXN*b;ga~fvU*O1GF{VzHt}Z zUa4osB0Sf~frMl(cdjXlV_b`n6%rfN)aN`qq z(Sqb+&)x2q^zW*>!o;@c(0QQj*Tai^dSjyQ3_~qs?eyUQ`rsiQakMeru1ZCzMxLDM zh%p&^%qE_x3eA+Nu3pi^z6Jx~L3Qt>xM^)P6MnTr#sF~H>HUUg{!l{bkrc%_ba%QP z)x0K&U5<;EADe#ytnvO=ztr$vH2pW4{@6wE&o}=+z?@T}A_5f*V;UtRl*g3hr6_roRkIm97AK)|QrkUa<`o_{}# zQV>cgnPH`W;a3C7#ErlS@o^rDABE#cXW)&2mxl%}fcEF|w}L{pbo^Jp=L6{W&QaT+ zx{^=iIT~`G17sa7X43PgBLQ_jCd%wInYN{IxCQM|=Q)$+U6GV(5pwE?jyO<;2P_>> zhNC3r1g3Y*C`A4vNuU%A$?@;|dG(FC0C5DBMv6`~4?JaY&~32wodd~(2n5=qP`>NU z=ZCDK=a?+$*8$b$W12&GE)#cMW&ICEFdp8OzsL|`6worLnek|wpMHX}y?JmK7^4d? z$6Gv&3_sZn5I;u>fW)K)806=O4sysQc@Ag~!f8(5(3^RV2)TfuvYoch;i>D77 zwsKUrbASY53aa29?{Wev=)Ag}Eb70wE4MX4n}!0am(MA){D%+4);I82JIXTSLII-rn9R7B^>QO+Z zOHEol5SVS1eHYJ)XEuD$5I1d07BwIO8*UsSWb)Ay`hwn}CjiTRN9;f#*0Y>)E6cKf z?3fV4zHWXpjx7!!-Mhqz1PJdx=D>@3EOy&wA}Rm$WdSPTb>Lb`r)}fbTaK-dP{=vN zK3s23BXoy68@h`>Vx})3_k=wc%X9$Wh4l`*?8nCw=@px;fZl>-BV`itlhJ~56>xse ze~Fk*s>Ce7<7xvJ&jLnmq32vpF)?);hqv^Yg=c!EmsFR`)}+t4<@S7XH}^hTkR4dB@>a3Ez-L3ii*bv!^4>m zBN0ad3yEC()EP$N1S9B8=&o?can(I*)%QKA5?MOcIFOVk`OsPA;_3PrRIzEh6uOZWYEk#e z9udS3qSc{iEsiQe*^%VPkbeM%y18VxMfCB;`uQ#`Col2x6 zqlj&Q)Ie8(-b}hZ;uq^dKkF=-o*v431Ou3I8~+jFK-7OHUIU1XpV8hwCAcu^e~`H_ zu6(*L;SbCG&(767d5-?jM(-dZa zn)n3IoRFAd44Glv>TC!9O9m9|ul9C=JA50Gdr!!+_3M2?)CZOu|5+`741WMs$uqU$VI4W;|HMY)OFp&CCe5b_r{26Diz~# zIB`nZcSZ6_mQo9F+V`V~Zn@=8U(tx8G|%#^vL>Z@tdjc)k=cm|rnDO~Wj)YiM%9=% zA&hsu;F7v1d4=)0Ub(5ZrDaa2dHn}43dQio8B;Ppm;+78%uXaFx^@iQu$;1+P^pV@ z#LiF*M}KnHxKIBg4unTXD8L_6%|p((BX?!8Zk41OCE?TLCSZvozAEg!m}e1id&6W$;! zZ0m|}(esezoyAr;`|s{zJiM;+#F*B4@ad@>ToQ*P-v9~eYjEYUyLVh~{5wqnOn~{i z&&6P-+oGeRpr$`*h)}*%ah3N0Q`fCK>=Jq-7-p!uYm|-}b0C4H@&Fbog$6|dO1UR$ zscrMM#6IRLyyMs>X$!NFj*2ZXcVvvk;i5%HP4hl;A#l3DETn*Uz9{njjS&3i<>nZ`a~KDw`ba9j6oOLr-PjeXp`(ZvsHSq(|^uB?nZ4Ev#tC+`lUgKrT^i8hEbr z3>rkb4LVRkJji&{AZ)Ub=~+3@tHls-Si4$iqtyQgMy@H%yyySlc;ScgEc*pzf!s4% zHSwDD7h06j0CZSt*9Y7`ZD%v?eRZuMR#OBZ>j}8%t7xe>w(0=&j5nFK593ZI#hldF z9&=gkxLExbIOZjCS5=YQ?exVig`OoYE={dZ{X-{#^o$1}k}fXz03Zo6`zqa~E{Nj- z!zI@mmz3uxhzgH~!jmrEy}OhsmgjgvP{bDjcB_0Sv!Ea#-1%NWM2jzsm*F?gXbJWv z=ARBu2L+iu2-pY^Razhc2!(?}OATgl&xBMrnGSk9BGi zc8#YCc)`hr&a^wOvgL0-nlRqg>w>^-pT|oJ0k$*IBnl${YXXx$b8O-Eb&R{sB!$G1 zGjAp|hAMw`b+P;DZCFX^%KJT$J!|xE^jl$$g8gF{vsX%x!piRQqVH7Sn47&z?8Uf* zJ_D?rhI#&Lvf@=|vHJ*JQnW6%R>t+j8zr1lr!W^xz(`aJ?ww{|S0Xo*XH;V(he=nA z`eq&!aQbng%S0jo)KhZ~#!jU{gG=sp+Cze5#*4Br-+6W(0nEeA%yVBIk6%SAdcz+w z!~qWt;R`QLJ&E(Iom$nLG=QN^rD`GX>o+l)PB{6h%oXXZ(*9~vq+t=1o%gdO~ekLbW z#tBSe#|4p<4t7GvKb~&jlo$h>sW${M%SW^eyY)TvTQt`+%eps9xg2uU$rEjH+Le(= zmY5|6XT?Ia4|ADlv3+lbbNjww0$KLn=qY~GG2g6JJ60Xo>h!2X-_OjN!BJDwvffg< z?XFo#SfQU$iKd8)=>YP{2x75M{OPWdQ86oN1%jz^rMBE+3iw^&Qky7MI)K+I-TjNo@^RjKW2b6nv;wL&Cv&wMCzZRt9jcWd0800itSS~F?AJ)m20ye}J4 zkuE)3u9ZNxw)RQ^4P=9axr8AjRYg$jxBc!?CMNmbCIyrN7Q1&T)!_g*N&jEtEl@kl z_@5*HKWF1&XIKn3!#!WU(%w^*-&BOI+Ak!Ep zH1s;nb?~+SHttqg0)Hysk3lB2d)Z$P(}N;QCU)2ME9W0DGuc4|<#aAc50Nw?bJ!dZgS_tEy*QxO+*I*lvc-2Z47+i_jKY&91VQoH-or z#Xow>QhUV3k02cw&lj;(elCB8?c~H(*#8`=KfnD8pW}dD`({zGl08oI7YCwdm>IMu zak{4WA`4dqzYpUHi%qv6rnCt6M6m~j8%@X+RJvsV=&JhU)tU3RSvSS=2SWNeaj&oM zGs|wM5<^y=<)2SpF|0{H(H%G(BY#}QB0+fMZd&LFS0Gq@)kS)C6VaXq>2h1|jBquc zE8YcRl<`KJYi8C9hm3Cm;Ky)}+Q)mo{C??jAhXNEQ)g+!;$o7R@O>DaZXrxk7Vwe1 z{6q0ljiYuV{~CYa{_Sr8QI977#DGrmzZgh4W03n#%aP6En@veMo19cP%iL4BcE}ai zI;0xGwWG6DyGA1khJ;$>6l z>y2c3A5X9N72XL_*AUba#udREqcKb<=aC z%G1kDK$`Zrc7B&x+$IOw=^?=Cu3Rp%{QmMHr_ws>eYu-u%tGv>RgZ7sz!`?eHF9IJ zw9=t6_{MHIBv!kAqFD6p-3U8t_DT7#F6FZW^qVC{R{lj z)QXIuDy;mujX!Vgq{5CnlpLuy=Xe@SyN`KHZz<{2#xW(h)qh4kY|ArH5P0tBoAQ)I ze9taMYf}~K?GR`NiMVV9|TNk{-o$G;Iwp}!mqSX~14qVJK#|O5k>-7SA zKNXC|feuywZ^#r*{_o4@;c;&YQpgdc1(nW+R_*$f%szT|JCQGvbJKE~34D{N zohfu?+5OPdVdKBYn~~u!_u3P}B#^UCQMO(Z0{cZDg&KMogVR%nXyr}u0&9@0s~{$<@v-kY8*B@Z)$VLFPb!-Jd?#NJ5f2>4Oy)+ zrV<9)xR)i_tIyoy!>JZ(#u`uFEqXHb-_PZD<}c&D-kSZiISGmx`i1QG^P#i4m(>lq z`5cQKAu%WVxJyN%S{WQk+;J*RPkUoPAotYktilhbkC2SfRNnx@bbgp7to zEiXIscl@D>UiPxZp(=aV&pv%$5~CzW6CTGeD1B^_b-Mm%D)`R-4cgSE^0!o8#Rbu7 z+3o@L*)`lT`SmB>o{!OM{WKi793~!l={>Iia_u4ao-Cj~?6_h9jd!bZHMs9up)*+x z(rnq~>$PqJc^P2JY%Bvx+JCo!p}R%nj{>u?T`1=&*g;`ugW3b`qRlC0?LXEs!I(F8 z@6U%n*7~0|6IXE~6z0mXMfjQ=jAz4GD{q!TV~7Ob(Eko}3rcU3z)S9e|Laxw#;cF{ z#~tl}9F>D&D0^Q^2KulNoyz!ER|O=3W9Osrxkv0h#+9Uy17|{%0C$Ub&+W5)) zd1m+Vk{ec1PGZ4**K^(+G=d5#YgrJt`CL(5ndc9Bbu;TM5((VFoRSdB!3A7et?a>p z3&t2>Mf@_CdVGR}sv=bAzccs)U7M1BLEXF8F&DQY8Ih7LAfZL)Oy9&)<7w8)UK2yY zZh3d?5L_A(&eidgr0uDBpRMSoVA?Z02KN1$R4mC2Royg@66Y61F|7)M5coDr^V2;) zEBryG%{za)@ysMmnA|)=m)`L%+21asj}g-rmzY(2jV5r)WD(SK?$3(p%#CCk|LnR$ zg<^MEzFfmV?P#@JLL6heL-Iyf_g1p_f4`>ZU+y+&d4Ry7dL{e5eKnb!D5!b4LM|;+ z8<4s9?TOxe5QmoaQsJxy_l7`Sa6wuzWX#8`BQtP@Z_H^~Z{h7iV5!@ah9{|yBtjmP zvD(Ye!riyGW{OE%rrS~;e>MphR8Ic|8cq*~dLsAqHqoWO8|HruNq?=mcIwQ|#Z|Cx zhS?2$`>`*e`pl1sCsu?Uy0GfP3MFJ7*6M6q#Ot6BKv&Yac)wJs`l+DW@zy{7)fsgD zFHV1Q!2&y766$v3mJ))?%zj^0M=iR&J_fbm3Hm2WOp4`dA08Jm1a$tM!HJ>lvv9Ps zrLNXiES1k}vr4jiJnfHv?6XAhg#7vNpU(2%-@T zk)M0Msn5~t@&EJL1sC~t^O#NUzg(1fig1V+7ryEk@N*6^o$nr5er!9dM_ZwDs=+U9 zVCn?>nBBF_8zJlq(I3%Gk+G0b*Zg5;geBaZn`O5wOv;xG|7kJa_cG+Q@|(qf?W8e( zxv3_Bd?PX>?~^mWpUC>jog-^nq%C;8ynGVe-qnV{46!!Jihi#^V{tuteOHdny)o%ZdHIUPplj0l1%>@_3V zU)rY=d}I3vfvRKIih7Jz18U|~lh&nvQO3rTJ7pRG>x|?+da`rXD2OrmX`s~vA5lSZ zPxmo**$pU21;$-a6)y``6on6ZeslNl1;`Rn2c}b_QJ*S+54jLQ;Mf3T~sbL;TQKPSk5R$()pO{L+^N=#~cJ zj0Pw>(4ZiJvV}md^|Sc;e^s2_G~c3McWeC{6ST z9}LAx&26GntOf@=$|$@zbEIC-oxhOCwrfYfL6&_cE_u4^AaJ>;a_1|9FZiy@SI1Nj zYck%SyBhI;HfMj@&L;&7EK@HTHE%0h_i-sWJ0e41q-cU~hqBJ8P!T!JIRSjMa+Jhc z>X*PmLg&wDqljsF zpCIwrDaDp=MEQq4(#gI~!~xD>+;nre(Oy&hd5M+UBS$b)*XA)x&>vh+T#vir? z_i!>j$E{+``|6C?GU}zf*1i%yONv>0*is8_p!mkGj&6IMQuI^{7ZSR?e9qjuWG zINhT{#j7yykFpG%+rFcZ&A0LwB^NGcJwCCj!r{tJ^0sz*DAVh_$ z58}DsG?%NRo5`ri<_k*v<4-aI+GGRxs=!-3U&Z2IIEH!!z<1K^eJw-oRqNeo@0)s9 zKTD^%H?rS+uv)g_fi_&L+&HOFiq* zhKihYNc=#>vqu^ux0^@m>GWsTeTyb*a$irEI%DtX&w3|;L$9Krl;nh%zm!Te4R0Ic zD|6ibCa_*V9G>L9LB+~E;}uT{WS{Oe%!xg<@HF(d;{bZo2STA)u%{y|e=}ZfKB&DI z5WgVl1Np{su>V7Q00G;XO9pdIyvF41NVo?$x*hM-Y1$yvNp~A3f+RbsV829J$nK`7 zo934~^ohhN)ObjBDb0AgwI}m#oTLuOj%}T7Gi>dX+0SO3wOt`6b%;#K^rL@uybJj4)VCM|e1H#%G^ie9zc-gEHM1zNUo-n!^_i-> zqQdDulRG~(%&cmRhWH}LS^!^!EDS}Rl}>gX1k(@fLBS$1ml#dA@;Tik9R#)y`!V~ zB2@}!>z7R3)^9pD84l9gWU=X_+dS0Y3G>_ocCVPu*^BK|*hfMdpeITu>?G- zGZwjD;`1b-!aQU#D8^yh7XH%pihV%uE1UeWrE^w!AnjjY7KS^2bQ#}>ky;6B-vEC$ zPn9025&{?jdxN=em;SA&%O2>E_`*}T)naAbyS0-(UqC$|te>NE@36VrjZ53}VBNhXf;XJurrHk8kR= z7h4zsR!hL&%SQTW*_a$$lS^To1 zKCXP@`8W(Bx}q(8P807T9LB{nDxdrikP9d#_V^sPs_w2B8hi!E zxq%ily0yB>$hXFnduwJ8JZ4^<2)MSmO}2Augks5yjpPnjh~02kca-xF<{tuDVT(Cg zv;Sm;W@OZ*`oJnEcOW4ISW!q6-X}}_S5-TD{3h7o24wWxCE}zZ=PC}ep+j~0V1JNV zgop@+3fJlCE_~NV7~SoYr8%JP(O^aF8)x@Y^?Ng|u%qiv5mphTq z(pd=asPBKD>w+w;T-%Ss%soW#S0l^%FpO`V~0FyPtHJWC=ejE1pVzM4n(?&fT2qQtYjF`>myJ z)HIHkIy+G9f(F~)n0EQqY2)MTR{9zWXl~vy^PP4AWiZ?=?MN^fzw|?By9W7khCM|=W|hmeXOvmNu)@Al(`=gY-c(FNNf#R9*< zq(p|_2ESonA7$(uyjXgsYJro{qq_7_X(t7*;*Hj&Im$i1D2QKUU``*laPs=%C+bq-yg{7nf>ksmD6 zVMUlg-F4GVriRTzGgX>b?0;0v8 zB9`UH`40@}&5wuy6fd-wTbBuV1PzS%2^K$Pl}hyVV9YriQy;whz31 zO6qRp(?=&ia>3pt`}VSXi5YTrFehEi-gvQEb+gX=i_=h5KQoSfq_Ym~*p9ABCttTT zHog4mbH?|Qxw)P%ubul~io{p1M*3_cma@O)vNLy=QF^$<11wo|83;EIswxmYtDX9W z4;pMP`xGx1U*fP6VR$X))grco`Esg` zmIH24uTn`|v@ZWgIOvo&0iiVtJl(q`ARlOFStUn|8nVE4@`Ye&0qH9-P`x45H`<+Mc1WF0;F`l zq5apYN9;+`X1+Fjk6Y(ty}rv#>MslP?s8edf8f{KSrvz>gR#`5&N!H60zkeCdWq1X7?G4Y| z?}?{C_l0jf^zY=VEhg`~gEdY!*)V06njfc)O7;RA$E_>-vWqft8AY4t2OUQ@n9V+d z=mi1GXwy*jnBEOzX)@~z4RS^;LPgA+$ZLn?{D_919_C`j{;kr+ zu`zH+SM1v3y}Eu#ad?LuE^uE_)jfS+rk>%Y-6-?b;7IR%v{&Fk57m<;rN$s^p~&s- zKtS+uyAvZ{(LVP>$peDf-ZJ|lXo21@S(lg(o0pdjWk*it2_h^Hf@-H)VKuYoK1nLMsiZBfdLU}H#OoT3Js=sem>C0?MM^oy zYE-FP_SyFQuCKE(dalUiTCW*|Pl9l=yi%)tq|b!`OA-T(QQb!dI2YijO({}#m`R2*GF>`!QT z9JU0w}3n;Sd--DIN*Pfnd%O8?a&1F%Hkpu z2WSMxDU?bM`Mmd(hPr3q4m~U78}U4E_>~s_$?mL_$$_~kQJ7=>sGY!7LeIGCx$K5_ z4u0d8a-(Ym##wAfr|Wvb9a&0#9<)BQiSfQ#AN!~%pa5z$SHoi#D8v_dsgxP#w0>Yb zf2`$ypP)uOQSBs9kyGo#c7(K`aD$HOpq3dIw;R>q;C-f+su^MH!IHN0_{lH6^%KJt*>7-Llds2Hs`)B5lZ#rdOZmDeY93IkTLMc5%19E>VKD#>t0k<@5l75KtT<=n1s80wMos{lP}|uM0pkLiHLseS2pR>(#G#k0%Q9 z=8Wi;#q48Y9kqk>fKC5$ zPWXcBF0r;x1ny_>pX{aeov7;&$9v%Ozt*nrcMKxsa2yC7)lp&Vf#R6FqV!_Xlx-!Z zXsAF&JH1){5s}fjsJ@*DOVqp8CAMz%#Kl*} zO19+>7xrOm6;+gS=eg^bYxsCR^s+r4A zru1T89psPTADA8L$~#|N{+avq za!vhO2so_zGX!ucr>^$qN+BNBi#?ge~Q$3hcYZQ^!_%l|8{<@5zR#x-5zUz!nf}*&58=x6nAQ;@wEq>FLd1TZ;)xCQKQ;Ci4zW_6rYB&h`g`UubyAA7MozQ zo%oy+5bGn0^~eLg_=WslJkB-HfifAofKpRDa8A%RX8EZTyTY4=c>k?P4(k7;GfZ&m zM2^q+n#me;O<_-B`JDDY{LTY4N;1R>(UvxtefHWG{xE<;tEsEl&N7R$KwRjtUN+e$51N|6#^7o_ zX(#n8AeZp68cLydMCc+T^Ir@hSmXh4h$%UhxVkF_6jE1=UDbJ(=(@!qeTwZn!9g~A z?6E$(bVK0Kf|Gt?{Vkz$^<9xEJhjI^pA>YgNqMkk-PCOOvk!9cuEtEMg!1lI$@OKI z+s0zAF21ayWD6;2$SJZn?-=rEyGnSwpE6kEA^Q9r{9M9|yOz&IihJsWn8%*+8Ca~B z7A|JCbU_BiXD2ACls4Uoeirm4m1YjBLajxsVD}W%h50hGYCfVOsf~L6j?Rxh#!poS zPdEGX$5%B+rbEEN$Z-u6NH%^T%{<5*68!-ah#^R>@)xzkfnCD!$RIABaZGA&D~{YD z8)R8~I8at2WkthshEDD5qm>8x5j+F;*QXg(s>`~(4Ef1Kn842N zT)T)`pqKU8Ku8YB?3|5wX|1F$3m9u3(zL&>Gkn3?8meFvI&2A{mFCS$Kk6k;u;HCu zJz2;TvPqUX4eIPMQSbZAO3_Z^w(}u+$!{|6KH12enIK9oB@wGq%~8*=GY>a| zj^h+8weRL-H{&eUR6PE&@!QBz_AWhBY9kZ4&OTVPo_2pQl#b9(kX1OAKo@%EECFG- zLB`)N3V2uWQu7|Xu=HMVa!c3^bz*id%a1P8%F#0QAp|@XxucrhI zlN4WJ*BjyOADEC)@3&TIIZ}F+yF1~pQE9Yany9zZMzMzpu+$Ck^Sl+@L?gJf7364C zQMPxp=zdH&ZsF9ATm@FwJQeI^6TiWcT90P=B!f_^vUjakb5`x98DQR&VK&Zl^B!8sTMdcxHc*+}@jje6 z^P1}wj*k!_e3F}0>j$HnwL{6VRO_=^SV4Y}=;S$tz}Ryf zV^3JO%|t{TJHsy9Yr?)Q3w3+cy@wyxj{f3Of&BC!F*u-_4vKAAoKGRUmd(!p%Q zrG`4I`&;H1mLq?mOkszr>_nYyv)L3 z%saY7(}szH$lxgd=>fgyNoUdyHK@9*NUqr1J6LQz(AAPH*}=WAiq`CJt{Hc-#FPwK zrh>!{1MV(2a6HMC5%j=Rtt{gD=2ga&WMR@GJ_H5&MXrR-=@ zZ(kE)@iqdiP06bWE@07)QrJ+|oIKBV!x$@vnFGOC@cKRq8XGc|a%?dNqhA$Yqs;ww zC*4lRGb++kou>^n6y$h+Swshv#%>u?cm2QR;GOt+m)s@&Kes-9FyU{$4%kOtD{pIX zy}??wb!bki&a6zk1V@c&!(e~MUUabUgw27SZphgx$!OAi&N;*=XN5oyv$K!)`keFE zHSiWUq|LwSaLRLY!UNiWt?AIb5MB*`qQ`IZ97gGWCN)ll&V_raZxzsNa z&M(j3;6nGTT-(2yv5tR@JHHssuG9Exk5&bUp|LI9M(QZ}1&6zWe);yLc|RRSy7+JX zZDaD&$(`3h*~wYCSMSSJzIkz>GoGG1ID(vWYE_pxIezPEo2JX}21rdc@1>#L>!))ev(Kf|W9$8~)h>x2EPhze`thq< zYd3g{!%YqwiC@UU10xSlXWAaaxGBEvmclk=-0)*dmG(B}6g7)XOeL_DZ-)cHDhp2fRg21Qm;T zc~FT9!=SU2`uw{2=IR#sOjIxW*K_PULiY62hAgzC?bp`Kz$06?y2)?f#{w*>_mh=0 zl>Gv35x6v0JMn_8uID~)+N)JVFUK-%Na?Ux#5@zwJWa`5xOguXhBi%MgB8S((pXfg zOB6$uJfG}vxx$2!t~xaow^Aq52J;yg!Ec95q|;&p%}KNEe%`O^-nLCC7B)R@^X{zc z{oWh7xwiI=yYQjKL#*GL19)I{3qnwCf_Q$Xme=np2D-kn$oHBWnSJcLS_;y&c6^>e zS`C~Or?}($vDmV;sUqupqxN8E70CLMKquqrYTM>80A#Ji1Mvc?&l0Ojvhri6Uo=N7 zR-t_EYed#WmAuEC_t~-9%xPuATcVoCZarPfo%fZe)P6H6&d=9fz_SyqkitGzCFA_s zXuW8;4rEs+Sf}F^t9b$PbXhMJ<3L*^-w;ye9mvXSF}BEx>~A<-Hq9SI8}l;`wANv% zun|~m-Eye-6&{|sm}Mna;tV14{O_4%xPv9b4*&kN;`s3j+Vh?>nj%_F{x>^4Y%doV zl`ESob}@L3wmfl7rJhu9oO67mCix(x#OD2B6?olw7Gk1MmHkY`fUIkAR_(&BJF4UM zKR^$SsL`Hyj}QXqJ&`HDDSa0F?$f2^^6`Z(Z5#G<*QJff%iO9P{+NLEA=!owU5iTI zEE5yKHpA)_>S)PHBT{>x8SaJ4L-Ag+)$zU6b?LmSk+nH$R;>O{n+=<|hAFpu11p3= zJzmNU%vHtT1|y>6P~45Z;>vEU87IHxz>>9$?_S}V5W?^ryGm5orG`~wmbS`y4!77o zMt$+Uc0@(81e3+d>gLJR+HL0|>?;fBd9gg@^;P4?FYop%3l!$2T{a1DHOJHLA1nNK`5^>Ad`5|2Fq;mkT*_gf{hHD%o>?{f6r{61P+Q$lZrBZ>DHd$zw28 zGxG3woRn8FdiSCU_+(8kNm8>r#GeG zsj{G?`BF}mB(HAc`G!ml-M*LUP1vtM_OeU&hFeF~5_{-M7Mu&L;?uScJNXuBkho}5-?=@#;V}}FR@O9xXx_?^z~c9 zwC(LngaMIh<4IHsTo)aQaaJ^KaMb$M(c#|&^8JUoO@VOr6k>DsO2xzu>7tGjO22EP898I=0p(is200O-OJ?~N;apN%mA z;~m$WUn+rA4$W7YKa!%`LP%K&FVK4(0h3KtY&7-G$D&QaJ6j~EAT8zEbb`yLVa{5a zI0Nq;D?^xaNI}Gvs(o#&J}&qY!teD?zI(izsr3e!TzKX1jdS=0BlM?bk zc66n5Bk-BfQzcXx3s=1kqo9Q1=*8z#0_=*bGE)YTi>-7S#ot9u6&u54rp{;f;I}|L zyo*9hIZpQyp|LC@hP!J)?EQ@m85+(jE9YwhvtA;Fg&FFaS!2D&OxNtQconZecyFdu zS@_h_P6fJb+CPg_tCA9Kx>koExeQfA@6DR{ER$~Ce!gsfydqJ;jFp8p@f1-ALW`V)-`5jc4aoMmT;A3Yg_{aWA1 za^;(Kn>a5uM&pRn*>lM5tW^Rjo}*6%~80h!uNO?Gmvf#Hu}F zlLSF;_kGUqd+u}Y@3}w!<-GD=PTud=b6nT;h}(<32n7+d@CRg`u6ceu{7vQJKyKA8 zqAeT_WcshV@3#D<|J#2h^AC$$_`{&J5QveEbdt0g;kw`?ezX2GsVAup>ARf32i#qq z++eI?3RXvb>ZhrOlxHij6!aGfd9|8stcDF?cL>k_MBBP6 zN2B9Du`n|!%X`fuFgAU?LbiZAj(C;*)KaoepYI)j#w=fNvwy|9lYUnINAQ}XmWg5w z>}j>wl)1Ej@z!KI!LPK|5_adfxOf`>Z)q zoQ)dN3^p6aYU`rNv7tYLkl6!|DQ$mUd~6beu)kG#|Dt{i9)LO2tO9v~CWAw}#~~X( z3<$8f^jJ0l=NpgcUfI6$=*oV)T3*5I?BLw|xoobc32$3NMc(M$f48y@-YG+9cD5-S zq(mdF!NAU;+V^(%;r8a`DGY;|lM~G@8651ZVycGM$PL36NO#jT{r6jk<^FzqU0?NM zrkm@ipw%kQ*3Y7A738JTXRe#?f|{-V)`0v2$6E_>3!i=s>ZmyiLUv50wh-;=F{asb z3@D|{wTZVf+SP7guQkic;*5o9MfmYsKrV1@7WasHE?G*<#~;cDXgT#8_s*KEkTX7O zl2c>Qy_)QRU`0IKp(S<`F)#O=*k}5VdmXUZRO43x3F}EjC_l142;^K*0HK1f+Dcwe z3!(E`nPy)URh_*zk|#Ck)--Crb58iQvD@Gx<(HXD$tNG=z?Ln9MzXI5xm3EZjx)g) zuXAz(e9{DJx6O>kmBxlWhn)67r+2{?^}`b%K!0YbTxHM;$m7l2#owlgsW>Tl0R9;q zGuB$vu|IE{{5^w?+gaEjVo%5vVBK=KGiv!XXoiI(!B2VJ_z4a2@zlc4d4 z+kwiEUpCZV5$c}k_mp?ugeFRD{3A`aY<1s#y!6yZX(p`YxqrCusbV z?Z`qvaR;(^#1*0mn-^1!lz~k^qtvignJu3N0}{GEc*bDhik~Ej!yO_%3pRa~XG?Z6 zSMeAK@oKQX2Ji`C`XVFXc+sZ$v|IEdy+mwxel_#=4>jh>8T-6qtkED7d&q0?|4^SV z)CZ8S{kupPW6St>!rHvE{!|FvIImPW)V7;?D43h1pUvy_;p>e)MlQpEu=`Q@(@f7q z?1;SM=|xv@oa<)_Q^r}@=2a{}1wM~=D1u>RDtK6D@9}Ss@z3$^9>$wyYsjT{Y~KEj z|3bW&0Yv(8Y(X_cksihbeo;*MOEe>of=cV%JFn~{aEsGGgS3t{_KQ>I_g&BL$SfDI zAcd7=YSMf+r=5hur;qzFLd?6zyRna+dgSyBzK}HOjI}xyjcFawQ!?R+69x=RuMDZh zc1Q*JpHH*FCHxfexb7qSx}e4TclI3kS0>Wm#V#KR|K6<7neb=hcl8w9F7A6dH4C+t zR!Xb{9eta)Y1~40q@i}$U?e(8fkDZw%_}-|4hkRW4nh^Ipi=#p5@?v_?Bc4>s){dm z1XOO=`C9Khv6oqm;Z=DH(4X~fzA|d5pZ`gR`m`*uiDsmDlBQ+HND_#*rz*MS=z7!@ z@HMxH*UbweGqh1;IQy#hWLAeduq(59&TD140%*Y#W8a!IuMANE4Q`(5)sgqVT_Nn`U4C5fy8Q#fa*KXMTlQ3W#>=D5X0n7$U_Pc-Sh{z}pu4Xx!RC z>kYt;40&i19(T`V+ZHMRxCl{Gb6l}!&q_QV_XddGIBzh4DVR+?m+G4N|EImzAlF(P{-s=puix&;JsH^TmB|bFyv&IfQ7!5bnD-4r?3)iS zNwwElBng$`K#1Zmcpok9;9k<2_jfe)9#kkj@s&u|{qegOqqkm!*JBB|8aj$);-$sJ z-yZhoCf+wa$473btMSL}zOXllF#}TGc^p3>vfHI$LK(N$lP3)sYQEV=$zkVy&w3E# z{3Ky!rJp${4@4I?Jxb)8GP=4MmHh!is~=K5(&GevJSiel4o3bynOwp#dO7(@3^v*e zBykEikd`FV5nYvjqMj9z^$lp}8(V_M)RpjNr|kiUpXJx1E;wEQPE`B4P*e+;45>KM zl=lQkg8uF>!VBYgYo#-eXM4Igq3tQv;O_d<_QPXY=PAB1;zxmE-!}2jlo!L@A}PUc z3>_a$v^=Q3Z9%*93rH4I&-r(v$6s;dR&&ibad$3x?g=V5*gXk48noCh0b}g@0VZZ$9~<7rrF@$nGb@L%7;ztj{u!wdVY0q!M;_0f%P{5CSK)ei?)58bT$&V{5sIk-_ zdcJMm>8d0eJd%cOvDJD0tQF@2G4Z_~7)AFh6}67F_9>w%iIxI-$?LnSC2){|u7OhL zyCnMIM?d}*Oa@QJmtAEw5%c_h!#;qs&mF)gq+X4CZm5PU)_@>?*7-A-YN}9QDU&D;YR!C_I#l1D_N8{4g-g z&BEN;pLA}cFdj!~k@C93psiwl_TVE4XU(d&mGpYyHCQb8Yz46J3cuu@2A`Ey&e$vP*eYUBA@fTmB-=Jzt+?Zq6^0AhSd9!71=)WL+^Un?Lo z9}T{f@?`YUqV0DHqX2)j*$Y+8`@`A}7r^CXvNSYEa`A8Z4>H83Z&{o~G36l#Jc%^R z{Ei*=NVmm_<=^^sp+u-9NqzZ`vN!M1;&xfEpkef?s`e+2qu#QJ$C5ypn;YWMrmB` zWw5!|$4phv;BN6+pU~^8tx_-99c*jO1H?@yr;O}C`@tqLU}O%r^m~M%KIJSH87L(p$Y{oMoPYyvGgJENSZs zaYm7Gq=}RMQ<103f$g)w;49R?%zdr32k~`t#}6I0@|Q*<|s=3EBeggd%;8-{pQ!@^zD z4AOW|)vMvv2eM3!HCDy)?>_>PVBHo5H@+p9%0i60V^?LvId$U}y>A_)mRWs+Ej(T6 zIBe>$&f!w!_vXrE&>4KDcWH=XS(m zUOg#=$OYaB?M?{NOW3g2#2SoH3^F(`7RV9b%APZUn)B1|1NJ#aMniO7nlt{?6X0

    PPKrRzdSC=0lu(o-Eh9rZ8@rsSA!#o|A>~4)A0iOhq0$ ztqLqKBMH+~b(}u;9;5iAMfR*)U8PMl6T%3GXHri$WHuek8sSo53-TG1bFvn&m7%ed zXi*s~GOipN1SZ2H-%|H$e|D5D`YqP@^HE;9-(bp<$uhjAPuwBac?4 zZNa;U4y-%imW!P{G35R;66qg$ebjgxl@aI|%?3$7Ni}A-_H-^svMMab?Oc@Et9EBP z*)=^4Z}p*B)#GZKh|xJ1ANgV0Y4;7h94RnTI!r>oRH*wVFV*K*Gciug6vanFX_Rx- z6k@jLum-kXGG1E2xf6L6O7>dCMvX@dsF%wuT=HhlK|Y&g(8kL35?IDAw(pp?$nhDR-=@I}W4!lit^+Da*vIvu9oz5=dj0fJ{?()l$v*X^$#Nvh4`5#ks#@E{R26mLrL@Bt znR+h9cHDsN{X4iV4p0`Rv%_o6_!%hA_jB+QfrF5;IDWEb_~hgVPJ{Ajw>yLm{GYHJ z5~fOM*s?sXxXmIV2<-h{hSXfVI{HH>Fxa7-%Dq=a&Yj}@ zbpfZiqsui{P{h4lB@`t^_#>?7tvtSQQbI?FOv7V}3lJL-m{A{%ah8>?a(G)ZL49nb z&EN?<#9w*D{#*d}IBbI%Fm8PI7}aM~{$m+?fgpLr{J7xgidSW#X_UJV-xkm&k@))9 zyKKkA+yY(BQ5=_dF5rSSH;ID9#Vvd9f%tvY{xIdD!y)2JHWZtNR_`ZvY0*}>E@$N63rml+XlntW*qvG3%5+*w) z0q7L(@32@RVTD6NZ5lG()ThDKL}r|fQT#nAwZ2M?j%>UB0q(ENF}3HTvFd*#aqm2c zBpgEdxYYB}*?F^QYwuw1t%Y9UHG#8}0SvJBbyFtc3HfK=%2jlTln4O7m0FEtpx z-P_LS4dX2BES7$AdZzHdmH7X2c4;)Pt6t@Kq}tghCZ405$h1x51}Rd3M&ERv`cW0$ z4_=1Mw`igPS6A9KO9In_z8$HY7=$dVx!kKQZ~RS3HkWcwEqn9S`9Qjv8#ziLd%}8F zB$?bN)NCy7u>0H0uPz1h4#SS)wpuZn$u?pJU#@6X3j=1yYu?U*A@Su;EG{kf*0PzJ zUIUOe+WaBzF0zskz(g`pbcIQ*mXFoA2Fw2eg{o{7aI6kfly~eez;%XkwESKS)bwJ#ER~sCTXv%*2xA;mt2}D< z)WOCOzAR}TUVvoaTflu7yIk18+~h|a#QjZY7eZ(DcNiK4fpU6wh54irCop%657!p` z12sZNEftdNVxlv>&F#v~Q|KITTcX_(tj&ZfbL6D>814?VE}>@`S~OUye>v^4f6puG zXh}+Yi?e$*GD~gZv3w$+1i26HPZ`ts0}iyqd{-;XhKyF8E5|-* ztIZ~Vp1owQ#QQc5Pkm0B80$O$xWMHy{ou_>Xi;mk6f68MFW!9zX?gTpYB(xCB?-l3>4)y@{zP}fR4o( z0J5RDN1@YJN=nDgdR~rAqx87&`eIxsjS{FTh|iM@8hdA2l#))4ZtO+SHCUwHjit*S zzC0uHxG7^3xVVpjeWhb=T2o$b^^Ru)c<^i82P^A0b7tgEE_Wzm{f;qIKACw{k3QF^ zYC$|(;e{dOpZ#GBcH60Us*!Bo>Z=Zx)ur|x?a`S18HJH{QO>IMUuk)wPW*{=hd|DS zcVbx;M}3DI(>h6_dR&on8;G3jOW64SSG`roVdWtu@=8U);8GlVVg1#J-;Y|CRYu|J za~eP3#Hz5~qBTka=wcrfwxFhtkaALj-^wkQU?&h+a}>m4hT@Js3c@*ftH$&iT+^

    b76UH^84)8iAOIA!b;%bZ!&+zy%MZz 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

    +W@kUgVmDkBEF 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 Wke0rm>-ul6KhiVREr0O*?f(VPR39<` literal 219715 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.kUAkUUM2sx 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 Date: Tue, 17 Dec 2024 18:03:57 -0600 Subject: [PATCH 051/119] Reindex status api updates (#118803) --- .../ReindexDataStreamTransportActionIT.java | 9 +- .../GetMigrationReindexStatusAction.java | 26 ++- ...MigrationReindexStatusTransportAction.java | 115 +++++++++++- .../task/ReindexDataStreamEnrichedStatus.java | 104 +++++++++++ ...indexDataStreamPersistentTaskExecutor.java | 4 +- .../migrate/task/ReindexDataStreamStatus.java | 17 +- .../migrate/task/ReindexDataStreamTask.java | 19 +- ...ationReindexStatusActionResponseTests.java | 112 ++++-------- .../ReindexDataStreamEnrichedStatusTests.java | 165 ++++++++++++++++++ .../task/ReindexDataStreamStatusTests.java | 18 +- .../test/migrate/20_reindex_status.yml | 4 +- 11 files changed, 464 insertions(+), 129 deletions(-) create mode 100644 x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatus.java create mode 100644 x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatusTests.java diff --git a/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportActionIT.java b/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportActionIT.java index 6e24e644cb2af..b32a6efb854d7 100644 --- a/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportActionIT.java +++ b/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportActionIT.java @@ -29,7 +29,7 @@ import org.elasticsearch.xpack.migrate.MigratePlugin; import org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.ReindexDataStreamRequest; import org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.ReindexDataStreamResponse; -import org.elasticsearch.xpack.migrate.task.ReindexDataStreamStatus; +import org.elasticsearch.xpack.migrate.task.ReindexDataStreamEnrichedStatus; import org.elasticsearch.xpack.migrate.task.ReindexDataStreamTask; import java.util.Collection; @@ -37,6 +37,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; @@ -100,7 +101,7 @@ public void testAlreadyUpToDateDataStream() throws Exception { assertThat(task.getStatus().complete(), equalTo(true)); assertNull(task.getStatus().exception()); assertThat(task.getStatus().pending(), equalTo(0)); - assertThat(task.getStatus().inProgress(), equalTo(0)); + assertThat(task.getStatus().inProgress(), equalTo(Set.of())); assertThat(task.getStatus().errors().size(), equalTo(0)); assertBusy(() -> { @@ -108,12 +109,12 @@ public void testAlreadyUpToDateDataStream() throws Exception { new ActionType(GetMigrationReindexStatusAction.NAME), new GetMigrationReindexStatusAction.Request(dataStreamName) ).actionGet(); - ReindexDataStreamStatus status = (ReindexDataStreamStatus) statusResponse.getTask().getTask().status(); + ReindexDataStreamEnrichedStatus status = statusResponse.getEnrichedStatus(); assertThat(status.complete(), equalTo(true)); assertThat(status.errors(), equalTo(List.of())); assertThat(status.exception(), equalTo(null)); assertThat(status.pending(), equalTo(0)); - assertThat(status.inProgress(), equalTo(0)); + assertThat(status.inProgress().size(), equalTo(0)); assertThat(status.totalIndices(), equalTo(backingIndexCount)); assertThat(status.totalIndicesToBeUpgraded(), equalTo(0)); }); diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusAction.java index 68ccaef4bf02c..bc084f3e0b5d6 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusAction.java @@ -16,10 +16,9 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.tasks.Task; -import org.elasticsearch.tasks.TaskResult; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xpack.migrate.task.ReindexDataStreamEnrichedStatus; import java.io.IOException; import java.util.Objects; @@ -36,46 +35,43 @@ public GetMigrationReindexStatusAction() { } public static class Response extends ActionResponse implements ToXContentObject { - private final TaskResult task; + private final ReindexDataStreamEnrichedStatus enrichedStatus; - public Response(TaskResult task) { - this.task = requireNonNull(task, "task is required"); + public Response(ReindexDataStreamEnrichedStatus enrichedStatus) { + this.enrichedStatus = requireNonNull(enrichedStatus, "status is required"); } public Response(StreamInput in) throws IOException { super(in); - task = in.readOptionalWriteable(TaskResult::new); + enrichedStatus = in.readOptionalWriteable(ReindexDataStreamEnrichedStatus::new); } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeOptionalWriteable(task); + out.writeOptionalWriteable(enrichedStatus); } /** * Get the actual result of the fetch. */ - public TaskResult getTask() { - return task; + public ReindexDataStreamEnrichedStatus getEnrichedStatus() { + return enrichedStatus; } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - Task.Status status = task.getTask().status(); - if (status != null) { - task.getTask().status().toXContent(builder, params); - } + enrichedStatus.toXContent(builder, params); return builder; } @Override public int hashCode() { - return Objects.hashCode(task); + return Objects.hashCode(enrichedStatus); } @Override public boolean equals(Object other) { - return other instanceof Response && task.equals(((Response) other).task); + return other instanceof Response && enrichedStatus.equals(((Response) other).enrichedStatus); } @Override diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusTransportAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusTransportAction.java index ca81a03fc5630..64864491191e5 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusTransportAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusTransportAction.java @@ -11,40 +11,55 @@ import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionListenerResponseHandler; +import org.elasticsearch.action.admin.indices.stats.IndexStats; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.Strings; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.index.shard.DocsStats; import org.elasticsearch.injection.guice.Inject; import org.elasticsearch.persistent.AllocatedPersistentTask; import org.elasticsearch.persistent.PersistentTasksCustomMetadata; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; import org.elasticsearch.tasks.TaskInfo; -import org.elasticsearch.tasks.TaskResult; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.migrate.action.GetMigrationReindexStatusAction.Request; import org.elasticsearch.xpack.migrate.action.GetMigrationReindexStatusAction.Response; +import org.elasticsearch.xpack.migrate.task.ReindexDataStreamEnrichedStatus; +import org.elasticsearch.xpack.migrate.task.ReindexDataStreamStatus; +import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; public class GetMigrationReindexStatusTransportAction extends HandledTransportAction { private final ClusterService clusterService; private final TransportService transportService; + private final Client client; @Inject public GetMigrationReindexStatusTransportAction( ClusterService clusterService, TransportService transportService, - ActionFilters actionFilters + ActionFilters actionFilters, + Client client ) { super(GetMigrationReindexStatusAction.NAME, transportService, actionFilters, Request::new, EsExecutors.DIRECT_EXECUTOR_SERVICE); this.clusterService = clusterService; this.transportService = transportService; + this.client = client; } @Override @@ -60,9 +75,9 @@ protected void doExecute(Task task, Request request, ActionListener li } else if (persistentTask.isAssigned()) { String nodeId = persistentTask.getExecutorNode(); if (clusterService.localNode().getId().equals(nodeId)) { - getRunningTaskFromNode(persistentTaskId, listener); + fetchAndReportStatusForTaskOnThisNode(persistentTaskId, listener); } else { - runOnNodeWithTaskIfPossible(task, request, nodeId, listener); + fetchAndReportStatusForTaskOnRemoteNode(task, request, nodeId, listener); } } else { listener.onFailure(new ElasticsearchException("Persistent task with id [{}] is not assigned to a node", persistentTaskId)); @@ -82,7 +97,7 @@ private Task getRunningPersistentTaskFromTaskManager(String persistentTaskId) { return optionalTask.map(Map.Entry::getValue).orElse(null); } - void getRunningTaskFromNode(String persistentTaskId, ActionListener listener) { + void fetchAndReportStatusForTaskOnThisNode(String persistentTaskId, ActionListener listener) { Task runningTask = getRunningPersistentTaskFromTaskManager(persistentTaskId); if (runningTask == null) { listener.onFailure( @@ -96,11 +111,97 @@ void getRunningTaskFromNode(String persistentTaskId, ActionListener li ); } else { TaskInfo info = runningTask.taskInfo(clusterService.localNode().getId(), true); - listener.onResponse(new Response(new TaskResult(false, info))); + ReindexDataStreamStatus status = (ReindexDataStreamStatus) info.status(); + Set inProgressIndices = status.inProgress(); + if (inProgressIndices.isEmpty()) { + // We have no reason to fetch index stats since there are no in progress indices + reportStatus(Map.of(), status, listener); + } else { + fetchInProgressStatsAndReportStatus(inProgressIndices, status, listener); + } } } - private void runOnNodeWithTaskIfPossible(Task thisTask, Request request, String nodeId, ActionListener listener) { + /* + * The status is enriched with the information from inProgressMap to create a new ReindexDataStreamEnrichedStatus, which is used in the + * response sent to the listener. + */ + private void reportStatus( + Map> inProgressMap, + ReindexDataStreamStatus status, + ActionListener listener + ) { + ReindexDataStreamEnrichedStatus enrichedStatus = new ReindexDataStreamEnrichedStatus( + status.persistentTaskStartTime(), + status.totalIndices(), + status.totalIndicesToBeUpgraded(), + status.complete(), + status.exception(), + inProgressMap, + status.pending(), + status.errors() + ); + listener.onResponse(new Response(enrichedStatus)); + } + + /* + * This method feches doc counts for all indices in inProgressIndices (and the indices they are being reindexed into). After + * successfully fetching those, reportStatus is called. + */ + private void fetchInProgressStatsAndReportStatus( + Set inProgressIndices, + ReindexDataStreamStatus status, + ActionListener listener + ) { + IndicesStatsRequest indicesStatsRequest = new IndicesStatsRequest(); + String[] indices = inProgressIndices.stream() + .flatMap(index -> Stream.of(index, ReindexDataStreamIndexTransportAction.generateDestIndexName(index))) + .toList() + .toArray(new String[0]); + indicesStatsRequest.indices(indices); + /* + * It is possible that the destination index will not exist yet, so we want to ignore the fact that it is missing + */ + indicesStatsRequest.indicesOptions(IndicesOptions.fromOptions(true, true, true, true)); + client.execute(IndicesStatsAction.INSTANCE, indicesStatsRequest, new ActionListener() { + @Override + public void onResponse(IndicesStatsResponse indicesStatsResponse) { + Map> inProgressMap = new HashMap<>(); + for (String index : inProgressIndices) { + IndexStats sourceIndexStats = indicesStatsResponse.getIndex(index); + final long totalDocsInIndex; + if (sourceIndexStats == null) { + totalDocsInIndex = 0; + } else { + DocsStats totalDocsStats = sourceIndexStats.getTotal().getDocs(); + totalDocsInIndex = totalDocsStats == null ? 0 : totalDocsStats.getCount(); + } + IndexStats migratedIndexStats = indicesStatsResponse.getIndex( + ReindexDataStreamIndexTransportAction.generateDestIndexName(index) + ); + final long reindexedDocsInIndex; + if (migratedIndexStats == null) { + reindexedDocsInIndex = 0; + } else { + DocsStats reindexedDocsStats = migratedIndexStats.getTotal().getDocs(); + reindexedDocsInIndex = reindexedDocsStats == null ? 0 : reindexedDocsStats.getCount(); + } + inProgressMap.put(index, Tuple.tuple(totalDocsInIndex, reindexedDocsInIndex)); + } + reportStatus(inProgressMap, status, listener); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } + + /* + * The task and its status exist on some other node, so this method forwards the request to that node. + */ + private void fetchAndReportStatusForTaskOnRemoteNode(Task thisTask, Request request, String nodeId, ActionListener listener) { DiscoveryNode node = clusterService.state().nodes().get(nodeId); if (node == null) { listener.onFailure( diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatus.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatus.java new file mode 100644 index 0000000000000..9dbe1f0c8eebc --- /dev/null +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatus.java @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.migrate.task; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/* + * This class represents information similar to that in ReindexDataStreamStatus, but enriched from other sources besides just the task + * itself. + */ +public record ReindexDataStreamEnrichedStatus( + long persistentTaskStartTime, + int totalIndices, + int totalIndicesToBeUpgraded, + boolean complete, + Exception exception, + Map> inProgress, + int pending, + List> errors +) implements ToXContentObject, Writeable { + public ReindexDataStreamEnrichedStatus { + Objects.requireNonNull(inProgress); + Objects.requireNonNull(errors); + } + + public ReindexDataStreamEnrichedStatus(StreamInput in) throws IOException { + this( + in.readLong(), + in.readInt(), + in.readInt(), + in.readBoolean(), + in.readException(), + in.readMap(StreamInput::readString, in2 -> Tuple.tuple(in2.readLong(), in2.readLong())), + in.readInt(), + in.readCollectionAsList(in1 -> Tuple.tuple(in1.readString(), in1.readException())) + ); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeLong(persistentTaskStartTime); + out.writeInt(totalIndices); + out.writeInt(totalIndicesToBeUpgraded); + out.writeBoolean(complete); + out.writeException(exception); + out.writeMap(inProgress, StreamOutput::writeString, (out2, tuple) -> { + out2.writeLong(tuple.v1()); + out2.writeLong(tuple.v2()); + }); + out.writeInt(pending); + out.writeCollection(errors, (out1, tuple) -> { + out1.writeString(tuple.v1()); + out1.writeException(tuple.v2()); + }); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.timestampFieldsFromUnixEpochMillis("start_time_millis", "start_time", persistentTaskStartTime); + builder.field("complete", complete); + builder.field("total_indices_in_data_stream", totalIndices); + builder.field("total_indices_requiring_upgrade", totalIndicesToBeUpgraded); + builder.field("successes", totalIndicesToBeUpgraded - (inProgress.size() + pending + errors.size())); + builder.startArray("in_progress"); + for (Map.Entry> inProgressEntry : inProgress.entrySet()) { + builder.startObject(); + builder.field("index", inProgressEntry.getKey()); + builder.field("total_doc_count", inProgressEntry.getValue().v1()); + builder.field("reindexed_doc_count", inProgressEntry.getValue().v2()); + builder.endObject(); + } + builder.endArray(); + builder.field("pending", pending); + builder.startArray("errors"); + for (Tuple error : errors) { + builder.startObject(); + builder.field("index", error.v1()); + builder.field("message", error.v2() == null ? "unknown" : error.v2().getMessage()); + builder.endObject(); + } + builder.endArray(); + if (exception != null) { + builder.field("exception", exception.getMessage()); + } + builder.endObject(); + return builder; + } +} diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java index 176220a1ccae8..494be303980a7 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java @@ -78,9 +78,9 @@ protected void nodeOperation(AllocatedPersistentTask task, ReindexDataStreamTask .toList(); reindexDataStreamTask.setPendingIndicesCount(indicesToBeReindexed.size()); for (Index index : indicesToBeReindexed) { - reindexDataStreamTask.incrementInProgressIndicesCount(); + reindexDataStreamTask.incrementInProgressIndicesCount(index.getName()); // TODO This is just a placeholder. This is where the real data stream reindex logic will go - reindexDataStreamTask.reindexSucceeded(); + reindexDataStreamTask.reindexSucceeded(index.getName()); } completeSuccessfulPersistentTask(reindexDataStreamTask); diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatus.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatus.java index 358062550b50a..632aea076ea5a 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatus.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatus.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.util.List; import java.util.Objects; +import java.util.Set; public record ReindexDataStreamStatus( long persistentTaskStartTime, @@ -23,11 +24,12 @@ public record ReindexDataStreamStatus( int totalIndicesToBeUpgraded, boolean complete, Exception exception, - int inProgress, + Set inProgress, int pending, List> errors ) implements Task.Status { public ReindexDataStreamStatus { + Objects.requireNonNull(inProgress); Objects.requireNonNull(errors); } @@ -40,7 +42,7 @@ public ReindexDataStreamStatus(StreamInput in) throws IOException { in.readInt(), in.readBoolean(), in.readException(), - in.readInt(), + in.readCollectionAsSet((Reader) StreamInput::readString), in.readInt(), in.readCollectionAsList(in1 -> Tuple.tuple(in1.readString(), in1.readException())) ); @@ -58,7 +60,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeInt(totalIndicesToBeUpgraded); out.writeBoolean(complete); out.writeException(exception); - out.writeInt(inProgress); + out.writeStringCollection(inProgress); out.writeInt(pending); out.writeCollection(errors, (out1, tuple) -> { out1.writeString(tuple.v1()); @@ -69,12 +71,13 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field("start_time", persistentTaskStartTime); + builder.timestampFieldsFromUnixEpochMillis("start_time_millis", "start_time", persistentTaskStartTime); builder.field("complete", complete); - builder.field("total_indices", totalIndices); + builder.field("total_indices_in_data_stream", totalIndices); builder.field("total_indices_requiring_upgrade", totalIndicesToBeUpgraded); - builder.field("successes", totalIndicesToBeUpgraded - (inProgress + pending + errors.size())); - builder.field("in_progress", inProgress); + final int inProgressSize = inProgress.size(); + builder.field("successes", totalIndicesToBeUpgraded - (inProgressSize + pending + errors.size())); + builder.field("in_progress", inProgressSize); builder.field("pending", pending); builder.startArray("errors"); for (Tuple error : errors) { diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamTask.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamTask.java index 844f24f45ab77..7a2b759dfd17a 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamTask.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamTask.java @@ -15,8 +15,11 @@ import org.elasticsearch.threadpool.ThreadPool; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; public class ReindexDataStreamTask extends AllocatedPersistentTask { @@ -26,9 +29,9 @@ public class ReindexDataStreamTask extends AllocatedPersistentTask { private final int totalIndicesToBeUpgraded; private volatile boolean complete = false; private volatile Exception exception; - private final AtomicInteger inProgress = new AtomicInteger(0); + private final Set inProgress = Collections.synchronizedSet(new HashSet<>()); private final AtomicInteger pending = new AtomicInteger(); - private final List> errors = new ArrayList<>(); + private final List> errors = Collections.synchronizedList(new ArrayList<>()); private final RunOnce completeTask; @SuppressWarnings("this-escape") @@ -64,7 +67,7 @@ public ReindexDataStreamStatus getStatus() { totalIndicesToBeUpgraded, complete, exception, - inProgress.get(), + inProgress, pending.get(), errors ); @@ -84,17 +87,17 @@ public void taskFailed(ThreadPool threadPool, TimeValue timeToLive, Exception e) allReindexesCompleted(threadPool, timeToLive); } - public void reindexSucceeded() { - inProgress.decrementAndGet(); + public void reindexSucceeded(String index) { + inProgress.remove(index); } public void reindexFailed(String index, Exception error) { this.errors.add(Tuple.tuple(index, error)); - inProgress.decrementAndGet(); + inProgress.remove(index); } - public void incrementInProgressIndicesCount() { - inProgress.incrementAndGet(); + public void incrementInProgressIndicesCount(String index) { + inProgress.add(index); pending.decrementAndGet(); } diff --git a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusActionResponseTests.java b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusActionResponseTests.java index a18030edbf42c..1361f30840c87 100644 --- a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusActionResponseTests.java +++ b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusActionResponseTests.java @@ -7,25 +7,17 @@ package org.elasticsearch.xpack.migrate.action; -import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.network.NetworkModule; -import org.elasticsearch.tasks.RawTaskStatus; -import org.elasticsearch.tasks.Task; -import org.elasticsearch.tasks.TaskId; -import org.elasticsearch.tasks.TaskInfo; -import org.elasticsearch.tasks.TaskResult; +import org.elasticsearch.core.Tuple; import org.elasticsearch.test.AbstractWireSerializingTestCase; -import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.migrate.action.GetMigrationReindexStatusAction.Response; +import org.elasticsearch.xpack.migrate.task.ReindexDataStreamEnrichedStatus; import java.io.IOException; -import java.util.Collections; +import java.util.List; import java.util.Map; -import java.util.TreeMap; public class GetMigrationReindexStatusActionResponseTests extends AbstractWireSerializingTestCase { @Override @@ -35,11 +27,7 @@ protected Writeable.Reader instanceReader() { @Override protected Response createTestInstance() { - try { - return new Response(randomTaskResult()); - } catch (IOException e) { - throw new RuntimeException(e); - } + return new Response(getRandomStatus()); } @Override @@ -47,76 +35,44 @@ protected Response mutateInstance(Response instance) throws IOException { return createTestInstance(); // There's only one field } - private static TaskResult randomTaskResult() throws IOException { - return switch (between(0, 2)) { - case 0 -> new TaskResult(randomBoolean(), randomTaskInfo()); - case 1 -> new TaskResult(randomTaskInfo(), new RuntimeException("error")); - case 2 -> new TaskResult(randomTaskInfo(), randomTaskResponse()); - default -> throw new UnsupportedOperationException("Unsupported random TaskResult constructor"); - }; - } - - static TaskInfo randomTaskInfo() { - String nodeId = randomAlphaOfLength(5); - TaskId taskId = randomTaskId(nodeId); - String type = randomAlphaOfLength(5); - String action = randomAlphaOfLength(5); - Task.Status status = randomBoolean() ? randomRawTaskStatus() : null; - String description = randomBoolean() ? randomAlphaOfLength(5) : null; - long startTime = randomLong(); - long runningTimeNanos = randomNonNegativeLong(); - boolean cancellable = randomBoolean(); - boolean cancelled = cancellable && randomBoolean(); - TaskId parentTaskId = randomBoolean() ? TaskId.EMPTY_TASK_ID : randomTaskId(randomAlphaOfLength(5)); - Map headers = randomBoolean() - ? Collections.emptyMap() - : Collections.singletonMap(randomAlphaOfLength(5), randomAlphaOfLength(5)); - return new TaskInfo( - taskId, - type, - nodeId, - action, - description, - status, - startTime, - runningTimeNanos, - cancellable, - cancelled, - parentTaskId, - headers + private ReindexDataStreamEnrichedStatus getRandomStatus() { + return new ReindexDataStreamEnrichedStatus( + randomLong(), + randomNegativeInt(), + randomNegativeInt(), + randomBoolean(), + nullableTestException(), + randomInProgressMap(), + randomNegativeInt(), + randomErrorList() ); } - private static TaskId randomTaskId(String nodeId) { - return new TaskId(nodeId, randomLong()); + private Map> randomInProgressMap() { + return randomMap(1, 50, () -> Tuple.tuple(randomAlphaOfLength(50), Tuple.tuple(randomNonNegativeLong(), randomNonNegativeLong()))); } - private static RawTaskStatus randomRawTaskStatus() { - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.startObject(); - int fields = between(0, 10); - for (int f = 0; f < fields; f++) { - builder.field(randomAlphaOfLength(5), randomAlphaOfLength(5)); - } - builder.endObject(); - return new RawTaskStatus(BytesReference.bytes(builder)); - } catch (IOException e) { - throw new IllegalStateException(e); + private Exception nullableTestException() { + if (randomBoolean()) { + return testException(); } + return null; } - private static ToXContent randomTaskResponse() { - Map result = new TreeMap<>(); - int fields = between(0, 10); - for (int f = 0; f < fields; f++) { - result.put(randomAlphaOfLength(5), randomAlphaOfLength(5)); - } - return (builder, params) -> { - for (Map.Entry entry : result.entrySet()) { - builder.field(entry.getKey(), entry.getValue()); - } - return builder; - }; + private Exception testException() { + /* + * Unfortunately ElasticsearchException doesn't have an equals and just falls back to Object::equals. So we can't test for equality + * when we're using an exception. So always just use null. + */ + return null; + } + + private List> randomErrorList() { + return randomErrorList(0); + } + + private List> randomErrorList(int minSize) { + return randomList(minSize, Math.max(minSize, 100), () -> Tuple.tuple(randomAlphaOfLength(30), testException())); } @Override diff --git a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatusTests.java b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatusTests.java new file mode 100644 index 0000000000000..acd8cd1a6add2 --- /dev/null +++ b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatusTests.java @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.migrate.task; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.json.JsonXContent; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.Map.entry; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; +import static org.hamcrest.Matchers.equalTo; + +public class ReindexDataStreamEnrichedStatusTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return ReindexDataStreamEnrichedStatus::new; + } + + @Override + protected ReindexDataStreamEnrichedStatus createTestInstance() { + return new ReindexDataStreamEnrichedStatus( + randomLong(), + randomNegativeInt(), + randomNegativeInt(), + randomBoolean(), + nullableTestException(), + randomInProgressMap(), + randomNegativeInt(), + randomErrorList() + ); + } + + private Map> randomInProgressMap() { + return randomMap(1, 50, () -> Tuple.tuple(randomAlphaOfLength(50), Tuple.tuple(randomNonNegativeLong(), randomNonNegativeLong()))); + } + + private Exception nullableTestException() { + if (randomBoolean()) { + return testException(); + } + return null; + } + + private Exception testException() { + /* + * Unfortunately ElasticsearchException doesn't have an equals and just falls back to Object::equals. So we can't test for equality + * when we're using an exception. So always just use null. + */ + return null; + } + + private List randomList() { + return randomList(0); + } + + private List randomList(int minSize) { + return randomList(minSize, Math.max(minSize, 100), () -> randomAlphaOfLength(50)); + } + + private Set randomSet(int minSize) { + return randomSet(minSize, 100, () -> randomAlphaOfLength(50)); + } + + private List> randomErrorList() { + return randomErrorList(0); + } + + private List> randomErrorList(int minSize) { + return randomList(minSize, Math.max(minSize, 100), () -> Tuple.tuple(randomAlphaOfLength(30), testException())); + } + + @Override + protected ReindexDataStreamEnrichedStatus mutateInstance(ReindexDataStreamEnrichedStatus instance) throws IOException { + long startTime = instance.persistentTaskStartTime(); + int totalIndices = instance.totalIndices(); + int totalIndicesToBeUpgraded = instance.totalIndicesToBeUpgraded(); + boolean complete = instance.complete(); + Exception exception = instance.exception(); + Map> inProgress = instance.inProgress(); + int pending = instance.pending(); + List> errors = instance.errors(); + switch (randomIntBetween(0, 6)) { + case 0 -> startTime = randomLong(); + case 1 -> totalIndices = totalIndices + 1; + case 2 -> totalIndicesToBeUpgraded = totalIndicesToBeUpgraded + 1; + case 3 -> complete = complete == false; + case 4 -> inProgress = randomInProgressMap(); + case 5 -> pending = pending + 1; + case 6 -> errors = randomErrorList(errors.size() + 1); + default -> throw new UnsupportedOperationException(); + } + return new ReindexDataStreamEnrichedStatus( + startTime, + totalIndices, + totalIndicesToBeUpgraded, + complete, + exception, + inProgress, + pending, + errors + ); + } + + public void testToXContent() throws IOException { + ReindexDataStreamEnrichedStatus status = new ReindexDataStreamEnrichedStatus( + 1234L, + 200, + 100, + true, + new ElasticsearchException("the whole task failed"), + Map.of("index-1", Tuple.tuple(10L, 8L)), + 8, + List.of( + Tuple.tuple("index7", new ElasticsearchException("index7 failed")), + Tuple.tuple("index8", new ElasticsearchException("index8 " + "failed")) + ) + ); + try (XContentBuilder builder = XContentBuilder.builder(JsonXContent.jsonXContent)) { + builder.humanReadable(true); + status.toXContent(builder, EMPTY_PARAMS); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(builder))) { + Map parserMap = parser.map(); + assertThat( + parserMap, + equalTo( + Map.ofEntries( + entry("start_time", "1970-01-01T00:00:01.234Z"), + entry("start_time_millis", 1234), + entry("total_indices_in_data_stream", 200), + entry("total_indices_requiring_upgrade", 100), + entry("complete", true), + entry("exception", "the whole task failed"), + entry("successes", 89), + entry("in_progress", List.of(Map.of("index", "index-1", "total_doc_count", 10, "reindexed_doc_count", 8))), + entry("pending", 8), + entry( + "errors", + List.of( + Map.of("index", "index7", "message", "index7 failed"), + Map.of("index", "index8", "message", "index8 failed") + ) + ) + ) + ) + ); + } + } + } +} diff --git a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatusTests.java b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatusTests.java index d81e9d35cd490..47e2d02bee3b0 100644 --- a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatusTests.java +++ b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatusTests.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Set; import static java.util.Map.entry; import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; @@ -39,7 +40,7 @@ protected ReindexDataStreamStatus createTestInstance() { randomNegativeInt(), randomBoolean(), nullableTestException(), - randomNegativeInt(), + randomSet(0), randomNegativeInt(), randomErrorList() ); @@ -68,6 +69,10 @@ private List randomList(int minSize) { return randomList(minSize, Math.max(minSize, 100), () -> randomAlphaOfLength(50)); } + private Set randomSet(int minSize) { + return randomSet(minSize, 100, () -> randomAlphaOfLength(50)); + } + private List> randomErrorList() { return randomErrorList(0); } @@ -83,7 +88,7 @@ protected ReindexDataStreamStatus mutateInstance(ReindexDataStreamStatus instanc int totalIndicesToBeUpgraded = instance.totalIndicesToBeUpgraded(); boolean complete = instance.complete(); Exception exception = instance.exception(); - int inProgress = instance.inProgress(); + Set inProgress = instance.inProgress(); int pending = instance.pending(); List> errors = instance.errors(); switch (randomIntBetween(0, 6)) { @@ -91,7 +96,7 @@ protected ReindexDataStreamStatus mutateInstance(ReindexDataStreamStatus instanc case 1 -> totalIndices = totalIndices + 1; case 2 -> totalIndicesToBeUpgraded = totalIndicesToBeUpgraded + 1; case 3 -> complete = complete == false; - case 4 -> inProgress = inProgress + 1; + case 4 -> inProgress = randomSet(inProgress.size() + 1); case 5 -> pending = pending + 1; case 6 -> errors = randomErrorList(errors.size() + 1); default -> throw new UnsupportedOperationException(); @@ -115,7 +120,7 @@ public void testToXContent() throws IOException { 100, true, new ElasticsearchException("the whole task failed"), - 12, + randomSet(12, 12, () -> randomAlphaOfLength(50)), 8, List.of( Tuple.tuple("index7", new ElasticsearchException("index7 failed")), @@ -131,8 +136,9 @@ public void testToXContent() throws IOException { parserMap, equalTo( Map.ofEntries( - entry("start_time", 1234), - entry("total_indices", 200), + entry("start_time", "1970-01-01T00:00:01.234Z"), + entry("start_time_millis", 1234), + entry("total_indices_in_data_stream", 200), entry("total_indices_requiring_upgrade", 100), entry("complete", true), entry("exception", "the whole task failed"), diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/20_reindex_status.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/20_reindex_status.yml index c94ce8dd211ae..80ca95d631491 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/20_reindex_status.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/20_reindex_status.yml @@ -61,10 +61,10 @@ setup: migrate.get_reindex_status: index: "my-data-stream" - match: { complete: true } - - match: { total_indices: 1 } + - match: { total_indices_in_data_stream: 1 } - match: { total_indices_requiring_upgrade: 0 } - match: { successes: 0 } - - match: { in_progress: 0 } + - match: { in_progress: [] } - match: { pending: 0 } - match: { errors: [] } From 15bec3cefa48c958dada0fba42f452fd278c1ab6 Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Tue, 17 Dec 2024 19:06:54 -0500 Subject: [PATCH 052/119] Add support for sparse_vector queries against semantic_text fields (#118617) --- docs/changelog/118617.yaml | 5 + .../ml/search/SparseVectorQueryBuilder.java | 38 ++- .../search/SparseVectorQueryBuilderTests.java | 6 +- .../xpack/inference/InferenceFeatures.java | 7 +- .../xpack/inference/InferencePlugin.java | 3 +- .../SemanticMatchQueryRewriteInterceptor.java | 88 ++----- .../queries/SemanticQueryBuilder.java | 8 + .../SemanticQueryRewriteInterceptor.java | 148 +++++++++++ ...icSparseVectorQueryRewriteInterceptor.java | 124 +++++++++ ...nticMatchQueryRewriteInterceptorTests.java | 110 ++++++++ ...rseVectorQueryRewriteInterceptorTests.java | 137 ++++++++++ .../46_semantic_text_sparse_vector.yml | 249 ++++++++++++++++++ .../test/ml/sparse_vector_search.yml | 40 ++- 13 files changed, 887 insertions(+), 76 deletions(-) create mode 100644 docs/changelog/118617.yaml create mode 100644 x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryRewriteInterceptor.java create mode 100644 x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticSparseVectorQueryRewriteInterceptor.java create mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/index/query/SemanticMatchQueryRewriteInterceptorTests.java create mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/index/query/SemanticSparseVectorQueryRewriteInterceptorTests.java create mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/46_semantic_text_sparse_vector.yml diff --git a/docs/changelog/118617.yaml b/docs/changelog/118617.yaml new file mode 100644 index 0000000000000..a8793a114e913 --- /dev/null +++ b/docs/changelog/118617.yaml @@ -0,0 +1,5 @@ +pr: 118617 +summary: Add support for `sparse_vector` queries against `semantic_text` fields +area: "Search" +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/search/SparseVectorQueryBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/search/SparseVectorQueryBuilder.java index e9e4e90421adc..35cba890e5e0c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/search/SparseVectorQueryBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/search/SparseVectorQueryBuilder.java @@ -90,7 +90,8 @@ public SparseVectorQueryBuilder( : (this.shouldPruneTokens ? new TokenPruningConfig() : null)); this.weightedTokensSupplier = null; - if (queryVectors == null ^ inferenceId == null == false) { + // Preserve BWC error messaging + if (queryVectors != null && inferenceId != null) { throw new IllegalArgumentException( "[" + NAME @@ -98,18 +99,24 @@ public SparseVectorQueryBuilder( + QUERY_VECTOR_FIELD.getPreferredName() + "] or [" + INFERENCE_ID_FIELD.getPreferredName() - + "]" + + "] for " + + ALLOWED_FIELD_TYPE + + " fields" ); } - if (inferenceId != null && query == null) { + + // Preserve BWC error messaging + if ((queryVectors == null) == (query == null)) { throw new IllegalArgumentException( "[" + NAME - + "] requires [" - + QUERY_FIELD.getPreferredName() - + "] when [" + + "] requires one of [" + + QUERY_VECTOR_FIELD.getPreferredName() + + "] or [" + INFERENCE_ID_FIELD.getPreferredName() - + "] is specified" + + "] for " + + ALLOWED_FIELD_TYPE + + " fields" ); } } @@ -143,6 +150,14 @@ public List getQueryVectors() { return queryVectors; } + public String getInferenceId() { + return inferenceId; + } + + public String getQuery() { + return query; + } + public boolean shouldPruneTokens() { return shouldPruneTokens; } @@ -176,7 +191,9 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep } builder.endObject(); } else { - builder.field(INFERENCE_ID_FIELD.getPreferredName(), inferenceId); + if (inferenceId != null) { + builder.field(INFERENCE_ID_FIELD.getPreferredName(), inferenceId); + } builder.field(QUERY_FIELD.getPreferredName(), query); } builder.field(PRUNE_FIELD.getPreferredName(), shouldPruneTokens); @@ -228,6 +245,11 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) { shouldPruneTokens, tokenPruningConfig ); + } else if (inferenceId == null) { + // Edge case, where inference_id was not specified in the request, + // but we did not intercept this and rewrite to a query o field with + // pre-configured inference. So we trap here and output a nicer error message. + throw new IllegalArgumentException("inference_id required to perform vector search on query string"); } // TODO move this to xpack core and use inference APIs diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/search/SparseVectorQueryBuilderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/search/SparseVectorQueryBuilderTests.java index a5c1ba45d90b7..af557ed6b7f82 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/search/SparseVectorQueryBuilderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/search/SparseVectorQueryBuilderTests.java @@ -260,16 +260,16 @@ public void testIllegalValues() { { IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> new SparseVectorQueryBuilder("field name", null, "model id") + () -> new SparseVectorQueryBuilder("field name", null, null) ); - assertEquals("[sparse_vector] requires one of [query_vector] or [inference_id]", e.getMessage()); + assertEquals("[sparse_vector] requires one of [query_vector] or [inference_id] for sparse_vector fields", e.getMessage()); } { IllegalArgumentException e = expectThrows( IllegalArgumentException.class, () -> new SparseVectorQueryBuilder("field name", "model text", null) ); - assertEquals("[sparse_vector] requires [query] when [inference_id] is specified", e.getMessage()); + assertEquals("[sparse_vector] requires one of [query_vector] or [inference_id] for sparse_vector fields", e.getMessage()); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java index 3b7613b8b0e1f..876ff01812064 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java @@ -10,13 +10,15 @@ import org.elasticsearch.features.FeatureSpecification; import org.elasticsearch.features.NodeFeature; import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper; -import org.elasticsearch.xpack.inference.queries.SemanticMatchQueryRewriteInterceptor; import org.elasticsearch.xpack.inference.queries.SemanticQueryBuilder; import org.elasticsearch.xpack.inference.rank.random.RandomRankRetrieverBuilder; import org.elasticsearch.xpack.inference.rank.textsimilarity.TextSimilarityRankRetrieverBuilder; import java.util.Set; +import static org.elasticsearch.xpack.inference.queries.SemanticMatchQueryRewriteInterceptor.SEMANTIC_MATCH_QUERY_REWRITE_INTERCEPTION_SUPPORTED; +import static org.elasticsearch.xpack.inference.queries.SemanticSparseVectorQueryRewriteInterceptor.SEMANTIC_SPARSE_VECTOR_QUERY_REWRITE_INTERCEPTION_SUPPORTED; + /** * Provides inference features. */ @@ -45,7 +47,8 @@ public Set getTestFeatures() { SemanticTextFieldMapper.SEMANTIC_TEXT_ZERO_SIZE_FIX, SemanticTextFieldMapper.SEMANTIC_TEXT_ALWAYS_EMIT_INFERENCE_ID_FIX, SEMANTIC_TEXT_HIGHLIGHTER, - SemanticMatchQueryRewriteInterceptor.SEMANTIC_MATCH_QUERY_REWRITE_INTERCEPTION_SUPPORTED + SEMANTIC_MATCH_QUERY_REWRITE_INTERCEPTION_SUPPORTED, + SEMANTIC_SPARSE_VECTOR_QUERY_REWRITE_INTERCEPTION_SUPPORTED ); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java index 93743a5485c2c..169c8f87043e8 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java @@ -80,6 +80,7 @@ import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper; import org.elasticsearch.xpack.inference.queries.SemanticMatchQueryRewriteInterceptor; import org.elasticsearch.xpack.inference.queries.SemanticQueryBuilder; +import org.elasticsearch.xpack.inference.queries.SemanticSparseVectorQueryRewriteInterceptor; import org.elasticsearch.xpack.inference.rank.random.RandomRankBuilder; import org.elasticsearch.xpack.inference.rank.random.RandomRankRetrieverBuilder; import org.elasticsearch.xpack.inference.rank.textsimilarity.TextSimilarityRankBuilder; @@ -440,7 +441,7 @@ public List> getQueries() { @Override public List getQueryRewriteInterceptors() { - return List.of(new SemanticMatchQueryRewriteInterceptor()); + return List.of(new SemanticMatchQueryRewriteInterceptor(), new SemanticSparseVectorQueryRewriteInterceptor()); } @Override diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticMatchQueryRewriteInterceptor.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticMatchQueryRewriteInterceptor.java index a4a8123935c3e..fd1d65d00faf5 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticMatchQueryRewriteInterceptor.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticMatchQueryRewriteInterceptor.java @@ -7,24 +7,12 @@ package org.elasticsearch.xpack.inference.queries; -import org.elasticsearch.action.ResolvedIndices; -import org.elasticsearch.cluster.metadata.IndexMetadata; -import org.elasticsearch.cluster.metadata.InferenceFieldMetadata; import org.elasticsearch.features.NodeFeature; -import org.elasticsearch.index.mapper.IndexFieldMapper; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryRewriteContext; -import org.elasticsearch.index.query.TermQueryBuilder; -import org.elasticsearch.index.query.TermsQueryBuilder; -import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class SemanticMatchQueryRewriteInterceptor implements QueryRewriteInterceptor { +public class SemanticMatchQueryRewriteInterceptor extends SemanticQueryRewriteInterceptor { public static final NodeFeature SEMANTIC_MATCH_QUERY_REWRITE_INTERCEPTION_SUPPORTED = new NodeFeature( "search.semantic_match_query_rewrite_interception_supported" @@ -33,63 +21,45 @@ public class SemanticMatchQueryRewriteInterceptor implements QueryRewriteInterce public SemanticMatchQueryRewriteInterceptor() {} @Override - public QueryBuilder interceptAndRewrite(QueryRewriteContext context, QueryBuilder queryBuilder) { + protected String getFieldName(QueryBuilder queryBuilder) { assert (queryBuilder instanceof MatchQueryBuilder); MatchQueryBuilder matchQueryBuilder = (MatchQueryBuilder) queryBuilder; - QueryBuilder rewritten = queryBuilder; - ResolvedIndices resolvedIndices = context.getResolvedIndices(); - if (resolvedIndices != null) { - Collection indexMetadataCollection = resolvedIndices.getConcreteLocalIndicesMetadata().values(); - List inferenceIndices = new ArrayList<>(); - List nonInferenceIndices = new ArrayList<>(); - for (IndexMetadata indexMetadata : indexMetadataCollection) { - String indexName = indexMetadata.getIndex().getName(); - InferenceFieldMetadata inferenceFieldMetadata = indexMetadata.getInferenceFields().get(matchQueryBuilder.fieldName()); - if (inferenceFieldMetadata != null) { - inferenceIndices.add(indexName); - } else { - nonInferenceIndices.add(indexName); - } - } - - if (inferenceIndices.isEmpty()) { - return rewritten; - } else if (nonInferenceIndices.isEmpty() == false) { - BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); - for (String inferenceIndexName : inferenceIndices) { - // Add a separate clause for each semantic query, because they may be using different inference endpoints - // TODO - consolidate this to a single clause once the semantic query supports multiple inference endpoints - boolQueryBuilder.should( - createSemanticSubQuery(inferenceIndexName, matchQueryBuilder.fieldName(), (String) matchQueryBuilder.value()) - ); - } - boolQueryBuilder.should(createMatchSubQuery(nonInferenceIndices, matchQueryBuilder)); - rewritten = boolQueryBuilder; - } else { - rewritten = new SemanticQueryBuilder(matchQueryBuilder.fieldName(), (String) matchQueryBuilder.value(), false); - } - } - - return rewritten; + return matchQueryBuilder.fieldName(); + } + @Override + protected String getQuery(QueryBuilder queryBuilder) { + assert (queryBuilder instanceof MatchQueryBuilder); + MatchQueryBuilder matchQueryBuilder = (MatchQueryBuilder) queryBuilder; + return (String) matchQueryBuilder.value(); } @Override - public String getQueryName() { - return MatchQueryBuilder.NAME; + protected QueryBuilder buildInferenceQuery(QueryBuilder queryBuilder, InferenceIndexInformationForField indexInformation) { + return new SemanticQueryBuilder(indexInformation.fieldName(), getQuery(queryBuilder), false); } - private QueryBuilder createSemanticSubQuery(String indexName, String fieldName, String value) { + @Override + protected QueryBuilder buildCombinedInferenceAndNonInferenceQuery( + QueryBuilder queryBuilder, + InferenceIndexInformationForField indexInformation + ) { + assert (queryBuilder instanceof MatchQueryBuilder); + MatchQueryBuilder matchQueryBuilder = (MatchQueryBuilder) queryBuilder; BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); - boolQueryBuilder.must(new SemanticQueryBuilder(fieldName, value, true)); - boolQueryBuilder.filter(new TermQueryBuilder(IndexFieldMapper.NAME, indexName)); + boolQueryBuilder.should( + createSemanticSubQuery( + indexInformation.getInferenceIndices(), + matchQueryBuilder.fieldName(), + (String) matchQueryBuilder.value() + ) + ); + boolQueryBuilder.should(createSubQueryForIndices(indexInformation.nonInferenceIndices(), matchQueryBuilder)); return boolQueryBuilder; } - private QueryBuilder createMatchSubQuery(List indices, MatchQueryBuilder matchQueryBuilder) { - BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); - boolQueryBuilder.must(matchQueryBuilder); - boolQueryBuilder.filter(new TermsQueryBuilder(IndexFieldMapper.NAME, indices)); - return boolQueryBuilder; + @Override + public String getQueryName() { + return MatchQueryBuilder.NAME; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java index 2a34651efcd9d..dd0f6fe59ab23 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java @@ -148,6 +148,14 @@ public String getWriteableName() { return NAME; } + public String getFieldName() { + return fieldName; + } + + public String getQuery() { + return query; + } + @Override public TransportVersion getMinimalSupportedVersion() { return TransportVersions.V_8_15_0; diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryRewriteInterceptor.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryRewriteInterceptor.java new file mode 100644 index 0000000000000..bb76ef0be24e9 --- /dev/null +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryRewriteInterceptor.java @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.inference.queries; + +import org.elasticsearch.action.ResolvedIndices; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.InferenceFieldMetadata; +import org.elasticsearch.index.mapper.IndexFieldMapper; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.TermsQueryBuilder; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Intercepts and adapts a query to be rewritten to work seamlessly on a semantic_text field. + */ +public abstract class SemanticQueryRewriteInterceptor implements QueryRewriteInterceptor { + + public SemanticQueryRewriteInterceptor() {} + + @Override + public QueryBuilder interceptAndRewrite(QueryRewriteContext context, QueryBuilder queryBuilder) { + String fieldName = getFieldName(queryBuilder); + ResolvedIndices resolvedIndices = context.getResolvedIndices(); + + if (resolvedIndices == null) { + // No resolved indices, so return the original query. + return queryBuilder; + } + + InferenceIndexInformationForField indexInformation = resolveIndicesForField(fieldName, resolvedIndices); + if (indexInformation.getInferenceIndices().isEmpty()) { + // No inference fields were identified, so return the original query. + return queryBuilder; + } else if (indexInformation.nonInferenceIndices().isEmpty() == false) { + // Combined case where the field name requested by this query contains both + // semantic_text and non-inference fields, so we have to combine queries per index + // containing each field type. + return buildCombinedInferenceAndNonInferenceQuery(queryBuilder, indexInformation); + } else { + // The only fields we've identified are inference fields (e.g. semantic_text), + // so rewrite the entire query to work on a semantic_text field. + return buildInferenceQuery(queryBuilder, indexInformation); + } + } + + /** + * @param queryBuilder {@link QueryBuilder} + * @return The singular field name requested by the provided query builder. + */ + protected abstract String getFieldName(QueryBuilder queryBuilder); + + /** + * @param queryBuilder {@link QueryBuilder} + * @return The text/query string requested by the provided query builder. + */ + protected abstract String getQuery(QueryBuilder queryBuilder); + + /** + * Builds the inference query + * + * @param queryBuilder {@link QueryBuilder} + * @param indexInformation {@link InferenceIndexInformationForField} + * @return {@link QueryBuilder} + */ + protected abstract QueryBuilder buildInferenceQuery(QueryBuilder queryBuilder, InferenceIndexInformationForField indexInformation); + + /** + * Builds a combined inference and non-inference query, + * which separates the different queries into appropriate indices based on field type. + * @param queryBuilder {@link QueryBuilder} + * @param indexInformation {@link InferenceIndexInformationForField} + * @return {@link QueryBuilder} + */ + protected abstract QueryBuilder buildCombinedInferenceAndNonInferenceQuery( + QueryBuilder queryBuilder, + InferenceIndexInformationForField indexInformation + ); + + private InferenceIndexInformationForField resolveIndicesForField(String fieldName, ResolvedIndices resolvedIndices) { + Collection indexMetadataCollection = resolvedIndices.getConcreteLocalIndicesMetadata().values(); + Map inferenceIndicesMetadata = new HashMap<>(); + List nonInferenceIndices = new ArrayList<>(); + for (IndexMetadata indexMetadata : indexMetadataCollection) { + String indexName = indexMetadata.getIndex().getName(); + InferenceFieldMetadata inferenceFieldMetadata = indexMetadata.getInferenceFields().get(fieldName); + if (inferenceFieldMetadata != null) { + inferenceIndicesMetadata.put(indexName, inferenceFieldMetadata); + } else { + nonInferenceIndices.add(indexName); + } + } + + return new InferenceIndexInformationForField(fieldName, inferenceIndicesMetadata, nonInferenceIndices); + } + + protected QueryBuilder createSubQueryForIndices(Collection indices, QueryBuilder queryBuilder) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.must(queryBuilder); + boolQueryBuilder.filter(new TermsQueryBuilder(IndexFieldMapper.NAME, indices)); + return boolQueryBuilder; + } + + protected QueryBuilder createSemanticSubQuery(Collection indices, String fieldName, String value) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.must(new SemanticQueryBuilder(fieldName, value, true)); + boolQueryBuilder.filter(new TermsQueryBuilder(IndexFieldMapper.NAME, indices)); + return boolQueryBuilder; + } + + /** + * Represents the indices and associated inference information for a field. + */ + public record InferenceIndexInformationForField( + String fieldName, + Map inferenceIndicesMetadata, + List nonInferenceIndices + ) { + + public Collection getInferenceIndices() { + return inferenceIndicesMetadata.keySet(); + } + + public Map> getInferenceIdsIndices() { + return inferenceIndicesMetadata.entrySet() + .stream() + .collect( + Collectors.groupingBy( + entry -> entry.getValue().getSearchInferenceId(), + Collectors.mapping(Map.Entry::getKey, Collectors.toList()) + ) + ); + } + } +} diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticSparseVectorQueryRewriteInterceptor.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticSparseVectorQueryRewriteInterceptor.java new file mode 100644 index 0000000000000..a35e83450c55a --- /dev/null +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticSparseVectorQueryRewriteInterceptor.java @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.inference.queries; + +import org.apache.lucene.search.join.ScoreMode; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.xpack.core.ml.search.SparseVectorQueryBuilder; +import org.elasticsearch.xpack.inference.mapper.SemanticTextField; + +import java.util.List; +import java.util.Map; + +public class SemanticSparseVectorQueryRewriteInterceptor extends SemanticQueryRewriteInterceptor { + + public static final NodeFeature SEMANTIC_SPARSE_VECTOR_QUERY_REWRITE_INTERCEPTION_SUPPORTED = new NodeFeature( + "search.semantic_sparse_vector_query_rewrite_interception_supported" + ); + + public SemanticSparseVectorQueryRewriteInterceptor() {} + + @Override + protected String getFieldName(QueryBuilder queryBuilder) { + assert (queryBuilder instanceof SparseVectorQueryBuilder); + SparseVectorQueryBuilder sparseVectorQueryBuilder = (SparseVectorQueryBuilder) queryBuilder; + return sparseVectorQueryBuilder.getFieldName(); + } + + @Override + protected String getQuery(QueryBuilder queryBuilder) { + assert (queryBuilder instanceof SparseVectorQueryBuilder); + SparseVectorQueryBuilder sparseVectorQueryBuilder = (SparseVectorQueryBuilder) queryBuilder; + return sparseVectorQueryBuilder.getQuery(); + } + + @Override + protected QueryBuilder buildInferenceQuery(QueryBuilder queryBuilder, InferenceIndexInformationForField indexInformation) { + Map> inferenceIdsIndices = indexInformation.getInferenceIdsIndices(); + if (inferenceIdsIndices.size() == 1) { + // Simple case, everything uses the same inference ID + String searchInferenceId = inferenceIdsIndices.keySet().iterator().next(); + return buildNestedQueryFromSparseVectorQuery(queryBuilder, searchInferenceId); + } else { + // Multiple inference IDs, construct a boolean query + return buildInferenceQueryWithMultipleInferenceIds(queryBuilder, inferenceIdsIndices); + } + } + + private QueryBuilder buildInferenceQueryWithMultipleInferenceIds( + QueryBuilder queryBuilder, + Map> inferenceIdsIndices + ) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + for (String inferenceId : inferenceIdsIndices.keySet()) { + boolQueryBuilder.should( + createSubQueryForIndices( + inferenceIdsIndices.get(inferenceId), + buildNestedQueryFromSparseVectorQuery(queryBuilder, inferenceId) + ) + ); + } + return boolQueryBuilder; + } + + @Override + protected QueryBuilder buildCombinedInferenceAndNonInferenceQuery( + QueryBuilder queryBuilder, + InferenceIndexInformationForField indexInformation + ) { + assert (queryBuilder instanceof SparseVectorQueryBuilder); + SparseVectorQueryBuilder sparseVectorQueryBuilder = (SparseVectorQueryBuilder) queryBuilder; + Map> inferenceIdsIndices = indexInformation.getInferenceIdsIndices(); + + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.should( + createSubQueryForIndices( + indexInformation.nonInferenceIndices(), + createSubQueryForIndices(indexInformation.nonInferenceIndices(), sparseVectorQueryBuilder) + ) + ); + // We always perform nested subqueries on semantic_text fields, to support + // sparse_vector queries using query vectors. + for (String inferenceId : inferenceIdsIndices.keySet()) { + boolQueryBuilder.should( + createSubQueryForIndices( + inferenceIdsIndices.get(inferenceId), + buildNestedQueryFromSparseVectorQuery(sparseVectorQueryBuilder, inferenceId) + ) + ); + } + return boolQueryBuilder; + } + + private QueryBuilder buildNestedQueryFromSparseVectorQuery(QueryBuilder queryBuilder, String searchInferenceId) { + assert (queryBuilder instanceof SparseVectorQueryBuilder); + SparseVectorQueryBuilder sparseVectorQueryBuilder = (SparseVectorQueryBuilder) queryBuilder; + return QueryBuilders.nestedQuery( + SemanticTextField.getChunksFieldName(sparseVectorQueryBuilder.getFieldName()), + new SparseVectorQueryBuilder( + SemanticTextField.getEmbeddingsFieldName(sparseVectorQueryBuilder.getFieldName()), + sparseVectorQueryBuilder.getQueryVectors(), + (sparseVectorQueryBuilder.getInferenceId() == null && sparseVectorQueryBuilder.getQuery() != null) + ? searchInferenceId + : sparseVectorQueryBuilder.getInferenceId(), + sparseVectorQueryBuilder.getQuery(), + sparseVectorQueryBuilder.shouldPruneTokens(), + sparseVectorQueryBuilder.getTokenPruningConfig() + ), + ScoreMode.Max + ); + } + + @Override + public String getQueryName() { + return SparseVectorQueryBuilder.NAME; + } +} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/index/query/SemanticMatchQueryRewriteInterceptorTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/index/query/SemanticMatchQueryRewriteInterceptorTests.java new file mode 100644 index 0000000000000..47705c14d5941 --- /dev/null +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/index/query/SemanticMatchQueryRewriteInterceptorTests.java @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.action.MockResolvedIndices; +import org.elasticsearch.action.OriginalIndices; +import org.elasticsearch.action.ResolvedIndices; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.InferenceFieldMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.client.NoOpClient; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.xpack.inference.queries.SemanticMatchQueryRewriteInterceptor; +import org.elasticsearch.xpack.inference.queries.SemanticQueryBuilder; +import org.junit.After; +import org.junit.Before; + +import java.io.IOException; +import java.util.Map; + +public class SemanticMatchQueryRewriteInterceptorTests extends ESTestCase { + + private TestThreadPool threadPool; + private NoOpClient client; + private Index index; + + private static final String FIELD_NAME = "fieldName"; + private static final String VALUE = "value"; + + @Before + public void setup() { + threadPool = createThreadPool(); + client = new NoOpClient(threadPool); + index = new Index(randomAlphaOfLength(10), randomAlphaOfLength(10)); + } + + @After + public void cleanup() { + threadPool.close(); + } + + public void testMatchQueryOnInferenceFieldIsInterceptedAndRewrittenToSemanticQuery() throws IOException { + Map inferenceFields = Map.of( + FIELD_NAME, + new InferenceFieldMetadata(index.getName(), "inferenceId", new String[] { FIELD_NAME }) + ); + QueryRewriteContext context = createQueryRewriteContext(inferenceFields); + QueryBuilder original = createTestQueryBuilder(); + QueryBuilder rewritten = original.rewrite(context); + assertTrue( + "Expected query to be intercepted, but was [" + rewritten.getClass().getName() + "]", + rewritten instanceof InterceptedQueryBuilderWrapper + ); + InterceptedQueryBuilderWrapper intercepted = (InterceptedQueryBuilderWrapper) rewritten; + assertTrue(intercepted.queryBuilder instanceof SemanticQueryBuilder); + SemanticQueryBuilder semanticQueryBuilder = (SemanticQueryBuilder) intercepted.queryBuilder; + assertEquals(FIELD_NAME, semanticQueryBuilder.getFieldName()); + assertEquals(VALUE, semanticQueryBuilder.getQuery()); + } + + public void testMatchQueryOnNonInferenceFieldRemainsMatchQuery() throws IOException { + QueryRewriteContext context = createQueryRewriteContext(Map.of()); // No inference fields + QueryBuilder original = createTestQueryBuilder(); + QueryBuilder rewritten = original.rewrite(context); + assertTrue( + "Expected query to remain match but was [" + rewritten.getClass().getName() + "]", + rewritten instanceof MatchQueryBuilder + ); + assertEquals(original, rewritten); + } + + private MatchQueryBuilder createTestQueryBuilder() { + return new MatchQueryBuilder(FIELD_NAME, VALUE); + } + + private QueryRewriteContext createQueryRewriteContext(Map inferenceFields) { + IndexMetadata indexMetadata = IndexMetadata.builder(index.getName()) + .settings( + Settings.builder() + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()) + .put(IndexMetadata.SETTING_INDEX_UUID, index.getUUID()) + ) + .numberOfShards(1) + .numberOfReplicas(0) + .putInferenceFields(inferenceFields) + .build(); + + ResolvedIndices resolvedIndices = new MockResolvedIndices( + Map.of(), + new OriginalIndices(new String[] { index.getName() }, IndicesOptions.DEFAULT), + Map.of(index, indexMetadata) + ); + + return new QueryRewriteContext(null, client, null, resolvedIndices, null, createRewriteInterceptor()); + } + + private QueryRewriteInterceptor createRewriteInterceptor() { + return new SemanticMatchQueryRewriteInterceptor(); + } +} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/index/query/SemanticSparseVectorQueryRewriteInterceptorTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/index/query/SemanticSparseVectorQueryRewriteInterceptorTests.java new file mode 100644 index 0000000000000..1adad1df7b29b --- /dev/null +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/index/query/SemanticSparseVectorQueryRewriteInterceptorTests.java @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.action.MockResolvedIndices; +import org.elasticsearch.action.OriginalIndices; +import org.elasticsearch.action.ResolvedIndices; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.InferenceFieldMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.client.NoOpClient; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.xpack.core.ml.search.SparseVectorQueryBuilder; +import org.elasticsearch.xpack.inference.mapper.SemanticTextField; +import org.elasticsearch.xpack.inference.queries.SemanticSparseVectorQueryRewriteInterceptor; +import org.junit.After; +import org.junit.Before; + +import java.io.IOException; +import java.util.Map; + +public class SemanticSparseVectorQueryRewriteInterceptorTests extends ESTestCase { + + private TestThreadPool threadPool; + private NoOpClient client; + private Index index; + + private static final String FIELD_NAME = "fieldName"; + private static final String INFERENCE_ID = "inferenceId"; + private static final String QUERY = "query"; + + @Before + public void setup() { + threadPool = createThreadPool(); + client = new NoOpClient(threadPool); + index = new Index(randomAlphaOfLength(10), randomAlphaOfLength(10)); + } + + @After + public void cleanup() { + threadPool.close(); + } + + public void testSparseVectorQueryOnInferenceFieldIsInterceptedAndRewritten() throws IOException { + Map inferenceFields = Map.of( + FIELD_NAME, + new InferenceFieldMetadata(index.getName(), "inferenceId", new String[] { FIELD_NAME }) + ); + QueryRewriteContext context = createQueryRewriteContext(inferenceFields); + QueryBuilder original = new SparseVectorQueryBuilder(FIELD_NAME, INFERENCE_ID, QUERY); + QueryBuilder rewritten = original.rewrite(context); + assertTrue( + "Expected query to be intercepted, but was [" + rewritten.getClass().getName() + "]", + rewritten instanceof InterceptedQueryBuilderWrapper + ); + InterceptedQueryBuilderWrapper intercepted = (InterceptedQueryBuilderWrapper) rewritten; + assertTrue(intercepted.queryBuilder instanceof NestedQueryBuilder); + NestedQueryBuilder nestedQueryBuilder = (NestedQueryBuilder) intercepted.queryBuilder; + assertEquals(SemanticTextField.getChunksFieldName(FIELD_NAME), nestedQueryBuilder.path()); + QueryBuilder innerQuery = nestedQueryBuilder.query(); + assertTrue(innerQuery instanceof SparseVectorQueryBuilder); + SparseVectorQueryBuilder sparseVectorQueryBuilder = (SparseVectorQueryBuilder) innerQuery; + assertEquals(SemanticTextField.getEmbeddingsFieldName(FIELD_NAME), sparseVectorQueryBuilder.getFieldName()); + assertEquals(INFERENCE_ID, sparseVectorQueryBuilder.getInferenceId()); + assertEquals(QUERY, sparseVectorQueryBuilder.getQuery()); + } + + public void testSparseVectorQueryOnInferenceFieldWithoutInferenceIdIsInterceptedAndRewritten() throws IOException { + Map inferenceFields = Map.of( + FIELD_NAME, + new InferenceFieldMetadata(index.getName(), "inferenceId", new String[] { FIELD_NAME }) + ); + QueryRewriteContext context = createQueryRewriteContext(inferenceFields); + QueryBuilder original = new SparseVectorQueryBuilder(FIELD_NAME, null, QUERY); + QueryBuilder rewritten = original.rewrite(context); + assertTrue( + "Expected query to be intercepted, but was [" + rewritten.getClass().getName() + "]", + rewritten instanceof InterceptedQueryBuilderWrapper + ); + InterceptedQueryBuilderWrapper intercepted = (InterceptedQueryBuilderWrapper) rewritten; + assertTrue(intercepted.queryBuilder instanceof NestedQueryBuilder); + NestedQueryBuilder nestedQueryBuilder = (NestedQueryBuilder) intercepted.queryBuilder; + assertEquals(SemanticTextField.getChunksFieldName(FIELD_NAME), nestedQueryBuilder.path()); + QueryBuilder innerQuery = nestedQueryBuilder.query(); + assertTrue(innerQuery instanceof SparseVectorQueryBuilder); + SparseVectorQueryBuilder sparseVectorQueryBuilder = (SparseVectorQueryBuilder) innerQuery; + assertEquals(SemanticTextField.getEmbeddingsFieldName(FIELD_NAME), sparseVectorQueryBuilder.getFieldName()); + assertEquals(INFERENCE_ID, sparseVectorQueryBuilder.getInferenceId()); + assertEquals(QUERY, sparseVectorQueryBuilder.getQuery()); + } + + public void testSparseVectorQueryOnNonInferenceFieldRemainsUnchanged() throws IOException { + QueryRewriteContext context = createQueryRewriteContext(Map.of()); // No inference fields + QueryBuilder original = new SparseVectorQueryBuilder(FIELD_NAME, INFERENCE_ID, QUERY); + QueryBuilder rewritten = original.rewrite(context); + assertTrue( + "Expected query to remain sparse_vector but was [" + rewritten.getClass().getName() + "]", + rewritten instanceof SparseVectorQueryBuilder + ); + assertEquals(original, rewritten); + } + + private QueryRewriteContext createQueryRewriteContext(Map inferenceFields) { + IndexMetadata indexMetadata = IndexMetadata.builder(index.getName()) + .settings( + Settings.builder() + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()) + .put(IndexMetadata.SETTING_INDEX_UUID, index.getUUID()) + ) + .numberOfShards(1) + .numberOfReplicas(0) + .putInferenceFields(inferenceFields) + .build(); + + ResolvedIndices resolvedIndices = new MockResolvedIndices( + Map.of(), + new OriginalIndices(new String[] { index.getName() }, IndicesOptions.DEFAULT), + Map.of(index, indexMetadata) + ); + + return new QueryRewriteContext(null, client, null, resolvedIndices, null, createRewriteInterceptor()); + } + + private QueryRewriteInterceptor createRewriteInterceptor() { + return new SemanticSparseVectorQueryRewriteInterceptor(); + } +} diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/46_semantic_text_sparse_vector.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/46_semantic_text_sparse_vector.yml new file mode 100644 index 0000000000000..f1cff512fd209 --- /dev/null +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/46_semantic_text_sparse_vector.yml @@ -0,0 +1,249 @@ +setup: + - requires: + cluster_features: "search.semantic_sparse_vector_query_rewrite_interception_supported" + reason: semantic_text sparse_vector support introduced in 8.18.0 + + - do: + inference.put: + task_type: sparse_embedding + inference_id: sparse-inference-id + body: > + { + "service": "test_service", + "service_settings": { + "model": "my_model", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + inference.put: + task_type: sparse_embedding + inference_id: sparse-inference-id-2 + body: > + { + "service": "test_service", + "service_settings": { + "model": "my_model", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + indices.create: + index: test-semantic-text-index + body: + mappings: + properties: + inference_field: + type: semantic_text + inference_id: sparse-inference-id + + - do: + indices.create: + index: test-semantic-text-index-2 + body: + mappings: + properties: + inference_field: + type: semantic_text + inference_id: sparse-inference-id-2 + + - do: + indices.create: + index: test-sparse-vector-index + body: + mappings: + properties: + inference_field: + type: sparse_vector + + - do: + index: + index: test-semantic-text-index + id: doc_1 + body: + inference_field: [ "inference test", "another inference test" ] + refresh: true + + - do: + index: + index: test-semantic-text-index-2 + id: doc_3 + body: + inference_field: [ "inference test", "another inference test" ] + refresh: true + + - do: + index: + index: test-sparse-vector-index + id: doc_2 + body: + inference_field: { "feature_0": 1, "feature_1": 2, "feature_2": 3, "feature_3": 4, "feature_4": 5 } + refresh: true + +--- +"Nested sparse_vector queries using the old format on semantic_text embeddings and inference still work": + - skip: + features: [ "headers" ] + + - do: + headers: + # Force JSON content type so that we use a parser that interprets the floating-point score as a double + Content-Type: application/json + search: + index: test-semantic-text-index + body: + query: + nested: + path: inference_field.inference.chunks + query: + sparse_vector: + field: inference_field.inference.chunks.embeddings + inference_id: sparse-inference-id + query: test + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "doc_1" } + +--- +"Nested sparse_vector queries using the old format on semantic_text embeddings and query vectors still work": + - skip: + features: [ "headers" ] + + - do: + headers: + # Force JSON content type so that we use a parser that interprets the floating-point score as a double + Content-Type: application/json + search: + index: test-semantic-text-index + body: + query: + nested: + path: inference_field.inference.chunks + query: + sparse_vector: + field: inference_field.inference.chunks.embeddings + query_vector: { "feature_0": 1, "feature_1": 2, "feature_2": 3, "feature_3": 4, "feature_4": 5 } + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "doc_1" } + +--- +"sparse_vector query against semantic_text field using a specified inference ID": + + - do: + search: + index: test-semantic-text-index + body: + query: + sparse_vector: + field: inference_field + inference_id: sparse-inference-id + query: "inference test" + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "doc_1" } + +--- +"sparse_vector query against semantic_text field using inference ID configured in semantic_text field": + + - do: + search: + index: test-semantic-text-index + body: + query: + sparse_vector: + field: inference_field + query: "inference test" + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "doc_1" } + +--- +"sparse_vector query against semantic_text field using query vectors": + + - do: + search: + index: test-semantic-text-index + body: + query: + sparse_vector: + field: inference_field + query_vector: { "feature_0": 1, "feature_1": 2, "feature_2": 3, "feature_3": 4, "feature_4": 5 } + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "doc_1" } + +--- +"sparse_vector query against combined sparse_vector and semantic_text fields using inference": + + - do: + search: + index: + - test-semantic-text-index + - test-sparse-vector-index + body: + query: + sparse_vector: + field: inference_field + inference_id: sparse-inference-id + query: "inference test" + + - match: { hits.total.value: 2 } + +--- +"sparse_vector query against combined sparse_vector and semantic_text fields still requires inference ID": + + - do: + catch: bad_request + search: + index: + - test-semantic-text-index + - test-sparse-vector-index + body: + query: + sparse_vector: + field: inference_field + query: "inference test" + + - match: { error.type: "illegal_argument_exception" } + - match: { error.reason: "inference_id required to perform vector search on query string" } + +--- +"sparse_vector query against combined sparse_vector and semantic_text fields using query vectors": + + - do: + search: + index: + - test-semantic-text-index + - test-sparse-vector-index + body: + query: + sparse_vector: + field: inference_field + query_vector: { "feature_0": 1, "feature_1": 2, "feature_2": 3, "feature_3": 4, "feature_4": 5 } + + - match: { hits.total.value: 2 } + + +--- +"sparse_vector query against multiple semantic_text fields with multiple inference IDs specified in semantic_text fields": + + - do: + search: + index: + - test-semantic-text-index + - test-semantic-text-index-2 + body: + query: + sparse_vector: + field: inference_field + query: "inference test" + + - match: { hits.total.value: 2 } + diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/sparse_vector_search.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/sparse_vector_search.yml index 332981a580802..3481773b0bab3 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/sparse_vector_search.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/sparse_vector_search.yml @@ -268,7 +268,7 @@ setup: - match: { hits.hits.0._score: 0.25 } --- -"Test sparse_vector requires one of inference_id or query_vector": +"Test sparse_vector requires one of query or query_vector": - do: catch: /\[sparse_vector\] requires one of \[query_vector\] or \[inference_id\]/ search: @@ -281,7 +281,41 @@ setup: - match: { status: 400 } --- -"Test sparse_vector only allows one of inference_id or query_vector": +"Test sparse_vector returns an error if inference ID not specified with query": + - do: + catch: bad_request # This is for BWC, the actual error message is tested in a subsequent test + search: + index: index-with-sparse-vector + body: + query: + sparse_vector: + field: text + query: "octopus comforter smells" + + - match: { status: 400 } + +--- +"Test sparse_vector requires an inference ID to be specified on sparse_vector fields": + - requires: + cluster_features: [ "search.semantic_sparse_vector_query_rewrite_interception_supported" ] + reason: "Error message changed in 8.18" + - do: + catch: /inference_id required to perform vector search on query string/ + search: + index: index-with-sparse-vector + body: + query: + sparse_vector: + field: text + query: "octopus comforter smells" + + - match: { status: 400 } + +--- +"Test sparse_vector only allows one of query or query_vector (note the error message is misleading)": + - requires: + cluster_features: [ "search.semantic_sparse_vector_query_rewrite_interception_supported" ] + reason: "sparse vector inference checks updated in 8.18 to support sparse_vector on semantic_text fields" - do: catch: /\[sparse_vector\] requires one of \[query_vector\] or \[inference_id\]/ search: @@ -290,7 +324,7 @@ setup: query: sparse_vector: field: text - inference_id: text_expansion_model + query: "octopus comforter smells" query_vector: the: 1.0 comforter: 1.0 From a6dcaee4199353d8cbd4a2fd67df9d833684b8ab Mon Sep 17 00:00:00 2001 From: Nick Tindall Date: Wed, 18 Dec 2024 11:58:16 +1100 Subject: [PATCH 053/119] Add test for GCS fixture (#118737) Relates: ES-5679 --- .../org/elasticsearch/rest/RestUtils.java | 8 + test/fixtures/gcs-fixture/build.gradle | 1 - .../gcs/GoogleCloudStorageHttpHandler.java | 8 +- .../GoogleCloudStorageHttpHandlerTests.java | 509 ++++++++++++++++++ 4 files changed, 520 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/gcs-fixture/src/test/java/fixture/gcs/GoogleCloudStorageHttpHandlerTests.java diff --git a/server/src/main/java/org/elasticsearch/rest/RestUtils.java b/server/src/main/java/org/elasticsearch/rest/RestUtils.java index df51b57f0859c..bbca086e345f7 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestUtils.java +++ b/server/src/main/java/org/elasticsearch/rest/RestUtils.java @@ -15,6 +15,7 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; +import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -35,6 +36,13 @@ public class RestUtils { public static final UnaryOperator REST_DECODER = RestUtils::decodeComponent; + public static void decodeQueryString(URI uri, Map params) { + final var rawQuery = uri.getRawQuery(); + if (Strings.hasLength(rawQuery)) { + decodeQueryString(rawQuery, 0, params); + } + } + public static void decodeQueryString(String s, int fromIndex, Map params) { if (fromIndex < 0) { return; diff --git a/test/fixtures/gcs-fixture/build.gradle b/test/fixtures/gcs-fixture/build.gradle index e8f1a2e15a4e0..6cf2e1ee52c2c 100644 --- a/test/fixtures/gcs-fixture/build.gradle +++ b/test/fixtures/gcs-fixture/build.gradle @@ -9,7 +9,6 @@ apply plugin: 'elasticsearch.java' description = 'Fixture for Google Cloud Storage service' -tasks.named("test").configure { enabled = false } dependencies { api project(':server') diff --git a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java index f6b52a32a9a1d..163712fb05a50 100644 --- a/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java +++ b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java @@ -95,7 +95,7 @@ public void handle(final HttpExchange exchange) throws IOException { } else if (Regex.simpleMatch("GET /storage/v1/b/" + bucket + "/o*", request)) { // List Objects https://cloud.google.com/storage/docs/json_api/v1/objects/list final Map params = new HashMap<>(); - RestUtils.decodeQueryString(exchange.getRequestURI().getQuery(), 0, params); + RestUtils.decodeQueryString(exchange.getRequestURI(), params); final String prefix = params.getOrDefault("prefix", ""); final String delimiter = params.get("delimiter"); @@ -212,7 +212,7 @@ public void handle(final HttpExchange exchange) throws IOException { } else if (Regex.simpleMatch("POST /upload/storage/v1/b/" + bucket + "/*uploadType=resumable*", request)) { // Resumable upload initialization https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload final Map params = new HashMap<>(); - RestUtils.decodeQueryString(exchange.getRequestURI().getQuery(), 0, params); + RestUtils.decodeQueryString(exchange.getRequestURI(), params); final String blobName = params.get("name"); blobs.put(blobName, BytesArray.EMPTY); @@ -237,7 +237,7 @@ public void handle(final HttpExchange exchange) throws IOException { } else if (Regex.simpleMatch("PUT /upload/storage/v1/b/" + bucket + "/o?*uploadType=resumable*", request)) { // Resumable upload https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload final Map params = new HashMap<>(); - RestUtils.decodeQueryString(exchange.getRequestURI().getQuery(), 0, params); + RestUtils.decodeQueryString(exchange.getRequestURI(), params); final String blobName = params.get("test_blob_name"); if (blobs.containsKey(blobName) == false) { @@ -269,8 +269,6 @@ public void handle(final HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1); } } finally { - int read = exchange.getRequestBody().read(); - assert read == -1 : "Request body should have been fully read here but saw [" + read + "]"; exchange.close(); } } diff --git a/test/fixtures/gcs-fixture/src/test/java/fixture/gcs/GoogleCloudStorageHttpHandlerTests.java b/test/fixtures/gcs-fixture/src/test/java/fixture/gcs/GoogleCloudStorageHttpHandlerTests.java new file mode 100644 index 0000000000000..0caaa983f76df --- /dev/null +++ b/test/fixtures/gcs-fixture/src/test/java/fixture/gcs/GoogleCloudStorageHttpHandlerTests.java @@ -0,0 +1,509 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package fixture.gcs; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpPrincipal; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.bytes.CompositeBytesReference; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ESTestCase; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.GZIPOutputStream; + +public class GoogleCloudStorageHttpHandlerTests extends ESTestCase { + + private static final String HOST = "http://127.0.0.1:12345"; + private static final int RESUME_INCOMPLETE = 308; + + public void testRejectsBadUri() { + assertEquals( + RestStatus.NOT_FOUND.getStatus(), + handleRequest(new GoogleCloudStorageHttpHandler("bucket"), randomFrom("GET", "PUT", "POST", "DELETE", "HEAD"), "/not-in-bucket") + .status() + ); + } + + public void testCheckEndpoint() { + final var handler = new GoogleCloudStorageHttpHandler("bucket"); + + assertEquals( + RestStatus.OK, + handleRequest(handler, "GET", "/", BytesArray.EMPTY, Headers.of("Metadata-Flavor", "Google")).restStatus() + ); + } + + public void testSimpleObjectOperations() { + final var bucket = randomAlphaOfLength(10); + final var handler = new GoogleCloudStorageHttpHandler(bucket); + + assertEquals(RestStatus.NOT_FOUND, handleRequest(handler, "GET", "/download/storage/v1/b/" + bucket + "/o/blob").restStatus()); + + assertEquals( + new TestHttpResponse(RestStatus.OK, "{\"kind\":\"storage#objects\",\"items\":[],\"prefixes\":[]}"), + handleRequest(handler, "GET", "/storage/v1/b/" + bucket + "/o") + ); + + // Multipart upload + final var body = randomAlphaOfLength(50); + assertEquals( + RestStatus.OK, + handleRequest( + handler, + "POST", + "/upload/storage/v1/b/" + bucket + "/?uploadType=multipart", + createGzipCompressedMultipartUploadBody(bucket, "path/blob", body) + ).restStatus() + ); + assertEquals( + new TestHttpResponse(RestStatus.OK, body), + handleRequest(handler, "GET", "/download/storage/v1/b/" + bucket + "/o/path/blob") + ); + + assertEquals(new TestHttpResponse(RestStatus.OK, Strings.format(""" + {"kind":"storage#objects","items":[{"kind":"storage#object","bucket":"%s","name":"path/blob","id":"path/blob","size":"50"} + ],"prefixes":[]}""", bucket)), handleRequest(handler, "GET", "/storage/v1/b/" + bucket + "/o")); + + assertEquals(new TestHttpResponse(RestStatus.OK, Strings.format(""" + {"kind":"storage#objects","items":[{"kind":"storage#object","bucket":"%s","name":"path/blob","id":"path/blob","size":"50"} + ],"prefixes":[]}""", bucket)), handleRequest(handler, "GET", "/storage/v1/b/" + bucket + "/o?prefix=path/")); + + assertEquals( + new TestHttpResponse(RestStatus.OK, """ + {"kind":"storage#objects","items":[],"prefixes":[]}"""), + handleRequest(handler, "GET", "/storage/v1/b/" + bucket + "/o?prefix=path/other") + ); + + assertEquals( + new TestHttpResponse(RestStatus.OK, """ + --__END_OF_PART__d8b50acb-87dc-4630-a3d3-17d187132ebc__ + Content-Length: 162 + Content-Type: application/http + content-id: 1 + content-transfer-encoding: binary + + HTTP/1.1 204 NO_CONTENT + + + + + --__END_OF_PART__d8b50acb-87dc-4630-a3d3-17d187132ebc__ + """.replaceAll("\n", "\r\n")), + handleRequest( + handler, + "POST", + "/batch/storage/v1", + createBatchDeleteRequest(bucket, "path/blob"), + Headers.of("Content-Type", "mixed/multipart") + ) + ); + assertEquals( + RestStatus.OK, + handleRequest( + handler, + "POST", + "/batch/storage/v1", + createBatchDeleteRequest(bucket, "path/blob"), + Headers.of("Content-Type", "mixed/multipart") + ).restStatus() + ); + + assertEquals( + new TestHttpResponse(RestStatus.OK, """ + {"kind":"storage#objects","items":[],"prefixes":[]}"""), + handleRequest(handler, "GET", "/storage/v1/b/" + bucket + "/o?prefix=path/") + ); + } + + public void testGetWithBytesRange() { + final var bucket = randomIdentifier(); + final var handler = new GoogleCloudStorageHttpHandler(bucket); + final var blobName = "blob_name_" + randomIdentifier(); + final var blobPath = "/download/storage/v1/b/" + bucket + "/o/" + blobName; + final var blobBytes = randomBytesReference(256); + + assertEquals( + RestStatus.OK, + handleRequest( + handler, + "POST", + "/upload/storage/v1/b/" + bucket + "/?uploadType=multipart", + createGzipCompressedMultipartUploadBody(bucket, blobName, blobBytes) + ).restStatus() + ); + + assertEquals( + "No Range", + new TestHttpResponse(RestStatus.OK, blobBytes, TestHttpExchange.EMPTY_HEADERS), + handleRequest(handler, "GET", blobPath) + ); + + var end = blobBytes.length() - 1; + assertEquals( + "Exact Range: bytes=0-" + end, + new TestHttpResponse(RestStatus.OK, blobBytes, TestHttpExchange.EMPTY_HEADERS), + handleRequest(handler, "GET", blobPath, BytesArray.EMPTY, rangeHeader(0, end)) + ); + + end = randomIntBetween(blobBytes.length() - 1, Integer.MAX_VALUE); + assertEquals( + "Larger Range: bytes=0-" + end, + new TestHttpResponse(RestStatus.OK, blobBytes, TestHttpExchange.EMPTY_HEADERS), + handleRequest(handler, "GET", blobPath, BytesArray.EMPTY, rangeHeader(0, end)) + ); + + var start = randomIntBetween(blobBytes.length(), Integer.MAX_VALUE - 1); + end = randomIntBetween(start, Integer.MAX_VALUE); + assertEquals( + "Invalid Range: bytes=" + start + '-' + end, + new TestHttpResponse(RestStatus.REQUESTED_RANGE_NOT_SATISFIED, BytesArray.EMPTY, TestHttpExchange.EMPTY_HEADERS), + handleRequest(handler, "GET", blobPath, BytesArray.EMPTY, rangeHeader(start, end)) + ); + + start = randomIntBetween(0, blobBytes.length() - 1); + var length = randomIntBetween(1, blobBytes.length() - start); + end = start + length - 1; + assertEquals( + "Range: bytes=" + start + '-' + end, + new TestHttpResponse(RestStatus.OK, blobBytes.slice(start, length), TestHttpExchange.EMPTY_HEADERS), + handleRequest(handler, "GET", blobPath, BytesArray.EMPTY, rangeHeader(start, end)) + ); + } + + public void testResumableUpload() { + final var bucket = randomIdentifier(); + final var handler = new GoogleCloudStorageHttpHandler(bucket); + final var blobName = "blob_name_" + randomIdentifier(); + + final var createUploadResponse = handleRequest( + handler, + "POST", + "/upload/storage/v1/b/" + bucket + "/?uploadType=resumable&name=" + blobName + ); + final var locationHeader = createUploadResponse.headers.getFirst("Location"); + final var sessionURI = locationHeader.substring(locationHeader.indexOf(HOST) + HOST.length()); + assertEquals(RestStatus.OK, createUploadResponse.restStatus()); + + final var part1 = randomAlphaOfLength(50); + final var uploadPart1Response = handleRequest(handler, "PUT", sessionURI, part1, contentRangeHeader(0, 50, null)); + assertEquals(new TestHttpResponse(RESUME_INCOMPLETE, rangeHeader(0, 50)), uploadPart1Response); + + assertEquals( + new TestHttpResponse(RESUME_INCOMPLETE, TestHttpExchange.EMPTY_HEADERS), + handleRequest(handler, "PUT", sessionURI, BytesArray.EMPTY, contentRangeHeader(null, null, null)) + ); + + final var part2 = randomAlphaOfLength(50); + final var uploadPart2Response = handleRequest(handler, "PUT", sessionURI, part2, contentRangeHeader(51, 100, null)); + assertEquals(new TestHttpResponse(RESUME_INCOMPLETE, rangeHeader(51, 100)), uploadPart2Response); + + final var part3 = randomAlphaOfLength(30); + final var uploadPart3Response = handleRequest(handler, "PUT", sessionURI, part3, contentRangeHeader(101, 130, 130)); + assertEquals(new TestHttpResponse(RestStatus.OK, TestHttpExchange.EMPTY_HEADERS), uploadPart3Response); + + // complete upload should be visible now + + // can download contents + assertEquals( + new TestHttpResponse(RestStatus.OK, part1 + part2 + part3), + handleRequest(handler, "GET", "/download/storage/v1/b/" + bucket + "/o/" + blobName) + ); + + // can see in listing + assertEquals(new TestHttpResponse(RestStatus.OK, Strings.format(""" + {"kind":"storage#objects","items":[{"kind":"storage#object","bucket":"%s","name":"%s","id":"%s","size":"130"} + ],"prefixes":[]}""", bucket, blobName, blobName)), handleRequest(handler, "GET", "/storage/v1/b/" + bucket + "/o")); + + // can get metadata + assertEquals(new TestHttpResponse(RestStatus.OK, Strings.format(""" + {"kind":"storage#object","bucket":"%s","name":"%s","id":"%s","size":"130"} + """, bucket, blobName, blobName)), handleRequest(handler, "GET", "/storage/v1/b/" + bucket + "/o/" + blobName)); + } + + private record TestHttpResponse(int status, BytesReference body, Headers headers) { + TestHttpResponse(RestStatus status, BytesReference body, Headers headers) { + this(status.getStatus(), body, headers); + } + + TestHttpResponse(RestStatus status, String body) { + this(status.getStatus(), new BytesArray(body.getBytes(StandardCharsets.UTF_8)), TestHttpExchange.EMPTY_HEADERS); + } + + TestHttpResponse(RestStatus status, Headers headers) { + this(status.getStatus(), BytesArray.EMPTY, headers); + } + + TestHttpResponse(int statusCode, Headers headers) { + this(statusCode, BytesArray.EMPTY, headers); + } + + RestStatus restStatus() { + return Objects.requireNonNull(RestStatus.fromCode(status)); + } + + @Override + public String toString() { + return "TestHttpResponse{" + "status=" + status + ", body={size=" + body.utf8ToString() + "}, headers=" + headers + '}'; + } + } + + private static TestHttpResponse handleRequest(GoogleCloudStorageHttpHandler handler, String method, String uri) { + return handleRequest(handler, method, uri, ""); + } + + private static TestHttpResponse handleRequest(GoogleCloudStorageHttpHandler handler, String method, String uri, String requestBody) { + return handleRequest(handler, method, uri, new BytesArray(requestBody.getBytes(StandardCharsets.UTF_8))); + } + + private static TestHttpResponse handleRequest( + GoogleCloudStorageHttpHandler handler, + String method, + String uri, + String requestBody, + Headers headers + ) { + return handleRequest(handler, method, uri, new BytesArray(requestBody.getBytes(StandardCharsets.UTF_8)), headers); + } + + private static TestHttpResponse handleRequest( + GoogleCloudStorageHttpHandler handler, + String method, + String uri, + BytesReference requestBody + ) { + return handleRequest(handler, method, uri, requestBody, TestHttpExchange.EMPTY_HEADERS); + } + + private static TestHttpResponse handleRequest( + GoogleCloudStorageHttpHandler handler, + String method, + String uri, + BytesReference requestBody, + Headers requestHeaders + ) { + final var httpExchange = new TestHttpExchange(method, uri, requestBody, requestHeaders); + try { + handler.handle(httpExchange); + } catch (IOException e) { + fail(e); + } + assertNotEquals(0, httpExchange.getResponseCode()); + var responseHeaders = new Headers(); + httpExchange.getResponseHeaders().forEach((header, values) -> { + // com.sun.net.httpserver.Headers.Headers() normalize keys + if ("Range".equals(header) || "Content-range".equals(header) || "Location".equals(header)) { + responseHeaders.put(header, List.copyOf(values)); + } + }); + return new TestHttpResponse(httpExchange.getResponseCode(), httpExchange.getResponseBodyContents(), responseHeaders); + } + + private static Headers contentRangeHeader(@Nullable Integer startInclusive, @Nullable Integer endInclusive, @Nullable Integer limit) { + final String rangeString = startInclusive != null && endInclusive != null ? startInclusive + "-" + endInclusive : "*"; + final String limitString = limit == null ? "*" : limit.toString(); + return Headers.of("Content-Range", "bytes " + rangeString + "/" + limitString); + } + + private static Headers rangeHeader(long start, long end) { + return Headers.of("Range", Strings.format("bytes=%d-%d", start, end)); + } + + private static BytesReference createGzipCompressedMultipartUploadBody(String bucketName, String path, String content) { + return createGzipCompressedMultipartUploadBody(bucketName, path, new BytesArray(content.getBytes(StandardCharsets.UTF_8))); + } + + private static BytesReference createGzipCompressedMultipartUploadBody(String bucketName, String path, BytesReference content) { + final String metadataString = Strings.format("{\"bucket\":\"%s\", \"name\":\"%s\"}", bucketName, path); + final BytesReference header = new BytesArray(Strings.format(""" + --__END_OF_PART__a607a67c-6df7-4b87-b8a1-81f639a75a97__ + Content-Length: %d + Content-Type: application/json; charset=UTF-8 + content-transfer-encoding: binary + + %s + --__END_OF_PART__a607a67c-6df7-4b87-b8a1-81f639a75a97__ + Content-Type: application/octet-stream + content-transfer-encoding: binary + + """.replaceAll("\n", "\r\n"), metadataString.length(), metadataString).getBytes(StandardCharsets.UTF_8)); + + final BytesReference footer = new BytesArray(""" + + --__END_OF_PART__a607a67c-6df7-4b87-b8a1-81f639a75a97__-- + """.replaceAll("\n", "\r\n")); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(out)) { + gzipOutputStream.write(BytesReference.toBytes(CompositeBytesReference.of(header, content, footer))); + } catch (IOException e) { + fail(e); + } + return new BytesArray(out.toByteArray()); + } + + private static String createBatchDeleteRequest(String bucketName, String... paths) { + final String deleteRequestTemplate = """ + DELETE %s/storage/v1/b/%s/o/%s HTTP/1.1 + Authorization: Bearer foo + x-goog-api-client: gl-java/23.0.0 gdcl/2.1.1 mac-os-x/15.2 + + + """; + final String partTemplate = """ + --__END_OF_PART__d8b50acb-87dc-4630-a3d3-17d187132ebc__ + Content-Length: %d + Content-Type: application/http + content-id: %d + content-transfer-encoding: binary + + %s + """; + StringBuilder builder = new StringBuilder(); + AtomicInteger contentId = new AtomicInteger(); + Arrays.stream(paths).forEach(p -> { + final String deleteRequest = Strings.format(deleteRequestTemplate, HOST, bucketName, p); + final String part = Strings.format(partTemplate, deleteRequest.length(), contentId.incrementAndGet(), deleteRequest); + builder.append(part); + }); + builder.append("--__END_OF_PART__d8b50acb-87dc-4630-a3d3-17d187132ebc__"); + return builder.toString(); + } + + private static class TestHttpExchange extends HttpExchange { + + private static final Headers EMPTY_HEADERS = new Headers(); + + private final String method; + private final URI uri; + private final BytesReference requestBody; + private final Headers requestHeaders; + + private final Headers responseHeaders = new Headers(); + private final BytesStreamOutput responseBody = new BytesStreamOutput(); + private int responseCode; + + TestHttpExchange(String method, String uri, BytesReference requestBody, Headers requestHeaders) { + this.method = method; + this.uri = URI.create(uri); + this.requestBody = requestBody; + this.requestHeaders = new Headers(requestHeaders); + this.requestHeaders.add("Host", HOST); + } + + @Override + public Headers getRequestHeaders() { + return requestHeaders; + } + + @Override + public Headers getResponseHeaders() { + return responseHeaders; + } + + @Override + public URI getRequestURI() { + return uri; + } + + @Override + public String getRequestMethod() { + return method; + } + + @Override + public HttpContext getHttpContext() { + return null; + } + + @Override + public void close() {} + + @Override + public InputStream getRequestBody() { + try { + return requestBody.streamInput(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + public OutputStream getResponseBody() { + return responseBody; + } + + @Override + public void sendResponseHeaders(int rCode, long responseLength) { + this.responseCode = rCode; + } + + @Override + public InetSocketAddress getRemoteAddress() { + return null; + } + + @Override + public int getResponseCode() { + return responseCode; + } + + public BytesReference getResponseBodyContents() { + return responseBody.bytes(); + } + + @Override + public InetSocketAddress getLocalAddress() { + return null; + } + + @Override + public String getProtocol() { + return "HTTP/1.1"; + } + + @Override + public Object getAttribute(String name) { + return null; + } + + @Override + public void setAttribute(String name, Object value) { + fail("setAttribute not implemented"); + } + + @Override + public void setStreams(InputStream i, OutputStream o) { + fail("setStreams not implemented"); + } + + @Override + public HttpPrincipal getPrincipal() { + fail("getPrincipal not implemented"); + throw new UnsupportedOperationException("getPrincipal not implemented"); + } + } +} From ffe9002d3252315b2af453b662ad5c10bc626768 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 18 Dec 2024 08:17:37 +0400 Subject: [PATCH 054/119] Add missing timeouts to rest-api-spec ILM APIs (#118837) --- docs/changelog/118837.yaml | 5 +++++ .../rest-api-spec/api/ilm.delete_lifecycle.json | 11 ++++++++++- .../rest-api-spec/api/ilm.explain_lifecycle.json | 4 ++++ .../rest-api-spec/api/ilm.get_lifecycle.json | 11 ++++++++++- .../rest-api-spec/api/ilm.put_lifecycle.json | 11 ++++++++++- .../main/resources/rest-api-spec/api/ilm.start.json | 11 ++++++++++- .../main/resources/rest-api-spec/api/ilm.stop.json | 11 ++++++++++- 7 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/118837.yaml diff --git a/docs/changelog/118837.yaml b/docs/changelog/118837.yaml new file mode 100644 index 0000000000000..38cd32f3a3513 --- /dev/null +++ b/docs/changelog/118837.yaml @@ -0,0 +1,5 @@ +pr: 118837 +summary: Add missing timeouts to rest-api-spec ILM APIs +area: "ILM+SLM" +type: bug +issues: [] diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.delete_lifecycle.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.delete_lifecycle.json index 2ff1031ad5c52..cd6397fb61586 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.delete_lifecycle.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.delete_lifecycle.json @@ -25,6 +25,15 @@ } ] }, - "params":{} + "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + } + } } } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.explain_lifecycle.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.explain_lifecycle.json index c793ed09281ae..94c37adb802f6 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.explain_lifecycle.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.explain_lifecycle.json @@ -33,6 +33,10 @@ "only_errors": { "type": "boolean", "description": "filters the indices included in the response to ones in an ILM error state, implies only_managed" + }, + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" } } } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.get_lifecycle.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.get_lifecycle.json index 17bf813093dd6..5abdfac7f5b30 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.get_lifecycle.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.get_lifecycle.json @@ -31,6 +31,15 @@ } ] }, - "params":{} + "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + } + } } } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.put_lifecycle.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.put_lifecycle.json index 5a12a778241b3..b7fdbe04a0ffb 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.put_lifecycle.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.put_lifecycle.json @@ -26,7 +26,16 @@ } ] }, - "params":{}, + "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + } + }, "body":{ "description":"The lifecycle policy definition to register" } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.start.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.start.json index 88b020071ab82..7141673ff9a9d 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.start.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.start.json @@ -19,6 +19,15 @@ } ] }, - "params":{} + "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + } + } } } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.stop.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.stop.json index 8401f93badfc4..962fa77263ee4 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.stop.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ilm.stop.json @@ -19,6 +19,15 @@ } ] }, - "params":{} + "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + } + } } } From 2ceade4234ab2323184cfb9ff11259542508453f Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 18 Dec 2024 08:18:43 +0400 Subject: [PATCH 055/119] Add missing timeouts to rest-api-spec ingest APIs (#118844) --- docs/changelog/118844.yaml | 5 +++++ .../rest-api-spec/api/ingest.delete_geoip_database.json | 8 ++++++++ .../api/ingest.delete_ip_location_database.json | 8 ++++++++ .../rest-api-spec/api/ingest.put_geoip_database.json | 8 ++++++++ .../api/ingest.put_ip_location_database.json | 8 ++++++++ 5 files changed, 37 insertions(+) create mode 100644 docs/changelog/118844.yaml diff --git a/docs/changelog/118844.yaml b/docs/changelog/118844.yaml new file mode 100644 index 0000000000000..f9f92bcaeb8cb --- /dev/null +++ b/docs/changelog/118844.yaml @@ -0,0 +1,5 @@ +pr: 118844 +summary: Add missing timeouts to rest-api-spec ingest APIs +area: Ingest Node +type: bug +issues: [] diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.delete_geoip_database.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.delete_geoip_database.json index fe50da720a4da..f76d328836d90 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.delete_geoip_database.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.delete_geoip_database.json @@ -26,6 +26,14 @@ ] }, "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + } } } } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.delete_ip_location_database.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.delete_ip_location_database.json index e97d1da276906..341ff5081e270 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.delete_ip_location_database.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.delete_ip_location_database.json @@ -26,6 +26,14 @@ ] }, "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + } } } } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.put_geoip_database.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.put_geoip_database.json index 6d088e3f164f4..9c2677d1f7b2f 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.put_geoip_database.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.put_geoip_database.json @@ -27,6 +27,14 @@ ] }, "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + } }, "body":{ "description":"The database configuration definition", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.put_ip_location_database.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.put_ip_location_database.json index 18487969b1a90..782048b98160a 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.put_ip_location_database.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ingest.put_ip_location_database.json @@ -27,6 +27,14 @@ ] }, "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + } }, "body":{ "description":"The database configuration definition", From a0ce7420af1bad48ffd0c5f19978b5943b4b32aa Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:30:02 +1100 Subject: [PATCH 056/119] Mute org.elasticsearch.cluster.service.MasterServiceTests testThreadContext #118914 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index b5712b22fe583..ef302671de998 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -305,6 +305,9 @@ tests: - class: org.elasticsearch.smoketest.DocsClientYamlTestSuiteIT method: test {yaml=reference/indices/shard-stores/line_150} issue: https://github.com/elastic/elasticsearch/issues/118896 +- class: org.elasticsearch.cluster.service.MasterServiceTests + method: testThreadContext + issue: https://github.com/elastic/elasticsearch/issues/118914 # Examples: # From 3f1fed0f0900e486d328c6aaede5578a3a1f9919 Mon Sep 17 00:00:00 2001 From: Carlos Delgado <6339205+carlosdelest@users.noreply.github.com> Date: Wed, 18 Dec 2024 07:50:54 +0100 Subject: [PATCH 057/119] ESQL - Remove restrictions for disjunctions in full text functions (#118544) --- docs/changelog/118544.yaml | 5 + x-pack/plugin/build.gradle | 1 + .../main/resources/match-function.csv-spec | 74 +++++++++++ .../main/resources/match-operator.csv-spec | 121 ++++++++++++++---- .../xpack/esql/plugin/MatchFunctionIT.java | 29 +++-- .../xpack/esql/plugin/MatchOperatorIT.java | 3 +- .../xpack/esql/action/EsqlCapabilities.java | 7 +- .../xpack/esql/analysis/Verifier.java | 81 ++++++++---- .../xpack/esql/analysis/VerifierTests.java | 84 ++++++------ .../LocalPhysicalPlanOptimizerTests.java | 42 ++++++ .../test/esql/180_match_operator.yml | 17 ++- 11 files changed, 364 insertions(+), 100 deletions(-) create mode 100644 docs/changelog/118544.yaml diff --git a/docs/changelog/118544.yaml b/docs/changelog/118544.yaml new file mode 100644 index 0000000000000..d59783c4e6194 --- /dev/null +++ b/docs/changelog/118544.yaml @@ -0,0 +1,5 @@ +pr: 118544 +summary: ESQL - Remove restrictions for disjunctions in full text functions +area: ES|QL +type: enhancement +issues: [] diff --git a/x-pack/plugin/build.gradle b/x-pack/plugin/build.gradle index fb37fb3575551..aa6e8de4ec27c 100644 --- a/x-pack/plugin/build.gradle +++ b/x-pack/plugin/build.gradle @@ -94,5 +94,6 @@ tasks.named("yamlRestCompatTestTransform").configure({ task -> task.skipTest("privileges/11_builtin/Test get builtin privileges" ,"unnecessary to test compatibility") task.skipTest("esql/61_enrich_ip/Invalid IP strings", "We switched from exceptions to null+warnings for ENRICH runtime errors") task.skipTest("esql/180_match_operator/match with non text field", "Match operator can now be used on non-text fields") + task.skipTest("esql/180_match_operator/match with functions", "Error message changed") }) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec index 03b24555dbeff..5ea169e1b110d 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec @@ -115,6 +115,80 @@ book_no:keyword | title:text 7140 |The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1) ; +matchWithDisjunction +required_capability: match_function +required_capability: full_text_functions_disjunctions + +from books +| where match(author, "Vonnegut") or match(author, "Guinane") +| keep book_no, author; +ignoreOrder:true + +book_no:keyword | author:text +2464 | Kurt Vonnegut +6970 | Edith Vonnegut +8956 | Kurt Vonnegut +3950 | Kurt Vonnegut +4382 | Carole Guinane +; + +matchWithDisjunctionAndFiltersConjunction +required_capability: match_function +required_capability: full_text_functions_disjunctions + +from books +| where (match(author, "Vonnegut") or match(author, "Guinane")) and year > 1997 +| keep book_no, author, year; +ignoreOrder:true + +book_no:keyword | author:text | year:integer +6970 | Edith Vonnegut | 1998 +4382 | Carole Guinane | 2001 +; + +matchWithDisjunctionAndConjunction +required_capability: match_function +required_capability: full_text_functions_disjunctions + +from books +| where (match(author, "Vonnegut") or match(author, "Marquez")) and match(description, "realism") +| keep book_no; + +book_no:keyword +4814 +; + +matchWithMoreComplexDisjunctionAndConjunction +required_capability: match_function +required_capability: full_text_functions_disjunctions + +from books +| where (match(author, "Vonnegut") and match(description, "charming")) or (match(author, "Marquez") and match(description, "realism")) +| keep book_no; +ignoreOrder:true + +book_no:keyword +6970 +4814 +; + +matchWithDisjunctionIncludingConjunction +required_capability: match_function +required_capability: full_text_functions_disjunctions + +from books +| where match(author, "Vonnegut") or (match(author, "Marquez") and match(description, "realism")) +| keep book_no; +ignoreOrder:true + +book_no:keyword +2464 +6970 +4814 +8956 +3950 +; + matchWithFunctionPushedToLucene required_capability: match_function diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec index 56f7f5ccd8823..7906f8b69162b 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec @@ -102,6 +102,81 @@ book_no:keyword | title:text 7140 |The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1) ; + +matchWithDisjunction +required_capability: match_operator_colon +required_capability: full_text_functions_disjunctions + +from books +| where author : "Vonnegut" or author : "Guinane" +| keep book_no, author; +ignoreOrder:true + +book_no:keyword | author:text +2464 | Kurt Vonnegut +6970 | Edith Vonnegut +8956 | Kurt Vonnegut +3950 | Kurt Vonnegut +4382 | Carole Guinane +; + +matchWithDisjunctionAndFiltersConjunction +required_capability: match_operator_colon +required_capability: full_text_functions_disjunctions + +from books +| where (author : "Vonnegut" or author : "Guinane") and year > 1997 +| keep book_no, author, year; +ignoreOrder:true + +book_no:keyword | author:text | year:integer +6970 | Edith Vonnegut | 1998 +4382 | Carole Guinane | 2001 +; + +matchWithDisjunctionAndConjunction +required_capability: match_operator_colon +required_capability: full_text_functions_disjunctions + +from books +| where (author : "Vonnegut" or author : "Marquez") and description : "realism" +| keep book_no; + +book_no:keyword +4814 +; + +matchWithMoreComplexDisjunctionAndConjunction +required_capability: match_function +required_capability: full_text_functions_disjunctions + +from books +| where (author : "Vonnegut" and description : "charming") or (author : "Marquez" and description : "realism") +| keep book_no; +ignoreOrder:true + +book_no:keyword +6970 +4814 +; + +matchWithDisjunctionIncludingConjunction +required_capability: match_operator_colon +required_capability: full_text_functions_disjunctions + +from books +| where author : "Vonnegut" or (author : "Marquez" and description : "realism") +| keep book_no; +ignoreOrder:true + +book_no:keyword +2464 +6970 +4814 +8956 +3950 +; + matchWithFunctionPushedToLucene required_capability: match_operator_colon @@ -219,7 +294,7 @@ count(*): long | author.keyword:keyword ; testMatchBooleanField -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees @@ -235,7 +310,7 @@ Amabile | true | 2.09 ; testMatchIntegerField -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees @@ -247,7 +322,7 @@ emp_no:integer | first_name:keyword ; testMatchDoubleField -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees @@ -259,7 +334,7 @@ emp_no:integer | salary_change:double ; testMatchLongField -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from date_nanos @@ -271,7 +346,7 @@ num:long ; testMatchUnsignedLongField -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from ul_logs @@ -283,7 +358,7 @@ bytes_out:unsigned_long ; testMatchIpFieldAsString -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from sample_data @@ -295,7 +370,7 @@ client_ip:ip | message:keyword ; testMatchDateFieldAsString -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from date_nanos @@ -307,7 +382,7 @@ millis:date ; testMatchDateNanosFieldAsString -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from date_nanos @@ -319,7 +394,7 @@ nanos:date_nanos ; testMatchBooleanFieldAsString -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees @@ -335,7 +410,7 @@ Amabile | true | 2.09 ; testMatchIntegerFieldAsString -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees @@ -347,7 +422,7 @@ emp_no:integer | first_name:keyword ; testMatchDoubleFieldAsString -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees @@ -359,7 +434,7 @@ emp_no:integer | salary_change:double ; testMatchLongFieldAsString -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from date_nanos @@ -371,7 +446,7 @@ num:long ; testMatchUnsignedLongFieldAsString -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from ul_logs @@ -383,7 +458,7 @@ bytes_out:unsigned_long ; testMatchVersionFieldAsString -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from apps @@ -395,7 +470,7 @@ bbbbb | 2.1 ; testMatchIntegerAsDouble -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees @@ -408,7 +483,7 @@ emp_no:integer | first_name:keyword ; testMatchDoubleAsIntegerField -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees @@ -423,7 +498,7 @@ emp_no:integer | height:double ; testMatchMultipleFieldTypes -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees,employees_incompatible @@ -440,7 +515,7 @@ emp_as_int:integer | name_as_kw:keyword testMatchMultipleFieldTypesKeywordText -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees,employees_incompatible @@ -455,7 +530,7 @@ Kazuhito ; testMatchMultipleFieldTypesDoubleFloat -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees,employees_incompatible @@ -474,7 +549,7 @@ emp_no:integer | height_dbl:double ; testMatchMultipleFieldTypesBooleanKeyword -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees,employees_incompatible @@ -491,7 +566,7 @@ true ; testMatchMultipleFieldTypesLongUnsignedLong -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees,employees_incompatible @@ -506,7 +581,7 @@ avg_worked_seconds_ul:unsigned_long ; testMatchMultipleFieldTypesDateNanosDate -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees,employees_incompatible @@ -521,7 +596,7 @@ hire_date_nanos:date_nanos ; testMatchWithWrongFieldValue -required_capability: match_function +required_capability: match_operator_colon required_capability: match_additional_types from employees,employees_incompatible diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java index 58b1652653ca3..ad90bbf6ae9db 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java @@ -168,7 +168,7 @@ public void testWhereMatchWithScoringNoSort() { var query = """ FROM test METADATA _score - | WHERE content:"fox" + | WHERE match(content, "fox") | KEEP id, _score """; @@ -182,7 +182,7 @@ public void testWhereMatchWithScoringNoSort() { public void testNonExistingColumn() { var query = """ FROM test - | WHERE something:"fox" + | WHERE match(something, "fox") """; var error = expectThrows(VerificationException.class, () -> run(query)); @@ -193,14 +193,14 @@ public void testWhereMatchEvalColumn() { var query = """ FROM test | EVAL upper_content = to_upper(content) - | WHERE upper_content:"FOX" + | WHERE match(upper_content, "FOX") | KEEP id """; var error = expectThrows(VerificationException.class, () -> run(query)); assertThat( error.getMessage(), - containsString("[:] operator cannot operate on [upper_content], which is not a field from an index mapping") + containsString("[MATCH] function cannot operate on [upper_content], which is not a field from an index mapping") ); } @@ -209,13 +209,13 @@ public void testWhereMatchOverWrittenColumn() { FROM test | DROP content | EVAL content = CONCAT("document with ID ", to_str(id)) - | WHERE content:"document" + | WHERE match(content, "document") """; var error = expectThrows(VerificationException.class, () -> run(query)); assertThat( error.getMessage(), - containsString("[:] operator cannot operate on [content], which is not a field from an index mapping") + containsString("[MATCH] function cannot operate on [content], which is not a field from an index mapping") ); } @@ -223,7 +223,7 @@ public void testWhereMatchAfterStats() { var query = """ FROM test | STATS count(*) - | WHERE content:"fox" + | WHERE match(content, "fox") """; var error = expectThrows(VerificationException.class, () -> run(query)); @@ -233,14 +233,15 @@ public void testWhereMatchAfterStats() { public void testWhereMatchWithFunctions() { var query = """ FROM test - | WHERE content:"fox" OR to_upper(content) == "FOX" + | WHERE match(content, "fox") OR to_upper(content) == "FOX" """; var error = expectThrows(ElasticsearchException.class, () -> run(query)); assertThat( error.getMessage(), containsString( - "Invalid condition [content:\"fox\" OR to_upper(content) == \"FOX\"]. " - + "[:] operator can't be used as part of an or condition" + "Invalid condition [match(content, \"fox\") OR to_upper(content) == \"FOX\"]. " + + "Full text functions can be used in an OR condition," + + " but only if just full text functions are used in the OR condition" ) ); } @@ -248,24 +249,24 @@ public void testWhereMatchWithFunctions() { public void testWhereMatchWithRow() { var query = """ ROW content = "a brown fox" - | WHERE content:"fox" + | WHERE match(content, "fox") """; var error = expectThrows(ElasticsearchException.class, () -> run(query)); assertThat( error.getMessage(), - containsString("[:] operator cannot operate on [\"a brown fox\"], which is not a field from an index mapping") + containsString("[MATCH] function cannot operate on [\"a brown fox\"], which is not a field from an index mapping") ); } public void testMatchWithinEval() { var query = """ FROM test - | EVAL matches_query = content:"fox" + | EVAL matches_query = match(content, "fox") """; var error = expectThrows(VerificationException.class, () -> run(query)); - assertThat(error.getMessage(), containsString("[:] operator is only supported in WHERE commands")); + assertThat(error.getMessage(), containsString("[MATCH] function is only supported in WHERE commands")); } private void createAndPopulateIndex() { diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchOperatorIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchOperatorIT.java index d0a641f086fe4..758878b46d51f 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchOperatorIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchOperatorIT.java @@ -216,7 +216,8 @@ public void testWhereMatchWithFunctions() { error.getMessage(), containsString( "Invalid condition [content:\"fox\" OR to_upper(content) == \"FOX\"]. " - + "[:] operator can't be used as part of an or condition" + + "Full text functions can be used in an OR condition, " + + "but only if just full text functions are used in the OR condition" ) ); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index f766beb76dd3d..8c7e381d33322 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -585,7 +585,12 @@ public enum Cap { /** * Fix for regex folding with case-insensitive pattern https://github.com/elastic/elasticsearch/issues/118371 */ - FIXED_REGEX_FOLD; + FIXED_REGEX_FOLD, + + /** + * Full text functions can be used in disjunctions + */ + FULL_TEXT_FUNCTIONS_DISJUNCTIONS; private final boolean enabled; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java index f01cc265e330b..6b98b7d69834f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java @@ -766,41 +766,78 @@ private static void checkRemoteEnrich(LogicalPlan plan, Set failures) { } /** - * Checks whether a condition contains a disjunction with the specified typeToken. Adds to failure if it does. + * Checks whether a condition contains a disjunction with a full text search. + * If it does, check that every element of the disjunction is a full text search or combinations (AND, OR, NOT) of them. + * If not, add a failure to the failures collection. * - * @param condition condition to check for disjunctions + * @param condition condition to check for disjunctions of full text searches * @param typeNameProvider provider for the type name to add in the failure message * @param failures failures collection to add to */ - private static void checkNotPresentInDisjunctions( + private static void checkFullTextSearchDisjunctions( Expression condition, java.util.function.Function typeNameProvider, Set failures ) { - condition.forEachUp(Or.class, or -> { - checkNotPresentInDisjunctions(or.left(), or, typeNameProvider, failures); - checkNotPresentInDisjunctions(or.right(), or, typeNameProvider, failures); + int failuresCount = failures.size(); + condition.forEachDown(Or.class, or -> { + if (failures.size() > failuresCount) { + // Exit early if we already have a failures + return; + } + boolean hasFullText = or.anyMatch(FullTextFunction.class::isInstance); + if (hasFullText) { + boolean hasOnlyFullText = onlyFullTextFunctionsInExpression(or); + if (hasOnlyFullText == false) { + failures.add( + fail( + or, + "Invalid condition [{}]. Full text functions can be used in an OR condition, " + + "but only if just full text functions are used in the OR condition", + or.sourceText() + ) + ); + } + } }); } /** - * Checks whether a condition contains a disjunction with the specified typeToken. Adds to failure if it does. + * Checks whether an expression contains just full text functions or negations (NOT) and combinations (AND, OR) of full text functions * - * @param parentExpression parent expression to add to the failure message - * @param or disjunction that is being checked - * @param failures failures collection to add to + * @param expression expression to check + * @return true if all children are full text functions or negations of full text functions, false otherwise */ - private static void checkNotPresentInDisjunctions( - Expression parentExpression, - Or or, - java.util.function.Function elementName, - Set failures - ) { - parentExpression.forEachDown(FullTextFunction.class, ftp -> { - failures.add( - fail(or, "Invalid condition [{}]. {} can't be used as part of an or condition", or.sourceText(), elementName.apply(ftp)) - ); - }); + private static boolean onlyFullTextFunctionsInExpression(Expression expression) { + if (expression instanceof FullTextFunction) { + return true; + } else if (expression instanceof Not) { + return onlyFullTextFunctionsInExpression(expression.children().get(0)); + } else if (expression instanceof BinaryLogic binaryLogic) { + return onlyFullTextFunctionsInExpression(binaryLogic.left()) && onlyFullTextFunctionsInExpression(binaryLogic.right()); + } + + return false; + } + + /** + * Checks whether an expression contains a full text function as part of it + * + * @param expression expression to check + * @return true if the expression or any of its children is a full text function, false otherwise + */ + private static boolean anyFullTextFunctionsInExpression(Expression expression) { + if (expression instanceof FullTextFunction) { + return true; + } + + for (Expression child : expression.children()) { + if (anyFullTextFunctionsInExpression(child)) { + return true; + } + } + + return false; } /** @@ -870,7 +907,7 @@ private static void checkFullTextQueryFunctions(LogicalPlan plan, Set f m -> "[" + m.functionName() + "] " + m.functionType(), failures ); - checkNotPresentInDisjunctions(condition, ftf -> "[" + ftf.functionName() + "] " + ftf.functionType(), failures); + checkFullTextSearchDisjunctions(condition, ftf -> "[" + ftf.functionName() + "] " + ftf.functionType(), failures); checkFullTextFunctionsParents(condition, failures); } else { plan.forEachExpression(FullTextFunction.class, ftf -> { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 182e87d1ab9dd..a1e29117a25d3 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1166,12 +1166,14 @@ public void testMatchInsideEval() throws Exception { public void testMatchFilter() throws Exception { assertEquals( "1:19: Invalid condition [first_name:\"Anna\" or starts_with(first_name, \"Anne\")]. " - + "[:] operator can't be used as part of an or condition", + + "Full text functions can be used in an OR condition, " + + "but only if just full text functions are used in the OR condition", error("from test | where first_name:\"Anna\" or starts_with(first_name, \"Anne\")") ); assertEquals( - "1:51: Invalid condition [first_name:\"Anna\" OR new_salary > 100]. " + "[:] operator can't be used as part of an or condition", + "1:51: Invalid condition [first_name:\"Anna\" OR new_salary > 100]. Full text functions can be" + + " used in an OR condition, but only if just full text functions are used in the OR condition", error("from test | eval new_salary = salary + 10 | where first_name:\"Anna\" OR new_salary > 100") ); } @@ -1409,48 +1411,56 @@ public void testMatchOperatorWithDisjunctions() { } private void checkWithDisjunctions(String functionName, String functionInvocation, String functionType) { + String expression = functionInvocation + " or length(first_name) > 12"; + checkdisjunctionError("1:19", expression, functionName, functionType); + expression = "(" + functionInvocation + " or first_name is not null) or (length(first_name) > 12 and match(last_name, \"Smith\"))"; + checkdisjunctionError("1:19", expression, functionName, functionType); + expression = functionInvocation + " or (last_name is not null and first_name is null)"; + checkdisjunctionError("1:19", expression, functionName, functionType); + } + + private void checkdisjunctionError(String position, String expression, String functionName, String functionType) { assertEquals( LoggerMessageFormat.format( null, - "1:19: Invalid condition [{} or length(first_name) > 12]. " - + "[{}] " - + functionType - + " can't be used as part of an or condition", - functionInvocation, - functionName - ), - error("from test | where " + functionInvocation + " or length(first_name) > 12") - ); - assertEquals( - LoggerMessageFormat.format( - null, - "1:19: Invalid condition [({} and first_name is not null) or (length(first_name) > 12 and first_name is null)]. " - + "[{}] " - + functionType - + " can't be used as part of an or condition", - functionInvocation, - functionName - ), - error( - "from test | where (" - + functionInvocation - + " and first_name is not null) or (length(first_name) > 12 and first_name is null)" - ) - ); - assertEquals( - LoggerMessageFormat.format( - null, - "1:19: Invalid condition [({} and first_name is not null) or first_name is null]. " - + "[{}] " - + functionType - + " can't be used as part of an or condition", - functionInvocation, - functionName + "{}: Invalid condition [{}]. Full text functions can be used in an OR condition, " + + "but only if just full text functions are used in the OR condition", + position, + expression ), - error("from test | where (" + functionInvocation + " and first_name is not null) or first_name is null") + error("from test | where " + expression) ); } + public void testFullTextFunctionsDisjunctions() { + checkWithFullTextFunctionsDisjunctions("MATCH", "match(last_name, \"Smith\")", "function"); + checkWithFullTextFunctionsDisjunctions(":", "last_name : \"Smith\"", "operator"); + checkWithFullTextFunctionsDisjunctions("QSTR", "qstr(\"last_name: Smith\")", "function"); + + assumeTrue("KQL function capability not available", EsqlCapabilities.Cap.KQL_FUNCTION.isEnabled()); + checkWithFullTextFunctionsDisjunctions("KQL", "kql(\"last_name: Smith\")", "function"); + } + + private void checkWithFullTextFunctionsDisjunctions(String functionName, String functionInvocation, String functionType) { + + String expression = functionInvocation + " or length(first_name) > 10"; + checkdisjunctionError("1:19", expression, functionName, functionType); + + expression = "match(last_name, \"Anneke\") or (" + functionInvocation + " and length(first_name) > 10)"; + checkdisjunctionError("1:19", expression, functionName, functionType); + + expression = "(" + + functionInvocation + + " and length(first_name) > 0) or (match(last_name, \"Anneke\") and length(first_name) > 10)"; + checkdisjunctionError("1:19", expression, functionName, functionType); + + query("from test | where " + functionInvocation + " or match(first_name, \"Anna\")"); + query("from test | where " + functionInvocation + " or not match(first_name, \"Anna\")"); + query("from test | where (" + functionInvocation + " or match(first_name, \"Anna\")) and length(first_name) > 10"); + query("from test | where (" + functionInvocation + " or match(first_name, \"Anna\")) and match(last_name, \"Smith\")"); + query("from test | where " + functionInvocation + " or (match(first_name, \"Anna\") and match(last_name, \"Smith\"))"); + } + public void testQueryStringFunctionWithNonBooleanFunctions() { checkFullTextFunctionsWithNonBooleanFunctions("QSTR", "qstr(\"first_name: Anna\")", "function"); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 879a413615202..406e27c1517e5 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.query.QueryStringQueryBuilder; import org.elasticsearch.index.query.RangeQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.license.XPackLicenseState; @@ -57,6 +58,7 @@ import org.elasticsearch.xpack.esql.plan.physical.EvalExec; import org.elasticsearch.xpack.esql.plan.physical.ExchangeExec; import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec; +import org.elasticsearch.xpack.esql.plan.physical.FilterExec; import org.elasticsearch.xpack.esql.plan.physical.LimitExec; import org.elasticsearch.xpack.esql.plan.physical.LocalSourceExec; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; @@ -1543,6 +1545,46 @@ public void testMultipleMatchFilterPushdown() { assertThat(actualLuceneQuery.toString(), is(expectedLuceneQuery.toString())); } + public void testFullTextFunctionsDisjunctionPushdown() { + String query = """ + from test + | where (match(first_name, "Anna") or qstr("first_name: Anneke")) and last_name: "Smith" + | sort emp_no + """; + var plan = plannerOptimizer.plan(query); + var topNExec = as(plan, TopNExec.class); + var exchange = as(topNExec.child(), ExchangeExec.class); + var project = as(exchange.child(), ProjectExec.class); + var fieldExtract = as(project.child(), FieldExtractExec.class); + var actualLuceneQuery = as(fieldExtract.child(), EsQueryExec.class).query(); + var expectedLuceneQuery = new BoolQueryBuilder().must( + new BoolQueryBuilder().should(new MatchQueryBuilder("first_name", "Anna").lenient(true)) + .should(new QueryStringQueryBuilder("first_name: Anneke")) + ).must(new MatchQueryBuilder("last_name", "Smith").lenient(true)); + assertThat(actualLuceneQuery.toString(), is(expectedLuceneQuery.toString())); + } + + public void testFullTextFunctionsDisjunctionWithFiltersPushdown() { + String query = """ + from test + | where (first_name:"Anna" or first_name:"Anneke") and length(last_name) > 5 + | sort emp_no + """; + var plan = plannerOptimizer.plan(query); + var topNExec = as(plan, TopNExec.class); + var exchange = as(topNExec.child(), ExchangeExec.class); + var project = as(exchange.child(), ProjectExec.class); + var fieldExtract = as(project.child(), FieldExtractExec.class); + var secondTopNExec = as(fieldExtract.child(), TopNExec.class); + var secondFieldExtract = as(secondTopNExec.child(), FieldExtractExec.class); + var filterExec = as(secondFieldExtract.child(), FilterExec.class); + var thirdFilterExtract = as(filterExec.child(), FieldExtractExec.class); + var actualLuceneQuery = as(thirdFilterExtract.child(), EsQueryExec.class).query(); + var expectedLuceneQuery = new BoolQueryBuilder().should(new MatchQueryBuilder("first_name", "Anna").lenient(true)) + .should(new MatchQueryBuilder("first_name", "Anneke").lenient(true)); + assertThat(actualLuceneQuery.toString(), is(expectedLuceneQuery.toString())); + } + /** * Expecting * LimitExec[1000[INTEGER]] diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/180_match_operator.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/180_match_operator.yml index 663c0dc78acb3..118783b412d48 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/180_match_operator.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/180_match_operator.yml @@ -170,7 +170,7 @@ setup: - match: { error.reason: "Found 1 problem\nline 1:36: Unknown column [content], did you mean [count(*)]?" } --- -"match with functions": +"match with disjunctions": - do: catch: bad_request allowed_warnings_regex: @@ -181,7 +181,20 @@ setup: - match: { status: 400 } - match: { error.type: verification_exception } - - match: { error.reason: "Found 1 problem\nline 1:19: Invalid condition [content:\"fox\" OR to_upper(content) == \"FOX\"]. [:] operator can't be used as part of an or condition" } + - match: { error.reason: "/.+Invalid\\ condition\\ \\[content\\:\"fox\"\\ OR\\ to_upper\\(content\\)\\ ==\\ \"FOX\"\\]\\./" } + + - do: + catch: bad_request + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'FROM test | WHERE content:"fox" OR to_upper(content) == "FOX"' + + - match: { status: 400 } + - match: { error.type: verification_exception } + - match: { error.reason: "/.+Invalid\\ condition\\ \\[content\\:\"fox\"\\ OR\\ to_upper\\(content\\)\\ ==\\ \"FOX\"\\]\\./" } + --- "match within eval": From d118cbf529ec4da9f9d7378cb48e4355ee61c9ab Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Wed, 18 Dec 2024 09:22:08 +0100 Subject: [PATCH 058/119] [Build] Make ThirdPartyAudit task uptodate more effective (#118723) We should basically be able to skip the third party dependency audit tasks if no third party dependency has changed. Filtering out the project dependencies allows way better uptodate and caching behaviour. We are only interested in thirdparty libs anyhow in this context. A positive side effect of the reduced class path is a quicker execution of the task --- .../ThirdPartyAuditPrecommitPlugin.java | 9 +++-- .../precommit/ThirdPartyAuditTask.java | 15 ++------ .../internal/util/DependenciesUtils.java | 36 ++++++++++++++++--- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditPrecommitPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditPrecommitPlugin.java index e45a1d3dd25b1..7046a22204efa 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditPrecommitPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditPrecommitPlugin.java @@ -16,12 +16,14 @@ import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.TaskProvider; import java.io.File; import java.nio.file.Path; import static org.elasticsearch.gradle.internal.util.DependenciesUtils.createFileCollectionFromNonTransitiveArtifactsView; +import static org.elasticsearch.gradle.internal.util.DependenciesUtils.thirdPartyDependenciesView; import static org.elasticsearch.gradle.internal.util.ParamsUtils.loadBuildParams; public class ThirdPartyAuditPrecommitPlugin extends PrecommitPlugin { @@ -47,7 +49,6 @@ public TaskProvider createTask(Project project) { project.getDependencies().add(JDK_JAR_HELL_CONFIG_NAME, elasticsearchCoreProject); } } - TaskProvider resourcesTask = project.getTasks() .register("thirdPartyAuditResources", ExportElasticsearchBuildResourcesTask.class); Path resourcesDir = project.getBuildDir().toPath().resolve("third-party-audit-config"); @@ -59,9 +60,11 @@ public TaskProvider createTask(Project project) { // usually only one task is created. but this construct makes our integTests easier to setup project.getTasks().withType(ThirdPartyAuditTask.class).configureEach(t -> { Configuration runtimeConfiguration = project.getConfigurations().getByName("runtimeClasspath"); + FileCollection runtimeThirdParty = thirdPartyDependenciesView(runtimeConfiguration); Configuration compileOnly = project.getConfigurations() .getByName(CompileOnlyResolvePlugin.RESOLVEABLE_COMPILE_ONLY_CONFIGURATION_NAME); - t.setClasspath(runtimeConfiguration.plus(compileOnly)); + FileCollection compileOnlyThirdParty = thirdPartyDependenciesView(compileOnly); + t.getThirdPartyClasspath().from(runtimeThirdParty, compileOnlyThirdParty); t.getJarsToScan() .from( createFileCollectionFromNonTransitiveArtifactsView( @@ -78,7 +81,7 @@ public TaskProvider createTask(Project project) { t.getJavaHome().set(buildParams.flatMap(params -> params.getRuntimeJavaHome()).map(File::getPath)); t.setSignatureFile(resourcesDir.resolve("forbidden/third-party-audit.txt").toFile()); t.getJdkJarHellClasspath().from(jdkJarHellConfig); - t.getForbiddenAPIsClasspath().from(project.getConfigurations().getByName("forbiddenApisCliJar").plus(compileOnly)); + t.getForbiddenAPIsClasspath().from(project.getConfigurations().getByName("forbiddenApisCliJar").plus(compileOnlyThirdParty)); }); return audit; } diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditTask.java index 442797775de2f..59ba9bae0a57d 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditTask.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditTask.java @@ -17,7 +17,6 @@ import org.gradle.api.JavaVersion; import org.gradle.api.file.ArchiveOperations; import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileSystemOperations; import org.gradle.api.file.FileTree; import org.gradle.api.file.ProjectLayout; @@ -96,8 +95,6 @@ public abstract class ThirdPartyAuditTask extends DefaultTask { private final ProjectLayout projectLayout; - private FileCollection classpath; - @Inject public ThirdPartyAuditTask( ArchiveOperations archiveOperations, @@ -198,9 +195,7 @@ public Set getMissingClassExcludes() { public abstract Property getRuntimeJavaVersion(); @Classpath - public FileCollection getClasspath() { - return classpath; - } + public abstract ConfigurableFileCollection getThirdPartyClasspath(); @TaskAction public void runThirdPartyAudit() throws IOException { @@ -345,7 +340,7 @@ private String runForbiddenAPIsCli() throws IOException { if (javaHome.isPresent()) { spec.setExecutable(javaHome.get() + "/bin/java"); } - spec.classpath(getForbiddenAPIsClasspath(), classpath); + spec.classpath(getForbiddenAPIsClasspath(), getThirdPartyClasspath()); // Enable explicitly for each release as appropriate. Just JDK 20/21/22/23 for now, and just the vector module. if (isJavaVersion(VERSION_20) || isJavaVersion(VERSION_21) || isJavaVersion(VERSION_22) || isJavaVersion(VERSION_23)) { spec.jvmArgs("--add-modules", "jdk.incubator.vector"); @@ -383,7 +378,7 @@ private boolean isJavaVersion(JavaVersion version) { private Set runJdkJarHellCheck() throws IOException { ByteArrayOutputStream standardOut = new ByteArrayOutputStream(); ExecResult execResult = execOperations.javaexec(spec -> { - spec.classpath(getJdkJarHellClasspath(), classpath); + spec.classpath(getJdkJarHellClasspath(), getThirdPartyClasspath()); spec.getMainClass().set(JDK_JAR_HELL_MAIN_CLASS); spec.args(getJarExpandDir()); spec.setIgnoreExitValue(true); @@ -402,8 +397,4 @@ private Set runJdkJarHellCheck() throws IOException { return new TreeSet<>(Arrays.asList(jdkJarHellCheckList.split("\\r?\\n"))); } - public void setClasspath(FileCollection classpath) { - this.classpath = classpath; - } - } diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/util/DependenciesUtils.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/util/DependenciesUtils.java index 9080f62f19937..5d7386e2c2150 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/util/DependenciesUtils.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/util/DependenciesUtils.java @@ -9,12 +9,16 @@ package org.elasticsearch.gradle.internal.util; +import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin; + import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ResolvableDependencies; import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.gradle.api.artifacts.result.ResolvedComponentResult; import org.gradle.api.artifacts.result.ResolvedDependencyResult; import org.gradle.api.file.FileCollection; +import org.gradle.api.provider.Provider; import org.gradle.api.specs.AndSpec; import org.gradle.api.specs.Spec; @@ -29,7 +33,7 @@ public static FileCollection createFileCollectionFromNonTransitiveArtifactsView( ) { ResolvableDependencies incoming = configuration.getIncoming(); return incoming.artifactView(viewConfiguration -> { - Set firstLevelDependencyComponents = incoming.getResolutionResult() + Provider> firstLevelDependencyComponents = incoming.getResolutionResult() .getRootComponent() .map( rootComponent -> rootComponent.getDependencies() @@ -39,12 +43,36 @@ public static FileCollection createFileCollectionFromNonTransitiveArtifactsView( .filter(dependency -> dependency.getSelected() instanceof ResolvedComponentResult) .map(dependency -> dependency.getSelected().getId()) .collect(Collectors.toSet()) - ) - .get(); + ); viewConfiguration.componentFilter( - new AndSpec<>(identifier -> firstLevelDependencyComponents.contains(identifier), componentFilter) + new AndSpec<>(identifier -> firstLevelDependencyComponents.get().contains(identifier), componentFilter) ); }).getFiles(); } + /** + * This method gives us an artifact view of a configuration that filters out all + * project dependencies that are not shadowed jars. + * Basically a thirdparty only view of the dependency tree. + */ + public static FileCollection thirdPartyDependenciesView(Configuration configuration) { + ResolvableDependencies incoming = configuration.getIncoming(); + return incoming.artifactView(v -> { + // resolve componentIdentifier for all shadowed project dependencies + Provider> shadowedDependencies = incoming.getResolutionResult() + .getRootComponent() + .map( + root -> root.getDependencies() + .stream() + .filter(dep -> dep instanceof ResolvedDependencyResult) + .map(dep -> (ResolvedDependencyResult) dep) + .filter(dep -> dep.getResolvedVariant().getDisplayName() == ShadowBasePlugin.COMPONENT_NAME) + .filter(dep -> dep.getSelected() instanceof ResolvedComponentResult) + .map(dep -> dep.getSelected().getId()) + .collect(Collectors.toSet()) + ); + // filter out project dependencies if they are not a shadowed dependency + v.componentFilter(i -> (i instanceof ProjectComponentIdentifier == false || shadowedDependencies.get().contains(i))); + }).getFiles(); + } } From 140beb11849b42ceae311b34f2865f3db68df578 Mon Sep 17 00:00:00 2001 From: Tommaso Teofili Date: Wed, 18 Dec 2024 09:42:51 +0100 Subject: [PATCH 059/119] Make ES|QL scoring tests running consistently in serverless (#118854) * make scoring tests running consistently in serverless enforce _id in books dataset to make doc distribution across shards consistent. books settings with 3 shards to accomodate serverless settings. unmuting scoring tests. scoring test fixed. --- muted-tests.yml | 18 -- .../xpack/esql/CsvTestsDataLoader.java | 2 +- .../src/main/resources/books-settings.json | 5 + .../testFixtures/src/main/resources/books.csv | 160 +++++++++--------- .../src/main/resources/scoring.csv-spec | 104 ++++++------ 5 files changed, 140 insertions(+), 149 deletions(-) create mode 100644 x-pack/plugin/esql/qa/testFixtures/src/main/resources/books-settings.json diff --git a/muted-tests.yml b/muted-tests.yml index ef302671de998..2677b3e75d86e 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -153,24 +153,6 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/117473 - class: org.elasticsearch.repositories.s3.RepositoryS3EcsClientYamlTestSuiteIT issue: https://github.com/elastic/elasticsearch/issues/117525 -- class: "org.elasticsearch.xpack.esql.qa.multi_node.EsqlSpecIT" - method: "test {scoring.*}" - issue: https://github.com/elastic/elasticsearch/issues/117641 -- class: "org.elasticsearch.xpack.esql.qa.single_node.EsqlSpecIT" - method: "test {scoring.*}" - issue: https://github.com/elastic/elasticsearch/issues/117641 -- class: "org.elasticsearch.xpack.esql.qa.mixed.MixedClusterEsqlSpecIT" - method: "test {scoring.*}" - issue: https://github.com/elastic/elasticsearch/issues/117641 -- class: "org.elasticsearch.xpack.esql.qa.mixed.MultiClusterEsqlSpecIT" - method: "test {scoring.*}" - issue: https://github.com/elastic/elasticsearch/issues/118460 -- class: "org.elasticsearch.xpack.esql.ccq.MultiClusterSpecIT" - method: "test {scoring.*}" - issue: https://github.com/elastic/elasticsearch/issues/118460 -- class: org.elasticsearch.xpack.esql.ccq.MultiClusterSpecIT - method: test {scoring.QstrWithFieldAndScoringSortedEval} - issue: https://github.com/elastic/elasticsearch/issues/117751 - class: org.elasticsearch.search.ccs.CrossClusterIT method: testCancel issue: https://github.com/elastic/elasticsearch/issues/108061 diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java index abfe90f80e372..8e81d14b4dfd7 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java @@ -104,7 +104,7 @@ public class CsvTestsDataLoader { private static final TestsDataset DISTANCES = new TestsDataset("distances"); private static final TestsDataset K8S = new TestsDataset("k8s", "k8s-mappings.json", "k8s.csv").withSetting("k8s-settings.json"); private static final TestsDataset ADDRESSES = new TestsDataset("addresses"); - private static final TestsDataset BOOKS = new TestsDataset("books"); + private static final TestsDataset BOOKS = new TestsDataset("books").withSetting("books-settings.json"); private static final TestsDataset SEMANTIC_TEXT = new TestsDataset("semantic_text").withInferenceEndpoint(true); public static final Map CSV_DATASET_MAP = Map.ofEntries( diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/books-settings.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/books-settings.json new file mode 100644 index 0000000000000..b324c27b40653 --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/books-settings.json @@ -0,0 +1,5 @@ +{ + "index": { + "number_of_shards": 3 + } +} diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/books.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/books.csv index 1deefaa3c6475..1cb01687e6511 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/books.csv +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/books.csv @@ -1,80 +1,80 @@ -book_no:keyword,title:text,author:text,year:integer,publisher:text,ratings:float,description:text -2924,A Gentle Creature and Other Stories: White Nights\, A Gentle Creature\, and The Dream of a Ridiculous Man (The World's Classics),[Fyodor Dostoevsky, Alan Myers, W. J. Leatherbarrow],2009,Oxford Paperbacks,4.00,In these stories Dostoevsky explores both the figure of the dreamer divorced from reality and also his own ambiguous attitude to utopianism\, themes central to many of his great novels. This new translation captures the power and lyricism of Dostoevsky's writing\, while the introduction examines the stories in relation to one another and to his novels. -7670,A Middle English Reader and Vocabulary,[Kenneth Sisam, J. R. R. Tolkien],2011,Courier Corporation,4.33,This highly respected anthology of medieval English literature features poetry\, prose and popular tales from Arthurian legend and classical mythology. Includes notes on each extract\, appendices\, and an extensive glossary by J. R. R. Tolkien. -7381,A Psychic in the Heartland: The Extraordinary Experiences of a Small Town Doctor,Bettilu Stein Faulkner,2003,Red Wheel/Weiser,4.50,The true story of a small-town doctor destined to live his life along two paths: one as a successful physician\, the other as a psychic with ever more interesting adventures. Experiencing a wide range of spiritual phenomena\, Dr. Riblet Hout learned about the connection between the healer and the healed\, our individual missions on earth\, free will\, and our relationship with God. He also paints a vivid picture of life on the other side as well as the moment of transition from physical life to afterlife. -2883,A Summer of Faulkner: As I Lay Dying/The Sound and the Fury/Light in August (Oprah's Book Club),William Faulkner,2005,Vintage Books,3.89,Presents three novels\, including As I Lay Dying\, in which the Bundren family journeys across Mississippi to bury their mother\, The Sound and the Fury\, in which Caddy Compson's story is narrated by her three brothers\, and Light in August\, in which th -4023,A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings,[Walter Scheps, Agnes Perkins, Charles Adolph Huttar, John Ronald Reuel Tolkien],1975,Open Court Publishing,4.67,The structure\, content\, and character of Tolkien's The Hobbit and The Lord of the Rings are dealt with in ten critical essays. -2382,A Wizard of Earthsea (Earthsea Trilogy Ser.),Ursula K. Le Guin,1991,Atheneum Books for Young Readers,4.01,A boy grows to manhood while attempting to subdue the evil he unleashed on the world as an apprentice to the Master Wizard. -7541,A Writer's Diary (Volume 1: 1873-1876),Fyodor Dostoevsky,1997,Northwestern University Press,4.50,Winner of the AATSEEL Outstanding Translation Award This is the first paperback edition of the complete collection of writings that has been called Dostoevsky's boldest experiment with literary form\, it is a uniquely encyclopedic forum of fictional and nonfictional genres. The Diary's radical format was matched by the extreme range of its contents. In a single frame it incorporated an astonishing variety of material: short stories\, humorous sketches\, reports on sensational crimes\, historical predictions\, portraits of famous people\, autobiographical pieces\, and plans for stories\, some of which were never written while others appeared in the Diary itself. -7400,Anna Karenina: Television Tie-In Edition (Signet classics),[Leo Tolstoy, SBP Editors],2019,Samaira Book Publishers,4.45,The Russian novelist and moral philosopher Leo Tolstoy (1828-1910) ranks as one of the world s great writers\, and his 'War and Peace' has been called the greatest novel ever written. But during his long lifetime\, Tolstoy also wrote enough shorter works to fill many volumes. The message in all his stories is presented with such humour that the reader hardly realises that it is strongly didactic. These stories give a snapshot of Russia and its people in the late nineteenth century. -4917,Autumn of the Patriarch,Gabriel Garcia Marquez,2014,Penguin UK,4.33,Gabriel Garcia Marquez\, winner of the 1982 Nobel Prize for Literature and author of One Hundred Years of Solitude\, explores the loneliness of power in Autumn of the Patriarch. 'Over the weekend the vultures got into the presidential palace by pecking through the screens on the balcony windows and the flapping of their wings stirred up the stagnant time inside' As the citizens of an unnamed Caribbean nation creep through dusty corridors in search of their tyrannical leader\, they cannot comprehend that the frail and withered man lying dead on the floor can be the self-styled General of the Universe. Their arrogant\, manically violent leader\, known for serving up traitors to dinner guests and drowning young children at sea\, can surely not die the humiliating death of a mere mortal? Tracing the demands of a man whose egocentric excesses mask the loneliness of isolation and whose lies have become so ingrained that they are indistinguishable from truth\, Marquez has created a fantastical portrait of despotism that rings with an air of reality. 'Delights with its quirky humanity and black humour and impresses by its total originality' Vogue 'Captures perfectly the moral squalor and political paralysis that enshrouds a society awaiting the death of a long-term dictator' Guardian 'Marquez writes in this lyrical\, magical language that no-one else can do' Salman Rushdie -9896,Barn burning (A tale blazer book),William Faulkner,1979,Perfection Learning,3.50,Reprinted from Collected Stories of William Faulkner\, by permission of Random House\, Inc. -9607,Beowolf: The monsters and the critics,John Ronald Reuel Tolkien,1997,HarperCollins UK,4.12,A collection of seven essays by J.R.R. Tolkien arising out of Tolkien's work in medieval literature -1985,Brothers Karamazov,Fyodor Dostoevsky,2015,First Avenue Editions,5.00,Four brothers reunite in their hometown in Russia. The murder of their father forces the brothers to question their beliefs about each other\, religion\, and morality. -2713,Collected Stories of William Faulkner,William Faulkner,1995,Vintage,4.53,A collection of short stories focuses on the people of rural Mississippi -2464,Conversations with Kurt Vonnegut (Literary Conversations),Kurt Vonnegut,1988,Univ. Press of Mississippi,4.40,Gathers interviews with Vonnegut from each period of his career and offers a brief profile of his life and accomplishments -8534,Crime and Punishment (Oxford World's Classics),Fyodor Dostoevsky,2017,Oxford University Press,4.38,'One death\, in exchange for thousands of lives - it's simple arithmetic!' A new translation of Dostoevsky's epic masterpiece\, Crime and Punishment (1866). The impoverished student Raskolnikov decides to free himself from debt by killing an old moneylender\, an act he sees as elevating himself above conventional morality. Like Napoleon he will assert his will and his crime will be justified by its elimination of 'vermin' for the sake of the greater good. But Raskolnikov is torn apart by fear\, guilt\, and a growing conscience under the influence of his love for Sonya. Meanwhile the police detective Porfiry is on his trial. It is a powerfully psychological novel\, in which the St Petersburg setting\, Dostoevsky's own circumstances\, and contemporary social problems all play their part. -8605,Dead Souls,Nikolai Gogol,1997,Vintage,4.28,Chichikov\, an amusing and often confused schemer\, buys deceased serfs' names from landholders' poll tax lists hoping to mortgage them for profit -6970,Domestic Goddesses,Edith Vonnegut,1998,Pomegranate,4.67,In this immensely charming and insightful book\, artist Edith Vonnegut takes issue with traditional art imagery in which women are shown as weak and helpless. Through twenty-seven of her own paintings interspersed with her text\, she poignantly -- and humorously -- illustrates her maxim that the lives of mothers and homemakers are filled with endless challenges and vital decisions that should be portrayed with the dignity they deserve. In Vonnegut's paintings\, one woman bravely blocks the sun from harming a child (Sun Block) while another vacuums the stairs with angelic figures singing her praises (Electrolux). In contrasting her own Domestic Goddesses with the diaphanous women of classical art (seven paintings by masters such as Titian and Botticelli are included)\, she 'expresses the importance of traditional roles of women so cleverly and with such joy that her message and images will be forever emblazoned on our collective psyche. -4814,El Coronel No Tiene Quien Le Escriba / No One Writes to the Colonel (Spanish Edition),Gabriel Garcia Marquez,2005,Harper Collins,4.45,Written with compassionate realism and wit\, the stories in this mesmerizing collection depict the disparities of town and village life in South America\, of the frightfully poor and outrageously rich\, of memories and illusions\, and of lost opportunities and present joys. -4636,FINAL WITNESS,Simon Tolkien,2004,Random House Digital\, Inc.,3.94,The murder of Lady Anne Robinson by two intruders causes a schism in the victim's family when her son convinces police that his father's beautiful personal assistant hired the killers\, while his father\, the British minister of defense\, refuses to believe his son and marries the accused. A first novel. Reprint. -2936,Fellowship of the Ring 2ND Edition,John Ronald Reuel Tolkien,2008,HarperCollins UK,4.43,Sauron\, the Dark Lord\, has gathered to him all the Rings of Power - the means by which he intends to rule Middle-earth. All he lacks in his plans for dominion is the One Ring - the ring that rules them all - which has fallen into the hands of the hobbit\, Bilbo Baggins. In a sleepy village in the Shire\, young Frodo Baggins finds himself faced with an immense task\, as his elderly cousin Bilbo entrusts the Ring to his care. Frodo must leave his home and make a perilous journey across Middle-earth to the Cracks of Doom\, there to destroy the Ring and foil the Dark Lord in his evil purpose. JRR Tolkien's great work of imaginative fiction has been labelled both a heroic romance and a classic fantasy fiction. By turns comic and homely\, epic and diabolic\, the narrative moves through countless changes of scene and character in an imaginary world which is totally convincing in its detail. -8956,GOD BLESS YOU MR. ROSEWATER : Or Pearls Before Swine,Kurt Vonnegut,1970,New York : Dell,4.00,A lawyer schemes to gain control of a large fortune by having the present claimant declared insane. -6818,Hadji Murad,Leo Tolstoy,2022,Hachette UK,3.88,'How truth thickens and deepens when it migrates from didactic fable to the raw experience of a visceral awakening is one of the thrills of Tolstoy's stories' Sharon Cameron in her preface to Hadji Murad and Other Stories This\, the third volume of Tolstoy's shorter fiction concentrates on his later stories\, including one of his greatest\, 'Hadji Murad'. In the stark form of homily that shapes these later works\, life considered as one's own has no rational meaning. From the chain of events that follows in the wake of two schoolboys' deception in 'The Forged Coupon' to the disillusionment of the narrator in 'After the Ball' we see\, in Virginia Woolf's observation\, that Tolstoy puts at the centre of his writing one 'who gathers into himself all experience\, turns the world round between his fingers\, and never ceases to ask\, even as he enjoys it\, what is the meaning of it'. The riverrun edition reissues the translation of Louise and Aylmer Maude\, whose influential versions of Tolstoy first brought his work to a wide readership in English. -3950,Hocus,Kurt Vonnegut,1997,Penguin,4.67,Tarkington College\, a small\, exclusive college in upstate New York\, is turned upside down when ten thousand prisoners from the maximum security prison across Lake Mohiga break out and head for the college -5404,Intruder in the dust,William Faulkner,2011,Vintage,3.18,A classic Faulkner novel which explores the lives of a family of characters in the South. An aging black who has long refused to adopt the black's traditionally servile attitude is wrongfully accused of murdering a white man. -5578,Intruder in the dust: A novel,William Faulkner,1991,Vintage,3.18,Dramatizes the events that surround the murder of a white man in a volatile Southern community -6380,La hojarasca (Spanish Edition),Gabriel Garcia Marquez,1979,Harper Collins,3.75,Translated from the Spanish by Gregory Rabassa -5335,Letters of J R R Tolkien,J.R.R. Tolkien,2014,HarperCollins,4.70,This collection will entertain all who appreciate the art of masterful letter writing. The Letters of J.R.R Tolkien sheds much light on Tolkien's creative genius and grand design for the creation of a whole new world: Middle-earth. Featuring a radically expanded index\, this volume provides a valuable research tool for all fans wishing to trace the evolution of THE HOBBIT and THE LORD OF THE RINGS. -3870,My First 100 Words in Spanish/English (My First 100 Words Pull-Tab Book),Keith Faulkner,1998,Libros Para Ninos,4.50,Learning a foreign language has never been this much fun! Just pull the sturdy tabs and change the words under the pictures from English to Spanish and back again to English! -4502,O'Brian's Bride,Colleen Faulkner,1995,Zebra Books,5.00,Abandoning her pampered English life to marry a man in the American colonies\, Elizabeth finds her new world shattered when her husband is killed in an accident\, leaving her in charge of a business on the untamed frontier. Original. -7635,Oliphaunt (Beastly Verse),J. R. R. Tolkien,1989,Contemporary Books,2.50,A poem in which an elephant describes himself and his way of life. On board pages. -3254,Pearl and Sir Orfeo,[John Ronald Reuel Tolkien, Christopher Tolkien],1995,Harpercollins Pub Limited,5.00,Three epic poems from 14th century England speak of life during the age of chivalry. Translated from medieval English. -3677,Planet of Exile,Ursula K. Le Guin,1979,Orion,4.20,PLAYAWAY: An alliance between the powerful Tevars and the brown-skinned\, clairvoyant Farbons must take place if the two colonies are to withstand the fierce attack of the nomadic tribes from the north of the planet Eltanin. -4289,Poems from the Hobbit,J R R Tolkien,1999,HarperCollins Publishers,4.00,A collection of J.R.R. Tolkien's Hobbit poems in a miniature hardback volume complete with illustrations by Tolkien himself. Far over misty mountains cold To dungeons deep and caverns old We must away ere break of day To seek the pale enchanted gold. J.R.R. Tolkien's acclaimed The Hobbit contains 12 poems which are themselves masterpieces of writing. This miniature book\, illustrated with 30 of Tolkien's own paintings and drawings from the book -- some quite rare and all in full colour -- includes all the poems\, plus Gollum's eight riddles in verse\, and will be a perfect keepsake for lovers of The Hobbit and of accomplished poetry. -6151,Pop! Went Another Balloon: A Magical Counting Storybook (Magical Counting Storybooks),[Keith Faulkner, Rory Tyger],2003,Dutton Childrens Books,5.00,Toby the turtle goes from in-line skates to a motorcycle to a rocketship with a handful of balloons that pop\, one by one\, along the way. -3535,Rainbow's End: A Magical Story and Moneybox,[Keith Faulkner, Beverlie Manson],2003,Barrons Juveniles,4.00,In this combination picture storybook and coin bank\, the unusual front cover shows an illustration from the story that's embellished with five transparent plastic windows. Opening the book\, children will find a story about a poor little ballerina who is crying because her dancing shoes are worn and she has no money to replace them. Full color. Consumable. -8423,Raising Faithful Kids in a Fast-Paced World,Paul Faulkner,1995,Howard Publishing Company,5.00,To find help for struggling parents\, Dr. Paul Faulkner--renowned family counselor and popular speaker--interviewed 30 successful families who have managed to raise faithful kids while also maintaining demanding careers. The invaluable strategies and methods he gleaned are now available in this powerful book delivered in Dr. Faulkner's warm\, humorous style. -1463,Realms of Tolkien: Images of Middle-earth,J. R. R. Tolkien,1997,HarperCollins Publishers,4.00,Twenty new and familiar Tolkien artists are represented in this fabulous volume\, breathing an extraordinary variety of life into 58 different scenes\, each of which is accompanied by appropriate passage from The Hobbit and The Lord of the Rings and The Silmarillion -6323,Resurrection (The Penguin classics),Leo Tolstoy,2009,Penguin,3.25,Leo Tolstoy's last completed novel\, Resurrection is an intimate\, psychological tale of guilt\, anger and forgiveness Serving on the jury at a murder trial\, Prince Dmitri Nekhlyudov is devastated when he sees the prisoner - Katyusha\, a young maid he seduced and abandoned years before. As Dmitri faces the consequences of his actions\, he decides to give up his life of wealth and luxury to devote himself to rescuing Katyusha\, even if it means following her into exile in Siberia. But can a man truly find redemption by saving another person? Tolstoy's most controversial novel\, Resurrection (1899) is a scathing indictment of injustice\, corruption and hypocrisy at all levels of society. Creating a vast panorama of Russian life\, from peasants to aristocrats\, bureaucrats to convicts\, it reveals Tolstoy's magnificent storytelling powers. Anthony Briggs' superb new translation preserves Tolstoy's gripping realism and satirical humour. In his introduction\, Briggs discusses the true story behind Resurrection\, Tolstoy's political and religious reasons for writing the novel\, his gift for characterization and the compelling psychological portrait of Dmitri. This edition also includes a chronology\, notes and a summary of chapters. For more than seventy years\, Penguin has been the leading publisher of classic literature in the English-speaking world. With more than 1\,700 titles\, Penguin Classics represents a global bookshelf of the best works throughout history and across genres and disciplines. Readers trust the series to provide authoritative texts enhanced by introductions and notes by distinguished scholars and contemporary authors\, as well as up-to-date translations by award-winning translators. -2714,Return of the King Being the Third Part of The Lord of the Rings,J. R. R. Tolkien,2012,HarperCollins,4.60,Concluding the story begun in The Hobbit\, this is the final part of Tolkien s epic masterpiece\, The Lord of the Rings\, featuring an exclusive cover image from the film\, the definitive text\, and a detailed map of Middle-earth. The armies of the Dark Lord Sauron are massing as his evil shadow spreads ever wider. Men\, Dwarves\, Elves and Ents unite forces to do battle agains the Dark. Meanwhile\, Frodo and Sam struggle further into Mordor in their heroic quest to destroy the One Ring. The devastating conclusion of J.R.R. Tolkien s classic tale of magic and adventure\, begun in The Fellowship of the Ring and The Two Towers\, features the definitive edition of the text and includes the Appendices and a revised Index in full. To celebrate the release of the first of Peter Jackson s two-part film adaptation of The Hobbit\, THE HOBBIT: AN UNEXPECTED JOURNEY\, this third part of The Lord of the Rings is available for a limited time with an exclusive cover image from Peter Jackson s award-winning trilogy. -7350,Return of the Shadow,[John Ronald Reuel Tolkien, Christopher Tolkien],2000,Mariner Books,5.00,In this sixth volume of The History of Middle-earth the story reaches The Lord of the Rings. In The Return of the Shadow (an abandoned title for the first volume) Christopher Tolkien describes\, with full citation of the earliest notes\, outline plans\, and narrative drafts\, the intricate evolution of The Fellowship of the Ring and the gradual emergence of the conceptions that transformed what J.R.R. Tolkien for long believed would be a far shorter book\, 'a sequel to The Hobbit'. The enlargement of Bilbo's 'magic ring' into the supremely potent and dangerous Ruling Ring of the Dark Lord is traced and the precise moment is seen when\, in an astonishing and unforeseen leap in the earliest narrative\, a Black Rider first rode into the Shire\, his significance still unknown. The character of the hobbit called Trotter (afterwards Strider or Aragorn) is developed while his indentity remains an absolute puzzle\, and the suspicion only very slowly becomes certainty that he must after all be a Man. The hobbits\, Frodo's companions\, undergo intricate permutations of name and personality\, and other major figures appear in strange modes: a sinister Treebeard\, in league with the Enemy\, a ferocious and malevolent Farmer Maggot. The story in this book ends at the point where J.R.R. Tolkien halted in the story for a long time\, as the Company of the Ring\, still lacking Legolas and Gimli\, stood before the tomb of Balin in the Mines of Moria. The Return of the Shadow is illustrated with reproductions of the first maps and notable pages from the earliest manuscripts. -6760,Roverandom,J. R. R. Tolkien,1999,Mariner Books,4.38,Rover\, a dog who has been turned into a toy dog encounters rival wizards and experiences various adventures on the moon with giant spiders\, dragon moths\, and the Great White Dragon. By the author of The Hobbit. Reprint. -8873,Searoad: Chronicles of Klatsand,Ursula K. Le Guin,2004,Shambhala Publications,5.00,A series of interlinking tales and a novella by the author of the Earthsea trilogy portrays the triumphs and struggles of several generations of women who independently control Klatsand\, a small resort town on the Oregon coast. Reprint. -2378,Selected Letters of Lucretia Coffin Mott (Women in American History),[Lucretia Mott, Holly Byers Ochoa, Carol Faulkner],2002,University of Illinois Press,5.00,Dedicated to reform of almost every kind - temperance\, peace\, equal rights\, woman suffrage\, nonresistance\, and the abolition of slavery - Mott viewed women's rights as only one element of a broad-based reform agenda for American society. -1502,Selected Passages from Correspondence with Friends,Nikolai Vasilevich Gogol,2009,Vanderbilt University Press,4.00,Nikolai Gogol wrote some letters to his friends\, none of which were a nose of high rank. Many are reproduced here (the letters\, not noses). -5996,Smith of Wooten Manor & Farmer Giles of Ham,John Ronald Reuel Tolkien,1969,Del Rey,4.91,Two bewitching fantasies by J.R.R. Tolkien\, beloved author of THE HOBBIT. In SMITH OF WOOTTON MAJOR\, Tolkien explores the gift of fantasy\, and what it means to the life and character of the man who receives it. And FARMER GILES OF HAM tells a delightfully ribald mock-heroic tale\, where a dragon who invades a town refuses to fight\, and a farmer is chosen to slay him. -2301,Smith of Wootton Major & Farmer Giles of Ham,John Ronald Reuel Tolkien,1969,Del Rey,5.00,Two bewitching fantasies by J.R.R. Tolkien\, beloved author of THE HOBBIT. In SMITH OF WOOTTON MAJOR\, Tolkien explores the gift of fantasy\, and what it means to the life and character of the man who receives it. And FARMER GILES OF HAM tells a delightfully ribald mock-heroic tale\, where a dragon who invades a town refuses to fight\, and a farmer is chosen to slay him. -2236,Steering the Craft,Ursula K. Le Guin,2015,Houghton Mifflin Harcourt,4.73,A revised and updated guide to the essentials of a writer's craft\, presented by a brilliant practitioner of the art Completely revised and rewritten to address the challenges and opportunities of the modern era\, this handbook is a short\, deceptively simple guide to the craft of writing. Le Guin lays out ten chapters that address the most fundamental components of narrative\, from the sound of language to sentence construction to point of view. Each chapter combines illustrative examples from the global canon with Le Guin's own witty commentary and an exercise that the writer can do solo or in a group. She also offers a comprehensive guide to working in writing groups\, both actual and online. Masterly and concise\, Steering the Craft deserves a place on every writer's shelf. -4724,THE UNVANQUISHED,William Faulkner,2011,Vintage,3.50,Set in Mississippi during the Civil War and Reconstruction\, THE UNVANQUISHED focuses on the Sartoris family\, who\, with their code of personal responsibility and courage\, stand for the best of the Old South's traditions. -5948,That We Are Gentle Creatures,Fyodor Dostoevsky,2009,OUP Oxford,4.33,In the stories in this volume Dostoevsky explores both the figure of the dreamer divorced from reality and also his own ambiguous attitude to utopianism\, themes central to many of his great novels. In White Nights the apparent idyll of the dreamer's romantic fantasies disguises profound loneliness and estrangement from 'living life'. Despite his sentimental friendship with Nastenka\, his final withdrawal into the world of the imagination anticipates the retreat into the 'underground' of many of Dostoevsky's later intellectual heroes. A Gentle Creature and The Dream of a Ridiculous Man show how such withdrawal from reality can end in spiritual desolation and moral indifference and how\, in Dostoevsky's view\, the tragedy of the alienated individual can be resolved only by the rediscovery of a sense of compassion and responsibility towards fellow human beings. This new translation captures the power and lyricism of Dostoevsky's writing\, while the introduction examines the stories in relation to one another and to his novels. ABOUT THE SERIES: For over 100 years Oxford World's Classics has made available the widest range of literature from around the globe. Each affordable volume reflects Oxford's commitment to scholarship\, providing the most accurate text plus a wealth of other valuable features\, including expert introductions by leading authorities\, helpful notes to clarify the text\, up-to-date bibliographies for further study\, and much more. -1937,The Best Short Stories of Dostoevsky (Modern Library),Fyodor Dostoevsky,2012,Modern Library,4.33,This collection\, unique to the Modern Library\, gathers seven of Dostoevsky's key works and shows him to be equally adept at the short story as with the novel. Exploring many of the same themes as in his longer works\, these small masterpieces move from the tender and romantic White Nights\, an archetypal nineteenth-century morality tale of pathos and loss\, to the famous Notes from the Underground\, a story of guilt\, ineffectiveness\, and uncompromising cynicism\, and the first major work of existential literature. Among Dostoevsky's prototypical characters is Yemelyan in The Honest Thief\, whose tragedy turns on an inability to resist crime. Presented in chronological order\, in David Magarshack's celebrated translation\, this is the definitive edition of Dostoevsky's best stories. -2776,The Devil and Other Stories (Oxford World's Classics),Leo Tolstoy,2003,OUP Oxford,5.00,'It is impossible to explain why Yevgeny chose Liza Annenskaya\, as it is always impossible to explain why a man chooses this and not that woman.' This collection of eleven stories spans virtually the whole of Tolstoy's creative life. While each is unique in form\, as a group they are representative of his style\, and touch on the central themes that surface in War and Peace and Anna Karenina. Stories as different as 'The Snowstorm'\, 'Lucerne'\, 'The Diary of a Madman'\, and 'The Devil' are grounded in autobiographical experience. They deal with journeys of self-discovery and the moral and religious questioning that characterizes Tolstoy's works of criticism and philosophy. 'Strider' and 'Father Sergy'\, as well as reflecting Tolstoy's own experiences\, also reveal profound psychological insights. These stories range over much of the Russian world of the nineteenth century\, from the nobility to the peasantry\, the military to the clergy\, from merchants and cobblers to a horse and a tree. Together they present a fascinating picture of Tolstoy's skill and artistry. ABOUT THE SERIES: For over 100 years Oxford World's Classics has made available the widest range of literature from around the globe. Each affordable volume reflects Oxford's commitment to scholarship\, providing the most accurate text plus a wealth of other valuable features\, including expert introductions by leading authorities\, helpful notes to clarify the text\, up-to-date bibliographies for further study\, and much more. -4231,The Dispossessed,Ursula K. Le Guin,1974,Harpercollins,4.26,Frequently reissued with the same ISBN\, but with slightly differing bibliographical details. -7480,The Hobbit,J. R. R. Tolkien,2012,Mariner Books,4.64,Celebrating 75 years of one of the world's most treasured classics with an all new trade paperback edition. Repackaged with new cover art. 500\,000 first printing. -6405,The Hobbit or There and Back Again,J. R. R. Tolkien,2012,Mariner Books,4.63,Celebrating 75 years of one of the world's most treasured classics with an all new trade paperback edition. Repackaged with new cover art. 500\,000 first printing. -2540,The Inspector General (Language - Russian) (Russian Edition),[Nicolai Gogol, Thomas Seltzer],2014,CreateSpace,3.50,The Inspector-General is a national institution. To place a purely literary valuation upon it and call it the greatest of Russian comedies would not convey the significance of its position either in Russian literature or in Russian life itself. There is no other single work in the modern literature of any language that carries with it the wealth of associations which the Inspector-General does to the educated Russian. -2951,The Insulted and Injured,Fyodor Dostoevsky,2011,Wm. B. Eerdmans Publishing,4.00,The Insulted and Injured\, which came out in 1861\, was Fyodor Dostoevsky's first major work of fiction after his Siberian exile and the first of the long novels that made him famous. Set in nineteenth-century Petersburg\, this gripping novel features a vividly drawn set of characters - including Vanya (Dostoevsky's semi-autobiographical hero)\, Natasha (the woman he loves)\, and Alyosha (Natasha's aristocratic lover) - all suffering from the cruelly selfish machinations of Alyosha's father\, the dark and powerful Prince Valkovsky. Boris Jakim's fresh English-language rendering of this gem in the Doestoevsky canon is both more colorful and more accurate than any earlier translation. --from back cover. -2130,The J. R. R. Tolkien Audio Collection,[John Ronald Reuel Tolkien, Christopher Tolkien],2002,HarperCollins Publishers,4.89,For generations\, J R R Tolkien's words have brought to thrilling life a world of hobbits\, magic\, and historic myth\, woken from its foggy slumber within our minds. Here\, he tells the tales in his own voice. -9801,The Karamazov Brothers (Oxford World's Classics),Fyodor Dostoevsky,2008,Oxford University Press,4.40,A remarkable work showing the author's power to depict Russian character and his understanding of human nature. Driven by intense\, uncontrollable emotions of rage and revenge\, the four Karamazov brothers all become involved in the brutal murder of their despicable father. -5469,The Lays of Beleriand,[John Ronald Reuel Tolkien, Christopher Tolkien],2002,Harpercollins Pub Limited,4.42,The third volume that contains the early myths and legends which led to the writing of Tolkien's epic tale of war\, The Silmarillion. This\, the third volume of The History of Middle-earth\, gives us a priviledged insight into the creation of the mythology of Middle-earth\, through the alliterative verse tales of two of the most crucial stories in Tolkien's world -- those of Turien and Luthien. The first of the poems is the unpublished Lay of The Children of Hurin\, narrating on a grand scale the tragedy of Turin Turambar. The second is the moving Lay of Leithian\, the chief source of the tale of Beren and Luthien in The Silmarillion\, telling of the Quest of the Silmaril and the encounter with Morgoth in his subterranean fortress. Accompanying the poems are commentaries on the evolution of the history of the Elder Days. Also included is the notable criticism of The Lay of The Leithian by CS Lewis\, who read the poem in 1929. -2675,The Lord of the Rings - Boxed Set,J.R.R. Tolkien,2012,HarperCollins,4.56,This beautiful gift edition of The Hobbit\, J.R.R. Tolkien's classic prelude to his Lord of the Rings trilogy\, features cover art\, illustrations\, and watercolor paintings by the artist Alan Lee. Bilbo Baggins is a hobbit who enjoys a comfortable\, unambitious life\, rarely traveling any farther than his pantry or cellar. But his contentment is disturbed when the wizard Gandalf and a company of dwarves arrive on his doorstep one day to whisk him away on an adventure. They have launched a plot to raid the treasure hoard guarded by Smaug the Magnificent\, a large and very dangerous dragon. Bilbo reluctantly joins their quest\, unaware that on his journey to the Lonely Mountain he will encounter both a magic ring and a frightening creature known as Gollum. Written for J.R.R. Tolkien's own children\, The Hobbit has sold many millions of copies worldwide and established itself as a modern classic. -7140,The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1),[J. R. R. Tolkien, Alan Lee],2002,HarperSport,4.75,A selection of stunning poster paintings from the celebrated Tolkien artist Alan Lee - the man behind many of the striking images from The Lord of The Rings movie. The 50 paintings contained within the centenary edition of The Lord of the Rings in 1992 have themselves become classics and Alan Lee's interpretations are hailed as the most faithful to Tolkien's own vision. This new poster collection\, a perfect complement to volume one\, reproduces six more of the most popular paintings from the book in a format suitable either for hanging as posters or mounting and framing. -5127,The Overcoat, Nikolai Gogol,1992,Courier Corporation,3.75,Four short stories include a satirical tale of Russian bureaucrats and a portrayal of an elderly couple living in the secluded countryside. -8875,The Two Towers,John Ronald Reuel Tolkien,2007,HarperCollins UK,4.64,The second volume in The Lord of the Rings\, This title is also available as a film. -4977,The Unvanquished,William Faulkner,2011,Vintage,3.50,Set in Mississippi during the Civil War and Reconstruction\, THE UNVANQUISHED focuses on the Sartoris family\, who\, with their code of personal responsibility and courage\, stand for the best of the Old South's traditions. -4382,The Wolves of Witchmaker,Carole Guinane,2001,iUniverse,5.00,Polly Lavender is mysteriously lured onto Witchmaker's grounds along with her best friends Tony Rico\, Gracie Reene\, and Zeus\, the wolf they rescued as a pup. The three must quickly learn to master the art of magic because they have been chosen to lead Witchmaker Prep against a threat that has grim consequences. -7912,The Word For World is Forest,Ursula K. Le Guin,2015,Gollancz,4.22,When the inhabitants of a peaceful world are conquered by the bloodthirsty yumens\, their existence is irrevocably altered. Forced into servitude\, the Athsheans find themselves at the mercy of their brutal masters. Desperation causes the Athsheans\, led by Selver\, to retaliate against their captors\, abandoning their strictures against violence. But in defending their lives\, they have endangered the very foundations of their society. For every blow against the invaders is a blow to the humanity of the Athsheans. And once the killing starts\, there is no turning back. -1211,The brothers Karamazov,Fyodor Dostoevsky,2003,Bantam Classics,1.00,In 1880 Dostoevsky completed The Brothers Karamazov\, the literary effort for which he had been preparing all his life. Compelling\, profound\, complex\, it is the story of a patricide and of the four sons who each had a motive for murder: Dmitry\, the sensualist\, Ivan\, the intellectual\, Alyosha\, the mystic\, and twisted\, cunning Smerdyakov\, the bastard child. Frequently lurid\, nightmarish\, always brilliant\, the novel plunges the reader into a sordid love triangle\, a pathological obsession\, and a gripping courtroom drama. But throughout the whole\, Dostoevsky searhes for the truth--about man\, about life\, about the existence of God. A terrifying answer to man's eternal questions\, this monumental work remains the crowning achievement of perhaps the finest novelist of all time. From the Paperback edition. -8086,The grand inquisitor (Milestones of thought),Fyodor Dostoevsky,1981,A&C Black,4.09,Dostoevsky's portrayal of the Catholic Church during the Inquisition is a plea for the power of pure faith\, and a critique of the tyrannies of institutionalized religion. This is an except from the Brothers Karamazov which stands alone as a statement of philiosophy and a warning about the surrender of freedom for the sake of comfort. -8077,The unvanquished,William Faulkner,2011,Vintage,4.00,Set in Mississippi during the Civil War and Reconstruction\, THE UNVANQUISHED focuses on the Sartoris family\, who\, with their code of personal responsibility and courage\, stand for the best of the Old South's traditions. -8480,The wind's twelve quarters: Short stories,Ursula K. Le Guin,2017,HarperCollins,5.00,The recipient of numerous literary prizes\, including the National Book Award\, the Kafka Award\, and the Pushcart Prize\, Ursula K. Le Guin is renowned for her lyrical writing\, rich characters\, and diverse worlds. The Wind's Twelve Quarters collects seventeen powerful stories\, each with an introduction by the author\, ranging from fantasy to intriguing scientific concepts\, from medieval settings to the future. Including an insightful foreword by Le Guin\, describing her experience\, her inspirations\, and her approach to writing\, this stunning collection explores human values\, relationships\, and survival\, and showcases the myriad talents of one of the most provocative writers of our time. -2847,To Love A Dark Stranger (Lovegram Historical Romance),Colleen Faulkner,1997,Zebra Books,5.00,Bestselling author Colleen Faulkner's tumultuous saga of royal intrigue and forbidden desire sweeps from the magnificent estates of the aristocracy to the shadowy streets of London to King Charles II's glittering Restoration court. -3293,Universe by Design,Danny Faulkner,2004,New Leaf Publishing Group,4.25,Views the stars and planets from a creationist standpoint\, addresses common misconceptions and difficulties about relativity and cosmology\, and discusses problems with the big bang theory with many analogies\, examples\, diagrams\, and illustrations. Original. -5327,War and Peace,Leo Tolstoy,2016,Lulu.com,3.84,Covering the period from the French invasion under Napoleon into Russia. Although not covering solely the war itself\, the serialized novel does cover the effects the war had on Russian society from the common person right up to the Tsar himself. The book starts to move more to a philosophical consideration on war and peace near the end making the book as a whole an important piece of literature. -4536,War and Peace (Signet Classics),[Leo Tolstoy, Pat Conroy, John Hockenberry],2012,Signet Classics,4.75,Presents the classical epic of the Napoleonic Wars and their effects on four Russian families. -9032,War and Peace: A Novel (6 Volumes),Tolstoy Leo,2013,Hardpress Publishing,3.81,Unlike some other reproductions of classic texts (1) We have not used OCR(Optical Character Recognition)\, as this leads to bad quality books with introduced typos. (2) In books where there are images such as portraits\, maps\, sketches etc We have endeavoured to keep the quality of these images\, so they represent accurately the original artefact. Although occasionally there may be certain imperfections with these old texts\, we feel they deserve to be made available for future generations to enjoy. -5119,William Faulkner,William Faulkner,2011,Vintage,4.00,This invaluable volume\, which has been republished to commemorate the one-hundredth anniversary of Faulkner's birth\, contains some of the greatest short fiction by a writer who defined the course of American literature. Its forty-five stories fall into three categories: those not included in Faulkner's earlier collections\, previously unpublished short fiction\, and stories that were later expanded into such novels as The Unvanquished\, The Hamlet\, and Go Down\, Moses. With its Introduction and extensive notes by the biographer Joseph Blotner\, Uncollected Stories of William Faulkner is an essential addition to its author's canon--as well as a book of some of the most haunting\, harrowing\, and atmospheric short fiction written in the twentieth century. -8615,Winter notes on summer impressions,Fyodor Dostoevsky,2018,Alma Books,4.75,In June 1862\, Dostoevsky left Petersburg on his first excursion to Western Europe. Ostensibly making the trip to consult Western specialists about his epilepsy\, he also wished to see first-hand the source of the Western ideas he believed were corrupting Russia. Over the course of his journey he visited a number of major cities\, including Berlin\, Paris\, London\, Florence\, Milan and Vienna.His record of the trip\, Winter Notes on Summer Impressions - first published in the February 1863 issue of Vremya\, the periodical he edited - is the chrysalis out of which many elements of his later masterpieces developed. -6478,Woman-The Full Story: A Dynamic Celebration of Freedoms,Michele Guinness,2003,Zondervan,5.00,What does it mean to be a woman today? What have women inherited from their radical\, risk-taking sisters of the past? And how does God view this half of humanity? Michele Guinness invites us on an adventure of discovery\, exploring the biblical texts\, the annals of history and the experiences of women today in search of the challenges and achievements\, failures and joys\, of women throughout the ages. -8678,Worlds of Exile and Illusion: Three Complete Novels of the Hainish Series in One Volume--Rocannon's World\, Planet of Exile\, City of Illusions,Ursula K. Le Guin,2016,Orb Books,4.41,Worlds of Exile and Illusion contains three novels in the Hainish Series from Ursula K. Le Guin\, one of the greatest science fiction writers and many times the winner of the Hugo and Nebula Awards. Her career as a novelist was launched by the three novels contained here. These books\, Rocannon's World\, Planet of Exile\, and City of Illusions\, are set in the same universe as Le Guin's groundbreaking classic\, The Left Hand of Darkness. At the Publisher's request\, this title is being sold without Digital Rights Management Software (DRM) applied. +_id:keyword,book_no:keyword,title:text,author:text,year:integer,publisher:text,ratings:float,description:text +0,2924,A Gentle Creature and Other Stories: White Nights\, A Gentle Creature\, and The Dream of a Ridiculous Man (The World's Classics),[Fyodor Dostoevsky, Alan Myers, W. J. Leatherbarrow],2009,Oxford Paperbacks,4.00,In these stories Dostoevsky explores both the figure of the dreamer divorced from reality and also his own ambiguous attitude to utopianism\, themes central to many of his great novels. This new translation captures the power and lyricism of Dostoevsky's writing\, while the introduction examines the stories in relation to one another and to his novels. +1,7670,A Middle English Reader and Vocabulary,[Kenneth Sisam, J. R. R. Tolkien],2011,Courier Corporation,4.33,This highly respected anthology of medieval English literature features poetry\, prose and popular tales from Arthurian legend and classical mythology. Includes notes on each extract\, appendices\, and an extensive glossary by J. R. R. Tolkien. +2,7381,A Psychic in the Heartland: The Extraordinary Experiences of a Small Town Doctor,Bettilu Stein Faulkner,2003,Red Wheel/Weiser,4.50,The true story of a small-town doctor destined to live his life along two paths: one as a successful physician\, the other as a psychic with ever more interesting adventures. Experiencing a wide range of spiritual phenomena\, Dr. Riblet Hout learned about the connection between the healer and the healed\, our individual missions on earth\, free will\, and our relationship with God. He also paints a vivid picture of life on the other side as well as the moment of transition from physical life to afterlife. +3,2883,A Summer of Faulkner: As I Lay Dying/The Sound and the Fury/Light in August (Oprah's Book Club),William Faulkner,2005,Vintage Books,3.89,Presents three novels\, including As I Lay Dying\, in which the Bundren family journeys across Mississippi to bury their mother\, The Sound and the Fury\, in which Caddy Compson's story is narrated by her three brothers\, and Light in August\, in which th +4,4023,A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings,[Walter Scheps, Agnes Perkins, Charles Adolph Huttar, John Ronald Reuel Tolkien],1975,Open Court Publishing,4.67,The structure\, content\, and character of Tolkien's The Hobbit and The Lord of the Rings are dealt with in ten critical essays. +5,2382,A Wizard of Earthsea (Earthsea Trilogy Ser.),Ursula K. Le Guin,1991,Atheneum Books for Young Readers,4.01,A boy grows to manhood while attempting to subdue the evil he unleashed on the world as an apprentice to the Master Wizard. +6,7541,A Writer's Diary (Volume 1: 1873-1876),Fyodor Dostoevsky,1997,Northwestern University Press,4.50,Winner of the AATSEEL Outstanding Translation Award This is the first paperback edition of the complete collection of writings that has been called Dostoevsky's boldest experiment with literary form\, it is a uniquely encyclopedic forum of fictional and nonfictional genres. The Diary's radical format was matched by the extreme range of its contents. In a single frame it incorporated an astonishing variety of material: short stories\, humorous sketches\, reports on sensational crimes\, historical predictions\, portraits of famous people\, autobiographical pieces\, and plans for stories\, some of which were never written while others appeared in the Diary itself. +7,7400,Anna Karenina: Television Tie-In Edition (Signet classics),[Leo Tolstoy, SBP Editors],2019,Samaira Book Publishers,4.45,The Russian novelist and moral philosopher Leo Tolstoy (1828-1910) ranks as one of the world s great writers\, and his 'War and Peace' has been called the greatest novel ever written. But during his long lifetime\, Tolstoy also wrote enough shorter works to fill many volumes. The message in all his stories is presented with such humour that the reader hardly realises that it is strongly didactic. These stories give a snapshot of Russia and its people in the late nineteenth century. +8,4917,Autumn of the Patriarch,Gabriel Garcia Marquez,2014,Penguin UK,4.33,Gabriel Garcia Marquez\, winner of the 1982 Nobel Prize for Literature and author of One Hundred Years of Solitude\, explores the loneliness of power in Autumn of the Patriarch. 'Over the weekend the vultures got into the presidential palace by pecking through the screens on the balcony windows and the flapping of their wings stirred up the stagnant time inside' As the citizens of an unnamed Caribbean nation creep through dusty corridors in search of their tyrannical leader\, they cannot comprehend that the frail and withered man lying dead on the floor can be the self-styled General of the Universe. Their arrogant\, manically violent leader\, known for serving up traitors to dinner guests and drowning young children at sea\, can surely not die the humiliating death of a mere mortal? Tracing the demands of a man whose egocentric excesses mask the loneliness of isolation and whose lies have become so ingrained that they are indistinguishable from truth\, Marquez has created a fantastical portrait of despotism that rings with an air of reality. 'Delights with its quirky humanity and black humour and impresses by its total originality' Vogue 'Captures perfectly the moral squalor and political paralysis that enshrouds a society awaiting the death of a long-term dictator' Guardian 'Marquez writes in this lyrical\, magical language that no-one else can do' Salman Rushdie +9,9896,Barn burning (A tale blazer book),William Faulkner,1979,Perfection Learning,3.50,Reprinted from Collected Stories of William Faulkner\, by permission of Random House\, Inc. +10,9607,Beowolf: The monsters and the critics,John Ronald Reuel Tolkien,1997,HarperCollins UK,4.12,A collection of seven essays by J.R.R. Tolkien arising out of Tolkien's work in medieval literature +11,1985,Brothers Karamazov,Fyodor Dostoevsky,2015,First Avenue Editions,5.00,Four brothers reunite in their hometown in Russia. The murder of their father forces the brothers to question their beliefs about each other\, religion\, and morality. +12,2713,Collected Stories of William Faulkner,William Faulkner,1995,Vintage,4.53,A collection of short stories focuses on the people of rural Mississippi +13,2464,Conversations with Kurt Vonnegut (Literary Conversations),Kurt Vonnegut,1988,Univ. Press of Mississippi,4.40,Gathers interviews with Vonnegut from each period of his career and offers a brief profile of his life and accomplishments +14,8534,Crime and Punishment (Oxford World's Classics),Fyodor Dostoevsky,2017,Oxford University Press,4.38,'One death\, in exchange for thousands of lives - it's simple arithmetic!' A new translation of Dostoevsky's epic masterpiece\, Crime and Punishment (1866). The impoverished student Raskolnikov decides to free himself from debt by killing an old moneylender\, an act he sees as elevating himself above conventional morality. Like Napoleon he will assert his will and his crime will be justified by its elimination of 'vermin' for the sake of the greater good. But Raskolnikov is torn apart by fear\, guilt\, and a growing conscience under the influence of his love for Sonya. Meanwhile the police detective Porfiry is on his trial. It is a powerfully psychological novel\, in which the St Petersburg setting\, Dostoevsky's own circumstances\, and contemporary social problems all play their part. +15,8605,Dead Souls,Nikolai Gogol,1997,Vintage,4.28,Chichikov\, an amusing and often confused schemer\, buys deceased serfs' names from landholders' poll tax lists hoping to mortgage them for profit +16,6970,Domestic Goddesses,Edith Vonnegut,1998,Pomegranate,4.67,In this immensely charming and insightful book\, artist Edith Vonnegut takes issue with traditional art imagery in which women are shown as weak and helpless. Through twenty-seven of her own paintings interspersed with her text\, she poignantly -- and humorously -- illustrates her maxim that the lives of mothers and homemakers are filled with endless challenges and vital decisions that should be portrayed with the dignity they deserve. In Vonnegut's paintings\, one woman bravely blocks the sun from harming a child (Sun Block) while another vacuums the stairs with angelic figures singing her praises (Electrolux). In contrasting her own Domestic Goddesses with the diaphanous women of classical art (seven paintings by masters such as Titian and Botticelli are included)\, she 'expresses the importance of traditional roles of women so cleverly and with such joy that her message and images will be forever emblazoned on our collective psyche. +17,4814,El Coronel No Tiene Quien Le Escriba / No One Writes to the Colonel (Spanish Edition),Gabriel Garcia Marquez,2005,Harper Collins,4.45,Written with compassionate realism and wit\, the stories in this mesmerizing collection depict the disparities of town and village life in South America\, of the frightfully poor and outrageously rich\, of memories and illusions\, and of lost opportunities and present joys. +18,4636,FINAL WITNESS,Simon Tolkien,2004,Random House Digital\, Inc.,3.94,The murder of Lady Anne Robinson by two intruders causes a schism in the victim's family when her son convinces police that his father's beautiful personal assistant hired the killers\, while his father\, the British minister of defense\, refuses to believe his son and marries the accused. A first novel. Reprint. +19,2936,Fellowship of the Ring 2ND Edition,John Ronald Reuel Tolkien,2008,HarperCollins UK,4.43,Sauron\, the Dark Lord\, has gathered to him all the Rings of Power - the means by which he intends to rule Middle-earth. All he lacks in his plans for dominion is the One Ring - the ring that rules them all - which has fallen into the hands of the hobbit\, Bilbo Baggins. In a sleepy village in the Shire\, young Frodo Baggins finds himself faced with an immense task\, as his elderly cousin Bilbo entrusts the Ring to his care. Frodo must leave his home and make a perilous journey across Middle-earth to the Cracks of Doom\, there to destroy the Ring and foil the Dark Lord in his evil purpose. JRR Tolkien's great work of imaginative fiction has been labelled both a heroic romance and a classic fantasy fiction. By turns comic and homely\, epic and diabolic\, the narrative moves through countless changes of scene and character in an imaginary world which is totally convincing in its detail. +20,8956,GOD BLESS YOU MR. ROSEWATER : Or Pearls Before Swine,Kurt Vonnegut,1970,New York : Dell,4.00,A lawyer schemes to gain control of a large fortune by having the present claimant declared insane. +21,6818,Hadji Murad,Leo Tolstoy,2022,Hachette UK,3.88,'How truth thickens and deepens when it migrates from didactic fable to the raw experience of a visceral awakening is one of the thrills of Tolstoy's stories' Sharon Cameron in her preface to Hadji Murad and Other Stories This\, the third volume of Tolstoy's shorter fiction concentrates on his later stories\, including one of his greatest\, 'Hadji Murad'. In the stark form of homily that shapes these later works\, life considered as one's own has no rational meaning. From the chain of events that follows in the wake of two schoolboys' deception in 'The Forged Coupon' to the disillusionment of the narrator in 'After the Ball' we see\, in Virginia Woolf's observation\, that Tolstoy puts at the centre of his writing one 'who gathers into himself all experience\, turns the world round between his fingers\, and never ceases to ask\, even as he enjoys it\, what is the meaning of it'. The riverrun edition reissues the translation of Louise and Aylmer Maude\, whose influential versions of Tolstoy first brought his work to a wide readership in English. +22,3950,Hocus,Kurt Vonnegut,1997,Penguin,4.67,Tarkington College\, a small\, exclusive college in upstate New York\, is turned upside down when ten thousand prisoners from the maximum security prison across Lake Mohiga break out and head for the college +23,5404,Intruder in the dust,William Faulkner,2011,Vintage,3.18,A classic Faulkner novel which explores the lives of a family of characters in the South. An aging black who has long refused to adopt the black's traditionally servile attitude is wrongfully accused of murdering a white man. +24,5578,Intruder in the dust: A novel,William Faulkner,1991,Vintage,3.18,Dramatizes the events that surround the murder of a white man in a volatile Southern community +25,6380,La hojarasca (Spanish Edition),Gabriel Garcia Marquez,1979,Harper Collins,3.75,Translated from the Spanish by Gregory Rabassa +26,5335,Letters of J R R Tolkien,J.R.R. Tolkien,2014,HarperCollins,4.70,This collection will entertain all who appreciate the art of masterful letter writing. The Letters of J.R.R Tolkien sheds much light on Tolkien's creative genius and grand design for the creation of a whole new world: Middle-earth. Featuring a radically expanded index\, this volume provides a valuable research tool for all fans wishing to trace the evolution of THE HOBBIT and THE LORD OF THE RINGS. +27,3870,My First 100 Words in Spanish/English (My First 100 Words Pull-Tab Book),Keith Faulkner,1998,Libros Para Ninos,4.50,Learning a foreign language has never been this much fun! Just pull the sturdy tabs and change the words under the pictures from English to Spanish and back again to English! +28,4502,O'Brian's Bride,Colleen Faulkner,1995,Zebra Books,5.00,Abandoning her pampered English life to marry a man in the American colonies\, Elizabeth finds her new world shattered when her husband is killed in an accident\, leaving her in charge of a business on the untamed frontier. Original. +29,7635,Oliphaunt (Beastly Verse),J. R. R. Tolkien,1989,Contemporary Books,2.50,A poem in which an elephant describes himself and his way of life. On board pages. +30,3254,Pearl and Sir Orfeo,[John Ronald Reuel Tolkien, Christopher Tolkien],1995,Harpercollins Pub Limited,5.00,Three epic poems from 14th century England speak of life during the age of chivalry. Translated from medieval English. +31,3677,Planet of Exile,Ursula K. Le Guin,1979,Orion,4.20,PLAYAWAY: An alliance between the powerful Tevars and the brown-skinned\, clairvoyant Farbons must take place if the two colonies are to withstand the fierce attack of the nomadic tribes from the north of the planet Eltanin. +32,4289,Poems from the Hobbit,J R R Tolkien,1999,HarperCollins Publishers,4.00,A collection of J.R.R. Tolkien's Hobbit poems in a miniature hardback volume complete with illustrations by Tolkien himself. Far over misty mountains cold To dungeons deep and caverns old We must away ere break of day To seek the pale enchanted gold. J.R.R. Tolkien's acclaimed The Hobbit contains 12 poems which are themselves masterpieces of writing. This miniature book\, illustrated with 30 of Tolkien's own paintings and drawings from the book -- some quite rare and all in full colour -- includes all the poems\, plus Gollum's eight riddles in verse\, and will be a perfect keepsake for lovers of The Hobbit and of accomplished poetry. +33,6151,Pop! Went Another Balloon: A Magical Counting Storybook (Magical Counting Storybooks),[Keith Faulkner, Rory Tyger],2003,Dutton Childrens Books,5.00,Toby the turtle goes from in-line skates to a motorcycle to a rocketship with a handful of balloons that pop\, one by one\, along the way. +34,3535,Rainbow's End: A Magical Story and Moneybox,[Keith Faulkner, Beverlie Manson],2003,Barrons Juveniles,4.00,In this combination picture storybook and coin bank\, the unusual front cover shows an illustration from the story that's embellished with five transparent plastic windows. Opening the book\, children will find a story about a poor little ballerina who is crying because her dancing shoes are worn and she has no money to replace them. Full color. Consumable. +35,8423,Raising Faithful Kids in a Fast-Paced World,Paul Faulkner,1995,Howard Publishing Company,5.00,To find help for struggling parents\, Dr. Paul Faulkner--renowned family counselor and popular speaker--interviewed 30 successful families who have managed to raise faithful kids while also maintaining demanding careers. The invaluable strategies and methods he gleaned are now available in this powerful book delivered in Dr. Faulkner's warm\, humorous style. +36,1463,Realms of Tolkien: Images of Middle-earth,J. R. R. Tolkien,1997,HarperCollins Publishers,4.00,Twenty new and familiar Tolkien artists are represented in this fabulous volume\, breathing an extraordinary variety of life into 58 different scenes\, each of which is accompanied by appropriate passage from The Hobbit and The Lord of the Rings and The Silmarillion +37,6323,Resurrection (The Penguin classics),Leo Tolstoy,2009,Penguin,3.25,Leo Tolstoy's last completed novel\, Resurrection is an intimate\, psychological tale of guilt\, anger and forgiveness Serving on the jury at a murder trial\, Prince Dmitri Nekhlyudov is devastated when he sees the prisoner - Katyusha\, a young maid he seduced and abandoned years before. As Dmitri faces the consequences of his actions\, he decides to give up his life of wealth and luxury to devote himself to rescuing Katyusha\, even if it means following her into exile in Siberia. But can a man truly find redemption by saving another person? Tolstoy's most controversial novel\, Resurrection (1899) is a scathing indictment of injustice\, corruption and hypocrisy at all levels of society. Creating a vast panorama of Russian life\, from peasants to aristocrats\, bureaucrats to convicts\, it reveals Tolstoy's magnificent storytelling powers. Anthony Briggs' superb new translation preserves Tolstoy's gripping realism and satirical humour. In his introduction\, Briggs discusses the true story behind Resurrection\, Tolstoy's political and religious reasons for writing the novel\, his gift for characterization and the compelling psychological portrait of Dmitri. This edition also includes a chronology\, notes and a summary of chapters. For more than seventy years\, Penguin has been the leading publisher of classic literature in the English-speaking world. With more than 1\,700 titles\, Penguin Classics represents a global bookshelf of the best works throughout history and across genres and disciplines. Readers trust the series to provide authoritative texts enhanced by introductions and notes by distinguished scholars and contemporary authors\, as well as up-to-date translations by award-winning translators. +38,2714,Return of the King Being the Third Part of The Lord of the Rings,J. R. R. Tolkien,2012,HarperCollins,4.60,Concluding the story begun in The Hobbit\, this is the final part of Tolkien s epic masterpiece\, The Lord of the Rings\, featuring an exclusive cover image from the film\, the definitive text\, and a detailed map of Middle-earth. The armies of the Dark Lord Sauron are massing as his evil shadow spreads ever wider. Men\, Dwarves\, Elves and Ents unite forces to do battle agains the Dark. Meanwhile\, Frodo and Sam struggle further into Mordor in their heroic quest to destroy the One Ring. The devastating conclusion of J.R.R. Tolkien s classic tale of magic and adventure\, begun in The Fellowship of the Ring and The Two Towers\, features the definitive edition of the text and includes the Appendices and a revised Index in full. To celebrate the release of the first of Peter Jackson s two-part film adaptation of The Hobbit\, THE HOBBIT: AN UNEXPECTED JOURNEY\, this third part of The Lord of the Rings is available for a limited time with an exclusive cover image from Peter Jackson s award-winning trilogy. +39,7350,Return of the Shadow,[John Ronald Reuel Tolkien, Christopher Tolkien],2000,Mariner Books,5.00,In this sixth volume of The History of Middle-earth the story reaches The Lord of the Rings. In The Return of the Shadow (an abandoned title for the first volume) Christopher Tolkien describes\, with full citation of the earliest notes\, outline plans\, and narrative drafts\, the intricate evolution of The Fellowship of the Ring and the gradual emergence of the conceptions that transformed what J.R.R. Tolkien for long believed would be a far shorter book\, 'a sequel to The Hobbit'. The enlargement of Bilbo's 'magic ring' into the supremely potent and dangerous Ruling Ring of the Dark Lord is traced and the precise moment is seen when\, in an astonishing and unforeseen leap in the earliest narrative\, a Black Rider first rode into the Shire\, his significance still unknown. The character of the hobbit called Trotter (afterwards Strider or Aragorn) is developed while his indentity remains an absolute puzzle\, and the suspicion only very slowly becomes certainty that he must after all be a Man. The hobbits\, Frodo's companions\, undergo intricate permutations of name and personality\, and other major figures appear in strange modes: a sinister Treebeard\, in league with the Enemy\, a ferocious and malevolent Farmer Maggot. The story in this book ends at the point where J.R.R. Tolkien halted in the story for a long time\, as the Company of the Ring\, still lacking Legolas and Gimli\, stood before the tomb of Balin in the Mines of Moria. The Return of the Shadow is illustrated with reproductions of the first maps and notable pages from the earliest manuscripts. +40,6760,Roverandom,J. R. R. Tolkien,1999,Mariner Books,4.38,Rover\, a dog who has been turned into a toy dog encounters rival wizards and experiences various adventures on the moon with giant spiders\, dragon moths\, and the Great White Dragon. By the author of The Hobbit. Reprint. +41,8873,Searoad: Chronicles of Klatsand,Ursula K. Le Guin,2004,Shambhala Publications,5.00,A series of interlinking tales and a novella by the author of the Earthsea trilogy portrays the triumphs and struggles of several generations of women who independently control Klatsand\, a small resort town on the Oregon coast. Reprint. +42,2378,Selected Letters of Lucretia Coffin Mott (Women in American History),[Lucretia Mott, Holly Byers Ochoa, Carol Faulkner],2002,University of Illinois Press,5.00,Dedicated to reform of almost every kind - temperance\, peace\, equal rights\, woman suffrage\, nonresistance\, and the abolition of slavery - Mott viewed women's rights as only one element of a broad-based reform agenda for American society. +43,1502,Selected Passages from Correspondence with Friends,Nikolai Vasilevich Gogol,2009,Vanderbilt University Press,4.00,Nikolai Gogol wrote some letters to his friends\, none of which were a nose of high rank. Many are reproduced here (the letters\, not noses). +44,5996,Smith of Wooten Manor & Farmer Giles of Ham,John Ronald Reuel Tolkien,1969,Del Rey,4.91,Two bewitching fantasies by J.R.R. Tolkien\, beloved author of THE HOBBIT. In SMITH OF WOOTTON MAJOR\, Tolkien explores the gift of fantasy\, and what it means to the life and character of the man who receives it. And FARMER GILES OF HAM tells a delightfully ribald mock-heroic tale\, where a dragon who invades a town refuses to fight\, and a farmer is chosen to slay him. +45,2301,Smith of Wootton Major & Farmer Giles of Ham,John Ronald Reuel Tolkien,1969,Del Rey,5.00,Two bewitching fantasies by J.R.R. Tolkien\, beloved author of THE HOBBIT. In SMITH OF WOOTTON MAJOR\, Tolkien explores the gift of fantasy\, and what it means to the life and character of the man who receives it. And FARMER GILES OF HAM tells a delightfully ribald mock-heroic tale\, where a dragon who invades a town refuses to fight\, and a farmer is chosen to slay him. +46,2236,Steering the Craft,Ursula K. Le Guin,2015,Houghton Mifflin Harcourt,4.73,A revised and updated guide to the essentials of a writer's craft\, presented by a brilliant practitioner of the art Completely revised and rewritten to address the challenges and opportunities of the modern era\, this handbook is a short\, deceptively simple guide to the craft of writing. Le Guin lays out ten chapters that address the most fundamental components of narrative\, from the sound of language to sentence construction to point of view. Each chapter combines illustrative examples from the global canon with Le Guin's own witty commentary and an exercise that the writer can do solo or in a group. She also offers a comprehensive guide to working in writing groups\, both actual and online. Masterly and concise\, Steering the Craft deserves a place on every writer's shelf. +47,4724,THE UNVANQUISHED,William Faulkner,2011,Vintage,3.50,Set in Mississippi during the Civil War and Reconstruction\, THE UNVANQUISHED focuses on the Sartoris family\, who\, with their code of personal responsibility and courage\, stand for the best of the Old South's traditions. +48,5948,That We Are Gentle Creatures,Fyodor Dostoevsky,2009,OUP Oxford,4.33,In the stories in this volume Dostoevsky explores both the figure of the dreamer divorced from reality and also his own ambiguous attitude to utopianism\, themes central to many of his great novels. In White Nights the apparent idyll of the dreamer's romantic fantasies disguises profound loneliness and estrangement from 'living life'. Despite his sentimental friendship with Nastenka\, his final withdrawal into the world of the imagination anticipates the retreat into the 'underground' of many of Dostoevsky's later intellectual heroes. A Gentle Creature and The Dream of a Ridiculous Man show how such withdrawal from reality can end in spiritual desolation and moral indifference and how\, in Dostoevsky's view\, the tragedy of the alienated individual can be resolved only by the rediscovery of a sense of compassion and responsibility towards fellow human beings. This new translation captures the power and lyricism of Dostoevsky's writing\, while the introduction examines the stories in relation to one another and to his novels. ABOUT THE SERIES: For over 100 years Oxford World's Classics has made available the widest range of literature from around the globe. Each affordable volume reflects Oxford's commitment to scholarship\, providing the most accurate text plus a wealth of other valuable features\, including expert introductions by leading authorities\, helpful notes to clarify the text\, up-to-date bibliographies for further study\, and much more. +49,1937,The Best Short Stories of Dostoevsky (Modern Library),Fyodor Dostoevsky,2012,Modern Library,4.33,This collection\, unique to the Modern Library\, gathers seven of Dostoevsky's key works and shows him to be equally adept at the short story as with the novel. Exploring many of the same themes as in his longer works\, these small masterpieces move from the tender and romantic White Nights\, an archetypal nineteenth-century morality tale of pathos and loss\, to the famous Notes from the Underground\, a story of guilt\, ineffectiveness\, and uncompromising cynicism\, and the first major work of existential literature. Among Dostoevsky's prototypical characters is Yemelyan in The Honest Thief\, whose tragedy turns on an inability to resist crime. Presented in chronological order\, in David Magarshack's celebrated translation\, this is the definitive edition of Dostoevsky's best stories. +50,2776,The Devil and Other Stories (Oxford World's Classics),Leo Tolstoy,2003,OUP Oxford,5.00,'It is impossible to explain why Yevgeny chose Liza Annenskaya\, as it is always impossible to explain why a man chooses this and not that woman.' This collection of eleven stories spans virtually the whole of Tolstoy's creative life. While each is unique in form\, as a group they are representative of his style\, and touch on the central themes that surface in War and Peace and Anna Karenina. Stories as different as 'The Snowstorm'\, 'Lucerne'\, 'The Diary of a Madman'\, and 'The Devil' are grounded in autobiographical experience. They deal with journeys of self-discovery and the moral and religious questioning that characterizes Tolstoy's works of criticism and philosophy. 'Strider' and 'Father Sergy'\, as well as reflecting Tolstoy's own experiences\, also reveal profound psychological insights. These stories range over much of the Russian world of the nineteenth century\, from the nobility to the peasantry\, the military to the clergy\, from merchants and cobblers to a horse and a tree. Together they present a fascinating picture of Tolstoy's skill and artistry. ABOUT THE SERIES: For over 100 years Oxford World's Classics has made available the widest range of literature from around the globe. Each affordable volume reflects Oxford's commitment to scholarship\, providing the most accurate text plus a wealth of other valuable features\, including expert introductions by leading authorities\, helpful notes to clarify the text\, up-to-date bibliographies for further study\, and much more. +51,4231,The Dispossessed,Ursula K. Le Guin,1974,Harpercollins,4.26,Frequently reissued with the same ISBN\, but with slightly differing bibliographical details. +52,7480,The Hobbit,J. R. R. Tolkien,2012,Mariner Books,4.64,Celebrating 75 years of one of the world's most treasured classics with an all new trade paperback edition. Repackaged with new cover art. 500\,000 first printing. +53,6405,The Hobbit or There and Back Again,J. R. R. Tolkien,2012,Mariner Books,4.63,Celebrating 75 years of one of the world's most treasured classics with an all new trade paperback edition. Repackaged with new cover art. 500\,000 first printing. +54,2540,The Inspector General (Language - Russian) (Russian Edition),[Nicolai Gogol, Thomas Seltzer],2014,CreateSpace,3.50,The Inspector-General is a national institution. To place a purely literary valuation upon it and call it the greatest of Russian comedies would not convey the significance of its position either in Russian literature or in Russian life itself. There is no other single work in the modern literature of any language that carries with it the wealth of associations which the Inspector-General does to the educated Russian. +55,2951,The Insulted and Injured,Fyodor Dostoevsky,2011,Wm. B. Eerdmans Publishing,4.00,The Insulted and Injured\, which came out in 1861\, was Fyodor Dostoevsky's first major work of fiction after his Siberian exile and the first of the long novels that made him famous. Set in nineteenth-century Petersburg\, this gripping novel features a vividly drawn set of characters - including Vanya (Dostoevsky's semi-autobiographical hero)\, Natasha (the woman he loves)\, and Alyosha (Natasha's aristocratic lover) - all suffering from the cruelly selfish machinations of Alyosha's father\, the dark and powerful Prince Valkovsky. Boris Jakim's fresh English-language rendering of this gem in the Doestoevsky canon is both more colorful and more accurate than any earlier translation. --from back cover. +56,2130,The J. R. R. Tolkien Audio Collection,[John Ronald Reuel Tolkien, Christopher Tolkien],2002,HarperCollins Publishers,4.89,For generations\, J R R Tolkien's words have brought to thrilling life a world of hobbits\, magic\, and historic myth\, woken from its foggy slumber within our minds. Here\, he tells the tales in his own voice. +57,9801,The Karamazov Brothers (Oxford World's Classics),Fyodor Dostoevsky,2008,Oxford University Press,4.40,A remarkable work showing the author's power to depict Russian character and his understanding of human nature. Driven by intense\, uncontrollable emotions of rage and revenge\, the four Karamazov brothers all become involved in the brutal murder of their despicable father. +58,5469,The Lays of Beleriand,[John Ronald Reuel Tolkien, Christopher Tolkien],2002,Harpercollins Pub Limited,4.42,The third volume that contains the early myths and legends which led to the writing of Tolkien's epic tale of war\, The Silmarillion. This\, the third volume of The History of Middle-earth\, gives us a priviledged insight into the creation of the mythology of Middle-earth\, through the alliterative verse tales of two of the most crucial stories in Tolkien's world -- those of Turien and Luthien. The first of the poems is the unpublished Lay of The Children of Hurin\, narrating on a grand scale the tragedy of Turin Turambar. The second is the moving Lay of Leithian\, the chief source of the tale of Beren and Luthien in The Silmarillion\, telling of the Quest of the Silmaril and the encounter with Morgoth in his subterranean fortress. Accompanying the poems are commentaries on the evolution of the history of the Elder Days. Also included is the notable criticism of The Lay of The Leithian by CS Lewis\, who read the poem in 1929. +59,2675,The Lord of the Rings - Boxed Set,J.R.R. Tolkien,2012,HarperCollins,4.56,This beautiful gift edition of The Hobbit\, J.R.R. Tolkien's classic prelude to his Lord of the Rings trilogy\, features cover art\, illustrations\, and watercolor paintings by the artist Alan Lee. Bilbo Baggins is a hobbit who enjoys a comfortable\, unambitious life\, rarely traveling any farther than his pantry or cellar. But his contentment is disturbed when the wizard Gandalf and a company of dwarves arrive on his doorstep one day to whisk him away on an adventure. They have launched a plot to raid the treasure hoard guarded by Smaug the Magnificent\, a large and very dangerous dragon. Bilbo reluctantly joins their quest\, unaware that on his journey to the Lonely Mountain he will encounter both a magic ring and a frightening creature known as Gollum. Written for J.R.R. Tolkien's own children\, The Hobbit has sold many millions of copies worldwide and established itself as a modern classic. +60,7140,The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1),[J. R. R. Tolkien, Alan Lee],2002,HarperSport,4.75,A selection of stunning poster paintings from the celebrated Tolkien artist Alan Lee - the man behind many of the striking images from The Lord of The Rings movie. The 50 paintings contained within the centenary edition of The Lord of the Rings in 1992 have themselves become classics and Alan Lee's interpretations are hailed as the most faithful to Tolkien's own vision. This new poster collection\, a perfect complement to volume one\, reproduces six more of the most popular paintings from the book in a format suitable either for hanging as posters or mounting and framing. +61,5127,The Overcoat, Nikolai Gogol,1992,Courier Corporation,3.75,Four short stories include a satirical tale of Russian bureaucrats and a portrayal of an elderly couple living in the secluded countryside. +62,8875,The Two Towers,John Ronald Reuel Tolkien,2007,HarperCollins UK,4.64,The second volume in The Lord of the Rings\, This title is also available as a film. +63,4977,The Unvanquished,William Faulkner,2011,Vintage,3.50,Set in Mississippi during the Civil War and Reconstruction\, THE UNVANQUISHED focuses on the Sartoris family\, who\, with their code of personal responsibility and courage\, stand for the best of the Old South's traditions. +64,4382,The Wolves of Witchmaker,Carole Guinane,2001,iUniverse,5.00,Polly Lavender is mysteriously lured onto Witchmaker's grounds along with her best friends Tony Rico\, Gracie Reene\, and Zeus\, the wolf they rescued as a pup. The three must quickly learn to master the art of magic because they have been chosen to lead Witchmaker Prep against a threat that has grim consequences. +65,7912,The Word For World is Forest,Ursula K. Le Guin,2015,Gollancz,4.22,When the inhabitants of a peaceful world are conquered by the bloodthirsty yumens\, their existence is irrevocably altered. Forced into servitude\, the Athsheans find themselves at the mercy of their brutal masters. Desperation causes the Athsheans\, led by Selver\, to retaliate against their captors\, abandoning their strictures against violence. But in defending their lives\, they have endangered the very foundations of their society. For every blow against the invaders is a blow to the humanity of the Athsheans. And once the killing starts\, there is no turning back. +66,1211,The brothers Karamazov,Fyodor Dostoevsky,2003,Bantam Classics,1.00,In 1880 Dostoevsky completed The Brothers Karamazov\, the literary effort for which he had been preparing all his life. Compelling\, profound\, complex\, it is the story of a patricide and of the four sons who each had a motive for murder: Dmitry\, the sensualist\, Ivan\, the intellectual\, Alyosha\, the mystic\, and twisted\, cunning Smerdyakov\, the bastard child. Frequently lurid\, nightmarish\, always brilliant\, the novel plunges the reader into a sordid love triangle\, a pathological obsession\, and a gripping courtroom drama. But throughout the whole\, Dostoevsky searhes for the truth--about man\, about life\, about the existence of God. A terrifying answer to man's eternal questions\, this monumental work remains the crowning achievement of perhaps the finest novelist of all time. From the Paperback edition. +67,8086,The grand inquisitor (Milestones of thought),Fyodor Dostoevsky,1981,A&C Black,4.09,Dostoevsky's portrayal of the Catholic Church during the Inquisition is a plea for the power of pure faith\, and a critique of the tyrannies of institutionalized religion. This is an except from the Brothers Karamazov which stands alone as a statement of philiosophy and a warning about the surrender of freedom for the sake of comfort. +68,8077,The unvanquished,William Faulkner,2011,Vintage,4.00,Set in Mississippi during the Civil War and Reconstruction\, THE UNVANQUISHED focuses on the Sartoris family\, who\, with their code of personal responsibility and courage\, stand for the best of the Old South's traditions. +69,8480,The wind's twelve quarters: Short stories,Ursula K. Le Guin,2017,HarperCollins,5.00,The recipient of numerous literary prizes\, including the National Book Award\, the Kafka Award\, and the Pushcart Prize\, Ursula K. Le Guin is renowned for her lyrical writing\, rich characters\, and diverse worlds. The Wind's Twelve Quarters collects seventeen powerful stories\, each with an introduction by the author\, ranging from fantasy to intriguing scientific concepts\, from medieval settings to the future. Including an insightful foreword by Le Guin\, describing her experience\, her inspirations\, and her approach to writing\, this stunning collection explores human values\, relationships\, and survival\, and showcases the myriad talents of one of the most provocative writers of our time. +70,2847,To Love A Dark Stranger (Lovegram Historical Romance),Colleen Faulkner,1997,Zebra Books,5.00,Bestselling author Colleen Faulkner's tumultuous saga of royal intrigue and forbidden desire sweeps from the magnificent estates of the aristocracy to the shadowy streets of London to King Charles II's glittering Restoration court. +71,3293,Universe by Design,Danny Faulkner,2004,New Leaf Publishing Group,4.25,Views the stars and planets from a creationist standpoint\, addresses common misconceptions and difficulties about relativity and cosmology\, and discusses problems with the big bang theory with many analogies\, examples\, diagrams\, and illustrations. Original. +72,5327,War and Peace,Leo Tolstoy,2016,Lulu.com,3.84,Covering the period from the French invasion under Napoleon into Russia. Although not covering solely the war itself\, the serialized novel does cover the effects the war had on Russian society from the common person right up to the Tsar himself. The book starts to move more to a philosophical consideration on war and peace near the end making the book as a whole an important piece of literature. +73,4536,War and Peace (Signet Classics),[Leo Tolstoy, Pat Conroy, John Hockenberry],2012,Signet Classics,4.75,Presents the classical epic of the Napoleonic Wars and their effects on four Russian families. +74,9032,War and Peace: A Novel (6 Volumes),Tolstoy Leo,2013,Hardpress Publishing,3.81,Unlike some other reproductions of classic texts (1) We have not used OCR(Optical Character Recognition)\, as this leads to bad quality books with introduced typos. (2) In books where there are images such as portraits\, maps\, sketches etc We have endeavoured to keep the quality of these images\, so they represent accurately the original artefact. Although occasionally there may be certain imperfections with these old texts\, we feel they deserve to be made available for future generations to enjoy. +75,5119,William Faulkner,William Faulkner,2011,Vintage,4.00,This invaluable volume\, which has been republished to commemorate the one-hundredth anniversary of Faulkner's birth\, contains some of the greatest short fiction by a writer who defined the course of American literature. Its forty-five stories fall into three categories: those not included in Faulkner's earlier collections\, previously unpublished short fiction\, and stories that were later expanded into such novels as The Unvanquished\, The Hamlet\, and Go Down\, Moses. With its Introduction and extensive notes by the biographer Joseph Blotner\, Uncollected Stories of William Faulkner is an essential addition to its author's canon--as well as a book of some of the most haunting\, harrowing\, and atmospheric short fiction written in the twentieth century. +76,8615,Winter notes on summer impressions,Fyodor Dostoevsky,2018,Alma Books,4.75,In June 1862\, Dostoevsky left Petersburg on his first excursion to Western Europe. Ostensibly making the trip to consult Western specialists about his epilepsy\, he also wished to see first-hand the source of the Western ideas he believed were corrupting Russia. Over the course of his journey he visited a number of major cities\, including Berlin\, Paris\, London\, Florence\, Milan and Vienna.His record of the trip\, Winter Notes on Summer Impressions - first published in the February 1863 issue of Vremya\, the periodical he edited - is the chrysalis out of which many elements of his later masterpieces developed. +77,6478,Woman-The Full Story: A Dynamic Celebration of Freedoms,Michele Guinness,2003,Zondervan,5.00,What does it mean to be a woman today? What have women inherited from their radical\, risk-taking sisters of the past? And how does God view this half of humanity? Michele Guinness invites us on an adventure of discovery\, exploring the biblical texts\, the annals of history and the experiences of women today in search of the challenges and achievements\, failures and joys\, of women throughout the ages. +78,8678,Worlds of Exile and Illusion: Three Complete Novels of the Hainish Series in One Volume--Rocannon's World\, Planet of Exile\, City of Illusions,Ursula K. Le Guin,2016,Orb Books,4.41,Worlds of Exile and Illusion contains three novels in the Hainish Series from Ursula K. Le Guin\, one of the greatest science fiction writers and many times the winner of the Hugo and Nebula Awards. Her career as a novelist was launched by the three novels contained here. These books\, Rocannon's World\, Planet of Exile\, and City of Illusions\, are set in the same universe as Le Guin's groundbreaking classic\, The Left Hand of Darkness. At the Publisher's request\, this title is being sold without Digital Rights Management Software (DRM) applied. diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec index cb38204a71ab0..72632c62603aa 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec @@ -13,9 +13,9 @@ from books metadata _score | sort c_score desc, book_no asc | LIMIT 2; -book_no:keyword | title:text | c_score:double -2675 | The Lord of the Rings - Boxed Set | 6.0 -4023 | A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings | 6.0 +book_no:keyword | title:text | c_score:double +1463 | Realms of Tolkien: Images of Middle-earth | 6.0 +2675 | The Lord of the Rings - Boxed Set | 6.0 ; singleMatchWithKeywordFieldScoring @@ -28,15 +28,15 @@ from books metadata _score | sort book_no; book_no:keyword | author:text | _score:double -2713 | William Faulkner | 2.3142893314361572 -2883 | William Faulkner | 2.3142893314361572 -4724 | William Faulkner | 2.3142893314361572 -4977 | William Faulkner | 2.3142893314361572 -5119 | William Faulkner | 2.3142893314361572 -5404 | William Faulkner | 2.3142893314361572 -5578 | William Faulkner | 2.3142893314361572 -8077 | William Faulkner | 2.3142893314361572 -9896 | William Faulkner | 2.3142893314361572 +2713 | William Faulkner | 1.7589385509490967 +2883 | William Faulkner | 1.7589385509490967 +4724 | William Faulkner | 1.7589385509490967 +4977 | William Faulkner | 2.6145541667938232 +5119 | William Faulkner | 2.513157367706299 +5404 | William Faulkner | 1.7589385509490967 +5578 | William Faulkner | 2.513157367706299 +8077 | William Faulkner | 1.7589385509490967 +9896 | William Faulkner | 2.6145541667938232 ; qstrWithFieldAndScoringSortedEval @@ -51,9 +51,9 @@ from books metadata _score | limit 3; book_no:keyword | title:text | _score:double -2675 | The Lord of the Rings - Boxed Set | 2.7583377361297607 -7140 | The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1) | 1.9239964485168457 -2714 | Return of the King Being the Third Part of The Lord of the Rings | 1.9239964485168457 +2675 | The Lord of the Rings - Boxed Set | 2.5619282722473145 +2714 | Return of the King Being the Third Part of The Lord of the Rings | 1.9245924949645996 +7140 | The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1) | 1.746896743774414 ; qstrWithFieldAndScoringSorted @@ -67,9 +67,9 @@ from books metadata _score | limit 3; book_no:keyword | title:text | _score:double -2675 | The Lord of the Rings - Boxed Set | 2.7583377361297607 -7140 | The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1) | 1.9239964485168457 -2714 | Return of the King Being the Third Part of The Lord of the Rings | 1.9239964485168457 +2675 | The Lord of the Rings - Boxed Set | 2.5619282722473145 +2714 | Return of the King Being the Third Part of The Lord of the Rings | 1.9245924949645996 +7140 | The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1) | 1.746896743774414 ; singleQstrScoringManipulated @@ -84,8 +84,8 @@ from books metadata _score | LIMIT 2; book_no:keyword | author:text | add_score:double -2378 | [Carol Faulkner, Holly Byers Ochoa, Lucretia Mott] | 2.0 -2713 | William Faulkner | 7.0 +2378 | [Carol Faulkner, Holly Byers Ochoa, Lucretia Mott] | 3.0 +2713 | William Faulkner | 6.0 ; testMultiValuedFieldWithConjunctionWithScore @@ -125,7 +125,7 @@ from books metadata _score ignoreOrder:true book_no:keyword | title:text | author:text | _score:double -8480 | The wind's twelve quarters: Short stories | Ursula K. Le Guin | 14.489097595214844 +8480 | The wind's twelve quarters: Short stories | Ursula K. Le Guin | 11.193471908569336 ; multipleWhereWithMatchScoring @@ -139,7 +139,7 @@ from books metadata _score | sort book_no; book_no:keyword | title:text | author:text | _score:double -8480 | The wind's twelve quarters: Short stories | Ursula K. Le Guin | 14.489097595214844 +8480 | The wind's twelve quarters: Short stories | Ursula K. Le Guin | 11.193471908569336 ; combinedMatchWithFunctionsScoring @@ -153,7 +153,7 @@ from books metadata _score | sort book_no; book_no:keyword | title:text | author:text | year:integer | _score:double -5335 | Letters of J R R Tolkien | J.R.R. Tolkien | 2014 | 5.448054313659668 +5335 | Letters of J R R Tolkien | J.R.R. Tolkien | 2014 | 4.733664035797119 ; singleQstrScoring @@ -167,8 +167,8 @@ from books metadata _score | LIMIT 2; book_no:keyword | author:text | _score:double -2378 | [Carol Faulkner, Holly Byers Ochoa, Lucretia Mott] | 0.9976131916046143 -2713 | William Faulkner | 5.9556169509887695 +2378 | [Carol Faulkner, Holly Byers Ochoa, Lucretia Mott] | 1.3697924613952637 +2713 | William Faulkner | 4.631696701049805 ; singleQstrScoringGrok @@ -183,9 +183,9 @@ from books metadata _score | LIMIT 3; book_no:keyword | title:keyword | _score:double -8875 | The | 2.9505908489227295 -4023 | A | 2.8327860832214355 -2675 | The | 2.7583377361297607 +8875 | The | 2.769660472869873 +1463 | Realms | 2.6714818477630615 +2675 | The | 2.5619282722473145 ; combinedMatchWithScoringEvalNoSort @@ -200,7 +200,7 @@ from books metadata _score ignoreOrder:true book_no:keyword | title:text | author:text | year:integer | c_score:double -5335 | Letters of J R R Tolkien | J.R.R. Tolkien | 2014 | 6 +5335 | Letters of J R R Tolkien | J.R.R. Tolkien | 2014 | 5.0 ; singleQstrScoringRename @@ -215,9 +215,9 @@ from books metadata _score | LIMIT 3; book_no:keyword | rank:double -8875 | 2.9505908489227295 -4023 | 2.8327860832214355 -2675 | 2.7583377361297607 +8875 | 2.769660472869873 +1463 | 2.6714818477630615 +2675 | 2.5619282722473145 ; singleMatchWithTextFieldScoring @@ -231,11 +231,11 @@ from books metadata _score | limit 5; book_no:keyword | author:text | _score:double -2378 | [Carol Faulkner, Holly Byers Ochoa, Lucretia Mott] | 0.9976131916046143 -2713 | William Faulkner | 4.272439002990723 -2847 | Colleen Faulkner | 1.7401835918426514 -2883 | William Faulkner | 4.272439002990723 -3293 | Danny Faulkner | 1.7401835918426514 +2378 | [Carol Faulkner, Holly Byers Ochoa, Lucretia Mott] | 1.3697924613952637 +2713 | William Faulkner | 3.2750158309936523 +2847 | Colleen Faulkner | 1.593343734741211 +2883 | William Faulkner | 3.2750158309936523 +3293 | Danny Faulkner | 1.593343734741211 ; combinedMatchWithFunctionsScoringNoSort @@ -249,7 +249,7 @@ from books metadata _score ignoreOrder:true book_no:keyword | title:text | author:text | year:integer | _score:double -5335 | Letters of J R R Tolkien | J.R.R. Tolkien | 2014 | 5.448054313659668 +5335 | Letters of J R R Tolkien | J.R.R. Tolkien | 2014 | 4.733664035797119 ; combinedMatchWithScoringEval @@ -264,7 +264,7 @@ from books metadata _score | sort book_no; book_no:keyword | title:text | author:text | year:integer | c_score:double -5335 | Letters of J R R Tolkien | J.R.R. Tolkien | 2014 | 6 +5335 | Letters of J R R Tolkien | J.R.R. Tolkien | 2014 | 5.0 ; singleQstrScoringEval @@ -280,7 +280,7 @@ from books metadata _score book_no:keyword | c_score:double 8875 | 3.0 -7350 | 2.0 +7350 | 1.0 7140 | 3.0 ; @@ -289,14 +289,16 @@ required_capability: metadata_score required_capability: qstr_function from books metadata _score -| where qstr("title:rings") +| where qstr("title:gentle") | eval _score = _score + 1 | keep book_no, title, _score -| limit 2; +| limit 10 +; +ignoreOrder:true -book_no:keyword | title:text | _score:double -4023 | A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings | 2.6404519081115723 -2714 | Return of the King Being the Third Part of The Lord of the Rings | 2.9239964485168457 +book_no:keyword | title:text | _score:double +2924 | A Gentle Creature and Other Stories: White Nights, A Gentle Creature, and The Dream of a Ridiculous Man (The World's Classics) | 3.158426523208618 +5948 | That We Are Gentle Creatures | 3.727346897125244 ; QstrScoreOverride @@ -304,12 +306,14 @@ required_capability: metadata_score required_capability: qstr_function from books metadata _score -| where qstr("title:rings") +| where qstr("title:gentle") | eval _score = "foobar" | keep book_no, title, _score -| limit 2; +| limit 10 +; +ignoreOrder:true -book_no:keyword | title:text | _score:keyword -4023 | A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings | foobar -2714 | Return of the King Being the Third Part of The Lord of the Rings | foobar +book_no:keyword | title:text | _score:keyword +2924 | A Gentle Creature and Other Stories: White Nights, A Gentle Creature, and The Dream of a Ridiculous Man (The World's Classics) | foobar +5948 | That We Are Gentle Creatures | foobar ; From 7cf28a910e6aa8e5679a73eb54075b6778277b67 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Wed, 18 Dec 2024 09:56:42 +0100 Subject: [PATCH 060/119] ESQL Add esql hash function (#117989) This change introduces esql hash(alg, input) function that relies on the Java MessageDigest to compute the hash. --- docs/changelog/117989.yaml | 5 + .../esql/functions/description/hash.asciidoc | 5 + .../functions/kibana/definition/hash.json | 82 +++++++ .../esql/functions/kibana/docs/hash.md | 7 + .../esql/functions/layout/hash.asciidoc | 14 ++ .../esql/functions/parameters/hash.asciidoc | 9 + .../esql/functions/signature/hash.svg | 1 + .../esql/functions/string-functions.asciidoc | 2 + .../esql/functions/types/hash.asciidoc | 12 + .../src/main/resources/hash.csv-spec | 105 +++++++++ .../scalar/string/HashConstantEvaluator.java | 142 ++++++++++++ .../function/scalar/string/HashEvaluator.java | 174 ++++++++++++++ .../xpack/esql/action/EsqlCapabilities.java | 5 + .../function/EsqlFunctionRegistry.java | 2 + .../scalar/ScalarFunctionWritables.java | 2 + .../function/scalar/string/Hash.java | 217 ++++++++++++++++++ .../AbstractExpressionSerializationTests.java | 5 + .../scalar/string/HashSerializationTests.java | 27 +++ .../scalar/string/HashStaticTests.java | 66 ++++++ .../function/scalar/string/HashTests.java | 107 +++++++++ .../rest-api-spec/test/esql/60_usage.yml | 4 +- 21 files changed, 991 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/117989.yaml create mode 100644 docs/reference/esql/functions/description/hash.asciidoc create mode 100644 docs/reference/esql/functions/kibana/definition/hash.json create mode 100644 docs/reference/esql/functions/kibana/docs/hash.md create mode 100644 docs/reference/esql/functions/layout/hash.asciidoc create mode 100644 docs/reference/esql/functions/parameters/hash.asciidoc create mode 100644 docs/reference/esql/functions/signature/hash.svg create mode 100644 docs/reference/esql/functions/types/hash.asciidoc create mode 100644 x-pack/plugin/esql/qa/testFixtures/src/main/resources/hash.csv-spec create mode 100644 x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashConstantEvaluator.java create mode 100644 x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashEvaluator.java create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashSerializationTests.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashTests.java diff --git a/docs/changelog/117989.yaml b/docs/changelog/117989.yaml new file mode 100644 index 0000000000000..e4967141b3ebd --- /dev/null +++ b/docs/changelog/117989.yaml @@ -0,0 +1,5 @@ +pr: 117989 +summary: ESQL Add esql hash function +area: ES|QL +type: enhancement +issues: [] diff --git a/docs/reference/esql/functions/description/hash.asciidoc b/docs/reference/esql/functions/description/hash.asciidoc new file mode 100644 index 0000000000000..e074915c5132a --- /dev/null +++ b/docs/reference/esql/functions/description/hash.asciidoc @@ -0,0 +1,5 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Description* + +Computes the hash of the input using various algorithms such as MD5, SHA, SHA-224, SHA-256, SHA-384, SHA-512. diff --git a/docs/reference/esql/functions/kibana/definition/hash.json b/docs/reference/esql/functions/kibana/definition/hash.json new file mode 100644 index 0000000000000..17a60cf45acfe --- /dev/null +++ b/docs/reference/esql/functions/kibana/definition/hash.json @@ -0,0 +1,82 @@ +{ + "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", + "type" : "eval", + "name" : "hash", + "description" : "Computes the hash of the input using various algorithms such as MD5, SHA, SHA-224, SHA-256, SHA-384, SHA-512.", + "signatures" : [ + { + "params" : [ + { + "name" : "algorithm", + "type" : "keyword", + "optional" : false, + "description" : "Hash algorithm to use." + }, + { + "name" : "input", + "type" : "keyword", + "optional" : false, + "description" : "Input to hash." + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "algorithm", + "type" : "keyword", + "optional" : false, + "description" : "Hash algorithm to use." + }, + { + "name" : "input", + "type" : "text", + "optional" : false, + "description" : "Input to hash." + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "algorithm", + "type" : "text", + "optional" : false, + "description" : "Hash algorithm to use." + }, + { + "name" : "input", + "type" : "keyword", + "optional" : false, + "description" : "Input to hash." + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "algorithm", + "type" : "text", + "optional" : false, + "description" : "Hash algorithm to use." + }, + { + "name" : "input", + "type" : "text", + "optional" : false, + "description" : "Input to hash." + } + ], + "variadic" : false, + "returnType" : "keyword" + } + ], + "preview" : false, + "snapshot_only" : false +} diff --git a/docs/reference/esql/functions/kibana/docs/hash.md b/docs/reference/esql/functions/kibana/docs/hash.md new file mode 100644 index 0000000000000..9826e80ec5bec --- /dev/null +++ b/docs/reference/esql/functions/kibana/docs/hash.md @@ -0,0 +1,7 @@ + + +### HASH +Computes the hash of the input using various algorithms such as MD5, SHA, SHA-224, SHA-256, SHA-384, SHA-512. + diff --git a/docs/reference/esql/functions/layout/hash.asciidoc b/docs/reference/esql/functions/layout/hash.asciidoc new file mode 100644 index 0000000000000..27c55ada6319b --- /dev/null +++ b/docs/reference/esql/functions/layout/hash.asciidoc @@ -0,0 +1,14 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +[discrete] +[[esql-hash]] +=== `HASH` + +*Syntax* + +[.text-center] +image::esql/functions/signature/hash.svg[Embedded,opts=inline] + +include::../parameters/hash.asciidoc[] +include::../description/hash.asciidoc[] +include::../types/hash.asciidoc[] diff --git a/docs/reference/esql/functions/parameters/hash.asciidoc b/docs/reference/esql/functions/parameters/hash.asciidoc new file mode 100644 index 0000000000000..d47a82d4ab214 --- /dev/null +++ b/docs/reference/esql/functions/parameters/hash.asciidoc @@ -0,0 +1,9 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Parameters* + +`algorithm`:: +Hash algorithm to use. + +`input`:: +Input to hash. diff --git a/docs/reference/esql/functions/signature/hash.svg b/docs/reference/esql/functions/signature/hash.svg new file mode 100644 index 0000000000000..f819e14c9d1a4 --- /dev/null +++ b/docs/reference/esql/functions/signature/hash.svg @@ -0,0 +1 @@ +HASH(algorithm,input) \ No newline at end of file diff --git a/docs/reference/esql/functions/string-functions.asciidoc b/docs/reference/esql/functions/string-functions.asciidoc index ce9636f5c5a3a..da9580a55151a 100644 --- a/docs/reference/esql/functions/string-functions.asciidoc +++ b/docs/reference/esql/functions/string-functions.asciidoc @@ -13,6 +13,7 @@ * <> * <> * <> +* <> * <> * <> * <> @@ -37,6 +38,7 @@ include::layout/byte_length.asciidoc[] include::layout/concat.asciidoc[] include::layout/ends_with.asciidoc[] include::layout/from_base64.asciidoc[] +include::layout/hash.asciidoc[] include::layout/left.asciidoc[] include::layout/length.asciidoc[] include::layout/locate.asciidoc[] diff --git a/docs/reference/esql/functions/types/hash.asciidoc b/docs/reference/esql/functions/types/hash.asciidoc new file mode 100644 index 0000000000000..786ba03b2aa60 --- /dev/null +++ b/docs/reference/esql/functions/types/hash.asciidoc @@ -0,0 +1,12 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Supported types* + +[%header.monospaced.styled,format=dsv,separator=|] +|=== +algorithm | input | result +keyword | keyword | keyword +keyword | text | keyword +text | keyword | keyword +text | text | keyword +|=== diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/hash.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/hash.csv-spec new file mode 100644 index 0000000000000..fcac1e1859c6d --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/hash.csv-spec @@ -0,0 +1,105 @@ +hash +required_capability: hash_function + +FROM sample_data +| WHERE message != "Connection error" +| EVAL md5 = hash("md5", message), sha256 = hash("sha256", message) +| KEEP message, md5, sha256; +ignoreOrder:true + +message:keyword | md5:keyword | sha256:keyword +Connected to 10.1.0.1 | abd7d1ce2bb636842a29246b3512dcae | 6d8372129ad78770f7185554dd39864749a62690216460752d6c075fa38ad85c +Connected to 10.1.0.2 | 8f8f1cb60832d153f5b9ec6dc828b93f | b0db24720f15857091b3c99f4c4833586d0ea3229911b8777efb8d917cf27e9a +Connected to 10.1.0.3 | 912b6dc13503165a15de43304bb77c78 | 75b0480188db8acc4d5cc666a51227eb2bc5b989cd8ca912609f33e0846eff57 +Disconnected | ef70e46fd3bbc21e3e1f0b6815e750c0 | 04dfac3671b494ad53fcd152f7a14511bfb35747278aad8ce254a0d6e4ba4718 +; + + +hashOfConvertedType +required_capability: hash_function + +FROM sample_data +| WHERE message != "Connection error" +| EVAL input = event_duration::STRING, md5 = hash("md5", input), sha256 = hash("sha256", input) +| KEEP message, input, md5, sha256; +ignoreOrder:true + +message:keyword | input:keyword | md5:keyword | sha256:keyword +Connected to 10.1.0.1 | 1756467 | c4fc1c57ee9b1d2b2023b70c8c167b54 | 8376a50a7ba7e6bd1bf9ad0c32d27d2f49fd0fa422573f98f239e21048b078f3 +Connected to 10.1.0.2 | 2764889 | 8e8cf005e11a7b5df1d9478a4715a444 | 1031f2bef8eaecbf47319505422300b27ea1f7c38b6717d41332325062f9a56a +Connected to 10.1.0.3 | 3450233 | 09f2c64f5a55e9edf8ffbad336b561d8 | f77d7545769c4ecc85092f4f0b7ec8c20f467e4beb15fe67ca29f9aa8e9a6900 +Disconnected | 1232382 | 6beac1485638d51e13c2c53990a2f611 | 9a03c1274a3ebb6c1cb85d170ce0a6fdb9d2232724e06b9f5e7cb9274af3cad6 +; + + +hashOfEmptyInput +required_capability: hash_function + +ROW input="" | EVAL md5 = hash("md5", input), sha256 = hash("sha256", input); + +input:keyword | md5:keyword | sha256:keyword + | d41d8cd98f00b204e9800998ecf8427e | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +; + +hashOfNullInput +required_capability: hash_function + +ROW input=null::STRING | EVAL md5 = hash("md5", input), sha256 = hash("sha256", input); + +input:keyword | md5:keyword | sha256:keyword +null | null | null +; + + +hashWithNullAlgorithm +required_capability: hash_function + +ROW input="input" | EVAL hash = hash(null, input); + +input:keyword | hash:keyword +input | null +; + + +hashWithMv +required_capability: hash_function + +ROW input=["foo", "bar"] | mv_expand input | EVAL md5 = hash("md5", input), sha256 = hash("sha256", input); + +input:keyword | md5:keyword | sha256:keyword +foo | acbd18db4cc2f85cedef654fccc4a4d8 | 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae +bar | 37b51d194a7513e45b56f6524f2d51f2 | fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 +; + + +hashWithNestedFunctions +required_capability: hash_function + +ROW input=["foo", "bar"] | EVAL hash = concat(hash("md5", mv_concat(input, "-")), "-", hash("sha256", mv_concat(input, "-"))); + +input:keyword | hash:keyword +["foo", "bar"] | e5f9ec048d1dbe19c70f720e002f9cb1-7d89c4f517e3bd4b5e8e76687937005b602ea00c5cba3e25ef1fc6575a55103e +; + + +hashWithConvertedTypes +required_capability: hash_function + +ROW input=42 | EVAL md5 = hash("md5", input::STRING), sha256 = hash("sha256", to_string(input)); + +input:integer | md5:keyword | sha256:keyword +42 | a1d0c6e83f027327d8461063f4ac58a6 | 73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049 +; + + +hashWithStats +required_capability: hash_function + +FROM sample_data +| EVAL md5="md5" +| STATS count = count(*) by hash(md5, message) +| WHERE count > 1; + +count:long | hash(md5, message):keyword +3 | 2e92ae79ff32b37fee4368a594792183 +; diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashConstantEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashConstantEvaluator.java new file mode 100644 index 0000000000000..34cff73018634 --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashConstantEvaluator.java @@ -0,0 +1,142 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.xpack.esql.expression.function.scalar.string; + +import java.lang.IllegalArgumentException; +import java.lang.Override; +import java.lang.String; +import java.util.function.Function; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.xpack.esql.core.tree.Source; + +/** + * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Hash}. + * This class is generated. Do not edit it. + */ +public final class HashConstantEvaluator implements EvalOperator.ExpressionEvaluator { + private final Source source; + + private final BreakingBytesRefBuilder scratch; + + private final Hash.HashFunction algorithm; + + private final EvalOperator.ExpressionEvaluator input; + + private final DriverContext driverContext; + + private Warnings warnings; + + public HashConstantEvaluator(Source source, BreakingBytesRefBuilder scratch, + Hash.HashFunction algorithm, EvalOperator.ExpressionEvaluator input, + DriverContext driverContext) { + this.source = source; + this.scratch = scratch; + this.algorithm = algorithm; + this.input = input; + this.driverContext = driverContext; + } + + @Override + public Block eval(Page page) { + try (BytesRefBlock inputBlock = (BytesRefBlock) input.eval(page)) { + BytesRefVector inputVector = inputBlock.asVector(); + if (inputVector == null) { + return eval(page.getPositionCount(), inputBlock); + } + return eval(page.getPositionCount(), inputVector).asBlock(); + } + } + + public BytesRefBlock eval(int positionCount, BytesRefBlock inputBlock) { + try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) { + BytesRef inputScratch = new BytesRef(); + position: for (int p = 0; p < positionCount; p++) { + if (inputBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (inputBlock.getValueCount(p) != 1) { + if (inputBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + result.appendBytesRef(Hash.processConstant(this.scratch, this.algorithm, inputBlock.getBytesRef(inputBlock.getFirstValueIndex(p), inputScratch))); + } + return result.build(); + } + } + + public BytesRefVector eval(int positionCount, BytesRefVector inputVector) { + try(BytesRefVector.Builder result = driverContext.blockFactory().newBytesRefVectorBuilder(positionCount)) { + BytesRef inputScratch = new BytesRef(); + position: for (int p = 0; p < positionCount; p++) { + result.appendBytesRef(Hash.processConstant(this.scratch, this.algorithm, inputVector.getBytesRef(p, inputScratch))); + } + return result.build(); + } + } + + @Override + public String toString() { + return "HashConstantEvaluator[" + "algorithm=" + algorithm + ", input=" + input + "]"; + } + + @Override + public void close() { + Releasables.closeExpectNoException(scratch, input); + } + + private Warnings warnings() { + if (warnings == null) { + this.warnings = Warnings.createWarnings( + driverContext.warningsMode(), + source.source().getLineNumber(), + source.source().getColumnNumber(), + source.text() + ); + } + return warnings; + } + + static class Factory implements EvalOperator.ExpressionEvaluator.Factory { + private final Source source; + + private final Function scratch; + + private final Function algorithm; + + private final EvalOperator.ExpressionEvaluator.Factory input; + + public Factory(Source source, Function scratch, + Function algorithm, + EvalOperator.ExpressionEvaluator.Factory input) { + this.source = source; + this.scratch = scratch; + this.algorithm = algorithm; + this.input = input; + } + + @Override + public HashConstantEvaluator get(DriverContext context) { + return new HashConstantEvaluator(source, scratch.apply(context), algorithm.apply(context), input.get(context), context); + } + + @Override + public String toString() { + return "HashConstantEvaluator[" + "algorithm=" + algorithm + ", input=" + input + "]"; + } + } +} diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashEvaluator.java new file mode 100644 index 0000000000000..8b01cc0330142 --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashEvaluator.java @@ -0,0 +1,174 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.xpack.esql.expression.function.scalar.string; + +import java.lang.IllegalArgumentException; +import java.lang.Override; +import java.lang.String; +import java.security.NoSuchAlgorithmException; +import java.util.function.Function; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.xpack.esql.core.tree.Source; + +/** + * {@link EvalOperator.ExpressionEvaluator} implementation for {@link Hash}. + * This class is generated. Do not edit it. + */ +public final class HashEvaluator implements EvalOperator.ExpressionEvaluator { + private final Source source; + + private final BreakingBytesRefBuilder scratch; + + private final EvalOperator.ExpressionEvaluator algorithm; + + private final EvalOperator.ExpressionEvaluator input; + + private final DriverContext driverContext; + + private Warnings warnings; + + public HashEvaluator(Source source, BreakingBytesRefBuilder scratch, + EvalOperator.ExpressionEvaluator algorithm, EvalOperator.ExpressionEvaluator input, + DriverContext driverContext) { + this.source = source; + this.scratch = scratch; + this.algorithm = algorithm; + this.input = input; + this.driverContext = driverContext; + } + + @Override + public Block eval(Page page) { + try (BytesRefBlock algorithmBlock = (BytesRefBlock) algorithm.eval(page)) { + try (BytesRefBlock inputBlock = (BytesRefBlock) input.eval(page)) { + BytesRefVector algorithmVector = algorithmBlock.asVector(); + if (algorithmVector == null) { + return eval(page.getPositionCount(), algorithmBlock, inputBlock); + } + BytesRefVector inputVector = inputBlock.asVector(); + if (inputVector == null) { + return eval(page.getPositionCount(), algorithmBlock, inputBlock); + } + return eval(page.getPositionCount(), algorithmVector, inputVector); + } + } + } + + public BytesRefBlock eval(int positionCount, BytesRefBlock algorithmBlock, + BytesRefBlock inputBlock) { + try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) { + BytesRef algorithmScratch = new BytesRef(); + BytesRef inputScratch = new BytesRef(); + position: for (int p = 0; p < positionCount; p++) { + if (algorithmBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (algorithmBlock.getValueCount(p) != 1) { + if (algorithmBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + if (inputBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (inputBlock.getValueCount(p) != 1) { + if (inputBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + try { + result.appendBytesRef(Hash.process(this.scratch, algorithmBlock.getBytesRef(algorithmBlock.getFirstValueIndex(p), algorithmScratch), inputBlock.getBytesRef(inputBlock.getFirstValueIndex(p), inputScratch))); + } catch (NoSuchAlgorithmException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + public BytesRefBlock eval(int positionCount, BytesRefVector algorithmVector, + BytesRefVector inputVector) { + try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) { + BytesRef algorithmScratch = new BytesRef(); + BytesRef inputScratch = new BytesRef(); + position: for (int p = 0; p < positionCount; p++) { + try { + result.appendBytesRef(Hash.process(this.scratch, algorithmVector.getBytesRef(p, algorithmScratch), inputVector.getBytesRef(p, inputScratch))); + } catch (NoSuchAlgorithmException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + @Override + public String toString() { + return "HashEvaluator[" + "algorithm=" + algorithm + ", input=" + input + "]"; + } + + @Override + public void close() { + Releasables.closeExpectNoException(scratch, algorithm, input); + } + + private Warnings warnings() { + if (warnings == null) { + this.warnings = Warnings.createWarnings( + driverContext.warningsMode(), + source.source().getLineNumber(), + source.source().getColumnNumber(), + source.text() + ); + } + return warnings; + } + + static class Factory implements EvalOperator.ExpressionEvaluator.Factory { + private final Source source; + + private final Function scratch; + + private final EvalOperator.ExpressionEvaluator.Factory algorithm; + + private final EvalOperator.ExpressionEvaluator.Factory input; + + public Factory(Source source, Function scratch, + EvalOperator.ExpressionEvaluator.Factory algorithm, + EvalOperator.ExpressionEvaluator.Factory input) { + this.source = source; + this.scratch = scratch; + this.algorithm = algorithm; + this.input = input; + } + + @Override + public HashEvaluator get(DriverContext context) { + return new HashEvaluator(source, scratch.apply(context), algorithm.get(context), input.get(context), context); + } + + @Override + public String toString() { + return "HashEvaluator[" + "algorithm=" + algorithm + ", input=" + input + "]"; + } + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 8c7e381d33322..d6c1539088d47 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -449,6 +449,11 @@ public enum Cap { */ KQL_FUNCTION(Build.current().isSnapshot()), + /** + * Hash function + */ + HASH_FUNCTION, + /** * Don't optimize CASE IS NOT NULL function by not requiring the fields to be not null as well. * https://github.com/elastic/elasticsearch/issues/112704 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index a59ef5bb1575d..908c9c5f197a8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -129,6 +129,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.ByteLength; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat; import org.elasticsearch.xpack.esql.expression.function.scalar.string.EndsWith; +import org.elasticsearch.xpack.esql.expression.function.scalar.string.Hash; import org.elasticsearch.xpack.esql.expression.function.scalar.string.LTrim; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Left; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Length; @@ -327,6 +328,7 @@ private static FunctionDefinition[][] functions() { def(ByteLength.class, ByteLength::new, "byte_length"), def(Concat.class, Concat::new, "concat"), def(EndsWith.class, EndsWith::new, "ends_with"), + def(Hash.class, Hash::new, "hash"), def(LTrim.class, LTrim::new, "ltrim"), def(Left.class, Left::new, "left"), def(Length.class, Length::new, "length"), diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java index 192ca6c43e57d..3cf0eef9074ad 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java @@ -34,6 +34,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.BitLength; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat; import org.elasticsearch.xpack.esql.expression.function.scalar.string.EndsWith; +import org.elasticsearch.xpack.esql.expression.function.scalar.string.Hash; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Left; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Locate; import org.elasticsearch.xpack.esql.expression.function.scalar.string.Repeat; @@ -64,6 +65,7 @@ public static List getNamedWriteables() { entries.add(E.ENTRY); entries.add(EndsWith.ENTRY); entries.add(Greatest.ENTRY); + entries.add(Hash.ENTRY); entries.add(Hypot.ENTRY); entries.add(In.ENTRY); entries.add(InsensitiveEquals.ENTRY); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java new file mode 100644 index 0000000000000..99c5908699ec2 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.string; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.compute.ann.Evaluator; +import org.elasticsearch.compute.ann.Fixed; +import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.xpack.esql.core.InvalidArgumentException; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.NodeInfo; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction; +import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.function.Function; + +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; + +public class Hash extends EsqlScalarFunction { + + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Hash", Hash::new); + + private final Expression algorithm; + private final Expression input; + + @FunctionInfo( + returnType = "keyword", + description = "Computes the hash of the input using various algorithms such as MD5, SHA, SHA-224, SHA-256, SHA-384, SHA-512." + ) + public Hash( + Source source, + @Param(name = "algorithm", type = { "keyword", "text" }, description = "Hash algorithm to use.") Expression algorithm, + @Param(name = "input", type = { "keyword", "text" }, description = "Input to hash.") Expression input + ) { + super(source, List.of(algorithm, input)); + this.algorithm = algorithm; + this.input = input; + } + + private Hash(StreamInput in) throws IOException { + this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), in.readNamedWriteable(Expression.class)); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + source().writeTo(out); + out.writeNamedWriteable(algorithm); + out.writeNamedWriteable(input); + } + + @Override + public String getWriteableName() { + return ENTRY.name; + } + + @Override + public DataType dataType() { + return DataType.KEYWORD; + } + + @Override + protected TypeResolution resolveType() { + if (childrenResolved() == false) { + return new TypeResolution("Unresolved children"); + } + + TypeResolution resolution = isString(algorithm, sourceText(), FIRST); + if (resolution.unresolved()) { + return resolution; + } + + return isString(input, sourceText(), SECOND); + } + + @Override + public boolean foldable() { + return algorithm.foldable() && input.foldable(); + } + + @Evaluator(warnExceptions = NoSuchAlgorithmException.class) + static BytesRef process( + @Fixed(includeInToString = false, scope = THREAD_LOCAL) BreakingBytesRefBuilder scratch, + BytesRef algorithm, + BytesRef input + ) throws NoSuchAlgorithmException { + return hash(scratch, MessageDigest.getInstance(algorithm.utf8ToString()), input); + } + + @Evaluator(extraName = "Constant") + static BytesRef processConstant( + @Fixed(includeInToString = false, scope = THREAD_LOCAL) BreakingBytesRefBuilder scratch, + @Fixed(scope = THREAD_LOCAL) HashFunction algorithm, + BytesRef input + ) { + return hash(scratch, algorithm.digest, input); + } + + private static BytesRef hash(BreakingBytesRefBuilder scratch, MessageDigest algorithm, BytesRef input) { + algorithm.reset(); + algorithm.update(input.bytes, input.offset, input.length); + var digest = algorithm.digest(); + scratch.clear(); + scratch.grow(digest.length * 2); + appendUtf8HexDigest(scratch, digest); + return scratch.bytesRefView(); + } + + private static final byte[] ASCII_HEX_BYTES = new byte[] { 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102 }; + + /** + * This function allows to append hex bytes dirrectly to the {@link BreakingBytesRefBuilder} + * bypassing unnecessary array allocations and byte array copying. + */ + private static void appendUtf8HexDigest(BreakingBytesRefBuilder scratch, byte[] bytes) { + for (byte b : bytes) { + scratch.append(ASCII_HEX_BYTES[b >> 4 & 0xf]); + scratch.append(ASCII_HEX_BYTES[b & 0xf]); + } + } + + @Override + public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { + if (algorithm.foldable()) { + try { + // hash function is created here in order to validate the algorithm is valid before evaluator is created + var hf = HashFunction.create((BytesRef) algorithm.fold()); + return new HashConstantEvaluator.Factory( + source(), + context -> new BreakingBytesRefBuilder(context.breaker(), "hash"), + new Function<>() { + @Override + public HashFunction apply(DriverContext context) { + return hf.copy(); + } + + @Override + public String toString() { + return hf.toString(); + } + }, + toEvaluator.apply(input) + ); + } catch (NoSuchAlgorithmException e) { + throw new InvalidArgumentException(e, "invalid algorithm for [{}]: {}", sourceText(), e.getMessage()); + } + } else { + return new HashEvaluator.Factory( + source(), + context -> new BreakingBytesRefBuilder(context.breaker(), "hash"), + toEvaluator.apply(algorithm), + toEvaluator.apply(input) + ); + } + } + + @Override + public Expression replaceChildren(List newChildren) { + return new Hash(source(), newChildren.get(0), newChildren.get(1)); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Hash::new, children().get(0), children().get(1)); + } + + public record HashFunction(String algorithm, MessageDigest digest) { + + public static HashFunction create(BytesRef literal) throws NoSuchAlgorithmException { + var algorithm = literal.utf8ToString(); + var digest = MessageDigest.getInstance(algorithm); + return new HashFunction(algorithm, digest); + } + + public HashFunction copy() { + try { + return new HashFunction(algorithm, MessageDigest.getInstance(algorithm)); + } catch (NoSuchAlgorithmException e) { + assert false : "Algorithm should be valid at this point"; + throw new IllegalStateException(e); + } + } + + @Override + public String toString() { + return algorithm; + } + } + + Expression algorithm() { + return algorithm; + } + + Expression input() { + return input; + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/AbstractExpressionSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/AbstractExpressionSerializationTests.java index 6dd0c5fe88afd..050293e58c19d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/AbstractExpressionSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/AbstractExpressionSerializationTests.java @@ -14,10 +14,15 @@ import org.elasticsearch.xpack.esql.plan.AbstractNodeSerializationTests; public abstract class AbstractExpressionSerializationTests extends AbstractNodeSerializationTests { + public static Expression randomChild() { return ReferenceAttributeTests.randomReferenceAttribute(false); } + public static Expression mutateExpression(Expression expression) { + return randomValueOtherThan(expression, AbstractExpressionSerializationTests::randomChild); + } + @Override protected final NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry(ExpressionWritables.getNamedWriteables()); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashSerializationTests.java new file mode 100644 index 0000000000000..f21105c2c8bca --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashSerializationTests.java @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.string; + +import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests; + +import java.io.IOException; + +public class HashSerializationTests extends AbstractExpressionSerializationTests { + + @Override + protected Hash createTestInstance() { + return new Hash(randomSource(), randomChild(), randomChild()); + } + + @Override + protected Hash mutateInstance(Hash instance) throws IOException { + return randomBoolean() + ? new Hash(instance.source(), mutateExpression(instance.algorithm()), instance.input()) + : new Hash(instance.source(), instance.algorithm(), mutateExpression(instance.input())); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java new file mode 100644 index 0000000000000..871bec7c06804 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.string; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.breaker.CircuitBreaker; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.MockBigArrays; +import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.core.InvalidArgumentException; +import org.elasticsearch.xpack.esql.core.expression.Literal; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.junit.After; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase.evaluator; +import static org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase.field; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; + +public class HashStaticTests extends ESTestCase { + + public void testInvalidAlgorithmLiteral() { + Source source = new Source(0, 0, "hast(\"invalid\", input)"); + DriverContext driverContext = driverContext(); + InvalidArgumentException e = expectThrows( + InvalidArgumentException.class, + () -> evaluator( + new Hash(source, new Literal(source, new BytesRef("invalid"), DataType.KEYWORD), field("input", DataType.KEYWORD)) + ).get(driverContext) + ); + assertThat(e.getMessage(), startsWith("invalid algorithm for [hast(\"invalid\", input)]: invalid MessageDigest not available")); + } + + /** + * The following fields and methods were borrowed from AbstractScalarFunctionTestCase + */ + private final List breakers = Collections.synchronizedList(new ArrayList<>()); + + private DriverContext driverContext() { + BigArrays bigArrays = new MockBigArrays(PageCacheRecycler.NON_RECYCLING_INSTANCE, ByteSizeValue.ofMb(256)).withCircuitBreaking(); + CircuitBreaker breaker = bigArrays.breakerService().getBreaker(CircuitBreaker.REQUEST); + breakers.add(breaker); + return new DriverContext(bigArrays, new BlockFactory(breaker, bigArrays)); + } + + @After + public void allMemoryReleased() { + for (CircuitBreaker breaker : breakers) { + assertThat(breaker.getUsed(), equalTo(0L)); + } + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashTests.java new file mode 100644 index 0000000000000..c5cdf97eccd17 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashTests.java @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.string; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.xpack.esql.core.InvalidArgumentException; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.List; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +public class HashTests extends AbstractScalarFunctionTestCase { + + public HashTests(@Name("TestCase") Supplier testCaseSupplier) { + this.testCase = testCaseSupplier.get(); + } + + @ParametersFactory + public static Iterable parameters() { + List cases = new ArrayList<>(); + for (String algorithm : List.of("MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512")) { + cases.addAll(createTestCases(algorithm)); + } + cases.add(new TestCaseSupplier("Invalid algorithm", List.of(DataType.KEYWORD, DataType.KEYWORD), () -> { + var input = randomAlphaOfLength(10); + return new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(new BytesRef("invalid"), DataType.KEYWORD, "algorithm"), + new TestCaseSupplier.TypedData(new BytesRef(input), DataType.KEYWORD, "input") + ), + "HashEvaluator[algorithm=Attribute[channel=0], input=Attribute[channel=1]]", + DataType.KEYWORD, + is(nullValue()) + ).withWarning("Line -1:-1: evaluation of [] failed, treating result as null. Only first 20 failures recorded.") + .withWarning("Line -1:-1: java.security.NoSuchAlgorithmException: invalid MessageDigest not available") + .withFoldingException(InvalidArgumentException.class, "invalid algorithm for []: invalid MessageDigest not available"); + })); + return parameterSuppliersFromTypedDataWithDefaultChecks(true, cases, (v, p) -> "string"); + } + + private static List createTestCases(String algorithm) { + return List.of( + createTestCase(algorithm, false, DataType.KEYWORD, DataType.KEYWORD), + createTestCase(algorithm, false, DataType.KEYWORD, DataType.TEXT), + createTestCase(algorithm, false, DataType.TEXT, DataType.KEYWORD), + createTestCase(algorithm, false, DataType.TEXT, DataType.TEXT), + createTestCase(algorithm, true, DataType.KEYWORD, DataType.KEYWORD), + createTestCase(algorithm, true, DataType.KEYWORD, DataType.TEXT), + createTestCase(algorithm, true, DataType.TEXT, DataType.KEYWORD), + createTestCase(algorithm, true, DataType.TEXT, DataType.TEXT) + ); + } + + private static TestCaseSupplier createTestCase(String algorithm, boolean forceLiteral, DataType algorithmType, DataType inputType) { + return new TestCaseSupplier(algorithm, List.of(algorithmType, inputType), () -> { + var input = randomFrom(TestCaseSupplier.stringCases(inputType)).get(); + return new TestCaseSupplier.TestCase( + List.of(createTypedData(algorithm, forceLiteral, algorithmType, "algorithm"), input), + forceLiteral + ? "HashConstantEvaluator[algorithm=" + algorithm + ", input=Attribute[channel=0]]" + : "HashEvaluator[algorithm=Attribute[channel=0], input=Attribute[channel=1]]", + DataType.KEYWORD, + equalTo(new BytesRef(hash(algorithm, BytesRefs.toString(input.data())))) + ); + }); + } + + private static TestCaseSupplier.TypedData createTypedData(String value, boolean forceLiteral, DataType type, String name) { + var data = new TestCaseSupplier.TypedData(new BytesRef(value), type, name); + return forceLiteral ? data.forceLiteral() : data; + } + + private static String hash(String algorithm, String input) { + try { + return HexFormat.of().formatHex(MessageDigest.getInstance(algorithm).digest(input.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unknown algorithm: " + algorithm); + } + } + + @Override + protected Expression build(Source source, List args) { + return new Hash(source, args.get(0), args.get(1)); + } +} diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml index 2a4cde9a680e9..b6d75048591e5 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml @@ -92,7 +92,7 @@ setup: - gt: {esql.functions.to_long: $functions_to_long} - match: {esql.functions.coalesce: $functions_coalesce} # Testing for the entire function set isn't feasbile, so we just check that we return the correct count as an approximation. - - length: {esql.functions: 129} # check the "sister" test below for a likely update to the same esql.functions length check + - length: {esql.functions: 130} # check the "sister" test below for a likely update to the same esql.functions length check --- "Basic ESQL usage output (telemetry) non-snapshot version": @@ -163,4 +163,4 @@ setup: - match: {esql.functions.cos: $functions_cos} - gt: {esql.functions.to_long: $functions_to_long} - match: {esql.functions.coalesce: $functions_coalesce} - - length: {esql.functions: 125} # check the "sister" test above for a likely update to the same esql.functions length check + - length: {esql.functions: 126} # check the "sister" test above for a likely update to the same esql.functions length check From c75e92a5e3d46cfae264fc54c0245be5a328c9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 18 Dec 2024 11:13:46 +0100 Subject: [PATCH 061/119] ESQL: Allow using the same index in FROM and LOOKUP (#118768) Changed the logic so that, instead of getting all the indices of the plan and then subtracting the lookup ones, it will now directly ignore the lookup part in the initial calculation. --- .../src/main/resources/lookup-join.csv-spec | 17 ++++++- .../xpack/esql/action/EsqlCapabilities.java | 5 ++ .../xpack/esql/planner/PlannerUtils.java | 24 ++++++++- .../xpack/esql/plugin/ComputeService.java | 49 ++----------------- 4 files changed, 47 insertions(+), 48 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec index 8bcc2c2ff3502..e75c68f4a379d 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec @@ -182,6 +182,22 @@ language_code:integer | language_name:keyword | country:keyword 2 | [German, German, German] | [Austria, Germany, Switzerland] ; +repeatedIndexOnFrom +required_capability: join_lookup_v7 +required_capability: join_lookup_repeated_index_from + +FROM languages_lookup +| LOOKUP JOIN languages_lookup ON language_code +| SORT language_code +; + +language_code:integer | language_name:keyword +1 | English +2 | French +3 | Spanish +4 | German +; + ############################################### # Filtering tests with languages_lookup index ############################################### @@ -1061,4 +1077,3 @@ ignoreOrder:true 2023-10-23T12:27:28.948Z | 172.21.2.113 | 2764889 | QA | null 2023-10-23T12:15:03.360Z | 172.21.2.162 | 3450233 | QA | null ; - diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index d6c1539088d47..bfc2675d59791 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -557,6 +557,11 @@ public enum Cap { */ JOIN_LOOKUP_V7(Build.current().isSnapshot()), + /** + * LOOKUP JOIN with the same index as the FROM + */ + JOIN_LOOKUP_REPEATED_INDEX_FROM(JOIN_LOOKUP_V7.isEnabled()), + /** * Fix for https://github.com/elastic/elasticsearch/issues/117054 */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/PlannerUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/PlannerUtils.java index 37f89891860d8..a312d048db0ad 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/PlannerUtils.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/PlannerUtils.java @@ -22,6 +22,7 @@ import org.elasticsearch.xpack.esql.core.expression.AttributeSet; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.predicate.Predicates; +import org.elasticsearch.xpack.esql.core.tree.Node; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.util.Holder; @@ -40,6 +41,7 @@ import org.elasticsearch.xpack.esql.plan.physical.ExchangeSinkExec; import org.elasticsearch.xpack.esql.plan.physical.ExchangeSourceExec; import org.elasticsearch.xpack.esql.plan.physical.FragmentExec; +import org.elasticsearch.xpack.esql.plan.physical.LookupJoinExec; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; import org.elasticsearch.xpack.esql.planner.mapper.LocalMapper; import org.elasticsearch.xpack.esql.planner.mapper.Mapper; @@ -48,9 +50,12 @@ import org.elasticsearch.xpack.esql.stats.SearchStats; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; import static java.util.Arrays.asList; import static org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference.DOC_VALUES; @@ -105,10 +110,27 @@ public static Set planConcreteIndices(PhysicalPlan plan) { return Set.of(); } var indices = new LinkedHashSet(); - plan.forEachUp(FragmentExec.class, f -> f.fragment().forEachUp(EsRelation.class, r -> indices.addAll(r.index().concreteIndices()))); + // TODO: This only works for LEFT join, we still need to support RIGHT join + forEachUpWithChildren(plan, node -> { + if (node instanceof FragmentExec f) { + f.fragment().forEachUp(EsRelation.class, r -> indices.addAll(r.index().concreteIndices())); + } + }, node -> node instanceof LookupJoinExec join ? List.of(join.left()) : node.children()); return indices; } + /** + * Similar to {@link Node#forEachUp(Consumer)}, but with a custom callback to get the node children. + */ + private static > void forEachUpWithChildren( + T node, + Consumer action, + Function> childrenGetter + ) { + childrenGetter.apply(node).forEach(c -> forEachUpWithChildren(c, action, childrenGetter)); + action.accept(node); + } + /** * Returns the original indices specified in the FROM command of the query. We need the original query to resolve alias filters. */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java index 9b59b98a7cdc2..e77a2443df2dd 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java @@ -63,12 +63,8 @@ import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.enrich.EnrichLookupService; import org.elasticsearch.xpack.esql.enrich.LookupFromIndexService; -import org.elasticsearch.xpack.esql.plan.logical.EsRelation; -import org.elasticsearch.xpack.esql.plan.logical.join.Join; import org.elasticsearch.xpack.esql.plan.physical.ExchangeSinkExec; import org.elasticsearch.xpack.esql.plan.physical.ExchangeSourceExec; -import org.elasticsearch.xpack.esql.plan.physical.FragmentExec; -import org.elasticsearch.xpack.esql.plan.physical.LookupJoinExec; import org.elasticsearch.xpack.esql.plan.physical.OutputExec; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; import org.elasticsearch.xpack.esql.planner.EsPhysicalOperationProviders; @@ -81,7 +77,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -167,11 +162,9 @@ public void execute( Map clusterToConcreteIndices = transportService.getRemoteClusterService() .groupIndices(SearchRequest.DEFAULT_INDICES_OPTIONS, PlannerUtils.planConcreteIndices(physicalPlan).toArray(String[]::new)); QueryPragmas queryPragmas = configuration.pragmas(); - Set lookupIndexNames = findLookupIndexNames(physicalPlan); - Set concreteIndexNames = selectConcreteIndices(clusterToConcreteIndices, lookupIndexNames); if (dataNodePlan == null) { - if (concreteIndexNames.isEmpty() == false) { - String error = "expected no concrete indices without data node plan; got " + concreteIndexNames; + if (clusterToConcreteIndices.values().stream().allMatch(v -> v.indices().length == 0) == false) { + String error = "expected no concrete indices without data node plan; got " + clusterToConcreteIndices; assert false : error; listener.onFailure(new IllegalStateException(error)); return; @@ -194,7 +187,7 @@ public void execute( return; } } else { - if (concreteIndexNames.isEmpty()) { + if (clusterToConcreteIndices.values().stream().allMatch(v -> v.indices().length == 0)) { var error = "expected concrete indices with data node plan but got empty; data node plan " + dataNodePlan; assert false : error; listener.onFailure(new IllegalStateException(error)); @@ -268,42 +261,6 @@ public void execute( } } - private Set selectConcreteIndices(Map clusterToConcreteIndices, Set indexesToIgnore) { - Set concreteIndexNames = new HashSet<>(); - clusterToConcreteIndices.forEach((clusterAlias, concreteIndices) -> { - for (String index : concreteIndices.indices()) { - if (indexesToIgnore.contains(index) == false) { - concreteIndexNames.add(index); - } - } - }); - return concreteIndexNames; - } - - private Set findLookupIndexNames(PhysicalPlan physicalPlan) { - Set lookupIndexNames = new HashSet<>(); - // When planning JOIN on the coordinator node: "LookupJoinExec.lookup()->FragmentExec.fragment()->EsRelation.index()" - physicalPlan.forEachDown( - LookupJoinExec.class, - lookupJoinExec -> lookupJoinExec.lookup() - .forEachDown( - FragmentExec.class, - frag -> frag.fragment().forEachDown(EsRelation.class, esRelation -> lookupIndexNames.add(esRelation.index().name())) - ) - ); - // When planning JOIN on the data node: "FragmentExec.fragment()->Join.right()->EsRelation.index()" - // TODO this only works for LEFT join, so we still need to support RIGHT join - physicalPlan.forEachDown( - FragmentExec.class, - fragmentExec -> fragmentExec.fragment() - .forEachDown( - Join.class, - join -> join.right().forEachDown(EsRelation.class, esRelation -> lookupIndexNames.add(esRelation.index().name())) - ) - ); - return lookupIndexNames; - } - // For queries like: FROM logs* | LIMIT 0 (including cross-cluster LIMIT 0 queries) private static void updateShardCountForCoordinatorOnlyQuery(EsqlExecutionInfo execInfo) { if (execInfo.isCrossClusterSearch()) { From 37807b83b7500cbe861817fa1e20aef07d72f6d9 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 18 Dec 2024 11:45:41 +0100 Subject: [PATCH 062/119] Support flattened label field with downsampling. (#118816) If flattened field is configured as non-dimension and non-metric field, then downsampling fails to execute successfully. Downsampling doesn't know how to use the flattened field or how to serialize it. This change addresses this. Closes #116319 --- docs/changelog/118816.yaml | 6 + .../flattened/FlattenedFieldMapper.java | 9 +- .../FlattenedFieldSyntheticWriterHelper.java | 8 +- .../downsample/70_flattened_field_type.yml | 307 ++++++++++++++++++ .../xpack/downsample/FieldValueFetcher.java | 11 +- .../xpack/downsample/LabelFieldProducer.java | 42 ++- .../downsample/LabelFieldProducerTests.java | 54 +++ 7 files changed, 428 insertions(+), 9 deletions(-) create mode 100644 docs/changelog/118816.yaml create mode 100644 x-pack/plugin/downsample/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/downsample/70_flattened_field_type.yml diff --git a/docs/changelog/118816.yaml b/docs/changelog/118816.yaml new file mode 100644 index 0000000000000..f1c1eac90dbcf --- /dev/null +++ b/docs/changelog/118816.yaml @@ -0,0 +1,6 @@ +pr: 118816 +summary: Support flattened field with downsampling +area: Downsampling +type: bug +issues: + - 116319 diff --git a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java index 93a2157b2338a..de2632165b0cc 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java @@ -52,6 +52,7 @@ import org.elasticsearch.index.mapper.DocumentParserContext; import org.elasticsearch.index.mapper.DynamicFieldType; import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.MapperBuilderContext; @@ -670,7 +671,7 @@ public static final class RootFlattenedFieldType extends StringFieldType impleme private final boolean isDimension; private final int ignoreAbove; - public RootFlattenedFieldType( + RootFlattenedFieldType( String name, boolean indexed, boolean hasDocValues, @@ -682,7 +683,7 @@ public RootFlattenedFieldType( this(name, indexed, hasDocValues, meta, splitQueriesOnWhitespace, eagerGlobalOrdinals, Collections.emptyList(), ignoreAbove); } - public RootFlattenedFieldType( + RootFlattenedFieldType( String name, boolean indexed, boolean hasDocValues, @@ -806,6 +807,10 @@ public MappedFieldType getChildFieldType(String childPath) { return new KeyedFlattenedFieldType(name(), childPath, this); } + public MappedFieldType getKeyedFieldType() { + return new KeywordFieldMapper.KeywordFieldType(name() + KEYED_FIELD_SUFFIX); + } + @Override public boolean isDimension() { return isDimension; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldSyntheticWriterHelper.java b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldSyntheticWriterHelper.java index 950fef95772fb..53f68fb6edeef 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldSyntheticWriterHelper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldSyntheticWriterHelper.java @@ -55,7 +55,7 @@ * }` * */ -class FlattenedFieldSyntheticWriterHelper { +public class FlattenedFieldSyntheticWriterHelper { private record Prefix(List prefix) { @@ -225,17 +225,17 @@ public boolean equals(Object obj) { } } - interface SortedKeyedValues { + public interface SortedKeyedValues { BytesRef next() throws IOException; } private final SortedKeyedValues sortedKeyedValues; - FlattenedFieldSyntheticWriterHelper(final SortedKeyedValues sortedKeyedValues) { + public FlattenedFieldSyntheticWriterHelper(final SortedKeyedValues sortedKeyedValues) { this.sortedKeyedValues = sortedKeyedValues; } - void write(final XContentBuilder b) throws IOException { + public void write(final XContentBuilder b) throws IOException { KeyValue curr = new KeyValue(sortedKeyedValues.next()); KeyValue prev = KeyValue.EMPTY; final List values = new ArrayList<>(); diff --git a/x-pack/plugin/downsample/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/downsample/70_flattened_field_type.yml b/x-pack/plugin/downsample/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/downsample/70_flattened_field_type.yml new file mode 100644 index 0000000000000..0f586ec0ed669 --- /dev/null +++ b/x-pack/plugin/downsample/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/downsample/70_flattened_field_type.yml @@ -0,0 +1,307 @@ +--- +"A flattened label field": + - do: + indices.create: + index: source_index + body: + settings: + number_of_shards: 1 + index: + mode: time_series + routing_path: [ metricset, k8s.pod.uid ] + time_series: + start_time: 2021-04-28T00:00:00Z + end_time: 2021-04-29T00:00:00Z + mappings: + subobjects: false + properties: + "@timestamp": + type: date + metricset: + type: keyword + time_series_dimension: true + k8s: + properties: + pod: + properties: + uid: + type: keyword + time_series_dimension: true + name: + type: keyword + agent: + type: flattened + value: + type: long + time_series_metric: gauge + + - do: + bulk: + refresh: true + index: source_index + body: + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:04.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.4" }, "value": 10 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:24.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.5" }, "value": 20 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T20:50:44.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.6" }, "value": 12 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T20:51:04.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.7" }, "value": 15 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:03.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.7" }, "value": 9 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:23.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.8" }, "value": 16 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T19:50:53.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.9" }, "value": 25 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T19:51:03.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.10" }, "value": 17 }}' + + - do: + indices.put_settings: + index: source_index + body: + index.blocks.write: true + + - do: + indices.downsample: + index: source_index + target_index: target_index + body: > + { + "fixed_interval": "1h" + } + - is_true: acknowledged + + - do: + search: + index: target_index + body: + sort: [ "_tsid", "@timestamp" ] + + - length: { hits.hits: 4 } + - match: { hits.hits.0._source._doc_count: 2 } + - match: { hits.hits.0._source.k8s\.pod\.uid: df3145b3-0563-4d3b-a0f7-897eb2876ea9 } + - match: { hits.hits.0._source.@timestamp: 2021-04-28T18:00:00.000Z } + - match: { hits.hits.0._source.k8s\.agent: { "id": "second", "version": "2.1.8" } } + + - match: { hits.hits.1._source._doc_count: 2 } + - match: { hits.hits.1._source.k8s\.pod\.uid: df3145b3-0563-4d3b-a0f7-897eb2876ea9 } + - match: { hits.hits.1._source.@timestamp: 2021-04-28T19:00:00.000Z } + - match: { hits.hits.1._source.k8s\.agent: { "id": "second", "version": "2.1.10" } } + + - match: { hits.hits.2._source._doc_count: 2 } + - match: { hits.hits.2._source.k8s\.pod\.uid: 947e4ced-1786-4e53-9e0c-5c447e959507 } + - match: { hits.hits.2._source.@timestamp: 2021-04-28T18:00:00.000Z } + - match: { hits.hits.2._source.k8s\.agent: { "id": "first", "version": "2.0.5" } } + + - match: { hits.hits.3._source._doc_count: 2 } + - match: { hits.hits.3._source.k8s\.pod\.uid: 947e4ced-1786-4e53-9e0c-5c447e959507 } + - match: { hits.hits.3._source.@timestamp: 2021-04-28T20:00:00.000Z } + - match: { hits.hits.3._source.k8s\.agent: { "id": "first", "version": "2.0.7" } } + +--- +"A flattened label field with no doc values": + - do: + indices.create: + index: source_index + body: + settings: + number_of_shards: 1 + index: + mode: time_series + routing_path: [ metricset, k8s.pod.uid ] + time_series: + start_time: 2021-04-28T00:00:00Z + end_time: 2021-04-29T00:00:00Z + mappings: + subobjects: false + properties: + "@timestamp": + type: date + metricset: + type: keyword + time_series_dimension: true + k8s: + properties: + pod: + properties: + uid: + type: keyword + time_series_dimension: true + name: + type: keyword + agent: + type: flattened + doc_values: false + value: + type: long + time_series_metric: gauge + + - do: + bulk: + refresh: true + index: source_index + body: + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:04.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.4" }, "value": 10 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:24.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.5" }, "value": 20 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T20:50:44.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.6" }, "value": 12 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T20:51:04.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.7" }, "value": 15 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:03.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.7" }, "value": 9 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:23.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.8" }, "value": 16 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T19:50:53.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.9" }, "value": 25 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T19:51:03.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.10" }, "value": 17 }}' + + - do: + indices.put_settings: + index: source_index + body: + index.blocks.write: true + + - do: + indices.downsample: + index: source_index + target_index: target_index + body: > + { + "fixed_interval": "1h" + } + - is_true: acknowledged + + - do: + search: + index: target_index + body: + sort: [ "_tsid", "@timestamp" ] + + - length: { hits.hits: 4 } + - match: { hits.hits.0._source._doc_count: 2 } + - match: { hits.hits.0._source.k8s\.pod\.uid: df3145b3-0563-4d3b-a0f7-897eb2876ea9 } + - match: { hits.hits.0._source.@timestamp: 2021-04-28T18:00:00.000Z } + - is_false: hits.hits.0._source.k8s\.agent + + - match: { hits.hits.1._source._doc_count: 2 } + - match: { hits.hits.1._source.k8s\.pod\.uid: df3145b3-0563-4d3b-a0f7-897eb2876ea9 } + - match: { hits.hits.1._source.@timestamp: 2021-04-28T19:00:00.000Z } + - is_false: hits.hits.1._source.k8s\.agent + + - match: { hits.hits.2._source._doc_count: 2 } + - match: { hits.hits.2._source.k8s\.pod\.uid: 947e4ced-1786-4e53-9e0c-5c447e959507 } + - match: { hits.hits.2._source.@timestamp: 2021-04-28T18:00:00.000Z } + - is_false: hits.hits.2._source.k8s\.agent + + - match: { hits.hits.3._source._doc_count: 2 } + - match: { hits.hits.3._source.k8s\.pod\.uid: 947e4ced-1786-4e53-9e0c-5c447e959507 } + - match: { hits.hits.3._source.@timestamp: 2021-04-28T20:00:00.000Z } + - is_false: hits.hits.3._source.k8s\.agent + +--- +"A flattened label field with mixed content": + - do: + indices.create: + index: source_index + body: + settings: + number_of_shards: 1 + index: + mode: time_series + routing_path: [ metricset, k8s.pod.uid ] + time_series: + start_time: 2021-04-28T00:00:00Z + end_time: 2021-04-29T00:00:00Z + mappings: + subobjects: false + properties: + "@timestamp": + type: date + metricset: + type: keyword + time_series_dimension: true + k8s: + properties: + pod: + properties: + uid: + type: keyword + time_series_dimension: true + name: + type: keyword + agent: + type: flattened + null_value: my_null_value + value: + type: long + time_series_metric: gauge + + - do: + bulk: + refresh: true + index: source_index + body: + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:04.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.4", "versions": ["1", "2", "3"], "optional_version": null, "dotted.version": "1.1", "numeric_version": 11 }, "value": 10 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:24.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.5", "versions": ["1", "2", "3"], "optional_version": null, "dotted.version": "1.1", "numeric_version": 11}, "value": 20 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T20:50:44.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.6", "versions": ["1", "2", "3"], "optional_version": null, "dotted.version": "1.1", "numeric_version": 11}, "value": 12 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T20:51:04.467Z", "metricset": "pod", "k8s": {"pod": {"name": "cat", "uid":"947e4ced-1786-4e53-9e0c-5c447e959507"}, "agent": { "id": "first", "version": "2.0.7", "versions": ["1", "2", "3"], "optional_version": null, "dotted.version": "1.1", "numeric_version": 11}, "value": 15 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:03.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.7", "versions": ["1", "2", "3"], "optional_version": null, "dotted.version": "1.1", "numeric_version": 11}, "value": 9 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T18:50:23.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.8", "versions": ["1", "2", "3"], "optional_version": null, "dotted.version": "1.1", "numeric_version": 11}, "value": 16 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T19:50:53.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.9", "versions": ["1", "2", "3"], "optional_version": null, "dotted.version": "1.1", "numeric_version": 11}, "value": 25 }}' + - '{"index": {}}' + - '{"@timestamp": "2021-04-28T19:51:03.142Z", "metricset": "pod", "k8s": {"pod": {"name": "dog", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876ea9"}, "agent": { "id": "second", "version": "2.1.10", "versions": ["1", "2", "3"], "optional_version": null, "dotted.version": "1.1", "numeric_version": 11}, "value": 17 }}' + + - do: + indices.put_settings: + index: source_index + body: + index.blocks.write: true + + - do: + indices.downsample: + index: source_index + target_index: target_index + body: > + { + "fixed_interval": "1h" + } + - is_true: acknowledged + + - do: + search: + index: target_index + body: + sort: [ "_tsid", "@timestamp" ] + + - length: { hits.hits: 4 } + - match: { hits.hits.0._source._doc_count: 2 } + - match: { hits.hits.0._source.k8s\.pod\.uid: df3145b3-0563-4d3b-a0f7-897eb2876ea9 } + - match: { hits.hits.0._source.@timestamp: 2021-04-28T18:00:00.000Z } + - match: { hits.hits.0._source.k8s\.agent: { "id": "second", "version": "2.1.8", "versions": ["1", "2", "3"], "dotted": {"version": "1.1"}, "numeric_version": "11", optional_version: "my_null_value" } } + + - match: { hits.hits.1._source._doc_count: 2 } + - match: { hits.hits.1._source.k8s\.pod\.uid: df3145b3-0563-4d3b-a0f7-897eb2876ea9 } + - match: { hits.hits.1._source.@timestamp: 2021-04-28T19:00:00.000Z } + - match: { hits.hits.1._source.k8s\.agent: { "id": "second", "version": "2.1.10", "versions": ["1", "2", "3"], "dotted": {"version": "1.1"}, "numeric_version": "11", optional_version: "my_null_value" } } + + - match: { hits.hits.2._source._doc_count: 2 } + - match: { hits.hits.2._source.k8s\.pod\.uid: 947e4ced-1786-4e53-9e0c-5c447e959507 } + - match: { hits.hits.2._source.@timestamp: 2021-04-28T18:00:00.000Z } + - match: { hits.hits.2._source.k8s\.agent: { "id": "first", "version": "2.0.5", "versions": ["1", "2", "3"], "dotted": {"version": "1.1"}, "numeric_version": "11", optional_version: "my_null_value" } } + + - match: { hits.hits.3._source._doc_count: 2 } + - match: { hits.hits.3._source.k8s\.pod\.uid: 947e4ced-1786-4e53-9e0c-5c447e959507 } + - match: { hits.hits.3._source.@timestamp: 2021-04-28T20:00:00.000Z } + - match: { hits.hits.3._source.k8s\.agent: { "id": "first", "version": "2.0.7", "versions": ["1", "2", "3"], "dotted": {"version": "1.1"}, "numeric_version": "11", optional_version: "my_null_value" } } diff --git a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/FieldValueFetcher.java b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/FieldValueFetcher.java index 74375bbe27939..3657e4989ccbd 100644 --- a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/FieldValueFetcher.java +++ b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/FieldValueFetcher.java @@ -12,6 +12,7 @@ import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.NumberFieldMapper; +import org.elasticsearch.index.mapper.flattened.FlattenedFieldMapper; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper; @@ -65,6 +66,8 @@ private AbstractDownsampleFieldProducer createFieldProducer() { // If field is not a metric, we downsample it as a label if ("histogram".equals(fieldType.typeName())) { return new LabelFieldProducer.HistogramLastLabelFieldProducer(name()); + } else if ("flattened".equals(fieldType.typeName())) { + return new LabelFieldProducer.FlattenedLastValueFieldProducer(name()); } return new LabelFieldProducer.LabelLastValueFieldProducer(name()); } @@ -90,7 +93,13 @@ static List create(SearchExecutionContext context, String[] f } } else { if (context.fieldExistsInIndex(field)) { - final IndexFieldData fieldData = context.getForField(fieldType, MappedFieldType.FielddataOperation.SEARCH); + final IndexFieldData fieldData; + if (fieldType instanceof FlattenedFieldMapper.RootFlattenedFieldType flattenedFieldType) { + var keyedFieldType = flattenedFieldType.getKeyedFieldType(); + fieldData = context.getForField(keyedFieldType, MappedFieldType.FielddataOperation.SEARCH); + } else { + fieldData = context.getForField(fieldType, MappedFieldType.FielddataOperation.SEARCH); + } final String fieldName = context.isMultiField(field) ? fieldType.name().substring(0, fieldType.name().lastIndexOf('.')) : fieldType.name(); diff --git a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/LabelFieldProducer.java b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/LabelFieldProducer.java index 05b4852d0dfd3..b211c5bfb0d12 100644 --- a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/LabelFieldProducer.java +++ b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/LabelFieldProducer.java @@ -7,8 +7,10 @@ package org.elasticsearch.xpack.downsample; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.index.fielddata.FormattedDocValues; import org.elasticsearch.index.fielddata.HistogramValue; +import org.elasticsearch.index.mapper.flattened.FlattenedFieldSyntheticWriterHelper; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper.Metric; @@ -141,14 +143,14 @@ public void reset() { } } - static class AggregateMetricFieldProducer extends LabelLastValueFieldProducer { + static final class AggregateMetricFieldProducer extends LabelLastValueFieldProducer { AggregateMetricFieldProducer(String name, Metric metric) { super(name, new LastValueLabel(metric.name())); } } - public static class HistogramLastLabelFieldProducer extends LabelLastValueFieldProducer { + static final class HistogramLastLabelFieldProducer extends LabelLastValueFieldProducer { HistogramLastLabelFieldProducer(String name) { super(name); } @@ -167,4 +169,40 @@ public void write(XContentBuilder builder) throws IOException { } } } + + static final class FlattenedLastValueFieldProducer extends LabelLastValueFieldProducer { + + FlattenedLastValueFieldProducer(String name) { + super(name); + } + + @Override + public void write(XContentBuilder builder) throws IOException { + if (isEmpty() == false) { + builder.startObject(name()); + + var value = label.get(); + List list; + if (value instanceof Object[] values) { + list = new ArrayList<>(values.length); + for (Object v : values) { + list.add(new BytesRef(v.toString())); + } + } else { + list = List.of(new BytesRef(value.toString())); + } + + var iterator = list.iterator(); + var helper = new FlattenedFieldSyntheticWriterHelper(() -> { + if (iterator.hasNext()) { + return iterator.next(); + } else { + return null; + } + }); + helper.write(builder); + builder.endObject(); + } + } + } } diff --git a/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/LabelFieldProducerTests.java b/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/LabelFieldProducerTests.java index 469e00f7af9af..844eb1b8e27d8 100644 --- a/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/LabelFieldProducerTests.java +++ b/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/LabelFieldProducerTests.java @@ -7,10 +7,18 @@ package org.elasticsearch.xpack.downsample; +import org.elasticsearch.common.Strings; import org.elasticsearch.index.fielddata.FormattedDocValues; import org.elasticsearch.search.aggregations.AggregatorTestCase; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentType; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; public class LabelFieldProducerTests extends AggregatorTestCase { @@ -93,4 +101,50 @@ public Object nextValue() { assertTrue(producer.isEmpty()); assertNull(producer.label().get()); } + + public void testFlattenedLastValueFieldProducer() throws IOException { + var producer = new LabelFieldProducer.FlattenedLastValueFieldProducer("dummy"); + assertTrue(producer.isEmpty()); + assertEquals("dummy", producer.name()); + assertEquals("last_value", producer.label().name()); + + var bytes = List.of("a\0value_a", "b\0value_b", "c\0value_c", "d\0value_d"); + var docValues = new FormattedDocValues() { + + Iterator iterator = bytes.iterator(); + + @Override + public boolean advanceExact(int docId) { + return true; + } + + @Override + public int docValueCount() { + return bytes.size(); + } + + @Override + public Object nextValue() { + return iterator.next(); + } + }; + + producer.collect(docValues, 1); + assertFalse(producer.isEmpty()); + assertEquals("a\0value_a", (((Object[]) producer.label().get())[0]).toString()); + assertEquals("b\0value_b", (((Object[]) producer.label().get())[1]).toString()); + assertEquals("c\0value_c", (((Object[]) producer.label().get())[2]).toString()); + assertEquals("d\0value_d", (((Object[]) producer.label().get())[3]).toString()); + + var builder = new XContentBuilder(XContentType.JSON.xContent(), new ByteArrayOutputStream()); + builder.startObject(); + producer.write(builder); + builder.endObject(); + var content = Strings.toString(builder); + assertThat(content, equalTo("{\"dummy\":{\"a\":\"value_a\",\"b\":\"value_b\",\"c\":\"value_c\",\"d\":\"value_d\"}}")); + + producer.reset(); + assertTrue(producer.isEmpty()); + assertNull(producer.label().get()); + } } From b34867115cee9461f6aaf67aee05fb2c303b6f7d Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Wed, 18 Dec 2024 13:09:33 +0200 Subject: [PATCH 063/119] Lookup join on multiple join fields not yet supported (#118858) --- docs/changelog/118858.yaml | 5 +++ .../xpack/esql/parser/LogicalPlanBuilder.java | 5 +++ .../xpack/esql/analysis/ParsingTests.java | 41 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 docs/changelog/118858.yaml diff --git a/docs/changelog/118858.yaml b/docs/changelog/118858.yaml new file mode 100644 index 0000000000000..a2161df1c84c7 --- /dev/null +++ b/docs/changelog/118858.yaml @@ -0,0 +1,5 @@ +pr: 118858 +summary: Lookup join on multiple join fields not yet supported +area: ES|QL +type: enhancement +issues: [] diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java index 24398afa18010..49d77bc36fb2e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java @@ -564,6 +564,11 @@ public PlanFactory visitJoinCommand(EsqlBaseParser.JoinCommandContext ctx) { } } + var matchFieldsCount = joinFields.size(); + if (matchFieldsCount > 1) { + throw new ParsingException(source, "JOIN ON clause only supports one field at the moment, found [{}]", matchFieldsCount); + } + return p -> new LookupJoin(source, p, right, joinFields); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java index 205c8943d4e3c..90a215653f251 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.esql.LoadMapping; +import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry; @@ -111,6 +112,46 @@ public void testTooBigQuery() { assertEquals("-1:-1: ESQL statement is too large [1000011 characters > 1000000]", error(query.toString())); } + public void testJoinOnConstant() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assertEquals( + "1:55: JOIN ON clause only supports fields at the moment, found [123]", + error("row languages = 1, gender = \"f\" | lookup join test on 123") + ); + assertEquals( + "1:55: JOIN ON clause only supports fields at the moment, found [\"abc\"]", + error("row languages = 1, gender = \"f\" | lookup join test on \"abc\"") + ); + assertEquals( + "1:55: JOIN ON clause only supports fields at the moment, found [false]", + error("row languages = 1, gender = \"f\" | lookup join test on false") + ); + } + + public void testJoinOnMultipleFields() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assertEquals( + "1:35: JOIN ON clause only supports one field at the moment, found [2]", + error("row languages = 1, gender = \"f\" | lookup join test on gender, languages") + ); + } + + public void testJoinTwiceOnTheSameField() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assertEquals( + "1:35: JOIN ON clause only supports one field at the moment, found [2]", + error("row languages = 1, gender = \"f\" | lookup join test on languages, languages") + ); + } + + public void testJoinTwiceOnTheSameField_TwoLookups() { + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assertEquals( + "1:80: JOIN ON clause only supports one field at the moment, found [2]", + error("row languages = 1, gender = \"f\" | lookup join test on languages | eval x = 1 | lookup join test on gender, gender") + ); + } + private String functionName(EsqlFunctionRegistry registry, Expression functionCall) { for (FunctionDefinition def : registry.listFunctions()) { if (functionCall.getClass().equals(def.clazz())) { From 4974336221f3b26678bc31ded4b34083e35523ae Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 18 Dec 2024 12:25:27 +0100 Subject: [PATCH 064/119] Fix synthetic recovery source version validation. (#118924) The validation didn't work as expected, because created version was always 0 (which is created version default value). Moving the validation to a place that does have access to the index version. --- .../indices.create/20_synthetic_source.yml | 39 ++++++++++++++ .../elasticsearch/index/IndexSettings.java | 38 ++++++------- .../index/mapper/MapperFeatures.java | 3 +- .../index/mapper/SourceFieldMapper.java | 1 + .../index/mapper/SourceFieldMapperTests.java | 54 ------------------- 5 files changed, 61 insertions(+), 74 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml index af3d88fb35734..fc0bfa144bbc4 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml @@ -2012,3 +2012,42 @@ synthetic_source with copy_to pointing inside dynamic object: hits.hits.2.fields: c.copy.keyword: [ "hello", "zap" ] +--- +create index with use_synthetic_source: + - requires: + cluster_features: ["mapper.synthetic_recovery_source"] + reason: requires synthetic recovery source + + - do: + indices.create: + index: test + body: + settings: + index: + recovery: + use_synthetic_source: true + mapping: + source: + mode: synthetic + + - do: + indices.get_settings: {} + - match: { test.settings.index.mapping.source.mode: synthetic} + - is_true: test.settings.index.recovery.use_synthetic_source + + - do: + bulk: + index: test + refresh: true + body: + - '{ "create": { } }' + - '{ "field": "aaaa" }' + - '{ "create": { } }' + - '{ "field": "bbbb" }' + + - do: + indices.disk_usage: + index: test + run_expensive_tasks: true + - gt: { test.fields.field.total_in_bytes: 0 } + - is_false: test.fields.field._recovery_source diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index b15828c5594ae..9273888b9ec91 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -685,29 +685,11 @@ public void validate(Boolean enabled, Map, Object> settings) { ); } } - - // Verify that all nodes can handle this setting - var version = (IndexVersion) settings.get(SETTING_INDEX_VERSION_CREATED); - if (version.before(IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY) - && version.between( - IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY_BACKPORT, - IndexVersions.UPGRADE_TO_LUCENE_10_0_0 - ) == false) { - throw new IllegalArgumentException( - String.format( - Locale.ROOT, - "The setting [%s] is unavailable on this cluster because some nodes are running older " - + "versions that do not support it. Please upgrade all nodes to the latest version " - + "and try again.", - RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey() - ) - ); - } } @Override public Iterator> settings() { - List> res = List.of(INDEX_MAPPER_SOURCE_MODE_SETTING, SETTING_INDEX_VERSION_CREATED, MODE); + List> res = List.of(INDEX_MAPPER_SOURCE_MODE_SETTING, MODE); return res.iterator(); } }, @@ -1050,6 +1032,24 @@ public IndexSettings(final IndexMetadata indexMetadata, final Settings nodeSetti indexMappingSourceMode = scopedSettings.get(INDEX_MAPPER_SOURCE_MODE_SETTING); recoverySourceEnabled = RecoverySettings.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.get(nodeSettings); recoverySourceSyntheticEnabled = scopedSettings.get(RECOVERY_USE_SYNTHETIC_SOURCE_SETTING); + if (recoverySourceSyntheticEnabled) { + // Verify that all nodes can handle this setting + if (version.before(IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY) + && version.between( + IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY_BACKPORT, + IndexVersions.UPGRADE_TO_LUCENE_10_0_0 + ) == false) { + throw new IllegalArgumentException( + String.format( + Locale.ROOT, + "The setting [%s] is unavailable on this cluster because some nodes are running older " + + "versions that do not support it. Please upgrade all nodes to the latest version " + + "and try again.", + RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey() + ) + ); + } + } scopedSettings.addSettingsUpdateConsumer( MergePolicyConfig.INDEX_COMPOUND_FORMAT_SETTING, diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java index 276d3e151361c..5dbaf0e0f40ad 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java @@ -77,7 +77,8 @@ public Set getTestFeatures() { DocumentParser.FIX_PARSING_SUBOBJECTS_FALSE_DYNAMIC_FALSE, CONSTANT_KEYWORD_SYNTHETIC_SOURCE_WRITE_FIX, META_FETCH_FIELDS_ERROR_CODE_CHANGED, - SPARSE_VECTOR_STORE_SUPPORT + SPARSE_VECTOR_STORE_SUPPORT, + SourceFieldMapper.SYNTHETIC_RECOVERY_SOURCE ); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java index 85f4217811a84..5f1ba6f0ab2a1 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java @@ -56,6 +56,7 @@ public class SourceFieldMapper extends MetadataFieldMapper { "mapper.source.remove_synthetic_source_only_validation" ); public static final NodeFeature SOURCE_MODE_FROM_INDEX_SETTING = new NodeFeature("mapper.source.mode_from_index_setting"); + public static final NodeFeature SYNTHETIC_RECOVERY_SOURCE = new NodeFeature("mapper.synthetic_recovery_source"); public static final String NAME = "_source"; public static final String RECOVERY_SOURCE_NAME = "_recovery_source"; diff --git a/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java index 378920d0e6db5..b7693513a434d 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java @@ -465,60 +465,6 @@ public void testRecoverySourceWitInvalidSettings() { ) ); } - { - Settings settings = Settings.builder() - .put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), SourceFieldMapper.Mode.SYNTHETIC.toString()) - .put(IndexSettings.RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey(), true) - .build(); - IllegalArgumentException exc = expectThrows( - IllegalArgumentException.class, - () -> createMapperService( - IndexVersionUtils.randomPreviousCompatibleVersion(random(), IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY_BACKPORT), - settings, - () -> false, - topMapping(b -> {}) - ) - ); - assertThat( - exc.getMessage(), - containsString( - String.format( - Locale.ROOT, - "The setting [%s] is unavailable on this cluster", - IndexSettings.RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey() - ) - ) - ); - } - { - Settings settings = Settings.builder() - .put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), SourceFieldMapper.Mode.SYNTHETIC.toString()) - .put(IndexSettings.RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey(), true) - .build(); - IllegalArgumentException exc = expectThrows( - IllegalArgumentException.class, - () -> createMapperService( - IndexVersionUtils.randomVersionBetween( - random(), - IndexVersions.UPGRADE_TO_LUCENE_10_0_0, - IndexVersions.DEPRECATE_SOURCE_MODE_MAPPER - ), - settings, - () -> false, - topMapping(b -> {}) - ) - ); - assertThat( - exc.getMessage(), - containsString( - String.format( - Locale.ROOT, - "The setting [%s] is unavailable on this cluster", - IndexSettings.RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey() - ) - ) - ); - } } public void testRecoverySourceWithSyntheticSource() throws IOException { From 4fbe306a1e0d137ae299dab44659a263a7f9dc46 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Wed, 18 Dec 2024 12:57:33 +0100 Subject: [PATCH 065/119] Revert "Support mTLS in Elastic Inference Service plugin (#116423)" (#118765) (#118902) --- docs/changelog/116423.yaml | 5 - .../xpack/core/ssl/SSLService.java | 2 - .../core/LocalStateCompositeXPackPlugin.java | 2 +- .../xpack/core/ssl/SSLServiceTests.java | 3 +- .../ShardBulkInferenceActionFilterIT.java | 3 +- .../integration/ModelRegistryIT.java | 4 +- .../inference/src/main/java/module-info.java | 1 - .../xpack/inference/InferencePlugin.java | 101 +++++------------- .../external/http/HttpClientManager.java | 44 -------- .../TextSimilarityRankRetrieverBuilder.java | 11 +- .../ElasticInferenceServiceSettings.java | 24 +---- .../SemanticTextClusterMetadataTests.java | 3 +- .../xpack/inference/InferencePluginTests.java | 65 ----------- .../inference/LocalStateInferencePlugin.java | 71 ------------ .../elasticsearch/xpack/inference/Utils.java | 15 +++ ...emanticTextNonDynamicFieldMapperTests.java | 3 +- .../TextSimilarityRankMultiNodeTests.java | 4 +- ...SimilarityRankRetrieverTelemetryTests.java | 5 +- .../TextSimilarityRankTests.java | 4 +- .../xpack/ml/LocalStateMachineLearning.java | 7 -- .../xpack/ml/support/BaseMlIntegTestCase.java | 4 +- .../security/CrossClusterShardTests.java | 2 + 22 files changed, 69 insertions(+), 314 deletions(-) delete mode 100644 docs/changelog/116423.yaml delete mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java delete mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java diff --git a/docs/changelog/116423.yaml b/docs/changelog/116423.yaml deleted file mode 100644 index d6d10eab410e4..0000000000000 --- a/docs/changelog/116423.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 116423 -summary: Support mTLS for the Elastic Inference Service integration inside the inference API -area: Machine Learning -type: feature -issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java index d0d5e463f9652..9704335776f11 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java @@ -596,8 +596,6 @@ static Map getSSLSettingsMap(Settings settings) { sslSettingsMap.put(WatcherField.EMAIL_NOTIFICATION_SSL_PREFIX, settings.getByPrefix(WatcherField.EMAIL_NOTIFICATION_SSL_PREFIX)); sslSettingsMap.put(XPackSettings.TRANSPORT_SSL_PREFIX, settings.getByPrefix(XPackSettings.TRANSPORT_SSL_PREFIX)); sslSettingsMap.putAll(getTransportProfileSSLSettings(settings)); - // Mount Elastic Inference Service (part of the Inference plugin) configuration - sslSettingsMap.put("xpack.inference.elastic.http.ssl", settings.getByPrefix("xpack.inference.elastic.http.ssl.")); // Only build remote cluster server SSL if the port is enabled if (REMOTE_CLUSTER_SERVER_ENABLED.get(settings)) { sslSettingsMap.put(XPackSettings.REMOTE_CLUSTER_SERVER_SSL_PREFIX, getRemoteClusterServerSslSettings(settings)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java index d50f7bb27a5df..1f2c89c473a62 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java @@ -623,7 +623,7 @@ public Map getSnapshotCommitSup } @SuppressWarnings("unchecked") - protected List filterPlugins(Class type) { + private List filterPlugins(Class type) { return plugins.stream().filter(x -> type.isAssignableFrom(x.getClass())).map(p -> ((T) p)).collect(Collectors.toList()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java index bfac286bc3c35..9663e41a647a8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java @@ -614,8 +614,7 @@ public void testGetConfigurationByContextName() throws Exception { "xpack.security.authc.realms.ldap.realm1.ssl", "xpack.security.authc.realms.saml.realm2.ssl", "xpack.monitoring.exporters.mon1.ssl", - "xpack.monitoring.exporters.mon2.ssl", - "xpack.inference.elastic.http.ssl" }; + "xpack.monitoring.exporters.mon2.ssl" }; assumeTrue("Not enough cipher suites are available to support this test", getCipherSuites.length >= contextNames.length); diff --git a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java index c7b3a9d42f579..3b0fc869c8124 100644 --- a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java +++ b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java @@ -22,7 +22,6 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import org.elasticsearch.xpack.inference.Utils; import org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension; import org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension; @@ -59,7 +58,7 @@ public void setup() throws Exception { @Override protected Collection> nodePlugins() { - return Arrays.asList(LocalStateInferencePlugin.class); + return Arrays.asList(Utils.TestInferencePlugin.class); } public void testBulkOperations() throws Exception { diff --git a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java index d5c156d1d4f46..be6b3725b0f35 100644 --- a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java +++ b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java @@ -31,7 +31,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.inference.chunking.ChunkingSettingsTests; import org.elasticsearch.xpack.inference.registry.ModelRegistry; import org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalModel; @@ -76,7 +76,7 @@ public void createComponents() { @Override protected Collection> getPlugins() { - return pluginList(ReindexPlugin.class, LocalStateInferencePlugin.class); + return pluginList(ReindexPlugin.class, InferencePlugin.class); } public void testStoreModel() throws Exception { diff --git a/x-pack/plugin/inference/src/main/java/module-info.java b/x-pack/plugin/inference/src/main/java/module-info.java index 1c2240e8c5217..53974657e4e23 100644 --- a/x-pack/plugin/inference/src/main/java/module-info.java +++ b/x-pack/plugin/inference/src/main/java/module-info.java @@ -34,7 +34,6 @@ requires software.amazon.awssdk.retries.api; requires org.reactivestreams; requires org.elasticsearch.logging; - requires org.elasticsearch.sslconfig; exports org.elasticsearch.xpack.inference.action; exports org.elasticsearch.xpack.inference.registry; diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java index 169c8f87043e8..72fa840ad19b0 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java @@ -28,7 +28,6 @@ import org.elasticsearch.indices.SystemIndexDescriptor; import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.InferenceServiceRegistry; -import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.node.PluginComponentBinding; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.ExtensiblePlugin; @@ -46,7 +45,6 @@ import org.elasticsearch.threadpool.ScalingExecutorBuilder; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xpack.core.ClientHelper; -import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction; import org.elasticsearch.xpack.core.inference.action.DeleteInferenceEndpointAction; import org.elasticsearch.xpack.core.inference.action.GetInferenceDiagnosticsAction; @@ -56,7 +54,6 @@ import org.elasticsearch.xpack.core.inference.action.PutInferenceModelAction; import org.elasticsearch.xpack.core.inference.action.UnifiedCompletionAction; import org.elasticsearch.xpack.core.inference.action.UpdateInferenceModelAction; -import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.inference.action.TransportDeleteInferenceEndpointAction; import org.elasticsearch.xpack.inference.action.TransportGetInferenceDiagnosticsAction; import org.elasticsearch.xpack.inference.action.TransportGetInferenceModelAction; @@ -122,6 +119,7 @@ import java.util.Map; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Collections.singletonList; @@ -155,7 +153,6 @@ public class InferencePlugin extends Plugin implements ActionPlugin, ExtensibleP private final Settings settings; private final SetOnce httpFactory = new SetOnce<>(); private final SetOnce amazonBedrockFactory = new SetOnce<>(); - private final SetOnce elasicInferenceServiceFactory = new SetOnce<>(); private final SetOnce serviceComponents = new SetOnce<>(); private final SetOnce elasticInferenceServiceComponents = new SetOnce<>(); private final SetOnce inferenceServiceRegistry = new SetOnce<>(); @@ -238,31 +235,31 @@ public Collection createComponents(PluginServices services) { var inferenceServices = new ArrayList<>(inferenceServiceExtensions); inferenceServices.add(this::getInferenceServiceFactories); - if (isElasticInferenceServiceEnabled()) { - // Create a separate instance of HTTPClientManager with its own SSL configuration (`xpack.inference.elastic.http.ssl.*`). - var elasticInferenceServiceHttpClientManager = HttpClientManager.create( - settings, - services.threadPool(), - services.clusterService(), - throttlerManager, - getSslService() - ); + // Set elasticInferenceUrl based on feature flags to support transitioning to the new Elastic Inference Service URL without exposing + // internal names like "eis" or "gateway". + ElasticInferenceServiceSettings inferenceServiceSettings = new ElasticInferenceServiceSettings(settings); + + String elasticInferenceUrl = null; - var elasticInferenceServiceRequestSenderFactory = new HttpRequestSender.Factory( - serviceComponents.get(), - elasticInferenceServiceHttpClientManager, - services.clusterService() + if (ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { + elasticInferenceUrl = inferenceServiceSettings.getElasticInferenceServiceUrl(); + } else if (DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { + log.warn( + "Deprecated flag {} detected for enabling {}. Please use {}.", + ELASTIC_INFERENCE_SERVICE_IDENTIFIER, + DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG, + ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG ); - elasicInferenceServiceFactory.set(elasticInferenceServiceRequestSenderFactory); + elasticInferenceUrl = inferenceServiceSettings.getEisGatewayUrl(); + } - ElasticInferenceServiceSettings inferenceServiceSettings = new ElasticInferenceServiceSettings(settings); - String elasticInferenceUrl = this.getElasticInferenceServiceUrl(inferenceServiceSettings); + if (elasticInferenceUrl != null) { elasticInferenceServiceComponents.set(new ElasticInferenceServiceComponents(elasticInferenceUrl)); inferenceServices.add( () -> List.of( context -> new ElasticInferenceService( - elasicInferenceServiceFactory.get(), + httpFactory.get(), serviceComponents.get(), elasticInferenceServiceComponents.get() ) @@ -385,21 +382,16 @@ public static ExecutorBuilder inferenceUtilityExecutor(Settings settings) { @Override public List> getSettings() { - ArrayList> settings = new ArrayList<>(); - settings.addAll(HttpSettings.getSettingsDefinitions()); - settings.addAll(HttpClientManager.getSettingsDefinitions()); - settings.addAll(ThrottlerManager.getSettingsDefinitions()); - settings.addAll(RetrySettings.getSettingsDefinitions()); - settings.addAll(Truncator.getSettingsDefinitions()); - settings.addAll(RequestExecutorServiceSettings.getSettingsDefinitions()); - settings.add(SKIP_VALIDATE_AND_START); - - // Register Elastic Inference Service settings definitions if the corresponding feature flag is enabled. - if (isElasticInferenceServiceEnabled()) { - settings.addAll(ElasticInferenceServiceSettings.getSettingsDefinitions()); - } - - return settings; + return Stream.of( + HttpSettings.getSettingsDefinitions(), + HttpClientManager.getSettingsDefinitions(), + ThrottlerManager.getSettingsDefinitions(), + RetrySettings.getSettingsDefinitions(), + ElasticInferenceServiceSettings.getSettingsDefinitions(), + Truncator.getSettingsDefinitions(), + RequestExecutorServiceSettings.getSettingsDefinitions(), + List.of(SKIP_VALIDATE_AND_START) + ).flatMap(Collection::stream).collect(Collectors.toList()); } @Override @@ -447,10 +439,7 @@ public List getQueryRewriteInterceptors() { @Override public List> getRetrievers() { return List.of( - new RetrieverSpec<>( - new ParseField(TextSimilarityRankBuilder.NAME), - (parser, context) -> TextSimilarityRankRetrieverBuilder.fromXContent(parser, context, getLicenseState()) - ), + new RetrieverSpec<>(new ParseField(TextSimilarityRankBuilder.NAME), TextSimilarityRankRetrieverBuilder::fromXContent), new RetrieverSpec<>(new ParseField(RandomRankBuilder.NAME), RandomRankRetrieverBuilder::fromXContent) ); } @@ -459,36 +448,4 @@ public List> getRetrievers() { public Map getHighlighters() { return Map.of(SemanticTextHighlighter.NAME, new SemanticTextHighlighter()); } - - // Get Elastic Inference service URL based on feature flags to support transitioning - // to the new Elastic Inference Service URL. - private String getElasticInferenceServiceUrl(ElasticInferenceServiceSettings settings) { - String elasticInferenceUrl = null; - - if (ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { - elasticInferenceUrl = settings.getElasticInferenceServiceUrl(); - } else if (DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { - log.warn( - "Deprecated flag {} detected for enabling {}. Please use {}.", - ELASTIC_INFERENCE_SERVICE_IDENTIFIER, - DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG, - ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG - ); - elasticInferenceUrl = settings.getEisGatewayUrl(); - } - - return elasticInferenceUrl; - } - - protected Boolean isElasticInferenceServiceEnabled() { - return (ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled() || DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()); - } - - protected SSLService getSslService() { - return XPackPlugin.getSharedSslService(); - } - - protected XPackLicenseState getLicenseState() { - return XPackPlugin.getSharedLicenseState(); - } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java index 6d09c9e67b363..e5d76b9bb5570 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java @@ -7,14 +7,9 @@ package org.elasticsearch.xpack.inference.external.http; -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager; import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor; import org.apache.http.impl.nio.reactor.IOReactorConfig; -import org.apache.http.nio.conn.NoopIOSessionStrategy; -import org.apache.http.nio.conn.SchemeIOSessionStrategy; -import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; import org.apache.http.nio.reactor.ConnectingIOReactor; import org.apache.http.nio.reactor.IOReactorException; import org.apache.http.pool.PoolStats; @@ -26,7 +21,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import java.io.Closeable; @@ -34,13 +28,11 @@ import java.util.List; import static org.elasticsearch.core.Strings.format; -import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceServiceSettings.ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX; public class HttpClientManager implements Closeable { private static final Logger logger = LogManager.getLogger(HttpClientManager.class); /** * The maximum number of total connections the connection pool can lease to all routes. - * The configuration applies to each instance of HTTPClientManager (max_total_connections=10 and instances=5 leads to 50 connections). * From googling around the connection pools maxTotal value should be close to the number of available threads. * * https://stackoverflow.com/questions/30989637/how-to-decide-optimal-settings-for-setmaxtotal-and-setdefaultmaxperroute @@ -55,7 +47,6 @@ public class HttpClientManager implements Closeable { /** * The max number of connections a single route can lease. - * This configuration applies to each instance of HttpClientManager. */ public static final Setting MAX_ROUTE_CONNECTIONS = Setting.intSetting( "xpack.inference.http.max_route_connections", @@ -107,22 +98,6 @@ public static HttpClientManager create( return new HttpClientManager(settings, connectionManager, threadPool, clusterService, throttlerManager); } - public static HttpClientManager create( - Settings settings, - ThreadPool threadPool, - ClusterService clusterService, - ThrottlerManager throttlerManager, - SSLService sslService - ) { - // Set the sslStrategy to ensure an encrypted connection, as Elastic Inference Service requires it. - SSLIOSessionStrategy sslioSessionStrategy = sslService.sslIOSessionStrategy( - sslService.getSSLConfiguration(ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX) - ); - - PoolingNHttpClientConnectionManager connectionManager = createConnectionManager(sslioSessionStrategy); - return new HttpClientManager(settings, connectionManager, threadPool, clusterService, throttlerManager); - } - // Default for testing HttpClientManager( Settings settings, @@ -146,25 +121,6 @@ public static HttpClientManager create( this.addSettingsUpdateConsumers(clusterService); } - private static PoolingNHttpClientConnectionManager createConnectionManager(SSLIOSessionStrategy sslStrategy) { - ConnectingIOReactor ioReactor; - try { - var configBuilder = IOReactorConfig.custom().setSoKeepAlive(true); - ioReactor = new DefaultConnectingIOReactor(configBuilder.build()); - } catch (IOReactorException e) { - var message = "Failed to initialize HTTP client manager with SSL."; - logger.error(message, e); - throw new ElasticsearchException(message, e); - } - - Registry registry = RegistryBuilder.create() - .register("http", NoopIOSessionStrategy.INSTANCE) - .register("https", sslStrategy) - .build(); - - return new PoolingNHttpClientConnectionManager(ioReactor, registry); - } - private static PoolingNHttpClientConnectionManager createConnectionManager() { ConnectingIOReactor ioReactor; try { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java index f54696895a818..fd2427dc8ac6a 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java @@ -12,7 +12,6 @@ import org.elasticsearch.features.NodeFeature; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.license.LicenseUtils; -import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.rank.RankDoc; import org.elasticsearch.search.retriever.CompoundRetrieverBuilder; @@ -22,6 +21,7 @@ import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xpack.core.XPackPlugin; import java.io.IOException; import java.util.List; @@ -73,11 +73,8 @@ public class TextSimilarityRankRetrieverBuilder extends CompoundRetrieverBuilder RetrieverBuilder.declareBaseParserFields(TextSimilarityRankBuilder.NAME, PARSER); } - public static TextSimilarityRankRetrieverBuilder fromXContent( - XContentParser parser, - RetrieverParserContext context, - XPackLicenseState licenceState - ) throws IOException { + public static TextSimilarityRankRetrieverBuilder fromXContent(XContentParser parser, RetrieverParserContext context) + throws IOException { if (context.clusterSupportsFeature(TEXT_SIMILARITY_RERANKER_RETRIEVER_SUPPORTED) == false) { throw new ParsingException(parser.getTokenLocation(), "unknown retriever [" + TextSimilarityRankBuilder.NAME + "]"); } @@ -86,7 +83,7 @@ public static TextSimilarityRankRetrieverBuilder fromXContent( "[text_similarity_reranker] retriever composition feature is not supported by all nodes in the cluster" ); } - if (TextSimilarityRankBuilder.TEXT_SIMILARITY_RERANKER_FEATURE.check(licenceState) == false) { + if (TextSimilarityRankBuilder.TEXT_SIMILARITY_RERANKER_FEATURE.check(XPackPlugin.getSharedLicenseState()) == false) { throw LicenseUtils.newComplianceException(TextSimilarityRankBuilder.NAME); } return PARSER.apply(parser, context); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java index 431a3647e2879..bc2daddc2a346 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java @@ -9,9 +9,7 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; -import java.util.ArrayList; import java.util.List; public class ElasticInferenceServiceSettings { @@ -19,8 +17,6 @@ public class ElasticInferenceServiceSettings { @Deprecated static final Setting EIS_GATEWAY_URL = Setting.simpleString("xpack.inference.eis.gateway.url", Setting.Property.NodeScope); - public static final String ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX = "xpack.inference.elastic.http.ssl."; - static final Setting ELASTIC_INFERENCE_SERVICE_URL = Setting.simpleString( "xpack.inference.elastic.url", Setting.Property.NodeScope @@ -35,27 +31,11 @@ public class ElasticInferenceServiceSettings { public ElasticInferenceServiceSettings(Settings settings) { eisGatewayUrl = EIS_GATEWAY_URL.get(settings); elasticInferenceServiceUrl = ELASTIC_INFERENCE_SERVICE_URL.get(settings); - } - - public static final SSLConfigurationSettings ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_SETTINGS = SSLConfigurationSettings.withPrefix( - ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX, - false - ); - public static final Setting ELASTIC_INFERENCE_SERVICE_SSL_ENABLED = Setting.boolSetting( - ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX + "enabled", - true, - Setting.Property.NodeScope - ); + } public static List> getSettingsDefinitions() { - ArrayList> settings = new ArrayList<>(); - settings.add(EIS_GATEWAY_URL); - settings.add(ELASTIC_INFERENCE_SERVICE_URL); - settings.add(ELASTIC_INFERENCE_SERVICE_SSL_ENABLED); - settings.addAll(ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_SETTINGS.getEnabledSettings()); - - return settings; + return List.of(EIS_GATEWAY_URL, ELASTIC_INFERENCE_SERVICE_URL); } @Deprecated diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java index 61033a0211065..bfec2d5ac3484 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.index.IndexService; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; -import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.inference.InferencePlugin; import org.hamcrest.Matchers; @@ -29,7 +28,7 @@ public class SemanticTextClusterMetadataTests extends ESSingleNodeTestCase { @Override protected Collection> getPlugins() { - return List.of(XPackPlugin.class, InferencePlugin.class); + return List.of(InferencePlugin.class); } public void testCreateIndexWithSemanticTextField() { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java deleted file mode 100644 index d1db5b8b12cc6..0000000000000 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.inference; - -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceServiceSettings; -import org.junit.After; -import org.junit.Before; - -import static org.hamcrest.Matchers.is; - -public class InferencePluginTests extends ESTestCase { - private InferencePlugin inferencePlugin; - - private Boolean elasticInferenceServiceEnabled = true; - - private void setElasticInferenceServiceEnabled(Boolean elasticInferenceServiceEnabled) { - this.elasticInferenceServiceEnabled = elasticInferenceServiceEnabled; - } - - @Before - public void setUp() throws Exception { - super.setUp(); - - Settings settings = Settings.builder().build(); - inferencePlugin = new InferencePlugin(settings) { - @Override - protected Boolean isElasticInferenceServiceEnabled() { - return elasticInferenceServiceEnabled; - } - }; - } - - @After - public void tearDown() throws Exception { - super.tearDown(); - } - - public void testElasticInferenceServiceSettingsPresent() throws Exception { - setElasticInferenceServiceEnabled(true); // enable elastic inference service - boolean anyMatch = inferencePlugin.getSettings() - .stream() - .map(Setting::getKey) - .anyMatch(key -> key.startsWith(ElasticInferenceServiceSettings.ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX)); - - assertThat("xpack.inference.elastic settings are present", anyMatch, is(true)); - } - - public void testElasticInferenceServiceSettingsNotPresent() throws Exception { - setElasticInferenceServiceEnabled(false); // disable elastic inference service - boolean noneMatch = inferencePlugin.getSettings() - .stream() - .map(Setting::getKey) - .noneMatch(key -> key.startsWith(ElasticInferenceServiceSettings.ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX)); - - assertThat("xpack.inference.elastic settings are not present", noneMatch, is(true)); - } -} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java deleted file mode 100644 index 68ea175bd9870..0000000000000 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.inference; - -import org.elasticsearch.action.support.MappedActionFilter; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.mapper.Mapper; -import org.elasticsearch.inference.InferenceServiceExtension; -import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.plugins.SearchPlugin; -import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; -import org.elasticsearch.xpack.core.ssl.SSLService; -import org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension; -import org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension; - -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import static java.util.stream.Collectors.toList; - -public class LocalStateInferencePlugin extends LocalStateCompositeXPackPlugin { - private final InferencePlugin inferencePlugin; - - public LocalStateInferencePlugin(final Settings settings, final Path configPath) throws Exception { - super(settings, configPath); - LocalStateInferencePlugin thisVar = this; - this.inferencePlugin = new InferencePlugin(settings) { - @Override - protected SSLService getSslService() { - return thisVar.getSslService(); - } - - @Override - protected XPackLicenseState getLicenseState() { - return thisVar.getLicenseState(); - } - - @Override - public List getInferenceServiceFactories() { - return List.of( - TestSparseInferenceServiceExtension.TestInferenceService::new, - TestDenseInferenceServiceExtension.TestInferenceService::new - ); - } - }; - plugins.add(inferencePlugin); - } - - @Override - public List> getRetrievers() { - return this.filterPlugins(SearchPlugin.class).stream().flatMap(p -> p.getRetrievers().stream()).collect(toList()); - } - - @Override - public Map getMappers() { - return inferencePlugin.getMappers(); - } - - @Override - public Collection getMappedActionFilters() { - return inferencePlugin.getMappedActionFilters(); - } - -} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java index 0f322e64755be..9395ae222e9ba 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; +import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -142,6 +143,20 @@ private static void blockingCall( latch.await(); } + public static class TestInferencePlugin extends InferencePlugin { + public TestInferencePlugin(Settings settings) { + super(settings); + } + + @Override + public List getInferenceServiceFactories() { + return List.of( + TestSparseInferenceServiceExtension.TestInferenceService::new, + TestDenseInferenceServiceExtension.TestInferenceService::new + ); + } + } + public static Model getInvalidModel(String inferenceEntityId, String serviceName) { var mockConfigs = mock(ModelConfigurations.class); when(mockConfigs.getInferenceEntityId()).thenReturn(inferenceEntityId); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java index 24183b21f73e7..1f58c4165056d 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.index.mapper.NonDynamicFieldMapperTests; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import org.elasticsearch.xpack.inference.Utils; import org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension; import org.junit.Before; @@ -27,7 +26,7 @@ public void setup() throws Exception { @Override protected Collection> getPlugins() { - return List.of(LocalStateInferencePlugin.class); + return List.of(Utils.TestInferencePlugin.class); } @Override diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java index daed03c198e0d..6d6403b69ea11 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java @@ -10,7 +10,7 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.rank.RankBuilder; import org.elasticsearch.search.rank.rerank.AbstractRerankerIT; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; +import org.elasticsearch.xpack.inference.InferencePlugin; import java.util.Collection; import java.util.List; @@ -40,7 +40,7 @@ protected RankBuilder getThrowingRankBuilder(int rankWindowSize, String rankFeat @Override protected Collection> pluginsNeeded() { - return List.of(LocalStateInferencePlugin.class, TextSimilarityTestPlugin.class); + return List.of(InferencePlugin.class, TextSimilarityTestPlugin.class); } public void testQueryPhaseShardThrowingAllShardsFail() throws Exception { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java index ba6924ba0ff3b..084a7f3de4a53 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java @@ -24,7 +24,8 @@ import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; +import org.elasticsearch.xpack.core.XPackPlugin; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.junit.Before; import java.io.IOException; @@ -46,7 +47,7 @@ protected boolean addMockHttpTransport() { @Override protected Collection> nodePlugins() { - return List.of(LocalStateInferencePlugin.class, TextSimilarityTestPlugin.class); + return List.of(InferencePlugin.class, XPackPlugin.class, TextSimilarityTestPlugin.class); } @Override diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java index f81f2965c392e..a042fca44fdb5 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java @@ -20,7 +20,7 @@ import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.hamcrest.ElasticsearchAssertions; import org.elasticsearch.xpack.core.inference.action.InferenceAction; -import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.junit.Before; import java.util.Collection; @@ -108,7 +108,7 @@ protected InferenceAction.Request generateRequest(List docFeatures) { @Override protected Collection> getPlugins() { - return List.of(LocalStateInferencePlugin.class, TextSimilarityTestPlugin.class); + return List.of(InferencePlugin.class, TextSimilarityTestPlugin.class); } @Before diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java index ff1a1d19779df..bab012afc3101 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java @@ -27,7 +27,6 @@ import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.core.rollup.action.GetRollupIndexCapsAction; import org.elasticsearch.xpack.core.ssl.SSLService; -import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.monitoring.Monitoring; import org.elasticsearch.xpack.security.Security; @@ -87,12 +86,6 @@ protected XPackLicenseState getLicenseState() { } }); plugins.add(new MockedRollupPlugin()); - plugins.add(new InferencePlugin(settings) { - @Override - protected SSLService getSslService() { - return thisVar.getSslService(); - } - }); } @Override diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java index 5cf15454e47f2..aeebfabdce704 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java @@ -82,6 +82,7 @@ import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.DataCounts; import org.elasticsearch.xpack.core.ml.utils.MlTaskState; import org.elasticsearch.xpack.ilm.IndexLifecycle; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.ml.LocalStateMachineLearning; import org.elasticsearch.xpack.ml.MachineLearning; import org.elasticsearch.xpack.ml.MlSingleNodeTestCase; @@ -160,7 +161,8 @@ protected Collection> nodePlugins() { DataStreamsPlugin.class, // To remove errors from parsing build in templates that contain scaled_float MapperExtrasPlugin.class, - Wildcard.class + Wildcard.class, + InferencePlugin.class ); } diff --git a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java index 057ebdece5c61..ab5be0f48f5f3 100644 --- a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java +++ b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.xpack.frozen.FrozenIndices; import org.elasticsearch.xpack.graph.Graph; import org.elasticsearch.xpack.ilm.IndexLifecycle; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.profiling.ProfilingPlugin; import org.elasticsearch.xpack.rollup.Rollup; import org.elasticsearch.xpack.search.AsyncSearch; @@ -88,6 +89,7 @@ protected Collection> getPlugins() { FrozenIndices.class, Graph.class, IndexLifecycle.class, + InferencePlugin.class, IngestCommonPlugin.class, IngestTestPlugin.class, MustachePlugin.class, From 724e9bef3b923e64306db68999ffe02a226aa014 Mon Sep 17 00:00:00 2001 From: Jedr Blaszyk Date: Wed, 18 Dec 2024 13:05:16 +0100 Subject: [PATCH 066/119] [Connector APIs] Enforce index prefix for managed connectors (#117778) * [Connector APIs] Enforce manage connector index prefix * Update ConnectorIndexServiceTests to check for prefix * Update docs/changelog/117778.yaml * Fix accidental changes to muted tests * Review feedback * Review feedback --------- Co-authored-by: Elastic Machine --- docs/changelog/117778.yaml | 5 + .../entsearch/connector/10_connector_put.yml | 13 ++ .../130_connector_update_index_name.yml | 15 ++ .../connector/140_connector_update_native.yml | 40 +++++ .../entsearch/connector/15_connector_post.yml | 12 ++ .../connector/ConnectorIndexService.java | 137 +++++++++++++----- .../connector/ConnectorTemplateRegistry.java | 2 + .../action/ConnectorActionRequest.java | 27 ++++ .../connector/action/PostConnectorAction.java | 4 + .../connector/action/PutConnectorAction.java | 4 + .../connector/ConnectorIndexServiceTests.java | 80 +++++++++- .../connector/ConnectorTestUtils.java | 12 ++ .../action/PostConnectorActionTests.java | 20 ++- .../action/PutConnectorActionTests.java | 21 ++- 14 files changed, 351 insertions(+), 41 deletions(-) create mode 100644 docs/changelog/117778.yaml diff --git a/docs/changelog/117778.yaml b/docs/changelog/117778.yaml new file mode 100644 index 0000000000000..880d4f831e533 --- /dev/null +++ b/docs/changelog/117778.yaml @@ -0,0 +1,5 @@ +pr: 117778 +summary: "[Connector APIs] Enforce index prefix for managed connectors" +area: Extract&Transform +type: feature +issues: [] diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/10_connector_put.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/10_connector_put.yml index 094d9cbf43089..4240467ea4ff3 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/10_connector_put.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/10_connector_put.yml @@ -152,6 +152,19 @@ setup: service_type: super-connector +--- +'Create Connector - Invalid Managed Connector Index Prefix': + - do: + catch: "bad_request" + connector.put: + connector_id: test-connector-test-managed + body: + index_name: wrong-prefix-index + name: my-connector + language: pl + is_native: true + service_type: super-connector + --- 'Create Connector - Id returned as part of response': - do: diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/130_connector_update_index_name.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/130_connector_update_index_name.yml index f804dc02a9e01..b63bf595af5f4 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/130_connector_update_index_name.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/130_connector_update_index_name.yml @@ -151,3 +151,18 @@ setup: - match: { index_name: content-search-2-test } +--- +"Update Managed Connector Index Name - Bad Prefix": + - do: + connector.put: + connector_id: test-connector-2 + body: + is_native: true + service_type: super-connector + + - do: + catch: "bad_request" + connector.update_index_name: + connector_id: test-connector-2 + body: + index_name: wrong-prefix-search-2-test diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/140_connector_update_native.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/140_connector_update_native.yml index f8cd24d175312..6811c3340ce42 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/140_connector_update_native.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/140_connector_update_native.yml @@ -73,3 +73,43 @@ setup: field_1: test field_2: something +--- +"Update Connector Native - changing connector to Elastic-managed wrong index name": + + - do: + connector.put: + connector_id: test-connector-1 + body: + is_native: false + index_name: super-connector + + - do: + catch: "bad_request" + connector.update_native: + connector_id: test-connector-1 + body: + is_native: true + +--- +"Update Connector Native - changing connector to Elastic-managed correct index name": + + - do: + connector.put: + connector_id: test-connector-1 + body: + is_native: false + index_name: content-super-connector + + - do: + connector.update_native: + connector_id: test-connector-1 + body: + is_native: true + + - match: { result: updated } + + - do: + connector.get: + connector_id: test-connector-1 + + - match: { is_native: true } diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/15_connector_post.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/15_connector_post.yml index 634f99cd53fde..4acca493c42c8 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/15_connector_post.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/15_connector_post.yml @@ -103,6 +103,18 @@ setup: service_type: super-connector +--- +'Create Connector - Invalid Managed Connector Index Prefix': + - do: + catch: "bad_request" + connector.post: + body: + index_name: wrong-prefix-index + name: my-connector + language: pl + is_native: true + service_type: super-connector + --- 'Create Connector - Index name used by another connector': - do: diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorIndexService.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorIndexService.java index 5e1fde0dfb942..d5d2159d8f373 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorIndexService.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorIndexService.java @@ -76,6 +76,7 @@ import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.application.connector.ConnectorFiltering.fromXContentBytesConnectorFiltering; import static org.elasticsearch.xpack.application.connector.ConnectorFiltering.sortFilteringRulesByOrder; +import static org.elasticsearch.xpack.application.connector.ConnectorTemplateRegistry.MANAGED_CONNECTOR_INDEX_PREFIX; /** * A service that manages persistent {@link Connector} configurations. @@ -807,8 +808,8 @@ public void updateConnectorLastSyncStats(UpdateConnectorLastSyncStatsAction.Requ } /** - * Updates the is_native property of a {@link Connector}. It always sets the {@link ConnectorStatus} to - * CONFIGURED. + * Updates the is_native property of a {@link Connector}. It sets the {@link ConnectorStatus} to + * CONFIGURED when connector is in CONNECTED state to indicate that connector needs to reconnect. * * @param request The request for updating the connector's is_native property. * @param listener The listener for handling responses, including successful updates or errors. @@ -816,29 +817,62 @@ public void updateConnectorLastSyncStats(UpdateConnectorLastSyncStatsAction.Requ public void updateConnectorNative(UpdateConnectorNativeAction.Request request, ActionListener listener) { try { String connectorId = request.getConnectorId(); + boolean isNative = request.isNative(); - final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_INDEX_NAME, connectorId).doc( - new IndexRequest(CONNECTOR_INDEX_NAME).opType(DocWriteRequest.OpType.INDEX) - .id(connectorId) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .source( - Map.of( - Connector.IS_NATIVE_FIELD.getPreferredName(), - request.isNative(), - Connector.STATUS_FIELD.getPreferredName(), - ConnectorStatus.CONFIGURED.toString() - ) - ) + getConnector(connectorId, listener.delegateFailure((l, connector) -> { - ); - client.update(updateRequest, new DelegatingIndexNotFoundActionListener<>(connectorId, listener, (l, updateResponse) -> { - if (updateResponse.getResult() == UpdateResponse.Result.NOT_FOUND) { - l.onFailure(new ResourceNotFoundException(connectorNotFoundErrorMsg(connectorId))); + String indexName = getConnectorIndexNameFromSearchResult(connector); + + boolean doesNotHaveContentPrefix = indexName != null && isValidManagedConnectorIndexName(indexName) == false; + // Ensure attached content index is prefixed correctly + if (isNative && doesNotHaveContentPrefix) { + l.onFailure( + new ElasticsearchStatusException( + "The index name [" + + indexName + + "] attached to the connector [" + + connectorId + + "] must start with the required prefix: [" + + MANAGED_CONNECTOR_INDEX_PREFIX + + "] to be Elastic-managed. Please update the attached index first to comply with this requirement.", + RestStatus.BAD_REQUEST + ) + ); return; } - l.onResponse(updateResponse); - })); + ConnectorStatus status = getConnectorStatusFromSearchResult(connector); + + // If connector was connected already, change its status to CONFIGURED as we need to re-connect + boolean isConnected = status == ConnectorStatus.CONNECTED; + boolean isValidTransitionToConfigured = ConnectorStateMachine.isValidTransition(status, ConnectorStatus.CONFIGURED); + if (isConnected && isValidTransitionToConfigured) { + status = ConnectorStatus.CONFIGURED; + } + + final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_INDEX_NAME, connectorId).setRefreshPolicy( + WriteRequest.RefreshPolicy.IMMEDIATE + ) + .doc( + new IndexRequest(CONNECTOR_INDEX_NAME).opType(DocWriteRequest.OpType.INDEX) + .id(connectorId) + .source( + Map.of( + Connector.IS_NATIVE_FIELD.getPreferredName(), + isNative, + Connector.STATUS_FIELD.getPreferredName(), + status.toString() + ) + ) + ); + client.update(updateRequest, new DelegatingIndexNotFoundActionListener<>(connectorId, listener, (ll, updateResponse) -> { + if (updateResponse.getResult() == UpdateResponse.Result.NOT_FOUND) { + ll.onFailure(new ResourceNotFoundException(connectorNotFoundErrorMsg(connectorId))); + return; + } + ll.onResponse(updateResponse); + })); + })); } catch (Exception e) { listener.onFailure(e); } @@ -896,22 +930,45 @@ public void updateConnectorIndexName(UpdateConnectorIndexNameAction.Request requ return; } - final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_INDEX_NAME, connectorId).doc( - new IndexRequest(CONNECTOR_INDEX_NAME).opType(DocWriteRequest.OpType.INDEX) - .id(connectorId) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .source(new HashMap<>() { - { - put(Connector.INDEX_NAME_FIELD.getPreferredName(), request.getIndexName()); - } - }) - ); - client.update(updateRequest, new DelegatingIndexNotFoundActionListener<>(connectorId, listener, (ll, updateResponse) -> { - if (updateResponse.getResult() == UpdateResponse.Result.NOT_FOUND) { - ll.onFailure(new ResourceNotFoundException(connectorNotFoundErrorMsg(connectorId))); + getConnector(connectorId, l.delegateFailure((ll, connector) -> { + + Boolean isNativeConnector = getConnectorIsNativeFlagFromSearchResult(connector); + Boolean doesNotHaveContentPrefix = indexName != null && isValidManagedConnectorIndexName(indexName) == false; + + if (isNativeConnector && doesNotHaveContentPrefix) { + ll.onFailure( + new ElasticsearchStatusException( + "Index attached to an Elastic-managed connector must start with the prefix: [" + + MANAGED_CONNECTOR_INDEX_PREFIX + + "]. The index name in the payload [" + + indexName + + "] doesn't comply with this requirement.", + RestStatus.BAD_REQUEST + ) + ); return; } - ll.onResponse(updateResponse); + + final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_INDEX_NAME, connectorId).doc( + new IndexRequest(CONNECTOR_INDEX_NAME).opType(DocWriteRequest.OpType.INDEX) + .id(connectorId) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .source(new HashMap<>() { + { + put(Connector.INDEX_NAME_FIELD.getPreferredName(), request.getIndexName()); + } + }) + ); + client.update( + updateRequest, + new DelegatingIndexNotFoundActionListener<>(connectorId, listener, (lll, updateResponse) -> { + if (updateResponse.getResult() == UpdateResponse.Result.NOT_FOUND) { + lll.onFailure(new ResourceNotFoundException(connectorNotFoundErrorMsg(connectorId))); + return; + } + lll.onResponse(updateResponse); + }) + ); })); })); @@ -1064,6 +1121,18 @@ private ConnectorStatus getConnectorStatusFromSearchResult(ConnectorSearchResult return ConnectorStatus.connectorStatus((String) searchResult.getResultMap().get(Connector.STATUS_FIELD.getPreferredName())); } + private Boolean getConnectorIsNativeFlagFromSearchResult(ConnectorSearchResult searchResult) { + return (Boolean) searchResult.getResultMap().get(Connector.IS_NATIVE_FIELD.getPreferredName()); + } + + private String getConnectorIndexNameFromSearchResult(ConnectorSearchResult searchResult) { + return (String) searchResult.getResultMap().get(Connector.INDEX_NAME_FIELD.getPreferredName()); + } + + private boolean isValidManagedConnectorIndexName(String indexName) { + return indexName.startsWith(MANAGED_CONNECTOR_INDEX_PREFIX); + } + @SuppressWarnings("unchecked") private Map getConnectorConfigurationFromSearchResult(ConnectorSearchResult searchResult) { return (Map) searchResult.getResultMap().get(Connector.CONFIGURATION_FIELD.getPreferredName()); diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistry.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistry.java index 9b8cc7cfdbe4f..e630f929bc09b 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistry.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistry.java @@ -45,6 +45,8 @@ public class ConnectorTemplateRegistry extends IndexTemplateRegistry { public static final String ACCESS_CONTROL_INDEX_NAME_PATTERN = ".search-acl-filter-*"; public static final String ACCESS_CONTROL_TEMPLATE_NAME = "search-acl-filter"; + public static final String MANAGED_CONNECTOR_INDEX_PREFIX = "content-"; + // Pipeline constants public static final String ENT_SEARCH_GENERIC_PIPELINE_NAME = "ent-search-generic-ingestion"; diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/ConnectorActionRequest.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/ConnectorActionRequest.java index 1799121505da5..66f347bc4dbb4 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/ConnectorActionRequest.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/ConnectorActionRequest.java @@ -19,6 +19,7 @@ import java.io.IOException; import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.xpack.application.connector.ConnectorTemplateRegistry.MANAGED_CONNECTOR_INDEX_PREFIX; /** * Abstract base class for action requests targeting the connectors index. Implements {@link org.elasticsearch.action.IndicesRequest} @@ -52,6 +53,32 @@ public ActionRequestValidationException validateIndexName(String indexName, Acti return validationException; } + /** + * Validates that the given index name starts with the required prefix for Elastic-managed connectors. + * If the index name does not start with the required prefix, the validation exception is updated with an error message. + * + * @param indexName The index name to validate. If null, no validation is performed. + * @param validationException The exception to accumulate validation errors. + * @return The updated or original {@code validationException} with any new validation errors added, + * if the index name does not start with the required prefix. + */ + public ActionRequestValidationException validateManagedConnectorIndexPrefix( + String indexName, + ActionRequestValidationException validationException + ) { + if (indexName != null && indexName.startsWith(MANAGED_CONNECTOR_INDEX_PREFIX) == false) { + return addValidationError( + "Index [" + + indexName + + "] is invalid. Index attached to an Elastic-managed connector must start with the prefix: [" + + MANAGED_CONNECTOR_INDEX_PREFIX + + "]", + validationException + ); + } + return validationException; + } + @Override public String[] indices() { return new String[] { ConnectorTemplateRegistry.CONNECTOR_INDEX_NAME_PATTERN }; diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/PostConnectorAction.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/PostConnectorAction.java index fad349cd31877..b1c38637298c4 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/PostConnectorAction.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/PostConnectorAction.java @@ -127,6 +127,10 @@ public ActionRequestValidationException validate() { validationException = validateIndexName(indexName, validationException); + if (Boolean.TRUE.equals(isNative)) { + validationException = validateManagedConnectorIndexPrefix(indexName, validationException); + } + return validationException; } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/PutConnectorAction.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/PutConnectorAction.java index 687a801ab8fd6..f3e8ed6b6e76d 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/PutConnectorAction.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/PutConnectorAction.java @@ -147,6 +147,10 @@ public ActionRequestValidationException validate() { validationException = validateIndexName(indexName, validationException); + if (Boolean.TRUE.equals(isNative)) { + validationException = validateManagedConnectorIndexPrefix(indexName, validationException); + } + return validationException; } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIndexServiceTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIndexServiceTests.java index 12abca3a78591..28d4fe0956d03 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIndexServiceTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIndexServiceTests.java @@ -56,6 +56,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static org.elasticsearch.xpack.application.connector.ConnectorTemplateRegistry.MANAGED_CONNECTOR_INDEX_PREFIX; import static org.elasticsearch.xpack.application.connector.ConnectorTestUtils.getRandomConnectorFeatures; import static org.elasticsearch.xpack.application.connector.ConnectorTestUtils.getRandomCronExpression; import static org.elasticsearch.xpack.application.connector.ConnectorTestUtils.randomConnectorFeatureEnabled; @@ -648,8 +649,8 @@ public void testUpdateConnectorScheduling_OnlyFullSchedule() throws Exception { assertThat(initialScheduling.getIncremental(), equalTo(indexedConnector.getScheduling().getIncremental())); } - public void testUpdateConnectorIndexName() throws Exception { - Connector connector = ConnectorTestUtils.getRandomConnector(); + public void testUpdateConnectorIndexName_ForSelfManagedConnector() throws Exception { + Connector connector = ConnectorTestUtils.getRandomSelfManagedConnector(); String connectorId = randomUUID(); ConnectorCreateActionResponse resp = awaitCreateConnector(connectorId, connector); @@ -669,8 +670,8 @@ public void testUpdateConnectorIndexName() throws Exception { assertThat(newIndexName, equalTo(indexedConnector.getIndexName())); } - public void testUpdateConnectorIndexName_WithTheSameIndexName() throws Exception { - Connector connector = ConnectorTestUtils.getRandomConnector(); + public void testUpdateConnectorIndexName_ForSelfManagedConnector_WithTheSameIndexName() throws Exception { + Connector connector = ConnectorTestUtils.getRandomSelfManagedConnector(); String connectorId = randomUUID(); ConnectorCreateActionResponse resp = awaitCreateConnector(connectorId, connector); @@ -685,6 +686,42 @@ public void testUpdateConnectorIndexName_WithTheSameIndexName() throws Exception assertThat(updateResponse.getResult(), equalTo(DocWriteResponse.Result.NOOP)); } + public void testUpdateConnectorIndexName_ForManagedConnector_WithIllegalIndexName() throws Exception { + Connector connector = ConnectorTestUtils.getRandomElasticManagedConnector(); + String connectorId = randomUUID(); + + ConnectorCreateActionResponse resp = awaitCreateConnector(connectorId, connector); + assertThat(resp.status(), anyOf(equalTo(RestStatus.CREATED), equalTo(RestStatus.OK))); + + UpdateConnectorIndexNameAction.Request updateIndexNameRequest = new UpdateConnectorIndexNameAction.Request( + connectorId, + "wrong-prefix-" + randomAlphaOfLengthBetween(3, 10) + ); + + expectThrows(ElasticsearchStatusException.class, () -> awaitUpdateConnectorIndexName(updateIndexNameRequest)); + } + + public void testUpdateConnectorIndexName_ForManagedConnector_WithPrefixedIndexName() throws Exception { + Connector connector = ConnectorTestUtils.getRandomElasticManagedConnector(); + String connectorId = randomUUID(); + + ConnectorCreateActionResponse resp = awaitCreateConnector(connectorId, connector); + assertThat(resp.status(), anyOf(equalTo(RestStatus.CREATED), equalTo(RestStatus.OK))); + + String newIndexName = MANAGED_CONNECTOR_INDEX_PREFIX + randomAlphaOfLengthBetween(3, 10); + + UpdateConnectorIndexNameAction.Request updateIndexNameRequest = new UpdateConnectorIndexNameAction.Request( + connectorId, + newIndexName + ); + + DocWriteResponse updateResponse = awaitUpdateConnectorIndexName(updateIndexNameRequest); + assertThat(updateResponse.status(), equalTo(RestStatus.OK)); + + Connector indexedConnector = awaitGetConnector(connectorId); + assertThat(newIndexName, equalTo(indexedConnector.getIndexName())); + } + public void testUpdateConnectorServiceType() throws Exception { Connector connector = ConnectorTestUtils.getRandomConnector(); String connectorId = randomUUID(); @@ -756,7 +793,7 @@ public void testUpdateConnectorNameOrDescription() throws Exception { } public void testUpdateConnectorNative() throws Exception { - Connector connector = ConnectorTestUtils.getRandomConnector(); + Connector connector = ConnectorTestUtils.getRandomConnectorWithDetachedIndex(); String connectorId = randomUUID(); ConnectorCreateActionResponse resp = awaitCreateConnector(connectorId, connector); @@ -773,6 +810,39 @@ public void testUpdateConnectorNative() throws Exception { assertThat(isNative, equalTo(indexedConnector.isNative())); } + public void testUpdateConnectorNativeTrue_WhenIllegalIndexPrefix() throws Exception { + Connector connector = ConnectorTestUtils.getRandomConnectorWithAttachedIndex("wrong-prefix-" + randomAlphaOfLength(10)); + String connectorId = randomUUID(); + + ConnectorCreateActionResponse resp = awaitCreateConnector(connectorId, connector); + assertThat(resp.status(), anyOf(equalTo(RestStatus.CREATED), equalTo(RestStatus.OK))); + + boolean isNative = true; + + UpdateConnectorNativeAction.Request updateNativeRequest = new UpdateConnectorNativeAction.Request(connectorId, isNative); + + expectThrows(ElasticsearchStatusException.class, () -> awaitUpdateConnectorNative(updateNativeRequest)); + } + + public void testUpdateConnectorNativeTrue_WithCorrectIndexPrefix() throws Exception { + Connector connector = ConnectorTestUtils.getRandomConnectorWithAttachedIndex( + MANAGED_CONNECTOR_INDEX_PREFIX + randomAlphaOfLength(10) + ); + String connectorId = randomUUID(); + + ConnectorCreateActionResponse resp = awaitCreateConnector(connectorId, connector); + assertThat(resp.status(), anyOf(equalTo(RestStatus.CREATED), equalTo(RestStatus.OK))); + + boolean isNative = true; + + UpdateConnectorNativeAction.Request updateNativeRequest = new UpdateConnectorNativeAction.Request(connectorId, isNative); + DocWriteResponse updateResponse = awaitUpdateConnectorNative(updateNativeRequest); + assertThat(updateResponse.status(), equalTo(RestStatus.OK)); + + Connector indexedConnector = awaitGetConnector(connectorId); + assertThat(isNative, equalTo(indexedConnector.isNative())); + } + public void testUpdateConnectorStatus() throws Exception { Connector connector = ConnectorTestUtils.getRandomConnector(); String connectorId = randomUUID(); diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java index f052ef79d82fb..c563bc0a14ee3 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java @@ -343,6 +343,18 @@ public static Connector getRandomConnectorWithDetachedIndex() { return getRandomConnectorBuilder().setIndexName(null).build(); } + public static Connector getRandomConnectorWithAttachedIndex(String indexName) { + return getRandomConnectorBuilder().setIndexName(indexName).build(); + } + + public static Connector getRandomSelfManagedConnector() { + return getRandomConnectorBuilder().setIsNative(false).build(); + } + + public static Connector getRandomElasticManagedConnector() { + return getRandomConnectorBuilder().setIsNative(true).build(); + } + public static Connector getRandomConnectorWithServiceTypeNotDefined() { return getRandomConnectorBuilder().setServiceType(null).build(); } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/PostConnectorActionTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/PostConnectorActionTests.java index 0f0e83f2b9c51..e482bf3f6bb74 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/PostConnectorActionTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/PostConnectorActionTests.java @@ -20,7 +20,7 @@ public void testValidate_WhenConnectorIdAndIndexNamePresent_ExpectNoValidationEr PostConnectorAction.Request request = new PostConnectorAction.Request( randomAlphaOfLength(10), randomAlphaOfLength(10), - randomBoolean(), + false, randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10) @@ -30,6 +30,24 @@ public void testValidate_WhenConnectorIdAndIndexNamePresent_ExpectNoValidationEr assertThat(exception, nullValue()); } + public void testValidate_WrongIndexNamePresentForManagedConnector_ExpectValidationError() { + PostConnectorAction.Request requestWithIllegalIndexName = new PostConnectorAction.Request( + randomAlphaOfLength(10), + "wrong-prefix-" + randomAlphaOfLength(10), + true, + randomAlphaOfLength(10), + randomAlphaOfLength(10), + randomAlphaOfLength(10) + ); + ActionRequestValidationException exception = requestWithIllegalIndexName.validate(); + + assertThat(exception, notNullValue()); + assertThat( + exception.getMessage(), + containsString("Index attached to an Elastic-managed connector must start with the prefix: [content-]") + ); + } + public void testValidate_WhenMalformedIndexName_ExpectValidationError() { PostConnectorAction.Request requestWithMissingConnectorId = new PostConnectorAction.Request( randomAlphaOfLength(10), diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/PutConnectorActionTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/PutConnectorActionTests.java index 873e102e40931..10ab049413565 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/PutConnectorActionTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/PutConnectorActionTests.java @@ -21,7 +21,7 @@ public void testValidate_WhenConnectorIdAndIndexNamePresent_ExpectNoValidationEr randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10), - randomBoolean(), + false, randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10) @@ -31,6 +31,25 @@ public void testValidate_WhenConnectorIdAndIndexNamePresent_ExpectNoValidationEr assertThat(exception, nullValue()); } + public void testValidate_WrongIndexNamePresentForManagedConnector_ExpectValidationError() { + PutConnectorAction.Request requestWithIllegalIndexName = new PutConnectorAction.Request( + randomAlphaOfLength(10), + randomAlphaOfLength(10), + "wrong-prefix-" + randomAlphaOfLength(10), + true, + randomAlphaOfLength(10), + randomAlphaOfLength(10), + randomAlphaOfLength(10) + ); + ActionRequestValidationException exception = requestWithIllegalIndexName.validate(); + + assertThat(exception, notNullValue()); + assertThat( + exception.getMessage(), + containsString("Index attached to an Elastic-managed connector must start with the prefix: [content-]") + ); + } + public void testValidate_WhenMalformedIndexName_ExpectValidationError() { PutConnectorAction.Request requestWithMissingConnectorId = new PutConnectorAction.Request( randomAlphaOfLength(10), From fb6d7db8bdd3db245004c36a608a1df4c5a92933 Mon Sep 17 00:00:00 2001 From: Ioana Tagirta Date: Wed, 18 Dec 2024 13:11:31 +0100 Subject: [PATCH 067/119] Semantic search with query builder rewrite (#118676) * Semantic search with query builder rewrite * Address review feedback * Add feature behind snapshot * Use after/before instead of afterClass/beforeClass * Call onFailure instead of throwing exception * Fix KqlFunctionIT by requiring KqlPlugin * Update scoring tests now that they are enabled * Drop the score column for now --- .../elasticsearch/action/ResolvedIndices.java | 20 +- .../esql/qa/multi_node/SemanticMatchIT.java | 26 +++ .../esql/qa/single_node/SemanticMatchIT.java | 26 +++ .../esql/qa/rest/SemanticMatchTestCase.java | 121 ++++++++++++ .../xpack/esql/EsqlTestUtils.java | 3 + .../xpack/esql/MockQueryBuilderResolver.java | 30 +++ .../main/resources/match-function.csv-spec | 71 +++++++ .../main/resources/match-operator.csv-spec | 70 +++++++ .../src/main/resources/scoring.csv-spec | 40 ++++ .../xpack/esql/plugin/KqlFunctionIT.java | 9 + .../xpack/esql/execution/PlanExecutor.java | 5 +- .../expression/function/fulltext/Match.java | 2 + .../esql/plugin/TransportEsqlQueryAction.java | 8 +- .../xpack/esql/session/EsqlSession.java | 16 +- .../esql/session/QueryBuilderResolver.java | 178 ++++++++++++++++++ .../elasticsearch/xpack/esql/CsvTests.java | 3 +- .../LocalPhysicalPlanOptimizerTests.java | 6 + .../esql/stats/PlanExecutorMetricsTests.java | 2 + 18 files changed, 629 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/SemanticMatchIT.java create mode 100644 x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/SemanticMatchIT.java create mode 100644 x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/SemanticMatchTestCase.java create mode 100644 x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/MockQueryBuilderResolver.java create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/QueryBuilderResolver.java diff --git a/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java b/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java index f149603f12d8b..16f37c9573a8e 100644 --- a/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java +++ b/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java @@ -150,10 +150,26 @@ public static ResolvedIndices resolveWithIndicesRequest( RemoteClusterService remoteClusterService, long startTimeInMillis ) { - final Map remoteClusterIndices = remoteClusterService.groupIndices( + return resolveWithIndexNamesAndOptions( + request.indices(), request.indicesOptions(), - request.indices() + clusterState, + indexNameExpressionResolver, + remoteClusterService, + startTimeInMillis ); + } + + public static ResolvedIndices resolveWithIndexNamesAndOptions( + String[] indexNames, + IndicesOptions indicesOptions, + ClusterState clusterState, + IndexNameExpressionResolver indexNameExpressionResolver, + RemoteClusterService remoteClusterService, + long startTimeInMillis + ) { + final Map remoteClusterIndices = remoteClusterService.groupIndices(indicesOptions, indexNames); + final OriginalIndices localIndices = remoteClusterIndices.remove(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY); Index[] concreteLocalIndices = localIndices == null diff --git a/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/SemanticMatchIT.java b/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/SemanticMatchIT.java new file mode 100644 index 0000000000000..0ce84330b0b01 --- /dev/null +++ b/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/SemanticMatchIT.java @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.qa.multi_node; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; + +import org.elasticsearch.test.TestClustersThreadFilter; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.xpack.esql.qa.rest.SemanticMatchTestCase; +import org.junit.ClassRule; + +@ThreadLeakFilters(filters = TestClustersThreadFilter.class) +public class SemanticMatchIT extends SemanticMatchTestCase { + @ClassRule + public static ElasticsearchCluster cluster = Clusters.testCluster(spec -> spec.plugin("inference-service-test")); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } +} diff --git a/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/SemanticMatchIT.java b/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/SemanticMatchIT.java new file mode 100644 index 0000000000000..8edc2dbcf35a2 --- /dev/null +++ b/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/SemanticMatchIT.java @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.qa.single_node; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; + +import org.elasticsearch.test.TestClustersThreadFilter; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.xpack.esql.qa.rest.SemanticMatchTestCase; +import org.junit.ClassRule; + +@ThreadLeakFilters(filters = TestClustersThreadFilter.class) +public class SemanticMatchIT extends SemanticMatchTestCase { + @ClassRule + public static ElasticsearchCluster cluster = Clusters.testCluster(spec -> spec.plugin("inference-service-test")); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } +} diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/SemanticMatchTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/SemanticMatchTestCase.java new file mode 100644 index 0000000000000..aafa57e764ae7 --- /dev/null +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/SemanticMatchTestCase.java @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.qa.rest; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xpack.esql.action.EsqlCapabilities; +import org.junit.After; +import org.junit.Before; + +import java.io.IOException; +import java.util.Map; + +import static org.hamcrest.core.StringContains.containsString; + +public abstract class SemanticMatchTestCase extends ESRestTestCase { + public void testWithMultipleInferenceIds() throws IOException { + String query = """ + from test-semantic1,test-semantic2 + | where match(semantic_text_field, "something") + """; + ResponseException re = expectThrows(ResponseException.class, () -> runEsqlQuery(query)); + + assertThat(re.getMessage(), containsString("Field [semantic_text_field] has multiple inference IDs associated with it")); + + assertEquals(400, re.getResponse().getStatusLine().getStatusCode()); + } + + public void testWithInferenceNotConfigured() { + String query = """ + from test-semantic3 + | where match(semantic_text_field, "something") + """; + ResponseException re = expectThrows(ResponseException.class, () -> runEsqlQuery(query)); + + assertThat(re.getMessage(), containsString("Inference endpoint not found")); + assertEquals(404, re.getResponse().getStatusLine().getStatusCode()); + } + + @Before + public void setUpIndices() throws IOException { + assumeTrue("semantic text capability not available", EsqlCapabilities.Cap.SEMANTIC_TEXT_TYPE.isEnabled()); + + var settings = Settings.builder().build(); + + String mapping1 = """ + "properties": { + "semantic_text_field": { + "type": "semantic_text", + "inference_id": "test_sparse_inference" + } + } + """; + createIndex(adminClient(), "test-semantic1", settings, mapping1); + + String mapping2 = """ + "properties": { + "semantic_text_field": { + "type": "semantic_text", + "inference_id": "test_dense_inference" + } + } + """; + createIndex(adminClient(), "test-semantic2", settings, mapping2); + + String mapping3 = """ + "properties": { + "semantic_text_field": { + "type": "semantic_text", + "inference_id": "inexistent" + } + } + """; + createIndex(adminClient(), "test-semantic3", settings, mapping3); + } + + @Before + public void setUpTextEmbeddingInferenceEndpoint() throws IOException { + assumeTrue("semantic text capability not available", EsqlCapabilities.Cap.SEMANTIC_TEXT_TYPE.isEnabled()); + Request request = new Request("PUT", "_inference/text_embedding/test_dense_inference"); + request.setJsonEntity(""" + { + "service": "test_service", + "service_settings": { + "model": "my_model", + "api_key": "abc64" + }, + "task_settings": { + } + } + """); + adminClient().performRequest(request); + } + + @After + public void wipeData() throws IOException { + assumeTrue("semantic text capability not available", EsqlCapabilities.Cap.SEMANTIC_TEXT_TYPE.isEnabled()); + adminClient().performRequest(new Request("DELETE", "*")); + + try { + adminClient().performRequest(new Request("DELETE", "_inference/test_dense_inference")); + } catch (ResponseException e) { + // 404 here means the endpoint was not created + if (e.getResponse().getStatusLine().getStatusCode() != 404) { + throw e; + } + } + } + + private Map runEsqlQuery(String query) throws IOException { + RestEsqlTestCase.RequestObjectBuilder builder = RestEsqlTestCase.requestObjectBuilder().query(query); + return RestEsqlTestCase.runEsqlSync(builder); + } +} diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java index 18ce9d7e3e057..66fd7d3ee5eb5 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java @@ -70,6 +70,7 @@ import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; import org.elasticsearch.xpack.esql.plugin.QueryPragmas; import org.elasticsearch.xpack.esql.session.Configuration; +import org.elasticsearch.xpack.esql.session.QueryBuilderResolver; import org.elasticsearch.xpack.esql.stats.Metrics; import org.elasticsearch.xpack.esql.stats.SearchStats; import org.elasticsearch.xpack.versionfield.Version; @@ -351,6 +352,8 @@ public String toString() { public static final Verifier TEST_VERIFIER = new Verifier(new Metrics(new EsqlFunctionRegistry()), new XPackLicenseState(() -> 0L)); + public static final QueryBuilderResolver MOCK_QUERY_BUILDER_RESOLVER = new MockQueryBuilderResolver(); + private EsqlTestUtils() {} public static Configuration configuration(QueryPragmas pragmas, String query) { diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/MockQueryBuilderResolver.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/MockQueryBuilderResolver.java new file mode 100644 index 0000000000000..7af3a89108fc0 --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/MockQueryBuilderResolver.java @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; +import org.elasticsearch.xpack.esql.session.QueryBuilderResolver; +import org.elasticsearch.xpack.esql.session.Result; + +import java.util.function.BiConsumer; + +public class MockQueryBuilderResolver extends QueryBuilderResolver { + public MockQueryBuilderResolver() { + super(null, null, null, null); + } + + @Override + public void resolveQueryBuilders( + LogicalPlan plan, + ActionListener listener, + BiConsumer> callback + ) { + callback.accept(plan, listener); + } +} diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec index 5ea169e1b110d..6c9a6fed3853c 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec @@ -597,3 +597,74 @@ from employees,employees_incompatible emp_no_bool:boolean ; + +testMatchWithSemanticText +required_capability: match_function +required_capability: semantic_text_type + +from semantic_text +| where match(semantic_text_field, "something") +| keep semantic_text_field +| sort semantic_text_field asc +; + +semantic_text_field:semantic_text +all we have to decide is what to do with the time that is given to us +be excellent to each other +live long and prosper +; + +testMatchWithSemanticTextAndKeyword +required_capability: match_function +required_capability: semantic_text_type + +from semantic_text +| where match(semantic_text_field, "something") AND match(host, "host1") +| keep semantic_text_field, host +; + +semantic_text_field:semantic_text | host:keyword +live long and prosper | host1 +; + +testMatchWithSemanticTextMultiValueField +required_capability: match_function +required_capability: semantic_text_type + +from semantic_text metadata _id +| where match(st_multi_value, "something") AND match(host, "host1") +| keep _id, st_multi_value +; + +_id: keyword | st_multi_value:semantic_text +1 | ["Hello there!", "This is a random value", "for testing purposes"] +; + +testMatchWithSemanticTextWithEvalsAndOtherFunctionsAndStats +required_capability: match_function +required_capability: semantic_text_type + +from semantic_text +| where qstr("description:some*") +| eval size = mv_count(st_multi_value) +| where match(semantic_text_field, "something") AND size > 1 AND match(host, "host1") +| STATS result = count(*) +; + +result:long +1 +; + +testMatchWithSemanticTextAndKql +required_capability: match_function +required_capability: semantic_text_type +required_capability: kql_function + +from semantic_text +| where kql("host:host1") AND match(semantic_text_field, "something") +| KEEP host, semantic_text_field +; + +host:keyword | semantic_text_field:semantic_text +"host1" | live long and prosper +; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec index 7906f8b69162b..721443a70fe20 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec @@ -608,3 +608,73 @@ from employees,employees_incompatible emp_no_bool:boolean ; +testMatchWithSemanticText +required_capability: match_operator_colon +required_capability: semantic_text_type + +from semantic_text +| where semantic_text_field:"something" +| keep semantic_text_field +| sort semantic_text_field asc +; + +semantic_text_field:semantic_text +all we have to decide is what to do with the time that is given to us +be excellent to each other +live long and prosper +; + +testMatchWithSemanticTextAndKeyword +required_capability: match_operator_colon +required_capability: semantic_text_type + +from semantic_text +| where semantic_text_field:"something" AND host:"host1" +| keep semantic_text_field, host +; + +semantic_text_field:semantic_text | host:keyword +live long and prosper | host1 +; + +testMatchWithSemanticTextMultiValueField +required_capability: match_operator_colon +required_capability: semantic_text_type + +from semantic_text metadata _id +| where st_multi_value:"something" AND match(host, "host1") +| keep _id, st_multi_value +; + +_id: keyword | st_multi_value:semantic_text +1 | ["Hello there!", "This is a random value", "for testing purposes"] +; + +testMatchWithSemanticTextWithEvalsAndOtherFunctionsAndStats +required_capability: match_operator_colon +required_capability: semantic_text_type + +from semantic_text +| where qstr("description:some*") +| eval size = mv_count(st_multi_value) +| where semantic_text_field:"something" AND size > 1 AND match(host, "host1") +| STATS result = count(*) +; + +result:long +1 +; + +testMatchWithSemanticTextAndKql +required_capability: match_operator_colon +required_capability: semantic_text_type +required_capability: kql_function + +from semantic_text +| where kql("host:host1") AND semantic_text_field:"something" +| KEEP host, semantic_text_field +; + +host:keyword | semantic_text_field:semantic_text +"host1" | live long and prosper +; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec index 72632c62603aa..9d3526982f9ef 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec @@ -317,3 +317,43 @@ book_no:keyword | title:text 2924 | A Gentle Creature and Other Stories: White Nights, A Gentle Creature, and The Dream of a Ridiculous Man (The World's Classics) | foobar 5948 | That We Are Gentle Creatures | foobar ; + + +semanticTextMatch +required_capability: metadata_score +required_capability: semantic_text_type +required_capability: match_function + +from semantic_text metadata _id, _score +| where match(semantic_text_field, "something") +| sort _score desc +| keep _id +; + +_id:keyword +2 +3 +1 +; + +semanticTextMatchWithAllTheTextFunctions + +required_capability: metadata_score +required_capability: semantic_text_type +required_capability: match_function +required_capability: kql_function +required_capability: qstr_function + +from semantic_text metadata _id, _score +| where match(semantic_text_field, "something") + AND match(description, "some") + AND kql("description:some*") + AND NOT qstr("host:host1") +| sort _score desc +| keep _id +; + +_id:keyword +2 +3 +; diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/KqlFunctionIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/KqlFunctionIT.java index d58637ab52c86..0e84ac7588ad6 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/KqlFunctionIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/KqlFunctionIT.java @@ -10,13 +10,17 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.index.query.QueryShardException; +import org.elasticsearch.plugins.Plugin; import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.action.AbstractEsqlIntegTestCase; import org.elasticsearch.xpack.esql.action.EsqlCapabilities; +import org.elasticsearch.xpack.kql.KqlPlugin; import org.junit.Before; import org.junit.BeforeClass; +import java.util.Collection; import java.util.List; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -141,4 +145,9 @@ private void createAndPopulateIndex() { .get(); ensureYellow(indexName); } + + @Override + protected Collection> nodePlugins() { + return CollectionUtils.appendToCopy(super.nodePlugins(), KqlPlugin.class); + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/execution/PlanExecutor.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/execution/PlanExecutor.java index c1269009c6a41..dad63d25046d9 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/execution/PlanExecutor.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/execution/PlanExecutor.java @@ -23,6 +23,7 @@ import org.elasticsearch.xpack.esql.session.Configuration; import org.elasticsearch.xpack.esql.session.EsqlSession; import org.elasticsearch.xpack.esql.session.IndexResolver; +import org.elasticsearch.xpack.esql.session.QueryBuilderResolver; import org.elasticsearch.xpack.esql.session.Result; import org.elasticsearch.xpack.esql.stats.Metrics; import org.elasticsearch.xpack.esql.stats.PlanningMetrics; @@ -59,6 +60,7 @@ public void esql( EsqlExecutionInfo executionInfo, IndicesExpressionGrouper indicesExpressionGrouper, EsqlSession.PlanRunner planRunner, + QueryBuilderResolver queryBuilderResolver, ActionListener listener ) { final PlanningMetrics planningMetrics = new PlanningMetrics(); @@ -73,7 +75,8 @@ public void esql( mapper, verifier, planningMetrics, - indicesExpressionGrouper + indicesExpressionGrouper, + queryBuilderResolver ); QueryMetric clientId = QueryMetric.fromString("rest"); metrics.total(clientId); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 0b2268fe1b022..e695a94198dab 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -51,6 +51,7 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.IP; import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; import static org.elasticsearch.xpack.esql.core.type.DataType.LONG; +import static org.elasticsearch.xpack.esql.core.type.DataType.SEMANTIC_TEXT; import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG; import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION; @@ -70,6 +71,7 @@ public class Match extends FullTextFunction implements Validatable { public static final Set FIELD_DATA_TYPES = Set.of( KEYWORD, TEXT, + SEMANTIC_TEXT, BOOLEAN, DATETIME, DATE_NANOS, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlQueryAction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlQueryAction.java index 76bfb95d07926..50d5819688e46 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlQueryAction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlQueryAction.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; @@ -42,6 +43,7 @@ import org.elasticsearch.xpack.esql.execution.PlanExecutor; import org.elasticsearch.xpack.esql.session.Configuration; import org.elasticsearch.xpack.esql.session.EsqlSession.PlanRunner; +import org.elasticsearch.xpack.esql.session.QueryBuilderResolver; import org.elasticsearch.xpack.esql.session.Result; import java.io.IOException; @@ -68,6 +70,7 @@ public class TransportEsqlQueryAction extends HandledTransportAction asyncTaskManagementService; private final RemoteClusterService remoteClusterService; + private final QueryBuilderResolver queryBuilderResolver; @Inject @SuppressWarnings("this-escape") @@ -82,7 +85,8 @@ public TransportEsqlQueryAction( BigArrays bigArrays, BlockFactory blockFactory, Client client, - NamedWriteableRegistry registry + NamedWriteableRegistry registry, + IndexNameExpressionResolver indexNameExpressionResolver ) { // TODO replace SAME when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 @@ -121,6 +125,7 @@ public TransportEsqlQueryAction( bigArrays ); this.remoteClusterService = transportService.getRemoteClusterService(); + this.queryBuilderResolver = new QueryBuilderResolver(searchService, clusterService, transportService, indexNameExpressionResolver); } @Override @@ -191,6 +196,7 @@ private void innerExecute(Task task, EsqlQueryRequest request, ActionListener toResponse(task, request, configuration, result)) ); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java index c0290fa2b1d73..bd3b3bdb3483c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java @@ -113,6 +113,7 @@ public interface PlanRunner { private final PhysicalPlanOptimizer physicalPlanOptimizer; private final PlanningMetrics planningMetrics; private final IndicesExpressionGrouper indicesExpressionGrouper; + private final QueryBuilderResolver queryBuilderResolver; public EsqlSession( String sessionId, @@ -125,7 +126,8 @@ public EsqlSession( Mapper mapper, Verifier verifier, PlanningMetrics planningMetrics, - IndicesExpressionGrouper indicesExpressionGrouper + IndicesExpressionGrouper indicesExpressionGrouper, + QueryBuilderResolver queryBuilderResolver ) { this.sessionId = sessionId; this.configuration = configuration; @@ -139,6 +141,7 @@ public EsqlSession( this.physicalPlanOptimizer = new PhysicalPlanOptimizer(new PhysicalOptimizerContext(configuration)); this.planningMetrics = planningMetrics; this.indicesExpressionGrouper = indicesExpressionGrouper; + this.queryBuilderResolver = queryBuilderResolver; } public String sessionId() { @@ -158,7 +161,16 @@ public void execute(EsqlQueryRequest request, EsqlExecutionInfo executionInfo, P new EsqlSessionCCSUtils.CssPartialErrorsActionListener(executionInfo, listener) { @Override public void onResponse(LogicalPlan analyzedPlan) { - executeOptimizedPlan(request, executionInfo, planRunner, optimizedPlan(analyzedPlan), listener); + try { + var optimizedPlan = optimizedPlan(analyzedPlan); + queryBuilderResolver.resolveQueryBuilders( + optimizedPlan, + listener, + (newPlan, next) -> executeOptimizedPlan(request, executionInfo, planRunner, newPlan, next) + ); + } catch (Exception e) { + listener.onFailure(e); + } } } ); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/QueryBuilderResolver.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/QueryBuilderResolver.java new file mode 100644 index 0000000000000..b6424c5f7fa56 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/QueryBuilderResolver.java @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.session; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ResolvedIndices; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.Rewriteable; +import org.elasticsearch.search.SearchService; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.esql.action.EsqlCapabilities; +import org.elasticsearch.xpack.esql.core.util.Holder; +import org.elasticsearch.xpack.esql.expression.function.fulltext.FullTextFunction; +import org.elasticsearch.xpack.esql.plan.logical.EsRelation; +import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; +import org.elasticsearch.xpack.esql.planner.PlannerUtils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +/** + * Some {@link FullTextFunction} implementations such as {@link org.elasticsearch.xpack.esql.expression.function.fulltext.Match} + * will be translated to a {@link QueryBuilder} that require a rewrite phase on the coordinator. + * {@link QueryBuilderResolver#resolveQueryBuilders(LogicalPlan, ActionListener, BiConsumer)} will rewrite the plan by replacing + * {@link FullTextFunction} expression with new ones that hold rewritten {@link QueryBuilder}s. + */ +public class QueryBuilderResolver { + private final SearchService searchService; + private final ClusterService clusterService; + private final TransportService transportService; + private final IndexNameExpressionResolver indexNameExpressionResolver; + + public QueryBuilderResolver( + SearchService searchService, + ClusterService clusterService, + TransportService transportService, + IndexNameExpressionResolver indexNameExpressionResolver + ) { + this.searchService = searchService; + this.clusterService = clusterService; + this.transportService = transportService; + this.indexNameExpressionResolver = indexNameExpressionResolver; + } + + public void resolveQueryBuilders( + LogicalPlan plan, + ActionListener listener, + BiConsumer> callback + ) { + // TODO: remove once SEMANTIC_TEXT_TYPE is enabled outside of snapshots + if (false == EsqlCapabilities.Cap.SEMANTIC_TEXT_TYPE.isEnabled()) { + callback.accept(plan, listener); + return; + } + + if (plan.optimized() == false) { + listener.onFailure(new IllegalStateException("Expected optimized plan before query builder rewrite.")); + return; + } + + Set unresolved = fullTextFunctions(plan); + Set indexNames = indexNames(plan); + + if (indexNames == null || indexNames.isEmpty() || unresolved.isEmpty()) { + callback.accept(plan, listener); + return; + } + QueryRewriteContext ctx = queryRewriteContext(indexNames); + FullTextFunctionsRewritable rewritable = new FullTextFunctionsRewritable(unresolved); + Rewriteable.rewriteAndFetch(rewritable, ctx, new ActionListener() { + @Override + public void onResponse(FullTextFunctionsRewritable fullTextFunctionsRewritable) { + try { + LogicalPlan newPlan = planWithResolvedQueryBuilders(plan, fullTextFunctionsRewritable.results()); + callback.accept(newPlan, listener); + } catch (Exception e) { + onFailure(e); + } + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } + + private Set fullTextFunctions(LogicalPlan plan) { + Set functions = new HashSet<>(); + plan.forEachExpressionDown(FullTextFunction.class, func -> functions.add(func)); + return functions; + } + + public Set indexNames(LogicalPlan plan) { + Holder> indexNames = new Holder<>(); + + plan.forEachDown(EsRelation.class, esRelation -> { indexNames.set(esRelation.index().concreteIndices()); }); + + return indexNames.get(); + } + + public LogicalPlan planWithResolvedQueryBuilders(LogicalPlan plan, Map newQueryBuilders) { + LogicalPlan newPlan = plan.transformExpressionsDown(FullTextFunction.class, m -> { + if (newQueryBuilders.keySet().contains(m)) { + return m.replaceQueryBuilder(newQueryBuilders.get(m)); + } + return m; + }); + // The given plan was already analyzed and optimized, so we set the resulted plan to optimized as well. + newPlan.setOptimized(); + return newPlan; + } + + private QueryRewriteContext queryRewriteContext(Set indexNames) { + ResolvedIndices resolvedIndices = ResolvedIndices.resolveWithIndexNamesAndOptions( + indexNames.toArray(String[]::new), + IndexResolver.FIELD_CAPS_INDICES_OPTIONS, + clusterService.state(), + indexNameExpressionResolver, + transportService.getRemoteClusterService(), + System.currentTimeMillis() + ); + + return searchService.getRewriteContext(() -> System.currentTimeMillis(), resolvedIndices, null); + } + + private class FullTextFunctionsRewritable implements Rewriteable { + + private final Map queryBuilderMap; + + FullTextFunctionsRewritable(Map queryBuilderMap) { + this.queryBuilderMap = queryBuilderMap; + } + + FullTextFunctionsRewritable(Set functions) { + this.queryBuilderMap = new HashMap<>(); + + for (FullTextFunction func : functions) { + queryBuilderMap.put(func, func.asQuery(PlannerUtils.TRANSLATOR_HANDLER).asBuilder()); + } + } + + @Override + public FullTextFunctionsRewritable rewrite(QueryRewriteContext ctx) throws IOException { + Map results = new HashMap<>(); + + boolean hasChanged = false; + for (FullTextFunction func : queryBuilderMap.keySet()) { + var initial = queryBuilderMap.get(func); + var rewritten = Rewriteable.rewrite(initial, ctx, false); + + if (rewritten.equals(initial) == false) { + hasChanged = true; + } + + results.put(func, rewritten); + } + + return hasChanged ? new FullTextFunctionsRewritable(results) : this; + } + + public Map results() { + return queryBuilderMap; + } + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index 717ac7b5a62a7..70794e4a82130 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -445,7 +445,8 @@ private ActualResults executePlan(BigArrays bigArrays) throws Exception { mapper, TEST_VERIFIER, new PlanningMetrics(), - null + null, + EsqlTestUtils.MOCK_QUERY_BUILDER_RESOLVER ); TestPhysicalOperationProviders physicalOperationProviders = testOperationProviders(testDataset); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 406e27c1517e5..928c849b847d5 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -1450,6 +1450,11 @@ private void checkMatchFunctionPushDown( var analyzer = makeAnalyzer("mapping-all-types.json"); // Check for every possible query data type for (DataType fieldDataType : fieldDataTypes) { + // TODO: semantic_text is not present in mapping-all-types.json so we skip it for now + if (fieldDataType == DataType.SEMANTIC_TEXT) { + continue; + } + var queryValue = randomQueryValue(fieldDataType); String fieldName = fieldDataType == DataType.DATETIME ? "date" : fieldDataType.name().toLowerCase(Locale.ROOT); @@ -1483,6 +1488,7 @@ private static Object randomQueryValue(DataType dataType) { case KEYWORD -> randomAlphaOfLength(5); case IP -> NetworkAddress.format(randomIp(randomBoolean())); case TEXT -> randomAlphaOfLength(50); + case SEMANTIC_TEXT -> randomAlphaOfLength(5); case VERSION -> VersionUtils.randomVersion(random()).toString(); default -> throw new IllegalArgumentException("Unexpected type: " + dataType); }; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/stats/PlanExecutorMetricsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/stats/PlanExecutorMetricsTests.java index b323efad2b4c3..539cd0314a4d1 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/stats/PlanExecutorMetricsTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/stats/PlanExecutorMetricsTests.java @@ -123,6 +123,7 @@ public void testFailedMetric() { new EsqlExecutionInfo(randomBoolean()), groupIndicesByCluster, runPhase, + EsqlTestUtils.MOCK_QUERY_BUILDER_RESOLVER, new ActionListener<>() { @Override public void onResponse(Result result) { @@ -152,6 +153,7 @@ public void onFailure(Exception e) { new EsqlExecutionInfo(randomBoolean()), groupIndicesByCluster, runPhase, + EsqlTestUtils.MOCK_QUERY_BUILDER_RESOLVER, new ActionListener<>() { @Override public void onResponse(Result result) {} From 47d5e87d21dd682539c9da24a4cd321b68a97f05 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 18 Dec 2024 13:18:14 +0100 Subject: [PATCH 068/119] Prevent boot if bind DN is set without password (#118366) LDAP/AD authentication realms can be configured to authenticate through LDAP via a bind user. For this it's necessary to set a bind DN (via `bind_dn`) together with a bind password (via `bind_password` or `secure_bind_password`). Setting a bind DN without a bind password will cause all LDAP/AD realm authentication to fail, leaving the node non-operational. This PR adds a bootstrap check to prevent a misconfigured node from starting. This behavior was deprecated in https://github.com/elastic/elasticsearch/pull/85326. Closes: ES-9749 --- docs/changelog/118366.yaml | 22 +++++++++++ .../authc/ldap/PoolingSessionFactory.java | 37 +++++-------------- .../LdapUserSearchSessionFactoryTests.java | 29 +++++++-------- 3 files changed, 46 insertions(+), 42 deletions(-) create mode 100644 docs/changelog/118366.yaml diff --git a/docs/changelog/118366.yaml b/docs/changelog/118366.yaml new file mode 100644 index 0000000000000..cfeab1937738b --- /dev/null +++ b/docs/changelog/118366.yaml @@ -0,0 +1,22 @@ +pr: 118366 +summary: |- + Configuring a bind DN in an LDAP or Active Directory (AD) realm without a corresponding bind password + will prevent node from starting +area: Authentication +type: breaking +issues: [] +breaking: + title: -| + Configuring a bind DN in an LDAP or Active Directory (AD) realm without + a corresponding bind password will prevent node from starting + area: Cluster and node setting + details: -| + For LDAP or AD authentication realms, setting a bind DN (via the + `xpack.security.authc.realms.ldap.*.bind_dn` or `xpack.security.authc.realms.active_directory.*.bind_dn` + realm settings) without a bind password is a misconfiguration that may prevent successful authentication + to the node. Nodes will fail to start if a bind DN is specified without a password. + impact: -| + If you have a bind DN configured for an LDAP or AD authentication + realm, set a bind password for {ref}/ldap-realm.html#ldap-realm-configuration[LDAP] + or {ref}/active-directory-realm.html#ad-realm-configuration[Active Directory]. + Configuring a bind DN without a password prevents the misconfigured node from starting. diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java index 4f87ac27be141..f31d5647b4cc5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java @@ -16,7 +16,6 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; @@ -74,7 +73,7 @@ abstract class PoolingSessionFactory extends SessionFactory implements Releasabl super(config, sslService, threadPool); this.groupResolver = groupResolver; this.bindDn = bindDn; - this.bindRequest = new AtomicReference<>(buildBindRequest(config.settings(), false)); + this.bindRequest = new AtomicReference<>(buildBindRequest(config.settings())); this.useConnectionPool = config.getSetting(poolingEnabled); if (useConnectionPool) { this.connectionPool = createConnectionPool(config, serverSet, timeout, logger, bindRequest.get(), healthCheckDNSupplier); @@ -93,11 +92,9 @@ abstract class PoolingSessionFactory extends SessionFactory implements Releasabl * will perform a setting consistency validation and throw {@link SettingsException} in case of violation. * Due to legacy reasons and BWC, when {@code reloadRequest} is se to {@code false}, this method will only log a warning message. * - * @param reloadRequest {@code true} if this method is called during reloading of secure settings, - * {@code false} if it is called during bootstrapping. * @return A new {@link SimpleBindRequest} that contains configured bind DN and password. */ - private SimpleBindRequest buildBindRequest(Settings settings, boolean reloadRequest) { + private SimpleBindRequest buildBindRequest(Settings settings) { final byte[] bindPassword; final Setting legacyPasswordSetting = config.getConcreteSetting(LEGACY_BIND_PASSWORD); final Setting securePasswordSetting = config.getConcreteSetting(SECURE_BIND_PASSWORD); @@ -119,27 +116,13 @@ private SimpleBindRequest buildBindRequest(Settings settings, boolean reloadRequ return new SimpleBindRequest(); } else { if (bindPassword == null) { - if (reloadRequest) { - throw new SettingsException( - "[{}] is set but no bind password is specified. Without a corresponding bind password, " - + "all {} realm authentication will fail. Specify a bind password via [{}].", - RealmSettings.getFullSettingKey(config, PoolingSessionFactorySettings.BIND_DN), - config.type(), - RealmSettings.getFullSettingKey(config, SECURE_BIND_PASSWORD) - ); - } else { - deprecationLogger.critical( - DeprecationCategory.SECURITY, - "bind_dn_set_without_password", - "[{}] is set but no bind password is specified. Without a corresponding bind password, " - + "all {} realm authentication will fail. Specify a bind password via [{}] or [{}]. " - + "In the next major release, nodes with incomplete bind credentials will fail to start.", - RealmSettings.getFullSettingKey(config, PoolingSessionFactorySettings.BIND_DN), - config.type(), - RealmSettings.getFullSettingKey(config, SECURE_BIND_PASSWORD), - RealmSettings.getFullSettingKey(config, LEGACY_BIND_PASSWORD) - ); - } + throw new SettingsException( + "[{}] is set but no bind password is specified. Without a corresponding bind password, " + + "all {} realm authentication will fail. Specify a bind password via [{}].", + RealmSettings.getFullSettingKey(config, PoolingSessionFactorySettings.BIND_DN), + config.type(), + RealmSettings.getFullSettingKey(config, SECURE_BIND_PASSWORD) + ); } return new SimpleBindRequest(this.bindDn, bindPassword); } @@ -148,7 +131,7 @@ private SimpleBindRequest buildBindRequest(Settings settings, boolean reloadRequ @Override public void reload(Settings settings) { final SimpleBindRequest oldRequest = bindRequest.get(); - final SimpleBindRequest newRequest = buildBindRequest(settings, true); + final SimpleBindRequest newRequest = buildBindRequest(settings); if (bindRequestEquals(newRequest, oldRequest) == false) { if (bindRequest.compareAndSet(oldRequest, newRequest)) { if (connectionPool != null) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java index acb4359b37323..5482d7711031e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsException; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.TimeValue; import org.elasticsearch.env.TestEnvironment; @@ -45,7 +46,6 @@ import java.util.Locale; import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey; -import static org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings.BIND_DN; import static org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings.LEGACY_BIND_PASSWORD; import static org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings.SECURE_BIND_PASSWORD; import static org.hamcrest.Matchers.containsString; @@ -199,7 +199,7 @@ public void testUserSearchBaseScopeFailsWithWrongBaseDN() throws Exception { assertDeprecationWarnings(config.identifier(), useAttribute, useLegacyBindPassword); } - public void testConstructorLogsErrorIfBindDnSetWithoutPassword() throws Exception { + public void testConstructorThrowsIfBindDnSetWithoutPassword() throws Exception { String groupSearchBase = "o=sevenSeas"; String userSearchBase = "cn=William Bush,ou=people,o=sevenSeas"; @@ -216,19 +216,18 @@ public void testConstructorLogsErrorIfBindDnSetWithoutPassword() throws Exceptio new ThreadContext(globalSettings) ); - try (LdapUserSearchSessionFactory ignored = getLdapUserSearchSessionFactory(config, sslService, threadPool)) { - assertCriticalWarnings( - String.format( - Locale.ROOT, - "[%s] is set but no bind password is specified. Without a corresponding bind password, " - + "all ldap realm authentication will fail. Specify a bind password via [%s] or [%s]. " - + "In the next major release, nodes with incomplete bind credentials will fail to start.", - RealmSettings.getFullSettingKey(config, BIND_DN), - RealmSettings.getFullSettingKey(config, SECURE_BIND_PASSWORD), - RealmSettings.getFullSettingKey(config, LEGACY_BIND_PASSWORD) - ) - ); - } + Exception ex = expectThrows(SettingsException.class, () -> getLdapUserSearchSessionFactory(config, sslService, threadPool)); + assertEquals( + String.format( + Locale.ROOT, + "[%s] is set but no bind password is specified. Without a corresponding bind password, " + + "all %s realm authentication will fail. Specify a bind password via [%s].", + RealmSettings.getFullSettingKey(config, PoolingSessionFactorySettings.BIND_DN), + config.type(), + RealmSettings.getFullSettingKey(config, SECURE_BIND_PASSWORD) + ), + ex.getMessage() + ); } public void testConstructorThrowsIfBothLegacyAndSecureBindPasswordSet() throws Exception { From ede1a7ab2af7156edd0e3cb6626cb9cbf8e858c4 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Wed, 18 Dec 2024 13:46:21 +0100 Subject: [PATCH 069/119] Include v7 in IndexVersionUtils#ALL_VERSIONS (#118793) We are going to retain read-only compatibility with v7 index versions upon upgrade to v9. This means that all the v7 index versions and corresponding version conditionals will stay around for another major series. This PR reflects this decision in the codebase. No need to filter index versions when retrieving ALL_VERSIONS, and we can remove corresponding @UpdateForV9 annotations from the IndexVersions class. At the same time, there are tests that need to randomize version but need to write to an index, hence 7x versions should be filtered out. The overall goal is to extend testing to v7 index versions when possible, and making randomization across writeable versions an exception that certain tests can rely on as needed. These are the mechanical steps I made in this PR: - Rename `getFirstVersion` to `getLowestReadCompatibleVersion`, which returns the lowest supported index version ( which can not be written to) - Introduce `getLowestWriteCompatibleVersion` to identify the lowest writeable version. This is used by tests that used to call `getFirstVersion` and need to write to the index. - Remove `randomVersion(Random random)` in favour of `randomVersion()` . It was always called providing `random()` which is equivalent to what `randomVersion()` already does. - Introduce `randomWriteVersion` for tests that need a random writeable version. Moved tests that need it to use it (from `randomVersion` to `randomWriteVersion`) There is still work to do in `IndexVersionUtils` to extend testing, especially as some randomized tests have a lower bound of `MINIMUM_COMPATIBLE_VERSION` hence don't include v7 index versions yet. We will address that as a follow-up. --- .../common/NGramTokenizerFactoryTests.java | 2 +- .../common/PersianAnalyzerProviderTests.java | 2 +- .../common/RomanianAnalyzerTests.java | 2 +- .../StemmerTokenFilterFactoryTests.java | 10 +++++----- .../admin/indices/create/ShrinkIndexIT.java | 2 +- .../cluster/ClusterStateDiffIT.java | 7 ++++--- .../PreBuiltAnalyzerIntegrationIT.java | 2 +- .../indices/recovery/IndexRecoveryIT.java | 2 +- .../elasticsearch/index/IndexVersions.java | 11 +++++----- .../HumanReadableIndexSettingsTests.java | 2 +- .../MetadataCreateIndexServiceTests.java | 6 +----- .../MetadataDeleteIndexServiceTests.java | 6 +++--- .../MetadataIndexAliasesServiceTests.java | 2 +- .../MetadataIndexStateServiceTests.java | 2 +- .../elasticsearch/env/NodeMetadataTests.java | 2 +- .../index/IndexVersionTests.java | 9 ++------- .../index/analysis/AnalysisRegistryTests.java | 12 +++++------ .../index/analysis/PreBuiltAnalyzerTests.java | 6 +++--- .../PreConfiguredTokenFilterTests.java | 6 +++--- .../index/engine/InternalEngineTests.java | 2 +- .../vectors/DenseVectorFieldMapperTests.java | 8 ++++---- .../indices/analysis/AnalysisModuleTests.java | 6 +++--- .../IncorrectSetupStablePluginsTests.java | 6 +++--- .../StableAnalysisPluginsNoSettingsTests.java | 2 +- ...tableAnalysisPluginsWithSettingsTests.java | 8 ++++---- .../recovery/RecoverySourceHandlerTests.java | 2 +- .../nodesinfo/NodeInfoStreamingTests.java | 2 +- .../PersistentTasksClusterServiceTests.java | 4 ++-- ...SnapshotsInProgressSerializationTests.java | 2 +- .../index/KnownIndexVersions.java | 6 +++++- .../test/index/IndexVersionUtils.java | 20 ++++++++++++------- ...eprecationRoleDescriptorConsumerTests.java | 2 +- 32 files changed, 83 insertions(+), 80 deletions(-) diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/NGramTokenizerFactoryTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/NGramTokenizerFactoryTests.java index 8c365a1362f85..35c01b5b9296f 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/NGramTokenizerFactoryTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/NGramTokenizerFactoryTests.java @@ -161,7 +161,7 @@ public void testBackwardsCompatibilityEdgeNgramTokenFilter() throws Exception { for (int i = 0; i < iters; i++) { final Index index = new Index("test", "_na_"); final String name = "ngr"; - IndexVersion v = IndexVersionUtils.randomVersion(random()); + IndexVersion v = IndexVersionUtils.randomVersion(); Builder builder = newAnalysisSettingsBuilder().put("min_gram", 2).put("max_gram", 3); boolean reverse = random().nextBoolean(); if (reverse) { diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PersianAnalyzerProviderTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PersianAnalyzerProviderTests.java index 7b962538c2a10..153c3e9549285 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PersianAnalyzerProviderTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PersianAnalyzerProviderTests.java @@ -56,7 +56,7 @@ public void testPersianAnalyzerPostLucene10() throws IOException { public void testPersianAnalyzerPreLucene10() throws IOException { IndexVersion preLucene10Version = IndexVersionUtils.randomVersionBetween( random(), - IndexVersionUtils.getFirstVersion(), + IndexVersionUtils.getLowestReadCompatibleVersion(), IndexVersionUtils.getPreviousVersion(IndexVersions.UPGRADE_TO_LUCENE_10_0_0) ); Settings settings = ESTestCase.indexSettings(1, 1) diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/RomanianAnalyzerTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/RomanianAnalyzerTests.java index 1af44bc71f35d..29e27e62e3164 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/RomanianAnalyzerTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/RomanianAnalyzerTests.java @@ -57,7 +57,7 @@ public void testRomanianAnalyzerPostLucene10() throws IOException { public void testRomanianAnalyzerPreLucene10() throws IOException { IndexVersion preLucene10Version = IndexVersionUtils.randomVersionBetween( random(), - IndexVersionUtils.getFirstVersion(), + IndexVersionUtils.getLowestReadCompatibleVersion(), IndexVersionUtils.getPreviousVersion(IndexVersions.UPGRADE_TO_LUCENE_10_0_0) ); Settings settings = ESTestCase.indexSettings(1, 1) diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/StemmerTokenFilterFactoryTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/StemmerTokenFilterFactoryTests.java index bb06c221873b5..4e774d92e3d62 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/StemmerTokenFilterFactoryTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/StemmerTokenFilterFactoryTests.java @@ -39,7 +39,7 @@ public class StemmerTokenFilterFactoryTests extends ESTokenStreamTestCase { public void testEnglishFilterFactory() throws IOException { int iters = scaledRandomIntBetween(20, 100); for (int i = 0; i < iters; i++) { - IndexVersion v = IndexVersionUtils.randomVersion(random()); + IndexVersion v = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder() .put("index.analysis.filter.my_english.type", "stemmer") .put("index.analysis.filter.my_english.language", "english") @@ -66,7 +66,7 @@ public void testPorter2FilterFactory() throws IOException { int iters = scaledRandomIntBetween(20, 100); for (int i = 0; i < iters; i++) { - IndexVersion v = IndexVersionUtils.randomVersion(random()); + IndexVersion v = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder() .put("index.analysis.filter.my_porter2.type", "stemmer") .put("index.analysis.filter.my_porter2.language", "porter2") @@ -90,7 +90,7 @@ public void testPorter2FilterFactory() throws IOException { } public void testMultipleLanguagesThrowsException() throws IOException { - IndexVersion v = IndexVersionUtils.randomVersion(random()); + IndexVersion v = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder() .put("index.analysis.filter.my_english.type", "stemmer") .putList("index.analysis.filter.my_english.language", "english", "light_english") @@ -142,7 +142,7 @@ private static Analyzer createGermanStemmer(String variant, IndexVersion v) thro } public void testKpDeprecation() throws IOException { - IndexVersion v = IndexVersionUtils.randomVersion(random()); + IndexVersion v = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder() .put("index.analysis.filter.my_kp.type", "stemmer") .put("index.analysis.filter.my_kp.language", "kp") @@ -155,7 +155,7 @@ public void testKpDeprecation() throws IOException { } public void testLovinsDeprecation() throws IOException { - IndexVersion v = IndexVersionUtils.randomVersion(random()); + IndexVersion v = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder() .put("index.analysis.filter.my_lovins.type", "stemmer") .put("index.analysis.filter.my_lovins.language", "lovins") diff --git a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/ShrinkIndexIT.java b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/ShrinkIndexIT.java index 4f6d24b419595..ca9fdb27ac389 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/ShrinkIndexIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/ShrinkIndexIT.java @@ -236,7 +236,7 @@ private static IndexMetadata indexMetadata(final Client client, final String ind public void testCreateShrinkIndex() { internalCluster().ensureAtLeastNumDataNodes(2); - IndexVersion version = IndexVersionUtils.randomVersion(random()); + IndexVersion version = IndexVersionUtils.randomWriteVersion(); prepareCreate("source").setSettings( Settings.builder().put(indexSettings()).put("number_of_shards", randomIntBetween(2, 7)).put("index.version.created", version) ).get(); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java index 58b9af7724aaa..e8e4eb7562462 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java @@ -71,6 +71,7 @@ import static org.elasticsearch.test.XContentTestUtils.convertToMap; import static org.elasticsearch.test.XContentTestUtils.differenceBetweenMapsIgnoringArrayOrder; import static org.elasticsearch.test.index.IndexVersionUtils.randomVersion; +import static org.elasticsearch.test.index.IndexVersionUtils.randomWriteVersion; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -218,7 +219,7 @@ private ClusterState.Builder randomCoordinationMetadata(ClusterState clusterStat private DiscoveryNode randomNode(String nodeId) { Version nodeVersion = VersionUtils.randomVersion(random()); - IndexVersion indexVersion = randomVersion(random()); + IndexVersion indexVersion = randomVersion(); return DiscoveryNodeUtils.builder(nodeId) .roles(emptySet()) .version(nodeVersion, IndexVersion.fromId(indexVersion.id() - 1_000_000), indexVersion) @@ -561,7 +562,7 @@ public IndexMetadata randomCreate(String name) { IndexMetadata.Builder builder = IndexMetadata.builder(name); Settings.Builder settingsBuilder = Settings.builder(); setRandomIndexSettings(random(), settingsBuilder); - settingsBuilder.put(randomSettings(Settings.EMPTY)).put(IndexMetadata.SETTING_VERSION_CREATED, randomVersion(random())); + settingsBuilder.put(randomSettings(Settings.EMPTY)).put(IndexMetadata.SETTING_VERSION_CREATED, randomWriteVersion()); builder.settings(settingsBuilder); builder.numberOfShards(randomIntBetween(1, 10)).numberOfReplicas(randomInt(10)); builder.eventIngestedRange(IndexLongFieldRange.UNKNOWN, TransportVersion.current()); @@ -736,7 +737,7 @@ public ClusterState.Custom randomCreate(String name) { ImmutableOpenMap.of(), null, SnapshotInfoTestUtils.randomUserMetadata(), - randomVersion(random()) + randomVersion() ) ); case 1 -> new RestoreInProgress.Builder().add( diff --git a/server/src/internalClusterTest/java/org/elasticsearch/indices/analysis/PreBuiltAnalyzerIntegrationIT.java b/server/src/internalClusterTest/java/org/elasticsearch/indices/analysis/PreBuiltAnalyzerIntegrationIT.java index 48aef0d348045..891b0319f880d 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/indices/analysis/PreBuiltAnalyzerIntegrationIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/indices/analysis/PreBuiltAnalyzerIntegrationIT.java @@ -47,7 +47,7 @@ public void testThatPreBuiltAnalyzersAreNotClosedOnIndexClose() throws Exception PreBuiltAnalyzers preBuiltAnalyzer = PreBuiltAnalyzers.values()[randomInt]; String name = preBuiltAnalyzer.name().toLowerCase(Locale.ROOT); - IndexVersion randomVersion = IndexVersionUtils.randomVersion(random()); + IndexVersion randomVersion = IndexVersionUtils.randomWriteVersion(); if (loadedAnalyzers.containsKey(preBuiltAnalyzer) == false) { loadedAnalyzers.put(preBuiltAnalyzer, new ArrayList<>()); } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java b/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java index 7d4269550bb88..fa1348c82d71a 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java @@ -2050,7 +2050,7 @@ public void testPostRecoveryMergeDisabledOnOlderIndices() throws Exception { IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersionBetween( random(), - IndexVersionUtils.getFirstVersion(), + IndexVersionUtils.getLowestWriteCompatibleVersion(), IndexVersionUtils.getPreviousVersion(IndexVersions.MERGE_ON_RECOVERY_VERSION) ) ) diff --git a/server/src/main/java/org/elasticsearch/index/IndexVersions.java b/server/src/main/java/org/elasticsearch/index/IndexVersions.java index fd321f6256194..8af10524813cc 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexVersions.java +++ b/server/src/main/java/org/elasticsearch/index/IndexVersions.java @@ -12,7 +12,6 @@ import org.apache.lucene.util.Version; import org.elasticsearch.ReleaseVersions; import org.elasticsearch.core.Assertions; -import org.elasticsearch.core.UpdateForV9; import java.lang.reflect.Field; import java.text.ParseException; @@ -25,6 +24,7 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.function.IntFunction; +import java.util.stream.Collectors; @SuppressWarnings("deprecation") public class IndexVersions { @@ -58,7 +58,6 @@ private static Version parseUnchecked(String version) { } } - @UpdateForV9(owner = UpdateForV9.Owner.SEARCH_FOUNDATIONS) // remove the index versions with which v9 will not need to interact public static final IndexVersion ZERO = def(0, Version.LATEST); public static final IndexVersion V_7_0_0 = def(7_00_00_99, parseUnchecked("8.0.0")); @@ -244,10 +243,12 @@ static NavigableMap getAllVersionIds(Class cls) { return Collections.unmodifiableNavigableMap(builder); } - @UpdateForV9(owner = UpdateForV9.Owner.CORE_INFRA) - // We can simplify this once we've removed all references to index versions earlier than MINIMUM_COMPATIBLE + static Collection getAllWriteVersions() { + return VERSION_IDS.values().stream().filter(v -> v.onOrAfter(IndexVersions.MINIMUM_COMPATIBLE)).collect(Collectors.toSet()); + } + static Collection getAllVersions() { - return VERSION_IDS.values().stream().filter(v -> v.onOrAfter(MINIMUM_COMPATIBLE)).toList(); + return VERSION_IDS.values(); } static final IntFunction VERSION_LOOKUP = ReleaseVersions.generateVersionsLookup(IndexVersions.class, LATEST_DEFINED.id()); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/HumanReadableIndexSettingsTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/HumanReadableIndexSettingsTests.java index 476ade8576586..8752e68112bff 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/HumanReadableIndexSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/HumanReadableIndexSettingsTests.java @@ -22,7 +22,7 @@ public class HumanReadableIndexSettingsTests extends ESTestCase { public void testHumanReadableSettings() { - IndexVersion versionCreated = randomVersion(random()); + IndexVersion versionCreated = randomVersion(); long created = System.currentTimeMillis(); Settings testSettings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, versionCreated) diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java index c0e397c9fb9c9..d45b2c119d2ae 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java @@ -369,11 +369,7 @@ public void testValidateSplitIndex() { } public void testPrepareResizeIndexSettings() { - final List versions = Stream.of(IndexVersionUtils.randomVersion(random()), IndexVersionUtils.randomVersion(random())) - .sorted() - .toList(); - final IndexVersion version = versions.get(0); - final IndexVersion upgraded = versions.get(1); + final IndexVersion version = IndexVersionUtils.randomWriteVersion(); final Settings.Builder indexSettingsBuilder = Settings.builder() .put("index.version.created", version) .put("index.similarity.default.type", "BM25") diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataDeleteIndexServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataDeleteIndexServiceTests.java index 0354b6f0bcea8..3ada92dbe7ae5 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataDeleteIndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataDeleteIndexServiceTests.java @@ -97,7 +97,7 @@ public void testDeleteSnapshotting() { Map.of(), null, SnapshotInfoTestUtils.randomUserMetadata(), - IndexVersionUtils.randomVersion(random()) + IndexVersionUtils.randomVersion() ) ); ClusterState state = ClusterState.builder(clusterState(index)).putCustom(SnapshotsInProgress.TYPE, snaps).build(); @@ -153,7 +153,7 @@ public void testDeleteIndexWithAnAlias() { String alias = randomAlphaOfLength(5); IndexMetadata idxMetadata = IndexMetadata.builder(index) - .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random()))) + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion())) .putAlias(AliasMetadata.builder(alias).writeIndex(true).build()) .numberOfShards(1) .numberOfReplicas(1) @@ -348,7 +348,7 @@ public void testDeleteCurrentWriteFailureIndexForDataStream() { private ClusterState clusterState(String index) { IndexMetadata indexMetadata = IndexMetadata.builder(index) - .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random()))) + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion())) .numberOfShards(1) .numberOfReplicas(1) .build(); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexAliasesServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexAliasesServiceTests.java index 4f2c84d76b5a4..22ddb5cc2ba35 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexAliasesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexAliasesServiceTests.java @@ -744,7 +744,7 @@ private ClusterState applyHiddenAliasMix(ClusterState before, Boolean isHidden1, private ClusterState createIndex(ClusterState state, String index) { IndexMetadata indexMetadata = IndexMetadata.builder(index) - .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random()))) + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomWriteVersion())) .numberOfShards(1) .numberOfReplicas(1) .build(); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexStateServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexStateServiceTests.java index db5a98a4878ca..bd11e636d51c1 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexStateServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexStateServiceTests.java @@ -377,7 +377,7 @@ private static ClusterState addSnapshotIndex(final String index, final int numSh shardsBuilder, null, SnapshotInfoTestUtils.randomUserMetadata(), - IndexVersionUtils.randomVersion(random()) + IndexVersionUtils.randomVersion() ); return ClusterState.builder(newState).putCustom(SnapshotsInProgress.TYPE, SnapshotsInProgress.EMPTY.withAddedEntry(entry)).build(); } diff --git a/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java b/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java index 22308e15f4845..eccdd1c6ffea7 100644 --- a/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java @@ -43,7 +43,7 @@ private BuildVersion randomBuildVersion() { } private IndexVersion randomIndexVersion() { - return rarely() ? IndexVersion.fromId(randomInt()) : IndexVersionUtils.randomVersion(random()); + return rarely() ? IndexVersion.fromId(randomInt()) : IndexVersionUtils.randomVersion(); } public void testEqualsHashcodeSerialization() { diff --git a/server/src/test/java/org/elasticsearch/index/IndexVersionTests.java b/server/src/test/java/org/elasticsearch/index/IndexVersionTests.java index 2a425c9256c31..8575b87c36799 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexVersionTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexVersionTests.java @@ -11,7 +11,6 @@ import org.apache.lucene.util.Version; import org.elasticsearch.common.lucene.Lucene; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.index.IndexVersionUtils; import org.hamcrest.Matchers; @@ -151,9 +150,7 @@ public void testMax() { } } - @UpdateForV9(owner = UpdateForV9.Owner.CORE_INFRA) - @AwaitsFix(bugUrl = "believe this fails because index version has not yet been bumped to 9.0") - public void testMinimumCompatibleVersion() { + public void testGetMinimumCompatibleIndexVersion() { assertThat(IndexVersion.getMinimumCompatibleIndexVersion(7170099), equalTo(IndexVersion.fromId(6000099))); assertThat(IndexVersion.getMinimumCompatibleIndexVersion(8000099), equalTo(IndexVersion.fromId(7000099))); assertThat(IndexVersion.getMinimumCompatibleIndexVersion(10000000), equalTo(IndexVersion.fromId(9000000))); @@ -193,8 +190,6 @@ public void testParseLenient() { } } - @UpdateForV9(owner = UpdateForV9.Owner.CORE_INFRA) - @AwaitsFix(bugUrl = "can be unmuted once lucene is bumped to version 10") public void testLuceneVersionOnUnknownVersions() { // between two known versions, should use the lucene version of the previous version IndexVersion previousVersion = IndexVersionUtils.getPreviousVersion(); @@ -207,7 +202,7 @@ public void testLuceneVersionOnUnknownVersions() { // too old version, major should be the oldest supported lucene version minus 1 IndexVersion oldVersion = IndexVersion.fromId(5020199); - assertThat(oldVersion.luceneVersion().major, equalTo(IndexVersionUtils.getFirstVersion().luceneVersion().major - 1)); + assertThat(oldVersion.luceneVersion().major, equalTo(IndexVersionUtils.getLowestReadCompatibleVersion().luceneVersion().major - 1)); // future version, should be the same version as today IndexVersion futureVersion = IndexVersion.fromId(currentVersion.id() + 100); diff --git a/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java b/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java index 04170030c1173..db780f0640986 100644 --- a/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java +++ b/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java @@ -106,7 +106,7 @@ public void setUp() throws Exception { } public void testDefaultAnalyzers() throws IOException { - IndexVersion version = IndexVersionUtils.randomVersion(random()); + IndexVersion version = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, version) .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()) @@ -120,7 +120,7 @@ public void testDefaultAnalyzers() throws IOException { } public void testOverrideDefaultAnalyzer() throws IOException { - IndexVersion version = IndexVersionUtils.randomVersion(random()); + IndexVersion version = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); IndexAnalyzers indexAnalyzers = AnalysisRegistry.build( IndexCreationContext.CREATE_INDEX, @@ -137,7 +137,7 @@ public void testOverrideDefaultAnalyzer() throws IOException { } public void testOverrideDefaultAnalyzerWithoutAnalysisModeAll() throws IOException { - IndexVersion version = IndexVersionUtils.randomVersion(random()); + IndexVersion version = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); IndexSettings indexSettings = IndexSettingsModule.newIndexSettings("index", settings); TokenFilterFactory tokenFilter = new AbstractTokenFilterFactory("my_filter") { @@ -216,7 +216,7 @@ public void testOverrideDefaultIndexAnalyzerIsUnsupported() { } public void testOverrideDefaultSearchAnalyzer() { - IndexVersion version = IndexVersionUtils.randomVersion(random()); + IndexVersion version = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); IndexAnalyzers indexAnalyzers = AnalysisRegistry.build( IndexCreationContext.CREATE_INDEX, @@ -319,8 +319,8 @@ public void testBuiltInAnalyzersAreCached() throws IOException { } } - public void testNoTypeOrTokenizerErrorMessage() throws IOException { - IndexVersion version = IndexVersionUtils.randomVersion(random()); + public void testNoTypeOrTokenizerErrorMessage() { + IndexVersion version = IndexVersionUtils.randomVersion(); Settings settings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, version) .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()) diff --git a/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java b/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java index 0a7bd495f2f22..f5b86f422915e 100644 --- a/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java +++ b/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java @@ -63,7 +63,7 @@ public void testThatInstancesAreCachedAndReused() { PreBuiltAnalyzers.STANDARD.getAnalyzer(IndexVersion.current()) ); // same index version should be cached - IndexVersion v = IndexVersionUtils.randomVersion(random()); + IndexVersion v = IndexVersionUtils.randomVersion(); assertSame(PreBuiltAnalyzers.STANDARD.getAnalyzer(v), PreBuiltAnalyzers.STANDARD.getAnalyzer(v)); assertNotSame( PreBuiltAnalyzers.STANDARD.getAnalyzer(IndexVersion.current()), @@ -71,7 +71,7 @@ public void testThatInstancesAreCachedAndReused() { ); // Same Lucene version should be cached: - IndexVersion v1 = IndexVersionUtils.randomVersion(random()); + IndexVersion v1 = IndexVersionUtils.randomVersion(); IndexVersion v2 = new IndexVersion(v1.id() - 1, v1.luceneVersion()); assertSame(PreBuiltAnalyzers.STOP.getAnalyzer(v1), PreBuiltAnalyzers.STOP.getAnalyzer(v2)); } @@ -81,7 +81,7 @@ public void testThatAnalyzersAreUsedInMapping() throws IOException { PreBuiltAnalyzers randomPreBuiltAnalyzer = PreBuiltAnalyzers.values()[randomInt]; String analyzerName = randomPreBuiltAnalyzer.name().toLowerCase(Locale.ROOT); - IndexVersion randomVersion = IndexVersionUtils.randomVersion(random()); + IndexVersion randomVersion = IndexVersionUtils.randomWriteVersion(); Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, randomVersion).build(); NamedAnalyzer namedAnalyzer = new PreBuiltAnalyzerProvider( diff --git a/server/src/test/java/org/elasticsearch/index/analysis/PreConfiguredTokenFilterTests.java b/server/src/test/java/org/elasticsearch/index/analysis/PreConfiguredTokenFilterTests.java index 40b37452990c7..a1a91ef2373f3 100644 --- a/server/src/test/java/org/elasticsearch/index/analysis/PreConfiguredTokenFilterTests.java +++ b/server/src/test/java/org/elasticsearch/index/analysis/PreConfiguredTokenFilterTests.java @@ -41,7 +41,7 @@ public boolean incrementToken() { IndexSettings indexSettings = IndexSettingsModule.newIndexSettings("test", Settings.EMPTY); - IndexVersion version1 = IndexVersionUtils.randomVersion(random()); + IndexVersion version1 = IndexVersionUtils.randomVersion(); Settings settings1 = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version1).build(); TokenFilterFactory tff_v1_1 = pctf.get(indexSettings, TestEnvironment.newEnvironment(emptyNodeSettings), "singleton", settings1); TokenFilterFactory tff_v1_2 = pctf.get(indexSettings, TestEnvironment.newEnvironment(emptyNodeSettings), "singleton", settings1); @@ -66,7 +66,7 @@ public boolean incrementToken() { } ); - IndexVersion version1 = IndexVersionUtils.randomVersion(random()); + IndexVersion version1 = IndexVersionUtils.randomVersion(); IndexSettings indexSettings1 = IndexSettingsModule.newIndexSettings( "test", Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version1).build() @@ -133,7 +133,7 @@ public boolean incrementToken() { ); assertSame(tff_v1_1, tff_v1_2); - IndexVersion version2 = IndexVersionUtils.getPreviousMajorVersion(IndexVersionUtils.getFirstVersion()); + IndexVersion version2 = IndexVersionUtils.getPreviousMajorVersion(IndexVersionUtils.getLowestReadCompatibleVersion()); IndexSettings indexSettings2 = IndexSettingsModule.newIndexSettings( "test", Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version2).build() diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 3e3be6a315af2..d07c775da7e2a 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -6645,7 +6645,7 @@ public void testStoreHonorsLuceneVersion() throws IOException { for (IndexVersion createdVersion : List.of( IndexVersion.current(), lowestCompatiblePreviousVersion, - IndexVersionUtils.getFirstVersion() + IndexVersionUtils.getLowestWriteCompatibleVersion() )) { Settings settings = Settings.builder().put(indexSettings()).put(IndexMetadata.SETTING_VERSION_CREATED, createdVersion).build(); IndexSettings indexSettings = IndexSettingsModule.newIndexSettings("test", settings); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java index 342d61b78defd..3f574a29469c2 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java @@ -1514,19 +1514,19 @@ public void testVectorSimilarity() { ); assertEquals( VectorSimilarityFunction.EUCLIDEAN, - VectorSimilarity.L2_NORM.vectorSimilarityFunction(IndexVersionUtils.randomVersion(random()), ElementType.BYTE) + VectorSimilarity.L2_NORM.vectorSimilarityFunction(IndexVersionUtils.randomVersion(), ElementType.BYTE) ); assertEquals( VectorSimilarityFunction.EUCLIDEAN, - VectorSimilarity.L2_NORM.vectorSimilarityFunction(IndexVersionUtils.randomVersion(random()), ElementType.FLOAT) + VectorSimilarity.L2_NORM.vectorSimilarityFunction(IndexVersionUtils.randomVersion(), ElementType.FLOAT) ); assertEquals( VectorSimilarityFunction.DOT_PRODUCT, - VectorSimilarity.DOT_PRODUCT.vectorSimilarityFunction(IndexVersionUtils.randomVersion(random()), ElementType.BYTE) + VectorSimilarity.DOT_PRODUCT.vectorSimilarityFunction(IndexVersionUtils.randomVersion(), ElementType.BYTE) ); assertEquals( VectorSimilarityFunction.DOT_PRODUCT, - VectorSimilarity.DOT_PRODUCT.vectorSimilarityFunction(IndexVersionUtils.randomVersion(random()), ElementType.FLOAT) + VectorSimilarity.DOT_PRODUCT.vectorSimilarityFunction(IndexVersionUtils.randomVersion(), ElementType.FLOAT) ); } diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java index cf6941b84b791..1bcd84aadd6cd 100644 --- a/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java +++ b/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java @@ -233,7 +233,7 @@ public Map> getTokenizers() { new StablePluginsRegistry() ).getAnalysisRegistry(); - IndexVersion version = IndexVersionUtils.randomVersion(random()); + IndexVersion version = IndexVersionUtils.randomVersion(); IndexAnalyzers analyzers = getIndexAnalyzers( registry, Settings.builder() @@ -302,7 +302,7 @@ public List getPreConfiguredTokenFilters() { new StablePluginsRegistry() ).getAnalysisRegistry(); - IndexVersion version = IndexVersionUtils.randomVersion(random()); + IndexVersion version = IndexVersionUtils.randomVersion(); IndexAnalyzers analyzers = getIndexAnalyzers( registry, Settings.builder() @@ -389,7 +389,7 @@ public List getPreConfiguredTokenizers() { new StablePluginsRegistry() ).getAnalysisRegistry(); - IndexVersion version = IndexVersionUtils.randomVersion(random()); + IndexVersion version = IndexVersionUtils.randomVersion(); IndexAnalyzers analyzers = getIndexAnalyzers( registry, Settings.builder() diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/IncorrectSetupStablePluginsTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/IncorrectSetupStablePluginsTests.java index ca9184bca75da..181d3ec44f2b3 100644 --- a/server/src/test/java/org/elasticsearch/indices/analysis/IncorrectSetupStablePluginsTests.java +++ b/server/src/test/java/org/elasticsearch/indices/analysis/IncorrectSetupStablePluginsTests.java @@ -63,7 +63,7 @@ public void testIncorrectlyAnnotatedSettingsClass() throws IOException { Settings.builder() .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") .put("index.analysis.analyzer.char_filter_test.char_filter", "incorrectlyAnnotatedSettings") - .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random())) + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion()) .build(), Map.of( "incorrectlyAnnotatedSettings", @@ -90,7 +90,7 @@ public void testIncorrectlyAnnotatedConstructor() throws IOException { Settings.builder() .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") .put("index.analysis.analyzer.char_filter_test.char_filter", "noInjectCharFilter") - .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random())) + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion()) .build(), Map.of("noInjectCharFilter", new PluginInfo("noInjectCharFilter", NoInjectCharFilter.class.getName(), classLoader)) ) @@ -112,7 +112,7 @@ public void testMultiplePublicConstructors() throws IOException { Settings.builder() .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") .put("index.analysis.analyzer.char_filter_test.char_filter", "multipleConstructors") - .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random())) + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion()) .build(), Map.of("multipleConstructors", new PluginInfo("multipleConstructors", MultipleConstructors.class.getName(), classLoader)) ) diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsNoSettingsTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsNoSettingsTests.java index 7cbda0e7086cb..6eac3847efa43 100644 --- a/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsNoSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsNoSettingsTests.java @@ -61,7 +61,7 @@ public IndexAnalyzers getIndexAnalyzers(Settings settings) throws IOException { } public void testStablePlugins() throws IOException { - IndexVersion version = IndexVersionUtils.randomVersion(random()); + IndexVersion version = IndexVersionUtils.randomVersion(); IndexAnalyzers analyzers = getIndexAnalyzers( Settings.builder() .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsWithSettingsTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsWithSettingsTests.java index acde315b140ab..82f49888e911d 100644 --- a/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsWithSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsWithSettingsTests.java @@ -72,7 +72,7 @@ public void testCharFilters() throws IOException { .put("index.analysis.analyzer.char_filter_with_defaults_test.tokenizer", "standard") .put("index.analysis.analyzer.char_filter_with_defaults_test.char_filter", "stableCharFilterFactory") - .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random())) + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion()) .build() ); assertTokenStreamContents(analyzers.get("char_filter_test").tokenStream("", "t#st"), new String[] { "t3st" }); @@ -88,7 +88,7 @@ public void testTokenFilters() throws IOException { .put("index.analysis.analyzer.token_filter_test.tokenizer", "standard") .put("index.analysis.analyzer.token_filter_test.filter", "my_token_filter") - .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random())) + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion()) .build() ); assertTokenStreamContents( @@ -109,7 +109,7 @@ public void testTokenizer() throws IOException { .putList("index.analysis.tokenizer.my_tokenizer.tokenizer_list_of_chars", "_", " ") .put("index.analysis.analyzer.tokenizer_test.tokenizer", "my_tokenizer") - .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random())) + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion()) .build() ); assertTokenStreamContents(analyzers.get("tokenizer_test").tokenStream("", "x_y z"), new String[] { "x", "y", "z" }); @@ -124,7 +124,7 @@ public void testAnalyzer() throws IOException { .put("index.analysis.analyzer.analyzer_provider_test.old_char", "#") .put("index.analysis.analyzer.analyzer_provider_test.new_number", 3) .put("index.analysis.analyzer.analyzer_provider_test.analyzerUseTokenListOfChars", true) - .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random())) + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion()) .build() ); assertTokenStreamContents(analyzers.get("analyzer_provider_test").tokenStream("", "1x_y_#z"), new String[] { "y", "3z" }); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java index d039c265c98ae..d9b2936dc30c0 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java @@ -721,7 +721,7 @@ public void testThrowExceptionOnPrimaryRelocatedBeforePhase1Started() throws IOE final IndexMetadata.Builder indexMetadata = IndexMetadata.builder("test") .settings( - indexSettings(IndexVersionUtils.randomVersion(random()), between(1, 5), between(0, 5)).put( + indexSettings(IndexVersionUtils.randomVersion(), between(1, 5), between(0, 5)).put( IndexMetadata.SETTING_INDEX_UUID, UUIDs.randomBase64UUID(random()) ) diff --git a/server/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java b/server/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java index 33801dfb98417..ace8499d8ffd0 100644 --- a/server/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java +++ b/server/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java @@ -243,7 +243,7 @@ private static NodeInfo createNodeInfo() { return new NodeInfo( randomAlphaOfLengthBetween(6, 32), new CompatibilityVersions(TransportVersionUtils.randomVersion(random()), Map.of()), - IndexVersionUtils.randomVersion(random()), + IndexVersionUtils.randomVersion(), componentVersions, build, node, diff --git a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksClusterServiceTests.java b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksClusterServiceTests.java index c568f6a38a5fb..cd2327d90c5c5 100644 --- a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksClusterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksClusterServiceTests.java @@ -966,7 +966,7 @@ private ClusterState insignificantChange(ClusterState clusterState) { } // Just add a random index - that shouldn't change anything IndexMetadata indexMetadata = IndexMetadata.builder(randomAlphaOfLength(10)) - .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random()))) + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion())) .numberOfShards(1) .numberOfReplicas(1) .build(); @@ -1044,7 +1044,7 @@ private ClusterState initialState() { private void changeRoutingTable(Metadata.Builder metadata, RoutingTable.Builder routingTable) { IndexMetadata indexMetadata = IndexMetadata.builder(randomAlphaOfLength(10)) - .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random()))) + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion())) .numberOfShards(1) .numberOfReplicas(1) .build(); diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotsInProgressSerializationTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotsInProgressSerializationTests.java index a92d55f6d419c..91ab253b2e1fe 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotsInProgressSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotsInProgressSerializationTests.java @@ -133,7 +133,7 @@ private Entry randomSnapshot() { shards, null, SnapshotInfoTestUtils.randomUserMetadata(), - IndexVersionUtils.randomVersion(random()) + IndexVersionUtils.randomVersion() ); } diff --git a/test/framework/src/main/java/org/elasticsearch/index/KnownIndexVersions.java b/test/framework/src/main/java/org/elasticsearch/index/KnownIndexVersions.java index 47d239540814e..5cdb3f1808a38 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/KnownIndexVersions.java +++ b/test/framework/src/main/java/org/elasticsearch/index/KnownIndexVersions.java @@ -16,7 +16,11 @@ */ public class KnownIndexVersions { /** - * A sorted list of all known transport versions + * A sorted list of all known index versions */ public static final List ALL_VERSIONS = List.copyOf(IndexVersions.getAllVersions()); + /** + * A sorted list of all known index versions that can be written to + */ + public static final List ALL_WRITE_VERSIONS = List.copyOf(IndexVersions.getAllWriteVersions()); } diff --git a/test/framework/src/main/java/org/elasticsearch/test/index/IndexVersionUtils.java b/test/framework/src/main/java/org/elasticsearch/test/index/IndexVersionUtils.java index f83e7e17f9aaa..592cffac33552 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/index/IndexVersionUtils.java +++ b/test/framework/src/main/java/org/elasticsearch/test/index/IndexVersionUtils.java @@ -24,32 +24,38 @@ public class IndexVersionUtils { private static final List ALL_VERSIONS = KnownIndexVersions.ALL_VERSIONS; + private static final List ALL_WRITE_VERSIONS = KnownIndexVersions.ALL_WRITE_VERSIONS; /** Returns all released versions */ public static List allReleasedVersions() { return ALL_VERSIONS; } - /** Returns the oldest known {@link IndexVersion} */ - public static IndexVersion getFirstVersion() { + /** Returns the oldest known {@link IndexVersion}. This version can only be read from and not written to */ + public static IndexVersion getLowestReadCompatibleVersion() { return ALL_VERSIONS.get(0); } + /** Returns the oldest known {@link IndexVersion} that can be written to */ + public static IndexVersion getLowestWriteCompatibleVersion() { + return ALL_WRITE_VERSIONS.get(0); + } + /** Returns a random {@link IndexVersion} from all available versions. */ public static IndexVersion randomVersion() { return ESTestCase.randomFrom(ALL_VERSIONS); } + /** Returns a random {@link IndexVersion} from all versions that can be written to. */ + public static IndexVersion randomWriteVersion() { + return ESTestCase.randomFrom(ALL_WRITE_VERSIONS); + } + /** Returns a random {@link IndexVersion} from all available versions without the ignore set */ public static IndexVersion randomVersion(Set ignore) { return ESTestCase.randomFrom(ALL_VERSIONS.stream().filter(v -> ignore.contains(v) == false).collect(Collectors.toList())); } - /** Returns a random {@link IndexVersion} from all available versions. */ - public static IndexVersion randomVersion(Random random) { - return ALL_VERSIONS.get(random.nextInt(ALL_VERSIONS.size())); - } - /** Returns a random {@link IndexVersion} between minVersion and maxVersion (inclusive). */ public static IndexVersion randomVersionBetween(Random random, @Nullable IndexVersion minVersion, @Nullable IndexVersion maxVersion) { if (minVersion != null && maxVersion != null && maxVersion.before(minVersion)) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumerTests.java index ee91b8b65e540..c572b3a35d5a4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumerTests.java @@ -342,7 +342,7 @@ public void testMultipleIndicesSameAlias() throws Exception { private void addIndex(Metadata.Builder metadataBuilder, String index, String... aliases) { final IndexMetadata.Builder indexMetadataBuilder = IndexMetadata.builder(index) - .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion(random()))) + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomVersion())) .numberOfShards(1) .numberOfReplicas(1); for (final String alias : aliases) { From 54fa07450a6aeddf5cfc4c210cfe9241cb8e83c0 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:54:17 +0100 Subject: [PATCH 070/119] [DOCS] Make Wolfi hardened Docker option more prominent (#118755) --- docs/reference/setup/install/docker.asciidoc | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/reference/setup/install/docker.asciidoc b/docs/reference/setup/install/docker.asciidoc index 86a0e567f6eec..f3576db0c786c 100644 --- a/docs/reference/setup/install/docker.asciidoc +++ b/docs/reference/setup/install/docker.asciidoc @@ -25,6 +25,21 @@ TIP: This setup doesn't run multiple {es} nodes or {kib} by default. To create a multi-node cluster with {kib}, use Docker Compose instead. See <>. +[[docker-wolfi-hardened-image]] +===== Hardened Docker images + +You can also use the hardened https://wolfi.dev/[Wolfi] image for additional security. +Using Wolfi images requires Docker version 20.10.10 or higher. + +To use the Wolfi image, append `-wolfi` to the image tag in the Docker command. + +For example: + +[source,sh,subs="attributes"] +---- +docker pull {docker-wolfi-image} +---- + ===== Start a single-node cluster . Install Docker. Visit https://docs.docker.com/get-docker/[Get Docker] to @@ -55,12 +70,6 @@ docker pull {docker-image} // REVIEWED[DEC.10.24] -- -Alternatevely, you can use the Wolfi based image. Using Wolfi based images requires Docker version 20.10.10 or superior. -[source,sh,subs="attributes"] ----- -docker pull {docker-wolfi-image} ----- - . Optional: Install https://docs.sigstore.dev/cosign/system_config/installation/[Cosign] for your environment. Then use Cosign to verify the {es} image's signature. From dbbcb09244cd65b52c13f78e500deb5fdcd3edd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 18 Dec 2024 14:19:38 +0100 Subject: [PATCH 071/119] ESQL: Verify LOOKUP JOIN index has lookup mode (#118660) Verifies that LOOKUP JOIN indices resolve to a single concrete index with LOOKUP mode. --- .../esql/qa/mixed/MixedClusterEsqlSpecIT.java | 4 +- .../xpack/esql/ccq/MultiClusterSpecIT.java | 8 +- .../rest/RequestIndexFilteringTestCase.java | 2 +- .../src/main/resources/lookup-join.csv-spec | 128 +++++++++--------- .../xpack/esql/action/EsqlCapabilities.java | 4 +- .../xpack/esql/analysis/Analyzer.java | 31 +++++ .../xpack/esql/analysis/Verifier.java | 1 - .../elasticsearch/xpack/esql/CsvTests.java | 2 +- .../esql/analysis/AnalyzerTestUtils.java | 19 ++- .../xpack/esql/analysis/AnalyzerTests.java | 33 ++++- .../xpack/esql/analysis/ParsingTests.java | 8 +- .../xpack/esql/analysis/VerifierTests.java | 8 +- .../optimizer/LogicalPlanOptimizerTests.java | 12 +- .../optimizer/PhysicalPlanOptimizerTests.java | 2 +- .../session/IndexResolverFieldNamesTests.java | 24 ++-- .../test/esql/190_lookup_join.yml | 78 +++++++++++ 16 files changed, 261 insertions(+), 103 deletions(-) create mode 100644 x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/190_lookup_join.yml diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java index 004beaafb4009..d4b087277df52 100644 --- a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java @@ -21,7 +21,7 @@ import java.util.List; import static org.elasticsearch.xpack.esql.CsvTestUtils.isEnabled; -import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V7; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V8; import static org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase.Mode.ASYNC; public class MixedClusterEsqlSpecIT extends EsqlSpecTestCase { @@ -96,7 +96,7 @@ protected boolean supportsInferenceTestService() { @Override protected boolean supportsIndexModeLookup() throws IOException { - return hasCapabilities(List.of(JOIN_LOOKUP_V7.capabilityName())); + return hasCapabilities(List.of(JOIN_LOOKUP_V8.capabilityName())); } @Override diff --git a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java index c75a920e16973..d7c57e23b7147 100644 --- a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java +++ b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java @@ -48,7 +48,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.classpathResources; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS_V2; -import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V7; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V8; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_PLANNING_V1; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.METADATA_FIELDS_REMOTE_TEST; import static org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase.Mode.SYNC; @@ -124,7 +124,7 @@ protected void shouldSkipTest(String testName) throws IOException { assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS.capabilityName())); assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS_V2.capabilityName())); assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_PLANNING_V1.capabilityName())); - assumeFalse("LOOKUP JOIN not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_LOOKUP_V7.capabilityName())); + assumeFalse("LOOKUP JOIN not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_LOOKUP_V8.capabilityName())); } private TestFeatureService remoteFeaturesService() throws IOException { @@ -283,8 +283,8 @@ protected boolean supportsInferenceTestService() { @Override protected boolean supportsIndexModeLookup() throws IOException { - // CCS does not yet support JOIN_LOOKUP_V7 and clusters falsely report they have this capability - // return hasCapabilities(List.of(JOIN_LOOKUP_V7.capabilityName())); + // CCS does not yet support JOIN_LOOKUP_V8 and clusters falsely report they have this capability + // return hasCapabilities(List.of(JOIN_LOOKUP_V8.capabilityName())); return false; } } diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java index 40027249670f6..355c403ce2a86 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java @@ -221,7 +221,7 @@ public void testIndicesDontExist() throws IOException { assertThat(e.getMessage(), containsString("index_not_found_exception")); assertThat(e.getMessage(), containsString("no such index [foo]")); - if (EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()) { + if (EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()) { e = expectThrows( ResponseException.class, () -> runEsql(timestampFilter("gte", "2020-01-01").query("FROM test1 | LOOKUP JOIN foo ON id1")) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec index e75c68f4a379d..7d4f89ed920a9 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec @@ -8,7 +8,7 @@ ############################################### basicOnTheDataNode -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | EVAL language_code = languages @@ -25,7 +25,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; basicRow -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW language_code = 1 | LOOKUP JOIN languages_lookup ON language_code @@ -36,7 +36,7 @@ language_code:integer | language_name:keyword ; basicOnTheCoordinator -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | SORT emp_no @@ -53,7 +53,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; subsequentEvalOnTheDataNode -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | EVAL language_code = languages @@ -71,7 +71,7 @@ emp_no:integer | language_code:integer | language_name:keyword | language_code_x ; subsequentEvalOnTheCoordinator -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | SORT emp_no @@ -89,7 +89,7 @@ emp_no:integer | language_code:integer | language_name:keyword | language_code_x ; sortEvalBeforeLookup -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | SORT emp_no @@ -106,7 +106,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; nonUniqueLeftKeyOnTheDataNode -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | WHERE emp_no <= 10030 @@ -130,7 +130,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; nonUniqueRightKeyOnTheDataNode -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | EVAL language_code = emp_no % 10 @@ -150,7 +150,7 @@ emp_no:integer | language_code:integer | language_name:keyword | country:k ; nonUniqueRightKeyOnTheCoordinator -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | SORT emp_no @@ -170,7 +170,7 @@ emp_no:integer | language_code:integer | language_name:keyword | country:k ; nonUniqueRightKeyFromRow -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW language_code = 2 | LOOKUP JOIN languages_lookup_non_unique_key ON language_code @@ -183,7 +183,7 @@ language_code:integer | language_name:keyword | country:keyword ; repeatedIndexOnFrom -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 required_capability: join_lookup_repeated_index_from FROM languages_lookup @@ -203,7 +203,7 @@ language_code:integer | language_name:keyword ############################################### filterOnLeftSide -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | EVAL language_code = languages @@ -220,7 +220,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; filterOnRightSide -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -236,7 +236,7 @@ FROM sample_data ; filterOnRightSideAfterStats -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -249,7 +249,7 @@ count:long | type:keyword ; filterOnJoinKey -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | EVAL language_code = languages @@ -264,7 +264,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; filterOnJoinKeyAndRightSide -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | WHERE emp_no < 10006 @@ -281,7 +281,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; filterOnRightSideOnTheCoordinator -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | SORT emp_no @@ -297,7 +297,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; filterOnJoinKeyOnTheCoordinator -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | SORT emp_no @@ -313,7 +313,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; filterOnJoinKeyAndRightSideOnTheCoordinator -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | SORT emp_no @@ -330,7 +330,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; filterOnTheDataNodeThenFilterOnTheCoordinator -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | EVAL language_code = languages @@ -351,7 +351,7 @@ emp_no:integer | language_code:integer | language_name:keyword ########################################################################### nullJoinKeyOnTheDataNode -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | WHERE emp_no < 10004 @@ -368,7 +368,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; mvJoinKeyOnTheDataNode -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM employees | WHERE 10003 < emp_no AND emp_no < 10008 @@ -386,7 +386,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; mvJoinKeyFromRow -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW language_code = [4, 5, 6, 7] | LOOKUP JOIN languages_lookup_non_unique_key ON language_code @@ -399,7 +399,7 @@ language_code:integer | language_name:keyword | country:keyword ; mvJoinKeyFromRowExpanded -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW language_code = [4, 5, 6, 7, 8] | MV_EXPAND language_code @@ -421,7 +421,7 @@ language_code:integer | language_name:keyword | country:keyword ############################################### lookupIPFromRow -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -432,7 +432,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromKeepRow -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", right = "right" | KEEP left, client_ip, right @@ -444,7 +444,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowing -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -455,7 +455,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowingKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -468,7 +468,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowingKeepReordered -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -481,7 +481,7 @@ right | Development | 172.21.0.5 ; lookupIPFromIndex -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -500,7 +500,7 @@ ignoreOrder:true ; lookupIPFromIndexKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -520,7 +520,7 @@ ignoreOrder:true ; lookupIPFromIndexKeepKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | KEEP client_ip, event_duration, @timestamp, message @@ -542,7 +542,7 @@ timestamp:date | client_ip:keyword | event_duration:long | msg:keyword ; lookupIPFromIndexStats -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -558,7 +558,7 @@ count:long | env:keyword ; lookupIPFromIndexStatsKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -575,7 +575,7 @@ count:long | env:keyword ; statsAndLookupIPFromIndex -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -596,7 +596,7 @@ count:long | client_ip:keyword | env:keyword ############################################### lookupMessageFromRow -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", message = "Connected to 10.1.0.1", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -607,7 +607,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromKeepRow -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", message = "Connected to 10.1.0.1", right = "right" | KEEP left, message, right @@ -619,7 +619,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromRowWithShadowing -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", message = "Connected to 10.1.0.1", type = "unknown", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -630,7 +630,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromRowWithShadowingKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", message = "Connected to 10.1.0.1", type = "unknown", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -642,7 +642,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromIndex -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -660,7 +660,7 @@ ignoreOrder:true ; lookupMessageFromIndexKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -679,7 +679,7 @@ ignoreOrder:true ; lookupMessageFromIndexKeepKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | KEEP client_ip, event_duration, @timestamp, message @@ -699,7 +699,7 @@ ignoreOrder:true ; lookupMessageFromIndexKeepReordered -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -718,7 +718,7 @@ Success | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 ; lookupMessageFromIndexStats -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -733,7 +733,7 @@ count:long | type:keyword ; lookupMessageFromIndexStatsKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -749,7 +749,7 @@ count:long | type:keyword ; statsAndLookupMessageFromIndex -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | STATS count = count(message) BY message @@ -767,7 +767,7 @@ count:long | type:keyword | message:keyword ; lookupMessageFromIndexTwice -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -789,7 +789,7 @@ ignoreOrder:true ; lookupMessageFromIndexTwiceKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -816,7 +816,7 @@ ignoreOrder:true ############################################### lookupIPAndMessageFromRow -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -828,7 +828,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowKeepBefore -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" | KEEP left, client_ip, message, right @@ -841,7 +841,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowKeepBetween -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -854,7 +854,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowKeepAfter -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -867,7 +867,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowing -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", type = "type", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -879,7 +879,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowingKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -893,7 +893,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowingKeepKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -908,7 +908,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowingKeepKeepKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -924,7 +924,7 @@ left | 172.21.0.5 | Connected to 10.1.0.1 | right | Devel ; lookupIPAndMessageFromRowWithShadowingKeepReordered -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 ROW left = "left", client_ip = "172.21.0.5", message = "Connected to 10.1.0.1", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -938,7 +938,7 @@ right | Development | Success | 172.21.0.5 ; lookupIPAndMessageFromIndex -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -958,7 +958,7 @@ ignoreOrder:true ; lookupIPAndMessageFromIndexKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -979,7 +979,7 @@ ignoreOrder:true ; lookupIPAndMessageFromIndexStats -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -997,7 +997,7 @@ count:long | env:keyword | type:keyword ; lookupIPAndMessageFromIndexStatsKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -1016,7 +1016,7 @@ count:long | env:keyword | type:keyword ; statsAndLookupIPAndMessageFromIndex -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -1035,7 +1035,7 @@ count:long | client_ip:keyword | message:keyword | env:keyword | type:keyw ; lookupIPAndMessageFromIndexChainedEvalKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -1057,7 +1057,7 @@ ignoreOrder:true ; lookupIPAndMessageFromIndexChainedRenameKeep -required_capability: join_lookup_v7 +required_capability: join_lookup_v8 FROM sample_data | EVAL client_ip = client_ip::keyword diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index bfc2675d59791..a6e0f1d89c364 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -555,12 +555,12 @@ public enum Cap { /** * LOOKUP JOIN */ - JOIN_LOOKUP_V7(Build.current().isSnapshot()), + JOIN_LOOKUP_V8(Build.current().isSnapshot()), /** * LOOKUP JOIN with the same index as the FROM */ - JOIN_LOOKUP_REPEATED_INDEX_FROM(JOIN_LOOKUP_V7.isEnabled()), + JOIN_LOOKUP_REPEATED_INDEX_FROM(JOIN_LOOKUP_V8.isEnabled()), /** * Fix for https://github.com/elastic/elasticsearch/issues/117054 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java index e15731ca79038..ecd0821c626bf 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java @@ -235,6 +235,37 @@ private LogicalPlan resolveIndex(UnresolvedRelation plan, IndexResolution indexR } EsIndex esIndex = indexResolution.get(); + + if (plan.indexMode().equals(IndexMode.LOOKUP)) { + String indexResolutionMessage = null; + + var indexNameWithModes = esIndex.indexNameWithModes(); + if (indexNameWithModes.size() != 1) { + indexResolutionMessage = "invalid [" + + table + + "] resolution in lookup mode to [" + + indexNameWithModes.size() + + "] indices"; + } else if (indexNameWithModes.values().iterator().next() != IndexMode.LOOKUP) { + indexResolutionMessage = "invalid [" + + table + + "] resolution in lookup mode to an index in [" + + indexNameWithModes.values().iterator().next() + + "] mode"; + } + + if (indexResolutionMessage != null) { + return new UnresolvedRelation( + plan.source(), + plan.table(), + plan.frozen(), + plan.metadataFields(), + plan.indexMode(), + indexResolutionMessage, + plan.commandName() + ); + } + } var attributes = mappingAsAttributes(plan.source(), esIndex.mapping()); attributes.addAll(plan.metadataFields()); return new EsRelation(plan.source(), esIndex, attributes.isEmpty() ? NO_FIELDS : attributes, plan.indexMode()); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java index 6b98b7d69834f..93e9d59ed8c6e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java @@ -865,7 +865,6 @@ private static void checkJoin(LogicalPlan plan, Set failures) { ); } } - } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index 70794e4a82130..b81b80a9fdbb4 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -263,7 +263,7 @@ public final void test() throws Throwable { ); assumeFalse( "lookup join disabled for csv tests", - testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.JOIN_LOOKUP_V7.capabilityName()) + testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.JOIN_LOOKUP_V8.capabilityName()) ); assumeFalse( "can't use TERM function in csv tests", diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java index 85dd36ba0aaa5..d4e786a9d9bb0 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java @@ -46,6 +46,10 @@ public static Analyzer analyzer(IndexResolution indexResolution) { return analyzer(indexResolution, TEST_VERIFIER); } + public static Analyzer analyzer(IndexResolution indexResolution, Map lookupResolution) { + return analyzer(indexResolution, lookupResolution, TEST_VERIFIER); + } + public static Analyzer analyzer(IndexResolution indexResolution, Verifier verifier) { return new Analyzer( new AnalyzerContext( @@ -59,6 +63,19 @@ public static Analyzer analyzer(IndexResolution indexResolution, Verifier verifi ); } + public static Analyzer analyzer(IndexResolution indexResolution, Map lookupResolution, Verifier verifier) { + return new Analyzer( + new AnalyzerContext( + EsqlTestUtils.TEST_CFG, + new EsqlFunctionRegistry(), + indexResolution, + lookupResolution, + defaultEnrichResolution() + ), + verifier + ); + } + public static Analyzer analyzer(IndexResolution indexResolution, Verifier verifier, Configuration config) { return new Analyzer( new AnalyzerContext(config, new EsqlFunctionRegistry(), indexResolution, defaultLookupResolution(), defaultEnrichResolution()), @@ -111,7 +128,7 @@ public static IndexResolution loadMapping(String resource, String indexName, Ind } public static IndexResolution loadMapping(String resource, String indexName) { - EsIndex test = new EsIndex(indexName, EsqlTestUtils.loadMapping(resource)); + EsIndex test = new EsIndex(indexName, EsqlTestUtils.loadMapping(resource), Map.of(indexName, IndexMode.STANDARD)); return IndexResolution.valid(test); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index 9c71f20dcde0e..5d1ff43dfe31b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -2139,7 +2139,7 @@ public void testLookupMatchTypeWrong() { } public void testLookupJoinUnknownIndex() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); String errorMessage = "Unknown index [foobar]"; IndexResolution missingLookupIndex = IndexResolution.invalid(errorMessage); @@ -2168,7 +2168,7 @@ public void testLookupJoinUnknownIndex() { } public void testLookupJoinUnknownField() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); String query = "FROM test | LOOKUP JOIN languages_lookup ON last_name"; String errorMessage = "1:45: Unknown column [last_name] in right side of join"; @@ -2190,6 +2190,35 @@ public void testLookupJoinUnknownField() { assertThat(e.getMessage(), containsString(errorMessage3 + "right side of join")); } + public void testLookupJoinIndexMode() { + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); + + var indexResolution = AnalyzerTestUtils.expandedDefaultIndexResolution(); + var lookupResolution = AnalyzerTestUtils.defaultLookupResolution(); + var indexResolutionAsLookup = Map.of("test", indexResolution); + var lookupResolutionAsIndex = lookupResolution.get("languages_lookup"); + + analyze("FROM test | EVAL language_code = languages | LOOKUP JOIN languages_lookup ON language_code"); + analyze( + "FROM languages_lookup | LOOKUP JOIN languages_lookup ON language_code", + AnalyzerTestUtils.analyzer(lookupResolutionAsIndex, lookupResolution) + ); + + VerificationException e = expectThrows( + VerificationException.class, + () -> analyze( + "FROM languages_lookup | EVAL languages = language_code | LOOKUP JOIN test ON languages", + AnalyzerTestUtils.analyzer(lookupResolutionAsIndex, indexResolutionAsLookup) + ) + ); + assertThat(e.getMessage(), containsString("1:70: invalid [test] resolution in lookup mode to an index in [standard] mode")); + e = expectThrows( + VerificationException.class, + () -> analyze("FROM test | LOOKUP JOIN test ON languages", AnalyzerTestUtils.analyzer(indexResolution, indexResolutionAsLookup)) + ); + assertThat(e.getMessage(), containsString("1:25: invalid [test] resolution in lookup mode to an index in [standard] mode")); + } + public void testImplicitCasting() { var e = expectThrows(VerificationException.class, () -> analyze(""" from test | eval x = concat("2024", "-04", "-01") + 1 day diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java index 90a215653f251..549ddce03c206 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java @@ -113,7 +113,7 @@ public void testTooBigQuery() { } public void testJoinOnConstant() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertEquals( "1:55: JOIN ON clause only supports fields at the moment, found [123]", error("row languages = 1, gender = \"f\" | lookup join test on 123") @@ -129,7 +129,7 @@ public void testJoinOnConstant() { } public void testJoinOnMultipleFields() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertEquals( "1:35: JOIN ON clause only supports one field at the moment, found [2]", error("row languages = 1, gender = \"f\" | lookup join test on gender, languages") @@ -137,7 +137,7 @@ public void testJoinOnMultipleFields() { } public void testJoinTwiceOnTheSameField() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertEquals( "1:35: JOIN ON clause only supports one field at the moment, found [2]", error("row languages = 1, gender = \"f\" | lookup join test on languages, languages") @@ -145,7 +145,7 @@ public void testJoinTwiceOnTheSameField() { } public void testJoinTwiceOnTheSameField_TwoLookups() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertEquals( "1:80: JOIN ON clause only supports one field at the moment, found [2]", error("row languages = 1, gender = \"f\" | lookup join test on languages | eval x = 1 | lookup join test on gender, gender") diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index a1e29117a25d3..43d764ab2007d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1974,7 +1974,7 @@ public void testSortByAggregate() { } public void testLookupJoinDataTypeMismatch() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); query("FROM test | EVAL language_code = languages | LOOKUP JOIN languages_lookup ON language_code"); @@ -1985,7 +1985,11 @@ public void testLookupJoinDataTypeMismatch() { } private void query(String query) { - defaultAnalyzer.analyze(parser.createStatement(query)); + query(query, defaultAnalyzer); + } + + private void query(String query, Analyzer analyzer) { + analyzer.analyze(parser.createStatement(query)); } private String error(String query) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index cfb993a7dd73d..17e158f088fb3 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -4906,7 +4906,7 @@ public void testPlanSanityCheck() throws Exception { } public void testPlanSanityCheckWithBinaryPlans() throws Exception { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); var plan = optimizedPlan(""" FROM test @@ -5911,7 +5911,7 @@ public void testLookupStats() { * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#18, language_name{f}#19] */ public void testLookupJoinPushDownFilterOnJoinKeyWithRename() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); String query = """ FROM test @@ -5954,7 +5954,7 @@ public void testLookupJoinPushDownFilterOnJoinKeyWithRename() { * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#18, language_name{f}#19] */ public void testLookupJoinPushDownFilterOnLeftSideField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); String query = """ FROM test @@ -5998,7 +5998,7 @@ public void testLookupJoinPushDownFilterOnLeftSideField() { * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#18, language_name{f}#19] */ public void testLookupJoinPushDownDisabledForLookupField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); String query = """ FROM test @@ -6043,7 +6043,7 @@ public void testLookupJoinPushDownDisabledForLookupField() { * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#19, language_name{f}#20] */ public void testLookupJoinPushDownSeparatedForConjunctionBetweenLeftAndRightField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); String query = """ FROM test @@ -6096,7 +6096,7 @@ public void testLookupJoinPushDownSeparatedForConjunctionBetweenLeftAndRightFiel * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#19, language_name{f}#20] */ public void testLookupJoinPushDownDisabledForDisjunctionBetweenLeftAndRightField() { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); String query = """ FROM test diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index 964dd4642d7c2..c7bb6e49703ed 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -2331,7 +2331,7 @@ public void testVerifierOnMissingReferences() { } public void testVerifierOnMissingReferencesWithBinaryPlans() throws Exception { - assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); // Do not assert serialization: // This will have a LookupJoinExec, which is not serializable because it doesn't leave the coordinator. diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java index 31ec4663738f7..60bdf4e7f73d3 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java @@ -1365,7 +1365,7 @@ public void testMetrics() { } public void testLookupJoin() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( "FROM employees | KEEP languages | RENAME languages AS language_code | LOOKUP JOIN languages_lookup ON language_code", Set.of("languages", "languages.*", "language_code", "language_code.*"), @@ -1374,7 +1374,7 @@ public void testLookupJoin() { } public void testLookupJoinKeep() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM employees @@ -1388,7 +1388,7 @@ public void testLookupJoinKeep() { } public void testLookupJoinKeepWildcard() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM employees @@ -1402,7 +1402,7 @@ public void testLookupJoinKeepWildcard() { } public void testMultiLookupJoin() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1415,7 +1415,7 @@ public void testMultiLookupJoin() { } public void testMultiLookupJoinKeepBefore() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1429,7 +1429,7 @@ public void testMultiLookupJoinKeepBefore() { } public void testMultiLookupJoinKeepBetween() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1454,7 +1454,7 @@ public void testMultiLookupJoinKeepBetween() { } public void testMultiLookupJoinKeepAfter() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1481,7 +1481,7 @@ public void testMultiLookupJoinKeepAfter() { } public void testMultiLookupJoinKeepAfterWildcard() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1495,7 +1495,7 @@ public void testMultiLookupJoinKeepAfterWildcard() { } public void testMultiLookupJoinSameIndex() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1509,7 +1509,7 @@ public void testMultiLookupJoinSameIndex() { } public void testMultiLookupJoinSameIndexKeepBefore() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1524,7 +1524,7 @@ public void testMultiLookupJoinSameIndexKeepBefore() { } public void testMultiLookupJoinSameIndexKeepBetween() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM sample_data @@ -1550,7 +1550,7 @@ public void testMultiLookupJoinSameIndexKeepBetween() { } public void testMultiLookupJoinSameIndexKeepAfter() { - assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V7.isEnabled()); + assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); assertFieldNames( """ FROM sample_data diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/190_lookup_join.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/190_lookup_join.yml new file mode 100644 index 0000000000000..cbaac4e47fdd4 --- /dev/null +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/190_lookup_join.yml @@ -0,0 +1,78 @@ +--- +setup: + - requires: + test_runner_features: [capabilities] + capabilities: + - method: POST + path: /_query + parameters: [] + capabilities: [join_lookup_v8] + reason: "uses LOOKUP JOIN" + - do: + indices.create: + index: test + body: + settings: + number_of_shards: 5 + mappings: + properties: + key: + type: long + color: + type: keyword + - do: + indices.create: + index: test-lookup + body: + settings: + index: + mode: lookup + mappings: + properties: + key: + type: long + color: + type: keyword + - do: + bulk: + index: "test" + refresh: true + body: + - { "index": { } } + - { "key": 1, "color": "red" } + - { "index": { } } + - { "key": 2, "color": "blue" } + - do: + bulk: + index: "test-lookup" + refresh: true + body: + - { "index": { } } + - { "key": 1, "color": "cyan" } + - { "index": { } } + - { "key": 2, "color": "yellow" } + +--- +basic: + - do: + esql.query: + body: + query: 'FROM test | SORT key | LOOKUP JOIN `test-lookup` ON key | LIMIT 3' + + - match: {columns.0.name: "key"} + - match: {columns.0.type: "long"} + - match: {columns.1.name: "color"} + - match: {columns.1.type: "keyword"} + - match: {values.0: [1, "cyan"]} + - match: {values.1: [2, "yellow"]} + +--- +non-lookup index: + - do: + esql.query: + body: + query: 'FROM test-lookup | SORT key | LOOKUP JOIN `test` ON key | LIMIT 3' + catch: "bad_request" + + - match: { error.type: "verification_exception" } + - match: { error.reason: "Found 1 problem\nline 1:43: invalid [test] resolution in lookup mode to an index in [standard] mode" } From 6e725163dc2f466c4c64d8076d693a3feddf3213 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 19 Dec 2024 00:47:40 +1100 Subject: [PATCH 072/119] Mute org.elasticsearch.smoketest.SmokeTestMultiNodeClientYamlTestSuiteIT org.elasticsearch.smoketest.SmokeTestMultiNodeClientYamlTestSuiteIT #118955 --- muted-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 2677b3e75d86e..3a8594acf3bb1 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -290,6 +290,8 @@ tests: - class: org.elasticsearch.cluster.service.MasterServiceTests method: testThreadContext issue: https://github.com/elastic/elasticsearch/issues/118914 +- class: org.elasticsearch.smoketest.SmokeTestMultiNodeClientYamlTestSuiteIT + issue: https://github.com/elastic/elasticsearch/issues/118955 # Examples: # From 49f991bd982e3a81276ce0c43ea15eb399c5a322 Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Wed, 18 Dec 2024 14:56:57 +0100 Subject: [PATCH 073/119] [test] Fix RecoverySourcePruneMergePolicyTests#testPruneSome (#118944) The `extra_source_size` field is set to a value between 10 and 10000 inclusive, so the assertion should be `greaterThanOrEqualTo(10)` rather than `greaterThan(10)`. See #114618 Resolve #118728 --- .../index/engine/RecoverySourcePruneMergePolicyTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java index 74d6e83aff266..b8600842effe4 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java @@ -44,7 +44,7 @@ import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; public class RecoverySourcePruneMergePolicyTests extends ESTestCase { @@ -191,7 +191,7 @@ public void testPruneSome() throws IOException { } assertEquals(i, extra_source.docID()); if (syntheticRecoverySource) { - assertThat(extra_source.longValue(), greaterThan(10L)); + assertThat(extra_source.longValue(), greaterThanOrEqualTo(10L)); } else { assertThat(extra_source.longValue(), equalTo(1L)); } From ad9caae09918207f8ac0e8b1a92a95bdb189c507 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Wed, 18 Dec 2024 14:01:05 +0000 Subject: [PATCH 074/119] Improve handling of nested fields in index reader wrappers (#118757) This update enhances the handling of parent filters to ensure proper exclusion of child documents. --- docs/changelog/118757.yaml | 5 + .../test/AbstractBuilderTestCase.java | 4 + .../authz/permission/DocumentPermissions.java | 6 +- ...ityIndexReaderWrapperIntegrationTests.java | 180 ++++++++++++++++++ 4 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/118757.yaml diff --git a/docs/changelog/118757.yaml b/docs/changelog/118757.yaml new file mode 100644 index 0000000000000..956e220f21aeb --- /dev/null +++ b/docs/changelog/118757.yaml @@ -0,0 +1,5 @@ +pr: 118757 +summary: Improve handling of nested fields in index reader wrappers +area: Authorization +type: enhancement +issues: [] diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java index 20cb66affddee..d239c6453a7fe 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java @@ -241,6 +241,10 @@ protected static IndexSettings indexSettings() { return serviceHolder.idxSettings; } + protected static MapperService mapperService() { + return serviceHolder.mapperService; + } + protected static String expectedFieldName(String builderFieldName) { return ALIAS_TO_CONCRETE_FIELD_NAME.getOrDefault(builderFieldName, builderFieldName); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java index 24f0a52436203..92bb037888495 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java @@ -159,13 +159,15 @@ private static void buildRoleQuery( if (queryBuilder != null) { failIfQueryUsesClient(queryBuilder, context); Query roleQuery = context.toQuery(queryBuilder).query(); - filter.add(roleQuery, SHOULD); - if (context.nestedLookup() != NestedLookup.EMPTY) { + if (context.nestedLookup() == NestedLookup.EMPTY) { + filter.add(roleQuery, SHOULD); + } else { if (NestedHelper.mightMatchNestedDocs(roleQuery, context)) { roleQuery = new BooleanQuery.Builder().add(roleQuery, FILTER) .add(Queries.newNonNestedFilter(context.indexVersionCreated()), FILTER) .build(); } + filter.add(roleQuery, SHOULD); // If access is allowed on root doc then also access is allowed on all nested docs of that root document: BitSetProducer rootDocs = context.bitsetFilter(Queries.newNonNestedFilter(context.indexVersionCreated())); ToChildBlockJoinQuery includeNestedDocs = new ToChildBlockJoinQuery(roleQuery, rootDocs); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapperIntegrationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapperIntegrationTests.java index 4751f66cf548e..89b42228d8918 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapperIntegrationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapperIntegrationTests.java @@ -23,9 +23,12 @@ import org.apache.lucene.search.TotalHitCountCollectorManager; import org.apache.lucene.store.Directory; import org.elasticsearch.client.internal.Client; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; +import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.index.IndexSettings; @@ -33,9 +36,11 @@ import org.elasticsearch.index.mapper.KeywordFieldMapper.KeywordFieldType; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperMetrics; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.MappingLookup; import org.elasticsearch.index.mapper.MockFieldMapper; +import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.query.ParsedQuery; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.TermsQueryBuilder; @@ -45,6 +50,9 @@ import org.elasticsearch.search.internal.ContextIndexSearcher; import org.elasticsearch.test.AbstractBuilderTestCase; import org.elasticsearch.test.IndexSettingsModule; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; @@ -52,6 +60,8 @@ import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import java.io.IOException; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -340,6 +350,176 @@ protected IndicesAccessControl getIndicesAccessControl() { directory.close(); } + @Override + protected void initializeAdditionalMappings(MapperService mapperService) throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder() + .startObject() + .startObject("properties") + .startObject("f1") + .field("type", "keyword") + .endObject() + .startObject("nested1") + .field("type", "nested") + .startObject("properties") + .startObject("field") + .field("type", "keyword") + .endObject() + .endObject() + .endObject() + .endObject() + .endObject(); + mapperService.merge( + MapperService.SINGLE_MAPPING_NAME, + new CompressedXContent(Strings.toString(builder)), + MapperService.MergeReason.MAPPING_UPDATE + ); + } + + public void testDLSWithNestedDocs() throws Exception { + Directory directory = newDirectory(); + try ( + IndexWriter iw = new IndexWriter( + directory, + new IndexWriterConfig(new StandardAnalyzer()).setMergePolicy(NoMergePolicy.INSTANCE) + ) + ) { + var parser = mapperService().documentParser(); + String doc = """ + { + "f1": "value", + "nested1": [ + { + "field": "0" + }, + { + "field": "1" + }, + {} + ] + } + """; + var parsedDoc = parser.parseDocument( + new SourceToParse("0", new BytesArray(doc), XContentType.JSON), + mapperService().mappingLookup() + ); + iw.addDocuments(parsedDoc.docs()); + + doc = """ + { + "nested1": [ + { + "field": "12" + }, + { + "field": "13" + }, + {} + ] + } + """; + parsedDoc = parser.parseDocument( + new SourceToParse("1", new BytesArray(doc), XContentType.JSON), + mapperService().mappingLookup() + ); + iw.addDocuments(parsedDoc.docs()); + + doc = """ + { + "f1": "value", + "nested1": [ + { + "field": "12" + }, + {} + ] + } + """; + parsedDoc = parser.parseDocument( + new SourceToParse("2", new BytesArray(doc), XContentType.JSON), + mapperService().mappingLookup() + ); + iw.addDocuments(parsedDoc.docs()); + + doc = """ + { + "nested1": [ + { + "field": "12" + }, + {} + ] + } + """; + parsedDoc = parser.parseDocument( + new SourceToParse("3", new BytesArray(doc), XContentType.JSON), + mapperService().mappingLookup() + ); + iw.addDocuments(parsedDoc.docs()); + + iw.commit(); + } + + DirectoryReader directoryReader = ElasticsearchDirectoryReader.wrap( + DirectoryReader.open(directory), + new ShardId(indexSettings().getIndex(), 0) + ); + SearchExecutionContext context = createSearchExecutionContext(new IndexSearcher(directoryReader)); + + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + final SecurityContext securityContext = new SecurityContext(Settings.EMPTY, threadContext); + final Authentication authentication = AuthenticationTestHelper.builder().build(); + new AuthenticationContextSerializer().writeToContext(authentication, threadContext); + + Set queries = new HashSet<>(); + queries.add(new BytesArray("{\"bool\": { \"must_not\": { \"exists\": { \"field\": \"f1\" } } } }")); + IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl( + FieldPermissions.DEFAULT, + DocumentPermissions.filteredBy(queries) + ); + + DocumentSubsetBitsetCache bitsetCache = new DocumentSubsetBitsetCache(Settings.EMPTY, Executors.newSingleThreadExecutor()); + + final MockLicenseState licenseState = mock(MockLicenseState.class); + when(licenseState.isAllowed(DOCUMENT_LEVEL_SECURITY_FEATURE)).thenReturn(true); + ScriptService scriptService = mock(ScriptService.class); + SecurityIndexReaderWrapper wrapper = new SecurityIndexReaderWrapper( + s -> context, + bitsetCache, + securityContext, + licenseState, + scriptService + ) { + + @Override + protected IndicesAccessControl getIndicesAccessControl() { + IndicesAccessControl indicesAccessControl = new IndicesAccessControl( + true, + singletonMap(indexSettings().getIndex().getName(), indexAccessControl) + ); + return indicesAccessControl; + } + }; + + DirectoryReader wrappedDirectoryReader = wrapper.apply(directoryReader); + IndexSearcher indexSearcher = new ContextIndexSearcher( + wrappedDirectoryReader, + IndexSearcher.getDefaultSimilarity(), + IndexSearcher.getDefaultQueryCache(), + IndexSearcher.getDefaultQueryCachingPolicy(), + true + ); + + ScoreDoc[] hits = indexSearcher.search(new MatchAllDocsQuery(), 1000).scoreDocs; + assertThat(Arrays.stream(hits).map(h -> h.doc).collect(Collectors.toSet()), containsInAnyOrder(4, 5, 6, 7, 11, 12, 13)); + + hits = indexSearcher.search(Queries.newNonNestedFilter(context.indexVersionCreated()), 1000).scoreDocs; + assertThat(Arrays.stream(hits).map(h -> h.doc).collect(Collectors.toSet()), containsInAnyOrder(7, 13)); + + bitsetCache.close(); + directoryReader.close(); + directory.close(); + } + private static MappingLookup createMappingLookup(List concreteFields) { List mappers = concreteFields.stream().map(MockFieldMapper::new).collect(Collectors.toList()); return MappingLookup.fromMappers(Mapping.EMPTY, mappers, emptyList()); From 5255bfb6fb2f18afaebaf5f9951619a77a58667d Mon Sep 17 00:00:00 2001 From: Sean Story Date: Wed, 18 Dec 2024 08:03:08 -0600 Subject: [PATCH 075/119] Replace 'ent-search-generic' with 'search-default' pipeline (#118899) * Replace 'ent-search-generic' with 'search-default' pipeline * missed one * [CI] Auto commit changes from spotless --------- Co-authored-by: elasticsearchmachine --- .java-version | 1 + .../connectors-content-extraction.asciidoc | 2 +- ...nnectors-filter-extract-transform.asciidoc | 4 +- .../search-inference-processing.asciidoc | 2 +- .../ingest/search-ingest-pipelines.asciidoc | 33 +++-- .../ingest/search-nlp-tutorial.asciidoc | 4 +- .../elastic-connectors-mappings.json | 2 +- .../entsearch/generic_ingestion_pipeline.json | 130 ------------------ .../connector/ConnectorTemplateRegistry.java | 10 -- .../ConnectorIngestPipelineTests.java | 2 +- .../ConnectorTemplateRegistryTests.java | 15 +- .../application/connector/ConnectorTests.java | 6 +- .../syncjob/ConnectorSyncJobTests.java | 8 +- 13 files changed, 37 insertions(+), 182 deletions(-) create mode 100644 .java-version delete mode 100644 x-pack/plugin/core/template-resources/src/main/resources/entsearch/generic_ingestion_pipeline.json diff --git a/.java-version b/.java-version new file mode 100644 index 0000000000000..aabe6ec3909c9 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +21 diff --git a/docs/reference/connector/docs/connectors-content-extraction.asciidoc b/docs/reference/connector/docs/connectors-content-extraction.asciidoc index a87d38c9bf531..744fe1d87cb45 100644 --- a/docs/reference/connector/docs/connectors-content-extraction.asciidoc +++ b/docs/reference/connector/docs/connectors-content-extraction.asciidoc @@ -8,7 +8,7 @@ The logic for content extraction is defined in {connectors-python}/connectors/ut While intended primarily for PDF and Microsoft Office formats, you can use any of the <>. Enterprise Search uses an {ref}/ingest.html[Elasticsearch ingest pipeline^] to power the web crawler's binary content extraction. -The default pipeline, `ent-search-generic-ingestion`, is automatically created when Enterprise Search first starts. +The default pipeline, `search-default-ingestion`, is automatically created when Enterprise Search first starts. You can {ref}/ingest.html#create-manage-ingest-pipelines[view^] this pipeline in Kibana. Customizing your pipeline usage is also an option. diff --git a/docs/reference/connector/docs/connectors-filter-extract-transform.asciidoc b/docs/reference/connector/docs/connectors-filter-extract-transform.asciidoc index 278478c908bf0..62a99928bfb46 100644 --- a/docs/reference/connector/docs/connectors-filter-extract-transform.asciidoc +++ b/docs/reference/connector/docs/connectors-filter-extract-transform.asciidoc @@ -13,7 +13,7 @@ The following diagram provides an overview of how content extraction, sync rules [.screenshot] image::images/pipelines-extraction-sync-rules.png[Architecture diagram of data pipeline with content extraction, sync rules, and ingest pipelines] -By default, only the connector specific logic (2) and the default `ent-search-generic-ingestion` pipeline (6) extract and transform your data, as configured in your deployment. +By default, only the connector specific logic (2) and the default `search-default-ingestion` pipeline (6) extract and transform your data, as configured in your deployment. The following tools are available for more advanced use cases: @@ -50,4 +50,4 @@ Use ingest pipelines for data enrichment, normalization, and more. Elastic connectors use a default ingest pipeline, which you can copy and customize to meet your needs. -Refer to {ref}/ingest-pipeline-search.html[ingest pipelines in Search] in the {es} documentation. \ No newline at end of file +Refer to {ref}/ingest-pipeline-search.html[ingest pipelines in Search] in the {es} documentation. diff --git a/docs/reference/ingest/search-inference-processing.asciidoc b/docs/reference/ingest/search-inference-processing.asciidoc index 006cc96294477..73642b3bb3447 100644 --- a/docs/reference/ingest/search-inference-processing.asciidoc +++ b/docs/reference/ingest/search-inference-processing.asciidoc @@ -88,7 +88,7 @@ The `monitor_ml` <> is req To create the index-specific ML inference pipeline, go to *Search -> Content -> Indices -> -> Pipelines* in the Kibana UI. -If you only see the `ent-search-generic-ingestion` pipeline, you will need to click *Copy and customize* to create index-specific pipelines. +If you only see the `search-default-ingestion` pipeline, you will need to click *Copy and customize* to create index-specific pipelines. This will create the `{index_name}@ml-inference` pipeline. Once your index-specific ML inference pipeline is ready, you can add inference processors that use your ML trained models. diff --git a/docs/reference/ingest/search-ingest-pipelines.asciidoc b/docs/reference/ingest/search-ingest-pipelines.asciidoc index e414dacaab964..272c6ba2884b9 100644 --- a/docs/reference/ingest/search-ingest-pipelines.asciidoc +++ b/docs/reference/ingest/search-ingest-pipelines.asciidoc @@ -40,7 +40,7 @@ Considerations such as error handling, conditional execution, sequencing, versio To this end, when you create indices for search use cases, (including {enterprise-search-ref}/crawler.html[Elastic web crawler], <>. , and API indices), each index already has a pipeline set up with several processors that optimize your content for search. -This pipeline is called `ent-search-generic-ingestion`. +This pipeline is called `search-default-ingestion`. While it is a "managed" pipeline (meaning it should not be tampered with), you can view its details via the Kibana UI or the Elasticsearch API. You can also <>. @@ -56,14 +56,14 @@ This will not effect existing indices. Each index also provides the capability to easily create index-specific ingest pipelines with customizable processing. If you need that extra flexibility, you can create a custom pipeline by going to your pipeline settings and choosing to "copy and customize". -This will replace the index's use of `ent-search-generic-ingestion` with 3 newly generated pipelines: +This will replace the index's use of `search-default-ingestion` with 3 newly generated pipelines: 1. `` 2. `@custom` 3. `@ml-inference` -Like `ent-search-generic-ingestion`, the first of these is "managed", but the other two can and should be modified to fit your needs. -You can view these pipelines using the platform tools (Kibana UI, Elasticsearch API), and can also +Like `search-default-ingestion`, the first of these is "managed", but the other two can and should be modified to fit your needs. +You can view these pipelines using the platform tools (Kibana UI, Elasticsearch API), and can also <>. [discrete#ingest-pipeline-search-pipeline-settings] @@ -123,7 +123,7 @@ If the pipeline is not specified, the underscore-prefixed fields will actually b === Details [discrete#ingest-pipeline-search-details-generic-reference] -==== `ent-search-generic-ingestion` Reference +==== `search-default-ingestion` Reference You can access this pipeline with the <> or via Kibana's < Ingest Pipelines>> UI. @@ -149,7 +149,7 @@ If you want to make customizations, we recommend you utilize index-specific pipe [discrete#ingest-pipeline-search-details-generic-reference-params] ===== Control flow parameters -The `ent-search-generic-ingestion` pipeline does not always run all processors. +The `search-default-ingestion` pipeline does not always run all processors. It utilizes a feature of ingest pipelines to <> based on the contents of each individual document. * `_extract_binary_content` - if this field is present and has a value of `true` on a source document, the pipeline will attempt to run the `attachment`, `set_body`, and `remove_replacement_chars` processors. @@ -167,8 +167,8 @@ See <>. ==== Index-specific ingest pipelines In the Kibana UI for your index, by clicking on the Pipelines tab, then *Settings > Copy and customize*, you can quickly generate 3 pipelines which are specific to your index. -These 3 pipelines replace `ent-search-generic-ingestion` for the index. -There is nothing lost in this action, as the `` pipeline is a superset of functionality over the `ent-search-generic-ingestion` pipeline. +These 3 pipelines replace `search-default-ingestion` for the index. +There is nothing lost in this action, as the `` pipeline is a superset of functionality over the `search-default-ingestion` pipeline. [IMPORTANT] ==== @@ -179,7 +179,7 @@ Refer to the Elastic subscriptions pages for https://www.elastic.co/subscription [discrete#ingest-pipeline-search-details-specific-reference] ===== `` Reference -This pipeline looks and behaves a lot like the <>, but with <>. +This pipeline looks and behaves a lot like the <>, but with <>. [WARNING] ========================= @@ -197,7 +197,7 @@ If you want to make customizations, we recommend you utilize <>, the index-specific pipeline also defines: +In addition to the processors inherited from the <>, the index-specific pipeline also defines: * `index_ml_inference_pipeline` - this uses the <> processor to run the `@ml-inference` pipeline. This processor will only be run if the source document includes a `_run_ml_inference` field with the value `true`. @@ -206,7 +206,7 @@ In addition to the processors inherited from the <` pipeline does not always run all processors. +Like the `search-default-ingestion` pipeline, the `` pipeline does not always run all processors. In addition to the `_extract_binary_content` and `_reduce_whitespace` control flow parameters, the `` pipeline also supports: * `_run_ml_inference` - if this field is present and has a value of `true` on a source document, the pipeline will attempt to run the `index_ml_inference_pipeline` processor. @@ -220,7 +220,7 @@ See <>. ===== `@ml-inference` Reference This pipeline is empty to start (no processors), but can be added to via the Kibana UI either through the Pipelines tab of your index, or from the *Stack Management > Ingest Pipelines* page. -Unlike the `ent-search-generic-ingestion` pipeline and the `` pipeline, this pipeline is NOT "managed". +Unlike the `search-default-ingestion` pipeline and the `` pipeline, this pipeline is NOT "managed". It's possible to add one or more ML inference pipelines to an index in the *Content* UI. This pipeline will serve as a container for all of the ML inference pipelines configured for the index. @@ -241,7 +241,7 @@ The `monitor_ml` Elasticsearch cluster permission is required in order to manage This pipeline is empty to start (no processors), but can be added to via the Kibana UI either through the Pipelines tab of your index, or from the *Stack Management > Ingest Pipelines* page. -Unlike the `ent-search-generic-ingestion` pipeline and the `` pipeline, this pipeline is NOT "managed". +Unlike the `search-default-ingestion` pipeline and the `` pipeline, this pipeline is NOT "managed". You are encouraged to make additions and edits to this pipeline, provided its name remains the same. This provides a convenient hook from which to add custom processing and transformations for your data. @@ -272,9 +272,12 @@ extraction. These changes should be re-applied to each index's `@custom` pipeline in order to ensure a consistent data processing experience. In 8.5+, the <> is required *in addition* to the configurations mentioned in the {enterprise-search-ref}/crawler-managing.html#crawler-managing-binary-content[Elastic web crawler Guide]. -* `ent-search-generic-ingestion` - Since 8.5, Native Connectors, Connector Clients, and new (>8.4) Elastic web crawler indices will all make use of this pipeline by default. +* `ent-search-generic-ingestion` - Since 8.5, Native Connectors, Connector Clients, and new (>8.4) Elastic web crawler indices all made use of this pipeline by default. + This pipeline evolved into the `search-default-ingestion` pipeline. + +* `search-default-ingestion` - Since 9.0, Connectors have made use of this pipeline by default. You can <> above. - As this pipeline is "managed", any modifications that were made to `app_search_crawler` and/or `ent_search_crawler` should NOT be made to `ent-search-generic-ingestion`. + As this pipeline is "managed", any modifications that were made to `app_search_crawler` and/or `ent_search_crawler` should NOT be made to `search-default-ingestion`. Instead, if such customizations are desired, you should utilize <>, placing all modifications in the `@custom` pipeline(s). ============= diff --git a/docs/reference/ingest/search-nlp-tutorial.asciidoc b/docs/reference/ingest/search-nlp-tutorial.asciidoc index afdceeeb8bac2..b23a15c96b1a2 100644 --- a/docs/reference/ingest/search-nlp-tutorial.asciidoc +++ b/docs/reference/ingest/search-nlp-tutorial.asciidoc @@ -164,8 +164,8 @@ Now it's time to create an inference pipeline. 1. From the overview page for your `search-photo-comments` index in "Search", click the *Pipelines* tab. By default, Elasticsearch does not create any index-specific ingest pipelines. -2. Because we want to customize these pipelines, we need to *Copy and customize* the `ent-search-generic-ingestion` ingest pipeline. -Find this option above the settings for the `ent-search-generic-ingestion` ingest pipeline. +2. Because we want to customize these pipelines, we need to *Copy and customize* the `search-default-ingestion` ingest pipeline. +Find this option above the settings for the `search-default-ingestion` ingest pipeline. This will create two new index-specific ingest pipelines. Next, we'll add an inference pipeline. diff --git a/x-pack/plugin/core/template-resources/src/main/resources/entsearch/connector/elastic-connectors-mappings.json b/x-pack/plugin/core/template-resources/src/main/resources/entsearch/connector/elastic-connectors-mappings.json index 651e1c84da73a..5afa557e1405e 100644 --- a/x-pack/plugin/core/template-resources/src/main/resources/entsearch/connector/elastic-connectors-mappings.json +++ b/x-pack/plugin/core/template-resources/src/main/resources/entsearch/connector/elastic-connectors-mappings.json @@ -7,7 +7,7 @@ "dynamic": "false", "_meta": { "pipeline": { - "default_name": "ent-search-generic-ingestion", + "default_name": "search-default-ingestion", "default_extract_binary_content": true, "default_run_ml_inference": true, "default_reduce_whitespace": true diff --git a/x-pack/plugin/core/template-resources/src/main/resources/entsearch/generic_ingestion_pipeline.json b/x-pack/plugin/core/template-resources/src/main/resources/entsearch/generic_ingestion_pipeline.json deleted file mode 100644 index e2a2cbd460117..0000000000000 --- a/x-pack/plugin/core/template-resources/src/main/resources/entsearch/generic_ingestion_pipeline.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "version": ${xpack.application.connector.template.version}, - "description": "Generic Enterprise Search ingest pipeline", - "_meta": { - "managed_by": "Enterprise Search", - "managed": true - }, - "processors": [ - { - "attachment": { - "description": "Extract text from binary attachments", - "field": "_attachment", - "target_field": "_extracted_attachment", - "ignore_missing": true, - "indexed_chars_field": "_attachment_indexed_chars", - "if": "ctx?._extract_binary_content == true", - "on_failure": [ - { - "append": { - "description": "Record error information", - "field": "_ingestion_errors", - "value": "Processor 'attachment' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'" - } - } - ], - "remove_binary": false - } - }, - { - "set": { - "tag": "set_body", - "description": "Set any extracted text on the 'body' field", - "field": "body", - "copy_from": "_extracted_attachment.content", - "ignore_empty_value": true, - "if": "ctx?._extract_binary_content == true", - "on_failure": [ - { - "append": { - "description": "Record error information", - "field": "_ingestion_errors", - "value": "Processor 'set' with tag 'set_body' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'" - } - } - ] - } - }, - { - "gsub": { - "tag": "remove_replacement_chars", - "description": "Remove unicode 'replacement' characters", - "field": "body", - "pattern": "�", - "replacement": "", - "ignore_missing": true, - "if": "ctx?._extract_binary_content == true", - "on_failure": [ - { - "append": { - "description": "Record error information", - "field": "_ingestion_errors", - "value": "Processor 'gsub' with tag 'remove_replacement_chars' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'" - } - } - ] - } - }, - { - "gsub": { - "tag": "remove_extra_whitespace", - "description": "Squish whitespace", - "field": "body", - "pattern": "\\s+", - "replacement": " ", - "ignore_missing": true, - "if": "ctx?._reduce_whitespace == true", - "on_failure": [ - { - "append": { - "description": "Record error information", - "field": "_ingestion_errors", - "value": "Processor 'gsub' with tag 'remove_extra_whitespace' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'" - } - } - ] - } - }, - { - "trim" : { - "description": "Trim leading and trailing whitespace", - "field": "body", - "ignore_missing": true, - "if": "ctx?._reduce_whitespace == true", - "on_failure": [ - { - "append": { - "description": "Record error information", - "field": "_ingestion_errors", - "value": "Processor 'trim' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'" - } - } - ] - } - }, - { - "remove": { - "tag": "remove_meta_fields", - "description": "Remove meta fields", - "field": [ - "_attachment", - "_attachment_indexed_chars", - "_extracted_attachment", - "_extract_binary_content", - "_reduce_whitespace", - "_run_ml_inference" - ], - "ignore_missing": true, - "on_failure": [ - { - "append": { - "description": "Record error information", - "field": "_ingestion_errors", - "value": "Processor 'remove' with tag 'remove_meta_fields' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'" - } - } - ] - } - } - ] -} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistry.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistry.java index e630f929bc09b..fd35acc89db5c 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistry.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistry.java @@ -48,10 +48,6 @@ public class ConnectorTemplateRegistry extends IndexTemplateRegistry { public static final String MANAGED_CONNECTOR_INDEX_PREFIX = "content-"; // Pipeline constants - - public static final String ENT_SEARCH_GENERIC_PIPELINE_NAME = "ent-search-generic-ingestion"; - public static final String ENT_SEARCH_GENERIC_PIPELINE_FILE = "generic_ingestion_pipeline"; - public static final String SEARCH_DEFAULT_PIPELINE_NAME = "search-default-ingestion"; public static final String SEARCH_DEFAULT_PIPELINE_FILE = "search_default_pipeline"; @@ -111,12 +107,6 @@ public class ConnectorTemplateRegistry extends IndexTemplateRegistry { @Override protected List getIngestPipelines() { return List.of( - new JsonIngestPipelineConfig( - ENT_SEARCH_GENERIC_PIPELINE_NAME, - ROOT_RESOURCE_PATH + ENT_SEARCH_GENERIC_PIPELINE_FILE + ".json", - REGISTRY_VERSION, - TEMPLATE_VERSION_VARIABLE - ), new JsonIngestPipelineConfig( SEARCH_DEFAULT_PIPELINE_NAME, ROOT_RESOURCE_PATH + SEARCH_DEFAULT_PIPELINE_FILE + ".json", diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIngestPipelineTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIngestPipelineTests.java index f4a92e51e8c6a..c3d4bf8b72ff5 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIngestPipelineTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIngestPipelineTests.java @@ -50,7 +50,7 @@ public void testToXContent() throws IOException { String content = XContentHelper.stripWhitespace(""" { "extract_binary_content": true, - "name": "ent-search-generic-ingestion", + "name": "search-default-ingestion", "reduce_whitespace": true, "run_ml_inference": false } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistryTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistryTests.java index a4c7015afafcb..068b99626af9d 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistryTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTemplateRegistryTests.java @@ -132,10 +132,7 @@ public void testThatNonExistingComponentTemplatesAreAddedImmediately() throws Ex ClusterChangedEvent event = createClusterChangedEvent( Collections.emptyMap(), Collections.emptyMap(), - Collections.singletonMap( - ConnectorTemplateRegistry.ENT_SEARCH_GENERIC_PIPELINE_NAME, - ConnectorTemplateRegistry.REGISTRY_VERSION - ), + Collections.singletonMap(ConnectorTemplateRegistry.SEARCH_DEFAULT_PIPELINE_NAME, ConnectorTemplateRegistry.REGISTRY_VERSION), Collections.emptyMap(), nodes ); @@ -169,10 +166,7 @@ public void testThatVersionedOldComponentTemplatesAreUpgraded() throws Exception ConnectorTemplateRegistry.CONNECTOR_TEMPLATE_NAME + "-settings", ConnectorTemplateRegistry.REGISTRY_VERSION - 1 ), - Collections.singletonMap( - ConnectorTemplateRegistry.ENT_SEARCH_GENERIC_PIPELINE_NAME, - ConnectorTemplateRegistry.REGISTRY_VERSION - ), + Collections.singletonMap(ConnectorTemplateRegistry.SEARCH_DEFAULT_PIPELINE_NAME, ConnectorTemplateRegistry.REGISTRY_VERSION), Collections.emptyMap(), nodes ); @@ -189,10 +183,7 @@ public void testThatUnversionedOldComponentTemplatesAreUpgraded() throws Excepti ClusterChangedEvent event = createClusterChangedEvent( Collections.emptyMap(), Collections.singletonMap(ConnectorTemplateRegistry.CONNECTOR_TEMPLATE_NAME + "-mappings", null), - Collections.singletonMap( - ConnectorTemplateRegistry.ENT_SEARCH_GENERIC_PIPELINE_NAME, - ConnectorTemplateRegistry.REGISTRY_VERSION - ), + Collections.singletonMap(ConnectorTemplateRegistry.SEARCH_DEFAULT_PIPELINE_NAME, ConnectorTemplateRegistry.REGISTRY_VERSION), Collections.emptyMap(), nodes ); diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTests.java index 734c6eaf86965..bcb647d978abb 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTests.java @@ -225,7 +225,7 @@ public void testToXContent() throws IOException { "name":"test-name", "pipeline":{ "extract_binary_content":true, - "name":"ent-search-generic-ingestion", + "name":"search-default-ingestion", "reduce_whitespace":true, "run_ml_inference":false }, @@ -286,7 +286,7 @@ public void testToContent_WithNullValues() throws IOException { "name": null, "pipeline":{ "extract_binary_content":true, - "name":"ent-search-generic-ingestion", + "name":"search-default-ingestion", "reduce_whitespace":true, "run_ml_inference":false }, @@ -350,7 +350,7 @@ public void testToXContent_withOptionalFieldsMissing() throws IOException { "name": null, "pipeline":{ "extract_binary_content":true, - "name":"ent-search-generic-ingestion", + "name":"search-default-ingestion", "reduce_whitespace":true, "run_ml_inference":false }, diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTests.java index 81b05ce25e177..ed3338c715bdf 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTests.java @@ -77,7 +77,7 @@ public void testFromXContent_WithAllFields_AllSet() throws IOException { "language": "english", "pipeline": { "extract_binary_content": true, - "name": "ent-search-generic-ingestion", + "name": "search-default-ingestion", "reduce_whitespace": true, "run_ml_inference": false }, @@ -160,7 +160,7 @@ public void testFromXContent_WithOnlyNonNullableFieldsSet_DoesNotThrow() throws "language": "english", "pipeline": { "extract_binary_content": true, - "name": "ent-search-generic-ingestion", + "name": "search-default-ingestion", "reduce_whitespace": true, "run_ml_inference": false }, @@ -218,7 +218,7 @@ public void testFromXContent_WithAllNullableFieldsSetToNull_DoesNotThrow() throw "language": "english", "pipeline": { "extract_binary_content": true, - "name": "ent-search-generic-ingestion", + "name": "search-default-ingestion", "reduce_whitespace": true, "run_ml_inference": false }, @@ -275,7 +275,7 @@ public void testSyncJobConnectorFromXContent_WithAllFieldsSet() throws IOExcepti "language": "english", "pipeline": { "extract_binary_content": true, - "name": "ent-search-generic-ingestion", + "name": "search-default-ingestion", "reduce_whitespace": true, "run_ml_inference": false }, From a2360d18b434b89f0aae9dbc264a16a5e75d1475 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 19 Dec 2024 01:20:54 +1100 Subject: [PATCH 076/119] Mute org.elasticsearch.xpack.migrate.task.ReindexDataStreamStatusTests testEqualsAndHashcode #118965 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 3a8594acf3bb1..b35a70391f195 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -292,6 +292,9 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118914 - class: org.elasticsearch.smoketest.SmokeTestMultiNodeClientYamlTestSuiteIT issue: https://github.com/elastic/elasticsearch/issues/118955 +- class: org.elasticsearch.xpack.migrate.task.ReindexDataStreamStatusTests + method: testEqualsAndHashcode + issue: https://github.com/elastic/elasticsearch/issues/118965 # Examples: # From 97bc2919ffb5d9e90f809a18233c49a52cf58faa Mon Sep 17 00:00:00 2001 From: Matteo Piergiovanni <134913285+piergm@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:29:35 +0100 Subject: [PATCH 077/119] Prevent data nodes from sending stack traces to coordinator when `error_trace=false` (#118266) * first iterations * added tests * Update docs/changelog/118266.yaml * constant for error_trace and typos * centralized putHeader * moved threadContext to parent class * uses NodeClient.threadpool * updated async tests to retrieve final result * moved test to avoid starting up a node * added transport version to avoid sending useless bytes * more async tests --- docs/changelog/118266.yaml | 5 + .../bucket/SearchCancellationIT.java | 2 + .../http/SearchErrorTraceIT.java | 175 ++++++++++++++ .../org/elasticsearch/TransportVersions.java | 1 + .../action/search/SearchTransportService.java | 6 +- .../common/util/concurrent/ThreadContext.java | 21 ++ .../elasticsearch/rest/BaseRestHandler.java | 1 - .../elasticsearch/rest/RestController.java | 3 +- .../org/elasticsearch/rest/RestResponse.java | 3 +- .../action/search/RestMultiSearchAction.java | 3 + .../rest/action/search/RestSearchAction.java | 4 +- .../elasticsearch/search/SearchService.java | 56 ++++- .../elasticsearch/rest/RestResponseTests.java | 3 +- .../search/SearchServiceSingleNodeTests.java | 3 +- .../search/SearchServiceTests.java | 34 +++ .../xpack/search/AsyncSearchErrorTraceIT.java | 222 ++++++++++++++++++ .../search/RestSubmitAsyncSearchAction.java | 3 + .../ServerSentEventsRestActionListener.java | 3 +- 18 files changed, 535 insertions(+), 13 deletions(-) create mode 100644 docs/changelog/118266.yaml create mode 100644 qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/SearchErrorTraceIT.java create mode 100644 x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/AsyncSearchErrorTraceIT.java diff --git a/docs/changelog/118266.yaml b/docs/changelog/118266.yaml new file mode 100644 index 0000000000000..1b14b12b973c5 --- /dev/null +++ b/docs/changelog/118266.yaml @@ -0,0 +1,5 @@ +pr: 118266 +summary: Prevent data nodes from sending stack traces to coordinator when `error_trace=false` +area: Search +type: enhancement +issues: [] diff --git a/modules/aggregations/src/internalClusterTest/java/org/elasticsearch/aggregations/bucket/SearchCancellationIT.java b/modules/aggregations/src/internalClusterTest/java/org/elasticsearch/aggregations/bucket/SearchCancellationIT.java index 5249077bdfdbb..7adf6a09e9a19 100644 --- a/modules/aggregations/src/internalClusterTest/java/org/elasticsearch/aggregations/bucket/SearchCancellationIT.java +++ b/modules/aggregations/src/internalClusterTest/java/org/elasticsearch/aggregations/bucket/SearchCancellationIT.java @@ -96,6 +96,8 @@ public void testCancellationDuringTimeSeriesAggregation() throws Exception { } logger.info("Executing search"); + // we have to explicitly set error_trace=true for the later exception check for `TimeSeriesIndexSearcher` + client().threadPool().getThreadContext().putHeader("error_trace", "true"); TimeSeriesAggregationBuilder timeSeriesAggregationBuilder = new TimeSeriesAggregationBuilder("test_agg"); ActionFuture searchResponse = prepareSearch("test").setQuery(matchAllQuery()) .addAggregation( diff --git a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/SearchErrorTraceIT.java b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/SearchErrorTraceIT.java new file mode 100644 index 0000000000000..6f9ab8ccdfdec --- /dev/null +++ b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/SearchErrorTraceIT.java @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.http; + +import org.apache.http.entity.ContentType; +import org.apache.http.nio.entity.NByteArrayEntity; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.action.search.MultiSearchRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.client.Request; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.transport.TransportMessageListener; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xcontent.XContentType; +import org.junit.Before; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.elasticsearch.index.query.QueryBuilders.simpleQueryStringQuery; + +public class SearchErrorTraceIT extends HttpSmokeTestCase { + private AtomicBoolean hasStackTrace; + + @Before + private void setupMessageListener() { + internalCluster().getDataNodeInstances(TransportService.class).forEach(ts -> { + ts.addMessageListener(new TransportMessageListener() { + @Override + public void onResponseSent(long requestId, String action, Exception error) { + TransportMessageListener.super.onResponseSent(requestId, action, error); + if (action.startsWith("indices:data/read/search")) { + Optional throwable = ExceptionsHelper.unwrapCausesAndSuppressed( + error, + t -> t.getStackTrace().length > 0 + ); + hasStackTrace.set(throwable.isPresent()); + } + } + }); + }); + } + + private void setupIndexWithDocs() { + createIndex("test1", "test2"); + indexRandom( + true, + prepareIndex("test1").setId("1").setSource("field", "foo"), + prepareIndex("test2").setId("10").setSource("field", 5) + ); + refresh(); + } + + public void testSearchFailingQueryErrorTraceDefault() throws IOException { + hasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + Request searchRequest = new Request("POST", "/_search"); + searchRequest.setJsonEntity(""" + { + "query": { + "simple_query_string" : { + "query": "foo", + "fields": ["field"] + } + } + } + """); + getRestClient().performRequest(searchRequest); + assertFalse(hasStackTrace.get()); + } + + public void testSearchFailingQueryErrorTraceTrue() throws IOException { + hasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + Request searchRequest = new Request("POST", "/_search"); + searchRequest.setJsonEntity(""" + { + "query": { + "simple_query_string" : { + "query": "foo", + "fields": ["field"] + } + } + } + """); + searchRequest.addParameter("error_trace", "true"); + getRestClient().performRequest(searchRequest); + assertTrue(hasStackTrace.get()); + } + + public void testSearchFailingQueryErrorTraceFalse() throws IOException { + hasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + Request searchRequest = new Request("POST", "/_search"); + searchRequest.setJsonEntity(""" + { + "query": { + "simple_query_string" : { + "query": "foo", + "fields": ["field"] + } + } + } + """); + searchRequest.addParameter("error_trace", "false"); + getRestClient().performRequest(searchRequest); + assertFalse(hasStackTrace.get()); + } + + public void testMultiSearchFailingQueryErrorTraceDefault() throws IOException { + hasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + XContentType contentType = XContentType.JSON; + MultiSearchRequest multiSearchRequest = new MultiSearchRequest().add( + new SearchRequest("test*").source(new SearchSourceBuilder().query(simpleQueryStringQuery("foo").field("field"))) + ); + Request searchRequest = new Request("POST", "/_msearch"); + byte[] requestBody = MultiSearchRequest.writeMultiLineFormat(multiSearchRequest, contentType.xContent()); + searchRequest.setEntity( + new NByteArrayEntity(requestBody, ContentType.create(contentType.mediaTypeWithoutParameters(), (Charset) null)) + ); + getRestClient().performRequest(searchRequest); + assertFalse(hasStackTrace.get()); + } + + public void testMultiSearchFailingQueryErrorTraceTrue() throws IOException { + hasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + XContentType contentType = XContentType.JSON; + MultiSearchRequest multiSearchRequest = new MultiSearchRequest().add( + new SearchRequest("test*").source(new SearchSourceBuilder().query(simpleQueryStringQuery("foo").field("field"))) + ); + Request searchRequest = new Request("POST", "/_msearch"); + byte[] requestBody = MultiSearchRequest.writeMultiLineFormat(multiSearchRequest, contentType.xContent()); + searchRequest.setEntity( + new NByteArrayEntity(requestBody, ContentType.create(contentType.mediaTypeWithoutParameters(), (Charset) null)) + ); + searchRequest.addParameter("error_trace", "true"); + getRestClient().performRequest(searchRequest); + assertTrue(hasStackTrace.get()); + } + + public void testMultiSearchFailingQueryErrorTraceFalse() throws IOException { + hasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + XContentType contentType = XContentType.JSON; + MultiSearchRequest multiSearchRequest = new MultiSearchRequest().add( + new SearchRequest("test*").source(new SearchSourceBuilder().query(simpleQueryStringQuery("foo").field("field"))) + ); + Request searchRequest = new Request("POST", "/_msearch"); + byte[] requestBody = MultiSearchRequest.writeMultiLineFormat(multiSearchRequest, contentType.xContent()); + searchRequest.setEntity( + new NByteArrayEntity(requestBody, ContentType.create(contentType.mediaTypeWithoutParameters(), (Charset) null)) + ); + searchRequest.addParameter("error_trace", "false"); + getRestClient().performRequest(searchRequest); + + assertFalse(hasStackTrace.get()); + } +} diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index d3e235f1cd82a..bda66d6a2c8cd 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -140,6 +140,7 @@ static TransportVersion def(int id) { public static final TransportVersion ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS = def(8_808_00_0); public static final TransportVersion EQL_ALLOW_PARTIAL_SEARCH_RESULTS = def(8_809_00_0); public static final TransportVersion NODE_VERSION_INFORMATION_WITH_MIN_READ_ONLY_INDEX_VERSION = def(8_810_00_0); + public static final TransportVersion ERROR_TRACE_IN_TRANSPORT_HEADER = def(8_811_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchTransportService.java b/server/src/main/java/org/elasticsearch/action/search/SearchTransportService.java index cfc2e1bcdaf2b..2041754bc2bcc 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchTransportService.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchTransportService.java @@ -456,7 +456,8 @@ public static void registerRequestHandler(TransportService transportService, Sea (request, channel, task) -> searchService.executeQueryPhase( request, (SearchShardTask) task, - new ChannelActionListener<>(channel) + new ChannelActionListener<>(channel), + channel.getVersion() ) ); TransportActionProxy.registerProxyAction(transportService, QUERY_ID_ACTION_NAME, true, QuerySearchResult::new); @@ -468,7 +469,8 @@ public static void registerRequestHandler(TransportService transportService, Sea (request, channel, task) -> searchService.executeQueryPhase( request, (SearchShardTask) task, - new ChannelActionListener<>(channel) + new ChannelActionListener<>(channel), + channel.getVersion() ) ); TransportActionProxy.registerProxyAction(transportService, QUERY_SCROLL_ACTION_NAME, true, ScrollQuerySearchResult::new); diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java index a9e13b86a5159..6841cb5bead0a 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java @@ -24,6 +24,8 @@ import org.elasticsearch.core.Releasable; import org.elasticsearch.core.Tuple; import org.elasticsearch.http.HttpTransportSettings; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; import org.elasticsearch.tasks.Task; import org.elasticsearch.telemetry.tracing.TraceContext; @@ -530,6 +532,17 @@ public String getHeader(String key) { return value; } + /** + * Returns the header for the given key or defaultValue if not present + */ + public String getHeaderOrDefault(String key, String defaultValue) { + String value = getHeader(key); + if (value == null) { + return defaultValue; + } + return value; + } + /** * Returns all of the request headers from the thread's context.
    * Be advised, headers might contain credentials. @@ -589,6 +602,14 @@ public void putHeader(Map header) { threadLocal.set(threadLocal.get().putHeaders(header)); } + public void setErrorTraceTransportHeader(RestRequest r) { + // set whether data nodes should send back stack trace based on the `error_trace` query parameter + if (r.paramAsBoolean("error_trace", RestController.ERROR_TRACE_DEFAULT)) { + // We only set it if error_trace is true (defaults to false) to avoid sending useless bytes + putHeader("error_trace", "true"); + } + } + /** * Puts a transient header object into this context */ diff --git a/server/src/main/java/org/elasticsearch/rest/BaseRestHandler.java b/server/src/main/java/org/elasticsearch/rest/BaseRestHandler.java index 4564a37dacf4a..509086b982319 100644 --- a/server/src/main/java/org/elasticsearch/rest/BaseRestHandler.java +++ b/server/src/main/java/org/elasticsearch/rest/BaseRestHandler.java @@ -269,5 +269,4 @@ protected Set responseParams() { protected Set responseParams(RestApiVersion restApiVersion) { return responseParams(); } - } diff --git a/server/src/main/java/org/elasticsearch/rest/RestController.java b/server/src/main/java/org/elasticsearch/rest/RestController.java index 49fe794bbe615..49801499ea991 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestController.java +++ b/server/src/main/java/org/elasticsearch/rest/RestController.java @@ -93,6 +93,7 @@ public class RestController implements HttpServerTransport.Dispatcher { public static final String STATUS_CODE_KEY = "es_rest_status_code"; public static final String HANDLER_NAME_KEY = "es_rest_handler_name"; public static final String REQUEST_METHOD_KEY = "es_rest_request_method"; + public static final boolean ERROR_TRACE_DEFAULT = false; static { try (InputStream stream = RestController.class.getResourceAsStream("/config/favicon.ico")) { @@ -638,7 +639,7 @@ private void tryAllHandlers(final RestRequest request, final RestChannel channel private static void validateErrorTrace(RestRequest request, RestChannel channel) { // error_trace cannot be used when we disable detailed errors // we consume the error_trace parameter first to ensure that it is always consumed - if (request.paramAsBoolean("error_trace", false) && channel.detailedErrorsEnabled() == false) { + if (request.paramAsBoolean("error_trace", ERROR_TRACE_DEFAULT) && channel.detailedErrorsEnabled() == false) { throw new IllegalArgumentException("error traces in responses are disabled."); } } diff --git a/server/src/main/java/org/elasticsearch/rest/RestResponse.java b/server/src/main/java/org/elasticsearch/rest/RestResponse.java index d043974055667..0c359e0a4a053 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestResponse.java +++ b/server/src/main/java/org/elasticsearch/rest/RestResponse.java @@ -37,6 +37,7 @@ import static java.util.Collections.singletonMap; import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; import static org.elasticsearch.rest.RestController.ELASTIC_PRODUCT_HTTP_HEADER; +import static org.elasticsearch.rest.RestController.ERROR_TRACE_DEFAULT; public final class RestResponse implements Releasable { @@ -143,7 +144,7 @@ public RestResponse(RestChannel channel, RestStatus status, Exception e) throws // switched in the xcontent rendering parameters. // For authorization problems (RestStatus.UNAUTHORIZED) we don't want to do this since this could // leak information to the caller who is unauthorized to make this call - if (params.paramAsBoolean("error_trace", false) && status != RestStatus.UNAUTHORIZED) { + if (params.paramAsBoolean("error_trace", ERROR_TRACE_DEFAULT) && status != RestStatus.UNAUTHORIZED) { params = new ToXContent.DelegatingMapParams(singletonMap(REST_EXCEPTION_SKIP_STACK_TRACE, "false"), params); } diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java index 24fab92ced392..87b1a6b9c2fa8 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java @@ -72,6 +72,9 @@ public String getName() { @Override public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { + if (client.threadPool() != null && client.threadPool().getThreadContext() != null) { + client.threadPool().getThreadContext().setErrorTraceTransportHeader(request); + } final MultiSearchRequest multiSearchRequest = parseRequest(request, allowExplicitIndex, searchUsageHolder, clusterSupportsFeature); return channel -> { final RestCancellableNodeClient cancellableClient = new RestCancellableNodeClient(client, request.getHttpChannel()); diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java index a9c2ff7576b05..99c11bb60b8f0 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java @@ -95,7 +95,9 @@ public Set supportedCapabilities() { @Override public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { - + if (client.threadPool() != null && client.threadPool().getThreadContext() != null) { + client.threadPool().getThreadContext().setErrorTraceTransportHeader(request); + } SearchRequest searchRequest = new SearchRequest(); // access the BwC param, but just drop it // this might be set by old clients diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index b9bd398500c71..4557ccb3d2220 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -17,6 +17,8 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.TopDocs; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.TransportVersion; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.ResolvedIndices; @@ -152,6 +154,7 @@ import java.util.function.LongSupplier; import java.util.function.Supplier; +import static org.elasticsearch.TransportVersions.ERROR_TRACE_IN_TRANSPORT_HEADER; import static org.elasticsearch.core.TimeValue.timeValueHours; import static org.elasticsearch.core.TimeValue.timeValueMillis; import static org.elasticsearch.core.TimeValue.timeValueMinutes; @@ -272,6 +275,7 @@ public class SearchService extends AbstractLifecycleComponent implements IndexEv public static final int DEFAULT_SIZE = 10; public static final int DEFAULT_FROM = 0; + private static final StackTraceElement[] EMPTY_STACK_TRACE_ARRAY = new StackTraceElement[0]; private final ThreadPool threadPool; @@ -506,7 +510,41 @@ protected void doClose() { keepAliveReaper.cancel(); } + /** + * Wraps the listener to avoid sending StackTraces back to the coordinating + * node if the `error_trace` header is set to {@code false}. Upon reading we + * default to {@code true} to maintain the same behavior as before the change, + * due to older nodes not being able to specify whether it needs stack traces. + * + * @param the type of the response + * @param listener the action listener to be wrapped + * @param version channel version of the request + * @param threadPool with context where to write the new header + * @return the wrapped action listener + */ + static ActionListener maybeWrapListenerForStackTrace( + ActionListener listener, + TransportVersion version, + ThreadPool threadPool + ) { + boolean header = true; + if (version.onOrAfter(ERROR_TRACE_IN_TRANSPORT_HEADER) && threadPool.getThreadContext() != null) { + header = Boolean.parseBoolean(threadPool.getThreadContext().getHeaderOrDefault("error_trace", "false")); + } + if (header == false) { + return listener.delegateResponse((l, e) -> { + ExceptionsHelper.unwrapCausesAndSuppressed(e, err -> { + err.setStackTrace(EMPTY_STACK_TRACE_ARRAY); + return false; + }); + l.onFailure(e); + }); + } + return listener; + } + public void executeDfsPhase(ShardSearchRequest request, SearchShardTask task, ActionListener listener) { + listener = maybeWrapListenerForStackTrace(listener, request.getChannelVersion(), threadPool); final IndexShard shard = getShard(request); rewriteAndFetchShardRequest(shard, request, listener.delegateFailure((l, rewritten) -> { // fork the execution in the search thread pool @@ -544,10 +582,11 @@ private void loadOrExecuteQueryPhase(final ShardSearchRequest request, final Sea } public void executeQueryPhase(ShardSearchRequest request, SearchShardTask task, ActionListener listener) { + ActionListener finalListener = maybeWrapListenerForStackTrace(listener, request.getChannelVersion(), threadPool); assert request.canReturnNullResponseIfMatchNoDocs() == false || request.numberOfShards() > 1 : "empty responses require more than one shard"; final IndexShard shard = getShard(request); - rewriteAndFetchShardRequest(shard, request, listener.delegateFailure((l, orig) -> { + rewriteAndFetchShardRequest(shard, request, finalListener.delegateFailure((l, orig) -> { // check if we can shortcut the query phase entirely. if (orig.canReturnNullResponseIfMatchNoDocs()) { assert orig.scroll() == null; @@ -561,7 +600,7 @@ public void executeQueryPhase(ShardSearchRequest request, SearchShardTask task, ); CanMatchShardResponse canMatchResp = canMatch(canMatchContext, false); if (canMatchResp.canMatch() == false) { - listener.onResponse(QuerySearchResult.nullInstance()); + finalListener.onResponse(QuerySearchResult.nullInstance()); return; } } @@ -736,6 +775,7 @@ private SearchPhaseResult executeQueryPhase(ShardSearchRequest request, SearchSh } public void executeRankFeaturePhase(RankFeatureShardRequest request, SearchShardTask task, ActionListener listener) { + listener = maybeWrapListenerForStackTrace(listener, request.getShardSearchRequest().getChannelVersion(), threadPool); final ReaderContext readerContext = findReaderContext(request.contextId(), request); final ShardSearchRequest shardSearchRequest = readerContext.getShardSearchRequest(request.getShardSearchRequest()); final Releasable markAsUsed = readerContext.markAsUsed(getKeepAlive(shardSearchRequest)); @@ -779,8 +819,10 @@ private QueryFetchSearchResult executeFetchPhase(ReaderContext reader, SearchCon public void executeQueryPhase( InternalScrollSearchRequest request, SearchShardTask task, - ActionListener listener + ActionListener listener, + TransportVersion version ) { + listener = maybeWrapListenerForStackTrace(listener, version, threadPool); final LegacyReaderContext readerContext = (LegacyReaderContext) findReaderContext(request.contextId(), request); final Releasable markAsUsed; try { @@ -816,7 +858,13 @@ public void executeQueryPhase( * It is the responsibility of the caller to ensure that the ref count is correctly decremented * when the object is no longer needed. */ - public void executeQueryPhase(QuerySearchRequest request, SearchShardTask task, ActionListener listener) { + public void executeQueryPhase( + QuerySearchRequest request, + SearchShardTask task, + ActionListener listener, + TransportVersion version + ) { + listener = maybeWrapListenerForStackTrace(listener, version, threadPool); final ReaderContext readerContext = findReaderContext(request.contextId(), request.shardSearchRequest()); final ShardSearchRequest shardSearchRequest = readerContext.getShardSearchRequest(request.shardSearchRequest()); final Releasable markAsUsed = readerContext.markAsUsed(getKeepAlive(shardSearchRequest)); diff --git a/server/src/test/java/org/elasticsearch/rest/RestResponseTests.java b/server/src/test/java/org/elasticsearch/rest/RestResponseTests.java index b85ad31288c8c..bd810cea216fc 100644 --- a/server/src/test/java/org/elasticsearch/rest/RestResponseTests.java +++ b/server/src/test/java/org/elasticsearch/rest/RestResponseTests.java @@ -51,6 +51,7 @@ import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; import static org.elasticsearch.ElasticsearchExceptionTests.assertDeepEquals; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.rest.RestController.ERROR_TRACE_DEFAULT; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -180,7 +181,7 @@ public void testStackTrace() throws IOException { } else { assertThat(response.status(), is(RestStatus.BAD_REQUEST)); } - boolean traceExists = request.paramAsBoolean("error_trace", false) && channel.detailedErrorsEnabled(); + boolean traceExists = request.paramAsBoolean("error_trace", ERROR_TRACE_DEFAULT) && channel.detailedErrorsEnabled(); if (traceExists) { assertThat(response.content().utf8ToString(), containsString(ElasticsearchException.STACK_TRACE)); } else { diff --git a/server/src/test/java/org/elasticsearch/search/SearchServiceSingleNodeTests.java b/server/src/test/java/org/elasticsearch/search/SearchServiceSingleNodeTests.java index 02593e41f5d84..0fc1694d39926 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchServiceSingleNodeTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchServiceSingleNodeTests.java @@ -2684,7 +2684,8 @@ public void testDfsQueryPhaseRewrite() { service.executeQueryPhase( new QuerySearchRequest(null, context.id(), request, new AggregatedDfs(Map.of(), Map.of(), 10)), new SearchShardTask(42L, "", "", "", null, emptyMap()), - plainActionFuture + plainActionFuture, + TransportVersion.current() ); plainActionFuture.actionGet(); diff --git a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java index 31bcab31ca8a7..d041121b8a96b 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java @@ -13,6 +13,8 @@ import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.SortField; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.cluster.metadata.IndexMetadata; @@ -53,9 +55,14 @@ import java.io.IOException; import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.function.Predicate; +import static org.elasticsearch.search.SearchService.maybeWrapListenerForStackTrace; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.not; + public class SearchServiceTests extends IndexShardTestCase { public void testCanMatchMatchAll() throws IOException { @@ -117,6 +124,33 @@ public Type getType() { doTestCanMatch(searchRequest, sortField, true, null, false); } + public void testMaybeWrapListenerForStackTrace() { + // Tests that the same listener has stack trace if is not wrapped or does not have stack trace if it is wrapped. + AtomicBoolean isWrapped = new AtomicBoolean(false); + ActionListener listener = new ActionListener<>() { + @Override + public void onResponse(SearchPhaseResult searchPhaseResult) { + // noop - we only care about failure scenarios + } + + @Override + public void onFailure(Exception e) { + if (isWrapped.get()) { + assertThat(e.getStackTrace().length, is(0)); + } else { + assertThat(e.getStackTrace().length, is(not(0))); + } + } + }; + Exception e = new Exception(); + e.fillInStackTrace(); + assertThat(e.getStackTrace().length, is(not(0))); + listener.onFailure(e); + listener = maybeWrapListenerForStackTrace(listener, TransportVersion.current(), threadPool); + isWrapped.set(true); + listener.onFailure(e); + } + private void doTestCanMatch( SearchRequest searchRequest, SortField sortField, diff --git a/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/AsyncSearchErrorTraceIT.java b/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/AsyncSearchErrorTraceIT.java new file mode 100644 index 0000000000000..39a6fa1e4b34f --- /dev/null +++ b/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/AsyncSearchErrorTraceIT.java @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.search; + +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.transport.TransportMessageListener; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xcontent.XContentType; +import org.junit.Before; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class AsyncSearchErrorTraceIT extends ESIntegTestCase { + + @Override + protected boolean addMockHttpTransport() { + return false; // enable http + } + + @Override + protected Collection> nodePlugins() { + return List.of(AsyncSearch.class); + } + + private AtomicBoolean transportMessageHasStackTrace; + + @Before + private void setupMessageListener() { + internalCluster().getDataNodeInstances(TransportService.class).forEach(ts -> { + ts.addMessageListener(new TransportMessageListener() { + @Override + public void onResponseSent(long requestId, String action, Exception error) { + TransportMessageListener.super.onResponseSent(requestId, action, error); + if (action.startsWith("indices:data/read/search")) { + Optional throwable = ExceptionsHelper.unwrapCausesAndSuppressed( + error, + t -> t.getStackTrace().length > 0 + ); + transportMessageHasStackTrace.set(throwable.isPresent()); + } + } + }); + }); + } + + private void setupIndexWithDocs() { + createIndex("test1", "test2"); + indexRandom( + true, + prepareIndex("test1").setId("1").setSource("field", "foo"), + prepareIndex("test2").setId("10").setSource("field", 5) + ); + refresh(); + } + + public void testAsyncSearchFailingQueryErrorTraceDefault() throws IOException, InterruptedException { + transportMessageHasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + Request searchRequest = new Request("POST", "/_async_search"); + searchRequest.setJsonEntity(""" + { + "query": { + "simple_query_string" : { + "query": "foo", + "fields": ["field"] + } + } + } + """); + searchRequest.addParameter("keep_on_completion", "true"); + searchRequest.addParameter("wait_for_completion_timeout", "0ms"); + Map responseEntity = performRequestAndGetResponseEntityAfterDelay(searchRequest, TimeValue.ZERO); + String asyncExecutionId = (String) responseEntity.get("id"); + Request request = new Request("GET", "/_async_search/" + asyncExecutionId); + while (responseEntity.get("is_running") instanceof Boolean isRunning && isRunning) { + responseEntity = performRequestAndGetResponseEntityAfterDelay(request, TimeValue.timeValueSeconds(1L)); + } + // check that the stack trace was not sent from the data node to the coordinating node + assertFalse(transportMessageHasStackTrace.get()); + } + + public void testAsyncSearchFailingQueryErrorTraceTrue() throws IOException, InterruptedException { + transportMessageHasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + Request searchRequest = new Request("POST", "/_async_search"); + searchRequest.setJsonEntity(""" + { + "query": { + "simple_query_string" : { + "query": "foo", + "fields": ["field"] + } + } + } + """); + searchRequest.addParameter("error_trace", "true"); + searchRequest.addParameter("keep_on_completion", "true"); + searchRequest.addParameter("wait_for_completion_timeout", "0ms"); + Map responseEntity = performRequestAndGetResponseEntityAfterDelay(searchRequest, TimeValue.ZERO); + String asyncExecutionId = (String) responseEntity.get("id"); + Request request = new Request("GET", "/_async_search/" + asyncExecutionId); + request.addParameter("error_trace", "true"); + while (responseEntity.get("is_running") instanceof Boolean isRunning && isRunning) { + responseEntity = performRequestAndGetResponseEntityAfterDelay(request, TimeValue.timeValueSeconds(1L)); + } + // check that the stack trace was sent from the data node to the coordinating node + assertTrue(transportMessageHasStackTrace.get()); + } + + public void testAsyncSearchFailingQueryErrorTraceFalse() throws IOException, InterruptedException { + transportMessageHasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + Request searchRequest = new Request("POST", "/_async_search"); + searchRequest.setJsonEntity(""" + { + "query": { + "simple_query_string" : { + "query": "foo", + "fields": ["field"] + } + } + } + """); + searchRequest.addParameter("error_trace", "false"); + searchRequest.addParameter("keep_on_completion", "true"); + searchRequest.addParameter("wait_for_completion_timeout", "0ms"); + Map responseEntity = performRequestAndGetResponseEntityAfterDelay(searchRequest, TimeValue.ZERO); + String asyncExecutionId = (String) responseEntity.get("id"); + Request request = new Request("GET", "/_async_search/" + asyncExecutionId); + request.addParameter("error_trace", "false"); + while (responseEntity.get("is_running") instanceof Boolean isRunning && isRunning) { + responseEntity = performRequestAndGetResponseEntityAfterDelay(request, TimeValue.timeValueSeconds(1L)); + } + // check that the stack trace was not sent from the data node to the coordinating node + assertFalse(transportMessageHasStackTrace.get()); + } + + public void testAsyncSearchFailingQueryErrorTraceFalseOnSubmitAndTrueOnGet() throws IOException, InterruptedException { + transportMessageHasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + Request searchRequest = new Request("POST", "/_async_search"); + searchRequest.setJsonEntity(""" + { + "query": { + "simple_query_string" : { + "query": "foo", + "fields": ["field"] + } + } + } + """); + searchRequest.addParameter("error_trace", "false"); + searchRequest.addParameter("keep_on_completion", "true"); + searchRequest.addParameter("wait_for_completion_timeout", "0ms"); + Map responseEntity = performRequestAndGetResponseEntityAfterDelay(searchRequest, TimeValue.ZERO); + String asyncExecutionId = (String) responseEntity.get("id"); + Request request = new Request("GET", "/_async_search/" + asyncExecutionId); + request.addParameter("error_trace", "true"); + while (responseEntity.get("is_running") instanceof Boolean isRunning && isRunning) { + responseEntity = performRequestAndGetResponseEntityAfterDelay(request, TimeValue.timeValueSeconds(1L)); + } + // check that the stack trace was not sent from the data node to the coordinating node + assertFalse(transportMessageHasStackTrace.get()); + } + + public void testAsyncSearchFailingQueryErrorTraceTrueOnSubmitAndFalseOnGet() throws IOException, InterruptedException { + transportMessageHasStackTrace = new AtomicBoolean(); + setupIndexWithDocs(); + + Request searchRequest = new Request("POST", "/_async_search"); + searchRequest.setJsonEntity(""" + { + "query": { + "simple_query_string" : { + "query": "foo", + "fields": ["field"] + } + } + } + """); + searchRequest.addParameter("error_trace", "true"); + searchRequest.addParameter("keep_on_completion", "true"); + searchRequest.addParameter("wait_for_completion_timeout", "0ms"); + Map responseEntity = performRequestAndGetResponseEntityAfterDelay(searchRequest, TimeValue.ZERO); + String asyncExecutionId = (String) responseEntity.get("id"); + Request request = new Request("GET", "/_async_search/" + asyncExecutionId); + request.addParameter("error_trace", "false"); + while (responseEntity.get("is_running") instanceof Boolean isRunning && isRunning) { + responseEntity = performRequestAndGetResponseEntityAfterDelay(request, TimeValue.timeValueSeconds(1L)); + } + // check that the stack trace was sent from the data node to the coordinating node + assertTrue(transportMessageHasStackTrace.get()); + } + + private Map performRequestAndGetResponseEntityAfterDelay(Request r, TimeValue sleep) throws IOException, + InterruptedException { + Thread.sleep(sleep.millis()); + Response response = getRestClient().performRequest(r); + XContentType entityContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue()); + return XContentHelper.convertToMap(entityContentType.xContent(), response.getEntity().getContent(), false); + } +} diff --git a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchAction.java b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchAction.java index bd09d8f7740a1..952febd46c34c 100644 --- a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchAction.java +++ b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchAction.java @@ -55,6 +55,9 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + if (client.threadPool() != null && client.threadPool().getThreadContext() != null) { + client.threadPool().getThreadContext().setErrorTraceTransportHeader(request); + } SubmitAsyncSearchRequest submit = new SubmitAsyncSearchRequest(); IntConsumer setSize = size -> submit.getSearchRequest().source().size(size); // for simplicity, we share parsing with ordinary search. That means a couple of unsupported parameters, like scroll diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rest/ServerSentEventsRestActionListener.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rest/ServerSentEventsRestActionListener.java index 3177474ea8ca6..bf94f072b6e04 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rest/ServerSentEventsRestActionListener.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rest/ServerSentEventsRestActionListener.java @@ -43,6 +43,7 @@ import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_CAUSE; import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; +import static org.elasticsearch.rest.RestController.ERROR_TRACE_DEFAULT; /** * A version of {@link org.elasticsearch.rest.action.RestChunkedToXContentListener} that reads from a {@link Flow.Publisher} and encodes @@ -161,7 +162,7 @@ private ChunkedToXContent errorChunk(Throwable t) { } var errorParams = p; - if (errorParams.paramAsBoolean("error_trace", false) && status != RestStatus.UNAUTHORIZED) { + if (errorParams.paramAsBoolean("error_trace", ERROR_TRACE_DEFAULT) && status != RestStatus.UNAUTHORIZED) { errorParams = new ToXContent.DelegatingMapParams( Map.of(REST_EXCEPTION_SKIP_STACK_TRACE, "false", REST_EXCEPTION_SKIP_CAUSE, "true"), params From 4e5b94ae033c10921a4a3ebb7274d89420d11d6b Mon Sep 17 00:00:00 2001 From: Salvatore Campagna <93581129+salvatore-campagna@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:38:42 +0100 Subject: [PATCH 078/119] feature: extend shard chanbes api to support data streams and aliases (#118937) Extends the `/{index}/ccr/shard_changes` API to accept data stream and alias names in addition to index names. This allows users to retrieve shard changes for data streams and indices accessed through aliases. When a data stream is provided, the API targets the **first backing index** for retrieving shard changes. Similarly, for an alias, the API targets the **first index** associated with the alias. --- .../xpack/ccr/rest/ShardChangesRestIT.java | 140 ++++++++++++++++-- .../ccr/rest/RestShardChangesAction.java | 112 ++++++++++---- 2 files changed, 214 insertions(+), 38 deletions(-) diff --git a/x-pack/plugin/ccr/src/javaRestTest/java/org/elasticsearch/xpack/ccr/rest/ShardChangesRestIT.java b/x-pack/plugin/ccr/src/javaRestTest/java/org/elasticsearch/xpack/ccr/rest/ShardChangesRestIT.java index e5dfea7b772f2..4c61904475093 100644 --- a/x-pack/plugin/ccr/src/javaRestTest/java/org/elasticsearch/xpack/ccr/rest/ShardChangesRestIT.java +++ b/x-pack/plugin/ccr/src/javaRestTest/java/org/elasticsearch/xpack/ccr/rest/ShardChangesRestIT.java @@ -26,6 +26,9 @@ import org.junit.ClassRule; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Locale; import java.util.Map; @@ -33,11 +36,14 @@ public class ShardChangesRestIT extends ESRestTestCase { private static final String CCR_SHARD_CHANGES_ENDPOINT = "/%s/ccr/shard_changes"; private static final String BULK_INDEX_ENDPOINT = "/%s/_bulk"; + private static final String DATA_STREAM_ENDPOINT = "/_data_stream/%s"; + private static final String INDEX_TEMPLATE_ENDPOINT = "/_index_template/%s"; private static final String[] SHARD_RESPONSE_FIELDS = new String[] { "took_in_millis", "operations", "shard_id", + "index_abstraction", "index", "settings_version", "max_seq_no_of_updates_or_deletes", @@ -46,6 +52,11 @@ public class ShardChangesRestIT extends ESRestTestCase { "aliases_version", "max_seq_no", "global_checkpoint" }; + + private static final String BULK_INDEX_TEMPLATE = """ + { "index": { "op_type": "create" } } + { "@timestamp": "%s", "name": "%s" } + """;; private static final String[] NAMES = { "skywalker", "leia", "obi-wan", "yoda", "chewbacca", "r2-d2", "c-3po", "darth-vader" }; @ClassRule public static ElasticsearchCluster cluster = ElasticsearchCluster.local() @@ -99,13 +110,86 @@ public void testShardChangesDefaultParams() throws IOException { createIndex(indexName, settings, mappings); assertTrue(indexExists(indexName)); - assertOK(client().performRequest(bulkRequest(indexName, randomIntBetween(10, 20)))); + assertOK(bulkIndex(indexName, randomIntBetween(10, 20))); final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); final Response response = client().performRequest(shardChangesRequest); assertOK(response); assertShardChangesResponse( - XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false) + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false), + indexName + ); + } + + public void testDataStreamShardChangesDefaultParams() throws IOException { + final String templateName = randomAlphanumericOfLength(8).toLowerCase(Locale.ROOT); + assertOK(createIndexTemplate(templateName, """ + { + "index_patterns": [ "test-*-*" ], + "data_stream": {}, + "priority": 100, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "name": { + "type": "keyword" + } + } + } + } + }""")); + + final String dataStreamName = "test-" + + randomAlphanumericOfLength(5).toLowerCase(Locale.ROOT) + + "-" + + randomAlphaOfLength(5).toLowerCase(Locale.ROOT); + assertOK(createDataStream(dataStreamName)); + + assertOK(bulkIndex(dataStreamName, randomIntBetween(10, 20))); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(dataStreamName)); + final Response response = client().performRequest(shardChangesRequest); + assertOK(response); + assertShardChangesResponse( + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false), + dataStreamName + ); + } + + public void testIndexAliasShardChangesDefaultParams() throws IOException { + final String indexName = randomAlphanumericOfLength(10).toLowerCase(Locale.ROOT); + final String aliasName = randomAlphanumericOfLength(8).toLowerCase(Locale.ROOT); + final Settings settings = Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexSettings.INDEX_TRANSLOG_SYNC_INTERVAL_SETTING.getKey(), "1s") + .build(); + final String mappings = """ + { + "properties": { + "name": { + "type": "keyword" + } + } + } + """; + createIndex(indexName, settings, mappings); + assertTrue(indexExists(indexName)); + + final Request putAliasRequest = new Request("PUT", "/" + indexName + "/_alias/" + aliasName); + assertOK(client().performRequest(putAliasRequest)); + + assertOK(bulkIndex(aliasName, randomIntBetween(10, 20))); + + final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(aliasName)); + final Response response = client().performRequest(shardChangesRequest); + assertOK(response); + assertShardChangesResponse( + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false), + aliasName ); } @@ -121,7 +205,7 @@ public void testShardChangesWithAllParameters() throws IOException { ); assertTrue(indexExists(indexName)); - assertOK(client().performRequest(bulkRequest(indexName, randomIntBetween(100, 200)))); + assertOK(bulkIndex(indexName, randomIntBetween(100, 200))); final Request shardChangesRequest = new Request("GET", shardChangesEndpoint(indexName)); shardChangesRequest.addParameter("from_seq_no", "0"); @@ -132,7 +216,8 @@ public void testShardChangesWithAllParameters() throws IOException { final Response response = client().performRequest(shardChangesRequest); assertOK(response); assertShardChangesResponse( - XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false) + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false), + indexName ); } @@ -148,7 +233,7 @@ public void testShardChangesMultipleRequests() throws IOException { ); assertTrue(indexExists(indexName)); - assertOK(client().performRequest(bulkRequest(indexName, randomIntBetween(100, 200)))); + assertOK(bulkIndex(indexName, randomIntBetween(100, 200))); final Request firstRequest = new Request("GET", shardChangesEndpoint(indexName)); firstRequest.addParameter("from_seq_no", "0"); @@ -159,7 +244,8 @@ public void testShardChangesMultipleRequests() throws IOException { final Response firstResponse = client().performRequest(firstRequest); assertOK(firstResponse); assertShardChangesResponse( - XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(firstResponse.getEntity()), false) + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(firstResponse.getEntity()), false), + indexName ); final Request secondRequest = new Request("GET", shardChangesEndpoint(indexName)); @@ -171,7 +257,8 @@ public void testShardChangesMultipleRequests() throws IOException { final Response secondResponse = client().performRequest(secondRequest); assertOK(secondResponse); assertShardChangesResponse( - XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(secondResponse.getEntity()), false) + XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(secondResponse.getEntity()), false), + indexName ); } @@ -231,17 +318,36 @@ public void testShardChangesMissingIndex() throws IOException { assertResponseException(ex, RestStatus.BAD_REQUEST, "Failed to process shard changes for index [" + indexName + "]"); } - private static Request bulkRequest(final String indexName, int numberOfDocuments) { + private static Response bulkIndex(final String indexName, int numberOfDocuments) throws IOException { final StringBuilder sb = new StringBuilder(); + long timestamp = System.currentTimeMillis(); for (int i = 0; i < numberOfDocuments; i++) { - sb.append(String.format(Locale.ROOT, "{ \"index\": { \"_id\": \"%d\" } }\n{ \"name\": \"%s\" }\n", i + 1, randomFrom(NAMES))); + sb.append( + String.format( + Locale.ROOT, + BULK_INDEX_TEMPLATE, + Instant.ofEpochMilli(timestamp).atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + randomFrom(NAMES) + ) + ); + timestamp += 1000; // 1 second } final Request request = new Request("POST", bulkEndpoint(indexName)); request.setJsonEntity(sb.toString()); request.addParameter("refresh", "true"); - return request; + return client().performRequest(request); + } + + private Response createDataStream(final String dataStreamName) throws IOException { + return client().performRequest(new Request("PUT", dataStreamEndpoint(dataStreamName))); + } + + private static Response createIndexTemplate(final String templateName, final String mappings) throws IOException { + final Request request = new Request("PUT", indexTemplateEndpoint(templateName)); + request.setJsonEntity(mappings); + return client().performRequest(request); } private static String shardChangesEndpoint(final String indexName) { @@ -252,16 +358,28 @@ private static String bulkEndpoint(final String indexName) { return String.format(Locale.ROOT, BULK_INDEX_ENDPOINT, indexName); } + private static String dataStreamEndpoint(final String dataStreamName) { + return String.format(Locale.ROOT, DATA_STREAM_ENDPOINT, dataStreamName); + } + + private static String indexTemplateEndpoint(final String templateName) { + return String.format(Locale.ROOT, INDEX_TEMPLATE_ENDPOINT, templateName); + } + private void assertResponseException(final ResponseException ex, final RestStatus restStatus, final String error) { assertEquals(restStatus.getStatus(), ex.getResponse().getStatusLine().getStatusCode()); assertThat(ex.getMessage(), Matchers.containsString(error)); } - private void assertShardChangesResponse(final Map shardChangesResponseBody) { + private void assertShardChangesResponse(final Map shardChangesResponseBody, final String indexAbstractionName) { for (final String fieldName : SHARD_RESPONSE_FIELDS) { final Object fieldValue = shardChangesResponseBody.get(fieldName); assertNotNull("Field " + fieldName + " is missing or has a null value.", fieldValue); + if ("index_abstraction".equals(fieldName)) { + assertEquals(indexAbstractionName, fieldValue); + } + if ("operations".equals(fieldName)) { if (fieldValue instanceof List operationsList) { assertFalse("Field 'operations' is empty.", operationsList.isEmpty()); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestShardChangesAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestShardChangesAction.java index 84171ebce162f..4a1d26d05a980 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestShardChangesAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestShardChangesAction.java @@ -10,6 +10,8 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.admin.indices.stats.ShardStats; import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; @@ -32,6 +34,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; +import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -42,10 +45,14 @@ import static org.elasticsearch.rest.RestRequest.Method.GET; /** - * A REST handler that retrieves shard changes in a specific index whose name is provided as a parameter. - * It handles GET requests to the "/{index}/ccr/shard_changes" endpoint retrieving shard-level changes, - * such as translog operations, mapping version, settings version, aliases version, the global checkpoint, - * maximum sequence number and maximum sequence number of updates or deletes. + * A REST handler that retrieves shard changes in a specific index, data stream or alias whose name is + * provided as a parameter. It handles GET requests to the "/{index}/ccr/shard_changes" endpoint retrieving + * shard-level changes, such as Translog operations, mapping version, settings version, aliases version, + * the global checkpoint, maximum sequence number and maximum sequence number of updates or deletes. + *

    + * In the case of a data stream, the first backing index is considered the target for retrieving shard changes. + * In the case of an alias, the first index that the alias points to is considered the target for retrieving + * shard changes. *

    * Note: This handler is only available for snapshot builds. */ @@ -84,32 +91,36 @@ public List routes() { */ @Override protected RestChannelConsumer prepareRequest(final RestRequest restRequest, final NodeClient client) throws IOException { - final var indexName = restRequest.param(INDEX_PARAM_NAME); + final var indexAbstractionName = restRequest.param(INDEX_PARAM_NAME); final var fromSeqNo = restRequest.paramAsLong(FROM_SEQ_NO_PARAM_NAME, DEFAULT_FROM_SEQ_NO); final var maxBatchSize = restRequest.paramAsSize(MAX_BATCH_SIZE_PARAM_NAME, DEFAULT_MAX_BATCH_SIZE); final var pollTimeout = restRequest.paramAsTime(POLL_TIMEOUT_PARAM_NAME, DEFAULT_POLL_TIMEOUT); final var maxOperationsCount = restRequest.paramAsInt(MAX_OPERATIONS_COUNT_PARAM_NAME, DEFAULT_MAX_OPERATIONS_COUNT); - final CompletableFuture indexUUIDCompletableFuture = asyncGetIndexUUID( + // NOTE: we first retrieve the concrete index name in case we are dealing with an alias or data stream. + // Then we use the concrete index name to retrieve the index UUID and shard stats. + final CompletableFuture indexNameCompletableFuture = asyncGetIndexName( client, - indexName, + indexAbstractionName, client.threadPool().executor(Ccr.CCR_THREAD_POOL_NAME) ); - final CompletableFuture shardStatsCompletableFuture = asyncShardStats( - client, - indexName, - client.threadPool().executor(Ccr.CCR_THREAD_POOL_NAME) + final CompletableFuture indexUUIDCompletableFuture = indexNameCompletableFuture.thenCompose( + concreteIndexName -> asyncGetIndexUUID(client, concreteIndexName, client.threadPool().executor(Ccr.CCR_THREAD_POOL_NAME)) + ); + final CompletableFuture shardStatsCompletableFuture = indexNameCompletableFuture.thenCompose( + concreteIndexName -> asyncShardStats(client, concreteIndexName, client.threadPool().executor(Ccr.CCR_THREAD_POOL_NAME)) ); return channel -> CompletableFuture.allOf(indexUUIDCompletableFuture, shardStatsCompletableFuture).thenRun(() -> { try { + final String concreteIndexName = indexNameCompletableFuture.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); final String indexUUID = indexUUIDCompletableFuture.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); final ShardStats shardStats = shardStatsCompletableFuture.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); final ShardId shardId = shardStats.getShardRouting().shardId(); final String expectedHistoryUUID = shardStats.getCommitStats().getUserData().get(Engine.HISTORY_UUID_KEY); final ShardChangesAction.Request shardChangesRequest = shardChangesRequest( - indexName, + concreteIndexName, indexUUID, shardId, expectedHistoryUUID, @@ -121,7 +132,12 @@ protected RestChannelConsumer prepareRequest(final RestRequest restRequest, fina client.execute(ShardChangesAction.INSTANCE, shardChangesRequest, new RestActionListener<>(channel) { @Override protected void processResponse(final ShardChangesAction.Response response) { - channel.sendResponse(new RestResponse(RestStatus.OK, shardChangesResponseToXContent(response, indexName, shardId))); + channel.sendResponse( + new RestResponse( + RestStatus.OK, + shardChangesResponseToXContent(response, indexAbstractionName, concreteIndexName, shardId) + ) + ); } }); @@ -132,7 +148,12 @@ protected void processResponse(final ShardChangesAction.Response response) { throw new IllegalStateException("Timeout while waiting for shard stats or index UUID", te); } }).exceptionally(ex -> { - channel.sendResponse(new RestResponse(RestStatus.BAD_REQUEST, "Failed to process shard changes for index [" + indexName + "]")); + channel.sendResponse( + new RestResponse( + RestStatus.BAD_REQUEST, + "Failed to process shard changes for index [" + indexAbstractionName + "] " + ex.getMessage() + ) + ); return null; }); } @@ -175,17 +196,20 @@ private static ShardChangesAction.Request shardChangesRequest( * Converts the response to XContent JSOn format. * * @param response The ShardChangesAction response. - * @param indexName The name of the index. + * @param indexAbstractionName The name of the index abstraction. + * @param concreteIndexName The name of the index. * @param shardId The ShardId. */ private static XContentBuilder shardChangesResponseToXContent( final ShardChangesAction.Response response, - final String indexName, + final String indexAbstractionName, + final String concreteIndexName, final ShardId shardId ) { try (XContentBuilder builder = XContentFactory.jsonBuilder()) { builder.startObject(); - builder.field("index", indexName); + builder.field("index_abstraction", indexAbstractionName); + builder.field("index", concreteIndexName); builder.field("shard_id", shardId); builder.field("mapping_version", response.getMappingVersion()); builder.field("settings_version", response.getSettingsVersion()); @@ -249,26 +273,60 @@ private static CompletableFuture supplyAsyncTask( }, executorService); } + /** + * Asynchronously retrieves the index name for a given index, alias or data stream. + * If the name represents a data stream, the name of the first backing index is returned. + * If the name represents an alias, the name of the first index that the alias points to is returned. + * + * @param client The NodeClient for executing the asynchronous request. + * @param indexAbstractionName The name of the index, alias or data stream. + * @return A CompletableFuture that completes with the retrieved index name. + */ + private static CompletableFuture asyncGetIndexName( + final NodeClient client, + final String indexAbstractionName, + final ExecutorService executorService + ) { + return supplyAsyncTask(() -> { + final ClusterState clusterState = client.admin() + .cluster() + .prepareState(new TimeValue(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + .get(GET_INDEX_UUID_TIMEOUT) + .getState(); + final IndexAbstraction indexAbstraction = clusterState.metadata().getIndicesLookup().get(indexAbstractionName); + if (indexAbstraction == null) { + throw new IllegalArgumentException( + String.format(Locale.ROOT, "Invalid index or data stream name [%s]", indexAbstractionName) + ); + } + if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM + || indexAbstraction.getType() == IndexAbstraction.Type.ALIAS) { + return indexAbstraction.getIndices().getFirst().getName(); + } + return indexAbstractionName; + }, executorService, "Error while retrieving index name for index or data stream [" + indexAbstractionName + "]"); + } + /** * Asynchronously retrieves the shard stats for a given index using an executor service. * * @param client The NodeClient for executing the asynchronous request. - * @param indexName The name of the index for which to retrieve shard statistics. + * @param concreteIndexName The name of the index for which to retrieve shard statistics. * @param executorService The executorService service for executing the asynchronous task. * @return A CompletableFuture that completes with the retrieved ShardStats. * @throws ElasticsearchException If an error occurs while retrieving shard statistics. */ private static CompletableFuture asyncShardStats( final NodeClient client, - final String indexName, + final String concreteIndexName, final ExecutorService executorService ) { return supplyAsyncTask( - () -> Arrays.stream(client.admin().indices().prepareStats(indexName).clear().get(SHARD_STATS_TIMEOUT).getShards()) + () -> Arrays.stream(client.admin().indices().prepareStats(concreteIndexName).clear().get(SHARD_STATS_TIMEOUT).getShards()) .max(Comparator.comparingLong(shardStats -> shardStats.getCommitStats().getGeneration())) - .orElseThrow(() -> new ElasticsearchException("Unable to retrieve shard stats for index: " + indexName)), + .orElseThrow(() -> new ElasticsearchException("Unable to retrieve shard stats for index: " + concreteIndexName)), executorService, - "Error while retrieving shard stats for index [" + indexName + "]" + "Error while retrieving shard stats for index [" + concreteIndexName + "]" ); } @@ -276,25 +334,25 @@ private static CompletableFuture asyncShardStats( * Asynchronously retrieves the index UUID for a given index using an executor service. * * @param client The NodeClient for executing the asynchronous request. - * @param indexName The name of the index for which to retrieve the index UUID. + * @param concreteIndexName The name of the index for which to retrieve the index UUID. * @param executorService The executorService service for executing the asynchronous task. * @return A CompletableFuture that completes with the retrieved index UUID. * @throws ElasticsearchException If an error occurs while retrieving the index UUID. */ private static CompletableFuture asyncGetIndexUUID( final NodeClient client, - final String indexName, + final String concreteIndexName, final ExecutorService executorService ) { return supplyAsyncTask( () -> client.admin() .indices() .prepareGetIndex() - .setIndices(indexName) + .setIndices(concreteIndexName) .get(GET_INDEX_UUID_TIMEOUT) - .getSetting(indexName, IndexMetadata.SETTING_INDEX_UUID), + .getSetting(concreteIndexName, IndexMetadata.SETTING_INDEX_UUID), executorService, - "Error while retrieving index UUID for index [" + indexName + "]" + "Error while retrieving index UUID for index [" + concreteIndexName + "]" ); } } From 31578be7502c267e11bed31af064b857bbd2d490 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 19 Dec 2024 01:43:17 +1100 Subject: [PATCH 079/119] Mute org.elasticsearch.repositories.blobstore.testkit.analyze.SecureHdfsRepositoryAnalysisRestIT org.elasticsearch.repositories.blobstore.testkit.analyze.SecureHdfsRepositoryAnalysisRestIT #118970 --- muted-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index b35a70391f195..6b433f232defd 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -295,6 +295,8 @@ tests: - class: org.elasticsearch.xpack.migrate.task.ReindexDataStreamStatusTests method: testEqualsAndHashcode issue: https://github.com/elastic/elasticsearch/issues/118965 +- class: org.elasticsearch.repositories.blobstore.testkit.analyze.SecureHdfsRepositoryAnalysisRestIT + issue: https://github.com/elastic/elasticsearch/issues/118970 # Examples: # From 1141edee4797f772a542528a296df2d802b50122 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 18 Dec 2024 15:49:53 +0100 Subject: [PATCH 080/119] Fix "create index with use_synthetic_source" yaml test. (#118946) The `flush` param defaults to `true` and some environments don't support that. Setting `flush` param to `false` to fix this. ``` [2024-12-18T19:51:01,213][INFO ][c.e.e.q.r.ServerlessClientYamlTestSuiteIT] [test] Stash dump on test failure [{ "stash" : { "body" : { "_shards" : { "total" : 3, "successful" : 0, "failed" : 3, "failures" : [ { "shard" : 0, "index" : "test", "status" : "BAD_REQUEST", "reason" : { "type" : "illegal_argument_exception", "reason" : "Search engine does not support acquiring last index commit with flush_first" } } ] } } } }] ``` --- .../test/indices.create/20_synthetic_source.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml index fc0bfa144bbc4..edb684168278b 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml @@ -2049,5 +2049,6 @@ create index with use_synthetic_source: indices.disk_usage: index: test run_expensive_tasks: true - - gt: { test.fields.field.total_in_bytes: 0 } - - is_false: test.fields.field._recovery_source + flush: false + - gt: { test.store_size_in_bytes: 0 } + - is_false: test.fields._recovery_source From e0f4b01fb8655c82e1b62dcb07973016ed411297 Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Wed, 18 Dec 2024 08:56:39 -0600 Subject: [PATCH 081/119] Fixing ReindexDataStreamStatusTests so that the in progress set max size is not less than min size (#118957) It is currently possible when we mutate a status object that we ask for a random set for the inProgress field where the max size of the set is less than the min size. This fixes that. --- .../migrate/task/ReindexDataStreamEnrichedStatusTests.java | 2 +- .../xpack/migrate/task/ReindexDataStreamStatusTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatusTests.java b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatusTests.java index acd8cd1a6add2..993db1096aac2 100644 --- a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatusTests.java +++ b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamEnrichedStatusTests.java @@ -74,7 +74,7 @@ private List randomList(int minSize) { } private Set randomSet(int minSize) { - return randomSet(minSize, 100, () -> randomAlphaOfLength(50)); + return randomSet(minSize, Math.max(minSize, 100), () -> randomAlphaOfLength(50)); } private List> randomErrorList() { diff --git a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatusTests.java b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatusTests.java index 47e2d02bee3b0..ad47eb6a23cd7 100644 --- a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatusTests.java +++ b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamStatusTests.java @@ -70,7 +70,7 @@ private List randomList(int minSize) { } private Set randomSet(int minSize) { - return randomSet(minSize, 100, () -> randomAlphaOfLength(50)); + return randomSet(minSize, Math.max(minSize, 100), () -> randomAlphaOfLength(50)); } private List> randomErrorList() { From 8a8dfe8f688ea0d1291f1bc5b82a3d1fe12c7c59 Mon Sep 17 00:00:00 2001 From: Sean Story Date: Wed, 18 Dec 2024 09:44:52 -0600 Subject: [PATCH 082/119] Delete accidentally added file, and gitignore (#118971) --- .gitignore | 3 +++ .java-version | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 .java-version diff --git a/.gitignore b/.gitignore index d1af97cbaea3b..8b2da4dc0832a 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,6 @@ testfixtures_shared/ # Generated checkstyle_ide.xml x-pack/plugin/esql/src/main/generated-src/generated/ + +# JEnv +.java-version diff --git a/.java-version b/.java-version deleted file mode 100644 index aabe6ec3909c9..0000000000000 --- a/.java-version +++ /dev/null @@ -1 +0,0 @@ -21 From cf73860b583e14c11eab28197b15b247236e4021 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Wed, 18 Dec 2024 15:57:12 +0000 Subject: [PATCH 083/119] Revert "Remove pre-7.2 token serialization support (#118057)" (#118967) * Revert "Remove pre-7.2 token serialization support (#118057)" This reverts commit ec66857ca13e2f5e7f9088a30aa48ea5ddab17fa. * Add missing constant --- .../org/elasticsearch/TransportVersions.java | 3 + .../security/SecurityFeatureSetUsage.java | 12 +- .../support/TokensInvalidationResult.java | 6 + .../security/authc/TokenAuthIntegTests.java | 37 +-- .../xpack/security/authc/TokenService.java | 236 ++++++++++++----- .../security/authc/TokenServiceTests.java | 241 +++++++++++++++++- 6 files changed, 447 insertions(+), 88 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 388123e86c882..fd8a3987cf4d3 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -52,7 +52,10 @@ static TransportVersion def(int id) { @UpdateForV9(owner = UpdateForV9.Owner.CORE_INFRA) // remove the transport versions with which v9 will not need to interact public static final TransportVersion ZERO = def(0); public static final TransportVersion V_7_0_0 = def(7_00_00_99); + public static final TransportVersion V_7_1_0 = def(7_01_00_99); + public static final TransportVersion V_7_2_0 = def(7_02_00_99); public static final TransportVersion V_7_3_0 = def(7_03_00_99); + public static final TransportVersion V_7_3_2 = def(7_03_02_99); public static final TransportVersion V_7_4_0 = def(7_04_00_99); public static final TransportVersion V_7_6_0 = def(7_06_00_99); public static final TransportVersion V_7_7_0 = def(7_07_00_99); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java index f44409daa37f8..3ebfad04a0f13 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java @@ -55,8 +55,10 @@ public SecurityFeatureSetUsage(StreamInput in) throws IOException { realmsUsage = in.readGenericMap(); rolesStoreUsage = in.readGenericMap(); sslUsage = in.readGenericMap(); - tokenServiceUsage = in.readGenericMap(); - apiKeyServiceUsage = in.readGenericMap(); + if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_2_0)) { + tokenServiceUsage = in.readGenericMap(); + apiKeyServiceUsage = in.readGenericMap(); + } auditUsage = in.readGenericMap(); ipFilterUsage = in.readGenericMap(); anonymousUsage = in.readGenericMap(); @@ -121,8 +123,10 @@ public void writeTo(StreamOutput out) throws IOException { out.writeGenericMap(realmsUsage); out.writeGenericMap(rolesStoreUsage); out.writeGenericMap(sslUsage); - out.writeGenericMap(tokenServiceUsage); - out.writeGenericMap(apiKeyServiceUsage); + if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_2_0)) { + out.writeGenericMap(tokenServiceUsage); + out.writeGenericMap(apiKeyServiceUsage); + } out.writeGenericMap(auditUsage); out.writeGenericMap(ipFilterUsage); out.writeGenericMap(anonymousUsage); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java index 59c16fc8a7a72..8fe018a825468 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java @@ -59,6 +59,9 @@ public TokensInvalidationResult(StreamInput in) throws IOException { this.invalidatedTokens = in.readStringCollectionAsList(); this.previouslyInvalidatedTokens = in.readStringCollectionAsList(); this.errors = in.readCollectionAsList(StreamInput::readException); + if (in.getTransportVersion().before(TransportVersions.V_7_2_0)) { + in.readVInt(); + } if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_0_0)) { this.restStatus = RestStatus.readFrom(in); } @@ -108,6 +111,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeStringCollection(invalidatedTokens); out.writeStringCollection(previouslyInvalidatedTokens); out.writeCollection(errors, StreamOutput::writeException); + if (out.getTransportVersion().before(TransportVersions.V_7_2_0)) { + out.writeVInt(5); + } if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_0_0)) { RestStatus.writeTo(out, restStatus); } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java index b56ea7ae3e456..fef1a98ca67e9 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java @@ -327,8 +327,8 @@ public void testInvalidateNotValidAccessTokens() throws Exception { ResponseException.class, () -> invalidateAccessToken( tokenService.prependVersionAndEncodeAccessToken( - TransportVersions.MINIMUM_COMPATIBLE, - tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, randomBoolean()).v1() + TransportVersions.V_7_3_2, + tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, randomBoolean()).v1() ) ) ); @@ -347,7 +347,7 @@ public void testInvalidateNotValidAccessTokens() throws Exception { byte[] longerAccessToken = new byte[randomIntBetween(17, 24)]; random().nextBytes(longerAccessToken); invalidateResponse = invalidateAccessToken( - tokenService.prependVersionAndEncodeAccessToken(TransportVersions.MINIMUM_COMPATIBLE, longerAccessToken) + tokenService.prependVersionAndEncodeAccessToken(TransportVersions.V_7_3_2, longerAccessToken) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0)); @@ -365,7 +365,7 @@ public void testInvalidateNotValidAccessTokens() throws Exception { byte[] shorterAccessToken = new byte[randomIntBetween(12, 15)]; random().nextBytes(shorterAccessToken); invalidateResponse = invalidateAccessToken( - tokenService.prependVersionAndEncodeAccessToken(TransportVersions.MINIMUM_COMPATIBLE, shorterAccessToken) + tokenService.prependVersionAndEncodeAccessToken(TransportVersions.V_7_3_2, shorterAccessToken) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0)); @@ -394,8 +394,8 @@ public void testInvalidateNotValidAccessTokens() throws Exception { invalidateResponse = invalidateAccessToken( tokenService.prependVersionAndEncodeAccessToken( - TransportVersions.MINIMUM_COMPATIBLE, - tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, randomBoolean()).v1() + TransportVersions.V_7_3_2, + tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, randomBoolean()).v1() ) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); @@ -420,8 +420,8 @@ public void testInvalidateNotValidRefreshTokens() throws Exception { ResponseException.class, () -> invalidateRefreshToken( TokenService.prependVersionAndEncodeRefreshToken( - TransportVersions.MINIMUM_COMPATIBLE, - tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, true).v2() + TransportVersions.V_7_3_2, + tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, true).v2() ) ) ); @@ -441,7 +441,7 @@ public void testInvalidateNotValidRefreshTokens() throws Exception { byte[] longerRefreshToken = new byte[randomIntBetween(17, 24)]; random().nextBytes(longerRefreshToken); invalidateResponse = invalidateRefreshToken( - TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.MINIMUM_COMPATIBLE, longerRefreshToken) + TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.V_7_3_2, longerRefreshToken) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0)); @@ -459,7 +459,7 @@ public void testInvalidateNotValidRefreshTokens() throws Exception { byte[] shorterRefreshToken = new byte[randomIntBetween(12, 15)]; random().nextBytes(shorterRefreshToken); invalidateResponse = invalidateRefreshToken( - TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.MINIMUM_COMPATIBLE, shorterRefreshToken) + TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.V_7_3_2, shorterRefreshToken) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0)); @@ -488,8 +488,8 @@ public void testInvalidateNotValidRefreshTokens() throws Exception { invalidateResponse = invalidateRefreshToken( TokenService.prependVersionAndEncodeRefreshToken( - TransportVersions.MINIMUM_COMPATIBLE, - tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, true).v2() + TransportVersions.V_7_3_2, + tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, true).v2() ) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); @@ -758,11 +758,18 @@ public void testAuthenticateWithWrongToken() throws Exception { assertAuthenticateWithToken(response.accessToken(), TEST_USER_NAME); // Now attempt to authenticate with an invalid access token string assertUnauthorizedToken(randomAlphaOfLengthBetween(0, 128)); - // Now attempt to authenticate with an invalid access token with valid structure (after 8.0 pre 8.10) + // Now attempt to authenticate with an invalid access token with valid structure (pre 7.2) assertUnauthorizedToken( tokenService.prependVersionAndEncodeAccessToken( - TransportVersions.V_8_0_0, - tokenService.getRandomTokenBytes(TransportVersions.V_8_0_0, randomBoolean()).v1() + TransportVersions.V_7_1_0, + tokenService.getRandomTokenBytes(TransportVersions.V_7_1_0, randomBoolean()).v1() + ) + ); + // Now attempt to authenticate with an invalid access token with valid structure (after 7.2 pre 8.10) + assertUnauthorizedToken( + tokenService.prependVersionAndEncodeAccessToken( + TransportVersions.V_7_4_0, + tokenService.getRandomTokenBytes(TransportVersions.V_7_4_0, randomBoolean()).v1() ) ); // Now attempt to authenticate with an invalid access token with valid structure (current version) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 900436a1fd874..4f7ba7808b823 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -48,7 +48,9 @@ import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.InputStreamStreamInput; +import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -57,6 +59,7 @@ import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Streams; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; @@ -90,8 +93,10 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; +import java.io.OutputStream; import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -127,6 +132,7 @@ import javax.crypto.Cipher; import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; @@ -195,8 +201,14 @@ public class TokenService { // UUIDs are 16 bytes encoded base64 without padding, therefore the length is (16 / 3) * 4 + ((16 % 3) * 8 + 5) / 6 chars private static final int TOKEN_LENGTH = 22; private static final String TOKEN_DOC_ID_PREFIX = TOKEN_DOC_TYPE + "_"; + static final int LEGACY_MINIMUM_BYTES = VERSION_BYTES + SALT_BYTES + IV_BYTES + 1; static final int MINIMUM_BYTES = VERSION_BYTES + TOKEN_LENGTH + 1; + static final int LEGACY_MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * LEGACY_MINIMUM_BYTES) / 3)).intValue(); public static final int MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * MINIMUM_BYTES) / 3)).intValue(); + static final TransportVersion VERSION_HASHED_TOKENS = TransportVersions.V_7_2_0; + static final TransportVersion VERSION_TOKENS_INDEX_INTRODUCED = TransportVersions.V_7_2_0; + static final TransportVersion VERSION_ACCESS_TOKENS_AS_UUIDS = TransportVersions.V_7_2_0; + static final TransportVersion VERSION_MULTIPLE_CONCURRENT_REFRESHES = TransportVersions.V_7_2_0; static final TransportVersion VERSION_CLIENT_AUTH_FOR_REFRESH = TransportVersions.V_8_2_0; static final TransportVersion VERSION_GET_TOKEN_DOC_FOR_REFRESH = TransportVersions.V_8_10_X; @@ -261,7 +273,8 @@ public TokenService( /** * Creates an access token and optionally a refresh token as well, based on the provided authentication and metadata with - * auto-generated values. The created tokens are stored a specific security tokens index. + * auto-generated values. The created tokens are stored in the security index for versions up to + * {@link #VERSION_TOKENS_INDEX_INTRODUCED} and to a specific security tokens index for later versions. */ public void createOAuth2Tokens( Authentication authentication, @@ -278,7 +291,8 @@ public void createOAuth2Tokens( /** * Creates an access token and optionally a refresh token as well from predefined values, based on the provided authentication and - * metadata. The created tokens are stored in a specific security tokens index. + * metadata. The created tokens are stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED} and to a + * specific security tokens index for later versions. */ // public for testing public void createOAuth2Tokens( @@ -300,15 +314,21 @@ public void createOAuth2Tokens( * * @param accessTokenBytes The predefined seed value for the access token. This will then be *

    * @param refreshTokenBytes The predefined seed value for the access token. This will then be *
      - *
    • Hashed before stored
    • - *
    • Stored in a specific security tokens index
    • - *
    • Prepended with a version ID and Base64 encoded before returned to the caller of the APIs
    • + *
    • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
    • + *
    • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
    • + *
    • Stored in a specific security tokens index for versions after + * {@link #VERSION_TOKENS_INDEX_INTRODUCED}
    • + *
    • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs + * for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
    • *
    * @param tokenVersion The version of the nodes with which these tokens will be compatible. * @param authentication The authentication object representing the user for which the tokens are created @@ -364,7 +384,7 @@ private void createOAuth2Tokens( } else { refreshTokenToStore = refreshTokenToReturn = null; } - } else { + } else if (tokenVersion.onOrAfter(VERSION_HASHED_TOKENS)) { assert accessTokenBytes.length == RAW_TOKEN_BYTES_LENGTH; userTokenId = hashTokenString(Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes)); accessTokenToStore = null; @@ -375,6 +395,18 @@ private void createOAuth2Tokens( } else { refreshTokenToStore = refreshTokenToReturn = null; } + } else { + assert accessTokenBytes.length == RAW_TOKEN_BYTES_LENGTH; + userTokenId = Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes); + accessTokenToStore = null; + if (refreshTokenBytes != null) { + assert refreshTokenBytes.length == RAW_TOKEN_BYTES_LENGTH; + refreshTokenToStore = refreshTokenToReturn = Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString( + refreshTokenBytes + ); + } else { + refreshTokenToStore = refreshTokenToReturn = null; + } } UserToken userToken = new UserToken(userTokenId, tokenVersion, tokenAuth, getExpirationTime(), metadata); tokenDocument = createTokenDocument(userToken, accessTokenToStore, refreshTokenToStore, originatingClientAuth); @@ -387,22 +419,23 @@ private void createOAuth2Tokens( final RefreshPolicy tokenCreationRefreshPolicy = tokenVersion.onOrAfter(VERSION_GET_TOKEN_DOC_FOR_REFRESH) ? RefreshPolicy.NONE : RefreshPolicy.WAIT_UNTIL; + final SecurityIndexManager tokensIndex = getTokensIndexForVersion(tokenVersion); logger.debug( () -> format( "Using refresh policy [%s] when creating token doc [%s] in the security index [%s]", tokenCreationRefreshPolicy, documentId, - securityTokensIndex.aliasName() + tokensIndex.aliasName() ) ); - final IndexRequest indexTokenRequest = client.prepareIndex(securityTokensIndex.aliasName()) + final IndexRequest indexTokenRequest = client.prepareIndex(tokensIndex.aliasName()) .setId(documentId) .setOpType(OpType.CREATE) .setSource(tokenDocument, XContentType.JSON) .setRefreshPolicy(tokenCreationRefreshPolicy) .request(); - securityTokensIndex.prepareIndexIfNeededThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + securityTokensIndex.aliasName() + "]", documentId, ex)), + tokensIndex.prepareIndexIfNeededThenExecute( + ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", documentId, ex)), () -> executeAsyncWithOrigin( client, SECURITY_ORIGIN, @@ -521,16 +554,17 @@ private void getTokenDocById( @Nullable String storedRefreshToken, ActionListener listener ) { - final SecurityIndexManager frozenTokensIndex = securityTokensIndex.defensiveCopy(); + final SecurityIndexManager tokensIndex = getTokensIndexForVersion(tokenVersion); + final SecurityIndexManager frozenTokensIndex = tokensIndex.defensiveCopy(); if (frozenTokensIndex.isAvailable(PRIMARY_SHARDS) == false) { - logger.warn("failed to get access token [{}] because index [{}] is not available", tokenId, securityTokensIndex.aliasName()); + logger.warn("failed to get access token [{}] because index [{}] is not available", tokenId, tokensIndex.aliasName()); listener.onFailure(frozenTokensIndex.getUnavailableReason(PRIMARY_SHARDS)); return; } - final GetRequest getRequest = client.prepareGet(securityTokensIndex.aliasName(), getTokenDocumentId(tokenId)).request(); + final GetRequest getRequest = client.prepareGet(tokensIndex.aliasName(), getTokenDocumentId(tokenId)).request(); final Consumer onFailure = ex -> listener.onFailure(traceLog("get token from id", tokenId, ex)); - securityTokensIndex.checkIndexVersionThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + securityTokensIndex.aliasName() + "]", tokenId, ex)), + tokensIndex.checkIndexVersionThenExecute( + ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", tokenId, ex)), () -> executeAsyncWithOrigin( client.threadPool().getThreadContext(), SECURITY_ORIGIN, @@ -576,11 +610,7 @@ private void getTokenDocById( // if the index or the shard is not there / available we assume that // the token is not valid if (isShardNotAvailableException(e)) { - logger.warn( - "failed to get token doc [{}] because index [{}] is not available", - tokenId, - securityTokensIndex.aliasName() - ); + logger.warn("failed to get token doc [{}] because index [{}] is not available", tokenId, tokensIndex.aliasName()); } else { logger.error(() -> "failed to get token doc [" + tokenId + "]", e); } @@ -620,7 +650,7 @@ void decodeToken(String token, boolean validateUserToken, ActionListener VERSION_ACCESS_TOKENS_UUIDS cluster if (in.available() < MINIMUM_BYTES) { logger.debug("invalid token, smaller than [{}] bytes", MINIMUM_BYTES); @@ -630,6 +660,41 @@ void decodeToken(String token, boolean validateUserToken, ActionListener { + if (decodeKey != null) { + try { + final Cipher cipher = getDecryptionCipher(iv, decodeKey, version, decodedSalt); + final String tokenId = decryptTokenId(encryptedTokenId, cipher, version); + getAndValidateUserToken(tokenId, version, null, validateUserToken, listener); + } catch (IOException | GeneralSecurityException e) { + // could happen with a token that is not ours + logger.warn("invalid token", e); + listener.onResponse(null); + } + } else { + // could happen with a token that is not ours + listener.onResponse(null); + } + }, listener::onFailure)); + } else { + logger.debug(() -> format("invalid key %s key: %s", passphraseHash, keyCache.cache.keySet())); + listener.onResponse(null); + } } } catch (Exception e) { // could happen with a token that is not ours @@ -787,7 +852,11 @@ private void indexInvalidation( final Set idsOfOlderTokens = new HashSet<>(); boolean anyOlderTokensBeforeRefreshViaGet = false; for (UserToken userToken : userTokens) { - idsOfRecentTokens.add(userToken.getId()); + if (userToken.getTransportVersion().onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) { + idsOfRecentTokens.add(userToken.getId()); + } else { + idsOfOlderTokens.add(userToken.getId()); + } anyOlderTokensBeforeRefreshViaGet |= userToken.getTransportVersion().before(VERSION_GET_TOKEN_DOC_FOR_REFRESH); } final RefreshPolicy tokensInvalidationRefreshPolicy = anyOlderTokensBeforeRefreshViaGet @@ -1055,7 +1124,7 @@ private void findTokenFromRefreshToken(String refreshToken, Iterator ); getTokenDocById(userTokenId, version, null, storedRefreshToken, listener); } - } else { + } else if (version.onOrAfter(VERSION_HASHED_TOKENS)) { final String unencodedRefreshToken = in.readString(); if (unencodedRefreshToken.length() != TOKEN_LENGTH) { logger.debug("Decoded refresh token [{}] with version [{}] is invalid.", unencodedRefreshToken, version); @@ -1064,6 +1133,9 @@ private void findTokenFromRefreshToken(String refreshToken, Iterator final String hashedRefreshToken = hashTokenString(unencodedRefreshToken); findTokenFromRefreshToken(hashedRefreshToken, securityTokensIndex, backoff, listener); } + } else { + logger.debug("Unrecognized refresh token version [{}].", version); + listener.onResponse(null); } } catch (IOException e) { logger.debug(() -> "Could not decode refresh token [" + refreshToken + "].", e); @@ -1178,6 +1250,7 @@ private void innerRefresh( return; } final RefreshTokenStatus refreshTokenStatus = checkRefreshResult.v1(); + final SecurityIndexManager refreshedTokenIndex = getTokensIndexForVersion(refreshTokenStatus.getTransportVersion()); if (refreshTokenStatus.isRefreshed()) { logger.debug( "Token document [{}] was recently refreshed, when a new token document was generated. Reusing that result.", @@ -1185,29 +1258,31 @@ private void innerRefresh( ); final Tuple parsedTokens = parseTokensFromDocument(tokenDoc.sourceAsMap(), null); Authentication authentication = parsedTokens.v1().getAuthentication(); - decryptAndReturnSupersedingTokens(refreshToken, refreshTokenStatus, securityTokensIndex, authentication, listener); + decryptAndReturnSupersedingTokens(refreshToken, refreshTokenStatus, refreshedTokenIndex, authentication, listener); } else { final TransportVersion newTokenVersion = getTokenVersionCompatibility(); final Tuple newTokenBytes = getRandomTokenBytes(newTokenVersion, true); final Map updateMap = new HashMap<>(); updateMap.put("refreshed", true); - updateMap.put("refresh_time", clock.instant().toEpochMilli()); - try { - final byte[] iv = getRandomBytes(IV_BYTES); - final byte[] salt = getRandomBytes(SALT_BYTES); - String encryptedAccessAndRefreshToken = encryptSupersedingTokens( - newTokenBytes.v1(), - newTokenBytes.v2(), - refreshToken, - iv, - salt - ); - updateMap.put("superseding.encrypted_tokens", encryptedAccessAndRefreshToken); - updateMap.put("superseding.encryption_iv", Base64.getEncoder().encodeToString(iv)); - updateMap.put("superseding.encryption_salt", Base64.getEncoder().encodeToString(salt)); - } catch (GeneralSecurityException e) { - logger.warn("could not encrypt access token and refresh token string", e); - onFailure.accept(invalidGrantException("could not refresh the requested token")); + if (newTokenVersion.onOrAfter(VERSION_MULTIPLE_CONCURRENT_REFRESHES)) { + updateMap.put("refresh_time", clock.instant().toEpochMilli()); + try { + final byte[] iv = getRandomBytes(IV_BYTES); + final byte[] salt = getRandomBytes(SALT_BYTES); + String encryptedAccessAndRefreshToken = encryptSupersedingTokens( + newTokenBytes.v1(), + newTokenBytes.v2(), + refreshToken, + iv, + salt + ); + updateMap.put("superseding.encrypted_tokens", encryptedAccessAndRefreshToken); + updateMap.put("superseding.encryption_iv", Base64.getEncoder().encodeToString(iv)); + updateMap.put("superseding.encryption_salt", Base64.getEncoder().encodeToString(salt)); + } catch (GeneralSecurityException e) { + logger.warn("could not encrypt access token and refresh token string", e); + onFailure.accept(invalidGrantException("could not refresh the requested token")); + } } assert tokenDoc.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO : "expected an assigned sequence number"; assert tokenDoc.primaryTerm() != SequenceNumbers.UNASSIGNED_PRIMARY_TERM : "expected an assigned primary term"; @@ -1218,17 +1293,17 @@ private void innerRefresh( "Using refresh policy [%s] when updating token doc [%s] for refresh in the security index [%s]", tokenRefreshUpdateRefreshPolicy, tokenDoc.id(), - securityTokensIndex.aliasName() + refreshedTokenIndex.aliasName() ) ); - final UpdateRequestBuilder updateRequest = client.prepareUpdate(securityTokensIndex.aliasName(), tokenDoc.id()) + final UpdateRequestBuilder updateRequest = client.prepareUpdate(refreshedTokenIndex.aliasName(), tokenDoc.id()) .setDoc("refresh_token", updateMap) .setFetchSource(logger.isDebugEnabled()) .setRefreshPolicy(tokenRefreshUpdateRefreshPolicy) .setIfSeqNo(tokenDoc.seqNo()) .setIfPrimaryTerm(tokenDoc.primaryTerm()); - securityTokensIndex.prepareIndexIfNeededThenExecute( - ex -> listener.onFailure(traceLog("prepare index [" + securityTokensIndex.aliasName() + "]", ex)), + refreshedTokenIndex.prepareIndexIfNeededThenExecute( + ex -> listener.onFailure(traceLog("prepare index [" + refreshedTokenIndex.aliasName() + "]", ex)), () -> executeAsyncWithOrigin( client.threadPool().getThreadContext(), SECURITY_ORIGIN, @@ -1274,7 +1349,7 @@ private void innerRefresh( if (cause instanceof VersionConflictEngineException) { // The document has been updated by another thread, get it again. logger.debug("version conflict while updating document [{}], attempting to get it again", tokenDoc.id()); - getTokenDocAsync(tokenDoc.id(), securityTokensIndex, true, new ActionListener<>() { + getTokenDocAsync(tokenDoc.id(), refreshedTokenIndex, true, new ActionListener<>() { @Override public void onResponse(GetResponse response) { if (response.isExists()) { @@ -1293,7 +1368,7 @@ public void onFailure(Exception e) { logger.info("could not get token document [{}] for refresh, retrying", tokenDoc.id()); client.threadPool() .schedule( - () -> getTokenDocAsync(tokenDoc.id(), securityTokensIndex, true, this), + () -> getTokenDocAsync(tokenDoc.id(), refreshedTokenIndex, true, this), backoff.next(), client.threadPool().generic() ); @@ -1614,13 +1689,17 @@ private static Optional checkMultipleRefreshes( RefreshTokenStatus refreshTokenStatus ) { if (refreshTokenStatus.isRefreshed()) { - if (refreshRequested.isAfter(refreshTokenStatus.getRefreshInstant().plus(30L, ChronoUnit.SECONDS))) { - return Optional.of(invalidGrantException("token has already been refreshed more than 30 seconds in the past")); - } - if (refreshRequested.isBefore(refreshTokenStatus.getRefreshInstant().minus(30L, ChronoUnit.SECONDS))) { - return Optional.of( - invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great") - ); + if (refreshTokenStatus.getTransportVersion().onOrAfter(VERSION_MULTIPLE_CONCURRENT_REFRESHES)) { + if (refreshRequested.isAfter(refreshTokenStatus.getRefreshInstant().plus(30L, ChronoUnit.SECONDS))) { + return Optional.of(invalidGrantException("token has already been refreshed more than 30 seconds in the past")); + } + if (refreshRequested.isBefore(refreshTokenStatus.getRefreshInstant().minus(30L, ChronoUnit.SECONDS))) { + return Optional.of( + invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great") + ); + } + } else { + return Optional.of(invalidGrantException("token has already been refreshed")); } } return Optional.empty(); @@ -1900,6 +1979,21 @@ private void ensureEnabled() { } } + /** + * In version {@code #VERSION_TOKENS_INDEX_INTRODUCED} security tokens were moved into a separate index, away from the other entities in + * the main security index, due to their ephemeral nature. They moved "seamlessly" - without manual user intervention. In this way, new + * tokens are created in the new index, while the existing ones were left in place - to be accessed from the old index - and due to be + * removed automatically by the {@code ExpiredTokenRemover} periodic job. Therefore, in general, when searching for a token we need to + * consider both the new and the old indices. + */ + private SecurityIndexManager getTokensIndexForVersion(TransportVersion version) { + if (version.onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) { + return securityTokensIndex; + } else { + return securityMainIndex; + } + } + public TimeValue getExpirationDelay() { return expirationDelay; } @@ -1928,13 +2022,41 @@ public String prependVersionAndEncodeAccessToken(TransportVersion version, byte[ out.writeByteArray(accessTokenBytes); return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); } - } else { + } else if (version.onOrAfter(VERSION_ACCESS_TOKENS_AS_UUIDS)) { try (BytesStreamOutput out = new BytesStreamOutput(MINIMUM_BASE64_BYTES)) { out.setTransportVersion(version); TransportVersion.writeVersion(version, out); out.writeString(Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes)); return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); } + } else { + // we know that the minimum length is larger than the default of the ByteArrayOutputStream so set the size to this explicitly + try ( + ByteArrayOutputStream os = new ByteArrayOutputStream(LEGACY_MINIMUM_BASE64_BYTES); + OutputStream base64 = Base64.getEncoder().wrap(os); + StreamOutput out = new OutputStreamStreamOutput(base64) + ) { + out.setTransportVersion(version); + KeyAndCache keyAndCache = keyCache.activeKeyCache; + TransportVersion.writeVersion(version, out); + out.writeByteArray(keyAndCache.getSalt().bytes); + out.writeByteArray(keyAndCache.getKeyHash().bytes); + final byte[] initializationVector = getRandomBytes(IV_BYTES); + out.writeByteArray(initializationVector); + try ( + CipherOutputStream encryptedOutput = new CipherOutputStream( + out, + getEncryptionCipher(initializationVector, keyAndCache, version) + ); + StreamOutput encryptedStreamOutput = new OutputStreamStreamOutput(encryptedOutput) + ) { + encryptedStreamOutput.setTransportVersion(version); + encryptedStreamOutput.writeString(Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes)); + // StreamOutput needs to be closed explicitly because it wraps CipherOutputStream + encryptedStreamOutput.close(); + return new String(os.toByteArray(), StandardCharsets.UTF_8); + } + } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index 702af75141093..75c2507a1dc5f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -126,6 +126,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -147,6 +148,7 @@ public class TokenServiceTests extends ESTestCase { private SecurityIndexManager securityMainIndex; private SecurityIndexManager securityTokensIndex; private ClusterService clusterService; + private DiscoveryNode pre72OldNode; private DiscoveryNode pre8500040OldNode; private Settings tokenServiceEnabledSettings = Settings.builder() .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true) @@ -226,12 +228,31 @@ public void setupClient() { licenseState = mock(MockLicenseState.class); when(licenseState.isAllowed(Security.TOKEN_SERVICE_FEATURE)).thenReturn(true); + if (randomBoolean()) { + // version 7.2 was an "inflection" point in the Token Service development (access_tokens as UUIDS, multiple concurrent + // refreshes, + // tokens docs on a separate index) + pre72OldNode = addAnother7071DataNode(this.clusterService); + } if (randomBoolean()) { // before refresh tokens used GET, i.e. TokenService#VERSION_GET_TOKEN_DOC_FOR_REFRESH pre8500040OldNode = addAnotherPre8500DataNode(this.clusterService); } } + private static DiscoveryNode addAnother7071DataNode(ClusterService clusterService) { + Version version; + TransportVersion transportVersion; + if (randomBoolean()) { + version = Version.V_7_0_0; + transportVersion = TransportVersions.V_7_0_0; + } else { + version = Version.V_7_1_0; + transportVersion = TransportVersions.V_7_1_0; + } + return addAnotherDataNodeWithVersion(clusterService, version, transportVersion); + } + private static DiscoveryNode addAnotherPre8500DataNode(ClusterService clusterService) { Version version; TransportVersion transportVersion; @@ -280,6 +301,53 @@ public static void shutdownThreadpool() { threadPool = null; } + public void testAttachAndGetToken() throws Exception { + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + // This test only makes sense in mixed clusters with pre v7.2.0 nodes where the Token Service Key is used (to encrypt tokens) + if (null == pre72OldNode) { + pre72OldNode = addAnother7071DataNode(this.clusterService); + } + Authentication authentication = AuthenticationTestHelper.builder() + .user(new User("joe", "admin")) + .realmRef(new RealmRef("native_realm", "native", "node1")) + .build(false); + PlainActionFuture tokenFuture = new PlainActionFuture<>(); + Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean()); + tokenService.createOAuth2Tokens( + newTokenBytes.v1(), + newTokenBytes.v2(), + authentication, + authentication, + Collections.emptyMap(), + tokenFuture + ); + final String accessToken = tokenFuture.get().getAccessToken(); + assertNotNull(accessToken); + mockGetTokenFromAccessTokenBytes(tokenService, newTokenBytes.v1(), authentication, false, null); + + ThreadContext requestContext = new ThreadContext(Settings.EMPTY); + requestContext.putHeader("Authorization", randomFrom("Bearer ", "BEARER ", "bearer ") + accessToken); + + try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { + PlainActionFuture future = new PlainActionFuture<>(); + final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); + UserToken serialized = future.get(); + assertAuthentication(authentication, serialized.getAuthentication()); + } + + try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { + // verify a second separate token service with its own salt can also verify + TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + anotherService.refreshMetadata(tokenService.getTokenMetadata()); + PlainActionFuture future = new PlainActionFuture<>(); + final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); + anotherService.tryAuthenticateToken(bearerToken, future); + UserToken fromOtherService = future.get(); + assertAuthentication(authentication, fromOtherService.getAuthentication()); + } + } + public void testInvalidAuthorizationHeader() throws Exception { TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); @@ -296,6 +364,89 @@ public void testInvalidAuthorizationHeader() throws Exception { } } + public void testPassphraseWorks() throws Exception { + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + // This test only makes sense in mixed clusters with pre v7.1.0 nodes where the Key is actually used + if (null == pre72OldNode) { + pre72OldNode = addAnother7071DataNode(this.clusterService); + } + Authentication authentication = AuthenticationTestHelper.builder() + .user(new User("joe", "admin")) + .realmRef(new RealmRef("native_realm", "native", "node1")) + .build(false); + PlainActionFuture tokenFuture = new PlainActionFuture<>(); + Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean()); + tokenService.createOAuth2Tokens( + newTokenBytes.v1(), + newTokenBytes.v2(), + authentication, + authentication, + Collections.emptyMap(), + tokenFuture + ); + final String accessToken = tokenFuture.get().getAccessToken(); + assertNotNull(accessToken); + mockGetTokenFromAccessTokenBytes(tokenService, newTokenBytes.v1(), authentication, false, null); + + ThreadContext requestContext = new ThreadContext(Settings.EMPTY); + storeTokenHeader(requestContext, accessToken); + + try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { + PlainActionFuture future = new PlainActionFuture<>(); + final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); + UserToken serialized = future.get(); + assertAuthentication(authentication, serialized.getAuthentication()); + } + + try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { + // verify a second separate token service with its own passphrase cannot verify + TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + PlainActionFuture future = new PlainActionFuture<>(); + final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); + anotherService.tryAuthenticateToken(bearerToken, future); + assertNull(future.get()); + } + } + + public void testGetTokenWhenKeyCacheHasExpired() throws Exception { + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + // This test only makes sense in mixed clusters with pre v7.1.0 nodes where the Key is actually used + if (null == pre72OldNode) { + pre72OldNode = addAnother7071DataNode(this.clusterService); + } + Authentication authentication = AuthenticationTestHelper.builder() + .user(new User("joe", "admin")) + .realmRef(new RealmRef("native_realm", "native", "node1")) + .build(false); + + PlainActionFuture tokenFuture = new PlainActionFuture<>(); + Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean()); + tokenService.createOAuth2Tokens( + newTokenBytes.v1(), + newTokenBytes.v2(), + authentication, + authentication, + Collections.emptyMap(), + tokenFuture + ); + String accessToken = tokenFuture.get().getAccessToken(); + assertThat(accessToken, notNullValue()); + + tokenService.clearActiveKeyCache(); + + tokenService.createOAuth2Tokens( + newTokenBytes.v1(), + newTokenBytes.v2(), + authentication, + authentication, + Collections.emptyMap(), + tokenFuture + ); + accessToken = tokenFuture.get().getAccessToken(); + assertThat(accessToken, notNullValue()); + } + public void testAuthnWithInvalidatedToken() throws Exception { when(securityMainIndex.indexExists()).thenReturn(true); TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); @@ -669,6 +820,57 @@ public void testMalformedRefreshTokens() throws Exception { } } + public void testNonExistingPre72Token() throws Exception { + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + // mock another random token so that we don't find a token in TokenService#getUserTokenFromId + Authentication authentication = AuthenticationTestHelper.builder() + .user(new User("joe", "admin")) + .realmRef(new RealmRef("native_realm", "native", "node1")) + .build(false); + mockGetTokenFromAccessTokenBytes(tokenService, tokenService.getRandomTokenBytes(randomBoolean()).v1(), authentication, false, null); + ThreadContext requestContext = new ThreadContext(Settings.EMPTY); + storeTokenHeader( + requestContext, + tokenService.prependVersionAndEncodeAccessToken( + TransportVersions.V_7_1_0, + tokenService.getRandomTokenBytes(TransportVersions.V_7_1_0, randomBoolean()).v1() + ) + ); + + try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { + PlainActionFuture future = new PlainActionFuture<>(); + final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); + assertNull(future.get()); + } + } + + public void testNonExistingUUIDToken() throws Exception { + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + // mock another random token so that we don't find a token in TokenService#getUserTokenFromId + Authentication authentication = AuthenticationTestHelper.builder() + .user(new User("joe", "admin")) + .realmRef(new RealmRef("native_realm", "native", "node1")) + .build(false); + mockGetTokenFromAccessTokenBytes(tokenService, tokenService.getRandomTokenBytes(randomBoolean()).v1(), authentication, false, null); + ThreadContext requestContext = new ThreadContext(Settings.EMPTY); + TransportVersion uuidTokenVersion = randomFrom(TransportVersions.V_7_2_0, TransportVersions.V_7_3_2); + storeTokenHeader( + requestContext, + tokenService.prependVersionAndEncodeAccessToken( + uuidTokenVersion, + tokenService.getRandomTokenBytes(uuidTokenVersion, randomBoolean()).v1() + ) + ); + + try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { + PlainActionFuture future = new PlainActionFuture<>(); + final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); + assertNull(future.get()); + } + } + public void testNonExistingLatestTokenVersion() throws Exception { TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); // mock another random token so that we don't find a token in TokenService#getUserTokenFromId @@ -723,11 +925,18 @@ public void testIndexNotAvailable() throws Exception { return Void.TYPE; }).when(client).get(any(GetRequest.class), anyActionListener()); - final SecurityIndexManager tokensIndex = securityTokensIndex; - when(securityMainIndex.isAvailable(SecurityIndexManager.Availability.PRIMARY_SHARDS)).thenReturn(false); - when(securityMainIndex.indexExists()).thenReturn(false); - when(securityMainIndex.defensiveCopy()).thenReturn(securityMainIndex); - + final SecurityIndexManager tokensIndex; + if (pre72OldNode != null) { + tokensIndex = securityMainIndex; + when(securityTokensIndex.isAvailable(SecurityIndexManager.Availability.PRIMARY_SHARDS)).thenReturn(false); + when(securityTokensIndex.indexExists()).thenReturn(false); + when(securityTokensIndex.defensiveCopy()).thenReturn(securityTokensIndex); + } else { + tokensIndex = securityTokensIndex; + when(securityMainIndex.isAvailable(SecurityIndexManager.Availability.PRIMARY_SHARDS)).thenReturn(false); + when(securityMainIndex.indexExists()).thenReturn(false); + when(securityMainIndex.defensiveCopy()).thenReturn(securityMainIndex); + } try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { PlainActionFuture future = new PlainActionFuture<>(); final SecureString bearerToken3 = Authenticator.extractBearerTokenFromHeader(requestContext); @@ -779,6 +988,7 @@ public void testGetAuthenticationWorksWithExpiredUserToken() throws Exception { } public void testSupersedingTokenEncryption() throws Exception { + assumeTrue("Superseding tokens are only created in post 7.2 clusters", pre72OldNode == null); TokenService tokenService = createTokenService(tokenServiceEnabledSettings, Clock.systemUTC()); Authentication authentication = AuthenticationTests.randomAuthentication(null, null); PlainActionFuture tokenFuture = new PlainActionFuture<>(); @@ -813,11 +1023,13 @@ public void testSupersedingTokenEncryption() throws Exception { authentication, tokenFuture ); - - assertThat( - tokenService.prependVersionAndEncodeAccessToken(version, newTokenBytes.v1()), - equalTo(tokenFuture.get().getAccessToken()) - ); + if (version.onOrAfter(TokenService.VERSION_ACCESS_TOKENS_AS_UUIDS)) { + // previous versions serialized the access token encrypted and the cipher text was different each time (due to different IVs) + assertThat( + tokenService.prependVersionAndEncodeAccessToken(version, newTokenBytes.v1()), + equalTo(tokenFuture.get().getAccessToken()) + ); + } assertThat( TokenService.prependVersionAndEncodeRefreshToken(version, newTokenBytes.v2()), equalTo(tokenFuture.get().getRefreshToken()) @@ -946,8 +1158,10 @@ public static String tokenDocIdFromAccessTokenBytes(byte[] accessTokenBytes, Tra MessageDigest userTokenIdDigest = sha256(); userTokenIdDigest.update(accessTokenBytes, RAW_TOKEN_BYTES_LENGTH, RAW_TOKEN_DOC_ID_BYTES_LENGTH); return Base64.getUrlEncoder().withoutPadding().encodeToString(userTokenIdDigest.digest()); - } else { + } else if (tokenVersion.onOrAfter(TokenService.VERSION_ACCESS_TOKENS_AS_UUIDS)) { return TokenService.hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(accessTokenBytes)); + } else { + return Base64.getUrlEncoder().withoutPadding().encodeToString(accessTokenBytes); } } @@ -964,9 +1178,12 @@ private void mockTokenForRefreshToken( if (userToken.getTransportVersion().onOrAfter(VERSION_GET_TOKEN_DOC_FOR_REFRESH)) { storedAccessToken = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256().digest(accessTokenBytes)); storedRefreshToken = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256().digest(refreshTokenBytes)); - } else { + } else if (userToken.getTransportVersion().onOrAfter(TokenService.VERSION_HASHED_TOKENS)) { storedAccessToken = null; storedRefreshToken = TokenService.hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(refreshTokenBytes)); + } else { + storedAccessToken = null; + storedRefreshToken = Base64.getUrlEncoder().withoutPadding().encodeToString(refreshTokenBytes); } final RealmRef realmRef = new RealmRef( refreshTokenStatus == null ? randomAlphaOfLength(6) : refreshTokenStatus.getAssociatedRealm(), From bc3b629d8d7010a61f5179279446346ffd45263b Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Wed, 18 Dec 2024 17:12:14 +0100 Subject: [PATCH 084/119] ESQL: Docs: add example of date bucketing with offset (#116680) Add an example of how to create date histograms with an offset. Fixes #114167 --- .../esql/functions/examples/bucket.asciidoc | 14 ++++++++++++++ .../functions/kibana/definition/bucket.json | 3 ++- .../elasticsearch/xpack/esql/CsvTestUtils.java | 2 +- .../src/main/resources/bucket.csv-spec | 18 ++++++++++++++++++ .../expression/function/grouping/Bucket.java | 11 +++++++++++ .../org/elasticsearch/xpack/esql/CsvTests.java | 5 +++-- 6 files changed, 49 insertions(+), 4 deletions(-) diff --git a/docs/reference/esql/functions/examples/bucket.asciidoc b/docs/reference/esql/functions/examples/bucket.asciidoc index 4afea30660339..264efc191748f 100644 --- a/docs/reference/esql/functions/examples/bucket.asciidoc +++ b/docs/reference/esql/functions/examples/bucket.asciidoc @@ -116,4 +116,18 @@ include::{esql-specs}/bucket.csv-spec[tag=reuseGroupingFunctionWithExpression] |=== include::{esql-specs}/bucket.csv-spec[tag=reuseGroupingFunctionWithExpression-result] |=== +Sometimes you need to change the start value of each bucket by a given duration (similar to date histogram +aggregation's <> parameter). To do so, you will need to +take into account how the language handles expressions within the `STATS` command: if these contain functions or +arithmetic operators, a virtual `EVAL` is inserted before and/or after the `STATS` command. Consequently, a double +compensation is needed to adjust the bucketed date value before the aggregation and then again after. For instance, +inserting a negative offset of `1 hour` to buckets of `1 year` looks like this: +[source.merge.styled,esql] +---- +include::{esql-specs}/bucket.csv-spec[tag=bucketWithOffset] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/bucket.csv-spec[tag=bucketWithOffset-result] +|=== diff --git a/docs/reference/esql/functions/kibana/definition/bucket.json b/docs/reference/esql/functions/kibana/definition/bucket.json index 18802f5ff8fef..3d96de05c8407 100644 --- a/docs/reference/esql/functions/kibana/definition/bucket.json +++ b/docs/reference/esql/functions/kibana/definition/bucket.json @@ -1598,7 +1598,8 @@ "FROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS c = COUNT(1) BY b = BUCKET(salary, 5000.)\n| SORT b", "FROM sample_data \n| WHERE @timestamp >= NOW() - 1 day and @timestamp < NOW()\n| STATS COUNT(*) BY bucket = BUCKET(@timestamp, 25, NOW() - 1 day, NOW())", "FROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS AVG(salary) BY bucket = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT bucket", - "FROM employees\n| STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.)\n| SORT b1, b2\n| KEEP s1, b1, s2, b2" + "FROM employees\n| STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.)\n| SORT b1, b2\n| KEEP s1, b1, s2, b2", + "FROM employees \n| STATS dates = VALUES(birth_date) BY b = BUCKET(birth_date + 1 HOUR, 1 YEAR) - 1 HOUR\n| EVAL d_count = MV_COUNT(dates)\n| SORT d_count\n| LIMIT 3" ], "preview" : false, "snapshot_only" : false diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestUtils.java index 7adafa908ce4f..f0bdf089f69d1 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestUtils.java @@ -63,7 +63,7 @@ import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.GEO; public final class CsvTestUtils { - private static final int MAX_WIDTH = 20; + private static final int MAX_WIDTH = 80; private static final CsvPreference CSV_SPEC_PREFERENCES = new CsvPreference.Builder('"', '|', "\r\n").build(); private static final String NULL_VALUE = "null"; private static final char ESCAPE_CHAR = '\\'; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec index b29c489910f65..8cfde2bb9bde7 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec @@ -145,6 +145,24 @@ AVG(salary):double | bucket:date // end::bucket_in_agg-result[] ; +bucketWithOffset#[skip:-8.13.99, reason:BUCKET renamed in 8.14] +// tag::bucketWithOffset[] +FROM employees +| STATS dates = MV_SORT(VALUES(birth_date)) BY b = BUCKET(birth_date + 1 HOUR, 1 YEAR) - 1 HOUR +| EVAL d_count = MV_COUNT(dates) +| SORT d_count, b +| LIMIT 3 +// end::bucketWithOffset[] +; + +// tag::bucketWithOffset-result[] +dates:date |b:date |d_count:integer +1965-01-03T00:00:00.000Z |1964-12-31T23:00:00.000Z|1 +[1955-01-21T00:00:00.000Z, 1955-08-20T00:00:00.000Z, 1955-08-28T00:00:00.000Z, 1955-10-04T00:00:00.000Z]|1954-12-31T23:00:00.000Z|4 +[1957-04-04T00:00:00.000Z, 1957-05-23T00:00:00.000Z, 1957-05-25T00:00:00.000Z, 1957-12-03T00:00:00.000Z]|1956-12-31T23:00:00.000Z|4 +// end::bucketWithOffset-result[] +; + docsBucketMonth#[skip:-8.13.99, reason:BUCKET renamed in 8.14] //tag::docsBucketMonth[] FROM employees diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java index 347d542f5212d..12932ba8d6e11 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java @@ -163,6 +163,17 @@ another in which the bucket size is provided directly (two parameters). grouping part, or that it is invoked with the exact same expression:""", file = "bucket", tag = "reuseGroupingFunctionWithExpression" + ), + @Example( + description = """ + Sometimes you need to change the start value of each bucket by a given duration (similar to date histogram + aggregation's <> parameter). To do so, you will need to + take into account how the language handles expressions within the `STATS` command: if these contain functions or + arithmetic operators, a virtual `EVAL` is inserted before and/or after the `STATS` command. Consequently, a double + compensation is needed to adjust the bucketed date value before the aggregation and then again after. For instance, + inserting a negative offset of `1 hour` to buckets of `1 year` looks like this:""", + file = "bucket", + tag = "bucketWithOffset" ) } ) public Bucket( diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index b81b80a9fdbb4..e627f99322f08 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -322,13 +322,14 @@ private void doTest() throws Exception { } protected void assertResults(ExpectedResults expected, ActualResults actual, boolean ignoreOrder, Logger logger) { - CsvAssert.assertResults(expected, actual, ignoreOrder, logger); /* - * Comment the assertion above and enable the next two lines to see the results returned by ES without any assertions being done. + * Enable the next two lines to see the results returned by ES. * This is useful when creating a new test or trying to figure out what are the actual results. */ // CsvTestUtils.logMetaData(actual.columnNames(), actual.columnTypes(), LOGGER); // CsvTestUtils.logData(actual.values(), LOGGER); + + CsvAssert.assertResults(expected, actual, ignoreOrder, logger); } private static IndexResolution loadIndexResolution(String mappingName, String indexName, Map typeMapping) { From a5ced2c48612b003dbbca3238526b29bfc6501d3 Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Wed, 18 Dec 2024 09:19:29 -0700 Subject: [PATCH 085/119] Unmute DotPrefixClientYamlTestSuiteIT (#118898) This failed due to an unrelated failure that appears to have affected more than just this suite. Resolves #118224 --- muted-tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index 6b433f232defd..0442c2305c6f8 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -238,8 +238,6 @@ tests: - class: org.elasticsearch.datastreams.DataStreamsClientYamlTestSuiteIT method: test {p0=data_stream/120_data_streams_stats/Multiple data stream} issue: https://github.com/elastic/elasticsearch/issues/118217 -- class: org.elasticsearch.validation.DotPrefixClientYamlTestSuiteIT - issue: https://github.com/elastic/elasticsearch/issues/118224 - class: org.elasticsearch.packaging.test.ArchiveTests method: test60StartAndStop issue: https://github.com/elastic/elasticsearch/issues/118216 From be769ab1226d928b2428b4b784a18dbcb62a8b9c Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Wed, 18 Dec 2024 10:19:44 -0600 Subject: [PATCH 086/119] Unmuting ReindexDataStreamStatusTests.testEqualsAndHashcode (#118974) This was fixed by #118957. Closes #118965 --- muted-tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index 0442c2305c6f8..f534f24718f52 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -290,9 +290,6 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118914 - class: org.elasticsearch.smoketest.SmokeTestMultiNodeClientYamlTestSuiteIT issue: https://github.com/elastic/elasticsearch/issues/118955 -- class: org.elasticsearch.xpack.migrate.task.ReindexDataStreamStatusTests - method: testEqualsAndHashcode - issue: https://github.com/elastic/elasticsearch/issues/118965 - class: org.elasticsearch.repositories.blobstore.testkit.analyze.SecureHdfsRepositoryAnalysisRestIT issue: https://github.com/elastic/elasticsearch/issues/118970 From 1147c9da0ca1adeeacc1cad2a7b9957296070c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 18 Dec 2024 17:24:47 +0100 Subject: [PATCH 087/119] ESQL: Fix match in LOOKUP JOIN YAML test (#118975) CI is broken, as sometimes the error comes with a "verification_error: " at the beginning, and the match assertion fails. Changing it to a contains. --- .../resources/rest-api-spec/test/esql/190_lookup_join.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/190_lookup_join.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/190_lookup_join.yml index cbaac4e47fdd4..fdb6746bbeed8 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/190_lookup_join.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/190_lookup_join.yml @@ -1,7 +1,7 @@ --- setup: - requires: - test_runner_features: [capabilities] + test_runner_features: [capabilities, contains] capabilities: - method: POST path: /_query @@ -75,4 +75,4 @@ non-lookup index: catch: "bad_request" - match: { error.type: "verification_exception" } - - match: { error.reason: "Found 1 problem\nline 1:43: invalid [test] resolution in lookup mode to an index in [standard] mode" } + - contains: { error.reason: "Found 1 problem\nline 1:43: invalid [test] resolution in lookup mode to an index in [standard] mode" } From 1004da29821471eea99afd64da0d151682f46595 Mon Sep 17 00:00:00 2001 From: Parker Timmins Date: Wed, 18 Dec 2024 10:35:19 -0600 Subject: [PATCH 088/119] Add action to create index from a source index (#118890) Add new action that creates an index from a source index, copying settings and mappings from the source index. This was refactored out of ReindexDataStreamIndexAction. --- docs/changelog/118890.yaml | 5 + .../action/CreateIndexFromSourceActionIT.java | 250 ++++++++++++++++++ ...ndexDatastreamIndexTransportActionIT.java} | 8 +- .../xpack/migrate/MigratePlugin.java | 3 + .../action/CreateIndexFromSourceAction.java | 117 ++++++++ .../CreateIndexFromSourceTransportAction.java | 126 +++++++++ .../action/ReindexDataStreamIndexAction.java | 4 - ...ReindexDataStreamIndexTransportAction.java | 104 ++++---- .../xpack/security/operator/Constants.java | 1 + 9 files changed, 552 insertions(+), 66 deletions(-) create mode 100644 docs/changelog/118890.yaml create mode 100644 x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceActionIT.java rename x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/{ReindexDatastreamIndexIT.java => ReindexDatastreamIndexTransportActionIT.java} (98%) create mode 100644 x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceAction.java create mode 100644 x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceTransportAction.java diff --git a/docs/changelog/118890.yaml b/docs/changelog/118890.yaml new file mode 100644 index 0000000000000..d3fc17157f130 --- /dev/null +++ b/docs/changelog/118890.yaml @@ -0,0 +1,5 @@ +pr: 118890 +summary: Add action to create index from a source index +area: Data streams +type: enhancement +issues: [] diff --git a/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceActionIT.java b/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceActionIT.java new file mode 100644 index 0000000000000..b460c6abfeee4 --- /dev/null +++ b/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceActionIT.java @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.migrate.action; + +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.get.GetIndexRequest; +import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest; +import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest; +import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.MappingMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.datastreams.DataStreamsPlugin; +import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.reindex.ReindexPlugin; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.migrate.MigratePlugin; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.REINDEX_DATA_STREAM_FEATURE_FLAG; + +public class CreateIndexFromSourceActionIT extends ESIntegTestCase { + + @Override + protected Collection> nodePlugins() { + return List.of(MigratePlugin.class, ReindexPlugin.class, MockTransportService.TestPlugin.class, DataStreamsPlugin.class); + } + + public void testDestIndexCreated() throws Exception { + assumeTrue("requires the migration reindex feature flag", REINDEX_DATA_STREAM_FEATURE_FLAG.isEnabled()); + + var sourceIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + indicesAdmin().create(new CreateIndexRequest(sourceIndex)).get(); + + // create from source + var destIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + assertAcked( + client().execute(CreateIndexFromSourceAction.INSTANCE, new CreateIndexFromSourceAction.Request(sourceIndex, destIndex)) + ); + + try { + indicesAdmin().getIndex(new GetIndexRequest().indices(destIndex)).actionGet(); + } catch (IndexNotFoundException e) { + fail(); + } + } + + public void testSettingsCopiedFromSource() throws Exception { + assumeTrue("requires the migration reindex feature flag", REINDEX_DATA_STREAM_FEATURE_FLAG.isEnabled()); + + // start with a static setting + var numShards = randomIntBetween(1, 10); + var staticSettings = Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, numShards).build(); + var sourceIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + indicesAdmin().create(new CreateIndexRequest(sourceIndex, staticSettings)).get(); + + // update with a dynamic setting + var numReplicas = randomIntBetween(0, 10); + var dynamicSettings = Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, numReplicas).build(); + indicesAdmin().updateSettings(new UpdateSettingsRequest(dynamicSettings, sourceIndex)).actionGet(); + + // create from source + var destIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + assertAcked( + client().execute(CreateIndexFromSourceAction.INSTANCE, new CreateIndexFromSourceAction.Request(sourceIndex, destIndex)) + ); + + // assert both static and dynamic settings set on dest index + var settingsResponse = indicesAdmin().getSettings(new GetSettingsRequest().indices(destIndex)).actionGet(); + assertEquals(numReplicas, Integer.parseInt(settingsResponse.getSetting(destIndex, IndexMetadata.SETTING_NUMBER_OF_REPLICAS))); + assertEquals(numShards, Integer.parseInt(settingsResponse.getSetting(destIndex, IndexMetadata.SETTING_NUMBER_OF_SHARDS))); + } + + public void testMappingsCopiedFromSource() { + assumeTrue("requires the migration reindex feature flag", REINDEX_DATA_STREAM_FEATURE_FLAG.isEnabled()); + + var sourceIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + String mapping = """ + { + "_doc":{ + "dynamic":"strict", + "properties":{ + "foo1":{ + "type":"text" + } + } + } + } + """; + indicesAdmin().create(new CreateIndexRequest(sourceIndex).mapping(mapping)).actionGet(); + + // create from source + var destIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + assertAcked( + client().execute(CreateIndexFromSourceAction.INSTANCE, new CreateIndexFromSourceAction.Request(sourceIndex, destIndex)) + ); + + var mappingsResponse = indicesAdmin().getMappings(new GetMappingsRequest().indices(sourceIndex, destIndex)).actionGet(); + Map mappings = mappingsResponse.mappings(); + var destMappings = mappings.get(destIndex).sourceAsMap(); + var sourceMappings = mappings.get(sourceIndex).sourceAsMap(); + + assertEquals(sourceMappings, destMappings); + // sanity check specific value from dest mapping + assertEquals("text", XContentMapValues.extractValue("properties.foo1.type", destMappings)); + } + + public void testSettingsOverridden() throws Exception { + assumeTrue("requires the migration reindex feature flag", REINDEX_DATA_STREAM_FEATURE_FLAG.isEnabled()); + + var numShardsSource = randomIntBetween(1, 10); + var numReplicasSource = randomIntBetween(0, 10); + var sourceIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + var sourceSettings = Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, numShardsSource) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, numReplicasSource) + .build(); + indicesAdmin().create(new CreateIndexRequest(sourceIndex, sourceSettings)).get(); + + boolean overrideNumShards = randomBoolean(); + Settings settingsOverride = overrideNumShards + ? Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, numShardsSource + 1).build() + : Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, numReplicasSource + 1).build(); + + // create from source + var destIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + assertAcked( + client().execute( + CreateIndexFromSourceAction.INSTANCE, + new CreateIndexFromSourceAction.Request(sourceIndex, destIndex, settingsOverride, Map.of()) + ) + ); + + // assert settings overridden + int expectedShards = overrideNumShards ? numShardsSource + 1 : numShardsSource; + int expectedReplicas = overrideNumShards ? numReplicasSource : numReplicasSource + 1; + var settingsResponse = indicesAdmin().getSettings(new GetSettingsRequest().indices(destIndex)).actionGet(); + assertEquals(expectedShards, Integer.parseInt(settingsResponse.getSetting(destIndex, IndexMetadata.SETTING_NUMBER_OF_SHARDS))); + assertEquals(expectedReplicas, Integer.parseInt(settingsResponse.getSetting(destIndex, IndexMetadata.SETTING_NUMBER_OF_REPLICAS))); + } + + public void testSettingsNullOverride() throws Exception { + assumeTrue("requires the migration reindex feature flag", REINDEX_DATA_STREAM_FEATURE_FLAG.isEnabled()); + + var sourceIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + var sourceSettings = Settings.builder().put(IndexMetadata.SETTING_BLOCKS_WRITE, true).build(); + indicesAdmin().create(new CreateIndexRequest(sourceIndex, sourceSettings)).get(); + + Settings settingsOverride = Settings.builder().putNull(IndexMetadata.SETTING_BLOCKS_WRITE).build(); + + // create from source + var destIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + assertAcked( + client().execute( + CreateIndexFromSourceAction.INSTANCE, + new CreateIndexFromSourceAction.Request(sourceIndex, destIndex, settingsOverride, Map.of()) + ) + ); + + // assert settings overridden + var settingsResponse = indicesAdmin().getSettings(new GetSettingsRequest().indices(destIndex)).actionGet(); + assertNull(settingsResponse.getSetting(destIndex, IndexMetadata.SETTING_BLOCKS_WRITE)); + } + + public void testMappingsOverridden() { + assumeTrue("requires the migration reindex feature flag", REINDEX_DATA_STREAM_FEATURE_FLAG.isEnabled()); + + var sourceIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + String sourceMapping = """ + { + "_doc":{ + "dynamic":"strict", + "properties":{ + "foo1":{ + "type":"text" + }, + "foo2":{ + "type":"boolean" + } + } + } + } + """; + indicesAdmin().create(new CreateIndexRequest(sourceIndex).mapping(sourceMapping)).actionGet(); + + String mappingOverrideStr = """ + { + "_doc":{ + "dynamic":"strict", + "properties":{ + "foo1":{ + "type":"integer" + }, + "foo3": { + "type":"keyword" + } + } + } + } + """; + var mappingOverride = XContentHelper.convertToMap(JsonXContent.jsonXContent, mappingOverrideStr, false); + + // create from source + var destIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); + assertAcked( + client().execute( + CreateIndexFromSourceAction.INSTANCE, + new CreateIndexFromSourceAction.Request(sourceIndex, destIndex, Settings.EMPTY, mappingOverride) + ) + ); + + var mappingsResponse = indicesAdmin().getMappings(new GetMappingsRequest().indices(destIndex)).actionGet(); + Map mappings = mappingsResponse.mappings(); + var destMappings = mappings.get(destIndex).sourceAsMap(); + + String expectedMappingStr = """ + { + "dynamic":"strict", + "properties":{ + "foo1":{ + "type":"integer" + }, + "foo2": { + "type":"boolean" + }, + "foo3": { + "type":"keyword" + } + } + } + """; + var expectedMapping = XContentHelper.convertToMap(JsonXContent.jsonXContent, expectedMappingStr, false); + assertEquals(expectedMapping, destMappings); + } +} diff --git a/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDatastreamIndexIT.java b/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDatastreamIndexTransportActionIT.java similarity index 98% rename from x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDatastreamIndexIT.java rename to x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDatastreamIndexTransportActionIT.java index e492f035da866..0ca58ecf0f0d5 100644 --- a/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDatastreamIndexIT.java +++ b/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDatastreamIndexTransportActionIT.java @@ -53,7 +53,7 @@ import static org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.REINDEX_DATA_STREAM_FEATURE_FLAG; import static org.hamcrest.Matchers.equalTo; -public class ReindexDatastreamIndexIT extends ESIntegTestCase { +public class ReindexDatastreamIndexTransportActionIT extends ESIntegTestCase { private static final String MAPPING = """ { @@ -126,12 +126,14 @@ public void testDestIndexContainsDocs() throws Exception { assertHitCount(prepareSearch(response.getDestIndex()).setSize(0), numDocs); } - public void testSetSourceToReadOnly() throws Exception { + public void testSetSourceToBlockWrites() throws Exception { assumeTrue("requires the migration reindex feature flag", REINDEX_DATA_STREAM_FEATURE_FLAG.isEnabled()); + var settings = randomBoolean() ? Settings.builder().put(IndexMetadata.SETTING_BLOCKS_WRITE, true).build() : Settings.EMPTY; + // empty source index var sourceIndex = randomAlphaOfLength(20).toLowerCase(Locale.ROOT); - indicesAdmin().create(new CreateIndexRequest(sourceIndex)).get(); + indicesAdmin().create(new CreateIndexRequest(sourceIndex, settings)).get(); // call reindex client().execute(ReindexDataStreamIndexAction.INSTANCE, new ReindexDataStreamIndexAction.Request(sourceIndex)).actionGet(); diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/MigratePlugin.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/MigratePlugin.java index f42d05727b9fd..d9dffdefafa2c 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/MigratePlugin.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/MigratePlugin.java @@ -34,6 +34,8 @@ import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xpack.migrate.action.CancelReindexDataStreamAction; import org.elasticsearch.xpack.migrate.action.CancelReindexDataStreamTransportAction; +import org.elasticsearch.xpack.migrate.action.CreateIndexFromSourceAction; +import org.elasticsearch.xpack.migrate.action.CreateIndexFromSourceTransportAction; import org.elasticsearch.xpack.migrate.action.GetMigrationReindexStatusAction; import org.elasticsearch.xpack.migrate.action.GetMigrationReindexStatusTransportAction; import org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction; @@ -87,6 +89,7 @@ public List getRestHandlers( actions.add(new ActionHandler<>(GetMigrationReindexStatusAction.INSTANCE, GetMigrationReindexStatusTransportAction.class)); actions.add(new ActionHandler<>(CancelReindexDataStreamAction.INSTANCE, CancelReindexDataStreamTransportAction.class)); actions.add(new ActionHandler<>(ReindexDataStreamIndexAction.INSTANCE, ReindexDataStreamIndexTransportAction.class)); + actions.add(new ActionHandler<>(CreateIndexFromSourceAction.INSTANCE, CreateIndexFromSourceTransportAction.class)); } return actions; } diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceAction.java new file mode 100644 index 0000000000000..d67eaee3d251f --- /dev/null +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceAction.java @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.migrate.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.Settings; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +public class CreateIndexFromSourceAction extends ActionType { + + public static final String NAME = "indices:admin/index/create_from_source"; + + public static final ActionType INSTANCE = new CreateIndexFromSourceAction(); + + private CreateIndexFromSourceAction() { + super(NAME); + } + + public static class Request extends ActionRequest implements IndicesRequest { + + private final String sourceIndex; + private final String destIndex; + private final Settings settingsOverride; + private final Map mappingsOverride; + + public Request(String sourceIndex, String destIndex) { + this(sourceIndex, destIndex, Settings.EMPTY, Map.of()); + } + + public Request(String sourceIndex, String destIndex, Settings settingsOverride, Map mappingsOverride) { + Objects.requireNonNull(mappingsOverride); + this.sourceIndex = sourceIndex; + this.destIndex = destIndex; + this.settingsOverride = settingsOverride; + this.mappingsOverride = mappingsOverride; + } + + @SuppressWarnings("unchecked") + public Request(StreamInput in) throws IOException { + super(in); + this.sourceIndex = in.readString(); + this.destIndex = in.readString(); + this.settingsOverride = Settings.readSettingsFromStream(in); + this.mappingsOverride = (Map) in.readGenericValue(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(sourceIndex); + out.writeString(destIndex); + settingsOverride.writeTo(out); + out.writeGenericValue(mappingsOverride); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getSourceIndex() { + return sourceIndex; + } + + public String getDestIndex() { + return destIndex; + } + + public Settings getSettingsOverride() { + return settingsOverride; + } + + public Map getMappingsOverride() { + return mappingsOverride; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Request request = (Request) o; + return Objects.equals(sourceIndex, request.sourceIndex) + && Objects.equals(destIndex, request.destIndex) + && Objects.equals(settingsOverride, request.settingsOverride) + && Objects.equals(mappingsOverride, request.mappingsOverride); + } + + @Override + public int hashCode() { + return Objects.hash(sourceIndex, destIndex, settingsOverride, mappingsOverride); + } + + @Override + public String[] indices() { + return new String[] { sourceIndex }; + } + + @Override + public IndicesOptions indicesOptions() { + return IndicesOptions.strictSingleIndexNoExpandForbidClosed(); + } + } +} diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceTransportAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceTransportAction.java new file mode 100644 index 0000000000000..968b2220628a9 --- /dev/null +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CreateIndexFromSourceTransportAction.java @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.migrate.action; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.MappingMetadata; +import org.elasticsearch.cluster.metadata.MetadataCreateIndexService; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.injection.guice.Inject; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xcontent.XContentType; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class CreateIndexFromSourceTransportAction extends HandledTransportAction< + CreateIndexFromSourceAction.Request, + AcknowledgedResponse> { + private static final Logger logger = LogManager.getLogger(CreateIndexFromSourceTransportAction.class); + + private final ClusterService clusterService; + private final Client client; + private final IndexScopedSettings indexScopedSettings; + + @Inject + public CreateIndexFromSourceTransportAction( + TransportService transportService, + ClusterService clusterService, + ActionFilters actionFilters, + Client client, + IndexScopedSettings indexScopedSettings + ) { + super( + CreateIndexFromSourceAction.NAME, + false, + transportService, + actionFilters, + CreateIndexFromSourceAction.Request::new, + transportService.getThreadPool().executor(ThreadPool.Names.GENERIC) + ); + this.clusterService = clusterService; + this.client = client; + this.indexScopedSettings = indexScopedSettings; + } + + @Override + protected void doExecute(Task task, CreateIndexFromSourceAction.Request request, ActionListener listener) { + + IndexMetadata sourceIndex = clusterService.state().getMetadata().index(request.getSourceIndex()); + + if (sourceIndex == null) { + listener.onFailure(new IndexNotFoundException(request.getSourceIndex())); + return; + } + + logger.debug("Creating destination index [{}] for source index [{}]", request.getDestIndex(), request.getSourceIndex()); + + Settings settings = Settings.builder() + // add source settings + .put(filterSettings(sourceIndex)) + // add override settings from request + .put(request.getSettingsOverride()) + .build(); + + Map mergeMappings; + try { + mergeMappings = mergeMappings(sourceIndex.mapping(), request.getMappingsOverride()); + } catch (IOException e) { + listener.onFailure(e); + return; + } + + var createIndexRequest = new CreateIndexRequest(request.getDestIndex()).settings(settings); + if (mergeMappings.isEmpty() == false) { + createIndexRequest.mapping(mergeMappings); + } + + client.admin().indices().create(createIndexRequest, listener.map(response -> response)); + } + + private static Map toMap(@Nullable MappingMetadata sourceMapping) { + return Optional.ofNullable(sourceMapping) + .map(MappingMetadata::source) + .map(CompressedXContent::uncompressed) + .map(s -> XContentHelper.convertToMap(s, true, XContentType.JSON).v2()) + .orElse(Map.of()); + } + + private static Map mergeMappings(@Nullable MappingMetadata sourceMapping, Map mappingAddition) + throws IOException { + Map combinedMappingMap = new HashMap<>(toMap(sourceMapping)); + XContentHelper.update(combinedMappingMap, mappingAddition, true); + return combinedMappingMap; + } + + // Filter source index settings to subset of settings that can be included during reindex. + // Similar to the settings filtering done when reindexing for upgrade in Kibana + // https://github.com/elastic/kibana/blob/8a8363f02cc990732eb9cbb60cd388643a336bed/x-pack + // /plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts#L155 + private Settings filterSettings(IndexMetadata sourceIndex) { + return MetadataCreateIndexService.copySettingsFromSource(false, sourceIndex.getSettings(), indexScopedSettings, Settings.builder()) + .build(); + } +} diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexAction.java index 00c81fdc9fbc6..2e3fd1b76ed32 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexAction.java @@ -41,10 +41,6 @@ public Request(StreamInput in) throws IOException { this.sourceIndex = in.readString(); } - public static Request readFrom(StreamInput in) throws IOException { - return new Request(in); - } - @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java index 8863c45691c92..165fd61ae6599 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java @@ -10,10 +10,10 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; -import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; -import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest; +import org.elasticsearch.action.admin.indices.readonly.AddIndexBlockRequest; +import org.elasticsearch.action.admin.indices.readonly.AddIndexBlockResponse; +import org.elasticsearch.action.admin.indices.readonly.TransportAddIndexBlockAction; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.action.support.IndicesOptions; @@ -22,9 +22,7 @@ import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.metadata.IndexMetadata; -import org.elasticsearch.cluster.metadata.MetadataCreateIndexService; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.reindex.BulkByScrollResponse; @@ -37,28 +35,25 @@ import java.util.Locale; import java.util.Map; -import java.util.Set; + +import static org.elasticsearch.cluster.metadata.IndexMetadata.APIBlock.READ_ONLY; +import static org.elasticsearch.cluster.metadata.IndexMetadata.APIBlock.WRITE; public class ReindexDataStreamIndexTransportAction extends HandledTransportAction< ReindexDataStreamIndexAction.Request, ReindexDataStreamIndexAction.Response> { private static final Logger logger = LogManager.getLogger(ReindexDataStreamIndexTransportAction.class); - - private static final Set SETTINGS_TO_ADD_BACK = Set.of(IndexMetadata.SETTING_BLOCKS_WRITE, IndexMetadata.SETTING_READ_ONLY); - private static final IndicesOptions IGNORE_MISSING_OPTIONS = IndicesOptions.fromOptions(true, true, false, false); private final ClusterService clusterService; private final Client client; - private final IndexScopedSettings indexScopedSettings; @Inject public ReindexDataStreamIndexTransportAction( TransportService transportService, ClusterService clusterService, ActionFilters actionFilters, - Client client, - IndexScopedSettings indexScopedSettings + Client client ) { super( ReindexDataStreamIndexAction.NAME, @@ -70,7 +65,6 @@ public ReindexDataStreamIndexTransportAction( ); this.clusterService = clusterService; this.client = client; - this.indexScopedSettings = indexScopedSettings; } @Override @@ -96,20 +90,19 @@ protected void doExecute( SubscribableListener.newForked(l -> setBlockWrites(sourceIndexName, l)) .andThen(l -> deleteDestIfExists(destIndexName, l)) - .andThen(l -> createIndex(sourceIndex, destIndexName, l)) + .andThen(l -> createIndex(sourceIndex, destIndexName, l)) .andThen(l -> reindex(sourceIndexName, destIndexName, l)) - .andThen(l -> updateSettings(settingsBefore, destIndexName, l)) + .andThen(l -> addBlockIfFromSource(READ_ONLY, settingsBefore, destIndexName, l)) + .andThen(l -> addBlockIfFromSource(WRITE, settingsBefore, destIndexName, l)) .andThenApply(ignored -> new ReindexDataStreamIndexAction.Response(destIndexName)) .addListener(listener); } private void setBlockWrites(String sourceIndexName, ActionListener listener) { logger.debug("Setting write block on source index [{}]", sourceIndexName); - final Settings readOnlySettings = Settings.builder().put(IndexMetadata.INDEX_BLOCKS_WRITE_SETTING.getKey(), true).build(); - var updateSettingsRequest = new UpdateSettingsRequest(readOnlySettings, sourceIndexName); - client.admin().indices().updateSettings(updateSettingsRequest, new ActionListener<>() { + addBlockToIndex(WRITE, sourceIndexName, new ActionListener<>() { @Override - public void onResponse(AcknowledgedResponse response) { + public void onResponse(AddIndexBlockResponse response) { if (response.isAcknowledged()) { listener.onResponse(null); } else { @@ -121,7 +114,7 @@ public void onResponse(AcknowledgedResponse response) { @Override public void onFailure(Exception e) { if (e instanceof ClusterBlockException || e.getCause() instanceof ClusterBlockException) { - // It's fine if read-only is already set + // It's fine if block-writes is already set listener.onResponse(null); } else { listener.onFailure(e); @@ -138,18 +131,23 @@ private void deleteDestIfExists(String destIndexName, ActionListener listener) { + private void createIndex(IndexMetadata sourceIndex, String destIndexName, ActionListener listener) { logger.debug("Creating destination index [{}] for source index [{}]", destIndexName, sourceIndex.getIndex().getName()); - // Create destination with subset of source index settings that can be added before reindex - var settings = getPreSettings(sourceIndex); - - var sourceMapping = sourceIndex.mapping(); - Map mapping = sourceMapping != null ? sourceMapping.rawSourceAsMap() : Map.of(); - var createIndexRequest = new CreateIndexRequest(destIndexName).settings(settings).mapping(mapping); - - var errorMessage = String.format(Locale.ROOT, "Could not create index [%s]", destIndexName); - client.admin().indices().create(createIndexRequest, failIfNotAcknowledged(listener, errorMessage)); + // override read-only settings if they exist + var removeReadOnlyOverride = Settings.builder() + .putNull(IndexMetadata.SETTING_READ_ONLY) + .putNull(IndexMetadata.SETTING_BLOCKS_WRITE) + .build(); + + var request = new CreateIndexFromSourceAction.Request( + sourceIndex.getIndex().getName(), + destIndexName, + removeReadOnlyOverride, + Map.of() + ); + var errorMessage = String.format(Locale.ROOT, "Could not create index [%s]", request.getDestIndex()); + client.execute(CreateIndexFromSourceAction.INSTANCE, request, failIfNotAcknowledged(listener, errorMessage)); } private void reindex(String sourceIndexName, String destIndexName, ActionListener listener) { @@ -162,35 +160,18 @@ private void reindex(String sourceIndexName, String destIndexName, ActionListene client.execute(ReindexAction.INSTANCE, reindexRequest, listener); } - private void updateSettings(Settings settingsBefore, String destIndexName, ActionListener listener) { - logger.debug("Adding settings from source index that could not be added before reindex"); - - Settings postSettings = getPostSettings(settingsBefore); - if (postSettings.isEmpty()) { + private void addBlockIfFromSource( + IndexMetadata.APIBlock block, + Settings settingsBefore, + String destIndexName, + ActionListener listener + ) { + if (settingsBefore.getAsBoolean(block.settingName(), false)) { + var errorMessage = String.format(Locale.ROOT, "Add [%s] block to index [%s] was not acknowledged", block.name(), destIndexName); + addBlockToIndex(block, destIndexName, failIfNotAcknowledged(listener, errorMessage)); + } else { listener.onResponse(null); - return; } - - var updateSettingsRequest = new UpdateSettingsRequest(postSettings, destIndexName); - var errorMessage = String.format(Locale.ROOT, "Could not update settings on index [%s]", destIndexName); - client.admin().indices().updateSettings(updateSettingsRequest, failIfNotAcknowledged(listener, errorMessage)); - } - - // Filter source index settings to subset of settings that can be included during reindex. - // Similar to the settings filtering done when reindexing for upgrade in Kibana - // https://github.com/elastic/kibana/blob/8a8363f02cc990732eb9cbb60cd388643a336bed/x-pack - // /plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts#L155 - private Settings getPreSettings(IndexMetadata sourceIndex) { - // filter settings that will be added back later - var filtered = sourceIndex.getSettings().filter(settingName -> SETTINGS_TO_ADD_BACK.contains(settingName) == false); - - // filter private and non-copyable settings - var builder = MetadataCreateIndexService.copySettingsFromSource(false, filtered, indexScopedSettings, Settings.builder()); - return builder.build(); - } - - private Settings getPostSettings(Settings settingsBefore) { - return settingsBefore.filter(SETTINGS_TO_ADD_BACK::contains); } public static String generateDestIndexName(String sourceIndex) { @@ -201,11 +182,16 @@ private static ActionListener failIfNotAckno ActionListener listener, String errorMessage ) { - return listener.delegateFailureAndWrap((delegate, response) -> { + return listener.delegateFailure((delegate, response) -> { if (response.isAcknowledged()) { delegate.onResponse(null); + } else { + delegate.onFailure(new ElasticsearchException(errorMessage)); } - throw new ElasticsearchException(errorMessage); }); } + + private void addBlockToIndex(IndexMetadata.APIBlock block, String index, ActionListener listener) { + client.admin().indices().execute(TransportAddIndexBlockAction.TYPE, new AddIndexBlockRequest(block, index), listener); + } } diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index c07e4b2c541a2..1cb73de4646cc 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -641,6 +641,7 @@ public class Constants { new FeatureFlag("reindex_data_stream").isEnabled() ? "indices:admin/data_stream/index/reindex" : null, new FeatureFlag("reindex_data_stream").isEnabled() ? "indices:admin/data_stream/reindex" : null, new FeatureFlag("reindex_data_stream").isEnabled() ? "indices:admin/data_stream/reindex_cancel" : null, + new FeatureFlag("reindex_data_stream").isEnabled() ? "indices:admin/index/create_from_source" : null, "internal:admin/repository/verify", "internal:admin/repository/verify/coordinate" ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableSet()); From dadf875bdf2267e50b12cc6455b3ee1fa3365023 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Wed, 18 Dec 2024 18:11:14 +0100 Subject: [PATCH 089/119] Push removal of search workers pool setting to v10 (#118877) The search workers thread pool has been removed in 8.16. We still support parsing its size and queue size settings, to prevent issues upon upgrade for users that may have customized them. We will provide such compatibility for the entire 9.x series. --- .../main/java/org/elasticsearch/node/NodeConstruction.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java index 17e56a392daff..5cfe1c104d45e 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java +++ b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java @@ -83,7 +83,7 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; -import org.elasticsearch.core.UpdateForV9; +import org.elasticsearch.core.UpdateForV10; import org.elasticsearch.discovery.DiscoveryModule; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; @@ -553,9 +553,10 @@ private SettingsModule validateSettings(Settings envSettings, Settings settings, return settingsModule; } - @UpdateForV9(owner = UpdateForV9.Owner.SEARCH_FOUNDATIONS) + @UpdateForV10(owner = UpdateForV10.Owner.SEARCH_FOUNDATIONS) private static void addBwcSearchWorkerSettings(List> additionalSettings) { - // TODO remove the below settings, they are unused and only here to enable BwC for deployments that still use them + // Search workers thread pool has been removed in Elasticsearch 8.16.0. These settings are deprecated and take no effect. + // They are here only to enable BwC for deployments that still use them additionalSettings.add( Setting.intSetting("thread_pool.search_worker.queue_size", 0, Setting.Property.NodeScope, Setting.Property.DeprecatedWarning) ); From 76b2968360dc4e71309e3b43dc18e6f41a142651 Mon Sep 17 00:00:00 2001 From: Niels Bauman <33722607+nielsbauman@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:30:58 +0100 Subject: [PATCH 090/119] Disable SLM history in docs tests (#118979) The SLM history data stream was causing issues in the docs tests because its presence was flaky and could result in the inability to remove its index template, which in turn resulted in failing tests. --- docs/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/build.gradle b/docs/build.gradle index dec0de8ffa844..93b7277327280 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -130,8 +130,9 @@ testClusters.matching { it.name == "yamlRestTest"}.configureEach { setting 'xpack.security.enabled', 'true' setting 'xpack.security.authc.api_key.enabled', 'true' setting 'xpack.security.authc.token.enabled', 'true' - // disable the ILM history for doc tests to avoid potential lingering tasks that'd cause test flakiness + // disable the ILM and SLM history for doc tests to avoid potential lingering tasks that'd cause test flakiness setting 'indices.lifecycle.history_index_enabled', 'false' + setting 'slm.history_index_enabled', 'false' setting 'xpack.license.self_generated.type', 'trial' setting 'xpack.security.authc.realms.file.file.order', '0' setting 'xpack.security.authc.realms.native.native.order', '1' From 5663efa5e344397d423eefd52d6a06e4fda9b70e Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 18 Dec 2024 18:38:54 +0100 Subject: [PATCH 091/119] Just mute the bad apple (#118989) --- muted-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/muted-tests.yml b/muted-tests.yml index f534f24718f52..a06334146ed7b 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -289,6 +289,7 @@ tests: method: testThreadContext issue: https://github.com/elastic/elasticsearch/issues/118914 - class: org.elasticsearch.smoketest.SmokeTestMultiNodeClientYamlTestSuiteIT + method: test {yaml=indices.create/20_synthetic_source/create index with use_synthetic_source} issue: https://github.com/elastic/elasticsearch/issues/118955 - class: org.elasticsearch.repositories.blobstore.testkit.analyze.SecureHdfsRepositoryAnalysisRestIT issue: https://github.com/elastic/elasticsearch/issues/118970 From e741fd62cd70016bc70c49b87def3397a4c48ed1 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Wed, 18 Dec 2024 19:30:36 +0100 Subject: [PATCH 092/119] [Build] Build hdfs fixture faster and less (#118801) The building of the shadowed hdfs2 and hdfs3 fixtures takes quite long time due to being 51 and 80mb in size. By removing non used dependencies from the shadow jar creation we can speed up this significantly. Also we avoid building hdfs fixture jars now for compile only (resulting in no shadow jar creation for precommit checks) --- test/fixtures/hdfs-fixture/build.gradle | 81 ++++++++++++++----- .../searchable-snapshots/qa/hdfs/build.gradle | 3 +- .../qa/hdfs/build.gradle | 3 +- 3 files changed, 66 insertions(+), 21 deletions(-) diff --git a/test/fixtures/hdfs-fixture/build.gradle b/test/fixtures/hdfs-fixture/build.gradle index 9dc0263f49aee..8296bc14fd665 100644 --- a/test/fixtures/hdfs-fixture/build.gradle +++ b/test/fixtures/hdfs-fixture/build.gradle @@ -10,12 +10,10 @@ apply plugin: 'elasticsearch.java' apply plugin: 'com.gradleup.shadow' + import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar configurations { -// all { -// transitive = true -// } hdfs2 hdfs3 consumable("shadowedHdfs2") @@ -27,20 +25,76 @@ dependencies { transitive false } compileOnly "junit:junit:${versions.junit}" - hdfs2 "org.apache.hadoop:hadoop-minicluster:2.8.5" - hdfs3 "org.apache.hadoop:hadoop-minicluster:3.3.1" + def commonExcludes = [ + [group: "org.apache.commons", module: "commons-compress"], + [group: "org.apache.hadoop", module: "hadoop-mapreduce-client-app"], + [group: "org.apache.hadoop", module: "hadoop-mapreduce-client-core"], + [group: "org.apache.hadoop", module: "hadoop-mapreduce-client-hs"], + [group: "org.apache.hadoop", module: "hadoop-mapreduce-client-jobclient"], + [group: "org.apache.hadoop", module: "hadoop-yarn-server-tests"], + [group: "org.apache.httpcomponents", module: "httpclient"], + [group: "org.apache.zookeeper", module: "zookeeper"], + [group: "org.apache.curator", module: "curator-recipes"], + [group: "org.apache.curator", module: "curator-client"], + [group: "org.apache.curator", module: "curator-framework"], + [group: "org.apache.avro", module: "avro"], + [group: "log4j", module: "log4j"], + [group: "io.netty", module: "netty-all"], + [group: "io.netty", module: "netty"], + [group: "com.squareup.okhttp", module: "okhttp"], + [group: "com.google.guava", module: "guava"], + [group: "com.google.code.gson", module: "gson"], + [group: "javax.servlet.jsp", module: "jsp-api"], + [group: "org.fusesource.leveldbjni", module: "leveldbjni-all"], + [group: "commons-cli", module: "commons-cli"], + [group: "org.mortbay.jetty", module: "servlet-api"], + [group: "commons-logging", module: "commons-logging"], + [group: "org.slf4j", module: "slf4j-log4j12"], + [group: "commons-codec", module: "commons-codec"], + [group: "com.sun.jersey", module: "jersey-core"], + [group: "com.sun.jersey", module: "jersey-json"], + [group: "com.google.code.findbugs", module: "jsr305"], + [group: "com.sun.jersey", module: "jersey-json"], + [group: "com.nimbusds", module: "nimbus-jose-jwt"], + [group: "com.jcraft", module: "jsch"], + [group: "org.slf4j", module: "slf4j-api"], + ] + + hdfs2("org.apache.hadoop:hadoop-minicluster:2.8.5") { + commonExcludes.each { exclude it } + exclude group: "org.apache.commons", module: "commons-math3" + exclude group: "xmlenc", module: "xmlenc" + exclude group: "net.java.dev.jets3t", module: "jets3t" + exclude group: "org.apache.directory.server", module: "apacheds-i18n" + exclude group: "xerces", module: "xercesImpl" + } + + hdfs3("org.apache.hadoop:hadoop-minicluster:3.3.1") { + commonExcludes.each { exclude it } + exclude group: "dnsjava", module: "dnsjava" + exclude group: "com.google.inject.extensions", module: "guice-servlet" + exclude group: "com.google.inject", module: "guice" + exclude group: "com.microsoft.sqlserver", module: "mssql-jdbc" + exclude group: "com.sun.jersey.contribs", module: "jersey-guice" + exclude group: "com.zaxxer", module: "HikariCP-java7" + exclude group: "com.sun.jersey", module: "jersey-server" + exclude group: "org.bouncycastle", module: "bcpkix-jdk15on" + exclude group: "org.bouncycastle", module: "bcprov-jdk15on" + exclude group: "org.ehcache", module: "ehcache" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jcache_1.0_spec" + exclude group: "org.xerial.snappy", module: "snappy-java" + } } tasks.named("shadowJar").configure { archiveClassifier.set("hdfs3") // fix issues with signed jars - relocate("org.apache.hadoop", "fixture.hdfs3.org.apache.hadoop") { exclude "org.apache.hadoop.hdfs.protocol.ClientProtocol" exclude "org.apache.hadoop.ipc.StandbyException" } - configurations << project.configurations.hdfs3 + configurations.add(project.configurations.hdfs3) } def hdfs2Jar = tasks.register("hdfs2jar", ShadowJar) { @@ -50,26 +104,15 @@ def hdfs2Jar = tasks.register("hdfs2jar", ShadowJar) { } archiveClassifier.set("hdfs2") from sourceSets.main.output - configurations << project.configurations.hdfs2 + configurations.add(project.configurations.hdfs2) } tasks.withType(ShadowJar).configureEach { dependencies { -// exclude(dependency('commons-io:commons-io:2.8.0')) exclude(dependency("com.carrotsearch.randomizedtesting:randomizedtesting-runner:.*")) exclude(dependency("junit:junit:.*")) - exclude(dependency("org.slf4j:slf4j-api:.*")) - exclude(dependency("com.google.guava:guava:.*")) - exclude(dependency("org.apache.commons:commons-compress:.*")) - exclude(dependency("commons-logging:commons-logging:.*")) - exclude(dependency("commons-codec:commons-codec:.*")) - exclude(dependency("org.apache.httpcomponents:httpclient:.*")) exclude(dependency("org.apache.httpcomponents:httpcore:.*")) exclude(dependency("org.apache.logging.log4j:log4j-1.2-api:.*")) - exclude(dependency("log4j:log4j:.*")) - exclude(dependency("io.netty:.*:.*")) - exclude(dependency("com.nimbusds:nimbus-jose-jwt:.*")) - exclude(dependency("commons-cli:commons-cli:1.2")) exclude(dependency("net.java.dev.jna:jna:.*")) exclude(dependency("org.objenesis:objenesis:.*")) exclude(dependency('com.fasterxml.jackson.core:.*:.*')) diff --git a/x-pack/plugin/searchable-snapshots/qa/hdfs/build.gradle b/x-pack/plugin/searchable-snapshots/qa/hdfs/build.gradle index b41e0f8dcc1cf..4577935e4e08d 100644 --- a/x-pack/plugin/searchable-snapshots/qa/hdfs/build.gradle +++ b/x-pack/plugin/searchable-snapshots/qa/hdfs/build.gradle @@ -12,7 +12,8 @@ apply plugin: 'elasticsearch.internal-available-ports' dependencies { clusterPlugins project(':plugins:repository-hdfs') javaRestTestImplementation(testArtifact(project(xpackModule('searchable-snapshots')))) - javaRestTestImplementation project(path: ':test:fixtures:hdfs-fixture', configuration:"shadowedHdfs2") + javaRestTestCompileOnly project(path: ':test:fixtures:hdfs-fixture') + javaRestTestRuntimeOnly project(path: ':test:fixtures:hdfs-fixture', configuration:"shadowedHdfs2") javaRestTestImplementation project(':test:fixtures:krb5kdc-fixture') javaRestTestRuntimeOnly "com.google.guava:guava:16.0.1" javaRestTestRuntimeOnly "commons-cli:commons-cli:1.2" diff --git a/x-pack/plugin/snapshot-repo-test-kit/qa/hdfs/build.gradle b/x-pack/plugin/snapshot-repo-test-kit/qa/hdfs/build.gradle index 81eb82a522389..d4615260952d1 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/qa/hdfs/build.gradle +++ b/x-pack/plugin/snapshot-repo-test-kit/qa/hdfs/build.gradle @@ -10,7 +10,8 @@ apply plugin: 'elasticsearch.rest-resources' dependencies { javaRestTestImplementation testArtifact(project(xpackModule('snapshot-repo-test-kit'))) - javaRestTestImplementation project(path: ':test:fixtures:hdfs-fixture', configuration:"shadow") + javaRestTestCompileOnly project(path: ':test:fixtures:hdfs-fixture') + javaRestTestRuntimeOnly project(path: ':test:fixtures:hdfs-fixture', configuration:"shadow") javaRestTestImplementation project(':test:fixtures:krb5kdc-fixture') javaRestTestImplementation "org.slf4j:slf4j-api:${versions.slf4j}" javaRestTestImplementation "org.slf4j:slf4j-simple:${versions.slf4j}" From 3a15d0b0840c911654d7affb779fc3c814e0b646 Mon Sep 17 00:00:00 2001 From: Parker Timmins Date: Wed, 18 Dec 2024 12:45:40 -0600 Subject: [PATCH 093/119] Mute test --- muted-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index a06334146ed7b..81480e89d1e8b 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -293,6 +293,8 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118955 - class: org.elasticsearch.repositories.blobstore.testkit.analyze.SecureHdfsRepositoryAnalysisRestIT issue: https://github.com/elastic/elasticsearch/issues/118970 +- class: org.elasticsearch.xpack.migrate.action.ReindexDatastreamIndexTransportActionIT + issue: https://github.com/elastic/elasticsearch/issues/119002 # Examples: # From fab3bff84fb3648cee59e1f5ac8a21dbb1532a66 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 18 Dec 2024 12:47:45 -0600 Subject: [PATCH 094/119] Update jvm.options (#118716) --- distribution/src/config/jvm.options | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/src/config/jvm.options b/distribution/src/config/jvm.options index f55d90933ed61..94fc6f2cb9025 100644 --- a/distribution/src/config/jvm.options +++ b/distribution/src/config/jvm.options @@ -9,7 +9,7 @@ ## should create one or more files in the jvm.options.d ## directory containing your adjustments. ## -## See https://www.elastic.co/guide/en/elasticsearch/reference/@project.minor.version@/jvm-options.html +## See https://www.elastic.co/guide/en/elasticsearch/reference/@project.minor.version@/advanced-configuration.html#set-jvm-options ## for more information. ## ################################################################ From 65faabd08d79043d10a0351e57f1b0c6239862da Mon Sep 17 00:00:00 2001 From: Stanislav Malyshev Date: Wed, 18 Dec 2024 11:50:18 -0700 Subject: [PATCH 095/119] Refactor pausable field plugin to have common codebase (#118909) --- .../action/AbstractPausableIntegTestCase.java | 62 +------------ .../esql/action/AbstractPauseFieldPlugin.java | 86 +++++++++++++++++++ .../esql/action/CrossClusterAsyncQueryIT.java | 72 +--------------- .../action/CrossClustersCancellationIT.java | 80 ++--------------- .../esql/action/SimplePauseFieldPlugin.java | 36 ++++++++ 5 files changed, 136 insertions(+), 200 deletions(-) create mode 100644 x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPauseFieldPlugin.java create mode 100644 x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/SimplePauseFieldPlugin.java diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPausableIntegTestCase.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPausableIntegTestCase.java index 8de65847c3f85..8054b260f0060 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPausableIntegTestCase.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPausableIntegTestCase.java @@ -10,26 +10,15 @@ import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.index.engine.SegmentsStats; -import org.elasticsearch.index.mapper.OnScriptError; -import org.elasticsearch.logging.LogManager; -import org.elasticsearch.logging.Logger; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.plugins.ScriptPlugin; -import org.elasticsearch.script.LongFieldScript; -import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptEngine; -import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.json.JsonXContent; import org.junit.Before; import java.io.IOException; import java.util.Collection; -import java.util.Map; -import java.util.Set; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -40,8 +29,6 @@ */ public abstract class AbstractPausableIntegTestCase extends AbstractEsqlIntegTestCase { - private static final Logger LOGGER = LogManager.getLogger(AbstractPausableIntegTestCase.class); - protected static final Semaphore scriptPermits = new Semaphore(0); protected int pageSize = -1; @@ -108,53 +95,10 @@ public void setupIndex() throws IOException { } } - public static class PausableFieldPlugin extends Plugin implements ScriptPlugin { - + public static class PausableFieldPlugin extends AbstractPauseFieldPlugin { @Override - public ScriptEngine getScriptEngine(Settings settings, Collection> contexts) { - return new ScriptEngine() { - @Override - public String getType() { - return "pause"; - } - - @Override - @SuppressWarnings("unchecked") - public FactoryType compile( - String name, - String code, - ScriptContext context, - Map params - ) { - return (FactoryType) new LongFieldScript.Factory() { - @Override - public LongFieldScript.LeafFactory newFactory( - String fieldName, - Map params, - SearchLookup searchLookup, - OnScriptError onScriptError - ) { - return ctx -> new LongFieldScript(fieldName, params, searchLookup, onScriptError, ctx) { - @Override - public void execute() { - try { - assertTrue(scriptPermits.tryAcquire(1, TimeUnit.MINUTES)); - } catch (Exception e) { - throw new AssertionError(e); - } - LOGGER.debug("--> emitting value"); - emit(1); - } - }; - } - }; - } - - @Override - public Set> getSupportedContexts() { - return Set.of(LongFieldScript.CONTEXT); - } - }; + protected boolean onWait() throws InterruptedException { + return scriptPermits.tryAcquire(1, TimeUnit.MINUTES); } } } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPauseFieldPlugin.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPauseFieldPlugin.java new file mode 100644 index 0000000000000..5554f7e571dfb --- /dev/null +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPauseFieldPlugin.java @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.action; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.mapper.OnScriptError; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.ScriptPlugin; +import org.elasticsearch.script.LongFieldScript; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.search.lookup.SearchLookup; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertTrue; + +/** + * A plugin that provides a script language "pause" that can be used to simulate slow running queries. + * See also {@link AbstractPausableIntegTestCase}. + */ +public abstract class AbstractPauseFieldPlugin extends Plugin implements ScriptPlugin { + + // Called when the engine enters the execute() method. + protected void onStartExecute() {} + + // Called when the engine needs to wait for further execution to be allowed. + protected abstract boolean onWait() throws InterruptedException; + + @Override + public ScriptEngine getScriptEngine(Settings settings, Collection> contexts) { + return new ScriptEngine() { + @Override + public String getType() { + return "pause"; + } + + @Override + @SuppressWarnings("unchecked") + public FactoryType compile( + String name, + String code, + ScriptContext context, + Map params + ) { + if (context == LongFieldScript.CONTEXT) { + return (FactoryType) new LongFieldScript.Factory() { + @Override + public LongFieldScript.LeafFactory newFactory( + String fieldName, + Map params, + SearchLookup searchLookup, + OnScriptError onScriptError + ) { + return ctx -> new LongFieldScript(fieldName, params, searchLookup, onScriptError, ctx) { + @Override + public void execute() { + onStartExecute(); + try { + assertTrue(onWait()); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + emit(1); + } + }; + } + }; + } + throw new IllegalStateException("unsupported type " + context); + } + + @Override + public Set> getSupportedContexts() { + return Set.of(LongFieldScript.CONTEXT); + } + }; + } +} diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterAsyncQueryIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterAsyncQueryIT.java index a2bba19db50fc..3926ea4c27a3d 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterAsyncQueryIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterAsyncQueryIT.java @@ -19,14 +19,8 @@ import org.elasticsearch.compute.operator.exchange.ExchangeService; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; -import org.elasticsearch.index.mapper.OnScriptError; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.plugins.ScriptPlugin; -import org.elasticsearch.script.LongFieldScript; -import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptEngine; -import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.test.AbstractMultiClustersTestCase; import org.elasticsearch.test.XContentTestUtils; import org.elasticsearch.transport.RemoteClusterAware; @@ -44,7 +38,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -80,7 +73,7 @@ protected Collection> nodePlugins(String clusterAlias) { plugins.add(EsqlPluginWithEnterpriseOrTrialLicense.class); plugins.add(EsqlAsyncActionIT.LocalStateEsqlAsync.class); // allows the async_search DELETE action plugins.add(InternalExchangePlugin.class); - plugins.add(PauseFieldPlugin.class); + plugins.add(SimplePauseFieldPlugin.class); return plugins; } @@ -99,64 +92,7 @@ public List> getSettings() { @Before public void resetPlugin() { - PauseFieldPlugin.allowEmitting = new CountDownLatch(1); - PauseFieldPlugin.startEmitting = new CountDownLatch(1); - } - - public static class PauseFieldPlugin extends Plugin implements ScriptPlugin { - public static CountDownLatch startEmitting = new CountDownLatch(1); - public static CountDownLatch allowEmitting = new CountDownLatch(1); - - @Override - public ScriptEngine getScriptEngine(Settings settings, Collection> contexts) { - return new ScriptEngine() { - @Override - - public String getType() { - return "pause"; - } - - @Override - @SuppressWarnings("unchecked") - public FactoryType compile( - String name, - String code, - ScriptContext context, - Map params - ) { - if (context == LongFieldScript.CONTEXT) { - return (FactoryType) new LongFieldScript.Factory() { - @Override - public LongFieldScript.LeafFactory newFactory( - String fieldName, - Map params, - SearchLookup searchLookup, - OnScriptError onScriptError - ) { - return ctx -> new LongFieldScript(fieldName, params, searchLookup, onScriptError, ctx) { - @Override - public void execute() { - startEmitting.countDown(); - try { - assertTrue(allowEmitting.await(30, TimeUnit.SECONDS)); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - emit(1); - } - }; - } - }; - } - throw new IllegalStateException("unsupported type " + context); - } - - @Override - public Set> getSupportedContexts() { - return Set.of(LongFieldScript.CONTEXT); - } - }; - } + SimplePauseFieldPlugin.resetPlugin(); } /** @@ -184,7 +120,7 @@ public void testSuccessfulPathways() throws Exception { } // wait until we know that the query against 'remote-b:blocking' has started - PauseFieldPlugin.startEmitting.await(30, TimeUnit.SECONDS); + SimplePauseFieldPlugin.startEmitting.await(30, TimeUnit.SECONDS); // wait until the query of 'cluster-a:logs-*' has finished (it is not blocked since we are not searching the 'blocking' index on it) assertBusy(() -> { @@ -234,7 +170,7 @@ public void testSuccessfulPathways() throws Exception { } // allow remoteB query to proceed - PauseFieldPlugin.allowEmitting.countDown(); + SimplePauseFieldPlugin.allowEmitting.countDown(); // wait until both remoteB and local queries have finished assertBusy(() -> { diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersCancellationIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersCancellationIT.java index 17f5f81486651..cfe6fdeccb190 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersCancellationIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersCancellationIT.java @@ -15,18 +15,11 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.compute.operator.DriverTaskRunner; import org.elasticsearch.compute.operator.exchange.ExchangeService; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.index.mapper.OnScriptError; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.plugins.ScriptPlugin; -import org.elasticsearch.script.LongFieldScript; -import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.script.ScriptEngine; -import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.tasks.TaskInfo; import org.elasticsearch.test.AbstractMultiClustersTestCase; import org.elasticsearch.transport.TransportService; @@ -38,9 +31,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.elasticsearch.xpack.esql.EsqlTestUtils.getValuesList; @@ -63,7 +53,7 @@ protected Collection> nodePlugins(String clusterAlias) { List> plugins = new ArrayList<>(super.nodePlugins(clusterAlias)); plugins.add(EsqlPluginWithEnterpriseOrTrialLicense.class); plugins.add(InternalExchangePlugin.class); - plugins.add(PauseFieldPlugin.class); + plugins.add(SimplePauseFieldPlugin.class); return plugins; } @@ -82,63 +72,7 @@ public List> getSettings() { @Before public void resetPlugin() { - PauseFieldPlugin.allowEmitting = new CountDownLatch(1); - PauseFieldPlugin.startEmitting = new CountDownLatch(1); - } - - public static class PauseFieldPlugin extends Plugin implements ScriptPlugin { - public static CountDownLatch startEmitting = new CountDownLatch(1); - public static CountDownLatch allowEmitting = new CountDownLatch(1); - - @Override - public ScriptEngine getScriptEngine(Settings settings, Collection> contexts) { - return new ScriptEngine() { - @Override - public String getType() { - return "pause"; - } - - @Override - @SuppressWarnings("unchecked") - public FactoryType compile( - String name, - String code, - ScriptContext context, - Map params - ) { - if (context == LongFieldScript.CONTEXT) { - return (FactoryType) new LongFieldScript.Factory() { - @Override - public LongFieldScript.LeafFactory newFactory( - String fieldName, - Map params, - SearchLookup searchLookup, - OnScriptError onScriptError - ) { - return ctx -> new LongFieldScript(fieldName, params, searchLookup, onScriptError, ctx) { - @Override - public void execute() { - startEmitting.countDown(); - try { - assertTrue(allowEmitting.await(30, TimeUnit.SECONDS)); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - emit(1); - } - }; - } - }; - } - throw new IllegalStateException("unsupported type " + context); - } - - @Override - public Set> getSupportedContexts() { - return Set.of(LongFieldScript.CONTEXT); - } - }; - } + SimplePauseFieldPlugin.resetPlugin(); } private void createRemoteIndex(int numDocs) throws Exception { @@ -169,7 +103,7 @@ public void testCancel() throws Exception { request.pragmas(randomPragmas()); PlainActionFuture requestFuture = new PlainActionFuture<>(); client().execute(EsqlQueryAction.INSTANCE, request, requestFuture); - assertTrue(PauseFieldPlugin.startEmitting.await(30, TimeUnit.SECONDS)); + assertTrue(SimplePauseFieldPlugin.startEmitting.await(30, TimeUnit.SECONDS)); List rootTasks = new ArrayList<>(); assertBusy(() -> { List tasks = client().admin().cluster().prepareListTasks().setActions(EsqlQueryAction.NAME).get().getTasks(); @@ -192,7 +126,7 @@ public void testCancel() throws Exception { } }); } finally { - PauseFieldPlugin.allowEmitting.countDown(); + SimplePauseFieldPlugin.allowEmitting.countDown(); } Exception error = expectThrows(Exception.class, requestFuture::actionGet); assertThat(error.getMessage(), containsString("proxy timeout")); @@ -223,7 +157,7 @@ public void testSameRemoteClusters() throws Exception { assertThat(tasks, hasSize(moreClusters + 1)); }); } finally { - PauseFieldPlugin.allowEmitting.countDown(); + SimplePauseFieldPlugin.allowEmitting.countDown(); } try (EsqlQueryResponse resp = future.actionGet(30, TimeUnit.SECONDS)) { // TODO: This produces incorrect results because data on the remote cluster is processed multiple times. @@ -244,7 +178,7 @@ public void testTasks() throws Exception { request.query("FROM *:test | STATS total=sum(const) | LIMIT 1"); request.pragmas(randomPragmas()); ActionFuture requestFuture = client().execute(EsqlQueryAction.INSTANCE, request); - assertTrue(PauseFieldPlugin.startEmitting.await(30, TimeUnit.SECONDS)); + assertTrue(SimplePauseFieldPlugin.startEmitting.await(30, TimeUnit.SECONDS)); try { assertBusy(() -> { List clusterTasks = client(REMOTE_CLUSTER).admin() @@ -270,7 +204,7 @@ public void testTasks() throws Exception { \\_ExchangeSinkOperator""")); }); } finally { - PauseFieldPlugin.allowEmitting.countDown(); + SimplePauseFieldPlugin.allowEmitting.countDown(); } requestFuture.actionGet(30, TimeUnit.SECONDS).close(); } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/SimplePauseFieldPlugin.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/SimplePauseFieldPlugin.java new file mode 100644 index 0000000000000..3ba73dd9a402e --- /dev/null +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/SimplePauseFieldPlugin.java @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.action; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * A plugin that provides a script language "pause" that can be used to simulate slow running queries. + * This implementation allows to know when it arrives at execute() via startEmitting and to allow the execution to proceed + * via allowEmitting. + */ +public class SimplePauseFieldPlugin extends AbstractPauseFieldPlugin { + public static CountDownLatch startEmitting = new CountDownLatch(1); + public static CountDownLatch allowEmitting = new CountDownLatch(1); + + public static void resetPlugin() { + allowEmitting = new CountDownLatch(1); + startEmitting = new CountDownLatch(1); + } + + @Override + public void onStartExecute() { + startEmitting.countDown(); + } + + @Override + public boolean onWait() throws InterruptedException { + return allowEmitting.await(30, TimeUnit.SECONDS); + } +} From 9cc362b9667f0903b1f124968cfb817cb56c86aa Mon Sep 17 00:00:00 2001 From: Patrick Doyle <810052+prdoyle@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:08:51 -0500 Subject: [PATCH 096/119] Entitlements: More robust frame skipping (#118983) * More robust frame skipping * Cosmetic improvements for clarity * Explicit set of runtime classes * Pass entitlements runtime module to PolicyManager ctor * Use the term "entitlements module" and filter instead of dropWhile * [CI] Auto commit changes from spotless --------- Co-authored-by: elasticsearchmachine --- .../EntitlementInitialization.java | 3 +- .../runtime/policy/PolicyManager.java | 71 ++++++++++---- .../runtime/policy/PolicyManagerTests.java | 94 +++++++++++++++++-- 3 files changed, 140 insertions(+), 28 deletions(-) diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java index 9118f67cdc145..8e4cddc4d63ee 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java @@ -53,6 +53,7 @@ public class EntitlementInitialization { private static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; + private static final Module ENTITLEMENTS_MODULE = PolicyManager.class.getModule(); private static ElasticsearchEntitlementChecker manager; @@ -92,7 +93,7 @@ private static PolicyManager createPolicyManager() throws IOException { "server", List.of(new Scope("org.elasticsearch.server", List.of(new ExitVMEntitlement(), new CreateClassLoaderEntitlement()))) ); - return new PolicyManager(serverPolicy, pluginPolicies, EntitlementBootstrap.bootstrapArgs().pluginResolver()); + return new PolicyManager(serverPolicy, pluginPolicies, EntitlementBootstrap.bootstrapArgs().pluginResolver(), ENTITLEMENTS_MODULE); } private static Map createPluginPolicies(Collection pluginData) throws IOException { diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index 8d3efe4eb98e6..74ba986041dac 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -15,6 +15,7 @@ import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; +import java.lang.StackWalker.StackFrame; import java.lang.module.ModuleFinder; import java.lang.module.ModuleReference; import java.util.ArrayList; @@ -29,6 +30,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; +import static java.util.Objects.requireNonNull; +import static java.util.function.Predicate.not; + public class PolicyManager { private static final Logger logger = LogManager.getLogger(ElasticsearchEntitlementChecker.class); @@ -63,6 +68,11 @@ public Stream getEntitlements(Class entitlementCla private static final Set systemModules = findSystemModules(); + /** + * Frames originating from this module are ignored in the permission logic. + */ + private final Module entitlementsModule; + private static Set findSystemModules() { var systemModulesDescriptors = ModuleFinder.ofSystem() .findAll() @@ -77,13 +87,18 @@ private static Set findSystemModules() { .collect(Collectors.toUnmodifiableSet()); } - public PolicyManager(Policy defaultPolicy, Map pluginPolicies, Function, String> pluginResolver) { - this.serverEntitlements = buildScopeEntitlementsMap(Objects.requireNonNull(defaultPolicy)); - this.pluginsEntitlements = Objects.requireNonNull(pluginPolicies) - .entrySet() + public PolicyManager( + Policy defaultPolicy, + Map pluginPolicies, + Function, String> pluginResolver, + Module entitlementsModule + ) { + this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(defaultPolicy)); + this.pluginsEntitlements = requireNonNull(pluginPolicies).entrySet() .stream() .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> buildScopeEntitlementsMap(e.getValue()))); this.pluginResolver = pluginResolver; + this.entitlementsModule = entitlementsModule; } private static Map> buildScopeEntitlementsMap(Policy policy) { @@ -185,7 +200,16 @@ private static boolean isServerModule(Module requestingModule) { return requestingModule.isNamed() && requestingModule.getLayer() == ModuleLayer.boot(); } - private static Module requestingModule(Class callerClass) { + /** + * Walks the stack to determine which module's entitlements should be checked. + * + * @param callerClass when non-null will be used if its module is suitable; + * this is a fast-path check that can avoid the stack walk + * in cases where the caller class is available. + * @return the requesting module, or {@code null} if the entire call stack + * comes from modules that are trusted. + */ + Module requestingModule(Class callerClass) { if (callerClass != null) { Module callerModule = callerClass.getModule(); if (systemModules.contains(callerModule) == false) { @@ -193,21 +217,34 @@ private static Module requestingModule(Class callerClass) { return callerModule; } } - int framesToSkip = 1 // getCallingClass (this method) - + 1 // the checkXxx method - + 1 // the runtime config method - + 1 // the instrumented method - ; - Optional module = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) - .walk( - s -> s.skip(framesToSkip) - .map(f -> f.getDeclaringClass().getModule()) - .filter(m -> systemModules.contains(m) == false) - .findFirst() - ); + Optional module = StackWalker.getInstance(RETAIN_CLASS_REFERENCE) + .walk(frames -> findRequestingModule(frames.map(StackFrame::getDeclaringClass))); return module.orElse(null); } + /** + * Given a stream of classes corresponding to the frames from a {@link StackWalker}, + * returns the module whose entitlements should be checked. + * + * @throws NullPointerException if the requesting module is {@code null} + */ + Optional findRequestingModule(Stream> classes) { + return classes.map(Objects::requireNonNull) + .map(PolicyManager::moduleOf) + .filter(m -> m != entitlementsModule) // Ignore the entitlements library itself + .filter(not(systemModules::contains)) // Skip trusted JDK modules + .findFirst(); + } + + private static Module moduleOf(Class c) { + var result = c.getModule(); + if (result == null) { + throw new NullPointerException("Entitlements system does not support non-modular class [" + c.getName() + "]"); + } else { + return result; + } + } + private static boolean isTriviallyAllowed(Module requestingModule) { if (requestingModule == null) { logger.debug("Entitlement trivially allowed: entire call stack is in composed of classes in system modules"); diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java index 45bdf2e457824..0789fcc8dc770 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import static java.util.Map.entry; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; @@ -37,11 +38,14 @@ @ESTestCase.WithoutSecurityManager public class PolicyManagerTests extends ESTestCase { + private static final Module NO_ENTITLEMENTS_MODULE = null; + public void testGetEntitlementsThrowsOnMissingPluginUnnamedModule() { var policyManager = new PolicyManager( createEmptyTestServerPolicy(), Map.of("plugin1", createPluginPolicy("plugin.module")), - c -> "plugin1" + c -> "plugin1", + NO_ENTITLEMENTS_MODULE ); // Any class from the current module (unnamed) will do @@ -62,7 +66,7 @@ public void testGetEntitlementsThrowsOnMissingPluginUnnamedModule() { } public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() { - var policyManager = new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "plugin1"); + var policyManager = new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "plugin1", NO_ENTITLEMENTS_MODULE); // Any class from the current module (unnamed) will do var callerClass = this.getClass(); @@ -82,7 +86,7 @@ public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() { } public void testGetEntitlementsFailureIsCached() { - var policyManager = new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "plugin1"); + var policyManager = new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "plugin1", NO_ENTITLEMENTS_MODULE); // Any class from the current module (unnamed) will do var callerClass = this.getClass(); @@ -103,7 +107,8 @@ public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() { var policyManager = new PolicyManager( createEmptyTestServerPolicy(), Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))), - c -> "plugin2" + c -> "plugin2", + NO_ENTITLEMENTS_MODULE ); // Any class from the current module (unnamed) will do @@ -115,7 +120,7 @@ public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() { } public void testGetEntitlementsThrowsOnMissingPolicyForServer() throws ClassNotFoundException { - var policyManager = new PolicyManager(createTestServerPolicy("example"), Map.of(), c -> null); + var policyManager = new PolicyManager(createTestServerPolicy("example"), Map.of(), c -> null, NO_ENTITLEMENTS_MODULE); // Tests do not run modular, so we cannot use a server class. // But we know that in production code the server module and its classes are in the boot layer. @@ -138,7 +143,7 @@ public void testGetEntitlementsThrowsOnMissingPolicyForServer() throws ClassNotF } public void testGetEntitlementsReturnsEntitlementsForServerModule() throws ClassNotFoundException { - var policyManager = new PolicyManager(createTestServerPolicy("jdk.httpserver"), Map.of(), c -> null); + var policyManager = new PolicyManager(createTestServerPolicy("jdk.httpserver"), Map.of(), c -> null, NO_ENTITLEMENTS_MODULE); // Tests do not run modular, so we cannot use a server class. // But we know that in production code the server module and its classes are in the boot layer. @@ -155,12 +160,13 @@ public void testGetEntitlementsReturnsEntitlementsForServerModule() throws Class public void testGetEntitlementsReturnsEntitlementsForPluginModule() throws IOException, ClassNotFoundException { final Path home = createTempDir(); - Path jar = creteMockPluginJar(home); + Path jar = createMockPluginJar(home); var policyManager = new PolicyManager( createEmptyTestServerPolicy(), Map.of("mock-plugin", createPluginPolicy("org.example.plugin")), - c -> "mock-plugin" + c -> "mock-plugin", + NO_ENTITLEMENTS_MODULE ); var layer = createLayerForJar(jar, "org.example.plugin"); @@ -179,7 +185,8 @@ public void testGetEntitlementsResultIsCached() { var policyManager = new PolicyManager( createEmptyTestServerPolicy(), Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))), - c -> "plugin2" + c -> "plugin2", + NO_ENTITLEMENTS_MODULE ); // Any class from the current module (unnamed) will do @@ -197,6 +204,73 @@ public void testGetEntitlementsResultIsCached() { assertThat(entitlementsAgain, sameInstance(cachedResult)); } + public void testRequestingModuleFastPath() throws IOException, ClassNotFoundException { + var callerClass = makeClassInItsOwnModule(); + assertEquals(callerClass.getModule(), policyManagerWithEntitlementsModule(NO_ENTITLEMENTS_MODULE).requestingModule(callerClass)); + } + + public void testRequestingModuleWithStackWalk() throws IOException, ClassNotFoundException { + var requestingClass = makeClassInItsOwnModule(); + var runtimeClass = makeClassInItsOwnModule(); // A class in the entitlements library itself + var ignorableClass = makeClassInItsOwnModule(); + var systemClass = Object.class; + + var policyManager = policyManagerWithEntitlementsModule(runtimeClass.getModule()); + + var requestingModule = requestingClass.getModule(); + + assertEquals( + "Skip one system frame", + requestingModule, + policyManager.findRequestingModule(Stream.of(systemClass, requestingClass, ignorableClass)).orElse(null) + ); + assertEquals( + "Skip multiple system frames", + requestingModule, + policyManager.findRequestingModule(Stream.of(systemClass, systemClass, systemClass, requestingClass, ignorableClass)) + .orElse(null) + ); + assertEquals( + "Skip system frame between runtime frames", + requestingModule, + policyManager.findRequestingModule(Stream.of(runtimeClass, systemClass, runtimeClass, requestingClass, ignorableClass)) + .orElse(null) + ); + assertEquals( + "Skip runtime frame between system frames", + requestingModule, + policyManager.findRequestingModule(Stream.of(systemClass, runtimeClass, systemClass, requestingClass, ignorableClass)) + .orElse(null) + ); + assertEquals( + "No system frames", + requestingModule, + policyManager.findRequestingModule(Stream.of(requestingClass, ignorableClass)).orElse(null) + ); + assertEquals( + "Skip runtime frames up to the first system frame", + requestingModule, + policyManager.findRequestingModule(Stream.of(runtimeClass, runtimeClass, systemClass, requestingClass, ignorableClass)) + .orElse(null) + ); + assertThrows( + "Non-modular caller frames are not supported", + NullPointerException.class, + () -> policyManager.findRequestingModule(Stream.of(systemClass, null)) + ); + } + + private static Class makeClassInItsOwnModule() throws IOException, ClassNotFoundException { + final Path home = createTempDir(); + Path jar = createMockPluginJar(home); + var layer = createLayerForJar(jar, "org.example.plugin"); + return layer.findLoader("org.example.plugin").loadClass("q.B"); + } + + private static PolicyManager policyManagerWithEntitlementsModule(Module entitlementsModule) { + return new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "test", entitlementsModule); + } + private static Policy createEmptyTestServerPolicy() { return new Policy("server", List.of()); } @@ -219,7 +293,7 @@ private static Policy createPluginPolicy(String... pluginModules) { ); } - private static Path creteMockPluginJar(Path home) throws IOException { + private static Path createMockPluginJar(Path home) throws IOException { Path jar = home.resolve("mock-plugin.jar"); Map sources = Map.ofEntries( From 7d301185bf1a650db09bb87033be70141353a5a5 Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Wed, 18 Dec 2024 20:40:11 +0100 Subject: [PATCH 097/119] Don't throw VerificationException on illegal state (#118826) If we end up here, we need to know this - and we won't know it if we return a 400 to the user. This should be a 500. --- .../elasticsearch/xpack/esql/analysis/Analyzer.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java index ecd0821c626bf..3d1bfdfd0ef42 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java @@ -691,15 +691,9 @@ private List resolveUsingColumns(List cols, List Date: Wed, 18 Dec 2024 19:47:12 +0000 Subject: [PATCH 098/119] Add a generic `rescorer` retriever based on the search request's rescore functionality (#118585) This pull request introduces a new retriever called `rescorer`, which leverages the `rescore` functionality of the search request. The `rescorer` retriever re-scores only the top documents retrieved by its child retriever, offering fine-tuned scoring capabilities. All rescorers supported in the `rescore` section of a search request are available in this retriever, and the same format is used to define the rescore configuration.
    Example: ```yaml - do: search: index: test body: retriever: rescorer: rescore: window_size: 10 query: rescore_query: rank_feature: field: "features.second_stage" linear: { } query_weight: 0 retriever: standard: query: rank_feature: field: "features.first_stage" linear: { } size: 2 ```
    Closes #118327 Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> --- docs/changelog/118585.yaml | 7 + docs/reference/search/retriever.asciidoc | 121 +++++- rest-api-spec/build.gradle | 1 + .../30_rescorer_retriever.yml | 225 ++++++++++ .../test/search/90_search_after.yml | 25 -- .../search/functionscore/QueryRescorerIT.java | 23 + .../search/DefaultSearchContext.java | 3 +- .../elasticsearch/search/SearchFeatures.java | 7 + .../elasticsearch/search/SearchModule.java | 2 + .../search/builder/SearchSourceBuilder.java | 14 - .../query/QueryPhaseCollectorManager.java | 3 +- .../search/rescore/RescorePhase.java | 102 ++++- .../search/rescore/RescorerBuilder.java | 2 +- .../retriever/CompoundRetrieverBuilder.java | 43 +- .../retriever/RescorerRetrieverBuilder.java | 173 ++++++++ .../search/retriever/RetrieverBuilder.java | 2 +- .../search/DefaultSearchContextTests.java | 6 +- .../RescorerRetrieverBuilderParsingTests.java | 78 ++++ .../retriever/QueryRuleRetrieverBuilder.java | 1 - .../TextSimilarityRankRetrieverBuilder.java | 1 - x-pack/plugin/rank-rrf/build.gradle | 1 + .../xpack/rank/rrf/RRFRetrieverBuilder.java | 1 - .../rrf/RRFRankClientYamlTestSuiteIT.java | 1 + .../test/rrf/900_rrf_with_rescorer.yml | 409 ++++++++++++++++++ 24 files changed, 1180 insertions(+), 71 deletions(-) create mode 100644 docs/changelog/118585.yaml create mode 100644 rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/30_rescorer_retriever.yml create mode 100644 server/src/main/java/org/elasticsearch/search/retriever/RescorerRetrieverBuilder.java create mode 100644 server/src/test/java/org/elasticsearch/search/retriever/RescorerRetrieverBuilderParsingTests.java create mode 100644 x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/rrf/900_rrf_with_rescorer.yml diff --git a/docs/changelog/118585.yaml b/docs/changelog/118585.yaml new file mode 100644 index 0000000000000..4caa5efabbd33 --- /dev/null +++ b/docs/changelog/118585.yaml @@ -0,0 +1,7 @@ +pr: 118585 +summary: Add a generic `rescorer` retriever based on the search request's rescore + functionality +area: Ranking +type: feature +issues: + - 118327 diff --git a/docs/reference/search/retriever.asciidoc b/docs/reference/search/retriever.asciidoc index f20e9148bf5e7..c7df40ff5e070 100644 --- a/docs/reference/search/retriever.asciidoc +++ b/docs/reference/search/retriever.asciidoc @@ -22,6 +22,9 @@ A <> that replaces the functionality of a traditi `knn`:: A <> that replaces the functionality of a <>. +`rescorer`:: +A <> that replaces the functionality of the <>. + `rrf`:: A <> that produces top documents from <>. @@ -371,6 +374,122 @@ GET movies/_search ---- // TEST[skip:uses ELSER] +[[rescorer-retriever]] +==== Rescorer Retriever + +The `rescorer` retriever re-scores only the results produced by its child retriever. +For the `standard` and `knn` retrievers, the `window_size` parameter specifies the number of documents examined per shard. + +For compound retrievers like `rrf`, the `window_size` parameter defines the total number of documents examined globally. + +When using the `rescorer`, an error is returned if the following conditions are not met: + +* The minimum configured rescore's `window_size` is: +** Greater than or equal to the `size` of the parent retriever for nested `rescorer` setups. +** Greater than or equal to the `size` of the search request when used as the primary retriever in the tree. + +* And the maximum rescore's `window_size` is: +** Smaller than or equal to the `size` or `rank_window_size` of the child retriever. + +[discrete] +[[rescorer-retriever-parameters]] +===== Parameters + +`rescore`:: +(Required. <>) ++ +Defines the <> applied sequentially to the top documents returned by the child retriever. + +`retriever`:: +(Required. <>) ++ +Specifies the child retriever responsible for generating the initial set of top documents to be re-ranked. + +`filter`:: +(Optional. <>) ++ +Applies a <> to the retriever, ensuring that all documents match the filter criteria without affecting their scores. + +[discrete] +[[rescorer-retriever-example]] +==== Example + +The `rescorer` retriever can be placed at any level within the retriever tree. +The following example demonstrates a `rescorer` applied to the results produced by an `rrf` retriever: + +[source,console] +---- +GET movies/_search +{ + "size": 10, <1> + "retriever": { + "rescorer": { <2> + "rescore": { + "query": { <3> + "window_size": 50, <4> + "rescore_query": { + "script_score": { + "script": { + "source": "cosineSimilarity(params.queryVector, 'product-vector_final_stage') + 1.0", + "params": { + "queryVector": [-0.5, 90.0, -10, 14.8, -156.0] + } + } + } + } + } + }, + "retriever": { <5> + "rrf": { + "rank_window_size": 100, <6> + "retrievers": [ + { + "standard": { + "query": { + "sparse_vector": { + "field": "plot_embedding", + "inference_id": "my-elser-model", + "query": "films that explore psychological depths" + } + } + } + }, + { + "standard": { + "query": { + "multi_match": { + "query": "crime", + "fields": [ + "plot", + "title" + ] + } + } + } + }, + { + "knn": { + "field": "vector", + "query_vector": [10, 22, 77], + "k": 10, + "num_candidates": 10 + } + } + ] + } + } + } + } +} +---- +// TEST[skip:uses ELSER] +<1> Specifies the number of top documents to return in the final response. +<2> A `rescorer` retriever applied as the final step. +<3> The definition of the `query` rescorer. +<4> Defines the number of documents to rescore from the child retriever. +<5> Specifies the child retriever definition. +<6> Defines the number of documents returned by the `rrf` retriever, which limits the available documents to + [[text-similarity-reranker-retriever]] ==== Text Similarity Re-ranker Retriever @@ -777,4 +896,4 @@ When a retriever is specified as part of a search, the following elements are no * <> * <> * <> -* <> +* <> use a <> instead diff --git a/rest-api-spec/build.gradle b/rest-api-spec/build.gradle index bdee32e596c4c..f23b5460f7d53 100644 --- a/rest-api-spec/build.gradle +++ b/rest-api-spec/build.gradle @@ -70,4 +70,5 @@ tasks.named("yamlRestCompatTestTransform").configure ({ task -> task.skipTest("search.vectors/41_knn_search_bbq_hnsw/Test knn search", "Scoring has changed in latest versions") task.skipTest("search.vectors/42_knn_search_bbq_flat/Test knn search", "Scoring has changed in latest versions") task.skipTest("synonyms/90_synonyms_reloading_for_synset/Reload analyzers for specific synonym set", "Can't work until auto-expand replicas is 0-1 for synonyms index") + task.skipTest("search/90_search_after/_shard_doc sort", "restriction has been lifted in latest versions") }) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/30_rescorer_retriever.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/30_rescorer_retriever.yml new file mode 100644 index 0000000000000..2c16de61c6b15 --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/30_rescorer_retriever.yml @@ -0,0 +1,225 @@ +setup: + - requires: + cluster_features: [ "search.retriever.rescorer.enabled" ] + reason: "Support for rescorer retriever" + + - do: + indices.create: + index: test + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + properties: + available: + type: boolean + features: + type: rank_features + + - do: + bulk: + refresh: true + index: test + body: + - '{"index": {"_id": 1 }}' + - '{"features": { "first_stage": 1, "second_stage": 10}, "available": true, "group": 1}' + - '{"index": {"_id": 2 }}' + - '{"features": { "first_stage": 2, "second_stage": 9}, "available": false, "group": 1}' + - '{"index": {"_id": 3 }}' + - '{"features": { "first_stage": 3, "second_stage": 8}, "available": false, "group": 3}' + - '{"index": {"_id": 4 }}' + - '{"features": { "first_stage": 4, "second_stage": 7}, "available": true, "group": 1}' + - '{"index": {"_id": 5 }}' + - '{"features": { "first_stage": 5, "second_stage": 6}, "available": true, "group": 3}' + - '{"index": {"_id": 6 }}' + - '{"features": { "first_stage": 6, "second_stage": 5}, "available": false, "group": 2}' + - '{"index": {"_id": 7 }}' + - '{"features": { "first_stage": 7, "second_stage": 4}, "available": true, "group": 3}' + - '{"index": {"_id": 8 }}' + - '{"features": { "first_stage": 8, "second_stage": 3}, "available": true, "group": 1}' + - '{"index": {"_id": 9 }}' + - '{"features": { "first_stage": 9, "second_stage": 2}, "available": true, "group": 2}' + - '{"index": {"_id": 10 }}' + - '{"features": { "first_stage": 10, "second_stage": 1}, "available": false, "group": 1}' + +--- +"Rescorer retriever basic": + - do: + search: + index: test + body: + retriever: + rescorer: + rescore: + window_size: 10 + query: + rescore_query: + rank_feature: + field: "features.second_stage" + linear: { } + query_weight: 0 + retriever: + standard: + query: + rank_feature: + field: "features.first_stage" + linear: { } + size: 2 + + - match: { hits.total.value: 10 } + - match: { hits.hits.0._id: "1" } + - match: { hits.hits.0._score: 10.0 } + - match: { hits.hits.1._id: "2" } + - match: { hits.hits.1._score: 9.0 } + + - do: + search: + index: test + body: + retriever: + rescorer: + rescore: + window_size: 3 + query: + rescore_query: + rank_feature: + field: "features.second_stage" + linear: {} + query_weight: 0 + retriever: + standard: + query: + rank_feature: + field: "features.first_stage" + linear: {} + size: 2 + + - match: {hits.total.value: 10} + - match: {hits.hits.0._id: "8"} + - match: { hits.hits.0._score: 3.0 } + - match: {hits.hits.1._id: "9"} + - match: { hits.hits.1._score: 2.0 } + +--- +"Rescorer retriever with pre-filters": + - do: + search: + index: test + body: + retriever: + rescorer: + filter: + match: + available: true + rescore: + window_size: 10 + query: + rescore_query: + rank_feature: + field: "features.second_stage" + linear: { } + query_weight: 0 + retriever: + standard: + query: + rank_feature: + field: "features.first_stage" + linear: { } + size: 2 + + - match: { hits.total.value: 6 } + - match: { hits.hits.0._id: "1" } + - match: { hits.hits.0._score: 10.0 } + - match: { hits.hits.1._id: "4" } + - match: { hits.hits.1._score: 7.0 } + + - do: + search: + index: test + body: + retriever: + rescorer: + rescore: + window_size: 4 + query: + rescore_query: + rank_feature: + field: "features.second_stage" + linear: { } + query_weight: 0 + retriever: + standard: + filter: + match: + available: true + query: + rank_feature: + field: "features.first_stage" + linear: { } + size: 2 + + - match: { hits.total.value: 6 } + - match: { hits.hits.0._id: "5" } + - match: { hits.hits.0._score: 6.0 } + - match: { hits.hits.1._id: "7" } + - match: { hits.hits.1._score: 4.0 } + +--- +"Rescorer retriever and collapsing": + - do: + search: + index: test + body: + retriever: + rescorer: + rescore: + window_size: 10 + query: + rescore_query: + rank_feature: + field: "features.second_stage" + linear: { } + query_weight: 0 + retriever: + standard: + query: + rank_feature: + field: "features.first_stage" + linear: { } + collapse: + field: group + size: 3 + + - match: { hits.total.value: 10 } + - match: { hits.hits.0._id: "1" } + - match: { hits.hits.0._score: 10.0 } + - match: { hits.hits.1._id: "3" } + - match: { hits.hits.1._score: 8.0 } + - match: { hits.hits.2._id: "6" } + - match: { hits.hits.2._score: 5.0 } + +--- +"Rescorer retriever and invalid window size": + - do: + catch: "/\\[rescorer\\] requires \\[window_size: 5\\] be greater than or equal to \\[size: 10\\]/" + search: + index: test + body: + retriever: + rescorer: + rescore: + window_size: 5 + query: + rescore_query: + rank_feature: + field: "features.second_stage" + linear: { } + query_weight: 0 + retriever: + standard: + query: + rank_feature: + field: "features.first_stage" + linear: { } + size: 10 diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/90_search_after.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/90_search_after.yml index 1fefc8bffffa1..d3b2b5a412717 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/90_search_after.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/90_search_after.yml @@ -218,31 +218,6 @@ - match: {hits.hits.0._source.timestamp: "2019-10-21 00:30:04.828740" } - match: {hits.hits.0.sort: [1571617804828740000] } - ---- -"_shard_doc sort": - - requires: - cluster_features: ["gte_v7.12.0"] - reason: _shard_doc sort was added in 7.12 - - - do: - indices.create: - index: test - - do: - index: - index: test - id: "1" - body: { id: 1, foo: bar, age: 18 } - - - do: - catch: /\[_shard_doc\] sort field cannot be used without \[point in time\]/ - search: - index: test - body: - size: 1 - sort: ["_shard_doc"] - search_after: [ 0L ] - --- "Format sort values": - requires: diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/QueryRescorerIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/QueryRescorerIT.java index a7efb2fe0e68b..fbdcfe26d28ee 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/QueryRescorerIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/QueryRescorerIT.java @@ -38,6 +38,7 @@ import org.elasticsearch.search.collapse.CollapseBuilder; import org.elasticsearch.search.rescore.QueryRescoreMode; import org.elasticsearch.search.rescore.QueryRescorerBuilder; +import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.xcontent.ParseField; @@ -840,6 +841,20 @@ public void testRescorePhaseWithInvalidSort() throws Exception { } } ); + + assertResponse( + prepareSearch().addSort(SortBuilders.scoreSort()) + .addSort(new FieldSortBuilder(FieldSortBuilder.SHARD_DOC_FIELD_NAME)) + .setTrackScores(true) + .addRescorer(new QueryRescorerBuilder(matchAllQuery()).setRescoreQueryWeight(100.0f), 50), + response -> { + assertThat(response.getHits().getTotalHits().value(), equalTo(5L)); + assertThat(response.getHits().getHits().length, equalTo(5)); + for (SearchHit hit : response.getHits().getHits()) { + assertThat(hit.getScore(), equalTo(101f)); + } + } + ); } record GroupDoc(String id, String group, float firstPassScore, float secondPassScore, boolean shouldFilter) {} @@ -879,6 +894,10 @@ public void testRescoreAfterCollapse() throws Exception { .setQuery(fieldValueScoreQuery("firstPassScore")) .addRescorer(new QueryRescorerBuilder(fieldValueScoreQuery("secondPassScore"))) .setCollapse(new CollapseBuilder("group")); + if (randomBoolean()) { + request.addSort(SortBuilders.scoreSort()); + request.addSort(new FieldSortBuilder(FieldSortBuilder.SHARD_DOC_FIELD_NAME)); + } assertResponse(request, resp -> { assertThat(resp.getHits().getTotalHits().value(), equalTo(5L)); assertThat(resp.getHits().getHits().length, equalTo(3)); @@ -958,6 +977,10 @@ public void testRescoreAfterCollapseRandom() throws Exception { .addRescorer(new QueryRescorerBuilder(fieldValueScoreQuery("secondPassScore")).setQueryWeight(0f).windowSize(numGroups)) .setCollapse(new CollapseBuilder("group")) .setSize(Math.min(numGroups, 10)); + if (randomBoolean()) { + request.addSort(SortBuilders.scoreSort()); + request.addSort(new FieldSortBuilder(FieldSortBuilder.SHARD_DOC_FIELD_NAME)); + } long expectedNumHits = numHits; assertResponse(request, resp -> { assertThat(resp.getHits().getTotalHits().value(), equalTo(expectedNumHits)); diff --git a/server/src/main/java/org/elasticsearch/search/DefaultSearchContext.java b/server/src/main/java/org/elasticsearch/search/DefaultSearchContext.java index b87d097413b67..47d3ed337af73 100644 --- a/server/src/main/java/org/elasticsearch/search/DefaultSearchContext.java +++ b/server/src/main/java/org/elasticsearch/search/DefaultSearchContext.java @@ -73,6 +73,7 @@ import org.elasticsearch.search.rank.context.QueryPhaseRankShardContext; import org.elasticsearch.search.rank.feature.RankFeatureResult; import org.elasticsearch.search.rescore.RescoreContext; +import org.elasticsearch.search.rescore.RescorePhase; import org.elasticsearch.search.slice.SliceBuilder; import org.elasticsearch.search.sort.SortAndFormats; import org.elasticsearch.search.suggest.SuggestionSearchContext; @@ -377,7 +378,7 @@ public void preProcess() { ); } if (rescore != null) { - if (sort != null) { + if (RescorePhase.validateSort(sort) == false) { throw new IllegalArgumentException("Cannot use [sort] option in conjunction with [rescore]."); } int maxWindow = indexService.getIndexSettings().getMaxRescoreWindow(); diff --git a/server/src/main/java/org/elasticsearch/search/SearchFeatures.java b/server/src/main/java/org/elasticsearch/search/SearchFeatures.java index beac39c2de304..553511346b182 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchFeatures.java +++ b/server/src/main/java/org/elasticsearch/search/SearchFeatures.java @@ -23,4 +23,11 @@ public final class SearchFeatures implements FeatureSpecification { public Set getFeatures() { return Set.of(KnnVectorQueryBuilder.K_PARAM_SUPPORTED, LUCENE_10_0_0_UPGRADE); } + + public static final NodeFeature RETRIEVER_RESCORER_ENABLED = new NodeFeature("search.retriever.rescorer.enabled"); + + @Override + public Set getTestFeatures() { + return Set.of(RETRIEVER_RESCORER_ENABLED); + } } diff --git a/server/src/main/java/org/elasticsearch/search/SearchModule.java b/server/src/main/java/org/elasticsearch/search/SearchModule.java index d282ba425b126..3294e1ba03f6b 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/server/src/main/java/org/elasticsearch/search/SearchModule.java @@ -231,6 +231,7 @@ import org.elasticsearch.search.rescore.QueryRescorerBuilder; import org.elasticsearch.search.rescore.RescorerBuilder; import org.elasticsearch.search.retriever.KnnRetrieverBuilder; +import org.elasticsearch.search.retriever.RescorerRetrieverBuilder; import org.elasticsearch.search.retriever.RetrieverBuilder; import org.elasticsearch.search.retriever.RetrieverParserContext; import org.elasticsearch.search.retriever.StandardRetrieverBuilder; @@ -1080,6 +1081,7 @@ private void registerFetchSubPhase(FetchSubPhase subPhase) { private void registerRetrieverParsers(List plugins) { registerRetriever(new RetrieverSpec<>(StandardRetrieverBuilder.NAME, StandardRetrieverBuilder::fromXContent)); registerRetriever(new RetrieverSpec<>(KnnRetrieverBuilder.NAME, KnnRetrieverBuilder::fromXContent)); + registerRetriever(new RetrieverSpec<>(RescorerRetrieverBuilder.NAME, RescorerRetrieverBuilder::fromXContent)); registerFromPlugin(plugins, SearchPlugin::getRetrievers, this::registerRetriever); } diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index 3554a6dc08b90..8c21abe4180ea 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -48,9 +48,7 @@ import org.elasticsearch.search.retriever.RetrieverParserContext; import org.elasticsearch.search.searchafter.SearchAfterBuilder; import org.elasticsearch.search.slice.SliceBuilder; -import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.ScoreSortBuilder; -import org.elasticsearch.search.sort.ShardDocSortField; import org.elasticsearch.search.sort.SortBuilder; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; @@ -2341,18 +2339,6 @@ public ActionRequestValidationException validate( validationException = rescorer.validate(this, validationException); } } - - if (pointInTimeBuilder() == null && sorts() != null) { - for (var sortBuilder : sorts()) { - if (sortBuilder instanceof FieldSortBuilder fieldSortBuilder - && ShardDocSortField.NAME.equals(fieldSortBuilder.getFieldName())) { - validationException = addValidationError( - "[" + FieldSortBuilder.SHARD_DOC_FIELD_NAME + "] sort field cannot be used without [point in time]", - validationException - ); - } - } - } return validationException; } } diff --git a/server/src/main/java/org/elasticsearch/search/query/QueryPhaseCollectorManager.java b/server/src/main/java/org/elasticsearch/search/query/QueryPhaseCollectorManager.java index cbc04dd460ff5..3d793a164f40a 100644 --- a/server/src/main/java/org/elasticsearch/search/query/QueryPhaseCollectorManager.java +++ b/server/src/main/java/org/elasticsearch/search/query/QueryPhaseCollectorManager.java @@ -58,6 +58,7 @@ import org.elasticsearch.search.profile.query.CollectorResult; import org.elasticsearch.search.profile.query.InternalProfileCollector; import org.elasticsearch.search.rescore.RescoreContext; +import org.elasticsearch.search.rescore.RescorePhase; import org.elasticsearch.search.sort.SortAndFormats; import java.io.IOException; @@ -238,7 +239,7 @@ static CollectorManager createQueryPhaseCollectorMa int numDocs = Math.min(searchContext.from() + searchContext.size(), totalNumDocs); final boolean rescore = searchContext.rescore().isEmpty() == false; if (rescore) { - assert searchContext.sort() == null; + assert RescorePhase.validateSort(searchContext.sort()); for (RescoreContext rescoreContext : searchContext.rescore()) { numDocs = Math.max(numDocs, rescoreContext.getWindowSize()); } diff --git a/server/src/main/java/org/elasticsearch/search/rescore/RescorePhase.java b/server/src/main/java/org/elasticsearch/search/rescore/RescorePhase.java index 7e3646e7689cc..c23df9cdfa441 100644 --- a/server/src/main/java/org/elasticsearch/search/rescore/RescorePhase.java +++ b/server/src/main/java/org/elasticsearch/search/rescore/RescorePhase.java @@ -13,6 +13,7 @@ import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.SortField; import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.TopFieldDocs; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.search.SearchShardTask; import org.elasticsearch.common.lucene.search.TopDocsAndMaxScore; @@ -22,9 +23,12 @@ import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.query.QueryPhase; import org.elasticsearch.search.query.SearchTimeoutException; +import org.elasticsearch.search.sort.ShardDocSortField; +import org.elasticsearch.search.sort.SortAndFormats; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -39,15 +43,27 @@ public static void execute(SearchContext context) { if (context.size() == 0 || context.rescore() == null || context.rescore().isEmpty()) { return; } - + if (validateSort(context.sort()) == false) { + throw new IllegalStateException("Cannot use [sort] option in conjunction with [rescore], missing a validate?"); + } TopDocs topDocs = context.queryResult().topDocs().topDocs; if (topDocs.scoreDocs.length == 0) { return; } + // Populate FieldDoc#score using the primary sort field (_score) to ensure compatibility with top docs rescoring + Arrays.stream(topDocs.scoreDocs).forEach(t -> { + if (t instanceof FieldDoc fieldDoc) { + fieldDoc.score = (float) fieldDoc.fields[0]; + } + }); TopFieldGroups topGroups = null; + TopFieldDocs topFields = null; if (topDocs instanceof TopFieldGroups topFieldGroups) { - assert context.collapse() != null; + assert context.collapse() != null && validateSortFields(topFieldGroups.fields); topGroups = topFieldGroups; + } else if (topDocs instanceof TopFieldDocs topFieldDocs) { + assert validateSortFields(topFieldDocs.fields); + topFields = topFieldDocs; } try { Runnable cancellationCheck = getCancellationChecks(context); @@ -56,17 +72,18 @@ public static void execute(SearchContext context) { topDocs = ctx.rescorer().rescore(topDocs, context.searcher(), ctx); // It is the responsibility of the rescorer to sort the resulted top docs, // here we only assert that this condition is met. - assert context.sort() == null && topDocsSortedByScore(topDocs) : "topdocs should be sorted after rescore"; + assert topDocsSortedByScore(topDocs) : "topdocs should be sorted after rescore"; ctx.setCancellationChecker(null); } + /** + * Since rescorers are building top docs with score only, we must reconstruct the {@link TopFieldGroups} + * or {@link TopFieldDocs} using their original version before rescoring. + */ if (topGroups != null) { assert context.collapse() != null; - /** - * Since rescorers don't preserve collapsing, we must reconstruct the group and field - * values from the originalTopGroups to create a new {@link TopFieldGroups} from the - * rescored top documents. - */ - topDocs = rewriteTopGroups(topGroups, topDocs); + topDocs = rewriteTopFieldGroups(topGroups, topDocs); + } else if (topFields != null) { + topDocs = rewriteTopFieldDocs(topFields, topDocs); } context.queryResult() .topDocs(new TopDocsAndMaxScore(topDocs, topDocs.scoreDocs[0].score), context.queryResult().sortValueFormats()); @@ -81,29 +98,84 @@ public static void execute(SearchContext context) { } } - private static TopFieldGroups rewriteTopGroups(TopFieldGroups originalTopGroups, TopDocs rescoredTopDocs) { - assert originalTopGroups.fields.length == 1 && SortField.FIELD_SCORE.equals(originalTopGroups.fields[0]) - : "rescore must always sort by score descending"; + /** + * Returns whether the provided {@link SortAndFormats} can be used to rescore + * top documents. + */ + public static boolean validateSort(SortAndFormats sortAndFormats) { + if (sortAndFormats == null) { + return true; + } + return validateSortFields(sortAndFormats.sort.getSort()); + } + + private static boolean validateSortFields(SortField[] fields) { + if (fields[0].equals(SortField.FIELD_SCORE) == false) { + return false; + } + if (fields.length == 1) { + return true; + } + + // The ShardDocSortField can be used as a tiebreaker because it maintains + // the natural document ID order within the shard. + if (fields[1] instanceof ShardDocSortField == false || fields[1].getReverse()) { + return false; + } + return true; + } + + private static TopFieldDocs rewriteTopFieldDocs(TopFieldDocs originalTopFieldDocs, TopDocs rescoredTopDocs) { + Map docIdToFieldDoc = Maps.newMapWithExpectedSize(originalTopFieldDocs.scoreDocs.length); + for (int i = 0; i < originalTopFieldDocs.scoreDocs.length; i++) { + docIdToFieldDoc.put(originalTopFieldDocs.scoreDocs[i].doc, (FieldDoc) originalTopFieldDocs.scoreDocs[i]); + } + var newScoreDocs = new FieldDoc[rescoredTopDocs.scoreDocs.length]; + int pos = 0; + for (var doc : rescoredTopDocs.scoreDocs) { + newScoreDocs[pos] = docIdToFieldDoc.get(doc.doc); + newScoreDocs[pos].score = doc.score; + newScoreDocs[pos].fields[0] = newScoreDocs[pos].score; + pos++; + } + return new TopFieldDocs(originalTopFieldDocs.totalHits, newScoreDocs, originalTopFieldDocs.fields); + } + + private static TopFieldGroups rewriteTopFieldGroups(TopFieldGroups originalTopGroups, TopDocs rescoredTopDocs) { + var newFieldDocs = rewriteFieldDocs((FieldDoc[]) originalTopGroups.scoreDocs, rescoredTopDocs.scoreDocs); + Map docIdToGroupValue = Maps.newMapWithExpectedSize(originalTopGroups.scoreDocs.length); for (int i = 0; i < originalTopGroups.scoreDocs.length; i++) { docIdToGroupValue.put(originalTopGroups.scoreDocs[i].doc, originalTopGroups.groupValues[i]); } - var newScoreDocs = new FieldDoc[rescoredTopDocs.scoreDocs.length]; var newGroupValues = new Object[originalTopGroups.groupValues.length]; int pos = 0; for (var doc : rescoredTopDocs.scoreDocs) { - newScoreDocs[pos] = new FieldDoc(doc.doc, doc.score, new Object[] { doc.score }); newGroupValues[pos++] = docIdToGroupValue.get(doc.doc); } return new TopFieldGroups( originalTopGroups.field, originalTopGroups.totalHits, - newScoreDocs, + newFieldDocs, originalTopGroups.fields, newGroupValues ); } + private static FieldDoc[] rewriteFieldDocs(FieldDoc[] originalTopDocs, ScoreDoc[] rescoredTopDocs) { + Map docIdToFieldDoc = Maps.newMapWithExpectedSize(rescoredTopDocs.length); + Arrays.stream(originalTopDocs).forEach(d -> docIdToFieldDoc.put(d.doc, d)); + var newDocs = new FieldDoc[rescoredTopDocs.length]; + int pos = 0; + for (var doc : rescoredTopDocs) { + newDocs[pos] = docIdToFieldDoc.get(doc.doc); + newDocs[pos].score = doc.score; + newDocs[pos].fields[0] = doc.score; + pos++; + } + return newDocs; + } + /** * Returns true if the provided docs are sorted by score. */ diff --git a/server/src/main/java/org/elasticsearch/search/rescore/RescorerBuilder.java b/server/src/main/java/org/elasticsearch/search/rescore/RescorerBuilder.java index f624961515389..38a319321207f 100644 --- a/server/src/main/java/org/elasticsearch/search/rescore/RescorerBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/rescore/RescorerBuilder.java @@ -39,7 +39,7 @@ public abstract class RescorerBuilder> protected Integer windowSize; - private static final ParseField WINDOW_SIZE_FIELD = new ParseField("window_size"); + public static final ParseField WINDOW_SIZE_FIELD = new ParseField("window_size"); /** * Construct an empty RescoreBuilder. diff --git a/server/src/main/java/org/elasticsearch/search/retriever/CompoundRetrieverBuilder.java b/server/src/main/java/org/elasticsearch/search/retriever/CompoundRetrieverBuilder.java index 2ab6395db73b5..298340e5c579e 100644 --- a/server/src/main/java/org/elasticsearch/search/retriever/CompoundRetrieverBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/retriever/CompoundRetrieverBuilder.java @@ -32,10 +32,12 @@ import org.elasticsearch.search.sort.ScoreSortBuilder; import org.elasticsearch.search.sort.ShardDocSortField; import org.elasticsearch.search.sort.SortBuilder; +import org.elasticsearch.xcontent.ParseField; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -49,6 +51,8 @@ public abstract class CompoundRetrieverBuilder rankWindowSize) { validationException = addValidationError( - "[" - + this.getName() - + "] requires [rank_window_size: " - + rankWindowSize - + "]" - + " be greater than or equal to [size: " - + source.size() - + "]", + String.format( + Locale.ROOT, + "[%s] requires [%s: %d] be greater than or equal to [size: %d]", + getName(), + getRankWindowSizeField().getPreferredName(), + rankWindowSize, + source.size() + ), validationException ); } @@ -231,6 +243,21 @@ public ActionRequestValidationException validate( } for (RetrieverSource innerRetriever : innerRetrievers) { validationException = innerRetriever.retriever().validate(source, validationException, isScroll, allowPartialSearchResults); + if (innerRetriever.retriever() instanceof CompoundRetrieverBuilder compoundChild) { + if (rankWindowSize > compoundChild.rankWindowSize) { + String errorMessage = String.format( + Locale.ROOT, + "[%s] requires [%s: %d] to be smaller than or equal to its sub retriever's %s [%s: %d]", + this.getName(), + getRankWindowSizeField().getPreferredName(), + rankWindowSize, + compoundChild.getName(), + compoundChild.getRankWindowSizeField(), + compoundChild.rankWindowSize + ); + validationException = addValidationError(errorMessage, validationException); + } + } } return validationException; } diff --git a/server/src/main/java/org/elasticsearch/search/retriever/RescorerRetrieverBuilder.java b/server/src/main/java/org/elasticsearch/search/retriever/RescorerRetrieverBuilder.java new file mode 100644 index 0000000000000..09688b5b9b001 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/retriever/RescorerRetrieverBuilder.java @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.search.retriever; + +import org.apache.lucene.search.ScoreDoc; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.rank.RankDoc; +import org.elasticsearch.search.rescore.RescorerBuilder; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.search.builder.SearchSourceBuilder.RESCORE_FIELD; +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; + +/** + * A {@link CompoundRetrieverBuilder} that re-scores only the results produced by its child retriever. + */ +public final class RescorerRetrieverBuilder extends CompoundRetrieverBuilder { + + public static final String NAME = "rescorer"; + public static final ParseField RETRIEVER_FIELD = new ParseField("retriever"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + NAME, + args -> new RescorerRetrieverBuilder((RetrieverBuilder) args[0], (List>) args[1]) + ); + + static { + PARSER.declareNamedObject(constructorArg(), (parser, context, n) -> { + RetrieverBuilder innerRetriever = parser.namedObject(RetrieverBuilder.class, n, context); + context.trackRetrieverUsage(innerRetriever.getName()); + return innerRetriever; + }, RETRIEVER_FIELD); + PARSER.declareField(constructorArg(), (parser, context) -> { + if (parser.currentToken() == XContentParser.Token.START_ARRAY) { + List> rescorers = new ArrayList<>(); + while ((parser.nextToken()) != XContentParser.Token.END_ARRAY) { + rescorers.add(RescorerBuilder.parseFromXContent(parser, name -> context.trackRescorerUsage(name))); + } + return rescorers; + } else if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + return List.of(RescorerBuilder.parseFromXContent(parser, name -> context.trackRescorerUsage(name))); + } else { + throw new IllegalArgumentException( + "Unknown format for [rescorer.rescore], expects an object or an array of objects, got: " + parser.currentToken() + ); + } + }, RESCORE_FIELD, ObjectParser.ValueType.OBJECT_ARRAY); + RetrieverBuilder.declareBaseParserFields(NAME, PARSER); + } + + public static RescorerRetrieverBuilder fromXContent(XContentParser parser, RetrieverParserContext context) throws IOException { + try { + return PARSER.apply(parser, context); + } catch (Exception e) { + throw new ParsingException(parser.getTokenLocation(), e.getMessage(), e); + } + } + + private final List> rescorers; + + public RescorerRetrieverBuilder(RetrieverBuilder retriever, List> rescorers) { + super(List.of(new RetrieverSource(retriever, null)), extractMinWindowSize(rescorers)); + if (rescorers.isEmpty()) { + throw new IllegalArgumentException("Missing rescore definition"); + } + this.rescorers = rescorers; + } + + private RescorerRetrieverBuilder(RetrieverSource retriever, List> rescorers) { + super(List.of(retriever), extractMinWindowSize(rescorers)); + this.rescorers = rescorers; + } + + /** + * The minimum window size is used as the {@link CompoundRetrieverBuilder#rankWindowSize}, + * the final number of top documents to return in this retriever. + */ + private static int extractMinWindowSize(List> rescorers) { + int windowSize = Integer.MAX_VALUE; + for (var rescore : rescorers) { + windowSize = Math.min(rescore.windowSize() == null ? RescorerBuilder.DEFAULT_WINDOW_SIZE : rescore.windowSize(), windowSize); + } + return windowSize; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public ParseField getRankWindowSizeField() { + return RescorerBuilder.WINDOW_SIZE_FIELD; + } + + @Override + protected SearchSourceBuilder finalizeSourceBuilder(SearchSourceBuilder source) { + /** + * The re-scorer is passed downstream because this query operates only on + * the top documents retrieved by the child retriever. + * + * - If the sub-retriever is a {@link CompoundRetrieverBuilder}, only the top + * documents are re-scored since they are already determined at this stage. + * - For other retrievers that do not require a rewrite, the re-scorer's window + * size is applied per shard. As a result, more documents are re-scored + * compared to the final top documents produced by these retrievers in isolation. + */ + for (var rescorer : rescorers) { + source.addRescorer(rescorer); + } + return source; + } + + @Override + public void doToXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(RETRIEVER_FIELD.getPreferredName(), innerRetrievers.getFirst().retriever()); + builder.startArray(RESCORE_FIELD.getPreferredName()); + for (RescorerBuilder rescorer : rescorers) { + rescorer.toXContent(builder, params); + } + builder.endArray(); + } + + @Override + protected RescorerRetrieverBuilder clone(List newChildRetrievers, List newPreFilterQueryBuilders) { + var newInstance = new RescorerRetrieverBuilder(newChildRetrievers.get(0), rescorers); + newInstance.preFilterQueryBuilders = newPreFilterQueryBuilders; + return newInstance; + } + + @Override + protected RankDoc[] combineInnerRetrieverResults(List rankResults) { + assert rankResults.size() == 1; + ScoreDoc[] scoreDocs = rankResults.getFirst(); + RankDoc[] rankDocs = new RankDoc[scoreDocs.length]; + for (int i = 0; i < scoreDocs.length; i++) { + ScoreDoc scoreDoc = scoreDocs[i]; + rankDocs[i] = new RankDoc(scoreDoc.doc, scoreDoc.score, scoreDoc.shardIndex); + rankDocs[i].rank = i + 1; + } + return rankDocs; + } + + @Override + public boolean doEquals(Object o) { + RescorerRetrieverBuilder that = (RescorerRetrieverBuilder) o; + return super.doEquals(o) && Objects.equals(rescorers, that.rescorers); + } + + @Override + public int doHashCode() { + return Objects.hash(super.doHashCode(), rescorers); + } +} diff --git a/server/src/main/java/org/elasticsearch/search/retriever/RetrieverBuilder.java b/server/src/main/java/org/elasticsearch/search/retriever/RetrieverBuilder.java index d52c354cad69e..b9bfdfdf3402f 100644 --- a/server/src/main/java/org/elasticsearch/search/retriever/RetrieverBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/retriever/RetrieverBuilder.java @@ -63,7 +63,7 @@ protected static void declareBaseParserFields( AbstractObjectParser parser ) { parser.declareObjectArray( - (r, v) -> r.preFilterQueryBuilders = v, + (r, v) -> r.preFilterQueryBuilders = new ArrayList<>(v), (p, c) -> AbstractQueryBuilder.parseTopLevelQuery(p, c::trackQueryUsage), PRE_FILTER_FIELD ); diff --git a/server/src/test/java/org/elasticsearch/search/DefaultSearchContextTests.java b/server/src/test/java/org/elasticsearch/search/DefaultSearchContextTests.java index a474c1dc38c50..d3a3792f605db 100644 --- a/server/src/test/java/org/elasticsearch/search/DefaultSearchContextTests.java +++ b/server/src/test/java/org/elasticsearch/search/DefaultSearchContextTests.java @@ -23,6 +23,7 @@ import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; import org.apache.lucene.store.Directory; import org.apache.lucene.tests.index.RandomIndexWriter; import org.apache.lucene.tests.store.BaseDirectoryWrapper; @@ -245,7 +246,10 @@ protected Engine.Searcher acquireSearcherInternal(String source) { // resultWindow not greater than maxResultWindow and both rescore and sort are not null context1.from(0); DocValueFormat docValueFormat = mock(DocValueFormat.class); - SortAndFormats sortAndFormats = new SortAndFormats(new Sort(), new DocValueFormat[] { docValueFormat }); + SortAndFormats sortAndFormats = new SortAndFormats( + new Sort(new SortField[] { SortField.FIELD_DOC }), + new DocValueFormat[] { docValueFormat } + ); context1.sort(sortAndFormats); RescoreContext rescoreContext = mock(RescoreContext.class); diff --git a/server/src/test/java/org/elasticsearch/search/retriever/RescorerRetrieverBuilderParsingTests.java b/server/src/test/java/org/elasticsearch/search/retriever/RescorerRetrieverBuilderParsingTests.java new file mode 100644 index 0000000000000..fa83246d90cb2 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/retriever/RescorerRetrieverBuilderParsingTests.java @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.rescore.QueryRescorerBuilderTests; +import org.elasticsearch.search.rescore.RescorerBuilder; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.usage.SearchUsage; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentParser; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; + +public class RescorerRetrieverBuilderParsingTests extends AbstractXContentTestCase { + private static List xContentRegistryEntries; + + @BeforeClass + public static void init() { + xContentRegistryEntries = new SearchModule(Settings.EMPTY, emptyList()).getNamedXContents(); + } + + @AfterClass + public static void afterClass() throws Exception { + xContentRegistryEntries = null; + } + + @Override + protected RescorerRetrieverBuilder createTestInstance() { + int num = randomIntBetween(1, 3); + List> rescorers = new ArrayList<>(); + for (int i = 0; i < num; i++) { + rescorers.add(QueryRescorerBuilderTests.randomRescoreBuilder()); + } + return new RescorerRetrieverBuilder(TestRetrieverBuilder.createRandomTestRetrieverBuilder(), rescorers); + } + + @Override + protected RescorerRetrieverBuilder doParseInstance(XContentParser parser) throws IOException { + return (RescorerRetrieverBuilder) RetrieverBuilder.parseTopLevelRetrieverBuilder( + parser, + new RetrieverParserContext(new SearchUsage(), n -> true) + ); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + List entries = new ArrayList<>(xContentRegistryEntries); + entries.add( + new NamedXContentRegistry.Entry( + RetrieverBuilder.class, + TestRetrieverBuilder.TEST_SPEC.getName(), + (p, c) -> TestRetrieverBuilder.TEST_SPEC.getParser().fromXContent(p, (RetrieverParserContext) c), + TestRetrieverBuilder.TEST_SPEC.getName().getForRestApiVersion() + ) + ); + return new NamedXContentRegistry(entries); + } +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/retriever/QueryRuleRetrieverBuilder.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/retriever/QueryRuleRetrieverBuilder.java index 5b27cc7a3e05a..3a53ed977318d 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/retriever/QueryRuleRetrieverBuilder.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/retriever/QueryRuleRetrieverBuilder.java @@ -50,7 +50,6 @@ public final class QueryRuleRetrieverBuilder extends CompoundRetrieverBuilder PARSER = new ConstructingObjectParser<>( diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java index fd2427dc8ac6a..46bebebff9c95 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java @@ -47,7 +47,6 @@ public class TextSimilarityRankRetrieverBuilder extends CompoundRetrieverBuilder public static final ParseField INFERENCE_ID_FIELD = new ParseField("inference_id"); public static final ParseField INFERENCE_TEXT_FIELD = new ParseField("inference_text"); public static final ParseField FIELD_FIELD = new ParseField("field"); - public static final ParseField RANK_WINDOW_SIZE_FIELD = new ParseField("rank_window_size"); public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(TextSimilarityRankBuilder.NAME, args -> { diff --git a/x-pack/plugin/rank-rrf/build.gradle b/x-pack/plugin/rank-rrf/build.gradle index 2c3f217243aa4..b2d470c6618ea 100644 --- a/x-pack/plugin/rank-rrf/build.gradle +++ b/x-pack/plugin/rank-rrf/build.gradle @@ -22,6 +22,7 @@ dependencies { testImplementation(testArtifact(project(xpackModule('core')))) testImplementation(testArtifact(project(':server'))) + clusterModules project(':modules:mapper-extras') clusterModules project(xpackModule('rank-rrf')) clusterModules project(xpackModule('inference')) clusterModules project(':modules:lang-painless') diff --git a/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilder.java b/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilder.java index f1171b74f7468..c1447623dd5b1 100644 --- a/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilder.java +++ b/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilder.java @@ -48,7 +48,6 @@ public final class RRFRetrieverBuilder extends CompoundRetrieverBuilder Date: Wed, 18 Dec 2024 14:31:17 -0600 Subject: [PATCH 099/119] block-writes cannot be added after read-only (#119007) Fix bug in ReindexDataStreamIndexAction. If the source index has both a block-writes and is read-only, these must be updated on the destination index. If read-only is set first, the block-writes cannot be added because settings cannot be modified. --- docs/changelog/119007.yaml | 6 ++++++ muted-tests.yml | 2 -- .../action/ReindexDataStreamIndexTransportAction.java | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 docs/changelog/119007.yaml diff --git a/docs/changelog/119007.yaml b/docs/changelog/119007.yaml new file mode 100644 index 0000000000000..458101b68d454 --- /dev/null +++ b/docs/changelog/119007.yaml @@ -0,0 +1,6 @@ +pr: 119007 +summary: Block-writes cannot be added after read-only +area: Data streams +type: bug +issues: + - 119002 diff --git a/muted-tests.yml b/muted-tests.yml index 81480e89d1e8b..a06334146ed7b 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -293,8 +293,6 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118955 - class: org.elasticsearch.repositories.blobstore.testkit.analyze.SecureHdfsRepositoryAnalysisRestIT issue: https://github.com/elastic/elasticsearch/issues/118970 -- class: org.elasticsearch.xpack.migrate.action.ReindexDatastreamIndexTransportActionIT - issue: https://github.com/elastic/elasticsearch/issues/119002 # Examples: # diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java index 165fd61ae6599..66b13a9ce22b0 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java @@ -92,8 +92,8 @@ protected void doExecute( .andThen(l -> deleteDestIfExists(destIndexName, l)) .andThen(l -> createIndex(sourceIndex, destIndexName, l)) .andThen(l -> reindex(sourceIndexName, destIndexName, l)) - .andThen(l -> addBlockIfFromSource(READ_ONLY, settingsBefore, destIndexName, l)) .andThen(l -> addBlockIfFromSource(WRITE, settingsBefore, destIndexName, l)) + .andThen(l -> addBlockIfFromSource(READ_ONLY, settingsBefore, destIndexName, l)) .andThenApply(ignored -> new ReindexDataStreamIndexAction.Response(destIndexName)) .addListener(listener); } From 6e2c614af34175f55b25ece83f90cffe0e96542c Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Wed, 18 Dec 2024 13:01:55 -0800 Subject: [PATCH 100/119] Use minimum java version for javadoc tool (#118908) When compiling we use a compiler for the minimum java version. However, javadoc is left to whatever Java gradle uses. This commit adjusts javadoc to also use a javadoc tool for the minimum java version. --- .../internal/ElasticsearchJavaPlugin.java | 20 +++++++++++++++++-- .../gradle/internal/MrjarPlugin.java | 14 ++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaPlugin.java index e62c26c7fbc01..3ab85ba69dc80 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaPlugin.java @@ -31,11 +31,15 @@ import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.javadoc.Javadoc; import org.gradle.external.javadoc.CoreJavadocOptions; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JavaToolchainService; import org.gradle.language.base.plugins.LifecycleBasePlugin; import java.io.File; import java.util.Map; +import javax.inject.Inject; + import static org.elasticsearch.gradle.internal.conventions.util.Util.toStringable; import static org.elasticsearch.gradle.internal.util.ParamsUtils.loadBuildParams; @@ -44,6 +48,14 @@ * common configuration for production code. */ public class ElasticsearchJavaPlugin implements Plugin { + + private final JavaToolchainService javaToolchains; + + @Inject + ElasticsearchJavaPlugin(JavaToolchainService javaToolchains) { + this.javaToolchains = javaToolchains; + } + @Override public void apply(Project project) { project.getRootProject().getPlugins().apply(GlobalBuildInfoPlugin.class); @@ -55,7 +67,7 @@ public void apply(Project project) { // configureConfigurations(project); configureJars(project, buildParams.get()); configureJarManifest(project, buildParams.get()); - configureJavadoc(project); + configureJavadoc(project, buildParams.get()); testCompileOnlyDeps(project); } @@ -128,7 +140,7 @@ private static void configureJarManifest(Project project, BuildParameterExtensio project.getPluginManager().apply("nebula.info-jar"); } - private static void configureJavadoc(Project project) { + private void configureJavadoc(Project project, BuildParameterExtension buildParams) { project.getTasks().withType(Javadoc.class).configureEach(javadoc -> { /* * Generate docs using html5 to suppress a warning from `javadoc` @@ -136,6 +148,10 @@ private static void configureJavadoc(Project project) { */ CoreJavadocOptions javadocOptions = (CoreJavadocOptions) javadoc.getOptions(); javadocOptions.addBooleanOption("html5", true); + + javadoc.getJavadocTool().set(javaToolchains.javadocToolFor(spec -> { + spec.getLanguageVersion().set(JavaLanguageVersion.of(buildParams.getMinimumRuntimeVersion().getMajorVersion())); + })); }); TaskProvider javadoc = project.getTasks().withType(Javadoc.class).named("javadoc"); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/MrjarPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/MrjarPlugin.java index 7c488e6e73fee..5402e0a04fe8f 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/MrjarPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/MrjarPlugin.java @@ -86,14 +86,14 @@ public void apply(Project project) { configurePreviewFeatures(project, javaExtension.getSourceSets().getByName(SourceSet.TEST_SOURCE_SET_NAME), 21); for (int javaVersion : mainVersions) { String mainSourceSetName = SourceSet.MAIN_SOURCE_SET_NAME + javaVersion; - SourceSet mainSourceSet = addSourceSet(project, javaExtension, mainSourceSetName, mainSourceSets, javaVersion); + SourceSet mainSourceSet = addSourceSet(project, javaExtension, mainSourceSetName, mainSourceSets, javaVersion, true); configureSourceSetInJar(project, mainSourceSet, javaVersion); addJar(project, mainSourceSet, javaVersion); mainSourceSets.add(mainSourceSetName); testSourceSets.add(mainSourceSetName); String testSourceSetName = SourceSet.TEST_SOURCE_SET_NAME + javaVersion; - SourceSet testSourceSet = addSourceSet(project, javaExtension, testSourceSetName, testSourceSets, javaVersion); + SourceSet testSourceSet = addSourceSet(project, javaExtension, testSourceSetName, testSourceSets, javaVersion, false); testSourceSets.add(testSourceSetName); createTestTask(project, buildParams, testSourceSet, javaVersion, mainSourceSets); } @@ -121,7 +121,8 @@ private SourceSet addSourceSet( JavaPluginExtension javaExtension, String sourceSetName, List parentSourceSets, - int javaVersion + int javaVersion, + boolean isMainSourceSet ) { SourceSet sourceSet = javaExtension.getSourceSets().maybeCreate(sourceSetName); for (String parentSourceSetName : parentSourceSets) { @@ -135,6 +136,13 @@ private SourceSet addSourceSet( CompileOptions compileOptions = compileTask.getOptions(); compileOptions.getRelease().set(javaVersion); }); + if (isMainSourceSet) { + project.getTasks().create(sourceSet.getJavadocTaskName(), Javadoc.class, javadocTask -> { + javadocTask.getJavadocTool().set(javaToolchains.javadocToolFor(spec -> { + spec.getLanguageVersion().set(JavaLanguageVersion.of(javaVersion)); + })); + }); + } configurePreviewFeatures(project, sourceSet, javaVersion); // Since we configure MRJAR sourcesets to allow preview apis, class signatures for those From 1f4fef13f49918afcc53593c99ec41ebab6b2de0 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Wed, 18 Dec 2024 13:12:17 -0800 Subject: [PATCH 101/119] Improve efficiency of incremental builds when building bwc distributions (#118713) --- .../InternalDistributionBwcSetupPlugin.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java index da26cb66122ad..0e8dbb7fce26c 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java @@ -17,12 +17,12 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.file.FileSystemOperations; import org.gradle.api.file.ProjectLayout; import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.JvmToolchainsPlugin; import org.gradle.api.provider.Provider; import org.gradle.api.provider.ProviderFactory; -import org.gradle.api.tasks.Copy; import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.toolchain.JavaToolchainService; @@ -54,11 +54,17 @@ public class InternalDistributionBwcSetupPlugin implements Plugin { private final ObjectFactory objectFactory; private ProviderFactory providerFactory; private JavaToolchainService toolChainService; + private FileSystemOperations fileSystemOperations; @Inject - public InternalDistributionBwcSetupPlugin(ObjectFactory objectFactory, ProviderFactory providerFactory) { + public InternalDistributionBwcSetupPlugin( + ObjectFactory objectFactory, + ProviderFactory providerFactory, + FileSystemOperations fileSystemOperations + ) { this.objectFactory = objectFactory; this.providerFactory = providerFactory; + this.fileSystemOperations = fileSystemOperations; } @Override @@ -76,7 +82,8 @@ public void apply(Project project) { providerFactory, objectFactory, toolChainService, - isCi + isCi, + fileSystemOperations ); }); } @@ -88,7 +95,8 @@ private static void configureBwcProject( ProviderFactory providerFactory, ObjectFactory objectFactory, JavaToolchainService toolChainService, - Boolean isCi + Boolean isCi, + FileSystemOperations fileSystemOperations ) { ProjectLayout layout = project.getLayout(); Provider versionInfoProvider = providerFactory.provider(() -> versionInfo); @@ -120,11 +128,18 @@ private static void configureBwcProject( List distributionProjects = resolveArchiveProjects(checkoutDir.get(), bwcVersion.get()); // Setup gradle user home directory - project.getTasks().register("setupGradleUserHome", Copy.class, copy -> { - copy.into(project.getGradle().getGradleUserHomeDir().getAbsolutePath() + "-" + project.getName()); - copy.from(project.getGradle().getGradleUserHomeDir().getAbsolutePath(), copySpec -> { - copySpec.include("gradle.properties"); - copySpec.include("init.d/*"); + // We don't use a normal `Copy` task here as snapshotting the entire gradle user home is very expensive. This task is cheap, so + // up-to-date checking doesn't buy us much + project.getTasks().register("setupGradleUserHome", task -> { + task.doLast(t -> { + fileSystemOperations.copy(copy -> { + String gradleUserHome = project.getGradle().getGradleUserHomeDir().getAbsolutePath(); + copy.into(gradleUserHome + "-" + project.getName()); + copy.from(gradleUserHome, copySpec -> { + copySpec.include("gradle.properties"); + copySpec.include("init.d/*"); + }); + }); }); }); From 24773e0ba619e8f99293521b389a7266a0c8214d Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 19 Dec 2024 08:23:51 +1100 Subject: [PATCH 102/119] Mute org.elasticsearch.xpack.security.authc.AuthenticationServiceTests testInvalidToken #119019 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index a06334146ed7b..2d215bfb04c57 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -293,6 +293,9 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118955 - class: org.elasticsearch.repositories.blobstore.testkit.analyze.SecureHdfsRepositoryAnalysisRestIT issue: https://github.com/elastic/elasticsearch/issues/118970 +- class: org.elasticsearch.xpack.security.authc.AuthenticationServiceTests + method: testInvalidToken + issue: https://github.com/elastic/elasticsearch/issues/119019 # Examples: # From cc69e06974a8b9b543962e7fe98e1b954e89a268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Wed, 18 Dec 2024 22:35:55 +0100 Subject: [PATCH 103/119] Re-add support for some metadata field parameters (#118825) We removed support for type, fields, copy_to and boost in metadata field definitions with #116944 but with the move towards supporting N-2 read-only indices we need to add them back. This change reverts previous removal commits and adapts tests to also check we now throw errors for newly created indices. --- docs/changelog/{116944.yaml => 118825.yaml} | 4 +- .../index/mapper/MetadataFieldMapper.java | 22 +++++ .../index/KnownIndexVersions.java | 1 + .../index/mapper/MetadataMapperTestCase.java | 85 +++++++++++++++++++ 4 files changed, 110 insertions(+), 2 deletions(-) rename docs/changelog/{116944.yaml => 118825.yaml} (84%) diff --git a/docs/changelog/116944.yaml b/docs/changelog/118825.yaml similarity index 84% rename from docs/changelog/116944.yaml rename to docs/changelog/118825.yaml index e7833e49cf965..23170ec4705da 100644 --- a/docs/changelog/116944.yaml +++ b/docs/changelog/118825.yaml @@ -1,4 +1,4 @@ -pr: 116944 +pr: 118825 summary: "Remove support for type, fields, `copy_to` and boost in metadata field definition" area: Mapping type: breaking @@ -6,6 +6,6 @@ issues: [] breaking: title: "Remove support for type, fields, copy_to and boost in metadata field definition" area: Mapping - details: The type, fields, copy_to and boost parameters are no longer supported in metadata field definition + details: The type, fields, copy_to and boost parameters are no longer supported in metadata field definition starting with version 9. impact: Users providing type, fields, copy_to or boost as part of metadata field definition should remove them from their mappings. notable: false diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MetadataFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/MetadataFieldMapper.java index 31aa787c3f758..033742b3b57fc 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MetadataFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MetadataFieldMapper.java @@ -10,13 +10,17 @@ package org.elasticsearch.index.mapper; import org.elasticsearch.common.Explicit; +import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.IndexVersions; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.util.Iterator; import java.util.Map; +import java.util.Set; import java.util.function.Function; /** @@ -132,6 +136,8 @@ public final MetadataFieldMapper build(MapperBuilderContext context) { return build(); } + private static final Set UNSUPPORTED_PARAMETERS_8_6_0 = Set.of("type", "fields", "copy_to", "boost"); + public final void parseMetadataField(String name, MappingParserContext parserContext, Map fieldNode) { final Parameter[] params = getParameters(); Map> paramsMap = Maps.newHashMapWithExpectedSize(params.length); @@ -144,6 +150,22 @@ public final void parseMetadataField(String name, MappingParserContext parserCon final Object propNode = entry.getValue(); Parameter parameter = paramsMap.get(propName); if (parameter == null) { + IndexVersion indexVersionCreated = parserContext.indexVersionCreated(); + if (indexVersionCreated.before(IndexVersions.UPGRADE_TO_LUCENE_10_0_0) + && UNSUPPORTED_PARAMETERS_8_6_0.contains(propName)) { + if (indexVersionCreated.onOrAfter(IndexVersions.V_8_6_0)) { + // silently ignore type, and a few other parameters: sadly we've been doing this for a long time + deprecationLogger.warn( + DeprecationCategory.API, + propName, + "Parameter [{}] has no effect on metadata field [{}] and will be removed in future", + propName, + name + ); + } + iterator.remove(); + continue; + } throw new MapperParsingException("unknown parameter [" + propName + "] on metadata field [" + name + "]"); } parameter.parse(name, parserContext, propNode); diff --git a/test/framework/src/main/java/org/elasticsearch/index/KnownIndexVersions.java b/test/framework/src/main/java/org/elasticsearch/index/KnownIndexVersions.java index 5cdb3f1808a38..4f559a5f3eaef 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/KnownIndexVersions.java +++ b/test/framework/src/main/java/org/elasticsearch/index/KnownIndexVersions.java @@ -19,6 +19,7 @@ public class KnownIndexVersions { * A sorted list of all known index versions */ public static final List ALL_VERSIONS = List.copyOf(IndexVersions.getAllVersions()); + /** * A sorted list of all known index versions that can be written to */ diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MetadataMapperTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MetadataMapperTestCase.java index 449ecc099412f..580eb6eacb27e 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MetadataMapperTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MetadataMapperTestCase.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.mapper.MapperService.MergeReason; import org.elasticsearch.test.index.IndexVersionUtils; import org.elasticsearch.xcontent.XContentBuilder; @@ -142,4 +143,88 @@ public final void testFixedMetaFieldsAreNotConfigurable() throws IOException { ); assertEquals("Failed to parse mapping: " + fieldName() + " is not configurable", exception.getMessage()); } + + public void testTypeAndFriendsAreAcceptedBefore_8_6_0() throws IOException { + assumeTrue("Metadata field " + fieldName() + " isn't configurable", isConfigurable()); + IndexVersion previousVersion = IndexVersionUtils.getPreviousVersion(IndexVersions.V_8_6_0); + // we randomly also pick read-only versions to test that we can still parse the parameters for them + IndexVersion version = IndexVersionUtils.randomVersionBetween( + random(), + IndexVersionUtils.getLowestReadCompatibleVersion(), + previousVersion + ); + assumeTrue("Metadata field " + fieldName() + " is not supported on version " + version, isSupportedOn(version)); + MapperService mapperService = createMapperService(version, mapping(b -> {})); + // these parameters were previously silently ignored, they will still be ignored in existing indices + String[] unsupportedParameters = new String[] { "fields", "copy_to", "boost", "type" }; + for (String param : unsupportedParameters) { + String mappingAsString = "{\n" + + " \"_doc\" : {\n" + + " \"" + + fieldName() + + "\" : {\n" + + " \"" + + param + + "\" : \"any\"\n" + + " }\n" + + " }\n" + + "}"; + assertNotNull(mapperService.parseMapping("_doc", MergeReason.MAPPING_UPDATE, new CompressedXContent(mappingAsString))); + } + } + + public void testTypeAndFriendsAreDeprecatedFrom_8_6_0_TO_9_0_0() throws IOException { + assumeTrue("Metadata field " + fieldName() + " isn't configurable", isConfigurable()); + IndexVersion previousVersion = IndexVersionUtils.getPreviousVersion(IndexVersions.UPGRADE_TO_LUCENE_10_0_0); + IndexVersion version = IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_8_6_0, previousVersion); + assumeTrue("Metadata field " + fieldName() + " is not supported on version " + version, isSupportedOn(version)); + MapperService mapperService = createMapperService(version, mapping(b -> {})); + // these parameters were deprecated, they now should throw an error in new indices + String[] unsupportedParameters = new String[] { "fields", "copy_to", "boost", "type" }; + for (String param : unsupportedParameters) { + String mappingAsString = "{\n" + + " \"_doc\" : {\n" + + " \"" + + fieldName() + + "\" : {\n" + + " \"" + + param + + "\" : \"any\"\n" + + " }\n" + + " }\n" + + "}"; + assertNotNull(mapperService.parseMapping("_doc", MergeReason.MAPPING_UPDATE, new CompressedXContent(mappingAsString))); + assertWarnings("Parameter [" + param + "] has no effect on metadata field [" + fieldName() + "] and will be removed in future"); + } + } + + public void testTypeAndFriendsThrow_After_9_0_0() throws IOException { + assumeTrue("Metadata field " + fieldName() + " isn't configurable", isConfigurable()); + IndexVersion version = IndexVersionUtils.randomVersionBetween( + random(), + IndexVersions.UPGRADE_TO_LUCENE_10_0_0, + IndexVersion.current() + ); + assumeTrue("Metadata field " + fieldName() + " is not supported on version " + version, isSupportedOn(version)); + MapperService mapperService = createMapperService(version, mapping(b -> {})); + // these parameters were previously silently ignored, they are now deprecated in new indices + String[] unsupportedParameters = new String[] { "fields", "copy_to", "boost", "type" }; + for (String param : unsupportedParameters) { + String mappingAsString = "{\n" + + " \"_doc\" : {\n" + + " \"" + + fieldName() + + "\" : {\n" + + " \"" + + param + + "\" : \"any\"\n" + + " }\n" + + " }\n" + + "}"; + expectThrows( + MapperParsingException.class, + () -> mapperService.parseMapping("_doc", MergeReason.MAPPING_UPDATE, new CompressedXContent(mappingAsString)) + ); + } + } } From b8130768f6c41e876014dff8838981c99c1078b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Wed, 18 Dec 2024 22:37:14 +0100 Subject: [PATCH 104/119] Add back version based logic from IndexSortConfig This change re-introduces pre-7.13 deprecation logging and silent handling of index sorting on alias fields. We need to still support this for v9 for read-only indices. --- .../elasticsearch/index/IndexSortConfig.java | 25 ++++++++++++++++++- .../index/IndexSortSettingsTests.java | 14 +++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/IndexSortConfig.java b/server/src/main/java/org/elasticsearch/index/IndexSortConfig.java index 2811c7493a277..6c044ab999899 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSortConfig.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSortConfig.java @@ -14,6 +14,8 @@ import org.apache.lucene.search.SortedNumericSortField; import org.apache.lucene.search.SortedSetSortField; import org.elasticsearch.cluster.metadata.DataStream; +import org.elasticsearch.common.logging.DeprecationCategory; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.fielddata.IndexFieldData; @@ -53,6 +55,8 @@ **/ public final class IndexSortConfig { + private static final DeprecationLogger DEPRECATION_LOGGER = DeprecationLogger.getLogger(IndexSortConfig.class); + /** * The list of field names */ @@ -134,10 +138,14 @@ private static MultiValueMode parseMultiValueMode(String value) { // visible for tests final FieldSortSpec[] sortSpecs; + private final IndexVersion indexCreatedVersion; + private final String indexName; private final IndexMode indexMode; public IndexSortConfig(IndexSettings indexSettings) { final Settings settings = indexSettings.getSettings(); + this.indexCreatedVersion = indexSettings.getIndexVersionCreated(); + this.indexName = indexSettings.getIndex().getName(); this.indexMode = indexSettings.getMode(); if (this.indexMode == IndexMode.TIME_SERIES) { @@ -230,7 +238,22 @@ public Sort buildIndexSort( throw new IllegalArgumentException(err); } if (Objects.equals(ft.name(), sortSpec.field) == false) { - throw new IllegalArgumentException("Cannot use alias [" + sortSpec.field + "] as an index sort field"); + if (this.indexCreatedVersion.onOrAfter(IndexVersions.V_7_13_0)) { + throw new IllegalArgumentException("Cannot use alias [" + sortSpec.field + "] as an index sort field"); + } else { + DEPRECATION_LOGGER.warn( + DeprecationCategory.MAPPINGS, + "index-sort-aliases", + "Index sort for index [" + + indexName + + "] defined on field [" + + sortSpec.field + + "] which resolves to field [" + + ft.name() + + "]. " + + "You will not be able to define an index sort over aliased fields in new indexes" + ); + } } boolean reverse = sortSpec.order == null ? false : (sortSpec.order == SortOrder.DESC); MultiValueMode mode = sortSpec.mode; diff --git a/server/src/test/java/org/elasticsearch/index/IndexSortSettingsTests.java b/server/src/test/java/org/elasticsearch/index/IndexSortSettingsTests.java index 441ad8a5a225a..7221d69b74d46 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexSortSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexSortSettingsTests.java @@ -160,6 +160,20 @@ public void testSortingAgainstAliases() { assertEquals("Cannot use alias [field] as an index sort field", e.getMessage()); } + public void testSortingAgainstAliasesPre713() { + IndexSettings indexSettings = indexSettings( + Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersions.V_7_12_0).put("index.sort.field", "field").build() + ); + MappedFieldType aliased = new KeywordFieldMapper.KeywordFieldType("aliased"); + Sort sort = buildIndexSort(indexSettings, Map.of("field", aliased)); + assertThat(sort.getSort(), arrayWithSize(1)); + assertThat(sort.getSort()[0].getField(), equalTo("aliased")); + assertWarnings( + "Index sort for index [test] defined on field [field] which resolves to field [aliased]. " + + "You will not be able to define an index sort over aliased fields in new indexes" + ); + } + public void testTimeSeriesMode() { IndexSettings indexSettings = indexSettings( Settings.builder() From e087f3d9371370c0cde96d83538938ca0d15276c Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Wed, 18 Dec 2024 15:43:53 -0600 Subject: [PATCH 105/119] Connecting the reindex data stream persistent task to ReindexDataStreamIndexAction (#118978) --- ...indexDataStreamPersistentTaskExecutor.java | 125 ++++++++++++-- .../upgrades/DataStreamsUpgradeIT.java | 156 ++++++++++++++++++ 2 files changed, 269 insertions(+), 12 deletions(-) diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java index 494be303980a7..dc8e33bc091e6 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java @@ -9,8 +9,15 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.rollover.RolloverAction; +import org.elasticsearch.action.admin.indices.rollover.RolloverRequest; import org.elasticsearch.action.datastreams.GetDataStreamAction; +import org.elasticsearch.action.datastreams.ModifyDataStreamsAction; +import org.elasticsearch.action.support.CountDownActionListener; +import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.metadata.DataStream; +import org.elasticsearch.cluster.metadata.DataStreamAction; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.Index; @@ -20,9 +27,13 @@ import org.elasticsearch.persistent.PersistentTasksExecutor; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.migrate.action.ReindexDataStreamIndexAction; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import static org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.getOldIndexVersionPredicate; @@ -72,22 +83,109 @@ protected void nodeOperation(AllocatedPersistentTask task, ReindexDataStreamTask reindexClient.execute(GetDataStreamAction.INSTANCE, request, ActionListener.wrap(response -> { List dataStreamInfos = response.getDataStreams(); if (dataStreamInfos.size() == 1) { - List indices = dataStreamInfos.getFirst().getDataStream().getIndices(); - List indicesToBeReindexed = indices.stream() - .filter(getOldIndexVersionPredicate(clusterService.state().metadata())) - .toList(); - reindexDataStreamTask.setPendingIndicesCount(indicesToBeReindexed.size()); - for (Index index : indicesToBeReindexed) { - reindexDataStreamTask.incrementInProgressIndicesCount(index.getName()); - // TODO This is just a placeholder. This is where the real data stream reindex logic will go - reindexDataStreamTask.reindexSucceeded(index.getName()); + DataStream dataStream = dataStreamInfos.getFirst().getDataStream(); + if (getOldIndexVersionPredicate(clusterService.state().metadata()).test(dataStream.getWriteIndex())) { + reindexClient.execute( + RolloverAction.INSTANCE, + new RolloverRequest(sourceDataStream, null), + ActionListener.wrap( + rolloverResponse -> reindexIndices(dataStream, reindexDataStreamTask, reindexClient, sourceDataStream), + e -> completeFailedPersistentTask(reindexDataStreamTask, e) + ) + ); + } else { + reindexIndices(dataStream, reindexDataStreamTask, reindexClient, sourceDataStream); } - - completeSuccessfulPersistentTask(reindexDataStreamTask); } else { completeFailedPersistentTask(reindexDataStreamTask, new ElasticsearchException("data stream does not exist")); } - }, reindexDataStreamTask::markAsFailed)); + }, exception -> completeFailedPersistentTask(reindexDataStreamTask, exception))); + } + + private void reindexIndices( + DataStream dataStream, + ReindexDataStreamTask reindexDataStreamTask, + ExecuteWithHeadersClient reindexClient, + String sourceDataStream + ) { + List indices = dataStream.getIndices(); + List indicesToBeReindexed = indices.stream().filter(getOldIndexVersionPredicate(clusterService.state().metadata())).toList(); + reindexDataStreamTask.setPendingIndicesCount(indicesToBeReindexed.size()); + // The CountDownActionListener is 1 more than the number of indices so that the count is not 0 if we have no indices + CountDownActionListener listener = new CountDownActionListener(indicesToBeReindexed.size() + 1, ActionListener.wrap(response1 -> { + completeSuccessfulPersistentTask(reindexDataStreamTask); + }, exception -> { completeFailedPersistentTask(reindexDataStreamTask, exception); })); + List indicesRemaining = Collections.synchronizedList(new ArrayList<>(indicesToBeReindexed)); + final int maxConcurrentIndices = 1; + for (int i = 0; i < maxConcurrentIndices; i++) { + maybeProcessNextIndex(indicesRemaining, reindexDataStreamTask, reindexClient, sourceDataStream, listener); + } + // This takes care of the additional latch count referenced above: + listener.onResponse(null); + } + + private void maybeProcessNextIndex( + List indicesRemaining, + ReindexDataStreamTask reindexDataStreamTask, + ExecuteWithHeadersClient reindexClient, + String sourceDataStream, + CountDownActionListener listener + ) { + if (indicesRemaining.isEmpty()) { + return; + } + Index index; + try { + index = indicesRemaining.removeFirst(); + } catch (NoSuchElementException e) { + return; + } + reindexDataStreamTask.incrementInProgressIndicesCount(index.getName()); + reindexClient.execute( + ReindexDataStreamIndexAction.INSTANCE, + new ReindexDataStreamIndexAction.Request(index.getName()), + ActionListener.wrap(response1 -> { + updateDataStream(sourceDataStream, index.getName(), response1.getDestIndex(), ActionListener.wrap(unused -> { + reindexDataStreamTask.reindexSucceeded(index.getName()); + listener.onResponse(null); + maybeProcessNextIndex(indicesRemaining, reindexDataStreamTask, reindexClient, sourceDataStream, listener); + }, exception -> { + reindexDataStreamTask.reindexFailed(index.getName(), exception); + listener.onResponse(null); + }), reindexClient); + }, exception -> { + reindexDataStreamTask.reindexFailed(index.getName(), exception); + listener.onResponse(null); + }) + ); + } + + private void updateDataStream( + String dataStream, + String oldIndex, + String newIndex, + ActionListener listener, + ExecuteWithHeadersClient reindexClient + ) { + reindexClient.execute( + ModifyDataStreamsAction.INSTANCE, + new ModifyDataStreamsAction.Request( + TimeValue.MAX_VALUE, + TimeValue.MAX_VALUE, + List.of(DataStreamAction.removeBackingIndex(dataStream, oldIndex), DataStreamAction.addBackingIndex(dataStream, newIndex)) + ), + new ActionListener<>() { + @Override + public void onResponse(AcknowledgedResponse response) { + listener.onResponse(null); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + } + ); } private void completeSuccessfulPersistentTask(ReindexDataStreamTask persistentTask) { @@ -105,6 +203,9 @@ private TimeValue getTimeToLive(ReindexDataStreamTask reindexDataStreamTask) { PersistentTasksCustomMetadata.PersistentTask persistentTask = persistentTasksCustomMetadata.getTask( reindexDataStreamTask.getPersistentTaskId() ); + if (persistentTask == null) { + return TimeValue.timeValueMillis(0); + } PersistentTaskState state = persistentTask.getState(); final long completionTime; if (state == null) { diff --git a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/DataStreamsUpgradeIT.java b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/DataStreamsUpgradeIT.java index 40ad5bba29baa..58556dd420ca6 100644 --- a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/DataStreamsUpgradeIT.java +++ b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/DataStreamsUpgradeIT.java @@ -11,15 +11,23 @@ import org.elasticsearch.client.Response; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.DataStreamTestHelper; +import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.common.time.FormatNames; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Booleans; import org.elasticsearch.core.Strings; +import org.elasticsearch.xcontent.json.JsonXContent; import org.hamcrest.Matchers; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.elasticsearch.upgrades.IndexingIT.assertCount; +import static org.hamcrest.Matchers.equalTo; public class DataStreamsUpgradeIT extends AbstractUpgradeTestCase { @@ -164,4 +172,152 @@ public void testDataStreamValidationDoesNotBreakUpgrade() throws Exception { } } + public void testUpgradeDataStream() throws Exception { + String dataStreamName = "reindex_test_data_stream"; + int numRollovers = 5; + if (CLUSTER_TYPE == ClusterType.OLD) { + createAndRolloverDataStream(dataStreamName, numRollovers); + } else if (CLUSTER_TYPE == ClusterType.UPGRADED) { + upgradeDataStream(dataStreamName, numRollovers); + } + } + + private static void createAndRolloverDataStream(String dataStreamName, int numRollovers) throws IOException { + // We want to create a data stream and roll it over several times so that we have several indices to upgrade + final String template = """ + { + "settings":{ + "index": { + "mode": "time_series" + } + }, + "mappings":{ + "dynamic_templates": [ + { + "labels": { + "path_match": "pod.labels.*", + "mapping": { + "type": "keyword", + "time_series_dimension": true + } + } + } + ], + "properties": { + "@timestamp" : { + "type": "date" + }, + "metricset": { + "type": "keyword", + "time_series_dimension": true + }, + "k8s": { + "properties": { + "pod": { + "properties": { + "name": { + "type": "keyword" + }, + "network": { + "properties": { + "tx": { + "type": "long" + }, + "rx": { + "type": "long" + } + } + } + } + } + } + } + } + } + } + """; + final String indexTemplate = """ + { + "index_patterns": ["$PATTERN"], + "template": $TEMPLATE, + "data_stream": { + } + }"""; + var putIndexTemplateRequest = new Request("POST", "/_index_template/reindex_test_data_stream_template"); + putIndexTemplateRequest.setJsonEntity(indexTemplate.replace("$TEMPLATE", template).replace("$PATTERN", dataStreamName)); + assertOK(client().performRequest(putIndexTemplateRequest)); + bulkLoadData(dataStreamName); + for (int i = 0; i < numRollovers; i++) { + rollover(dataStreamName); + bulkLoadData(dataStreamName); + } + } + + private void upgradeDataStream(String dataStreamName, int numRollovers) throws Exception { + Request reindexRequest = new Request("POST", "/_migration/reindex"); + reindexRequest.setJsonEntity(Strings.format(""" + { + "mode": "upgrade", + "source": { + "index": "%s" + } + }""", dataStreamName)); + Response reindexResponse = client().performRequest(reindexRequest); + assertOK(reindexResponse); + assertBusy(() -> { + Request statusRequest = new Request("GET", "_migration/reindex/" + dataStreamName + "/_status"); + Response statusResponse = client().performRequest(statusRequest); + Map statusResponseMap = XContentHelper.convertToMap( + JsonXContent.jsonXContent, + statusResponse.getEntity().getContent(), + false + ); + assertOK(statusResponse); + assertThat(statusResponseMap.get("complete"), equalTo(true)); + if (isOriginalClusterCurrent()) { + // If the original cluster was the same as this one, we don't want any indices reindexed: + assertThat(statusResponseMap.get("successes"), equalTo(0)); + } else { + assertThat(statusResponseMap.get("successes"), equalTo(numRollovers + 1)); + } + }, 60, TimeUnit.SECONDS); + Request cancelRequest = new Request("POST", "_migration/reindex/" + dataStreamName + "/_cancel"); + Response cancelResponse = client().performRequest(cancelRequest); + assertOK(cancelResponse); + } + + private static void bulkLoadData(String dataStreamName) throws IOException { + final String bulk = """ + {"create": {}} + {"@timestamp": "$now", "metricset": "pod", "k8s": {"pod": {"name": "cat", "network": {"tx": 2001818691, "rx": 802133794}}}} + {"create": {}} + {"@timestamp": "$now", "metricset": "pod", "k8s": {"pod": {"name": "hamster", "network": {"tx": 2005177954, "rx": 801479970}}}} + {"create": {}} + {"@timestamp": "$now", "metricset": "pod", "k8s": {"pod": {"name": "cow", "network": {"tx": 2006223737, "rx": 802337279}}}} + {"create": {}} + {"@timestamp": "$now", "metricset": "pod", "k8s": {"pod": {"name": "rat", "network": {"tx": 2012916202, "rx": 803685721}}}} + {"create": {}} + {"@timestamp": "$now", "metricset": "pod", "k8s": {"pod": {"name": "dog", "network": {"tx": 1434521831, "rx": 530575198}}}} + {"create": {}} + {"@timestamp": "$now", "metricset": "pod", "k8s": {"pod": {"name": "tiger", "network": {"tx": 1434577921, "rx": 530600088}}}} + {"create": {}} + {"@timestamp": "$now", "metricset": "pod", "k8s": {"pod": {"name": "lion", "network": {"tx": 1434587694, "rx": 530604797}}}} + {"create": {}} + {"@timestamp": "$now", "metricset": "pod", "k8s": {"pod": {"name": "elephant", "network": {"tx": 1434595272, "rx": 530605511}}}} + """; + var bulkRequest = new Request("POST", "/" + dataStreamName + "/_bulk"); + bulkRequest.setJsonEntity(bulk.replace("$now", formatInstant(Instant.now()))); + var response = client().performRequest(bulkRequest); + assertOK(response); + } + + static String formatInstant(Instant instant) { + return DateFormatter.forPattern(FormatNames.STRICT_DATE_OPTIONAL_TIME.getName()).format(instant); + } + + private static void rollover(String dataStreamName) throws IOException { + Request rolloverRequest = new Request("POST", "/" + dataStreamName + "/_rollover"); + Response rolloverResponse = client().performRequest(rolloverRequest); + assertOK(rolloverResponse); + } } From c54a26db49da892170f13434cfe5dd9f3bfe78bb Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:20:22 +1100 Subject: [PATCH 106/119] Mute org.elasticsearch.backwards.MixedClusterClientYamlTestSuiteIT test {p0=synonyms/90_synonyms_reloading_for_synset/Reload analyzers for specific synonym set} #116777 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 2d215bfb04c57..35a9b31685794 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -296,6 +296,9 @@ tests: - class: org.elasticsearch.xpack.security.authc.AuthenticationServiceTests method: testInvalidToken issue: https://github.com/elastic/elasticsearch/issues/119019 +- class: org.elasticsearch.backwards.MixedClusterClientYamlTestSuiteIT + method: test {p0=synonyms/90_synonyms_reloading_for_synset/Reload analyzers for specific synonym set} + issue: https://github.com/elastic/elasticsearch/issues/116777 # Examples: # From c3a59bb9659accaf36f83f0b28d49f68939f6ce7 Mon Sep 17 00:00:00 2001 From: Patrick Doyle <810052+prdoyle@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:06:52 -0500 Subject: [PATCH 107/119] Process execution checks and IT tests (#119010) * Process creation checks and IT tests * Remove process queries; only forbid execution --- .../bridge/EntitlementChecker.java | 7 ++++ .../common/RestEntitlementsCheckAction.java | 39 +++++++++++++------ .../EntitlementAllowedNonModularPlugin.java | 1 - .../qa/EntitlementAllowedPlugin.java | 1 - .../EntitlementDeniedNonModularPlugin.java | 1 - .../qa/EntitlementDeniedPlugin.java | 1 - .../api/ElasticsearchEntitlementChecker.java | 11 ++++++ .../runtime/policy/PolicyManager.java | 20 ++++++++++ 8 files changed, 66 insertions(+), 15 deletions(-) diff --git a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java index a6b8a31fc3894..25f4e97bd12ee 100644 --- a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java +++ b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java @@ -11,6 +11,7 @@ import java.net.URL; import java.net.URLStreamHandlerFactory; +import java.util.List; public interface EntitlementChecker { @@ -29,4 +30,10 @@ public interface EntitlementChecker { void check$java_net_URLClassLoader$(Class callerClass, String name, URL[] urls, ClassLoader parent); void check$java_net_URLClassLoader$(Class callerClass, String name, URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory); + + // Process creation + void check$$start(Class callerClass, ProcessBuilder that, ProcessBuilder.Redirect[] redirects); + + void check$java_lang_ProcessBuilder$startPipeline(Class callerClass, List builders); + } diff --git a/libs/entitlement/qa/common/src/main/java/org/elasticsearch/entitlement/qa/common/RestEntitlementsCheckAction.java b/libs/entitlement/qa/common/src/main/java/org/elasticsearch/entitlement/qa/common/RestEntitlementsCheckAction.java index 1ac4a7506eacb..3cc4b97e9bfea 100644 --- a/libs/entitlement/qa/common/src/main/java/org/elasticsearch/entitlement/qa/common/RestEntitlementsCheckAction.java +++ b/libs/entitlement/qa/common/src/main/java/org/elasticsearch/entitlement/qa/common/RestEntitlementsCheckAction.java @@ -29,43 +29,47 @@ import java.util.stream.Collectors; import static java.util.Map.entry; +import static org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction.CheckAction.deniedToPlugins; +import static org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction.CheckAction.forPlugins; import static org.elasticsearch.rest.RestRequest.Method.GET; public class RestEntitlementsCheckAction extends BaseRestHandler { private static final Logger logger = LogManager.getLogger(RestEntitlementsCheckAction.class); private final String prefix; - private record CheckAction(Runnable action, boolean isServerOnly) { - - static CheckAction serverOnly(Runnable action) { + record CheckAction(Runnable action, boolean isAlwaysDeniedToPlugins) { + /** + * These cannot be granted to plugins, so our test plugins cannot test the "allowed" case. + * Used both for always-denied entitlements as well as those granted only to the server itself. + */ + static CheckAction deniedToPlugins(Runnable action) { return new CheckAction(action, true); } - static CheckAction serverAndPlugin(Runnable action) { + static CheckAction forPlugins(Runnable action) { return new CheckAction(action, false); } } private static final Map checkActions = Map.ofEntries( - entry("runtime_exit", CheckAction.serverOnly(RestEntitlementsCheckAction::runtimeExit)), - entry("runtime_halt", CheckAction.serverOnly(RestEntitlementsCheckAction::runtimeHalt)), - entry("create_classloader", CheckAction.serverAndPlugin(RestEntitlementsCheckAction::createClassLoader)) + entry("runtime_exit", deniedToPlugins(RestEntitlementsCheckAction::runtimeExit)), + entry("runtime_halt", deniedToPlugins(RestEntitlementsCheckAction::runtimeHalt)), + entry("create_classloader", forPlugins(RestEntitlementsCheckAction::createClassLoader)), + // entry("processBuilder_start", deniedToPlugins(RestEntitlementsCheckAction::processBuilder_start)), + entry("processBuilder_startPipeline", deniedToPlugins(RestEntitlementsCheckAction::processBuilder_startPipeline)) ); @SuppressForbidden(reason = "Specifically testing Runtime.exit") private static void runtimeExit() { - logger.info("Calling Runtime.exit;"); Runtime.getRuntime().exit(123); } @SuppressForbidden(reason = "Specifically testing Runtime.halt") private static void runtimeHalt() { - logger.info("Calling Runtime.halt;"); Runtime.getRuntime().halt(123); } private static void createClassLoader() { - logger.info("Calling new URLClassLoader"); try (var classLoader = new URLClassLoader("test", new URL[0], RestEntitlementsCheckAction.class.getClassLoader())) { logger.info("Created URLClassLoader [{}]", classLoader.getName()); } catch (IOException e) { @@ -73,6 +77,18 @@ private static void createClassLoader() { } } + private static void processBuilder_start() { + // TODO: processBuilder().start(); + } + + private static void processBuilder_startPipeline() { + try { + ProcessBuilder.startPipeline(List.of()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + public RestEntitlementsCheckAction(String prefix) { this.prefix = prefix; } @@ -80,7 +96,7 @@ public RestEntitlementsCheckAction(String prefix) { public static Set getServerAndPluginsCheckActions() { return checkActions.entrySet() .stream() - .filter(kv -> kv.getValue().isServerOnly() == false) + .filter(kv -> kv.getValue().isAlwaysDeniedToPlugins() == false) .map(Map.Entry::getKey) .collect(Collectors.toSet()); } @@ -112,6 +128,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } return channel -> { + logger.info("Calling check action [{}]", actionName); checkAction.action().run(); channel.sendResponse(new RestResponse(RestStatus.OK, Strings.format("Succesfully executed action [%s]", actionName))); }; diff --git a/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementAllowedNonModularPlugin.java b/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementAllowedNonModularPlugin.java index d65981c30f0be..82146e6a87759 100644 --- a/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementAllowedNonModularPlugin.java +++ b/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementAllowedNonModularPlugin.java @@ -27,7 +27,6 @@ import java.util.function.Supplier; public class EntitlementAllowedNonModularPlugin extends Plugin implements ActionPlugin { - @Override public List getRestHandlers( final Settings settings, diff --git a/libs/entitlement/qa/entitlement-allowed/src/main/java/org/elasticsearch/entitlement/qa/EntitlementAllowedPlugin.java b/libs/entitlement/qa/entitlement-allowed/src/main/java/org/elasticsearch/entitlement/qa/EntitlementAllowedPlugin.java index d81e23e311be1..8649daf272e70 100644 --- a/libs/entitlement/qa/entitlement-allowed/src/main/java/org/elasticsearch/entitlement/qa/EntitlementAllowedPlugin.java +++ b/libs/entitlement/qa/entitlement-allowed/src/main/java/org/elasticsearch/entitlement/qa/EntitlementAllowedPlugin.java @@ -27,7 +27,6 @@ import java.util.function.Supplier; public class EntitlementAllowedPlugin extends Plugin implements ActionPlugin { - @Override public List getRestHandlers( final Settings settings, diff --git a/libs/entitlement/qa/entitlement-denied-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementDeniedNonModularPlugin.java b/libs/entitlement/qa/entitlement-denied-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementDeniedNonModularPlugin.java index 0f908d84260fb..7ca89c735a602 100644 --- a/libs/entitlement/qa/entitlement-denied-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementDeniedNonModularPlugin.java +++ b/libs/entitlement/qa/entitlement-denied-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementDeniedNonModularPlugin.java @@ -27,7 +27,6 @@ import java.util.function.Supplier; public class EntitlementDeniedNonModularPlugin extends Plugin implements ActionPlugin { - @Override public List getRestHandlers( final Settings settings, diff --git a/libs/entitlement/qa/entitlement-denied/src/main/java/org/elasticsearch/entitlement/qa/EntitlementDeniedPlugin.java b/libs/entitlement/qa/entitlement-denied/src/main/java/org/elasticsearch/entitlement/qa/EntitlementDeniedPlugin.java index 0ed27e2e576e7..2a2fd35d47cf3 100644 --- a/libs/entitlement/qa/entitlement-denied/src/main/java/org/elasticsearch/entitlement/qa/EntitlementDeniedPlugin.java +++ b/libs/entitlement/qa/entitlement-denied/src/main/java/org/elasticsearch/entitlement/qa/EntitlementDeniedPlugin.java @@ -27,7 +27,6 @@ import java.util.function.Supplier; public class EntitlementDeniedPlugin extends Plugin implements ActionPlugin { - @Override public List getRestHandlers( final Settings settings, diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java index a5ca0543ad15a..75365fbb74d65 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java @@ -14,6 +14,7 @@ import java.net.URL; import java.net.URLStreamHandlerFactory; +import java.util.List; /** * Implementation of the {@link EntitlementChecker} interface, providing additional @@ -67,4 +68,14 @@ public ElasticsearchEntitlementChecker(PolicyManager policyManager) { ) { policyManager.checkCreateClassLoader(callerClass); } + + @Override + public void check$$start(Class callerClass, ProcessBuilder processBuilder, ProcessBuilder.Redirect[] redirects) { + policyManager.checkStartProcess(callerClass); + } + + @Override + public void check$java_lang_ProcessBuilder$startPipeline(Class callerClass, List builders) { + policyManager.checkStartProcess(callerClass); + } } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index 74ba986041dac..e06f7768eb8be 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -105,6 +105,26 @@ private static Map> buildScopeEntitlementsMap(Policy p return policy.scopes.stream().collect(Collectors.toUnmodifiableMap(scope -> scope.name, scope -> scope.entitlements)); } + public void checkStartProcess(Class callerClass) { + neverEntitled(callerClass, "start process"); + } + + private void neverEntitled(Class callerClass, String operationDescription) { + var requestingModule = requestingModule(callerClass); + if (isTriviallyAllowed(requestingModule)) { + return; + } + + throw new NotEntitledException( + Strings.format( + "Not entitled: caller [%s], module [%s], operation [%s]", + callerClass, + requestingModule.getName(), + operationDescription + ) + ); + } + public void checkExitVM(Class callerClass) { checkEntitlementPresent(callerClass, ExitVMEntitlement.class); } From c98ca63b460ac6f546ee1c151d7bda3dbd641701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Thu, 19 Dec 2024 00:09:37 +0100 Subject: [PATCH 108/119] Revert 7.x related code from analysis common (#118972) This reverts #113009 and re-introduces v7 compatibility logic and previous v7 tests since we now support v7 indices as read-only on v9. --- .../analysis/common/CommonAnalysisPlugin.java | 131 +++++++- .../common/CommonAnalysisPluginTests.java | 292 ++++++++++++++++++ .../common/EdgeNGramTokenizerTests.java | 3 +- 3 files changed, 419 insertions(+), 7 deletions(-) create mode 100644 modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java index a97154fd4d1ff..c980aaba71444 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java @@ -101,7 +101,12 @@ import org.apache.lucene.analysis.tr.TurkishAnalyzer; import org.apache.lucene.analysis.util.ElisionFilter; import org.apache.lucene.util.SetOnce; +import org.elasticsearch.common.logging.DeprecationCategory; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.analysis.AnalyzerProvider; import org.elasticsearch.index.analysis.CharFilterFactory; @@ -134,6 +139,8 @@ public class CommonAnalysisPlugin extends Plugin implements AnalysisPlugin, ScriptPlugin { + private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(CommonAnalysisPlugin.class); + private final SetOnce scriptServiceHolder = new SetOnce<>(); private final SetOnce synonymsManagementServiceHolder = new SetOnce<>(); @@ -224,6 +231,28 @@ public Map> getTokenFilters() { filters.put("dictionary_decompounder", requiresAnalysisSettings(DictionaryCompoundWordTokenFilterFactory::new)); filters.put("dutch_stem", DutchStemTokenFilterFactory::new); filters.put("edge_ngram", EdgeNGramTokenFilterFactory::new); + filters.put("edgeNGram", (IndexSettings indexSettings, Environment environment, String name, Settings settings) -> { + return new EdgeNGramTokenFilterFactory(indexSettings, environment, name, settings) { + @Override + public TokenStream create(TokenStream tokenStream) { + if (indexSettings.getIndexVersionCreated().onOrAfter(IndexVersions.V_8_0_0)) { + throw new IllegalArgumentException( + "The [edgeNGram] token filter name was deprecated in 6.4 and cannot be used in new indices. " + + "Please change the filter name to [edge_ngram] instead." + ); + } else { + deprecationLogger.warn( + DeprecationCategory.ANALYSIS, + "edgeNGram_deprecation", + "The [edgeNGram] token filter name is deprecated and will be removed in a future version. " + + "Please change the filter name to [edge_ngram] instead." + ); + } + return super.create(tokenStream); + } + + }; + }); filters.put("elision", requiresAnalysisSettings(ElisionTokenFilterFactory::new)); filters.put("fingerprint", FingerprintTokenFilterFactory::new); filters.put("flatten_graph", FlattenGraphTokenFilterFactory::new); @@ -243,6 +272,28 @@ public Map> getTokenFilters() { filters.put("min_hash", MinHashTokenFilterFactory::new); filters.put("multiplexer", MultiplexerTokenFilterFactory::new); filters.put("ngram", NGramTokenFilterFactory::new); + filters.put("nGram", (IndexSettings indexSettings, Environment environment, String name, Settings settings) -> { + return new NGramTokenFilterFactory(indexSettings, environment, name, settings) { + @Override + public TokenStream create(TokenStream tokenStream) { + if (indexSettings.getIndexVersionCreated().onOrAfter(IndexVersions.V_8_0_0)) { + throw new IllegalArgumentException( + "The [nGram] token filter name was deprecated in 6.4 and cannot be used in new indices. " + + "Please change the filter name to [ngram] instead." + ); + } else { + deprecationLogger.warn( + DeprecationCategory.ANALYSIS, + "nGram_deprecation", + "The [nGram] token filter name is deprecated and will be removed in a future version. " + + "Please change the filter name to [ngram] instead." + ); + } + return super.create(tokenStream); + } + + }; + }); filters.put("pattern_capture", requiresAnalysisSettings(PatternCaptureGroupTokenFilterFactory::new)); filters.put("pattern_replace", requiresAnalysisSettings(PatternReplaceTokenFilterFactory::new)); filters.put("persian_normalization", PersianNormalizationFilterFactory::new); @@ -294,7 +345,39 @@ public Map> getTokenizers() { tokenizers.put("simple_pattern", SimplePatternTokenizerFactory::new); tokenizers.put("simple_pattern_split", SimplePatternSplitTokenizerFactory::new); tokenizers.put("thai", ThaiTokenizerFactory::new); + tokenizers.put("nGram", (IndexSettings indexSettings, Environment environment, String name, Settings settings) -> { + if (indexSettings.getIndexVersionCreated().onOrAfter(IndexVersions.V_8_0_0)) { + throw new IllegalArgumentException( + "The [nGram] tokenizer name was deprecated in 7.6. " + + "Please use the tokenizer name to [ngram] for indices created in versions 8 or higher instead." + ); + } else if (indexSettings.getIndexVersionCreated().onOrAfter(IndexVersions.V_7_6_0)) { + deprecationLogger.warn( + DeprecationCategory.ANALYSIS, + "nGram_tokenizer_deprecation", + "The [nGram] tokenizer name is deprecated and will be removed in a future version. " + + "Please change the tokenizer name to [ngram] instead." + ); + } + return new NGramTokenizerFactory(indexSettings, environment, name, settings); + }); tokenizers.put("ngram", NGramTokenizerFactory::new); + tokenizers.put("edgeNGram", (IndexSettings indexSettings, Environment environment, String name, Settings settings) -> { + if (indexSettings.getIndexVersionCreated().onOrAfter(IndexVersions.V_8_0_0)) { + throw new IllegalArgumentException( + "The [edgeNGram] tokenizer name was deprecated in 7.6. " + + "Please use the tokenizer name to [edge_nGram] for indices created in versions 8 or higher instead." + ); + } else if (indexSettings.getIndexVersionCreated().onOrAfter(IndexVersions.V_7_6_0)) { + deprecationLogger.warn( + DeprecationCategory.ANALYSIS, + "edgeNGram_tokenizer_deprecation", + "The [edgeNGram] tokenizer name is deprecated and will be removed in a future version. " + + "Please change the tokenizer name to [edge_ngram] instead." + ); + } + return new EdgeNGramTokenizerFactory(indexSettings, environment, name, settings); + }); tokenizers.put("edge_ngram", EdgeNGramTokenizerFactory::new); tokenizers.put("char_group", CharGroupTokenizerFactory::new); tokenizers.put("classic", ClassicTokenizerFactory::new); @@ -505,17 +588,53 @@ public List getPreConfiguredTokenizers() { tokenizers.add(PreConfiguredTokenizer.singleton("letter", LetterTokenizer::new)); tokenizers.add(PreConfiguredTokenizer.singleton("whitespace", WhitespaceTokenizer::new)); tokenizers.add(PreConfiguredTokenizer.singleton("ngram", NGramTokenizer::new)); - tokenizers.add( - PreConfiguredTokenizer.indexVersion( - "edge_ngram", - (version) -> new EdgeNGramTokenizer(NGramTokenizer.DEFAULT_MIN_NGRAM_SIZE, NGramTokenizer.DEFAULT_MAX_NGRAM_SIZE) - ) - ); + tokenizers.add(PreConfiguredTokenizer.indexVersion("edge_ngram", (version) -> { + if (version.onOrAfter(IndexVersions.V_7_3_0)) { + return new EdgeNGramTokenizer(NGramTokenizer.DEFAULT_MIN_NGRAM_SIZE, NGramTokenizer.DEFAULT_MAX_NGRAM_SIZE); + } + return new EdgeNGramTokenizer(EdgeNGramTokenizer.DEFAULT_MIN_GRAM_SIZE, EdgeNGramTokenizer.DEFAULT_MAX_GRAM_SIZE); + })); tokenizers.add(PreConfiguredTokenizer.singleton("pattern", () -> new PatternTokenizer(Regex.compile("\\W+", null), -1))); tokenizers.add(PreConfiguredTokenizer.singleton("thai", ThaiTokenizer::new)); // TODO deprecate and remove in API // This is already broken with normalization, so backwards compat isn't necessary? tokenizers.add(PreConfiguredTokenizer.singleton("lowercase", XLowerCaseTokenizer::new)); + + tokenizers.add(PreConfiguredTokenizer.indexVersion("nGram", (version) -> { + if (version.onOrAfter(IndexVersions.V_8_0_0)) { + throw new IllegalArgumentException( + "The [nGram] tokenizer name was deprecated in 7.6. " + + "Please use the tokenizer name to [ngram] for indices created in versions 8 or higher instead." + ); + } else if (version.onOrAfter(IndexVersions.V_7_6_0)) { + deprecationLogger.warn( + DeprecationCategory.ANALYSIS, + "nGram_tokenizer_deprecation", + "The [nGram] tokenizer name is deprecated and will be removed in a future version. " + + "Please change the tokenizer name to [ngram] instead." + ); + } + return new NGramTokenizer(); + })); + tokenizers.add(PreConfiguredTokenizer.indexVersion("edgeNGram", (version) -> { + if (version.onOrAfter(IndexVersions.V_8_0_0)) { + throw new IllegalArgumentException( + "The [edgeNGram] tokenizer name was deprecated in 7.6. " + + "Please use the tokenizer name to [edge_ngram] for indices created in versions 8 or higher instead." + ); + } else if (version.onOrAfter(IndexVersions.V_7_6_0)) { + deprecationLogger.warn( + DeprecationCategory.ANALYSIS, + "edgeNGram_tokenizer_deprecation", + "The [edgeNGram] tokenizer name is deprecated and will be removed in a future version. " + + "Please change the tokenizer name to [edge_ngram] instead." + ); + } + if (version.onOrAfter(IndexVersions.V_7_3_0)) { + return new EdgeNGramTokenizer(NGramTokenizer.DEFAULT_MIN_NGRAM_SIZE, NGramTokenizer.DEFAULT_MAX_NGRAM_SIZE); + } + return new EdgeNGramTokenizer(EdgeNGramTokenizer.DEFAULT_MIN_GRAM_SIZE, EdgeNGramTokenizer.DEFAULT_MAX_GRAM_SIZE); + })); tokenizers.add(PreConfiguredTokenizer.singleton("PathHierarchy", PathHierarchyTokenizer::new)); return tokenizers; diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java new file mode 100644 index 0000000000000..9972d58b2dcc1 --- /dev/null +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java @@ -0,0 +1,292 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.analysis.common; + +import org.apache.lucene.analysis.Tokenizer; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.IndexVersions; +import org.elasticsearch.index.analysis.TokenizerFactory; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.IndexSettingsModule; +import org.elasticsearch.test.index.IndexVersionUtils; + +import java.io.IOException; +import java.util.Map; + +public class CommonAnalysisPluginTests extends ESTestCase { + + /** + * Check that the deprecated "nGram" filter throws exception for indices created since 7.0.0 and + * logs a warning for earlier indices when the filter is used as a custom filter + */ + public void testNGramFilterInCustomAnalyzerDeprecationError() throws IOException { + final Settings settings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put( + IndexMetadata.SETTING_VERSION_CREATED, + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_8_0_0, IndexVersion.current()) + ) + .put("index.analysis.analyzer.custom_analyzer.type", "custom") + .put("index.analysis.analyzer.custom_analyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.custom_analyzer.filter", "my_ngram") + .put("index.analysis.filter.my_ngram.type", "nGram") + .build(); + + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + IllegalArgumentException ex = expectThrows( + IllegalArgumentException.class, + () -> createTestAnalysis(IndexSettingsModule.newIndexSettings("index", settings), settings, commonAnalysisPlugin) + ); + assertEquals( + "The [nGram] token filter name was deprecated in 6.4 and cannot be used in new indices. " + + "Please change the filter name to [ngram] instead.", + ex.getMessage() + ); + } + + final Settings settingsPre7 = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put( + IndexMetadata.SETTING_VERSION_CREATED, + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_7_0_0, IndexVersions.V_7_6_0) + ) + .put("index.analysis.analyzer.custom_analyzer.type", "custom") + .put("index.analysis.analyzer.custom_analyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.custom_analyzer.filter", "my_ngram") + .put("index.analysis.filter.my_ngram.type", "nGram") + .build(); + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + createTestAnalysis(IndexSettingsModule.newIndexSettings("index", settingsPre7), settingsPre7, commonAnalysisPlugin); + assertWarnings( + "The [nGram] token filter name is deprecated and will be removed in a future version. " + + "Please change the filter name to [ngram] instead." + ); + } + } + + /** + * Check that the deprecated "edgeNGram" filter throws exception for indices created since 7.0.0 and + * logs a warning for earlier indices when the filter is used as a custom filter + */ + public void testEdgeNGramFilterInCustomAnalyzerDeprecationError() throws IOException { + final Settings settings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put( + IndexMetadata.SETTING_VERSION_CREATED, + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_8_0_0, IndexVersion.current()) + ) + .put("index.analysis.analyzer.custom_analyzer.type", "custom") + .put("index.analysis.analyzer.custom_analyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.custom_analyzer.filter", "my_ngram") + .put("index.analysis.filter.my_ngram.type", "edgeNGram") + .build(); + + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + IllegalArgumentException ex = expectThrows( + IllegalArgumentException.class, + () -> createTestAnalysis(IndexSettingsModule.newIndexSettings("index", settings), settings, commonAnalysisPlugin) + ); + assertEquals( + "The [edgeNGram] token filter name was deprecated in 6.4 and cannot be used in new indices. " + + "Please change the filter name to [edge_ngram] instead.", + ex.getMessage() + ); + } + + final Settings settingsPre7 = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put( + IndexMetadata.SETTING_VERSION_CREATED, + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_7_0_0, IndexVersions.V_7_6_0) + ) + .put("index.analysis.analyzer.custom_analyzer.type", "custom") + .put("index.analysis.analyzer.custom_analyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.custom_analyzer.filter", "my_ngram") + .put("index.analysis.filter.my_ngram.type", "edgeNGram") + .build(); + + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + createTestAnalysis(IndexSettingsModule.newIndexSettings("index", settingsPre7), settingsPre7, commonAnalysisPlugin); + assertWarnings( + "The [edgeNGram] token filter name is deprecated and will be removed in a future version. " + + "Please change the filter name to [edge_ngram] instead." + ); + } + } + + /** + * Check that we log a deprecation warning for "nGram" and "edgeNGram" tokenizer names with 7.6 and + * disallow usages for indices created after 8.0 + */ + public void testNGramTokenizerDeprecation() throws IOException { + // tests for prebuilt tokenizer + doTestPrebuiltTokenizerDeprecation( + "nGram", + "ngram", + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_7_0_0, IndexVersions.V_7_5_2), + false + ); + doTestPrebuiltTokenizerDeprecation( + "edgeNGram", + "edge_ngram", + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_7_0_0, IndexVersions.V_7_5_2), + false + ); + doTestPrebuiltTokenizerDeprecation( + "nGram", + "ngram", + IndexVersionUtils.randomVersionBetween( + random(), + IndexVersions.V_7_6_0, + IndexVersion.max(IndexVersions.V_7_6_0, IndexVersionUtils.getPreviousVersion(IndexVersions.V_8_0_0)) + ), + true + ); + doTestPrebuiltTokenizerDeprecation( + "edgeNGram", + "edge_ngram", + IndexVersionUtils.randomVersionBetween( + random(), + IndexVersions.V_7_6_0, + IndexVersion.max(IndexVersions.V_7_6_0, IndexVersionUtils.getPreviousVersion(IndexVersions.V_8_0_0)) + ), + true + ); + expectThrows( + IllegalArgumentException.class, + () -> doTestPrebuiltTokenizerDeprecation( + "nGram", + "ngram", + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_8_0_0, IndexVersion.current()), + true + ) + ); + expectThrows( + IllegalArgumentException.class, + () -> doTestPrebuiltTokenizerDeprecation( + "edgeNGram", + "edge_ngram", + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_8_0_0, IndexVersion.current()), + true + ) + ); + + // same batch of tests for custom tokenizer definition in the settings + doTestCustomTokenizerDeprecation( + "nGram", + "ngram", + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_7_0_0, IndexVersions.V_7_5_2), + false + ); + doTestCustomTokenizerDeprecation( + "edgeNGram", + "edge_ngram", + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_7_0_0, IndexVersions.V_7_5_2), + false + ); + doTestCustomTokenizerDeprecation( + "nGram", + "ngram", + IndexVersionUtils.randomVersionBetween( + random(), + IndexVersions.V_7_6_0, + IndexVersion.max(IndexVersions.V_7_6_0, IndexVersionUtils.getPreviousVersion(IndexVersions.V_8_0_0)) + ), + true + ); + doTestCustomTokenizerDeprecation( + "edgeNGram", + "edge_ngram", + IndexVersionUtils.randomVersionBetween( + random(), + IndexVersions.V_7_6_0, + IndexVersion.max(IndexVersions.V_7_6_0, IndexVersionUtils.getPreviousVersion(IndexVersions.V_8_0_0)) + ), + true + ); + expectThrows( + IllegalArgumentException.class, + () -> doTestCustomTokenizerDeprecation( + "nGram", + "ngram", + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_8_0_0, IndexVersion.current()), + true + ) + ); + expectThrows( + IllegalArgumentException.class, + () -> doTestCustomTokenizerDeprecation( + "edgeNGram", + "edge_ngram", + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.V_8_0_0, IndexVersion.current()), + true + ) + ); + } + + public void doTestPrebuiltTokenizerDeprecation(String deprecatedName, String replacement, IndexVersion version, boolean expectWarning) + throws IOException { + final Settings settings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put(IndexMetadata.SETTING_VERSION_CREATED, version) + .build(); + + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + Map tokenizers = createTestAnalysis( + IndexSettingsModule.newIndexSettings("index", settings), + settings, + commonAnalysisPlugin + ).tokenizer; + TokenizerFactory tokenizerFactory = tokenizers.get(deprecatedName); + + Tokenizer tokenizer = tokenizerFactory.create(); + assertNotNull(tokenizer); + if (expectWarning) { + assertWarnings( + "The [" + + deprecatedName + + "] tokenizer name is deprecated and will be removed in a future version. " + + "Please change the tokenizer name to [" + + replacement + + "] instead." + ); + } + } + } + + public void doTestCustomTokenizerDeprecation(String deprecatedName, String replacement, IndexVersion version, boolean expectWarning) + throws IOException { + final Settings settings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put(IndexMetadata.SETTING_VERSION_CREATED, version) + .put("index.analysis.analyzer.custom_analyzer.type", "custom") + .put("index.analysis.analyzer.custom_analyzer.tokenizer", "my_tokenizer") + .put("index.analysis.tokenizer.my_tokenizer.type", deprecatedName) + .build(); + + try (CommonAnalysisPlugin commonAnalysisPlugin = new CommonAnalysisPlugin()) { + createTestAnalysis(IndexSettingsModule.newIndexSettings("index", settings), settings, commonAnalysisPlugin); + + if (expectWarning) { + assertWarnings( + "The [" + + deprecatedName + + "] tokenizer name is deprecated and will be removed in a future version. " + + "Please change the tokenizer name to [" + + replacement + + "] instead." + ); + } + } + } +} diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/EdgeNGramTokenizerTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/EdgeNGramTokenizerTests.java index 11d1653439e59..c998e927e25a8 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/EdgeNGramTokenizerTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/EdgeNGramTokenizerTests.java @@ -34,7 +34,7 @@ public class EdgeNGramTokenizerTests extends ESTokenStreamTestCase { - private static IndexAnalyzers buildAnalyzers(IndexVersion version, String tokenizer) throws IOException { + private IndexAnalyzers buildAnalyzers(IndexVersion version, String tokenizer) throws IOException { Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()).build(); Settings indexSettings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, version) @@ -54,6 +54,7 @@ public void testPreConfiguredTokenizer() throws IOException { assertNotNull(analyzer); assertAnalyzesTo(analyzer, "test", new String[] { "t", "te" }); } + } public void testCustomTokenChars() throws IOException { From 547c7800e478ae69900c42d955262da5f4350a60 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Wed, 18 Dec 2024 15:10:29 -0800 Subject: [PATCH 109/119] Improve error message when whitelist resource file is not found (#119012) This commit replaces a NullPointerException that occurs if a whitelist resource is not found with a customized message. Additionally it augments the message with specific actions, especially in the case the owning class is modularized which requies additional work. --- .../painless/spi/WhitelistLoader.java | 32 ++++++++++- .../painless/WhitelistLoaderTests.java | 57 +++++++++++++++++++ .../bootstrap/test-framework.policy | 2 + .../org/elasticsearch/test/jar/JarUtils.java | 33 +++++++++++ 4 files changed, 121 insertions(+), 3 deletions(-) diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java index 2e7f0de027de7..37bff97a07ae2 100644 --- a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java @@ -9,8 +9,10 @@ package org.elasticsearch.painless.spi; +import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.painless.spi.annotation.WhitelistAnnotationParser; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.lang.reflect.Constructor; @@ -140,7 +142,7 @@ public static Whitelist loadFromResourceFiles(Class resource, String... filep * } * } */ - public static Whitelist loadFromResourceFiles(Class resource, Map parsers, String... filepaths) { + public static Whitelist loadFromResourceFiles(Class owner, Map parsers, String... filepaths) { List whitelistClasses = new ArrayList<>(); List whitelistStatics = new ArrayList<>(); List whitelistClassBindings = new ArrayList<>(); @@ -153,7 +155,7 @@ public static Whitelist loadFromResourceFiles(Class resource, Map resource, Map) resource::getClassLoader); + ClassLoader loader = AccessController.doPrivileged((PrivilegedAction) owner::getClassLoader); return new Whitelist(loader, whitelistClasses, whitelistStatics, whitelistClassBindings, Collections.emptyList()); } + private static InputStream getResourceAsStream(Class owner, String name) { + InputStream stream = owner.getResourceAsStream(name); + if (stream == null) { + String msg = "Whitelist file [" + + owner.getPackageName().replace(".", "/") + + "/" + + name + + "] not found from owning class [" + + owner.getName() + + "]."; + if (owner.getModule().isNamed()) { + msg += " Check that the file exists and the package [" + + owner.getPackageName() + + "] is opened " + + "to module " + + WhitelistLoader.class.getModule().getName(); + } + throw new ResourceNotFoundException(msg); + } + return stream; + } + private static List parseWhitelistAnnotations(Map parsers, String line) { List annotations; diff --git a/modules/lang-painless/spi/src/test/java/org/elasticsearch/painless/WhitelistLoaderTests.java b/modules/lang-painless/spi/src/test/java/org/elasticsearch/painless/WhitelistLoaderTests.java index e62d0b438b098..b46bc118e0913 100644 --- a/modules/lang-painless/spi/src/test/java/org/elasticsearch/painless/WhitelistLoaderTests.java +++ b/modules/lang-painless/spi/src/test/java/org/elasticsearch/painless/WhitelistLoaderTests.java @@ -9,6 +9,7 @@ package org.elasticsearch.painless; +import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.painless.spi.WhitelistClass; import org.elasticsearch.painless.spi.WhitelistLoader; @@ -17,10 +18,18 @@ import org.elasticsearch.painless.spi.annotation.NoImportAnnotation; import org.elasticsearch.painless.spi.annotation.WhitelistAnnotationParser; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.compiler.InMemoryJavaCompiler; +import org.elasticsearch.test.jar.JarUtils; +import java.lang.ModuleLayer.Controller; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + public class WhitelistLoaderTests extends ESTestCase { public void testUnknownAnnotations() { @@ -96,4 +105,52 @@ public void testAnnotations() { assertEquals(3, count); } + + public void testMissingWhitelistResource() { + var e = expectThrows(ResourceNotFoundException.class, () -> WhitelistLoader.loadFromResourceFiles(Whitelist.class, "missing.txt")); + assertThat( + e.getMessage(), + equalTo( + "Whitelist file [org/elasticsearch/painless/spi/missing.txt] not found" + + " from owning class [org.elasticsearch.painless.spi.Whitelist]." + ) + ); + } + + public void testMissingWhitelistResourceInModule() throws Exception { + Map sources = new HashMap<>(); + sources.put("module-info", "module m {}"); + sources.put("p.TestOwner", "package p; public class TestOwner { }"); + var classToBytes = InMemoryJavaCompiler.compile(sources); + + Path dir = createTempDir(getTestName()); + Path jar = dir.resolve("m.jar"); + Map jarEntries = new HashMap<>(); + jarEntries.put("module-info.class", classToBytes.get("module-info")); + jarEntries.put("p/TestOwner.class", classToBytes.get("p.TestOwner")); + jarEntries.put("p/resource.txt", "# test resource".getBytes(StandardCharsets.UTF_8)); + JarUtils.createJarWithEntries(jar, jarEntries); + + try (var loader = JarUtils.loadJar(jar)) { + Controller controller = JarUtils.loadModule(jar, loader.classloader(), "m"); + Module module = controller.layer().findModule("m").orElseThrow(); + + Class ownerClass = module.getClassLoader().loadClass("p.TestOwner"); + + // first check we get a nice error message when accessing the resource + var e = expectThrows(ResourceNotFoundException.class, () -> WhitelistLoader.loadFromResourceFiles(ownerClass, "resource.txt")); + assertThat( + e.getMessage(), + equalTo( + "Whitelist file [p/resource.txt] not found from owning class [p.TestOwner]." + + " Check that the file exists and the package [p] is opened to module null" + ) + ); + + // now check we can actually read it once the package is opened to us + controller.addOpens(module, "p", WhitelistLoader.class.getModule()); + var whitelist = WhitelistLoader.loadFromResourceFiles(ownerClass, "resource.txt"); + assertThat(whitelist, notNullValue()); + } + } } diff --git a/server/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy b/server/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy index 040a7a6205f9c..462fab651c211 100644 --- a/server/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy +++ b/server/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy @@ -88,6 +88,7 @@ grant codeBase "${codebase.elasticsearch}" { // this is the test-framework, but the jar is horribly named grant codeBase "${codebase.framework}" { permission java.lang.RuntimePermission "setSecurityManager"; + permission java.lang.RuntimePermission "createClassLoader"; }; grant codeBase "${codebase.elasticsearch-rest-client}" { @@ -129,4 +130,5 @@ grant { permission java.nio.file.LinkPermission "symbolic"; // needed for keystore tests permission java.lang.RuntimePermission "accessUserInformation"; + permission java.lang.RuntimePermission "getClassLoader"; }; diff --git a/test/framework/src/main/java/org/elasticsearch/test/jar/JarUtils.java b/test/framework/src/main/java/org/elasticsearch/test/jar/JarUtils.java index e5bdd66e949f7..0da392cb7fb01 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/jar/JarUtils.java +++ b/test/framework/src/main/java/org/elasticsearch/test/jar/JarUtils.java @@ -9,13 +9,24 @@ package org.elasticsearch.test.jar; +import org.elasticsearch.test.PrivilegedOperations.ClosableURLClassLoader; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.OutputStream; +import java.lang.module.Configuration; +import java.lang.module.ModuleFinder; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; @@ -85,6 +96,28 @@ public static void createJarWithEntriesUTF(Path jarfile, Map ent createJarWithEntries(jarfile, map); } + /** + * Creates a class loader for the given jar file. + * @param path Path to the jar file to load + * @return A URLClassLoader that will load classes from the jar. It should be closed when no longer needed. + */ + public static ClosableURLClassLoader loadJar(Path path) { + try { + URL[] urls = new URL[] { path.toUri().toURL() }; + return new ClosableURLClassLoader(URLClassLoader.newInstance(urls, JarUtils.class.getClassLoader())); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public static ModuleLayer.Controller loadModule(Path path, ClassLoader loader, String name) { + var finder = ModuleFinder.of(path.getParent()); + var cf = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), ModuleFinder.of(), Set.of(name)); + return AccessController.doPrivileged( + (PrivilegedAction) () -> ModuleLayer.defineModulesWithOneLoader(cf, List.of(ModuleLayer.boot()), loader) + ); + } + @FunctionalInterface interface UncheckedIOFunction { R apply(T t) throws IOException; From 93aee0f1c6ccec241a6dce229b96e40ff36c9358 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:43:15 +1100 Subject: [PATCH 110/119] Mute org.elasticsearch.xpack.security.authc.ldap.ActiveDirectoryRunAsIT org.elasticsearch.xpack.security.authc.ldap.ActiveDirectoryRunAsIT #115727 --- muted-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 35a9b31685794..12f1fc510a332 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -299,6 +299,8 @@ tests: - class: org.elasticsearch.backwards.MixedClusterClientYamlTestSuiteIT method: test {p0=synonyms/90_synonyms_reloading_for_synset/Reload analyzers for specific synonym set} issue: https://github.com/elastic/elasticsearch/issues/116777 +- class: org.elasticsearch.xpack.security.authc.ldap.ActiveDirectoryRunAsIT + issue: https://github.com/elastic/elasticsearch/issues/115727 # Examples: # From d103036db1533b60ba217b4ab20d369e6a3be8d6 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Thu, 19 Dec 2024 09:53:18 +0100 Subject: [PATCH 111/119] Broaden index versions tested in some mappings tests (#119026) MINIMUM_READONLY_COMPATIBLE is used as a lower bound (N-2) as opposed to MINIMUM_COMPATIBLE (N-1). --- .../org/elasticsearch/index/mapper/MappingParserTests.java | 6 +++++- .../java/org/elasticsearch/indices/IndicesModuleTests.java | 7 ++----- .../xpack/spatial/index/mapper/ShapeFieldMapperTests.java | 7 +++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/mapper/MappingParserTests.java b/server/src/test/java/org/elasticsearch/index/mapper/MappingParserTests.java index e0f58b8922be2..b87ab09c530d6 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/MappingParserTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/MappingParserTests.java @@ -322,7 +322,11 @@ public void testBlankFieldName() throws Exception { } public void testBlankFieldNameBefore8_6_0() throws Exception { - IndexVersion version = IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersions.V_8_5_0); + IndexVersion version = IndexVersionUtils.randomVersionBetween( + random(), + IndexVersions.MINIMUM_READONLY_COMPATIBLE, + IndexVersions.V_8_5_0 + ); TransportVersion transportVersion = TransportVersionUtils.randomVersionBetween( random(), TransportVersions.MINIMUM_COMPATIBLE, diff --git a/server/src/test/java/org/elasticsearch/indices/IndicesModuleTests.java b/server/src/test/java/org/elasticsearch/indices/IndicesModuleTests.java index ab65d56557ad9..0e333491588a6 100644 --- a/server/src/test/java/org/elasticsearch/indices/IndicesModuleTests.java +++ b/server/src/test/java/org/elasticsearch/indices/IndicesModuleTests.java @@ -9,7 +9,6 @@ package org.elasticsearch.indices; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.mapper.DataStreamTimestampFieldMapper; @@ -98,8 +97,6 @@ public Map getMetadataMappers() { DataStreamTimestampFieldMapper.NAME, FieldNamesFieldMapper.NAME }; - @UpdateForV9(owner = UpdateForV9.Owner.SEARCH_FOUNDATIONS) - @AwaitsFix(bugUrl = "test is referencing 7.x index versions so needs to be updated for 9.0 bump") public void testBuiltinMappers() { IndicesModule module = new IndicesModule(Collections.emptyList()); { @@ -239,14 +236,14 @@ public Map getMetadataMappers() { public void testFieldNamesIsLast() { IndicesModule module = new IndicesModule(Collections.emptyList()); - IndexVersion version = IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()); + IndexVersion version = IndexVersionUtils.randomVersion(); List fieldNames = new ArrayList<>(module.getMapperRegistry().getMetadataMapperParsers(version).keySet()); assertEquals(FieldNamesFieldMapper.NAME, fieldNames.get(fieldNames.size() - 1)); } public void testFieldNamesIsLastWithPlugins() { IndicesModule module = new IndicesModule(fakePlugins); - IndexVersion version = IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()); + IndexVersion version = IndexVersionUtils.randomVersion(); List fieldNames = new ArrayList<>(module.getMapperRegistry().getMetadataMapperParsers(version).keySet()); assertEquals(FieldNamesFieldMapper.NAME, fieldNames.get(fieldNames.size() - 1)); } diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java index d030a2bbf81ad..5d2624735bebe 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java @@ -113,8 +113,11 @@ public void testDefaultConfiguration() throws IOException { } public void testDefaultDocValueConfigurationOnPre8_4() throws IOException { - // TODO verify which version this test is actually valid for (when PR is actually merged) - IndexVersion oldVersion = IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersions.V_8_3_0); + IndexVersion oldVersion = IndexVersionUtils.randomVersionBetween( + random(), + IndexVersions.MINIMUM_READONLY_COMPATIBLE, + IndexVersions.V_8_3_0 + ); DocumentMapper defaultMapper = createDocumentMapper(oldVersion, fieldMapping(this::minimalMapping)); Mapper fieldMapper = defaultMapper.mappers().getMapper(FIELD_NAME); assertThat(fieldMapper, instanceOf(fieldMapperClass())); From f760b40815ab30c1b07f01a72b7a81740a780bf1 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Thu, 19 Dec 2024 09:54:41 +0100 Subject: [PATCH 112/119] Broaden index versions tested to cover v7 versions for some analysis tests (#119024) This replaces usages of MINIMUM_COMPATIBLE with MINIMUM_READONLY_COMPATIBLE as a lower bound when randomizing the index version in some tests. This provides more coverage as it relies on readonly versions as opposed to only those that can be written to. --- .../analysis/common/SynonymsAnalysisTests.java | 12 ++++++------ .../analysis/common/UniqueTokenFilterTests.java | 6 +----- .../phonetic/AnalysisPhoneticFactoryTests.java | 2 +- .../index/analysis/PreBuiltAnalyzerTests.java | 4 ++++ .../index/similarity/SimilarityServiceTests.java | 6 +++--- .../script/VectorScoreScriptUtilsTests.java | 8 ++++---- .../script/field/vectors/DenseVectorTests.java | 6 +++++- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java index 4fc6ca96b5f08..af57b8270ff02 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java @@ -118,7 +118,7 @@ public void testSynonymWordDeleteByAnalyzer() throws IOException { // Test with an index version where lenient should always be false by default IndexVersion randomNonLenientIndexVersion = IndexVersionUtils.randomVersionBetween( random(), - IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersions.INDEX_SORTING_ON_NESTED ); assertIsNotLenient.accept(randomNonLenientIndexVersion, false); @@ -177,7 +177,7 @@ public void testSynonymWordDeleteByAnalyzerFromFile() throws IOException { // Test with an index version where lenient should always be false by default IndexVersion randomNonLenientIndexVersion = IndexVersionUtils.randomVersionBetween( random(), - IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersions.INDEX_SORTING_ON_NESTED ); assertIsNotLenient.accept(randomNonLenientIndexVersion, false); @@ -231,7 +231,7 @@ public void testExpandSynonymWordDeleteByAnalyzer() throws IOException { // Test with an index version where lenient should always be false by default IndexVersion randomNonLenientIndexVersion = IndexVersionUtils.randomVersionBetween( random(), - IndexVersions.MINIMUM_COMPATIBLE, + IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersions.INDEX_SORTING_ON_NESTED ); assertIsNotLenient.accept(randomNonLenientIndexVersion, false); @@ -338,7 +338,7 @@ public void testShingleFilters() { Settings settings = Settings.builder() .put( IndexMetadata.SETTING_VERSION_CREATED, - IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()) + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current()) ) .put("path.home", createTempDir().toString()) .put("index.analysis.filter.synonyms.type", "synonym") @@ -392,7 +392,7 @@ public void testPreconfiguredTokenFilters() throws IOException { Settings settings = Settings.builder() .put( IndexMetadata.SETTING_VERSION_CREATED, - IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()) + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current()) ) .put("path.home", createTempDir().toString()) .build(); @@ -424,7 +424,7 @@ public void testDisallowedTokenFilters() throws IOException { Settings settings = Settings.builder() .put( IndexMetadata.SETTING_VERSION_CREATED, - IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()) + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current()) ) .put("path.home", createTempDir().toString()) .putList("common_words", "a", "b") diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/UniqueTokenFilterTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/UniqueTokenFilterTests.java index 6bec8dc1ebc62..d30e9d3c68cc9 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/UniqueTokenFilterTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/UniqueTokenFilterTests.java @@ -124,11 +124,7 @@ public void testOldVersionGetXUniqueTokenFilter() throws IOException { Settings settings = Settings.builder() .put( IndexMetadata.SETTING_VERSION_CREATED, - IndexVersionUtils.randomVersionBetween( - random(), - IndexVersions.MINIMUM_COMPATIBLE, - IndexVersionUtils.getPreviousVersion(IndexVersions.UNIQUE_TOKEN_FILTER_POS_FIX) - ) + IndexVersionUtils.randomPreviousCompatibleVersion(random(), IndexVersions.UNIQUE_TOKEN_FILTER_POS_FIX) ) .build(); IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); diff --git a/plugins/analysis-phonetic/src/test/java/org/elasticsearch/plugin/analysis/phonetic/AnalysisPhoneticFactoryTests.java b/plugins/analysis-phonetic/src/test/java/org/elasticsearch/plugin/analysis/phonetic/AnalysisPhoneticFactoryTests.java index 483c8ccef1202..e51d1f24a88ad 100644 --- a/plugins/analysis-phonetic/src/test/java/org/elasticsearch/plugin/analysis/phonetic/AnalysisPhoneticFactoryTests.java +++ b/plugins/analysis-phonetic/src/test/java/org/elasticsearch/plugin/analysis/phonetic/AnalysisPhoneticFactoryTests.java @@ -44,7 +44,7 @@ public void testDisallowedWithSynonyms() throws IOException { Settings settings = Settings.builder() .put( IndexMetadata.SETTING_VERSION_CREATED, - IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()) + IndexVersionUtils.randomVersionBetween(random(), IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current()) ) .put("path.home", createTempDir().toString()) .build(); diff --git a/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java b/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java index f5b86f422915e..7f3399cb24a15 100644 --- a/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java +++ b/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java @@ -55,6 +55,10 @@ public void testThatInstancesAreTheSameAlwaysForKeywordAnalyzer() { PreBuiltAnalyzers.KEYWORD.getAnalyzer(IndexVersion.current()), is(PreBuiltAnalyzers.KEYWORD.getAnalyzer(IndexVersions.MINIMUM_COMPATIBLE)) ); + assertThat( + PreBuiltAnalyzers.KEYWORD.getAnalyzer(IndexVersion.current()), + is(PreBuiltAnalyzers.KEYWORD.getAnalyzer(IndexVersions.MINIMUM_READONLY_COMPATIBLE)) + ); } public void testThatInstancesAreCachedAndReused() { diff --git a/server/src/test/java/org/elasticsearch/index/similarity/SimilarityServiceTests.java b/server/src/test/java/org/elasticsearch/index/similarity/SimilarityServiceTests.java index f6d7b3d1d65f3..ecb942492af53 100644 --- a/server/src/test/java/org/elasticsearch/index/similarity/SimilarityServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/similarity/SimilarityServiceTests.java @@ -74,7 +74,7 @@ public float score(float freq, long norm) { }; IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> SimilarityService.validateSimilarity(IndexVersions.MINIMUM_COMPATIBLE, negativeScoresSim) + () -> SimilarityService.validateSimilarity(IndexVersions.MINIMUM_READONLY_COMPATIBLE, negativeScoresSim) ); assertThat(e.getMessage(), Matchers.containsString("Similarities should not return negative scores")); @@ -99,7 +99,7 @@ public float score(float freq, long norm) { }; e = expectThrows( IllegalArgumentException.class, - () -> SimilarityService.validateSimilarity(IndexVersions.MINIMUM_COMPATIBLE, decreasingScoresWithFreqSim) + () -> SimilarityService.validateSimilarity(IndexVersions.MINIMUM_READONLY_COMPATIBLE, decreasingScoresWithFreqSim) ); assertThat(e.getMessage(), Matchers.containsString("Similarity scores should not decrease when term frequency increases")); @@ -124,7 +124,7 @@ public float score(float freq, long norm) { }; e = expectThrows( IllegalArgumentException.class, - () -> SimilarityService.validateSimilarity(IndexVersions.MINIMUM_COMPATIBLE, increasingScoresWithNormSim) + () -> SimilarityService.validateSimilarity(IndexVersions.MINIMUM_READONLY_COMPATIBLE, increasingScoresWithNormSim) ); assertThat(e.getMessage(), Matchers.containsString("Similarity scores should not increase when norm increases")); } diff --git a/server/src/test/java/org/elasticsearch/script/VectorScoreScriptUtilsTests.java b/server/src/test/java/org/elasticsearch/script/VectorScoreScriptUtilsTests.java index dcaa64ede9e89..48d09c75cb2d1 100644 --- a/server/src/test/java/org/elasticsearch/script/VectorScoreScriptUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/script/VectorScoreScriptUtilsTests.java @@ -51,12 +51,12 @@ public void testFloatVectorClassBindings() throws IOException { BinaryDenseVectorScriptDocValuesTests.wrap( new float[][] { docVector }, ElementType.FLOAT, - IndexVersions.MINIMUM_COMPATIBLE + IndexVersions.MINIMUM_READONLY_COMPATIBLE ), "test", ElementType.FLOAT, dims, - IndexVersions.MINIMUM_COMPATIBLE + IndexVersions.MINIMUM_READONLY_COMPATIBLE ), new BinaryDenseVectorDocValuesField( BinaryDenseVectorScriptDocValuesTests.wrap(new float[][] { docVector }, ElementType.FLOAT, IndexVersion.current()), @@ -303,12 +303,12 @@ public void testByteVsFloatSimilarity() throws IOException { BinaryDenseVectorScriptDocValuesTests.wrap( new float[][] { docVector }, ElementType.FLOAT, - IndexVersions.MINIMUM_COMPATIBLE + IndexVersions.MINIMUM_READONLY_COMPATIBLE ), "field0", ElementType.FLOAT, dims, - IndexVersions.MINIMUM_COMPATIBLE + IndexVersions.MINIMUM_READONLY_COMPATIBLE ), new BinaryDenseVectorDocValuesField( BinaryDenseVectorScriptDocValuesTests.wrap(new float[][] { docVector }, ElementType.FLOAT, IndexVersion.current()), diff --git a/server/src/test/java/org/elasticsearch/script/field/vectors/DenseVectorTests.java b/server/src/test/java/org/elasticsearch/script/field/vectors/DenseVectorTests.java index 8a5298777ede0..63d502a248aa8 100644 --- a/server/src/test/java/org/elasticsearch/script/field/vectors/DenseVectorTests.java +++ b/server/src/test/java/org/elasticsearch/script/field/vectors/DenseVectorTests.java @@ -69,7 +69,11 @@ public void testFloatVsListQueryVector() { assertEquals(knn.cosineSimilarity(arrayQV), knn.cosineSimilarity(listQV), 0.001f); assertEquals(knn.cosineSimilarity((Object) listQV), knn.cosineSimilarity((Object) arrayQV), 0.001f); - for (IndexVersion indexVersion : List.of(IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current())) { + for (IndexVersion indexVersion : List.of( + IndexVersions.MINIMUM_READONLY_COMPATIBLE, + IndexVersions.MINIMUM_COMPATIBLE, + IndexVersion.current() + )) { BytesRef value = BinaryDenseVectorScriptDocValuesTests.mockEncodeDenseVector(docVector, ElementType.FLOAT, indexVersion); BinaryDenseVector bdv = new BinaryDenseVector(docVector, value, dims, indexVersion); From 90f038d80237279e89cdb559da7e82b1896af677 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Thu, 19 Dec 2024 09:57:02 +0100 Subject: [PATCH 113/119] group dataset files (#118739) *.csv files used for creating data and *.csv-spec used to define test scenarios are blending in the resource directory. This change moves all *.csv files to data/*.csv so that it is easier to distinguish between data and specs. This allows to have a quicker overview of existing data when starting a new spec. --- .../java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java | 2 +- .../qa/testFixtures/src/main/resources/{ => data}/addresses.csv | 0 .../esql/qa/testFixtures/src/main/resources/{ => data}/ages.csv | 0 .../src/main/resources/{ => data}/airport_city_boundaries.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/airports.csv | 0 .../testFixtures/src/main/resources/{ => data}/airports_mp.csv | 0 .../testFixtures/src/main/resources/{ => data}/airports_web.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/alerts.csv | 0 .../esql/qa/testFixtures/src/main/resources/{ => data}/apps.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/books.csv | 0 .../src/main/resources/{ => data}/cartesian_multipolygons.csv | 0 .../testFixtures/src/main/resources/{ => data}/client_cidr.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/clientips.csv | 0 .../src/main/resources/{ => data}/countries_bbox.csv | 0 .../src/main/resources/{ => data}/countries_bbox_web.csv | 0 .../testFixtures/src/main/resources/{ => data}/date_nanos.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/decades.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/distances.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/employees.csv | 0 .../src/main/resources/{ => data}/employees_incompatible.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/heights.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/hosts.csv | 0 .../esql/qa/testFixtures/src/main/resources/{ => data}/k8s.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/languages.csv | 0 .../src/main/resources/{ => data}/languages_non_unique_key.csv | 0 .../src/main/resources/{ => data}/message_types.csv | 0 .../src/main/resources/{ => data}/missing_ip_sample_data.csv | 0 .../src/main/resources/{ => data}/multivalue_geometries.csv | 0 .../src/main/resources/{ => data}/multivalue_points.csv | 0 .../src/main/resources/{ => data}/mv_sample_data.csv | 0 .../testFixtures/src/main/resources/{ => data}/sample_data.csv | 0 .../src/main/resources/{ => data}/sample_data_ts_long.csv | 0 .../src/main/resources/{ => data}/sample_data_ts_nanos.csv | 0 .../src/main/resources/{ => data}/semantic_text.csv | 0 .../qa/testFixtures/src/main/resources/{ => data}/ul_logs.csv | 0 .../src/test/java/org/elasticsearch/xpack/esql/CsvTests.java | 2 +- 36 files changed, 2 insertions(+), 2 deletions(-) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/addresses.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/ages.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/airport_city_boundaries.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/airports.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/airports_mp.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/airports_web.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/alerts.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/apps.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/books.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/cartesian_multipolygons.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/client_cidr.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/clientips.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/countries_bbox.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/countries_bbox_web.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/date_nanos.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/decades.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/distances.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/employees.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/employees_incompatible.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/heights.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/hosts.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/k8s.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/languages.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/languages_non_unique_key.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/message_types.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/missing_ip_sample_data.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/multivalue_geometries.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/multivalue_points.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/mv_sample_data.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/sample_data.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/sample_data_ts_long.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/sample_data_ts_nanos.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/semantic_text.csv (100%) rename x-pack/plugin/esql/qa/testFixtures/src/main/resources/{ => data}/ul_logs.csv (100%) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java index 8e81d14b4dfd7..1d2de407219ee 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java @@ -359,7 +359,7 @@ private static void load(RestClient client, TestsDataset dataset, Logger logger, if (mapping == null) { throw new IllegalArgumentException("Cannot find resource " + mappingName); } - final String dataName = "/" + dataset.dataFileName; + final String dataName = "/data/" + dataset.dataFileName; URL data = CsvTestsDataLoader.class.getResource(dataName); if (data == null) { throw new IllegalArgumentException("Cannot find resource " + dataName); diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/addresses.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/addresses.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/addresses.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/addresses.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/ages.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/ages.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/ages.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/ages.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/airport_city_boundaries.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/airport_city_boundaries.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/airport_city_boundaries.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/airport_city_boundaries.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/airports.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/airports.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/airports.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/airports.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/airports_mp.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/airports_mp.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/airports_mp.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/airports_mp.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/airports_web.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/airports_web.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/airports_web.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/airports_web.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/alerts.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/alerts.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/alerts.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/alerts.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/apps.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/apps.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/apps.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/apps.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/books.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/books.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/books.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/books.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/cartesian_multipolygons.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/cartesian_multipolygons.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/cartesian_multipolygons.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/cartesian_multipolygons.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/client_cidr.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/client_cidr.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/client_cidr.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/client_cidr.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/clientips.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/clientips.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/clientips.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/clientips.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/countries_bbox.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/countries_bbox.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/countries_bbox.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/countries_bbox.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/countries_bbox_web.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/countries_bbox_web.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/countries_bbox_web.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/countries_bbox_web.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/date_nanos.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/date_nanos.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/decades.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/decades.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/decades.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/decades.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/distances.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/distances.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/distances.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/distances.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/employees.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/employees.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/employees.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/employees.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/employees_incompatible.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/employees_incompatible.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/employees_incompatible.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/employees_incompatible.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/heights.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/heights.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/heights.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/heights.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/hosts.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/hosts.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/hosts.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/hosts.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/k8s.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/k8s.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/languages.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/languages.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/languages.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/languages.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/languages_non_unique_key.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/languages_non_unique_key.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/languages_non_unique_key.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/languages_non_unique_key.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/message_types.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/message_types.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/message_types.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/message_types.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/missing_ip_sample_data.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/missing_ip_sample_data.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/missing_ip_sample_data.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/missing_ip_sample_data.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multivalue_geometries.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/multivalue_geometries.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/multivalue_geometries.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/multivalue_geometries.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multivalue_points.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/multivalue_points.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/multivalue_points.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/multivalue_points.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_sample_data.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/mv_sample_data.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_sample_data.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/mv_sample_data.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/sample_data.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/sample_data.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/sample_data.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/sample_data.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/sample_data_ts_long.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/sample_data_ts_long.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/sample_data_ts_long.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/sample_data_ts_long.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/sample_data_ts_nanos.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/sample_data_ts_nanos.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/sample_data_ts_nanos.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/sample_data_ts_nanos.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/semantic_text.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/semantic_text.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/semantic_text.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/semantic_text.csv diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/ul_logs.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/ul_logs.csv similarity index 100% rename from x-pack/plugin/esql/qa/testFixtures/src/main/resources/ul_logs.csv rename to x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/ul_logs.csv diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index e627f99322f08..1e0374c648579 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -426,7 +426,7 @@ private static CsvTestsDataLoader.TestsDataset testsDataset(LogicalPlan parsed) } private static TestPhysicalOperationProviders testOperationProviders(CsvTestsDataLoader.TestsDataset dataset) throws Exception { - var testData = loadPageFromCsv(CsvTests.class.getResource("/" + dataset.dataFileName()), dataset.typeMapping()); + var testData = loadPageFromCsv(CsvTests.class.getResource("/data/" + dataset.dataFileName()), dataset.typeMapping()); return new TestPhysicalOperationProviders(testData.v1(), testData.v2()); } From 9cc6cd422912b253bb4410452c333609c4cefc7c Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Thu, 19 Dec 2024 10:22:13 +0100 Subject: [PATCH 114/119] ESQL: Fix attribute set equals (#118823) Also add a test that uses this, for lookup join field attribute ids. --- docs/changelog/118823.yaml | 5 +++ .../esql/core/expression/AttributeSet.java | 8 +++- .../core/expression/AttributeMapTests.java | 2 +- .../core/expression/AttributeSetTests.java | 42 +++++++++++++++++++ .../xpack/esql/analysis/AnalyzerTests.java | 31 ++++++++++++++ 5 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 docs/changelog/118823.yaml create mode 100644 x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/AttributeSetTests.java diff --git a/docs/changelog/118823.yaml b/docs/changelog/118823.yaml new file mode 100644 index 0000000000000..b1afe1c873c17 --- /dev/null +++ b/docs/changelog/118823.yaml @@ -0,0 +1,5 @@ +pr: 118823 +summary: Fix attribute set equals +area: ES|QL +type: bug +issues: [] diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/AttributeSet.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/AttributeSet.java index a092e17931237..8a075e8887512 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/AttributeSet.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/AttributeSet.java @@ -174,8 +174,12 @@ public Stream parallelStream() { } @Override - public boolean equals(Object o) { - return delegate.equals(o); + public boolean equals(Object obj) { + if (obj instanceof AttributeSet as) { + obj = as.delegate; + } + + return delegate.equals(obj); } @Override diff --git a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/AttributeMapTests.java b/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/AttributeMapTests.java index 511c7f4b1d2f8..ade79c8168076 100644 --- a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/AttributeMapTests.java +++ b/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/AttributeMapTests.java @@ -30,7 +30,7 @@ public class AttributeMapTests extends ESTestCase { - private static Attribute a(String name) { + static Attribute a(String name) { return new UnresolvedAttribute(Source.EMPTY, name); } diff --git a/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/AttributeSetTests.java b/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/AttributeSetTests.java new file mode 100644 index 0000000000000..0e97773fb90d2 --- /dev/null +++ b/x-pack/plugin/esql-core/src/test/java/org/elasticsearch/xpack/esql/core/expression/AttributeSetTests.java @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.esql.core.expression; + +import org.elasticsearch.test.ESTestCase; + +import java.util.List; + +import static org.elasticsearch.xpack.esql.core.expression.AttributeMapTests.a; + +public class AttributeSetTests extends ESTestCase { + + public void testEquals() { + Attribute a1 = a("1"); + Attribute a2 = a("2"); + + AttributeSet first = new AttributeSet(List.of(a1, a2)); + assertEquals(first, first); + + AttributeSet second = new AttributeSet(); + second.add(a1); + second.add(a2); + + assertEquals(first, second); + assertEquals(second, first); + + AttributeSet third = new AttributeSet(); + third.add(a("1")); + third.add(a("2")); + + assertNotEquals(first, third); + assertNotEquals(third, first); + + assertEquals(AttributeSet.EMPTY, AttributeSet.EMPTY); + assertEquals(AttributeSet.EMPTY, first.intersect(third)); + assertEquals(third.intersect(first), AttributeSet.EMPTY); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index 5d1ff43dfe31b..674eda8916c5a 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.Attribute; +import org.elasticsearch.xpack.esql.core.expression.AttributeSet; import org.elasticsearch.xpack.esql.core.expression.Expressions; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.expression.Literal; @@ -2190,6 +2191,36 @@ public void testLookupJoinUnknownField() { assertThat(e.getMessage(), containsString(errorMessage3 + "right side of join")); } + public void testMultipleLookupJoinsGiveDifferentAttributes() { + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); + + // The field attributes that get contributed by different LOOKUP JOIN commands must have different name ids, + // even if they have the same names. Otherwise, things like dependency analysis - like in PruneColumns - cannot work based on + // name ids and shadowing semantics proliferate into all kinds of optimizer code. + + String query = "FROM test" + + "| EVAL language_code = languages" + + "| LOOKUP JOIN languages_lookup ON language_code" + + "| LOOKUP JOIN languages_lookup ON language_code"; + LogicalPlan analyzedPlan = analyze(query); + + List lookupFields = new ArrayList<>(); + List> lookupFieldNames = new ArrayList<>(); + analyzedPlan.forEachUp(EsRelation.class, esRelation -> { + if (esRelation.indexMode() == IndexMode.LOOKUP) { + lookupFields.add(esRelation.outputSet()); + lookupFieldNames.add(esRelation.outputSet().stream().map(NamedExpression::name).collect(Collectors.toSet())); + } + }); + + assertEquals(lookupFieldNames.size(), 2); + assertEquals(lookupFieldNames.get(0), lookupFieldNames.get(1)); + + assertEquals(lookupFields.size(), 2); + AttributeSet intersection = lookupFields.get(0).intersect(lookupFields.get(1)); + assertEquals(AttributeSet.EMPTY, intersection); + } + public void testLookupJoinIndexMode() { assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V8.isEnabled()); From 8845cf725bf7412c7d53cfaac261026f95d78601 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Thu, 19 Dec 2024 10:32:47 +0100 Subject: [PATCH 115/119] Include read-only index versions in randomCompatbileVersion/randomCompatiblePreviousVersion (#119013) Read-only versions are now included in all index versions, see #118793 . The next step is to broaden testing where possible to include index versions that cannot be written to. To do that, we change the behaviour of the existing randomCompatbileVersion and randomCompatiblePreviousVersion and methods to include read-only versions and we introduce corresponding variants of these methods that only return write compatible index versions. As part of this change, we can also remove some of the `@UpdateForV9` annotations that relate to v7 index versions which randomCompatibleVersion no longer covered. That's now fixed and such tests can simply be restored. --- .../search/GeoBoundingBoxQueryLegacyGeoShapeIT.java | 2 +- .../legacygeo/search/LegacyGeoShapeIT.java | 2 +- .../legacygeo/GeoJsonShapeParserTests.java | 3 --- .../legacygeo/GeoWKTShapeParserTests.java | 7 ------- .../mapper/LegacyGeoShapeFieldMapperTests.java | 4 ---- .../legacygeo/mapper/LegacyGeoShapeFieldTypeTests.java | 4 ---- .../action/admin/indices/create/CloneIndexIT.java | 2 +- .../action/admin/indices/create/SplitIndexIT.java | 4 ++-- .../seqno/PeerRecoveryRetentionLeaseCreationIT.java | 2 +- .../search/aggregations/bucket/GeoDistanceIT.java | 2 +- .../search/aggregations/bucket/GeoHashGridIT.java | 2 +- .../search/functionscore/DecayFunctionScoreIT.java | 2 +- .../search/geo/GeoBoundingBoxQueryGeoPointIT.java | 2 +- .../org/elasticsearch/search/geo/GeoDistanceIT.java | 2 +- .../org/elasticsearch/search/geo/GeoPolygonIT.java | 2 +- .../org/elasticsearch/search/sort/GeoDistanceIT.java | 8 ++++---- .../search/sort/GeoDistanceSortBuilderIT.java | 6 +++--- .../cluster/metadata/IndexMetadataVerifierTests.java | 2 +- .../replication/RetentionLeasesReplicationTests.java | 2 +- .../test/test/InternalClusterForbiddenSettingIT.java | 4 ++-- .../elasticsearch/test/index/IndexVersionUtils.java | 10 ++++++++++ .../xpack/spatial/search/CartesianShapeIT.java | 2 +- .../GeoBoundingBoxQueryGeoShapeWithDocValuesIT.java | 2 +- .../xpack/spatial/search/GeoShapeWithDocValuesIT.java | 2 +- .../mapper/GeoShapeWithDocValuesFieldMapperTests.java | 3 --- 25 files changed, 36 insertions(+), 47 deletions(-) diff --git a/modules/legacy-geo/src/internalClusterTest/java/org/elasticsearch/legacygeo/search/GeoBoundingBoxQueryLegacyGeoShapeIT.java b/modules/legacy-geo/src/internalClusterTest/java/org/elasticsearch/legacygeo/search/GeoBoundingBoxQueryLegacyGeoShapeIT.java index 37c31c8af47b0..d2dd5b7442dd2 100644 --- a/modules/legacy-geo/src/internalClusterTest/java/org/elasticsearch/legacygeo/search/GeoBoundingBoxQueryLegacyGeoShapeIT.java +++ b/modules/legacy-geo/src/internalClusterTest/java/org/elasticsearch/legacygeo/search/GeoBoundingBoxQueryLegacyGeoShapeIT.java @@ -45,6 +45,6 @@ public XContentBuilder getMapping() throws IOException { @Override public IndexVersion randomSupportedVersion() { - return IndexVersionUtils.randomCompatibleVersion(random()); + return IndexVersionUtils.randomCompatibleWriteVersion(random()); } } diff --git a/modules/legacy-geo/src/internalClusterTest/java/org/elasticsearch/legacygeo/search/LegacyGeoShapeIT.java b/modules/legacy-geo/src/internalClusterTest/java/org/elasticsearch/legacygeo/search/LegacyGeoShapeIT.java index 918c343b79b7b..73b7c07c45fe5 100644 --- a/modules/legacy-geo/src/internalClusterTest/java/org/elasticsearch/legacygeo/search/LegacyGeoShapeIT.java +++ b/modules/legacy-geo/src/internalClusterTest/java/org/elasticsearch/legacygeo/search/LegacyGeoShapeIT.java @@ -41,7 +41,7 @@ protected void getGeoShapeMapping(XContentBuilder b) throws IOException { @Override protected IndexVersion randomSupportedVersion() { - return IndexVersionUtils.randomCompatibleVersion(random()); + return IndexVersionUtils.randomCompatibleWriteVersion(random()); } @Override diff --git a/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/GeoJsonShapeParserTests.java b/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/GeoJsonShapeParserTests.java index 9b83cd9ffdb2b..bd5b289abc588 100644 --- a/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/GeoJsonShapeParserTests.java +++ b/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/GeoJsonShapeParserTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.common.geo.GeometryNormalizer; import org.elasticsearch.common.geo.GeometryParser; import org.elasticsearch.common.geo.Orientation; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.GeometryCollection; import org.elasticsearch.geometry.Line; @@ -344,8 +343,6 @@ public void testParsePolygon() throws IOException, ParseException { assertGeometryEquals(p, polygonGeoJson, false); } - @UpdateForV9(owner = UpdateForV9.Owner.SEARCH_ANALYTICS) - @AwaitsFix(bugUrl = "this test is using pre 8.0.0 index versions so needs to be removed or updated") public void testParse3DPolygon() throws IOException, ParseException { XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() .startObject() diff --git a/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/GeoWKTShapeParserTests.java b/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/GeoWKTShapeParserTests.java index 5d0df9215ef25..f944a368b2a6c 100644 --- a/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/GeoWKTShapeParserTests.java +++ b/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/GeoWKTShapeParserTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeometryNormalizer; import org.elasticsearch.common.geo.Orientation; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.Line; import org.elasticsearch.geometry.MultiLine; @@ -303,8 +302,6 @@ public void testParseMixedDimensionPolyWithHole() throws IOException, ParseExcep assertThat(e, hasToString(containsString("coordinate dimensions do not match"))); } - @UpdateForV9(owner = UpdateForV9.Owner.SEARCH_ANALYTICS) - @AwaitsFix(bugUrl = "this test is using pre 8.0.0 index versions so needs to be removed or updated") public void testParseMixedDimensionPolyWithHoleStoredZ() throws IOException { List shellCoordinates = new ArrayList<>(); shellCoordinates.add(new Coordinate(100, 0)); @@ -338,8 +335,6 @@ public void testParseMixedDimensionPolyWithHoleStoredZ() throws IOException { assertThat(e, hasToString(containsString("unable to add coordinate to CoordinateBuilder: coordinate dimensions do not match"))); } - @UpdateForV9(owner = UpdateForV9.Owner.SEARCH_ANALYTICS) - @AwaitsFix(bugUrl = "this test is using pre 8.0.0 index versions so needs to be removed or updated") public void testParsePolyWithStoredZ() throws IOException { List shellCoordinates = new ArrayList<>(); shellCoordinates.add(new Coordinate(100, 0, 0)); @@ -363,8 +358,6 @@ public void testParsePolyWithStoredZ() throws IOException { assertEquals(shapeBuilder.numDimensions(), 3); } - @UpdateForV9(owner = UpdateForV9.Owner.SEARCH_ANALYTICS) - @AwaitsFix(bugUrl = "this test is using pre 8.0.0 index versions so needs to be removed or updated") public void testParseOpenPolygon() throws IOException { String openPolygon = "POLYGON ((100 5, 100 10, 90 10, 90 5))"; diff --git a/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapperTests.java b/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapperTests.java index 7352b4d88a42b..c97b0a28d22de 100644 --- a/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapperTests.java +++ b/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapperTests.java @@ -13,7 +13,6 @@ import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy; import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree; import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree; -import org.apache.lucene.tests.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.geo.GeoUtils; @@ -21,7 +20,6 @@ import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.core.CheckedConsumer; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.geometry.Point; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; @@ -56,8 +54,6 @@ import static org.mockito.Mockito.when; @SuppressWarnings("deprecation") -@UpdateForV9(owner = UpdateForV9.Owner.SEARCH_ANALYTICS) -@AwaitsFix(bugUrl = "this is testing legacy functionality so can likely be removed in 9.0") public class LegacyGeoShapeFieldMapperTests extends MapperTestCase { @Override diff --git a/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldTypeTests.java b/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldTypeTests.java index bf616c8190324..f5e09f19c1a71 100644 --- a/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldTypeTests.java +++ b/modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldTypeTests.java @@ -8,9 +8,7 @@ */ package org.elasticsearch.legacygeo.mapper; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.common.geo.SpatialStrategy; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.mapper.FieldTypeTestCase; @@ -23,8 +21,6 @@ import java.util.List; import java.util.Map; -@UpdateForV9(owner = UpdateForV9.Owner.SEARCH_ANALYTICS) -@LuceneTestCase.AwaitsFix(bugUrl = "this is testing legacy functionality so can likely be removed in 9.0") public class LegacyGeoShapeFieldTypeTests extends FieldTypeTestCase { /** diff --git a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/CloneIndexIT.java b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/CloneIndexIT.java index 47f96aebacd7d..fa2b053ead348 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/CloneIndexIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/CloneIndexIT.java @@ -39,7 +39,7 @@ protected boolean forbidPrivateIndexSettings() { } public void testCreateCloneIndex() { - IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); int numPrimaryShards = randomIntBetween(1, 5); prepareCreate("source").setSettings( Settings.builder().put(indexSettings()).put("number_of_shards", numPrimaryShards).put("index.version.created", version) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/SplitIndexIT.java b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/SplitIndexIT.java index 8391ab270b1d1..9ba6ac4bd9c58 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/SplitIndexIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/SplitIndexIT.java @@ -341,8 +341,8 @@ private static IndexMetadata indexMetadata(final Client client, final String ind return clusterStateResponse.getState().metadata().index(index); } - public void testCreateSplitIndex() throws Exception { - IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + public void testCreateSplitIndex() { + IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); prepareCreate("source").setSettings( Settings.builder().put(indexSettings()).put("number_of_shards", 1).put("index.version.created", version) ).get(); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/index/seqno/PeerRecoveryRetentionLeaseCreationIT.java b/server/src/internalClusterTest/java/org/elasticsearch/index/seqno/PeerRecoveryRetentionLeaseCreationIT.java index 07f9d9ee7b6c3..92e5eb8e046bc 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/index/seqno/PeerRecoveryRetentionLeaseCreationIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/index/seqno/PeerRecoveryRetentionLeaseCreationIT.java @@ -48,7 +48,7 @@ public void testCanRecoverFromStoreWithoutPeerRecoveryRetentionLease() throws Ex Settings.builder() .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) - .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomCompatibleVersion(random())) + .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomCompatibleWriteVersion(random())) ) ); ensureGreen(INDEX_NAME); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/GeoDistanceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/GeoDistanceIT.java index 907f943e68422..6c67bd2a98606 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/GeoDistanceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/GeoDistanceIT.java @@ -56,7 +56,7 @@ protected boolean forbidPrivateIndexSettings() { return false; } - private final IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + private final IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); private IndexRequestBuilder indexCity(String idx, String name, String... latLons) throws Exception { XContentBuilder source = jsonBuilder().startObject().field("city", name); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/GeoHashGridIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/GeoHashGridIT.java index 1ad7d1a11bea7..1de51d6df8197 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/GeoHashGridIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/GeoHashGridIT.java @@ -49,7 +49,7 @@ protected boolean forbidPrivateIndexSettings() { return false; } - private final IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + private final IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); static Map expectedDocCountsForGeoHash = null; static Map multiValuedExpectedDocCountsForGeoHash = null; diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/DecayFunctionScoreIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/DecayFunctionScoreIT.java index 9988624f6a677..a55edf3782bcc 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/DecayFunctionScoreIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/DecayFunctionScoreIT.java @@ -748,7 +748,7 @@ public void testDateWithoutOrigin() throws Exception { } public void testManyDocsLin() throws Exception { - IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = jsonBuilder().startObject() .startObject("_doc") diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoPointIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoPointIT.java index 2489889be19e5..8104e4ed7a825 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoPointIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoPointIT.java @@ -32,6 +32,6 @@ public XContentBuilder getMapping() throws IOException { @Override public IndexVersion randomSupportedVersion() { - return IndexVersionUtils.randomCompatibleVersion(random()); + return IndexVersionUtils.randomCompatibleWriteVersion(random()); } } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoDistanceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoDistanceIT.java index 9b4e28055a988..a309fa81f6dc1 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoDistanceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoDistanceIT.java @@ -96,7 +96,7 @@ protected boolean forbidPrivateIndexSettings() { @Before public void setupTestIndex() throws IOException { - IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder() .startObject() diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPolygonIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPolygonIT.java index 4b8f29f3cc9a5..aadefd9bd8018 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPolygonIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPolygonIT.java @@ -39,7 +39,7 @@ protected boolean forbidPrivateIndexSettings() { @Override protected void setupSuiteScopeCluster() throws Exception { - IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); assertAcked( diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/sort/GeoDistanceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/sort/GeoDistanceIT.java index e80678c4f5fc6..f55d4505f3f58 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/sort/GeoDistanceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/sort/GeoDistanceIT.java @@ -45,7 +45,7 @@ protected boolean forbidPrivateIndexSettings() { } public void testDistanceSortingMVFields() throws Exception { - IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder() .startObject() @@ -237,7 +237,7 @@ public void testDistanceSortingMVFields() throws Exception { // Regression bug: // https://github.com/elastic/elasticsearch/issues/2851 public void testDistanceSortingWithMissingGeoPoint() throws Exception { - IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder() .startObject() @@ -299,7 +299,7 @@ public void testDistanceSortingWithMissingGeoPoint() throws Exception { } public void testDistanceSortingNestedFields() throws Exception { - IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder() .startObject() @@ -551,7 +551,7 @@ public void testDistanceSortingNestedFields() throws Exception { * Issue 3073 */ public void testGeoDistanceFilter() throws IOException { - IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); double lat = 40.720611; double lon = -73.998776; diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderIT.java index aabca1b9333f8..d53c90a5d1e28 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderIT.java @@ -60,7 +60,7 @@ public void testManyToManyGeoPoints() throws ExecutionException, InterruptedExce * |___________________________ * 1 2 3 4 5 6 7 */ - IndexVersion version = randomBoolean() ? IndexVersion.current() : IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = randomBoolean() ? IndexVersion.current() : IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); assertAcked(prepareCreate("index").setSettings(settings).setMapping(LOCATION_FIELD, "type=geo_point")); XContentBuilder d1Builder = jsonBuilder(); @@ -152,7 +152,7 @@ public void testSingeToManyAvgMedian() throws ExecutionException, InterruptedExc * d1 = (0, 1), (0, 4), (0, 10); so avg. distance is 5, median distance is 4 * d2 = (0, 1), (0, 5), (0, 6); so avg. distance is 4, median distance is 5 */ - IndexVersion version = randomBoolean() ? IndexVersion.current() : IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = randomBoolean() ? IndexVersion.current() : IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); assertAcked(prepareCreate("index").setSettings(settings).setMapping(LOCATION_FIELD, "type=geo_point")); XContentBuilder d1Builder = jsonBuilder(); @@ -225,7 +225,7 @@ public void testManyToManyGeoPointsWithDifferentFormats() throws ExecutionExcept * |______________________ * 1 2 3 4 5 6 */ - IndexVersion version = randomBoolean() ? IndexVersion.current() : IndexVersionUtils.randomCompatibleVersion(random()); + IndexVersion version = randomBoolean() ? IndexVersion.current() : IndexVersionUtils.randomCompatibleWriteVersion(random()); Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); assertAcked(prepareCreate("index").setSettings(settings).setMapping(LOCATION_FIELD, "type=geo_point")); XContentBuilder d1Builder = jsonBuilder(); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetadataVerifierTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetadataVerifierTests.java index 3b122864aa472..6ee86470861b4 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetadataVerifierTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetadataVerifierTests.java @@ -97,7 +97,7 @@ public void testCustomSimilarity() { .put("index.similarity.my_similarity.after_effect", "l") .build() ); - service.verifyIndexMetadata(src, IndexVersions.MINIMUM_COMPATIBLE); + service.verifyIndexMetadata(src, IndexVersions.MINIMUM_READONLY_COMPATIBLE); } public void testIncompatibleVersion() { diff --git a/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java b/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java index 8f45a15c73bb6..1f82d7998257e 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/RetentionLeasesReplicationTests.java @@ -147,7 +147,7 @@ protected void syncRetentionLeases(ShardId id, RetentionLeases leases, ActionLis public void testTurnOffTranslogRetentionAfterAllShardStarted() throws Exception { final Settings.Builder settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true); if (randomBoolean()) { - settings.put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomCompatibleVersion(random())); + settings.put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersionUtils.randomCompatibleWriteVersion(random())); } try (ReplicationGroup group = createGroup(between(1, 2), settings.build())) { group.startAll(); diff --git a/test/framework/src/integTest/java/org/elasticsearch/test/test/InternalClusterForbiddenSettingIT.java b/test/framework/src/integTest/java/org/elasticsearch/test/test/InternalClusterForbiddenSettingIT.java index d13450fbb52dd..2033743354f34 100644 --- a/test/framework/src/integTest/java/org/elasticsearch/test/test/InternalClusterForbiddenSettingIT.java +++ b/test/framework/src/integTest/java/org/elasticsearch/test/test/InternalClusterForbiddenSettingIT.java @@ -26,7 +26,7 @@ protected boolean forbidPrivateIndexSettings() { } public void testRestart() throws Exception { - IndexVersion version = IndexVersionUtils.randomPreviousCompatibleVersion(random(), IndexVersion.current()); + IndexVersion version = IndexVersionUtils.randomPreviousCompatibleWriteVersion(random(), IndexVersion.current()); // create / delete an index with forbidden setting prepareCreate("test").setSettings(settings(version).build()).get(); indicesAdmin().prepareDelete("test").get(); @@ -38,7 +38,7 @@ public void testRestart() throws Exception { } public void testRollingRestart() throws Exception { - IndexVersion version = IndexVersionUtils.randomPreviousCompatibleVersion(random(), IndexVersion.current()); + IndexVersion version = IndexVersionUtils.randomPreviousCompatibleWriteVersion(random(), IndexVersion.current()); // create / delete an index with forbidden setting prepareCreate("test").setSettings(settings(version).build()).get(); indicesAdmin().prepareDelete("test").get(); diff --git a/test/framework/src/main/java/org/elasticsearch/test/index/IndexVersionUtils.java b/test/framework/src/main/java/org/elasticsearch/test/index/IndexVersionUtils.java index 592cffac33552..667149e4bdd3e 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/index/IndexVersionUtils.java +++ b/test/framework/src/main/java/org/elasticsearch/test/index/IndexVersionUtils.java @@ -122,11 +122,21 @@ public static IndexVersion getNextVersion(IndexVersion version) { /** Returns a random {@code IndexVersion} that is compatible with {@link IndexVersion#current()} */ public static IndexVersion randomCompatibleVersion(Random random) { + return randomVersionBetween(random, IndexVersions.MINIMUM_READONLY_COMPATIBLE, IndexVersion.current()); + } + + /** Returns a random {@code IndexVersion} that is compatible with {@link IndexVersion#current()} and can be written to */ + public static IndexVersion randomCompatibleWriteVersion(Random random) { return randomVersionBetween(random, IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current()); } /** Returns a random {@code IndexVersion} that is compatible with the previous version to {@code version} */ public static IndexVersion randomPreviousCompatibleVersion(Random random, IndexVersion version) { + return randomVersionBetween(random, IndexVersions.MINIMUM_READONLY_COMPATIBLE, getPreviousVersion(version)); + } + + /** Returns a random {@code IndexVersion} that is compatible with the previous version to {@code version} and can be written to */ + public static IndexVersion randomPreviousCompatibleWriteVersion(Random random, IndexVersion version) { return randomVersionBetween(random, IndexVersions.MINIMUM_COMPATIBLE, getPreviousVersion(version)); } } diff --git a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/CartesianShapeIT.java b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/CartesianShapeIT.java index 83fbd7262461d..eb4515a897118 100644 --- a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/CartesianShapeIT.java +++ b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/CartesianShapeIT.java @@ -36,7 +36,7 @@ protected void getGeoShapeMapping(XContentBuilder b) throws IOException { @Override protected IndexVersion randomSupportedVersion() { - return IndexVersionUtils.randomCompatibleVersion(random()); + return IndexVersionUtils.randomCompatibleWriteVersion(random()); } @Override diff --git a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoBoundingBoxQueryGeoShapeWithDocValuesIT.java b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoBoundingBoxQueryGeoShapeWithDocValuesIT.java index 3d91fb443aabd..4a6fa5d545bef 100644 --- a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoBoundingBoxQueryGeoShapeWithDocValuesIT.java +++ b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoBoundingBoxQueryGeoShapeWithDocValuesIT.java @@ -40,6 +40,6 @@ public XContentBuilder getMapping() throws IOException { @Override public IndexVersion randomSupportedVersion() { - return IndexVersionUtils.randomCompatibleVersion(random()); + return IndexVersionUtils.randomCompatibleWriteVersion(random()); } } diff --git a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeWithDocValuesIT.java b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeWithDocValuesIT.java index b4d7a472591bd..0857b078be579 100644 --- a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeWithDocValuesIT.java +++ b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeWithDocValuesIT.java @@ -60,7 +60,7 @@ protected void getGeoShapeMapping(XContentBuilder b) throws IOException { @Override protected IndexVersion randomSupportedVersion() { - return IndexVersionUtils.randomCompatibleVersion(random()); + return IndexVersionUtils.randomCompatibleWriteVersion(random()); } @Override diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapperTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapperTests.java index 4b13a7bf1f829..58fde288cfc60 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapperTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapperTests.java @@ -10,7 +10,6 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Strings; import org.elasticsearch.common.geo.Orientation; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.utils.GeometryValidator; import org.elasticsearch.geometry.utils.WellKnownBinary; @@ -280,8 +279,6 @@ public void testInvalidCurrentVersion() { ); } - @UpdateForV9(owner = UpdateForV9.Owner.SEARCH_ANALYTICS) - @AwaitsFix(bugUrl = "this is testing legacy functionality so can likely be removed in 9.0") public void testGeoShapeLegacyMerge() throws Exception { IndexVersion version = IndexVersionUtils.randomPreviousCompatibleVersion(random(), IndexVersions.V_8_0_0); MapperService m = createMapperService(version, fieldMapping(b -> b.field("type", getFieldName()))); From 1cf5b03b31716a5424a60a75aeade6d7379ca761 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 19 Dec 2024 21:08:51 +1100 Subject: [PATCH 116/119] Mute org.elasticsearch.cluster.coordination.NodeJoinExecutorTests testSuccess #119052 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 12f1fc510a332..8cfc7c082473f 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -301,6 +301,9 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/116777 - class: org.elasticsearch.xpack.security.authc.ldap.ActiveDirectoryRunAsIT issue: https://github.com/elastic/elasticsearch/issues/115727 +- class: org.elasticsearch.cluster.coordination.NodeJoinExecutorTests + method: testSuccess + issue: https://github.com/elastic/elasticsearch/issues/119052 # Examples: # From 4efeca83b4a9f40f5a8fac9a8a3b4f2191d5282e Mon Sep 17 00:00:00 2001 From: Mary Gouseti Date: Thu, 19 Dec 2024 12:20:12 +0200 Subject: [PATCH 117/119] [Failure store] Reconciliate failure indices during snapshotting (#118834) In this PR we reconciliate the failure indices of a data stream just like we do for the backing indices. The only difference is that a data stream can have an empty list of failure indices, while it cannot have an empty list of backing indices. An easy way to create a situation where certain backing or failure indices are not included in a snapshot is via using exclusions in the multi-target expression of the snapshot. For example: ``` PUT /_snapshot/my_repository/my-snapshot?wait_for_completion=true { "indices": "my-ds*", "-.fs-my-ds-000001" } ``` --- .../datastreams/DataStreamsSnapshotsIT.java | 279 +++++++++--------- .../cluster/metadata/DataStream.java | 49 ++- .../snapshots/SnapshotsService.java | 10 +- .../cluster/metadata/DataStreamTests.java | 66 ++++- 4 files changed, 224 insertions(+), 180 deletions(-) diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamsSnapshotsIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamsSnapshotsIT.java index 286ad68896797..32d080ccc46b1 100644 --- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamsSnapshotsIT.java +++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamsSnapshotsIT.java @@ -60,7 +60,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -77,6 +76,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; public class DataStreamsSnapshotsIT extends AbstractSnapshotIntegTestCase { @@ -145,18 +145,11 @@ public void setup() throws Exception { // Resolve backing index names after data streams have been created: // (these names have a date component, and running around midnight could lead to test failures otherwise) - GetDataStreamAction.Request getDataStreamRequest = new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "*" }); - GetDataStreamAction.Response getDataStreamResponse = client.execute(GetDataStreamAction.INSTANCE, getDataStreamRequest).actionGet(); - dsBackingIndexName = getDataStreamResponse.getDataStreams().get(0).getDataStream().getIndices().get(0).getName(); - otherDsBackingIndexName = getDataStreamResponse.getDataStreams().get(1).getDataStream().getIndices().get(0).getName(); - fsBackingIndexName = getDataStreamResponse.getDataStreams().get(2).getDataStream().getIndices().get(0).getName(); - fsFailureIndexName = getDataStreamResponse.getDataStreams() - .get(2) - .getDataStream() - .getFailureIndices() - .getIndices() - .get(0) - .getName(); + List dataStreamInfos = getDataStreamInfo("*"); + dsBackingIndexName = dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName(); + otherDsBackingIndexName = dataStreamInfos.get(1).getDataStream().getIndices().get(0).getName(); + fsBackingIndexName = dataStreamInfos.get(2).getDataStream().getIndices().get(0).getName(); + fsFailureIndexName = dataStreamInfos.get(2).getDataStream().getFailureIndices().getIndices().get(0).getName(); // Will be used in some tests, to test renaming while restoring a snapshot: ds2BackingIndexName = dsBackingIndexName.replace("-ds-", "-ds2-"); @@ -198,9 +191,7 @@ public void testSnapshotAndRestore() throws Exception { assertEquals(Collections.singletonList(dsBackingIndexName), getSnapshot(REPO, SNAPSHOT).indices()); - assertAcked( - client.execute(DeleteDataStreamAction.INSTANCE, new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "ds" })) - ); + assertAcked(client.execute(DeleteDataStreamAction.INSTANCE, new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, "ds"))); RestoreSnapshotResponse restoreSnapshotResponse = client.admin() .cluster() @@ -218,13 +209,10 @@ public void testSnapshotAndRestore() throws Exception { assertEquals(DOCUMENT_SOURCE, hits[0].getSourceAsMap()); }); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "ds" }) - ).get(); - assertEquals(1, ds.getDataStreams().size()); - assertEquals(1, ds.getDataStreams().get(0).getDataStream().getIndices().size()); - assertEquals(dsBackingIndexName, ds.getDataStreams().get(0).getDataStream().getIndices().get(0).getName()); + List ds = getDataStreamInfo("ds"); + assertEquals(1, ds.size()); + assertEquals(1, ds.get(0).getDataStream().getIndices().size()); + assertEquals(dsBackingIndexName, ds.get(0).getDataStream().getIndices().get(0).getName()); GetAliasesResponse getAliasesResponse = client.admin().indices().getAliases(new GetAliasesRequest("my-alias")).actionGet(); assertThat(getAliasesResponse.getDataStreamAliases().keySet(), containsInAnyOrder("ds", "other-ds")); @@ -278,19 +266,18 @@ public void testSnapshotAndRestoreAllDataStreamsInPlace() throws Exception { assertEquals(DOCUMENT_SOURCE, hits[0].getSourceAsMap()); }); - GetDataStreamAction.Request getDataSteamRequest = new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "*" }); - GetDataStreamAction.Response ds = client.execute(GetDataStreamAction.INSTANCE, getDataSteamRequest).get(); + List dataStreamInfos = getDataStreamInfo("*"); assertThat( - ds.getDataStreams().stream().map(e -> e.getDataStream().getName()).collect(Collectors.toList()), + dataStreamInfos.stream().map(e -> e.getDataStream().getName()).collect(Collectors.toList()), contains(equalTo("ds"), equalTo("other-ds"), equalTo("with-fs")) ); - List backingIndices = ds.getDataStreams().get(0).getDataStream().getIndices(); + List backingIndices = dataStreamInfos.get(0).getDataStream().getIndices(); assertThat(backingIndices.stream().map(Index::getName).collect(Collectors.toList()), contains(dsBackingIndexName)); - backingIndices = ds.getDataStreams().get(1).getDataStream().getIndices(); + backingIndices = dataStreamInfos.get(1).getDataStream().getIndices(); assertThat(backingIndices.stream().map(Index::getName).collect(Collectors.toList()), contains(otherDsBackingIndexName)); - backingIndices = ds.getDataStreams().get(2).getDataStream().getIndices(); + backingIndices = dataStreamInfos.get(2).getDataStream().getIndices(); assertThat(backingIndices.stream().map(Index::getName).collect(Collectors.toList()), contains(fsBackingIndexName)); - List failureIndices = ds.getDataStreams().get(2).getDataStream().getFailureIndices().getIndices(); + List failureIndices = dataStreamInfos.get(2).getDataStream().getFailureIndices().getIndices(); assertThat(failureIndices.stream().map(Index::getName).collect(Collectors.toList()), contains(fsFailureIndexName)); } @@ -337,14 +324,10 @@ public void testSnapshotAndRestoreInPlace() { assertEquals(DOCUMENT_SOURCE, hits[0].getSourceAsMap()); }); - GetDataStreamAction.Request getDataSteamRequest = new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "ds" }); - GetDataStreamAction.Response ds = client.execute(GetDataStreamAction.INSTANCE, getDataSteamRequest).actionGet(); - assertThat( - ds.getDataStreams().stream().map(e -> e.getDataStream().getName()).collect(Collectors.toList()), - contains(equalTo("ds")) - ); - List backingIndices = ds.getDataStreams().get(0).getDataStream().getIndices(); - assertThat(ds.getDataStreams().get(0).getDataStream().getIndices(), hasSize(1)); + List dsInfo = getDataStreamInfo("ds"); + assertThat(dsInfo.stream().map(e -> e.getDataStream().getName()).collect(Collectors.toList()), contains(equalTo("ds"))); + List backingIndices = dsInfo.get(0).getDataStream().getIndices(); + assertThat(dsInfo.get(0).getDataStream().getIndices(), hasSize(1)); assertThat(backingIndices.stream().map(Index::getName).collect(Collectors.toList()), contains(equalTo(dsBackingIndexName))); // The backing index created as part of rollover should still exist (but just not part of the data stream) @@ -357,39 +340,40 @@ public void testSnapshotAndRestoreInPlace() { } public void testFailureStoreSnapshotAndRestore() throws Exception { + String dataStreamName = "with-fs"; CreateSnapshotResponse createSnapshotResponse = client.admin() .cluster() .prepareCreateSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) .setWaitForCompletion(true) - .setIndices("with-fs") + .setIndices(dataStreamName) .setIncludeGlobalState(false) .get(); RestStatus status = createSnapshotResponse.getSnapshotInfo().status(); assertEquals(RestStatus.OK, status); + assertThat(getSnapshot(REPO, SNAPSHOT).dataStreams(), containsInAnyOrder(dataStreamName)); assertThat(getSnapshot(REPO, SNAPSHOT).indices(), containsInAnyOrder(fsBackingIndexName, fsFailureIndexName)); - assertAcked(client.execute(DeleteDataStreamAction.INSTANCE, new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, "with-fs"))); + assertAcked( + client.execute(DeleteDataStreamAction.INSTANCE, new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, dataStreamName)) + ); { RestoreSnapshotResponse restoreSnapshotResponse = client.admin() .cluster() .prepareRestoreSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) .setWaitForCompletion(true) - .setIndices("with-fs") + .setIndices(dataStreamName) .get(); assertEquals(2, restoreSnapshotResponse.getRestoreInfo().successfulShards()); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "with-fs" }) - ).get(); - assertEquals(1, ds.getDataStreams().size()); - assertEquals(1, ds.getDataStreams().get(0).getDataStream().getIndices().size()); - assertEquals(fsBackingIndexName, ds.getDataStreams().get(0).getDataStream().getIndices().get(0).getName()); - assertEquals(fsFailureIndexName, ds.getDataStreams().get(0).getDataStream().getFailureIndices().getIndices().get(0).getName()); + List dataStreamInfos = getDataStreamInfo(dataStreamName); + assertEquals(1, dataStreamInfos.size()); + assertEquals(1, dataStreamInfos.get(0).getDataStream().getIndices().size()); + assertEquals(fsBackingIndexName, dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName()); + assertEquals(fsFailureIndexName, dataStreamInfos.get(0).getDataStream().getFailureIndices().getIndices().get(0).getName()); } { // With rename pattern @@ -397,21 +381,18 @@ public void testFailureStoreSnapshotAndRestore() throws Exception { .cluster() .prepareRestoreSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) .setWaitForCompletion(true) - .setIndices("with-fs") + .setIndices(dataStreamName) .setRenamePattern("-fs") .setRenameReplacement("-fs2") .get(); assertEquals(2, restoreSnapshotResponse.getRestoreInfo().successfulShards()); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "with-fs2" }) - ).get(); - assertEquals(1, ds.getDataStreams().size()); - assertEquals(1, ds.getDataStreams().get(0).getDataStream().getIndices().size()); - assertEquals(fs2BackingIndexName, ds.getDataStreams().get(0).getDataStream().getIndices().get(0).getName()); - assertEquals(fs2FailureIndexName, ds.getDataStreams().get(0).getDataStream().getFailureIndices().getIndices().get(0).getName()); + List dataStreamInfos = getDataStreamInfo("with-fs2"); + assertEquals(1, dataStreamInfos.size()); + assertEquals(1, dataStreamInfos.get(0).getDataStream().getIndices().size()); + assertEquals(fs2BackingIndexName, dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName()); + assertEquals(fs2FailureIndexName, dataStreamInfos.get(0).getDataStream().getFailureIndices().getIndices().get(0).getName()); } } @@ -477,13 +458,10 @@ public void testSnapshotAndRestoreAllIncludeSpecificDataStream() throws Exceptio assertEquals(DOCUMENT_SOURCE, hits[0].getSourceAsMap()); }); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { dataStreamToSnapshot }) - ).get(); - assertEquals(1, ds.getDataStreams().size()); - assertEquals(1, ds.getDataStreams().get(0).getDataStream().getIndices().size()); - assertEquals(backingIndexName, ds.getDataStreams().get(0).getDataStream().getIndices().get(0).getName()); + List dataStreamInfos = getDataStreamInfo(dataStreamToSnapshot); + assertEquals(1, dataStreamInfos.size()); + assertEquals(1, dataStreamInfos.get(0).getDataStream().getIndices().size()); + assertEquals(backingIndexName, dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName()); GetAliasesResponse getAliasesResponse = client.admin().indices().getAliases(new GetAliasesRequest("my-alias")).actionGet(); assertThat(getAliasesResponse.getDataStreamAliases().keySet(), contains(dataStreamToSnapshot)); @@ -536,13 +514,10 @@ public void testSnapshotAndRestoreReplaceAll() throws Exception { assertEquals(DOCUMENT_SOURCE, hits[0].getSourceAsMap()); }); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "*" }) - ).get(); - assertEquals(3, ds.getDataStreams().size()); + List dataStreamInfos = getDataStreamInfo("*"); + assertEquals(3, dataStreamInfos.size()); assertThat( - ds.getDataStreams().stream().map(i -> i.getDataStream().getName()).collect(Collectors.toList()), + dataStreamInfos.stream().map(i -> i.getDataStream().getName()).collect(Collectors.toList()), containsInAnyOrder("ds", "other-ds", "with-fs") ); @@ -596,19 +571,16 @@ public void testSnapshotAndRestoreAll() throws Exception { assertEquals(DOCUMENT_SOURCE, hits[0].getSourceAsMap()); }); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "*" }) - ).get(); - assertEquals(3, ds.getDataStreams().size()); - assertEquals(1, ds.getDataStreams().get(0).getDataStream().getIndices().size()); - assertEquals(dsBackingIndexName, ds.getDataStreams().get(0).getDataStream().getIndices().get(0).getName()); - assertEquals(1, ds.getDataStreams().get(1).getDataStream().getIndices().size()); - assertEquals(otherDsBackingIndexName, ds.getDataStreams().get(1).getDataStream().getIndices().get(0).getName()); - assertEquals(1, ds.getDataStreams().get(2).getDataStream().getIndices().size()); - assertEquals(fsBackingIndexName, ds.getDataStreams().get(2).getDataStream().getIndices().get(0).getName()); - assertEquals(1, ds.getDataStreams().get(2).getDataStream().getFailureIndices().getIndices().size()); - assertEquals(fsFailureIndexName, ds.getDataStreams().get(2).getDataStream().getFailureIndices().getIndices().get(0).getName()); + List dataStreamInfos = getDataStreamInfo("*"); + assertEquals(3, dataStreamInfos.size()); + assertEquals(1, dataStreamInfos.get(0).getDataStream().getIndices().size()); + assertEquals(dsBackingIndexName, dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName()); + assertEquals(1, dataStreamInfos.get(1).getDataStream().getIndices().size()); + assertEquals(otherDsBackingIndexName, dataStreamInfos.get(1).getDataStream().getIndices().get(0).getName()); + assertEquals(1, dataStreamInfos.get(2).getDataStream().getIndices().size()); + assertEquals(fsBackingIndexName, dataStreamInfos.get(2).getDataStream().getIndices().get(0).getName()); + assertEquals(1, dataStreamInfos.get(2).getDataStream().getFailureIndices().getIndices().size()); + assertEquals(fsFailureIndexName, dataStreamInfos.get(2).getDataStream().getFailureIndices().getIndices().get(0).getName()); GetAliasesResponse getAliasesResponse = client.admin().indices().getAliases(new GetAliasesRequest("my-alias")).actionGet(); assertThat(getAliasesResponse.getDataStreamAliases().keySet(), containsInAnyOrder("ds", "other-ds")); @@ -667,19 +639,16 @@ public void testSnapshotAndRestoreIncludeAliasesFalse() throws Exception { assertEquals(DOCUMENT_SOURCE, hits[0].getSourceAsMap()); }); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "*" }) - ).get(); - assertEquals(3, ds.getDataStreams().size()); - assertEquals(1, ds.getDataStreams().get(0).getDataStream().getIndices().size()); - assertEquals(dsBackingIndexName, ds.getDataStreams().get(0).getDataStream().getIndices().get(0).getName()); - assertEquals(1, ds.getDataStreams().get(1).getDataStream().getIndices().size()); - assertEquals(otherDsBackingIndexName, ds.getDataStreams().get(1).getDataStream().getIndices().get(0).getName()); - assertEquals(1, ds.getDataStreams().get(2).getDataStream().getIndices().size()); - assertEquals(fsBackingIndexName, ds.getDataStreams().get(2).getDataStream().getIndices().get(0).getName()); - assertEquals(1, ds.getDataStreams().get(2).getDataStream().getIndices().size()); - assertEquals(fsFailureIndexName, ds.getDataStreams().get(2).getDataStream().getFailureIndices().getIndices().get(0).getName()); + List dataStreamInfos = getDataStreamInfo("*"); + assertEquals(3, dataStreamInfos.size()); + assertEquals(1, dataStreamInfos.get(0).getDataStream().getIndices().size()); + assertEquals(dsBackingIndexName, dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName()); + assertEquals(1, dataStreamInfos.get(1).getDataStream().getIndices().size()); + assertEquals(otherDsBackingIndexName, dataStreamInfos.get(1).getDataStream().getIndices().get(0).getName()); + assertEquals(1, dataStreamInfos.get(2).getDataStream().getIndices().size()); + assertEquals(fsBackingIndexName, dataStreamInfos.get(2).getDataStream().getIndices().get(0).getName()); + assertEquals(1, dataStreamInfos.get(2).getDataStream().getIndices().size()); + assertEquals(fsFailureIndexName, dataStreamInfos.get(2).getDataStream().getFailureIndices().getIndices().get(0).getName()); GetAliasesResponse getAliasesResponse = client.admin().indices().getAliases(new GetAliasesRequest("*")).actionGet(); assertThat(getAliasesResponse.getDataStreamAliases(), anEmptyMap()); @@ -721,13 +690,10 @@ public void testRename() throws Exception { .setRenameReplacement("ds2") .get(); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "ds2" }) - ).get(); - assertEquals(1, ds.getDataStreams().size()); - assertEquals(1, ds.getDataStreams().get(0).getDataStream().getIndices().size()); - assertEquals(ds2BackingIndexName, ds.getDataStreams().get(0).getDataStream().getIndices().get(0).getName()); + List dataStreamInfos = getDataStreamInfo("ds2"); + assertEquals(1, dataStreamInfos.size()); + assertEquals(1, dataStreamInfos.get(0).getDataStream().getIndices().size()); + assertEquals(ds2BackingIndexName, dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName()); assertResponse( client.prepareSearch("ds2"), response -> assertEquals(DOCUMENT_SOURCE, response.getHits().getHits()[0].getSourceAsMap()) @@ -779,13 +745,10 @@ public void testRenameWriteDataStream() throws Exception { .setRenameReplacement("other-ds2") .get(); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "other-ds2" }) - ).get(); - assertEquals(1, ds.getDataStreams().size()); - assertEquals(1, ds.getDataStreams().get(0).getDataStream().getIndices().size()); - assertEquals(otherDs2BackingIndexName, ds.getDataStreams().get(0).getDataStream().getIndices().get(0).getName()); + List dataStreamInfos = getDataStreamInfo("other-ds2"); + assertEquals(1, dataStreamInfos.size()); + assertEquals(1, dataStreamInfos.get(0).getDataStream().getIndices().size()); + assertEquals(otherDs2BackingIndexName, dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName()); GetAliasesResponse getAliasesResponse = client.admin().indices().getAliases(new GetAliasesRequest("my-alias")).actionGet(); assertThat(getAliasesResponse.getDataStreamAliases().keySet(), containsInAnyOrder("ds", "other-ds", "other-ds2")); @@ -849,9 +812,8 @@ public void testBackingIndexIsNotRenamedWhenRestoringDataStream() { assertThat(restoreSnapshotResponse.status(), is(RestStatus.OK)); - GetDataStreamAction.Request getDSRequest = new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "ds" }); - GetDataStreamAction.Response response = client.execute(GetDataStreamAction.INSTANCE, getDSRequest).actionGet(); - assertThat(response.getDataStreams().get(0).getDataStream().getIndices().get(0).getName(), is(dsBackingIndexName)); + List dataStreamInfos = getDataStreamInfo("ds"); + assertThat(dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName(), is(dsBackingIndexName)); } public void testDataStreamAndBackingIndicesAreRenamedUsingRegex() { @@ -888,17 +850,15 @@ public void testDataStreamAndBackingIndicesAreRenamedUsingRegex() { assertThat(restoreSnapshotResponse.status(), is(RestStatus.OK)); // assert "ds" was restored as "test-ds" and the backing index has a valid name - GetDataStreamAction.Request getRenamedDS = new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "test-ds" }); - GetDataStreamAction.Response response = client.execute(GetDataStreamAction.INSTANCE, getRenamedDS).actionGet(); + List dataStreamInfos = getDataStreamInfo("test-ds"); assertThat( - response.getDataStreams().get(0).getDataStream().getIndices().get(0).getName(), + dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName(), is(DataStream.getDefaultBackingIndexName("test-ds", 1L)) ); // data stream "ds" should still exist in the system - GetDataStreamAction.Request getDSRequest = new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "ds" }); - response = client.execute(GetDataStreamAction.INSTANCE, getDSRequest).actionGet(); - assertThat(response.getDataStreams().get(0).getDataStream().getIndices().get(0).getName(), is(dsBackingIndexName)); + dataStreamInfos = getDataStreamInfo("ds"); + assertThat(dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName(), is(dsBackingIndexName)); } public void testWildcards() throws Exception { @@ -924,16 +884,13 @@ public void testWildcards() throws Exception { assertEquals(RestStatus.OK, restoreSnapshotResponse.status()); - GetDataStreamAction.Response ds = client.execute( - GetDataStreamAction.INSTANCE, - new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "ds2" }) - ).get(); - assertEquals(1, ds.getDataStreams().size()); - assertEquals(1, ds.getDataStreams().get(0).getDataStream().getIndices().size()); - assertEquals(ds2BackingIndexName, ds.getDataStreams().get(0).getDataStream().getIndices().get(0).getName()); + List dataStreamInfos = getDataStreamInfo("ds2"); + assertEquals(1, dataStreamInfos.size()); + assertEquals(1, dataStreamInfos.get(0).getDataStream().getIndices().size()); + assertEquals(ds2BackingIndexName, dataStreamInfos.get(0).getDataStream().getIndices().get(0).getName()); assertThat( "we renamed the restored data stream to one that doesn't match any existing composable template", - ds.getDataStreams().get(0).getIndexTemplate(), + dataStreamInfos.get(0).getIndexTemplate(), is(nullValue()) ); } @@ -955,7 +912,7 @@ public void testDataStreamNotStoredWhenIndexRequested() { ); } - public void testDataStreamNotRestoredWhenIndexRequested() throws Exception { + public void testDataStreamNotRestoredWhenIndexRequested() { CreateSnapshotResponse createSnapshotResponse = client.admin() .cluster() .prepareCreateSnapshot(TEST_REQUEST_TIMEOUT, REPO, "snap2") @@ -984,7 +941,7 @@ public void testDataStreamNotRestoredWhenIndexRequested() throws Exception { expectThrows(ResourceNotFoundException.class, client.execute(GetDataStreamAction.INSTANCE, getRequest)); } - public void testDataStreamNotIncludedInLimitedSnapshot() throws ExecutionException, InterruptedException { + public void testDataStreamNotIncludedInLimitedSnapshot() { final String snapshotName = "test-snap"; CreateSnapshotResponse createSnapshotResponse = client.admin() .cluster() @@ -1042,12 +999,7 @@ public void testDeleteDataStreamDuringSnapshot() throws Exception { assertDocCount(dataStream, 100L); // Resolve backing index name after the data stream has been created because it has a date component, // and running around midnight could lead to test failures otherwise - GetDataStreamAction.Request getDataStreamRequest = new GetDataStreamAction.Request( - TEST_REQUEST_TIMEOUT, - new String[] { dataStream } - ); - GetDataStreamAction.Response getDataStreamResponse = client.execute(GetDataStreamAction.INSTANCE, getDataStreamRequest).actionGet(); - String backingIndexName = getDataStreamResponse.getDataStreams().get(0).getDataStream().getIndices().get(0).getName(); + String backingIndexName = getDataStreamInfo(dataStream).get(0).getDataStream().getIndices().get(0).getName(); logger.info("--> snapshot"); ActionFuture future = client1.admin() @@ -1235,7 +1187,7 @@ public void testSnapshotDSDuringRolloverAndDeleteOldIndex() throws Exception { assertEquals(restoreSnapshotResponse.failedShards(), 0); } - public void testExcludeDSFromSnapshotWhenExcludingItsIndices() { + public void testExcludeDSFromSnapshotWhenExcludingAnyOfItsIndices() { final String snapshot = "test-snapshot"; final String indexWithoutDataStream = "test-idx-no-ds"; createIndexWithContent(indexWithoutDataStream); @@ -1251,10 +1203,47 @@ public void testExcludeDSFromSnapshotWhenExcludingItsIndices() { .getRestoreInfo(); assertThat(restoreInfo.failedShards(), is(0)); assertThat(restoreInfo.successfulShards(), is(1)); + + // Exclude only failure store indices + { + String dataStreamName = "with-fs"; + CreateSnapshotResponse createSnapshotResponse = client.admin() + .cluster() + .prepareCreateSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) + .setWaitForCompletion(true) + .setIndices(dataStreamName + "*", "-.fs*") + .setIncludeGlobalState(false) + .get(); + + RestStatus status = createSnapshotResponse.getSnapshotInfo().status(); + assertEquals(RestStatus.OK, status); + + SnapshotInfo retrievedSnapshot = getSnapshot(REPO, SNAPSHOT); + assertThat(retrievedSnapshot.dataStreams(), contains(dataStreamName)); + assertThat(retrievedSnapshot.indices(), containsInAnyOrder(fsBackingIndexName)); + + assertAcked( + safeGet(client.execute(DeleteDataStreamAction.INSTANCE, new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, "*"))) + ); + + RestoreInfo restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) + .setWaitForCompletion(true) + .setIndices(dataStreamName) + .get() + .getRestoreInfo(); + + assertThat(restoreSnapshotResponse, notNullValue()); + assertThat(restoreSnapshotResponse.successfulShards(), equalTo(restoreSnapshotResponse.totalShards())); + assertThat(restoreSnapshotResponse.failedShards(), is(0)); + + GetDataStreamAction.Response.DataStreamInfo dataStream = getDataStreamInfo(dataStreamName).getFirst(); + assertThat(dataStream.getDataStream().getBackingIndices().getIndices(), not(empty())); + assertThat(dataStream.getDataStream().getFailureIndices().getIndices(), empty()); + } } /** - * This test is a copy of the {@link #testExcludeDSFromSnapshotWhenExcludingItsIndices()} the only difference + * This test is a copy of the {@link #testExcludeDSFromSnapshotWhenExcludingAnyOfItsIndices()} ()} the only difference * is that one include the global state and one doesn't. In general this shouldn't matter that's why it used to be * a random parameter of the test, but because of #107515 it fails when we include the global state. Keep them * separate until this is fixed. @@ -1284,10 +1273,7 @@ public void testRestoreSnapshotFully() throws Exception { createIndexWithContent(indexName); createFullSnapshot(REPO, snapshotName); - assertAcked( - client.execute(DeleteDataStreamAction.INSTANCE, new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "*" })) - .get() - ); + assertAcked(client.execute(DeleteDataStreamAction.INSTANCE, new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, "*")).get()); assertAcked(client.admin().indices().prepareDelete("*").setIndicesOptions(IndicesOptions.lenientExpandOpenHidden()).get()); RestoreSnapshotResponse restoreSnapshotResponse = client.admin() @@ -1297,8 +1283,7 @@ public void testRestoreSnapshotFully() throws Exception { .get(); assertEquals(RestStatus.OK, restoreSnapshotResponse.status()); - GetDataStreamAction.Request getRequest = new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, new String[] { "*" }); - assertThat(client.execute(GetDataStreamAction.INSTANCE, getRequest).get().getDataStreams(), hasSize(3)); + assertThat(getDataStreamInfo("*"), hasSize(3)); assertNotNull(client.admin().indices().prepareGetIndex().setIndices(indexName).get()); } @@ -1326,7 +1311,7 @@ public void testRestoreDataStreamAliasWithConflictingDataStream() throws Excepti } } - public void testRestoreDataStreamAliasWithConflictingIndicesAlias() throws Exception { + public void testRestoreDataStreamAliasWithConflictingIndicesAlias() { var snapshotName = "test-snapshot"; createFullSnapshot(REPO, snapshotName); client.execute(DeleteDataStreamAction.INSTANCE, new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, "*")).actionGet(); @@ -1484,4 +1469,8 @@ public void testWarningHeaderOnRestoreTemplateFromSnapshot() throws Exception { } + protected List getDataStreamInfo(String... dataStreamNames) { + GetDataStreamAction.Request getRequest = new GetDataStreamAction.Request(TEST_REQUEST_TIMEOUT, dataStreamNames); + return safeGet(client.execute(GetDataStreamAction.INSTANCE, getRequest)).getDataStreams(); + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java index 7745ec9cc75b2..db602ef6ef291 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java @@ -49,7 +49,6 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -794,27 +793,57 @@ public DataStream promoteDataStream() { /** * Reconciles this data stream with a list of indices available in a snapshot. Allows snapshots to store accurate data - * stream definitions that do not reference backing indices not contained in the snapshot. + * stream definitions that do not reference backing indices and failure indices not contained in the snapshot. * * @param indicesInSnapshot List of indices in the snapshot + * @param snapshotMetadataBuilder a metadata builder with the current view of the snapshot metadata * @return Reconciled {@link DataStream} instance or {@code null} if no reconciled version of this data stream could be built from the * given indices */ @Nullable - public DataStream snapshot(Collection indicesInSnapshot) { + public DataStream snapshot(Set indicesInSnapshot, Metadata.Builder snapshotMetadataBuilder) { + boolean backingIndicesChanged = false; + boolean failureIndicesChanged = false; + // do not include indices not available in the snapshot - List reconciledIndices = new ArrayList<>(this.backingIndices.indices); - if (reconciledIndices.removeIf(x -> indicesInSnapshot.contains(x.getName()) == false) == false) { + List reconciledBackingIndices = this.backingIndices.indices; + if (isAnyIndexMissing(this.backingIndices.getIndices(), snapshotMetadataBuilder, indicesInSnapshot)) { + reconciledBackingIndices = new ArrayList<>(this.backingIndices.indices); + backingIndicesChanged = reconciledBackingIndices.removeIf(x -> indicesInSnapshot.contains(x.getName()) == false); + if (reconciledBackingIndices.isEmpty()) { + return null; + } + } + + List reconciledFailureIndices = this.failureIndices.indices; + if (DataStream.isFailureStoreFeatureFlagEnabled() + && isAnyIndexMissing(failureIndices.indices, snapshotMetadataBuilder, indicesInSnapshot)) { + reconciledFailureIndices = new ArrayList<>(this.failureIndices.indices); + failureIndicesChanged = reconciledFailureIndices.removeIf(x -> indicesInSnapshot.contains(x.getName()) == false); + } + + if (backingIndicesChanged == false && failureIndicesChanged == false) { return this; } - if (reconciledIndices.size() == 0) { - return null; + Builder builder = copy(); + if (backingIndicesChanged) { + builder.setBackingIndices(backingIndices.copy().setIndices(reconciledBackingIndices).build()); + } + if (failureIndicesChanged) { + builder.setFailureIndices(failureIndices.copy().setIndices(reconciledFailureIndices).build()); } + return builder.setMetadata(metadata == null ? null : new HashMap<>(metadata)).build(); + } - return copy().setBackingIndices(backingIndices.copy().setIndices(reconciledIndices).build()) - .setMetadata(metadata == null ? null : new HashMap<>(metadata)) - .build(); + private static boolean isAnyIndexMissing(List indices, Metadata.Builder builder, Set indicesInSnapshot) { + for (Index index : indices) { + final String indexName = index.getName(); + if (builder.get(indexName) == null || indicesInSnapshot.contains(indexName) == false) { + return true; + } + } + return false; } /** diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index 8d526f3e114e1..6f690a9e6ccd5 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -786,15 +786,7 @@ private static Metadata metadataForSnapshot(SnapshotsInProgress.Entry snapshot, assert snapshot.partial() : "Data stream [" + dataStreamName + "] was deleted during a snapshot but snapshot was not partial."; } else { - boolean missingIndex = false; - for (Index index : dataStream.getIndices()) { - final String indexName = index.getName(); - if (builder.get(indexName) == null || indicesInSnapshot.contains(indexName) == false) { - missingIndex = true; - break; - } - } - final DataStream reconciled = missingIndex ? dataStream.snapshot(indicesInSnapshot) : dataStream; + final DataStream reconciled = dataStream.snapshot(indicesInSnapshot, builder); if (reconciled != null) { dataStreams.put(dataStreamName, reconciled); } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java index 7108a4fd4f19e..cfdcfe48c8d9a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java @@ -45,8 +45,10 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; +import java.util.stream.Collectors; import static org.elasticsearch.cluster.metadata.DataStream.getDefaultBackingIndexName; import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance; @@ -866,23 +868,39 @@ public void testReplaceFailureIndexThrowsExceptionIfReplacingWriteIndex() { } public void testSnapshot() { - var preSnapshotDataStream = DataStreamTestHelper.randomInstance(); - var indicesToRemove = randomSubsetOf(preSnapshotDataStream.getIndices()); - if (indicesToRemove.size() == preSnapshotDataStream.getIndices().size()) { + var preSnapshotDataStream = DataStreamTestHelper.randomInstance(true); + + // Mutate backing indices + var backingIndicesToRemove = randomSubsetOf(preSnapshotDataStream.getIndices()); + if (backingIndicesToRemove.size() == preSnapshotDataStream.getIndices().size()) { // never remove them all - indicesToRemove.remove(0); + backingIndicesToRemove.remove(0); } - var indicesToAdd = randomIndexInstances(); - var postSnapshotIndices = new ArrayList<>(preSnapshotDataStream.getIndices()); - postSnapshotIndices.removeAll(indicesToRemove); - postSnapshotIndices.addAll(indicesToAdd); + var backingIndicesToAdd = randomIndexInstances(); + var postSnapshotBackingIndices = new ArrayList<>(preSnapshotDataStream.getIndices()); + postSnapshotBackingIndices.removeAll(backingIndicesToRemove); + postSnapshotBackingIndices.addAll(backingIndicesToAdd); + + // Mutate failure indices + var failureIndicesToRemove = randomSubsetOf(preSnapshotDataStream.getFailureIndices().getIndices()); + var failureIndicesToAdd = randomIndexInstances(); + var postSnapshotFailureIndices = new ArrayList<>(preSnapshotDataStream.getFailureIndices().getIndices()); + postSnapshotFailureIndices.removeAll(failureIndicesToRemove); + postSnapshotFailureIndices.addAll(failureIndicesToAdd); var replicated = preSnapshotDataStream.isReplicated() && randomBoolean(); var postSnapshotDataStream = preSnapshotDataStream.copy() .setBackingIndices( preSnapshotDataStream.getBackingIndices() .copy() - .setIndices(postSnapshotIndices) + .setIndices(postSnapshotBackingIndices) + .setRolloverOnWrite(replicated == false && preSnapshotDataStream.rolloverOnWrite()) + .build() + ) + .setFailureIndices( + preSnapshotDataStream.getFailureIndices() + .copy() + .setIndices(postSnapshotFailureIndices) .setRolloverOnWrite(replicated == false && preSnapshotDataStream.rolloverOnWrite()) .build() ) @@ -891,9 +909,10 @@ public void testSnapshot() { .setReplicated(replicated) .build(); - var reconciledDataStream = postSnapshotDataStream.snapshot( - preSnapshotDataStream.getIndices().stream().map(Index::getName).toList() - ); + Set indicesInSnapshot = new HashSet<>(); + preSnapshotDataStream.getIndices().forEach(index -> indicesInSnapshot.add(index.getName())); + preSnapshotDataStream.getFailureIndices().getIndices().forEach(index -> indicesInSnapshot.add(index.getName())); + var reconciledDataStream = postSnapshotDataStream.snapshot(indicesInSnapshot, Metadata.builder()); assertThat(reconciledDataStream.getName(), equalTo(postSnapshotDataStream.getName())); assertThat(reconciledDataStream.getGeneration(), equalTo(postSnapshotDataStream.getGeneration())); @@ -907,9 +926,19 @@ public void testSnapshot() { } assertThat(reconciledDataStream.isHidden(), equalTo(postSnapshotDataStream.isHidden())); assertThat(reconciledDataStream.isReplicated(), equalTo(postSnapshotDataStream.isReplicated())); - assertThat(reconciledDataStream.getIndices(), everyItem(not(in(indicesToRemove)))); - assertThat(reconciledDataStream.getIndices(), everyItem(not(in(indicesToAdd)))); - assertThat(reconciledDataStream.getIndices().size(), equalTo(preSnapshotDataStream.getIndices().size() - indicesToRemove.size())); + assertThat(reconciledDataStream.getIndices(), everyItem(not(in(backingIndicesToRemove)))); + assertThat(reconciledDataStream.getIndices(), everyItem(not(in(backingIndicesToAdd)))); + assertThat( + reconciledDataStream.getIndices().size(), + equalTo(preSnapshotDataStream.getIndices().size() - backingIndicesToRemove.size()) + ); + var reconciledFailureIndices = reconciledDataStream.getFailureIndices().getIndices(); + assertThat(reconciledFailureIndices, everyItem(not(in(failureIndicesToRemove)))); + assertThat(reconciledFailureIndices, everyItem(not(in(failureIndicesToAdd)))); + assertThat( + reconciledFailureIndices.size(), + equalTo(preSnapshotDataStream.getFailureIndices().getIndices().size() - failureIndicesToRemove.size()) + ); } public void testSnapshotWithAllBackingIndicesRemoved() { @@ -920,7 +949,12 @@ public void testSnapshotWithAllBackingIndicesRemoved() { .setBackingIndices(preSnapshotDataStream.getBackingIndices().copy().setIndices(indicesToAdd).build()) .build(); - assertNull(postSnapshotDataStream.snapshot(preSnapshotDataStream.getIndices().stream().map(Index::getName).toList())); + assertNull( + postSnapshotDataStream.snapshot( + preSnapshotDataStream.getIndices().stream().map(Index::getName).collect(Collectors.toSet()), + Metadata.builder() + ) + ); } public void testSelectTimeSeriesWriteIndex() { From 78bd9ec6f082e67199bd1df742c72325b2169e2c Mon Sep 17 00:00:00 2001 From: kosabogi <105062005+kosabogi@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:43:34 +0100 Subject: [PATCH 118/119] [DOCS] Updates SharePoint Online page (#118318) --- .../connectors-sharepoint-online.asciidoc | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/docs/reference/connector/docs/connectors-sharepoint-online.asciidoc b/docs/reference/connector/docs/connectors-sharepoint-online.asciidoc index 02f598c16f63c..2680e3ff840a6 100644 --- a/docs/reference/connector/docs/connectors-sharepoint-online.asciidoc +++ b/docs/reference/connector/docs/connectors-sharepoint-online.asciidoc @@ -133,6 +133,58 @@ The application name will appear in the Title box. ---- +[discrete#es-connectors-sharepoint-online-sites-selected-permissions] +====== Granting `Sites.Selected` permissions + +To configure `Sites.Selected` permissions, follow these steps in the Azure Active Directory portal. These permissions enable precise access control to specific SharePoint sites. + +. Sign in to the https://portal.azure.com/[Azure Active Directory portal^]. +. Navigate to **App registrations** and locate the application created for the connector. +. Under **API permissions**, click **Add permission**. +. Select **Microsoft Graph** > **Application permissions**, then add `Sites.Selected`. +. Click **Grant admin consent** to approve the permission. + +[TIP] +==== +Refer to the official https://learn.microsoft.com/en-us/graph/permissions-reference[Microsoft documentation] for managing permissions in Azure AD. +==== + +To assign access to specific SharePoint sites using `Sites.Selected`: + +. Use Microsoft Graph Explorer or PowerShell to grant access. +. To fetch the site ID, run the following Graph API query: ++ +[source, http] +---- +GET https://graph.microsoft.com/v1.0/sites?select=webUrl,Title,Id&$search="*" +---- ++ +This will return the `id` of the site. + +. Use the `id` to assign read or write access: ++ +[source, http] +---- +POST https://graph.microsoft.com/v1.0/sites//permissions +{ + "roles": ["read"], // or "write" + "grantedToIdentities": [ + { + "application": { + "id": "", + "displayName": "" + } + } + ] +} +---- + +[NOTE] +==== +When using the `Comma-separated list of sites` configuration field, ensure the sites specified match those granted `Sites.Selected` permission in SharePoint. +If the `Comma-separated list of sites` field is set to `*` or the `Enumerate all sites` toggle is enabled, the connector will attempt to access all sites. This requires broader permissions, which are not supported with `Sites.Selected`. +==== + .Graph API permissions **** Microsoft recommends using Graph API for all operations with Sharepoint Online. Graph API is well-documented and more efficient at fetching data, which helps avoid throttling. @@ -594,6 +646,59 @@ The application name will appear in the Title box. ---- +[discrete#es-connectors-sharepoint-online-sites-selected-permissions-self-managed] +====== Granting `Sites.Selected` permissions + +To configure `Sites.Selected` permissions, follow these steps in the Azure Active Directory portal. These permissions enable precise access control to specific SharePoint sites. + +. Sign in to the https://portal.azure.com/[Azure Active Directory portal^]. +. Navigate to **App registrations** and locate the application created for the connector. +. Under **API permissions**, click **Add permission**. +. Select **Microsoft Graph** > **Application permissions**, then add `Sites.Selected`. +. Click **Grant admin consent** to approve the permission. + +[TIP] +==== +Refer to the official https://learn.microsoft.com/en-us/graph/permissions-reference[Microsoft documentation] for managing permissions in Azure AD. +==== + + +To assign access to specific SharePoint sites using `Sites.Selected`: + +. Use Microsoft Graph Explorer or PowerShell to grant access. +. To fetch the site ID, run the following Graph API query: ++ +[source, http] +---- +GET https://graph.microsoft.com/v1.0/sites?select=webUrl,Title,Id&$search="*" +---- ++ +This will return the `id` of the site. + +. Use the `id` to assign read or write access: ++ +[source, http] +---- +POST https://graph.microsoft.com/v1.0/sites//permissions +{ + "roles": ["read"], // or "write" + "grantedToIdentities": [ + { + "application": { + "id": "", + "displayName": "" + } + } + ] +} +---- + +[NOTE] +==== +When using the `Comma-separated list of sites` configuration field, ensure the sites specified match those granted `Sites.Selected` permission in SharePoint. +If the `Comma-separated list of sites` field is set to `*` or the `Enumerate all sites` toggle is enabled, the connector will attempt to access all sites. This requires broader permissions, which are not supported with `Sites.Selected`. +==== + .Graph API permissions **** Microsoft recommends using Graph API for all operations with Sharepoint Online. Graph API is well-documented and more efficient at fetching data, which helps avoid throttling. From 54879278b1c15ed5fd7fac2f22b6cc852326a143 Mon Sep 17 00:00:00 2001 From: Luke Whiting Date: Thu, 19 Dec 2024 11:40:09 +0000 Subject: [PATCH 119/119] Update data stream deprecations warnings to new format and filter searchable snapshots from response (#118562) * Update data stream deprecations warnings to new format * Add reindex_required flag to index version deprecation notice response * PR Changes * Move all deprecation checks to use a shared predicate which also excludes snapshots * Update docs/changelog/118562.yaml * Tests for excluding snapshots * PR Changes - Remove leftover comment --- docs/changelog/118562.yaml | 6 ++ .../deprecation/DeprecatedIndexPredicate.java | 47 ++++++++++++ .../DataStreamDeprecationChecks.java | 58 +++++--------- .../deprecation/IndexDeprecationChecks.java | 9 ++- .../DataStreamDeprecationChecksTests.java | 76 +++++++++---------- .../IndexDeprecationChecksTests.java | 24 +++++- .../action/ReindexDataStreamAction.java | 18 ----- ...ReindexDataStreamIndexTransportAction.java | 5 +- .../ReindexDataStreamTransportAction.java | 4 +- ...indexDataStreamPersistentTaskExecutor.java | 6 +- 10 files changed, 138 insertions(+), 115 deletions(-) create mode 100644 docs/changelog/118562.yaml create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecatedIndexPredicate.java diff --git a/docs/changelog/118562.yaml b/docs/changelog/118562.yaml new file mode 100644 index 0000000000000..a6b00b326151f --- /dev/null +++ b/docs/changelog/118562.yaml @@ -0,0 +1,6 @@ +pr: 118562 +summary: Update data stream deprecations warnings to new format and filter searchable + snapshots from response +area: Data streams +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecatedIndexPredicate.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecatedIndexPredicate.java new file mode 100644 index 0000000000000..024d24fdf5151 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecatedIndexPredicate.java @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.deprecation; + +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.IndexVersions; + +import java.util.function.Predicate; + +public class DeprecatedIndexPredicate { + + public static final IndexVersion MINIMUM_WRITEABLE_VERSION_AFTER_UPGRADE = IndexVersions.UPGRADE_TO_LUCENE_10_0_0; + + /* + * This predicate allows through only indices that were created with a previous lucene version, meaning that they need to be reindexed + * in order to be writable in the _next_ lucene version. + * + * It ignores searchable snapshots as they are not writable. + */ + public static Predicate getReindexRequiredPredicate(Metadata metadata) { + return index -> { + IndexMetadata indexMetadata = metadata.index(index); + return reindexRequired(indexMetadata); + }; + } + + public static boolean reindexRequired(IndexMetadata indexMetadata) { + return creationVersionBeforeMinimumWritableVersion(indexMetadata) && isNotSearchableSnapshot(indexMetadata); + } + + private static boolean isNotSearchableSnapshot(IndexMetadata indexMetadata) { + return indexMetadata.isSearchableSnapshot() == false; + } + + private static boolean creationVersionBeforeMinimumWritableVersion(IndexMetadata metadata) { + return metadata.getCreationVersion().before(MINIMUM_WRITEABLE_VERSION_AFTER_UPGRADE); + } + +} diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DataStreamDeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DataStreamDeprecationChecks.java index ee029d01427aa..65f2659fda04a 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DataStreamDeprecationChecks.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DataStreamDeprecationChecks.java @@ -10,10 +10,12 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.index.Index; -import org.elasticsearch.index.IndexVersions; +import org.elasticsearch.xpack.core.deprecation.DeprecatedIndexPredicate; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static java.util.Map.entry; import static java.util.Map.ofEntries; @@ -21,54 +23,28 @@ public class DataStreamDeprecationChecks { static DeprecationIssue oldIndicesCheck(DataStream dataStream, ClusterState clusterState) { List backingIndices = dataStream.getIndices(); - boolean hasOldIndices = backingIndices.stream() - .anyMatch(index -> clusterState.metadata().index(index).getCompatibilityVersion().before(IndexVersions.V_8_0_0)); - if (hasOldIndices) { - long totalIndices = backingIndices.size(); - List oldIndices = backingIndices.stream() - .filter(index -> clusterState.metadata().index(index).getCompatibilityVersion().before(IndexVersions.V_8_0_0)) - .toList(); - long totalOldIndices = oldIndices.size(); - long totalOldSearchableSnapshots = oldIndices.stream() - .filter(index -> clusterState.metadata().index(index).isSearchableSnapshot()) - .count(); - long totalOldPartiallyMountedSearchableSnapshots = oldIndices.stream() - .filter(index -> clusterState.metadata().index(index).isPartialSearchableSnapshot()) - .count(); - long totalOldFullyMountedSearchableSnapshots = totalOldSearchableSnapshots - totalOldPartiallyMountedSearchableSnapshots; + + Set indicesNeedingUpgrade = backingIndices.stream() + .filter(DeprecatedIndexPredicate.getReindexRequiredPredicate(clusterState.metadata())) + .map(Index::getName) + .collect(Collectors.toUnmodifiableSet()); + + if (indicesNeedingUpgrade.isEmpty() == false) { return new DeprecationIssue( DeprecationIssue.Level.CRITICAL, - "Old data stream with a compatibility version < 8.0", + "Old data stream with a compatibility version < 9.0", "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", - "This data stream has backing indices that were created before Elasticsearch 8.0.0", + "This data stream has backing indices that were created before Elasticsearch 9.0.0", false, ofEntries( - entry( - "backing_indices", - ofEntries( - entry("count", totalIndices), - entry( - "need_upgrading", - ofEntries( - entry("count", totalOldIndices), - entry( - "searchable_snapshots", - ofEntries( - entry("count", totalOldSearchableSnapshots), - entry("fully_mounted", ofEntries(entry("count", totalOldFullyMountedSearchableSnapshots))), - entry( - "partially_mounted", - ofEntries(entry("count", totalOldPartiallyMountedSearchableSnapshots)) - ) - ) - ) - ) - ) - ) - ) + entry("reindex_required", true), + entry("total_backing_indices", backingIndices.size()), + entry("indices_requiring_upgrade_count", indicesNeedingUpgrade.size()), + entry("indices_requiring_upgrade", indicesNeedingUpgrade) ) ); } + return null; } } diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java index aaf58a44a6565..de06e270a867e 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java @@ -14,12 +14,13 @@ import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexVersion; -import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.engine.frozen.FrozenEngine; import org.elasticsearch.index.mapper.SourceFieldMapper; +import org.elasticsearch.xpack.core.deprecation.DeprecatedIndexPredicate; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -36,14 +37,14 @@ static DeprecationIssue oldIndicesCheck(IndexMetadata indexMetadata, ClusterStat // TODO: this check needs to be revised. It's trivially true right now. IndexVersion currentCompatibilityVersion = indexMetadata.getCompatibilityVersion(); // We intentionally exclude indices that are in data streams because they will be picked up by DataStreamDeprecationChecks - if (currentCompatibilityVersion.before(IndexVersions.V_8_0_0) && isNotDataStreamIndex(indexMetadata, clusterState)) { + if (DeprecatedIndexPredicate.reindexRequired(indexMetadata) && isNotDataStreamIndex(indexMetadata, clusterState)) { return new DeprecationIssue( DeprecationIssue.Level.CRITICAL, - "Old index with a compatibility version < 8.0", + "Old index with a compatibility version < 9.0", "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", "This index has version: " + currentCompatibilityVersion.toReleaseVersion(), false, - null + Collections.singletonMap("reindex_required", true) ); } return null; diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/DataStreamDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/DataStreamDeprecationChecksTests.java index d5325fb0ff3a4..b297cc1a5bdf8 100644 --- a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/DataStreamDeprecationChecksTests.java +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/DataStreamDeprecationChecksTests.java @@ -17,41 +17,46 @@ import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.snapshots.SearchableSnapshotsSettings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static java.util.Collections.singletonList; +import static java.util.Map.entry; +import static java.util.Map.ofEntries; +import static org.elasticsearch.index.IndexModule.INDEX_STORE_TYPE_SETTING; import static org.elasticsearch.xpack.deprecation.DeprecationChecks.DATA_STREAM_CHECKS; import static org.hamcrest.Matchers.equalTo; public class DataStreamDeprecationChecksTests extends ESTestCase { public void testOldIndicesCheck() { - long oldIndexCount = randomIntBetween(1, 100); - long newIndexCount = randomIntBetween(1, 100); - long oldSearchableSnapshotCount = 0; - long oldFullyManagedSearchableSnapshotCount = 0; - long oldPartiallyManagedSearchableSnapshotCount = 0; + int oldIndexCount = randomIntBetween(1, 100); + int newIndexCount = randomIntBetween(1, 100); + List allIndices = new ArrayList<>(); Map nameToIndexMetadata = new HashMap<>(); + Set expectedIndices = new HashSet<>(); + for (int i = 0; i < oldIndexCount; i++) { - Settings.Builder settingsBuilder = settings(IndexVersion.fromId(7170099)); - if (randomBoolean()) { - settingsBuilder.put("index.store.type", "snapshot"); - if (randomBoolean()) { - oldFullyManagedSearchableSnapshotCount++; - } else { - settingsBuilder.put("index.store.snapshot.partial", true); - oldPartiallyManagedSearchableSnapshotCount++; - } - oldSearchableSnapshotCount++; + Settings.Builder settings = settings(IndexVersion.fromId(7170099)); + + String indexName = "old-data-stream-index-" + i; + if (expectedIndices.isEmpty() == false && randomIntBetween(0, 2) == 0) { + settings.put(INDEX_STORE_TYPE_SETTING.getKey(), SearchableSnapshotsSettings.SEARCHABLE_SNAPSHOT_STORE_TYPE); + } else { + expectedIndices.add(indexName); } - IndexMetadata oldIndexMetadata = IndexMetadata.builder("old-data-stream-index-" + i) + + Settings.Builder settingsBuilder = settings; + IndexMetadata oldIndexMetadata = IndexMetadata.builder(indexName) .settings(settingsBuilder) .numberOfShards(1) .numberOfReplicas(0) @@ -59,11 +64,9 @@ public void testOldIndicesCheck() { allIndices.add(oldIndexMetadata.getIndex()); nameToIndexMetadata.put(oldIndexMetadata.getIndex().getName(), oldIndexMetadata); } + for (int i = 0; i < newIndexCount; i++) { Settings.Builder settingsBuilder = settings(IndexVersion.current()); - if (randomBoolean()) { - settingsBuilder.put("index.store.type", "snapshot"); - } IndexMetadata newIndexMetadata = IndexMetadata.builder("new-data-stream-index-" + i) .settings(settingsBuilder) .numberOfShards(1) @@ -72,6 +75,7 @@ public void testOldIndicesCheck() { allIndices.add(newIndexMetadata.getIndex()); nameToIndexMetadata.put(newIndexMetadata.getIndex().getName(), newIndexMetadata); } + DataStream dataStream = new DataStream( randomAlphaOfLength(10), allIndices, @@ -88,37 +92,27 @@ public void testOldIndicesCheck() { randomBoolean(), null ); + Metadata metadata = Metadata.builder().indices(nameToIndexMetadata).build(); ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT).metadata(metadata).build(); + DeprecationIssue expected = new DeprecationIssue( DeprecationIssue.Level.CRITICAL, - "Old data stream with a compatibility version < 8.0", + "Old data stream with a compatibility version < 9.0", "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", - "This data stream has backing indices that were created before Elasticsearch 8.0.0", + "This data stream has backing indices that were created before Elasticsearch 9.0.0", false, - Map.of( - "backing_indices", - Map.of( - "count", - oldIndexCount + newIndexCount, - "need_upgrading", - Map.of( - "count", - oldIndexCount, - "searchable_snapshots", - Map.of( - "count", - oldSearchableSnapshotCount, - "fully_mounted", - Map.of("count", oldFullyManagedSearchableSnapshotCount), - "partially_mounted", - Map.of("count", oldPartiallyManagedSearchableSnapshotCount) - ) - ) - ) + ofEntries( + entry("reindex_required", true), + entry("total_backing_indices", oldIndexCount + newIndexCount), + entry("indices_requiring_upgrade_count", expectedIndices.size()), + entry("indices_requiring_upgrade", expectedIndices) ) ); + List issues = DeprecationChecks.filterChecks(DATA_STREAM_CHECKS, c -> c.apply(dataStream, clusterState)); + assertThat(issues, equalTo(singletonList(expected))); } + } diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java index 48cbef6831a2b..c6f3208a1cfb0 100644 --- a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java @@ -19,8 +19,8 @@ import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexVersion; -import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.engine.frozen.FrozenEngine; +import org.elasticsearch.snapshots.SearchableSnapshotsSettings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; @@ -29,6 +29,8 @@ import java.util.Map; import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.elasticsearch.index.IndexModule.INDEX_STORE_TYPE_SETTING; import static org.elasticsearch.xpack.deprecation.DeprecationChecks.INDEX_SETTINGS_CHECKS; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; @@ -48,11 +50,11 @@ public void testOldIndicesCheck() { .build(); DeprecationIssue expected = new DeprecationIssue( DeprecationIssue.Level.CRITICAL, - "Old index with a compatibility version < 8.0", + "Old index with a compatibility version < 9.0", "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", "This index has version: " + createdWith.toReleaseVersion(), false, - null + singletonMap("reindex_required", true) ); List issues = DeprecationChecks.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetadata, clusterState)); assertEquals(singletonList(expected), issues); @@ -100,6 +102,20 @@ public void testOldIndicesCheckDataStreamIndex() { assertThat(issues.size(), equalTo(0)); } + public void testOldIndicesCheckSnapshotIgnored() { + IndexVersion createdWith = IndexVersion.fromId(7170099); + Settings.Builder settings = settings(createdWith); + settings.put(INDEX_STORE_TYPE_SETTING.getKey(), SearchableSnapshotsSettings.SEARCHABLE_SNAPSHOT_STORE_TYPE); + IndexMetadata indexMetadata = IndexMetadata.builder("test").settings(settings).numberOfShards(1).numberOfReplicas(0).build(); + ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE) + .metadata(Metadata.builder().put(indexMetadata, true)) + .build(); + + List issues = DeprecationChecks.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetadata, clusterState)); + + assertThat(issues, empty()); + } + public void testTranslogRetentionSettings() { Settings.Builder settings = settings(IndexVersion.current()); settings.put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), randomPositiveTimeValue()); @@ -229,7 +245,7 @@ public void testCamelCaseDeprecation() throws IOException { + "} }"; IndexMetadata simpleIndex = IndexMetadata.builder(randomAlphaOfLengthBetween(5, 10)) - .settings(settings(IndexVersions.MINIMUM_COMPATIBLE)) + .settings(settings(IndexVersion.current())) .numberOfShards(1) .numberOfReplicas(1) .putMapping(simpleMapping) diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamAction.java index b10bea9e54230..9e4cbb1082215 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamAction.java @@ -13,14 +13,10 @@ import org.elasticsearch.action.ActionType; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.support.IndicesOptions; -import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.FeatureFlag; import org.elasticsearch.features.NodeFeature; -import org.elasticsearch.index.Index; -import org.elasticsearch.index.IndexVersion; -import org.elasticsearch.index.IndexVersions; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; @@ -43,24 +39,10 @@ public class ReindexDataStreamAction extends ActionType getOldIndexVersionPredicate(Metadata metadata) { - return index -> metadata.index(index).getCreationVersion().onOrBefore(MINIMUM_WRITEABLE_VERSION_AFTER_UPGRADE); - } - public enum Mode { UPGRADE } diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java index 66b13a9ce22b0..38b5da6527039 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamIndexTransportAction.java @@ -32,6 +32,7 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.deprecation.DeprecatedIndexPredicate; import java.util.Locale; import java.util.Map; @@ -78,13 +79,13 @@ protected void doExecute( IndexMetadata sourceIndex = clusterService.state().getMetadata().index(sourceIndexName); Settings settingsBefore = sourceIndex.getSettings(); - var hasOldVersion = ReindexDataStreamAction.getOldIndexVersionPredicate(clusterService.state().metadata()); + var hasOldVersion = DeprecatedIndexPredicate.getReindexRequiredPredicate(clusterService.state().metadata()); if (hasOldVersion.test(sourceIndex.getIndex()) == false) { logger.warn( "Migrating index [{}] with version [{}] is unnecessary as its version is not before [{}]", sourceIndexName, sourceIndex.getCreationVersion(), - ReindexDataStreamAction.MINIMUM_WRITEABLE_VERSION_AFTER_UPGRADE + DeprecatedIndexPredicate.MINIMUM_WRITEABLE_VERSION_AFTER_UPGRADE ); } diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportAction.java index f011c429ce79c..cc648c1984544 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportAction.java @@ -26,8 +26,8 @@ import org.elasticsearch.xpack.migrate.task.ReindexDataStreamTask; import org.elasticsearch.xpack.migrate.task.ReindexDataStreamTaskParams; +import static org.elasticsearch.xpack.core.deprecation.DeprecatedIndexPredicate.getReindexRequiredPredicate; import static org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.TASK_ID_PREFIX; -import static org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.getOldIndexVersionPredicate; /* * This transport action creates a new persistent task for reindexing the source data stream given in the request. On successful creation @@ -68,7 +68,7 @@ protected void doExecute(Task task, ReindexDataStreamRequest request, ActionList return; } int totalIndices = dataStream.getIndices().size(); - int totalIndicesToBeUpgraded = (int) dataStream.getIndices().stream().filter(getOldIndexVersionPredicate(metadata)).count(); + int totalIndicesToBeUpgraded = (int) dataStream.getIndices().stream().filter(getReindexRequiredPredicate(metadata)).count(); ReindexDataStreamTaskParams params = new ReindexDataStreamTaskParams( sourceDataStreamName, transportService.getThreadPool().absoluteTimeInMillis(), diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java index dc8e33bc091e6..30f64fdd1d6f6 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java @@ -35,7 +35,7 @@ import java.util.Map; import java.util.NoSuchElementException; -import static org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.getOldIndexVersionPredicate; +import static org.elasticsearch.xpack.core.deprecation.DeprecatedIndexPredicate.getReindexRequiredPredicate; public class ReindexDataStreamPersistentTaskExecutor extends PersistentTasksExecutor { private static final TimeValue TASK_KEEP_ALIVE_TIME = TimeValue.timeValueDays(1); @@ -84,7 +84,7 @@ protected void nodeOperation(AllocatedPersistentTask task, ReindexDataStreamTask List dataStreamInfos = response.getDataStreams(); if (dataStreamInfos.size() == 1) { DataStream dataStream = dataStreamInfos.getFirst().getDataStream(); - if (getOldIndexVersionPredicate(clusterService.state().metadata()).test(dataStream.getWriteIndex())) { + if (getReindexRequiredPredicate(clusterService.state().metadata()).test(dataStream.getWriteIndex())) { reindexClient.execute( RolloverAction.INSTANCE, new RolloverRequest(sourceDataStream, null), @@ -109,7 +109,7 @@ private void reindexIndices( String sourceDataStream ) { List indices = dataStream.getIndices(); - List indicesToBeReindexed = indices.stream().filter(getOldIndexVersionPredicate(clusterService.state().metadata())).toList(); + List indicesToBeReindexed = indices.stream().filter(getReindexRequiredPredicate(clusterService.state().metadata())).toList(); reindexDataStreamTask.setPendingIndicesCount(indicesToBeReindexed.size()); // The CountDownActionListener is 1 more than the number of indices so that the count is not 0 if we have no indices CountDownActionListener listener = new CountDownActionListener(indicesToBeReindexed.size() + 1, ActionListener.wrap(response1 -> {

    -TwGoV8w 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 %C|c?Yz9+ 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%0Dk$33i672kD0l-!J8OF;K!Y*GrGyD3q2lS;Kiu1 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