From c2b104bdb339515cbc20cddac038ba0c7470481c Mon Sep 17 00:00:00 2001 From: William Brafford Date: Tue, 12 Mar 2024 08:41:34 -0400 Subject: [PATCH 01/34] Clean up TODOs in BuildExtension and BuildVersion (#106155) * With downstream implementation in place, clean up default and superclass methods --- .../org/elasticsearch/env/BuildVersion.java | 5 +---- .../elasticsearch/env/DefaultBuildVersion.java | 5 ++--- .../elasticsearch/internal/BuildExtension.java | 17 ++++++++--------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/env/BuildVersion.java b/server/src/main/java/org/elasticsearch/env/BuildVersion.java index e1f5879ae9569..e19ad87932e7f 100644 --- a/server/src/main/java/org/elasticsearch/env/BuildVersion.java +++ b/server/src/main/java/org/elasticsearch/env/BuildVersion.java @@ -92,10 +92,7 @@ public static BuildVersion current() { } // only exists for NodeMetadata#toXContent - // TODO[wrb]: make this abstract once all downstream classes override it - protected int id() { - return -1; - } + public abstract int id(); private static class CurrentExtensionHolder { private static final BuildExtension BUILD_EXTENSION = findExtension(); diff --git a/server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java b/server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java index 6cec751a1cad1..8271b836269a7 100644 --- a/server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java +++ b/server/src/main/java/org/elasticsearch/env/DefaultBuildVersion.java @@ -23,15 +23,14 @@ * give users simple rules in terms of public-facing release versions for Elasticsearch * compatibility when upgrading nodes and prevents downgrades in place.

*/ -// TODO[wrb]: make package-private once default implementations are removed in BuildExtension -public final class DefaultBuildVersion extends BuildVersion { +final class DefaultBuildVersion extends BuildVersion { public static BuildVersion CURRENT = new DefaultBuildVersion(Version.CURRENT.id()); private final int versionId; private final Version version; - public DefaultBuildVersion(int versionId) { + DefaultBuildVersion(int versionId) { assert versionId >= 0 : "Release version IDs must be non-negative integers"; this.versionId = versionId; this.version = Version.fromId(versionId); diff --git a/server/src/main/java/org/elasticsearch/internal/BuildExtension.java b/server/src/main/java/org/elasticsearch/internal/BuildExtension.java index cc02495b39520..b1b9a568e3083 100644 --- a/server/src/main/java/org/elasticsearch/internal/BuildExtension.java +++ b/server/src/main/java/org/elasticsearch/internal/BuildExtension.java @@ -10,7 +10,6 @@ import org.elasticsearch.Build; import org.elasticsearch.env.BuildVersion; -import org.elasticsearch.env.DefaultBuildVersion; /** * Allows plugging in current build info. @@ -29,13 +28,13 @@ default boolean hasReleaseVersioning() { return true; } - // TODO[wrb]: Remove default implementation once downstream BuildExtensions are updated - default BuildVersion currentBuildVersion() { - return DefaultBuildVersion.CURRENT; - } + /** + * Returns the {@link BuildVersion} for the running Elasticsearch code. + */ + BuildVersion currentBuildVersion(); - // TODO[wrb]: Remove default implementation once downstream BuildExtensions are updated - default BuildVersion fromVersionId(int versionId) { - return new DefaultBuildVersion(versionId); - } + /** + * Returns the {@link BuildVersion} for a given version identifier. + */ + BuildVersion fromVersionId(int versionId); } From 19bd9f9a09a07431bf8e0802c90ea0dcc8fe7935 Mon Sep 17 00:00:00 2001 From: David Wong Date: Tue, 12 Mar 2024 21:16:45 +0800 Subject: [PATCH 02/34] [Docs] Fix typo in DocWriteRequest.java OpType enum documentation (#105937) --- .../src/main/java/org/elasticsearch/action/DocWriteRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/DocWriteRequest.java b/server/src/main/java/org/elasticsearch/action/DocWriteRequest.java index 7f3578ce9f16f..bfe1ff04b7b77 100644 --- a/server/src/main/java/org/elasticsearch/action/DocWriteRequest.java +++ b/server/src/main/java/org/elasticsearch/action/DocWriteRequest.java @@ -185,7 +185,7 @@ default Index getConcreteWriteIndex(IndexAbstraction ia, Metadata metadata) { */ enum OpType { /** - * Index the source. If there an existing document with the id, it will + * Index the source. If there is an existing document with the id, it will * be replaced. */ INDEX(0), From 7139546d5d50abc8c6aa2479119eb5312f3c46e2 Mon Sep 17 00:00:00 2001 From: Nicole Albee <2642763+a03nikki@users.noreply.github.com> Date: Tue, 12 Mar 2024 08:19:15 -0500 Subject: [PATCH 03/34] Clarify filters can be used while creating a normalizer. (#103826) --- docs/reference/analysis/normalizers.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/analysis/normalizers.asciidoc b/docs/reference/analysis/normalizers.asciidoc index deb04a9bd44ba..6acd415437525 100644 --- a/docs/reference/analysis/normalizers.asciidoc +++ b/docs/reference/analysis/normalizers.asciidoc @@ -6,15 +6,15 @@ token. As a consequence, they do not have a tokenizer and only accept a subset of the available char filters and token filters. Only the filters that work on a per-character basis are allowed. For instance a lowercasing filter would be allowed, but not a stemming filter, which needs to look at the keyword as a -whole. The current list of filters that can be used in a normalizer is -following: `arabic_normalization`, `asciifolding`, `bengali_normalization`, +whole. The current list of filters that can be used in a normalizer definition +are: `arabic_normalization`, `asciifolding`, `bengali_normalization`, `cjk_width`, `decimal_digit`, `elision`, `german_normalization`, `hindi_normalization`, `indic_normalization`, `lowercase`, `pattern_replace`, `persian_normalization`, `scandinavian_folding`, `serbian_normalization`, `sorani_normalization`, `trim`, `uppercase`. Elasticsearch ships with a `lowercase` built-in normalizer. For other forms of -normalization a custom configuration is required. +normalization, a custom configuration is required. [discrete] === Custom normalizers From 265461fb1bee01a3a580a8473b0468d05a353afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Tue, 12 Mar 2024 14:43:58 +0100 Subject: [PATCH 04/34] [DOCS] Changes element_type in index mapping for the infrence tutorial. (#106233) --- .../tab-widgets/inference-api/infer-api-mapping.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/tab-widgets/inference-api/infer-api-mapping.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-mapping.asciidoc index e43bbd036b44e..5ca5e0b7bf139 100644 --- a/docs/reference/tab-widgets/inference-api/infer-api-mapping.asciidoc +++ b/docs/reference/tab-widgets/inference-api/infer-api-mapping.asciidoc @@ -9,7 +9,7 @@ PUT cohere-embeddings "content_embedding": { <1> "type": "dense_vector", <2> "dims": 1024, <3> - "element_type": "float" + "element_type": "byte" }, "content": { <4> "type": "text" <5> From 8f7a2c4737eaf8311f56366ffcdb3b504867a9bb Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Tue, 12 Mar 2024 14:46:49 +0100 Subject: [PATCH 05/34] Log skipped elections due to recent leader heartbeat (#106223) Relates ES-6576 --- .../AtomicRegisterPreVoteCollector.java | 8 ++- .../stateless/StoreHeartbeatService.java | 6 +- .../AtomicRegisterPreVoteCollectorTests.java | 55 ++++++++++++++++++- .../stateless/StoreHeartbeatServiceTests.java | 8 +-- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/stateless/AtomicRegisterPreVoteCollector.java b/server/src/main/java/org/elasticsearch/cluster/coordination/stateless/AtomicRegisterPreVoteCollector.java index ae53fa19da655..e9659bde065d7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/stateless/AtomicRegisterPreVoteCollector.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/stateless/AtomicRegisterPreVoteCollector.java @@ -8,6 +8,8 @@ package org.elasticsearch.cluster.coordination.stateless; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.coordination.PreVoteCollector; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -16,6 +18,8 @@ import java.util.concurrent.atomic.AtomicBoolean; public class AtomicRegisterPreVoteCollector extends PreVoteCollector { + private static final Logger logger = LogManager.getLogger(AtomicRegisterPreVoteCollector.class); + private final StoreHeartbeatService heartbeatService; private final Runnable startElection; @@ -27,11 +31,11 @@ public AtomicRegisterPreVoteCollector(StoreHeartbeatService heartbeatService, Ru @Override public Releasable start(ClusterState clusterState, Iterable broadcastNodes) { final var shouldRun = new AtomicBoolean(true); - heartbeatService.runIfNoRecentLeader(() -> { + heartbeatService.checkLeaderHeartbeatAndRun(() -> { if (shouldRun.getAndSet(false)) { startElection.run(); } - }); + }, heartbeat -> logger.info("skipping election since there is a recent heartbeat[{}] from the leader", heartbeat)); return () -> shouldRun.set(false); } diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/stateless/StoreHeartbeatService.java b/server/src/main/java/org/elasticsearch/cluster/coordination/stateless/StoreHeartbeatService.java index 0ea515012a190..d21add7e6954f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/stateless/StoreHeartbeatService.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/stateless/StoreHeartbeatService.java @@ -95,15 +95,15 @@ protected long absoluteTimeInMillis() { return threadPool.absoluteTimeInMillis(); } - void runIfNoRecentLeader(Runnable runnable) { + void checkLeaderHeartbeatAndRun(Runnable noRecentLeaderRunnable, Consumer recentLeaderHeartbeatConsumer) { heartbeatStore.readLatestHeartbeat(new ActionListener<>() { @Override public void onResponse(Heartbeat heartBeat) { if (heartBeat == null || maxTimeSinceLastHeartbeat.millis() <= heartBeat.timeSinceLastHeartbeatInMillis(absoluteTimeInMillis())) { - runnable.run(); + noRecentLeaderRunnable.run(); } else { - logger.trace("runIfNoRecentLeader: found recent leader [{}]", heartBeat); + recentLeaderHeartbeatConsumer.accept(heartBeat); } } diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/stateless/AtomicRegisterPreVoteCollectorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/stateless/AtomicRegisterPreVoteCollectorTests.java index ddb1ccbbd4f9a..f0b6d62ef9767 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/stateless/AtomicRegisterPreVoteCollectorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/stateless/AtomicRegisterPreVoteCollectorTests.java @@ -8,12 +8,16 @@ package org.elasticsearch.cluster.coordination.stateless; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.node.DiscoveryNodeUtils; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.core.TimeValue; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.MockLogAppender; import org.elasticsearch.threadpool.ThreadPool; import org.junit.After; import org.junit.Before; @@ -65,7 +69,7 @@ protected long absoluteTimeInMillis() { // Either there's no heartbeat or is stale if (randomBoolean()) { PlainActionFuture.get(f -> heartbeatStore.writeHeartbeat(new Heartbeat(1, fakeClock.get()), f)); - fakeClock.set(maxTimeSinceLastHeartbeat.millis() + 1); + fakeClock.set(maxTimeSinceLastHeartbeat.millis() + randomLongBetween(0, 1000)); } var startElection = new AtomicBoolean(); @@ -76,6 +80,55 @@ protected long absoluteTimeInMillis() { assertThat(startElection.get(), is(true)); } + public void testLogSkippedElectionIfRecentLeaderHeartbeat() throws Exception { + final var currentTermProvider = new AtomicLong(1); + final var heartbeatFrequency = TimeValue.timeValueSeconds(randomIntBetween(15, 30)); + final var maxTimeSinceLastHeartbeat = TimeValue.timeValueSeconds(2 * heartbeatFrequency.seconds()); + DiscoveryNodeUtils.create("master"); + final var logger = LogManager.getLogger(AtomicRegisterPreVoteCollector.class); + final var appender = new MockLogAppender(); + appender.start(); + try { + Loggers.addAppender(logger, appender); + appender.addExpectation( + new MockLogAppender.SeenEventExpectation( + "log emitted when skipping election", + AtomicRegisterPreVoteCollector.class.getCanonicalName(), + Level.INFO, + "skipping election since there is a recent heartbeat*" + ) + ); + final var fakeClock = new AtomicLong(); + final var heartbeatStore = new InMemoryHeartbeatStore(); + final var heartbeatService = new StoreHeartbeatService( + heartbeatStore, + threadPool, + heartbeatFrequency, + maxTimeSinceLastHeartbeat, + listener -> listener.onResponse(OptionalLong.of(currentTermProvider.get())) + ) { + @Override + protected long absoluteTimeInMillis() { + return fakeClock.get(); + } + }; + + PlainActionFuture.get(f -> heartbeatStore.writeHeartbeat(new Heartbeat(1, fakeClock.get()), f)); + fakeClock.addAndGet(randomLongBetween(0L, maxTimeSinceLastHeartbeat.millis() - 1)); + + var startElection = new AtomicBoolean(); + var preVoteCollector = new AtomicRegisterPreVoteCollector(heartbeatService, () -> startElection.set(true)); + + preVoteCollector.start(ClusterState.EMPTY_STATE, Collections.emptyList()); + + assertThat(startElection.get(), is(false)); + appender.assertAllExpectationsMatched(); + } finally { + Loggers.removeAppender(logger, appender); + appender.stop(); + } + } + public void testElectionDoesNotRunWhenThereIsALeader() throws Exception { final var currentTermProvider = new AtomicLong(1); final var heartbeatFrequency = TimeValue.timeValueSeconds(randomIntBetween(15, 30)); diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/stateless/StoreHeartbeatServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/stateless/StoreHeartbeatServiceTests.java index 1df613a500f83..bad8385acfbf3 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/stateless/StoreHeartbeatServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/stateless/StoreHeartbeatServiceTests.java @@ -233,7 +233,7 @@ protected long absoluteTimeInMillis() { assertThat(heartbeat, is(nullValue())); AtomicBoolean noRecentLeaderFound = new AtomicBoolean(); - heartbeatService.runIfNoRecentLeader(() -> noRecentLeaderFound.set(true)); + heartbeatService.checkLeaderHeartbeatAndRun(() -> noRecentLeaderFound.set(true), hb -> {}); assertThat(noRecentLeaderFound.get(), is(true)); } @@ -242,7 +242,7 @@ protected long absoluteTimeInMillis() { PlainActionFuture.get(f -> heartbeatStore.writeHeartbeat(new Heartbeat(1, fakeClock.get()), f)); AtomicBoolean noRecentLeaderFound = new AtomicBoolean(); - heartbeatService.runIfNoRecentLeader(() -> noRecentLeaderFound.set(true)); + heartbeatService.checkLeaderHeartbeatAndRun(() -> noRecentLeaderFound.set(true), hb -> {}); assertThat(noRecentLeaderFound.get(), is(false)); } @@ -252,7 +252,7 @@ protected long absoluteTimeInMillis() { fakeClock.set(maxTimeSinceLastHeartbeat.millis() + 1); AtomicBoolean noRecentLeaderFound = new AtomicBoolean(); - heartbeatService.runIfNoRecentLeader(() -> noRecentLeaderFound.set(true)); + heartbeatService.checkLeaderHeartbeatAndRun(() -> noRecentLeaderFound.set(true), hb -> {}); assertThat(noRecentLeaderFound.get(), is(true)); } @@ -273,7 +273,7 @@ protected long absoluteTimeInMillis() { ) ); try (var ignored = mockAppender.capturing(StoreHeartbeatService.class)) { - heartbeatService.runIfNoRecentLeader(() -> fail("should not be called")); + heartbeatService.checkLeaderHeartbeatAndRun(() -> fail("should not be called"), hb -> {}); mockAppender.assertAllExpectationsMatched(); } } From 0dd6ce2df6c0922d1cf7afcdf06d9888128b83fd Mon Sep 17 00:00:00 2001 From: Kostas Krikellas <131142368+kkrik-es@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:48:03 +0200 Subject: [PATCH 06/34] Delete DownsampleClusterDisruptionIT (#106225) The test overlaps with DataStreamLifecycleDownsampleDisruptionIT and ILMDownsampleDisruptionIT. It offers no extra testing coverage and its test logic has subtle bugs, leading to flakiness. Fixes #100653 --- .../DownsampleClusterDisruptionIT.java | 443 ------------------ 1 file changed, 443 deletions(-) delete mode 100644 x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DownsampleClusterDisruptionIT.java diff --git a/x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DownsampleClusterDisruptionIT.java b/x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DownsampleClusterDisruptionIT.java deleted file mode 100644 index dc915738f6d13..0000000000000 --- a/x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DownsampleClusterDisruptionIT.java +++ /dev/null @@ -1,443 +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.downsample; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.action.DocWriteRequest; -import org.elasticsearch.action.admin.indices.get.GetIndexRequest; -import org.elasticsearch.action.admin.indices.get.GetIndexResponse; -import org.elasticsearch.action.bulk.BulkItemResponse; -import org.elasticsearch.action.bulk.BulkRequestBuilder; -import org.elasticsearch.action.bulk.BulkResponse; -import org.elasticsearch.action.downsample.DownsampleAction; -import org.elasticsearch.action.downsample.DownsampleConfig; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.support.WriteRequest; -import org.elasticsearch.cluster.metadata.IndexMetadata; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.time.DateFormatter; -import org.elasticsearch.core.TimeValue; -import org.elasticsearch.index.IndexMode; -import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.engine.VersionConflictEngineException; -import org.elasticsearch.index.mapper.DateFieldMapper; -import org.elasticsearch.index.query.MatchAllQueryBuilder; -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; -import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.test.InternalTestCluster; -import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xcontent.XContentFactory; -import org.elasticsearch.xpack.aggregatemetric.AggregateMetricMapperPlugin; -import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; - -import java.io.IOException; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertResponse; -import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.xpack.core.rollup.ConfigTestHelpers.randomInterval; - -@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, numClientNodes = 4) -public class DownsampleClusterDisruptionIT extends ESIntegTestCase { - private static final Logger logger = LogManager.getLogger(DownsampleClusterDisruptionIT.class); - private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); - private static final TimeValue TIMEOUT = new TimeValue(1, TimeUnit.MINUTES); - public static final String FIELD_TIMESTAMP = "@timestamp"; - public static final String FIELD_DIMENSION_1 = "dimension_kw"; - public static final String FIELD_DIMENSION_2 = "dimension_long"; - public static final String FIELD_METRIC_COUNTER = "counter"; - public static final int DOC_COUNT = 10_000; - - @Override - protected Collection> nodePlugins() { - return List.of(LocalStateCompositeXPackPlugin.class, Downsample.class, AggregateMetricMapperPlugin.class); - } - - interface DisruptionListener { - void disruptionStart(); - - void disruptionEnd(); - } - - private class Disruptor implements Runnable { - final InternalTestCluster cluster; - private final String sourceIndex; - private final DisruptionListener listener; - private final String clientNode; - private final Consumer disruption; - - private Disruptor( - final InternalTestCluster cluster, - final String sourceIndex, - final DisruptionListener listener, - final String clientNode, - final Consumer disruption - ) { - this.cluster = cluster; - this.sourceIndex = sourceIndex; - this.listener = listener; - this.clientNode = clientNode; - this.disruption = disruption; - } - - @Override - public void run() { - listener.disruptionStart(); - try { - final String candidateNode = cluster.client(clientNode) - .admin() - .cluster() - .prepareSearchShards(sourceIndex) - .get() - .getNodes()[0].getName(); - logger.info("Candidate node [" + candidateNode + "]"); - disruption.accept(candidateNode); - ensureGreen(TimeValue.timeValueSeconds(60), sourceIndex); - ensureStableCluster(cluster.numDataAndMasterNodes(), clientNode); - - } catch (Exception e) { - logger.error("Ignoring Error while injecting disruption [" + e.getMessage() + "]"); - } finally { - listener.disruptionEnd(); - } - } - } - - public void setup(final String sourceIndex, int numOfShards, int numOfReplicas, long startTime) throws IOException { - final Settings.Builder settings = indexSettings(numOfShards, numOfReplicas).put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES) - .putList(IndexMetadata.INDEX_ROUTING_PATH.getKey(), List.of(FIELD_DIMENSION_1)) - .put( - IndexSettings.TIME_SERIES_START_TIME.getKey(), - DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(Instant.ofEpochMilli(startTime).toEpochMilli()) - ) - .put(IndexSettings.TIME_SERIES_END_TIME.getKey(), "2106-01-08T23:40:53.384Z"); - - if (randomBoolean()) { - settings.put(IndexMetadata.SETTING_INDEX_HIDDEN, randomBoolean()); - } - - final XContentBuilder mapping = jsonBuilder().startObject().startObject("_doc").startObject("properties"); - mapping.startObject(FIELD_TIMESTAMP).field("type", "date").endObject(); - - mapping.startObject(FIELD_DIMENSION_1).field("type", "keyword").field("time_series_dimension", true).endObject(); - mapping.startObject(FIELD_DIMENSION_2).field("type", "long").field("time_series_dimension", true).endObject(); - - mapping.startObject(FIELD_METRIC_COUNTER) - .field("type", "double") /* numeric label indexed as a metric */ - .field("time_series_metric", "counter") - .endObject(); - - mapping.endObject().endObject().endObject(); - assertAcked(indicesAdmin().prepareCreate(sourceIndex).setSettings(settings.build()).setMapping(mapping).get()); - } - - public void testDownsampleIndexWithDataNodeRestart() throws Exception { - final InternalTestCluster cluster = internalCluster(); - final List masterNodes = cluster.startMasterOnlyNodes(1); - cluster.startDataOnlyNodes(3); - ensureStableCluster(cluster.size()); - ensureGreen(); - - final String sourceIndex = randomAlphaOfLength(10).toLowerCase(Locale.ROOT); - final String targetIndex = randomAlphaOfLength(11).toLowerCase(Locale.ROOT); - long startTime = LocalDateTime.parse("2020-09-09T18:00:00").atZone(ZoneId.of("UTC")).toInstant().toEpochMilli(); - setup(sourceIndex, 1, 0, startTime); - final DownsampleConfig config = new DownsampleConfig(randomInterval()); - final DownsampleActionSingleNodeTests.SourceSupplier sourceSupplier = () -> { - final String ts = randomDateForInterval(config.getInterval(), startTime); - double counterValue = DATE_FORMATTER.parseMillis(ts); - final List dimensionValues = new ArrayList<>(5); - for (int j = 0; j < randomIntBetween(1, 5); j++) { - dimensionValues.add(randomAlphaOfLength(6)); - } - return XContentFactory.jsonBuilder() - .startObject() - .field(FIELD_TIMESTAMP, ts) - .field(FIELD_DIMENSION_1, randomFrom(dimensionValues)) - .field(FIELD_DIMENSION_2, randomIntBetween(1, 10)) - .field(FIELD_METRIC_COUNTER, counterValue) - .endObject(); - }; - int indexedDocs = bulkIndex(sourceIndex, sourceSupplier, DOC_COUNT); - prepareSourceIndex(sourceIndex); - final CountDownLatch disruptionStart = new CountDownLatch(1); - final CountDownLatch disruptionEnd = new CountDownLatch(1); - - new Thread(new Disruptor(cluster, sourceIndex, new DisruptionListener() { - @Override - public void disruptionStart() { - disruptionStart.countDown(); - } - - @Override - public void disruptionEnd() { - disruptionEnd.countDown(); - } - }, masterNodes.get(0), (node) -> { - try { - cluster.restartNode(node, new InternalTestCluster.RestartCallback() { - @Override - public boolean validateClusterForming() { - return true; - } - }); - } catch (Exception e) { - throw new RuntimeException(e); - } - })).start(); - startDownsampleTaskDuringDisruption(sourceIndex, targetIndex, config, disruptionStart, disruptionEnd); - waitUntil(() -> getClusterPendingTasks(cluster.client()).pendingTasks().isEmpty()); - ensureStableCluster(cluster.numDataAndMasterNodes()); - assertTargetIndex(cluster, sourceIndex, targetIndex, indexedDocs); - } - - public void testDownsampleIndexWithRollingRestart() throws Exception { - final InternalTestCluster cluster = internalCluster(); - final List masterNodes = cluster.startMasterOnlyNodes(1); - cluster.startDataOnlyNodes(3); - ensureStableCluster(cluster.size()); - ensureGreen(); - - final String sourceIndex = randomAlphaOfLength(10).toLowerCase(Locale.ROOT); - final String targetIndex = randomAlphaOfLength(11).toLowerCase(Locale.ROOT); - long startTime = LocalDateTime.parse("2020-09-09T18:00:00").atZone(ZoneId.of("UTC")).toInstant().toEpochMilli(); - setup(sourceIndex, 1, 0, startTime); - final DownsampleConfig config = new DownsampleConfig(randomInterval()); - final DownsampleActionSingleNodeTests.SourceSupplier sourceSupplier = () -> { - final String ts = randomDateForInterval(config.getInterval(), startTime); - double counterValue = DATE_FORMATTER.parseMillis(ts); - final List dimensionValues = new ArrayList<>(5); - for (int j = 0; j < randomIntBetween(1, 5); j++) { - dimensionValues.add(randomAlphaOfLength(6)); - } - return XContentFactory.jsonBuilder() - .startObject() - .field(FIELD_TIMESTAMP, ts) - .field(FIELD_DIMENSION_1, randomFrom(dimensionValues)) - .field(FIELD_DIMENSION_2, randomIntBetween(1, 10)) - .field(FIELD_METRIC_COUNTER, counterValue) - .endObject(); - }; - int indexedDocs = bulkIndex(sourceIndex, sourceSupplier, DOC_COUNT); - prepareSourceIndex(sourceIndex); - final CountDownLatch disruptionStart = new CountDownLatch(1); - final CountDownLatch disruptionEnd = new CountDownLatch(1); - - new Thread(new Disruptor(cluster, sourceIndex, new DisruptionListener() { - @Override - public void disruptionStart() { - disruptionStart.countDown(); - } - - @Override - public void disruptionEnd() { - disruptionEnd.countDown(); - } - }, masterNodes.get(0), (ignored) -> { - try { - cluster.rollingRestart(new InternalTestCluster.RestartCallback() { - @Override - public boolean validateClusterForming() { - return true; - } - }); - } catch (Exception e) { - throw new RuntimeException(e); - } - })).start(); - - startDownsampleTaskDuringDisruption(sourceIndex, targetIndex, config, disruptionStart, disruptionEnd); - waitUntil(() -> getClusterPendingTasks(cluster.client()).pendingTasks().isEmpty()); - ensureStableCluster(cluster.numDataAndMasterNodes()); - assertTargetIndex(cluster, sourceIndex, targetIndex, indexedDocs); - } - - /** - * Starts a downsample operation. - * - * @param sourceIndex the idex to read data from - * @param targetIndex the idnex to write downsampled data to - * @param config the downsample configuration including the downsample granularity - * @param disruptionStart a latch to synchronize on the disruption starting - * @param disruptionEnd a latch to synchronize on the disruption ending - * @throws InterruptedException if the thread is interrupted while waiting - */ - private void startDownsampleTaskDuringDisruption( - final String sourceIndex, - final String targetIndex, - final DownsampleConfig config, - final CountDownLatch disruptionStart, - final CountDownLatch disruptionEnd - ) throws Exception { - disruptionStart.await(); - assertBusy(() -> { - try { - downsample(sourceIndex, targetIndex, config); - } catch (Exception e) { - throw new AssertionError(e); - } - }, 120, TimeUnit.SECONDS); - disruptionEnd.await(); - } - - public void testDownsampleIndexWithFullClusterRestart() throws Exception { - final InternalTestCluster cluster = internalCluster(); - final List masterNodes = cluster.startMasterOnlyNodes(1); - cluster.startDataOnlyNodes(3); - ensureStableCluster(cluster.size()); - ensureGreen(); - - final String sourceIndex = randomAlphaOfLength(10).toLowerCase(Locale.ROOT); - final String downsampleIndex = randomAlphaOfLength(11).toLowerCase(Locale.ROOT); - long startTime = LocalDateTime.parse("2020-09-09T18:00:00").atZone(ZoneId.of("UTC")).toInstant().toEpochMilli(); - setup(sourceIndex, 1, 0, startTime); - final DownsampleConfig config = new DownsampleConfig(randomInterval()); - final DownsampleActionSingleNodeTests.SourceSupplier sourceSupplier = () -> { - final String ts = randomDateForInterval(config.getInterval(), startTime); - double counterValue = DATE_FORMATTER.parseMillis(ts); - final List dimensionValues = new ArrayList<>(5); - for (int j = 0; j < randomIntBetween(1, 5); j++) { - dimensionValues.add(randomAlphaOfLength(6)); - } - return XContentFactory.jsonBuilder() - .startObject() - .field(FIELD_TIMESTAMP, ts) - .field(FIELD_DIMENSION_1, randomFrom(dimensionValues)) - .field(FIELD_DIMENSION_2, randomIntBetween(1, 10)) - .field(FIELD_METRIC_COUNTER, counterValue) - .endObject(); - }; - int indexedDocs = bulkIndex(sourceIndex, sourceSupplier, DOC_COUNT); - prepareSourceIndex(sourceIndex); - final CountDownLatch disruptionStart = new CountDownLatch(1); - final CountDownLatch disruptionEnd = new CountDownLatch(1); - - new Thread(new Disruptor(cluster, sourceIndex, new DisruptionListener() { - @Override - public void disruptionStart() { - disruptionStart.countDown(); - } - - @Override - public void disruptionEnd() { - disruptionEnd.countDown(); - } - }, masterNodes.get(0), (ignored) -> { - try { - cluster.fullRestart(new InternalTestCluster.RestartCallback() { - @Override - public boolean validateClusterForming() { - return true; - } - }); - } catch (Exception e) { - throw new RuntimeException(e); - } - })).start(); - - startDownsampleTaskDuringDisruption(sourceIndex, downsampleIndex, config, disruptionStart, disruptionEnd); - waitUntil(() -> getClusterPendingTasks(cluster.client()).pendingTasks().isEmpty()); - ensureStableCluster(cluster.numDataAndMasterNodes()); - assertTargetIndex(cluster, sourceIndex, downsampleIndex, indexedDocs); - } - - private void assertTargetIndex(final InternalTestCluster cluster, final String sourceIndex, final String targetIndex, int indexedDocs) { - final GetIndexResponse getIndexResponse = cluster.client() - .admin() - .indices() - .getIndex(new GetIndexRequest().indices(targetIndex)) - .actionGet(); - assertEquals(1, getIndexResponse.indices().length); - assertResponse( - cluster.client() - .prepareSearch(sourceIndex) - .setQuery(new MatchAllQueryBuilder()) - .setSize(Math.min(DOC_COUNT, indexedDocs)) - .setTrackTotalHitsUpTo(Integer.MAX_VALUE), - sourceIndexSearch -> { - assertEquals(indexedDocs, sourceIndexSearch.getHits().getHits().length); - } - ); - assertResponse( - cluster.client() - .prepareSearch(targetIndex) - .setQuery(new MatchAllQueryBuilder()) - .setSize(Math.min(DOC_COUNT, indexedDocs)) - .setTrackTotalHitsUpTo(Integer.MAX_VALUE), - targetIndexSearch -> { - assertTrue(targetIndexSearch.getHits().getHits().length > 0); - } - ); - } - - private int bulkIndex(final String indexName, final DownsampleActionSingleNodeTests.SourceSupplier sourceSupplier, int docCount) - throws IOException { - BulkRequestBuilder bulkRequestBuilder = internalCluster().client().prepareBulk(); - bulkRequestBuilder.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - for (int i = 0; i < docCount; i++) { - IndexRequest indexRequest = new IndexRequest(indexName).opType(DocWriteRequest.OpType.CREATE); - XContentBuilder source = sourceSupplier.get(); - indexRequest.source(source); - bulkRequestBuilder.add(indexRequest); - } - BulkResponse bulkResponse = bulkRequestBuilder.get(); - int duplicates = 0; - for (BulkItemResponse response : bulkResponse.getItems()) { - if (response.isFailed()) { - if (response.getFailure().getCause() instanceof VersionConflictEngineException) { - // A duplicate event was created by random generator. We should not fail for this - // reason. - logger.debug("We tried to insert a duplicate: [{}]", response.getFailureMessage()); - duplicates++; - } else { - fail("Failed to index data: " + bulkResponse.buildFailureMessage()); - } - } - } - int docsIndexed = docCount - duplicates; - logger.info("Indexed [{}] documents. Dropped [{}] duplicates.", docsIndexed, duplicates); - return docsIndexed; - } - - private void prepareSourceIndex(String sourceIndex) { - // Set the source index to read-only state - assertAcked( - indicesAdmin().prepareUpdateSettings(sourceIndex) - .setSettings(Settings.builder().put(IndexMetadata.INDEX_BLOCKS_WRITE_SETTING.getKey(), true).build()) - ); - } - - private void downsample(final String sourceIndex, final String downsampleIndex, final DownsampleConfig config) { - assertAcked( - internalCluster().client() - .execute(DownsampleAction.INSTANCE, new DownsampleAction.Request(sourceIndex, downsampleIndex, TIMEOUT, config)) - .actionGet(TIMEOUT) - ); - } - - private String randomDateForInterval(final DateHistogramInterval interval, final long startTime) { - long endTime = startTime + 10 * interval.estimateMillis(); - return randomDateForRange(startTime, endTime); - } - - private String randomDateForRange(long start, long end) { - return DATE_FORMATTER.formatMillis(randomLongBetween(start, end)); - } -} From d8da8fa61ab2c7e747d538dde93e2934e15682ad Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 12 Mar 2024 07:33:19 -0700 Subject: [PATCH 07/34] [DOCS] Update transform health rule details (#105719) --- .../images/transform-alert-actions.png | Bin 214762 -> 195127 bytes .../transform-alert-summary-actions.png | Bin 0 -> 142958 bytes .../transform/transform-alerts.asciidoc | 114 +++++++++++++----- 3 files changed, 81 insertions(+), 33 deletions(-) create mode 100644 docs/reference/transform/images/transform-alert-summary-actions.png diff --git a/docs/reference/transform/images/transform-alert-actions.png b/docs/reference/transform/images/transform-alert-actions.png index ee3328ebd9907f165a259b23a23d2f076fdfb766..a78c02fa305cd0a09a91358d8e63ac283b821d81 100644 GIT binary patch literal 195127 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 H;kW+)=R&Re literal 214762 zcmeFZ_g_=Z);_EtSP)TBl+G<(Dbl5b1*G@hdk5(?grcIfNUxz6>Ae$zfOJBWUV=gh zp#-G_NFcoNInTN8=X>xkct7Wd@Yygs%$_xC)~t11*X)n#s`BJFXl`7(bctNy^-Il5 zm#*{@KGWAo34ehtl$;)wH(6IUYExcyG;xy-j~ zk&)M^oNOXr?my)Xf5kv^FX|IVk5|S>ZKS`k@Doe=>R-OIv)6?m71X}AV%7f$o+Z7q z;D?q|$a~hk9h6CpzF8MCS$0Fi`y|wGiGP1CQB#78(`xsP<&DV?OgWA!al;6V3~nGF zb8<6cSWW6=dQhh;#aX{_7;)PD^+)d>9Dj$lYC|QaWvE)v!Z0NNISg#@e2?27wx+oj zYn?yJ8$Sd<&r9M?1XsM`BoTiJzi-x+oa#Uh#w-P#<#ZZbbNv-Xx+HU7Y+AkZdTeg| z>FjelzPG6f2Tz$pn3A6DQrb$!+{-(at5IR?HM~TfG52e9*gMcRostA*;gKB5tgn9D znlr?GZ#(A}dD+kS@XUKJU*ogkzKr%2dggUYTZibY^RBebA1_h*5|1QYvR#xxYJ4to zRN8y4DoGMF>^b6Ojv-HV)-d^m;I-Q|!g=El^a;j)W}~lQtDzo#9OA#Pr++s#ni>sWx+HT+;ias$-{tM4tC`=v&3ArJi4FOn zd4*Gv(wT$9V<+Up?5lfsa1DVw^j}9S&D}VBwqqVKEmfM!-qw&UJmLwciEm=9>~YZ2Pvn-#vyE& zl{jDi=RLo_!X!^}EgG@$4pD#ce_XzjM`(P7Q;)nvbd4$G|NZePpXf@j$3d5e;e{r` zo#TYYJOIb5KXlozahb?UG{8cX`%jN;PiP#MN__Mmz5V`m?enG38ahaICe_8K36ZHM zG}<#N-nn3j%U3+KLpt?iP4!jD{=-QBV*o=!V?9OTpGG12SjL=7o|mim-TyoG|9QCE z+=RyY2+co@g6=-k)8zaI$&=6jV1N%)Oi!tHUU2?t6tOpMKO7n-8*=#T4+eNicKcxz zCimW-M)BWi{O>gWcQyY1eKjKTqd2**6YZ~$q|DVJUmxz24!Lq0y-s?pqu6EP8GSK^ z$X;kU#6hbw4kqQvViQxkjKxZ z{)pHroRmCH3c#rRTBA&tTeJZ;aD6MO=SGHzTe1i|fc;>sFPil!Rpg7y7awNjT4I0a ztoZIt#myg(qvh)IThXZgrdLr^SF@C&Onj3zr<%yQCWpPZrX!LazU(2MJR17bZl~BL z^N}oCB?DhKoF%|svW1yLnl)d}6Z3Xt*MWY1Le6lSwK#(e?Oe+8ms3M|!k0pA(n~-e zrpt`Jf-u6pVdF94c(RAL4V)=#<% zhF31(^BC`5v~h@xBN3}(&+Lag%v$1YKqt^cQLzdi!9Q(a{U)Jz1uVC-f%rIIIDO`H z(wG98w|%9(em-3ecy^c)^z(Zkzg4fYLAzhga5hv%u*zr0uy!?8e2}MYn+tqvmqR5( zm`{Z$C7hHZj#X)oZJ1FSKh!CfesY3bMGOS11tM&Ql9TpF#q}QRmTw>+zt$J_7Tloz z3$}$pCwm?k1G|3V1!t?;M=5yENfDb$qbAi&F_4|Gy*ixQr73|fp#MwlR^AemFw}F66S@p0+VWY*;_5Grt4Z9ZY(HoKo|#nX5(Y zP3f5pJwh6C8#lkE;?iHyukfkVp?8?A&fTupl5|+(T4<6MbSSnyQjxr{`*Tne*<|{+ zH737(akg3JR%B2-JW%GmWn%-`}gv~G5YCaC8wl6x1{*4T;5sx z9>MS?%z(VT{d1B*o$Zk4D3qhB10$y`R8{UcRSIi&PU)wH8ObzNX>gpDOr19S*o9waxQYcRxHshlQ>b&xk5)VWIB`nJh zwdf|DVb@SsUQj*Wb2=FfjTro_@e$5b8rt|`!BP5L44+$t{OD+d$D*|K2%=t0C{9(wZ>n%A>jF zU%S;W9ybQjYk2n>AgMMMV~95u;}S#=D+=m z{lyUFefyB@HMFD+CR6ldsb=hjq*3&d`XeGMO17Z;)IFlL`*+ZYjfq0f4V&?hzIv2& zMDhX&c6Pnu+&2Q8G~F8O(^`0RUO3fQO`~$W_oU@4wvB8NBGu)8C^R#a%x$EJ-ue>3 zpwJRST8UjgH#cv~E>cM;&I)F$=1Njt>Z7*HWERV&zgQf2nB}~@(z{G6#&w|h&6Di| z+;dcNaW9-g>}h=#RkZRC_rV6I^G`!O(8sme+Be`qTl8t7p1O}!Zd9Omp;d88JS`F> zc&DlIwCZKMY~c4{D8{iXAg%Pa?a87iFU#WBJK$R>TUBjxJgq-Z`{iAcqP#lC!Xu(9 zcNwnSzffZ^m0hWodRnxv0oh0AsMlATsQqLMt1@n>^%5^}vNr*U+vNF@xBvWc$)Hwn zw<6AhUh+VOF-`E7lEE`za?t)1(!&shw$ND$0{0CS9KCPfYMg8oS@Hu9#2RIJ>?xQ^ zR+MGGbZd{^>Pg83MN}F$45nm2OTJV$!2?umM^bsjF@@)AZQ#$v9mU z3z!y-7I#aDVAy^_u?)=Kc!~QMAA4)Rn71Q!WpuBxEz7Tc7 zC{o57V&>YC^!lLjn^swPj5G6prtmTYxBIc3sj4B2Nv9w+2Eezt+$nFtvBcft_sLFU z+e^Mz-zRUa;q&EgCNZcomAfv{FNGKrwrzwgySmSplXG&HV#=ij1^9-&lurl5b#SN9 zBx|!ZYoRmr`(-)uNOFdQ#*NQyggDl+v}NDE;I_3BTN^nKzsmR$dil}k3eu;2G4y{S zvjUS3o*mv3-Kp0?REU@*DDk#%d~QtAJ{!_#Szh>29+9I}ku}n+zxa>>jG;W(-D|jRKYM(PsL@xRvieNHm z#va7}Cl{BA`$^$M>6B^cS2669?TS>I=5cw!C;=6`tKwRV2`aki22EU1XbWv6RQO== zAd~bKP3A-i%DW9$xd6Uoj7arC3??NwRkbMkxPE)e247AUa%zO2VyL1A>V=p6_Fd-M z{p*qvnp$zL+=xj~UkfAs+0oXhbaVcm*U1_ssCSB;_(DAMP-7}1w%VGmpais8=}+`= z3?;`%lF*ZTZS%=T2)WC>CBFqWJ}ay0xaCqzVwbrrP;VSA!S1`YN$M?Fy?V}Nl)Ow! z_rf>T^2>}FkaF%hcXxa&6K=P~F5>=>JnTcWHrw~zOS; z;XSRQ&^D6{k*I+>k!4?^TzWAEPBn>>MCJ5T@aYLM+tj_zR^^qn7*;Jjeg474m8=Mo zIdPVnqL4m)w)eA@j{1~Jk-XeqhJ^z~XC&4V(B^~dyU%NISE@-L*L^FFMyN5$$;xgq za1@NO)vb^R8};3a7{CZQzGT?FBTt7WP3d}EVG1#H6l4ApuOQ1jx$P4}BUCFwS;^=# zo15o0a8n5@Hz5VshufEg)&q`Qm|f=UlW)OVwwUu-E^dPWqMGiC+U?Vb_6K-jMM-V) z>T>+qQS<2XUm)@HNiy1Zjb0ANpDW*!cQft^oZ0U4Z!UUe)xRu3$9g|1<1*@aRJ+Ji zu2tL!p>84=Ze`rs{r>sLv-5V(?nt72p$|oF&F%R2rnP1Z#E4WYwM3+up8_7cRn>{L zzJMLbxZbed`M^i+7lT{retig-?*U;WoqxLJb|Cf|&)ZmCH8F)SV{?lVRq#fqx_+DU z0PjZ2R3$zAu%S*g<+O;sx|u1%&-P3J?8L!+i;VhUKW|@df<5!(E5=(cKJH>rJ0hv| z>6uOCTfLzIB&=V4i!MF)vy3Lb-uus}Spk9?-&$N|tN+?awAvGPo_$!i1OKd^nru9JqvDo>G96lX4HdN>D-Ld<5e|>BiR=hZ7fP;K>LtZ%*!wL@hu?mrSUl- z1CAOJpvEh}Re}`^7a^eG9X$z#-LEQLFbdd^BC9Q1Y(Fhg$%Dwt@Zd|zk=B+p)4sy{)b zn6cXPRJk;kaN&00)7npxZN9arWaB-%tkNX|RjtAE4UVspAL!(E6drtfamz>AcQLpO zM>UjPbhaH!l);Y(Jo%Zw@jz_DT@~*dd3w6fJ5g&Bl_%zm3BT}6Uzt87h1O`~_uUhF zC#72Qr3nUGpi*)C#t{^DhLS$#N?-#%5Iz4UG_$+{)3L$UrI@K4E7J%&o7>$-1>|{V zh#g*axc_T{2wX13rBh+3)2QO!bz0rQ53b^+8UwY!0+!&6e2NT#+thGqVFQ3o{`$dek8je-L zC-;7kjgn5Mtw*;`TwmyQ}adS%C>8@Wpjtb}byAyhCe9$fyQ;+}Ph4`ihs7N#JK+>XDM zccM1{+#)j#*Lb}aB*0@V^lJa{b+gt5{5h=vhPb6f&kcEJc!7|I2Z;fK_VS|e$SSnU z*4v*x0Il1Jc7trHnQNqerF!sb>-+X;8;b`j;I{4BI%EH@Hhj3>1wm#oc8<>U?nYD;$-ouLT+)J^udl^$*w^eg7m15Onl5S04 zMLxA$``phgy1ua{7nPr9am|4dtuY-*j^O0MjXCt|qtw z2ThRwCdy8EztaZ9utdC{A?R>7wJPn2UPb&FMtY2Xyy$X06}MqO>jSgfhSF(aFOfdy z#1un>Fv}4^@AFyU$j~NjEBam_ZYQ+X8Z|M7&Jc9v!l4(~>f5WyVnx?ETB_83z7`SM z_VJ)#xAzaNcbd_q*{zM@dP7_VA7lne-0`;$FufQu`&~lI$)d^Wp0Q}2j=i9W zxu^+ogDhq5xKPlGaj`x1X&1mM$j1}n*o&%IAR>Yhc=wDY!Ao#{V5yZsVyxD>-l zpB2l$;NB<3*^tYBp*BpE2>lUde?@;JQ&h<-9#>s(Im)GRj;;LJQ!{qc8F}jNc+$|| zb3lxqaglObYTpw`n4lg5@E~{0%5^RJS(#3qt)^tz5TRU5(_X4p3~;4b1ZE#?*y}^A z_p6|28VAVe@_;UHeE=3Spj<@j*3Of#H2&biQ&^YC=#umvxP)q;QmtZRAK&7hjMLOW zg;PjJa!!vw-zE3g+niEy={ikS&5SV$Jc{Qr5yRC%c03S+16pd5Xcu{-fT1b&y@%qi5UXw{1mzKo>y z@l!&*d0>@DKhKy>wH^lseQ*}B?hkZFwCzr7_7XC!9SU)lN-mA}u|$EWugEn^JJ+(8 z3hX#2Vm;%oj^_~Y^skma5lZhOkWDNdpYpBU*}pWVI`@!S07X|1Ih|J!38rd`zifTB z$Ck-s3&lnB7tS!=l}E4bcxuncesQ2^E+jIw3QSH!R!?-x6X!hF-_Fw^qT2~LkDF5ss+;bp2pQt`)T0bP%s5MU7NVcT0Iy zVHPm(Zg2c*K4SI?^ABN{<$F^qiZ%rNhrO!(G^|}Z=)-};&bPNNhl>Z~S(8AlX{^-a zh6hY6+lzX+Sf43?=t)MvqJ=`ZRkj#LEI^q!c7O#Sp%A#*^C=SRo5_0>WmB@B-1}82S&X@8P&E67X0fVE33O6*;&TXyeRG_UeJD`znm!_< z;z^XAIp@?V)s4d*`g-81^lL1?s50dpngP#P$sX|R!784=+e2q?Zi#^srpu5)H;(lk zdMe2(?7QE`)H=`cA7v&#n{hhagcr^?P8HaW&@Z+u`FIjp+<&aiQGYhccfl3!Rd#+D zubH$uIr1T}EBNwM7a}V@I_LkSNPg%4iT6UoNPBD7H3s8}VD1-E6T%&9?+MJsVsmUJ z$%THHLS!NdM2hB>e6c^Zxn3iX1u{yttLqbh2m_>OQ5fvl>b~=LWJ{x}-aW5(Lthqnk~tO1cmB0WO|o>jvhRP_ato6= zzRfNSmJ=(%eA&+ZgP(nVy(2Ja9N^#39mN6Yp}CzKBtwt0DB!Kl&sqv+K(uA&g~f0Yp) z>Yb;)qg;pgb`I$zY*ha)E;$Z~0-#6=za(y>={Ab`p|H*5fKHZ6wU3^zLu*79*dE$8 z9tMR?@3epRRBbfXW=#CoaMkIEWY5V3&cR%K(%F>f2iI2?7V=IcULi;AZ+*@ z-sV_4$vEn^0^SJz`=85*5xMF!e|+!%i=y}klzcHz*3KkFRl8^FOOIL@ku{tCx{fWY zdX7G^72D53dE4p3ymW8KkMq;jD_s zfyk1^h;K`jLOX<#tFbfn!KueB)0wkuZq-ZQjE{*j8C5QF&Y_ui$NUD?M~uMc`uJ=7 zBiHv*1ZLB{kt)jlPGIx3apJgUVbAEx58L7mFb2tCz*EbSQ<|&u62@#FA%w}j$uY?uCBRP zdr!UmWvkoCta+}4@{^@@NzA0YyZeyr`To_; z?VX9>J7MkpFaDmuzvUL1_p7bAe70ZVez_IT`|?bW`k4=RT#_{}TI34f`~zlqjY<9K zWy(Olq3Y$kNf8Z0?@pty_TJ5%owWsjuz4gj_y_;$A!6&bwB(uZGF4lAfhyHtAp}*D z>ZYB;iC3e4vQgTDYAbF`D|!G5K7pi^GYHT;ByO@c35un<;2pQsLnmk^2kgzcHp~yaV(EjXpMDs_vTx~14gnyxr-K7%#zWMX61rQbz5)F9(r0x z628C`J#md@O@`rOdiY6&sK4)3wKLyc{MQihpcjr)!g!_k1<YY<)Jm)MvW}Mf^UBkDZN$+1pG;ww$7?k$No-;1XS+3ON#pr_ty@hyEdJLjG?5p7=7pLtfQcfNOZ0sEwx#dH-u@3)54{w6!U#&*Wq_FB zjXPcn-kv@vm@KfsF&CIF_*`yym8^VfNEO7dx*o&8HhJ|R#h*DsajVSrlN|kCR{IU- z#Fw_}(X1T2A1f7KCGp>aO^0PUs6X(@O8cp@k3T^&ju)<@T7!k%mbWEr_40EfWN}>g zo`N)ku?ou^e4Zbal6exw^LzWncW=c>pBwY)l<6j!LMl^U+>#p8w)hzGVVYi~K5H-A>~|puDZaRLrq)NSw=)+V%x?@zQG4(+q_prLE4pOP}6{;4inN_ z0lZDBiFKUSolS7n_&8?ywrqlS;X$*sm`qMJ$pO@1U27JX2mCt{hyHl*5V#QIs$L$j zYCDk&zch#Z@g)9MBx>C0lFEX;(asBbPz-dZs9ueWtM_hlHYV1iz#@*cH*C21sqEuI zb~R$_qh-&CMXyzFw1V`n9xZ-H?d^@)iTp2Vk3wX4t{DfQk|sL3j&LG zYv}nEuY%drn%%a`6{=@uoMSJsV%MRV+%GjJRuNGcuhmm}IaZ!7z>y@A7zOp;dFlV|GojA+j?r-)=uFEU{BB3| zm9N91j&GE4->#o}^PhWBx~HtIMY24VyTsKJ6*to0br-PTvRAtGQks~weZ9lLW2yC2 zZwxzX(MP{^T=%F+O|+#<1Uak33%P3`lJE|A4k_sjH3a1L6sctJd*}nt8f0~=@FoP1 z{!<)2e#d&X7n)E(W1zUH7avvTGe9ysgR>Ijo!s0ysZLk~`vJIMq}yor!FQQ{yW^cKu~3?_7%;hGDOV|l znSahD4kKhUsO<~1EuptK>A{v$gBub!EA+Udchm;YJ~dwU0i|3>hf&w}H!&)DD#3URkrCnt7IpZD*83vit5 zUv(xsr#{5xuO+BSC7kR(c;Fi;6DB}Wk6c> zwsXWzQwk2Z%q)F=TGlSOa{q*&z)-$VeT=5nZ1drEYkR6!6eKMeZ~J~;h!>4@LDU;Sh}jae}~G2Kw?jP6Y*u%NGmU5UUz zeg)z!&hE2FZ_uDbcOXZm6L~gdqP3pvbFOWh*L`#0zjAjWyp-(0TL8T>?K!!0A(zMU z^xg$t+kUBmsuuJoquyU5llaF@=I}qeL6F@t{AvASMPa75ty+m0u(-3*b~KIsXnAIa zTl~0_crDWt=2bOA(7$Y@s(q|+oh*vUbjOyUjhu+Z>-Z0Mdb5Ku?+AEY!El=R%#QqM zl|up>%-e~xIBT98wQC`^jukkVr*&(Q7hIi5TaH@_96i(YUMNNTh$2U%M-PA#PwMmp ze`4p5tKjUuasI$*=h0k?aMD_}EeN8A669H_L)oS<9Pk*bW=T^U`gbD!DLJG=R#V;! zwZKmH!~x!#oknct%Z^mDlb&M;R6u6(FBQ!aS*1zEDPxMilm(w*+e}~t{5W7KQG_o@ z_Q<_z-!F$WP!RRHbQ;De&dLoroGz>N8sJakw~54kUe!dp_w9nsdmgMpy`*XGi6RnR zJv^EyAzDg8&H5FFLhK(Vbx7OGWgBFvDWV;oZcQ(sz3|7GtMvH><15L*UQhyw!F_%* ziZ9}@ErA-F2JWc%X+vgA5l{*lXL(|$#9@+~jahv-p6+m`Hkael*g2hX$J#b!q{prnRt^RMLQAaWv= z%x42`Pp_ancTM<^yHyz8;4A=|guU6{&0ZSQU|Q!>VGKAVHP|nuTu7_#sa65;^TjyI$FKA@P2XfkP?(nQGZ7}*B9r{;yp3Teh?I;r~ zvhtyfSiG;yv>>xs#cH>H^)V#g*`-MKgh{<9eKzhP$qT3}P$2H0ansc@%-Y-CDEg(^ z$-SvDk|^f=coZz6#?$_NYK0Fd0x6p(otNA+SjeqS+@~7cW%(o@(6`vJlM|VE z4BXEP1?L=$N*fjERh>tXMWSAFoTV0og2mCzea=*a{M$6B&qDNkw(+C`2jUf%3HNST zo&5;26f1T2wX;qER0uJZo^5ROyev85ZVHs8gTMlm@ddo&V<9@)=|Y~3i{Hwc^j;D7 zJYC@bU^65GV8WE9JrEshC-)zivMzek2 zqX>aWnRA)bE}8+4wK(}_AfB(Ff}c&?oC{)IO2jra!`RdP>*##1^;xrrLdtTgSsXKT z-tDKw?#EU#MI8o?*@3%DgY+>N@czd7UsoCJBX86PogFqoR$@pWxON~&=R84@1(siL zxI&{I@Fvr`_A~nPn|GFZGzoO|tHQ;;@a$hJSZDNG0cQ3=R1>JU)0Iy45U-F%G~XqP zPR3q-o>QBMg5GV`!HOSHXmJFIJCp%43 zwcWQCGJd2Yn%x-#7EChxG=>PGLzgPU!wei)keY_2WpD6&z_bFBSgKRn4DI)}v3qLOP5O%@*sze-V0Y9~&+eZVVf3-X?jrZA^@1+||f%QM% z2agkRLx1ck70QNVT_wTYn*zvR-5p2LYiGvxw>Jw@vOWftYZ6D(bhiG&Ize($N$(52 zncOY0Yq#=4w>&^#7WRqFOus;%tmC|Y)4qK=L)Up6Cg2%rV);HJyQbtcF%S9Xsdv1$ zq1^ja0jEds^J?Gi^uWoOnU+xV{o_i!GTav)zI+(4UBiy(B%W6=VefY<4ohcTIVfRN zNqP2V#$j@w!K&j|k&{i8 z(;`kqQ1-}QGu77EC@kRabaK$YGqg*gJw(va!N+rGXO6YoQBG@~;c@3`i9^Q167Clv#ZQV2Jo`8nzT zb-Uhr?ruwPr+~+3zeTN2x0#Q|8uqUUy0v5=dblm2 zQ=f2v1|%5Chf9#t3Lk7{B*`<5WHY~(PhWD!Rn+z|wE4?2pM_(NS7VbS7-7J`52QD{ zJz^J5;FR@x{r2?ZFA^1nsllrJkgrtK_{SV1^DctEb z_CN0@Qv$3B3pt#$y$fpt`BVU$**00h3pC^d*?b2Y&Qtf#RrbqI14DS*syPs$5q zD@EWz3tRY~u{($#QLz0mOT!$`P=}Sh#Dq5g!`|34CNy6^LH|2YH!8V1HGW52L=>cv zaVd(*xlYAKv_jsuMU?6Dy3lZEq!iXZjCK<{5jENhs+)&LK#X<}06p&%fnYESHf7^e z1=V8&N&pXvg0tP)cBptciVg+s=a$pRcGpc$WS^;^PKK1mPBCB&+AZ?t?AbNs8_m!=D9GNMu#&-g`?CMzT^Umj)#w_{LRZ*Oqj^c z!P~Tz7Aiaui-EU0oOgHg2v-1L} zcDhXn+Qpw&7sy7v^sI3RL2X#I`k6#~hKb7#++x}M%n;b+3ZQe@o^>~^3*2p)Oxtyp zcUv$WAGc324$4hA?52?R+md~fBF-E~(0mAg{yetl^)^p|My7SMbZEqOaW9Mv_R*hC zlU=DPp+mXrO1|Lv^fIL2`MdVGw?2|4yFoqQ-9e`(TLoY4?h$(QkAjfN zA+*8McF;$~sAq~X@O#?c`sKiT8v^LOx*ymnKm}NmnMXP)I{sdi0-~K?qe-2yaG2zH z2;2uhAO~Qd6|hm#1%9LEzJt;2%*jcWVn|Q|sTE-nfj^)R23*47cjzU3&|09uRir6P z+sp!IZHD7{$AHUOt9RU?Ca3dU-n*mEiyO+_Y|x0 z&KbhUDYY=GYLo3NAkuiB1#Wyw%hHPUxx_(c~h_-!=lV=@G(>JDXAZgX8p(DF* zk^9T&e$#*6~E0A_6Sm^dTDTRh21CvtOx7QeBxC<{`Eoj?v7);P`WztD{`7| zM}wzNXzEeuXc+wJ!bAD_S3#P5t75>mrh$7n@@?y}oo zYsm4ymM90-eXB_IlRmK=4c9lT$p_l0DNn5j7o|F0Zec%Eondt8rydHm)@tOx zhxz8#9r=}Xly^(CqTqV9P_@&Y*?$mOj^NO|==_b2JhW_-S-yR2PG@LMm}z}jBkv71 z$}xW66?u}rYmp>ocgVvaZ;_`h7-1er?#3ZI5ueqN%I2^@OZq zpbrVlAgIl7JK@M1`yyk~+uqin{I&NI)Sz#cSN!AkRN$_5P)T*9YwcvM%@9O|oU8qp z9Ny}KX=p>9=d-i|H@a`4aoc_E%4s}J3p}SPAS~4gpVgjx>DtWrHJitq0wtv>KN)|&LakCOKbIzPUhBSUa%#Z0fXb=^#0XxxFYL%|Yj>Fz z4vTk#_f;OQh=p>S$$^r&groM;C5!8T7O>K^NDyJ+7pd=tS^w&`e3gZJa z@>Avy6|RFgPCc&MBoswyl98J3)aEg3m7( zNs5gPo%*uQ5$X${|5IPEu&YDd1?;ZORJkXKOXD1kP1*RThq14`vP%lWdJPq|#>xqk zrp=?QP?@3p#`)v^L#^HArndaS;g)1foMo?U%G47#<}+1kBZ3wuHs(Ayo|Y~UHBnYz z5wjNd+{X-6>AU?V_4K$~jo+_dy^0K+-=9_dn%!7>(7)Un3NQ-v9*#`oGHw#OWOFrF z8C;DRPOIyR)Etx^O!Hc}(%UNrTAT~z1HWD>2+w>#_3c0fcAjzA*Gx*2<;^Cq#!3Lg zhrtotNy6`Rju`D~eF0Xv3gYt768zpYLXPITONjXfpx>7}fV5BraU-`JL4SOqBzrQn zoN2+QvpD$d^o~{u#^Gr4nYnfWR2s~z%*ZSZM@^OOOa9nO-nT1kF!>H|*_d!>4>(Lu zwOvQt;7hwhOUQtKv3+f-xM9gq)7G2^=Q5+Geke>d=V}k7_BOZlgD07KV^lU1O;0!Fk<{tJF0BHV&gsgh7^9<+;Pq#wfJ5)`lM%sBw(+n~ zn+bZwyr%)TVLgZI+f3;AqpfjW zZUN8)s#TJREA4^QM*e#Z&Lz*~F9GGc^0`GB+q;fqJ%_Squ*UVcG7ErS*d~>)Y0S*MW}(&NY)ByG!$edfL{w;K}JBVm;#8~%O6uY&z!?>p|h52w1jy5-BqcoJLDFbR|l z#es>DijyKL>8y&cvi|)pfPJ0<=n^QJfLX!>`cxD$LMKV@8{l?El(btpXJSmFPi_K{gVFb3M88-$%NolDJ^cX*U#il8&qrZu~Zoi^L zY%Xq?f6Gg9Cfu?Z^GwUN1tE+av0u%+<3SQ=v<9RRi3`Up&yegKX_$U0=IAgZXqbKF zBT06gD3BunNB|<{(l-xq#8!%#t)I@(qQLn`u$k)d1UP#6$xZ!}tt#?7x9lM=$E5%_ zzR=6`E_u9fABtm$-9*4*-Urh=Ny*&%5%5i`R53e!?TG=*Ot}fIPNDt~#$~B@;k-B- zko9yHUm|pM;qtiyvSBKz(PbZpUG!f-Y=6i}j79tJG>BNu+c{btEg1!vpc>9F3j+ic zIc|`xZ9P`tHJJa9NplkQC zW%daKp32tDfR|lBPM0Eq4|#F)>ahgj^#>%?Q!BG%qte{IaPJJ<_@OZ)XAvkBz=v0A zsTw7oIQu4G(-|^z;~rBRLFPh+KJc_o!qL1cy9 zVA-pEt|kN^B2bH<;#_lSa!NV(%j+m~|3vxeni7pf3m$9gVV$yycu(sV$J=qtU+y?~ zB6f9}Po5Fc5J)JiU%_iXE5MB%&U1HlJaiprDiIx16Kj%Ked#)y<{_e{} zn6bHaoT!?Ee+)~MpM6mT}9CKm-B8kGjs_^TxEty!dw&M?x!f9*Sd(&yk5F zCG*bn#l5=eBTZs56KCjRxV!2tK!9L!FY5!o?GfPnPkju#$l&Snf$UEmE(`5Bbv^}` zmm?ShhPNi6d7xB`H*BUrHG&Zx6EEmL*Sx)W@@BXCTPFi#>;7rmUg5s&w@P;KWA^Mt zEe7BZhjEc>8Ht>pli8B;OI;*U%07#=p2$=AnnUr!GRpE>N~03{VMhX$-UlkJ|7c@E ze^Aa62)F{pomPI&m91I=a7itFdq)d54cO>-{f=G6ygZ~4%U1K^mT3PO%f4<65xR0V z9f%#!A0ORU{i$a5xcX?hVzKYF8O_f~fvYp#{LCXFJe$l(^q2u0aq!Qz9%iWFVP&|QE3I9R#0NeX=r>}iy zZ>x%ZkQ6!oRk=u2-s@+p04Uh8*S(LP8yg>5?S(~2wm+h0{noVmBG1k4ptxpL$|HK& zr|wYYT~ub%v5nC@VFd(*cCbr$;`HrAXBKXqZoH;Hr*ozmZYCUTYq!OkwEXNkALx&> zTGR;lM2=9>EBHVt*Me~1P*X^kzUY2=mT41~zV%npvir2L%Pb3E zypsUS+6W^kks1rWS8si!)oHRFF>Jx1rA#GFQ1ge)sXNX~&*uFD%d?u;05`RX$M-Ws z8l+JhQ1RW?nUU#`brINTzJuMSZP(&KL)$(?(BWXC#?^enc|i%#T@Il0UG}dk*RPqV z{870zns>&wM8K(ez6S-kgFpQrrougRS`WSvyMe`4X1rny9M+!bs8^%sF2XV0uapU0 zf2g$XKh-4}6u|2kx(-`(#E$z!0C4MuflWEmP;Z`8Y_2G*3#KLq{$GYt36WjKn zy)zaRVE9*wuv%;IP;y&juwZ{a>f`}w|5)zsP$;ZFiY>-BP6t|%tideb7kx#nWa~O# z{)hUdG`mc;Y;n*I>I0vOn&e^hO-AA8hD~$wo6neQgHOEQE2j&Ju7kd_sn9A#U}~5X z7kf#Lp}lR}cRi(w%3|pkGNw8UV_C#7v-ya#Bl%VYvwVzu7+dzhm#d5{F*L$&|a%3 zbsqa8BE7@BwuAFc(%J{<8BA|UHMT?2ONxh9LDIVK$;lXVZD&-xP2{8JW?sv8gS|2S zwQd>H6ZH<1P7{XS0I#9GXnp8eIx43vbjqPB|j& z%>9PHo1m^--I{@Ovhf;;5ON$;bZVO>x`my$y7Dio~J5J|kNBd#LH1%l00Skxuf{)Hd0Hc=OW$}i{46lND@)Ow>I4fIcWs>7eDFPO;A-r<_e{u?)oVfkFEpS-J>mulB zbkLiDxd{|1g-imTO8T|Fs-e!V-&Gaf3i_c~XoIgIBI%9X`cVzvzHPtMnk0cg4o6!l z5O`J3a4X*SF|*C7CB8!9MaZ(MW|OLN$8F(pOznyP7wKc!JpqL zZl(gZjM#afozvB!xS!~ha!^PIK?(3)n+TE0dCruYx?_ivKnrwK62|z$COncX(N{Z# z86uPIQvh6?QRzD0EaRahf1K7CE9Gc6s$t8R7;Eo;oQtzspR5~-3N#6SMqv0fT{ou| z$nK$DKMRC$c}55D_=>_q31D_cd2?aMyLyZK_o_KZ1I_#i?yxz#L4_sNpQp;fY$*XT zUcsets@1v)u^+H2-Jm59Q2K$HSI5DId!+P&zj8~M1mQ5Quc?fHp(ZkUQce9gs*>4h z{+*;a9Ahxn4wLsv9D!L4`CQVH$Dr6b9GEu8UhFe|Y1p#vGR8R}B$CVWkA{8KfgQ?k zlo*fd&FzfYc86~pum7)tYe58YVxdzh8|84CD9?sC3M`5t3CG9d@6<4%3M0|AC&kkK9 z1zWVj5`}3`F1HD~6I;QD!3LiR<(O5O8C2e;Kd{?orBjwKzjc^zw*kq3roG;c_nQt7 zazZnK#E_z0*A3gZBorOikN5!_n({se+y12jt?1X|@=wo7G`~jT%M+Wt-%c`vq`o(7 z{n&nzrcKb5v|!So6o3V4ZF2VgmVC83D4cx=Czm}Y6xd(S(*6`2D*zrmuq`z%(da-{HG zU6n2DGk0KYX(v6^Q?06Ta+rPd*#i1WfU0dl{*sH69T5f%6P*L8&m&qZ^Wzx+2bZaZ zg9_TurrX?^FE_+W>6XHEz;@hRg@F}9&+LjL9fEhg&n-|J`5-U&y;F4Om)y(C-QtbD z<*GjN2Ja6OI6Wop$h)9>gIoMwvu}9K%Wz-;-^z{MhvKMfi)<=1Z6F@t_V7ET8a6M~ zb*Z&gRtd20u0Ghan~)Pc@Q-}(HP@RldO)Eg1dHykQR5NUOBYQ*?E1_bD1aCyH7qCL zL9c8(aOt??g4lajrZqk2v{H>;YMLgYC0JhiyxWfn?}{?Ux=K&cG>~eTDWHvhLh|Ok zO=w6*?p`9=PE0obaBqW|A~Ke3(yQOafAj?qeVX?{(W>~I#TRlISwHXXi}J zC8|5~?jR={EFJ$8yi^A|KzR2^ru>%fN0>@s=u<#qndV*D5@q@qMcDDCBf`5>l#|70 zUILNdd|384<*mxwwj>5{6FvZ=y&~JwHNBiZ~e*rQr!eF z_(9E1^DZ8?`bqu@L-VwmutVN3t>iHI2h>ZPSh2n=W43YIy2ue;qnLt^5gcKdO*2tw zAab?v#DdcizgaE(3M~Em1!Qd!MM{1BdzNjJu{TOzG4Q=J;LKs}Yt5b@(D$e5(KM31 zWYMgF)dZDM(IItKj+EoMSZRLTet%~0jrfC<52U6CTAB&?nyd{9OuUCH&8*XepWRUc zjE`{2M{`|NmVn#2(9Z>7wna_-yGhC054#M<&247;A=XDF=NisqIUP8KMiqs!Vi2cr z*>n&6l-`)t=-U>C@D9}UdE-L0RT+BtMNu{tcNgOkxs#IA-;z9;UpMS!l%%z=(mVp0 z<7NU^A>V_&;wi`zU1sXv1%?nLc|SWlomp0J>8GwHg0`}XcUxT(jsC6o|Y`0cXp#!b15WeJ-9=F;O-s>5ZocqxCeI&?(UM{?r!&F zCYhP{x%UU$PxJ$Hcb%$TySA*gD*E4J>DMc)u@{Nnjw2QGU9f}4Y-vJsjWyrw_sDSy z0C<>Eia3|anD0l+uGMJQ$MRTd7un znib=N#QhQqwXeeWsrZvzGco6_cd(1N%fD^zzg~5dE7T@k4ch$Q7*_xWN38~=b=?D_ zBmb|zev$^1%k28A^!_Oq{R_qPGX#X>)Jh%>UjO^Me_mzPL%~I0CGy?>zf|`A|NF`; zPrJjU$A2Km6pH!)>4xI8g4^`>gHuyLx|jca-YHzXW;Sh+NoLIzp?F}2j|f^+@c#`v z$`xu~!cFj3-o*SflYY1m7((Iw5p#XrLim1_p$Kwlzqc^JtLrHLFRu=>Txg14RT3!$ z4VP}^7}MkTTKm024Q;fb3tS$j__O&H_MjiG^(ZJ5#Pmz1e4HTu1|Mb_Biz__N?px= zy!f;D0EY@{Img7PBBD-dtLOT2)$jjQc?GrnFv2zLCHhJC57ghI zCy~#hw#oZsNc>*@&*EoQKwaqeJdb$?e6a4cLgFFU;>r?HZs9^YGtO7(}bTN=;Ff zqq0vmN#*pfqd`XqTn@=aKbR0uD$KkoK%WH^4Cg}ee?O^0Afy48DkvmcYB}p_<3Dwb zGT&!!2F10(vdjV-%E+{K4NA=2KMk^1^!lq%K~4!k{k!%*ya&uS;604AHzs>x(=EFs9PV5*W>kxhfczJE7kpEtz4E))F=)4jlWoDF*T2rQAP+TTM1fM^X3 zVU}q!=>=D!ir7vOiYB6V9}fNuLMp!z~UzjS5=Oh|X)_2(V$!~0a@84l`P z5WCOEfi|6_ZR1Eb^Gq%5xXis%R9tD*h`T?}_szGV5Z)@OfUiWS==S;HT)s&5pE1M5 zNkKUHm?jv&0kA{_x&e(=B4Gtone*XO=y)oJ7f?MhirL7&%^J+0Z=>eV6H1FvM4W&# z74?`Tq~NBZ_wUM3=m;Hq9YkeBc?n|HW{kMmt~AeEo5JP+Rda&bcpW13B#~h5tqSjI zz7qN(4*>kn^PBzUEA$mUDK*v68FgxAV|St?f0E1kU&~U$sW@(PjBAA{l*y7xbZ)q^ z8Bkh|y^6J2%<@9RMNlIBGj*@%*V=Ab2!l6M&niq5c!$|;P4Q+j{)4{A#;7=+lp&E$ zG+l0WVRP#zO1Idz#YrHZww;0M8HfubHpOvT`)5qfuSAxk&6A&DDEH`P^t5`b&$q3Y zQ?5-Zlx^;+_LG}M1|1imE|Uwr{qHFM3;{J1ojIrG!cHt*t20AL?>O<{T1L?;S8MfR zb2X%aYr?at%t^K-i*HhWYQn~BmhCprr)j7yg9Biq`2CZgP{_A01ws+Jsc5oZ*0kKt zT`KQ##W!9Fbk49X>&X^C5@)6-{(@fCudgZC3-2W zPfw`2@SFM}Gy%EQLzxWo)Yxe4(|N~yIK*0#k*=rVbm&#_58(x~#@FlEc;*uKz#}v3 zvS96tj^4eL%50hKUP^B5tnuEC(x6O(W(v<$?ILs6v$#HeSC-=+387v1IWF<5#w*j> z!wlIBO_=z3x5lIL{ne{kvws*|)E1ELKERIQp1bA;?K@|1Z)au2ZOuCc?3sJS+V1U5 zY$WH-%WDvpn$@vgvNZtw3rX3#-!Y3{HPk^WX;m5F{QgyKx^4Nr%aM-?Nl{rF!#_kX zAxX@!d&2viZT;He+qr9zt45kAt-JmEfcHGRZInLzP4WzEheH!nC=0dwi;u>J>Hl$Y zeoFNEHUL!9;MU->y7kta(8r_7dbaYObu+KjXR$1G*U3XK`6%`2&tC9Tfw~}uxio^5 z0Ss6VUR&NoFt2QP@L;qVYx08?f0fN%N^MHHTi$zM|EBOXA{Ln?QfYDB*IXMdV7lqc zpoj@>z1y95oyAR^@W(kkLK-|jA`hbQylFuh16vYQA*=rmN@COFBNoXgEvMQp9S0T7jt<9PlIp#^Z+WXo^Z4mpNofbE46Z0aU zioue3k@Jz-JdgAJdaG^LvnGG+7nK?w(p_-%QyE|L;Nq&-PZ7JQS95Y*3yq9ubK1yxZ_+~lvJB9R1B1k4dc8En&$#Z+M zuPT+UvRO#s<6SLT{Pl{PP_y`UUpKGDaDm68&OS$7Hg!j3P`NoyRANgVGT}Vm= zOlKm>kH&k+)`S+h*F=%VgRoC^B-5OpbyakKTeQS0p8roadKP+KcE?E=h|;dDK#jGj{A7gt25eWt1CzS z8))D|?;Teg&7{|^T<@9zQYdHFt3Dnb+#XQL@Se#5buaZ(%#L843PA!s4Hk)eG3^>f zhEspy7D)Ix2&Ty|4ddh17|d*LS4x9vK67!Fs>Ogfq6+Kqf7o8emA)|FE|h^X#5R!) z0oU7FKvi$t=Y$=^*s@&}VANP@k|LALE$ZNt6t;%z$C5LE{@c;K5^Z0qPa1Jr%rr~f zZKSD7Y}BoM=T@j_q^+F(&qMeM!DAL^RA%p(0a`~Zf}c{dG{z=;K5Y?=PjPJSLJh!0 z;PL-suPhJ_RX>lJsaH3WzZgDorj$DssTPx|;!U3Tf{+QJA}kc0v~TrA%o zi_Z!~P$^Q8q*W2{pA&EcjI>4n3eGb-yaYA<%Hr!5idQ22_5LT)EfM;en^9rOB^T)LU^h06D zJAj+Wl(d3yCmxXO(Ve9{^R2>oU}!=wsy#_B(E9h(NNAB9z;qo6?TbvanLN2S>V#z_ z$@c%FI1xPwiT|4II@mq?>hWtjabR@8(#5u|wEhrNC+F|Tf}A3HXjoiD+~Dx=0g#^) zbpql238j67kl|HIG{$)e?lZ;)9o0I|4tW&!pRNFT@M`Kmn^*|q8A((>H&n*QW0{lK z+H}C_r`Nw(p3p8TGsa1(2gt$4y!Nxd&*BDEQ}v=lU2+Nj<=>u_Iv4WlbAT*rg6*uN z34fU&m4@k7vP3TN-apv}T$~I<%k`wkyL%$$BO;d1%IDVx*Yf)GND3)`pLNmukWZgPlR)n^BiOjj)y0)3g&= z9db&=TURv1aSg=P0mkm-8nquZub;-~57!dr!z_vyL7UEi7NHZc7qI@U`P!>FE=*2I zjaD7;S&V5mTimlcwngo9f=m&wq{iPo|6lpUCw2YXPpYh!A-gQFm3CcXC0J10xf-?;{P5xKeCWVv^#OD5bd0yFQhItu2g^#!QtWMQHo`jF| zXK02H4lmI~P*t&21mpQ8>sl^mOsr^i6mfn#A5@_%Tqw?v2<%DWFj@p&m1Es8c&+(; ztwF@lejM>aPQzVY(gT zG>Wcc3p23(C-R4f9zv2;5q*|L231ls{6l8-n|PytMB%iX>D=Xmva8Nw{dj8tfOk5L ze@Adxm~iBcl@cK&ikV>pPML)(V_=80-$VLUN9YsqGd_Nav8+r<%Vj19?BR;vEqA|n zSfP#mm4h)YVKh?#xekAP)C(ZrXTA=+^2&bp%^rAg>Z*04Gf6_c{d3X+nSfsVHVNBt}3KM?U^pw#QjQ{P#!w@;5a4 ztMVxxLyq!s?HY~t)qgf47?_N3P0Hoyzl9h6enTK!l81B^Vj*_2tUi5A$R9*2b%9Ve z0ao1--jm#aszf9bLdQ64BW zMVUU`!RY(Hi~7Q^8XE3XuB`Zfgzv;jVy|PNdTykCXP`oU&_iu26nlMoOnZ4fdR&UD zasMEc;Nm>}Dm>b=?t3a4y@Lwe`J{Gd?_BPij$32lF(~`w1p(zOO4$TJ=28MEM3?{& z-Z)_teibdil}^Nbz^Z?Kg53ZF9i3Pr`LA%fPrqaM{bg)@Aisjt2uC@W!5Id;N1S%Y zNI%YhC$*bS|A?jT!#o*~jt%y8Bndo5>7(PGpVGQNKRs@K4Iw^_52eTBkP_p`Y?T|^ zerojsYuDM>Ira5n+-#-VZZy#-FCqDd<{oRG0)(%AUZCie?s|Q+Yyv2coz!ZAUiif@ z>D$n=x>4d|vK2Dlad}n!3*+e1g#h*(q((Ag}*hnX6m42 z;owc!OM`6|!=Z^JzFS@^>5U+qjKoGKmy0t1^F$j@r=CDyySKT;pxLMbkTxj+yh4b2 zZwWwn7S5APr>~DjNO?`l_+uDeo zTB|2?!rcm-rFt*RqSYdcw$W*Iu|RKeaVDY`^V2uS%S*G=jZ$mF)>eB3KgG z9?fJ*<+M&vE4{Mcl3A24QYkbnavcO@jR$T{*Ow0g*rf!3TwX%6&-A)V9!h2t=W*I2 z_c&_ER~Jl=2cY`{iF$I;zXbV+eM;g9U6#gcmaOZR(VgoIyQOaLipz~VI-$SaA2kiO z-XGBca~TUzhuy3J9b*iK$7cWN`1vZc!8n>#-q{afQ)N1fhjnh}TS}FN-O<=?^Uh6x zM4Q;&tSt#Fp`*SG>a?T2UXJ_aUgP=9;0Ql_r<$#(fed>s{{}JOO{zcQ%tf{tEQR9fbPmI_;!aW z8%5wo0YD)WOb){mdy=$0oE1+M>PTN%Epq0`a2>QNv#>wzJ_+0(I9#WVyj@2W%kPGd z^%H6qYUD!hua?e>d=8Z%sJck3`cH@N4(eh>LEI_XBoDe;57H=MFNMRZgu#w-DQbIn zp{97Mpt6Nasl;@6&bJd#mZMRBZYJcQ%c#42P{C4d{~Lgc!&8gs#txL2%tTYEL7i8&BNeZlJvCdRr+s@q~_lfa(ik- z>oV>0q3e0FgtclnizIwq%~)>RxdW_1X`4}J=jrjjQHEj8+l6#6F}P4zq_AUL7t@@_=^U_G8+ONF~tYWxsk{sI?Z~=lTGr2>Wn< zaU=>1qaB`igt=~KIDCyaIai1Z0_#IXGhk}OmubCPbDinW#o0ZP=f&a^mQ~S90%F}J0=>G z&I@5fQuqFg;m*C}q$vQH{4mJ8Gu!AyAg+XnRLNzIor3=%0YU@@O+Hub)Zr zr@V^Er8EDDqg>#U3HtdAicV;(!2*Z_Q2N6a){Am?_s8N)6SZ#Tx+EVTZY1|sa@rnj zG#e}#9!b4-oT!SuHKbeIQNS!lZCMP0wp1FG70i$kFW#W4!QGr6;FXYJsuh0{S;wYP z60vlmi4`@6EM?M&mcnF`>;8n5+w69(1Y8pF{-^+eCt=H0j->M_fySRl1#QQHNoMSa zMWy7hn#MNuT_hZyom$$R$jb#z-O!LPZJVBeXRR%<4$#p4LKbwXwMINlo&sq8lkCf7 z3dlM^|M&`(0s?62*!UcFxrJc+=t$p}l0oqLy^$kOxI<#397XCFd0+dfRf|+CB{Cza zUXhY-(T zXsM2q1KdjlRY4c=G&be+{g!K5Wd^$yU2pEj*JbjVtqBgF zy*F39)KLp(1I4E3WraZdjGza%IO#OOrZn_z7$p)PZwvPZ_e*>p`aV_)02Pu8xLQ3E z263#@Dy`|FIC@Q`6keAM&Kl7!8s)qRiLcx+erjr6!AO_7Sq;BPO8yO$GTtGGc(nJ1 z{IbyS^{%Hb--w@XKFL>`kDKR>!38*LwLQJOcq<%&nD;#lLm7ZTIckzHtIYCUsi^8h z{9H&AmhXWFHdQpRd|7KKjz2h}1g$*K?tI5^+06rQNNH}wtG}5@DIID}zpiU@oR`Aq zwISnoJyDqDvQ&}h%MY9)n?ZVz@GV%ZFsM4vtTrd5)dFV`=X^{&5z=4JWB3W%Dqv4r zf1=UoO4J>CD&;Cec*6Zr=efc?{iC*>ME!BS#I{chNyfHro=B&TRFMizw}ZFhKq6Bj zJ;Ke#=&o;@UJGSLCRhJ5&=h}PZxRN2yuSn+{%m!Q!Jv|xTyO$mW1nv^fj556J8gu5 zI*}yo^S9pjzyU@nRDi~uDXMBL?GnJsVw&}m?b~Q?RDM*dR!zJJAOHwt2F#vO@l#NlA-M zK>k_LrJD%2=ppCP)ejEJ&IDb$u`)dHF>U&B?Ir$O=FlNPgCt@Ku|%Qr+W``!X$T2h zI2IkH&!$$z$FD>+hCyfSV){AYIOZmLrIPxAiL+z%x9CRZ`npsMf!KuAV`^bmFCk+t zW=gd?9l0Wh76Fy=Mv>I2+7I(eLtn&Wb9y78ts)3{TthM8Ls-^$<&!%)1&IVU2Vl_( zVFTe0D~)d*VLaLBHpT*nqt2A`N5|GkL0d+x%5X;~9v&-^Y& z>ZgKs@vo`n(&o@&aM{ZBY>B|aQBpUNYllw>Oej1+Mb~?6xzQRno>7(06Ng}69*_^$ zOX2iF9?oD}eRpT55p6OzJ8nc}zFEXE0lQc5C7>H+jnu0-*dA}x#O3BaC$y{c(M0%6 zsg79XXJU|QDiF-eQf!|3pD^*6pGc&zx;5vfu>nr59WD?Wa_d<@eI2%vw#-tm{jB-+ zY@!Egzc87kH!H{x2Nf|B37sIsWn(x{-KV-CiEor&e!Ab)d4|$l^Sx@kxaqzbJJ|E zd*`o|-OMXbj~%Aq6YD$)77m;(7U->D)PmDgu=@Nr6AqeS_qTma>PI@{EeY$r0sU_r zEh&a%W&268W8~6KxREfQW=|BM;#>4)8Fhwj;@(Q4ThhUaQ8c;mW?D@J&~xPalg!J2 zPGG1YK*fCwQ^Lx{Vn7r~&FqCwaa@pam9Gi|7Bk31J~aK8|mWjwSP+~SyO)sYtG zGaX4QAFK+?;zMXlmrUg>$rcGi-#Npf@FTW9!=f$}99?fs^<0uVK5p;mVysJ|l9!X# z$&fo+TwlX>zuZE3RVn!W49{vQLv(sg1e5L$}Z zX3oPw=q3XfwO;smq-?4KDhrsW(Iie;lFoM*G}4Ry&2MrOWcloYS#fF@UcxUo$ZWkt`^eDQ8tQYwy`C>s~w zS1K+HP^h|sj?K}Ku07yZVe8E~D>gft#PNI$TZ5Hi6av$w;=d8VNr3;ADL#i*Yta1^VhRe1S-#e^S4+Nasb2I7^Xje7HgG8QU(k_awNpfjY+C)&ST&wtCUe(#wz zo(p9h=ibpqt>W+XMcapmw@@YTOIc45qo*g0V&pQH%@X(%c$LBATQGGhB`BMqL2{|Z zW3-Y}D(;y(;aW`bj?w2$U-0BS*~{x z1XhMjA_?&klQGX>tZlmBpe(S(p~)96xN52=HMK5JZ_XnPg_bbQMD zI8Sc7z)9Nmx&;F}<2jP(IhvDVaojN$;lrtIeeXVPjwXgAFg+${)!rn1GZ`lHY-^?P zdXI&9EuOieW3iHE3#Q5C=>Vj@C~mrpPLchV8ouoZCuVwQ?m&i!tTcD1z51Bk+4EAx?y*H(n4Gr9PA_BBNFDB)+!Gh(8c>WxL zYYN`LJxDf{6^mO<)*ni#iQjW;Ut5VdOIDb^F4DI zrFu__o$mJ3NK0AsaOx;#dFU|90nq?QVYjo5ue0@Rz1I}!+zyofdAJ)oKX`$AE8K0G zs4!=lzUz4|AwN~Kelyq^&ZtKM?7m*S9gCE5Fqzo_B?G0#=K<|O82xFx5hH1uPI4z- z43*t>t&fi$den3b(tpu+>=hZ=YLkmzy?&^vT|erAo!2hPyR-h-_5;{5$~15Da}$0) z8-g9H&csupi^%5(>6TowAcTpEuaClf`MWS=Unpg1fAAQwbC^%br0PTxJ#u4dFi+Ak zKt%*uU5unROhwr(19L5paBk?)3VkL;-dvCn%uiz`W_=b|SG{*e> zlb$lNBw@C%ohP|30)oTaz_?qIK!s+rKQ=@Ks%Ll7%yQ!8&+sPXCuWN zF*MqZpdriTCuQ?;ziRu&`giQeCZLXY(C}!?q>FX7IEtvLT4GS+`JsF&##m2sBx7W3 zk~PDfEN%R*9l#|>#@e>e&F6C1%b44ybB*WAprpFbNq526u1~dmO?qiGwf}S zkokU$p53H3l8`4=FYP6-T^?N*=@)(v&O4h^Lv9SBg|sq1pd5zrIwxhH{nzf1(}qmQ zOESvg%QiAEOP>TLWsMioSxIJd8tu(19oALYEh17>W)?4+l>1)3^Cs?;L)Z+|mx2s$ z9j=-~7n-Bx()dKFN1- zl<&BUZbR5%^!f1)B|(~63yj*)HXgOl6-FZLaGAm~6VvTrk_wMOH7Umuwd@7qDM!u;>Vk z2_v1EYfvVbAK4wXKBa$nycU$G5~-oAmQLm5h^JSK0228f&d%K;;u$nDtamBi6QQ5r zCWny->OzT2a4=-fE5!BVCF?P1bQ<5Px|L_UW;k=YYc@FtyoU7|%h#U0N-!hPv5*E! ze2L(xwq^uKUi- zLQ#$T8K2+piWts*aiY$JE3A~eHJNkK+5DMivoB-1gz}z9Xks|MPJ%C-Fvj79Evn85 zw?>mQ$B&Si76fF(qr+?1g-)i5nCGV|0pcS2Ur{Bn(1E_U5dqZ$+HQz^@^>SV+|TNE zTHOc=YG4VpmcisI({Z4pCYemW_5%&Mi`NDD+gEj@c$frMJr#fx5&4ZzpR`dQh|Fjl z5?rXQY7f|LmflWJZ5v%$syY#uC`*o$1qt8uD~;2Pn_eVU{`5O2v1E*@5_w*7J5yS4 z+$|Idiy_NuKTfz4GPB2hD)nACV*`pND)JD+#bwL>WX46E06?t}?75?iCjt+6qJ#+4 z+nw<@T8)l0UIGT$MOS;~vv9BZj0yrKdy9M;3q}H=#6SHE zs63>MG)0A*66Zv$G{pv@95_VCJyNkKQ@*(!RymQkP$w@Xj|BpD0vbOX8%xIOcNl&l zW^*Q+F3^kuB)GP(@9exh*{nII3oc2$n$~z_KN$#_zqFpa!MNO+&lIqp)Wndv>R%Hf z-tXAn8DF3H%5yh2TXNTthTxPhubug23ioXa;|mo*+eov~^!y%KEh%D|R8HH<5FHlhyS<*GzH8ah!0MFR{=(PQb zXW0;%v=VZ4uw35TRo`@dko&buO!*x9aOdhUi#TuCE?T#R$2B7uNdOU~Fu$1UcGCt) z3N~JSA|aF7wEIp}Ib#jn9`|g;pj}fJZD^_;j|?SN|AZ-4_JCP@J5+mpEU3_ZXMO}R zS7$z-LFsi{vlPqYL*0A1@J$u=SM3DO|FZ6A^zQ&Og94J0G;ap&=e&~Y%}2*)f)5X%#P#xLeXJ5ZSdDR~;ZX5K=c_>B)1Te& z;JJkwtC|2$zW3Pl3PQ5$=amtmuJbM8qo|KSWf?vD0J!-w2Pl_VnO&I-v`Sy@&E!JC z{|vt*#Qak1FM;YPLzYcL%hrhc61srIoZrFeg`b?ic^aEpB)hgKGla5siC2>|CYz}Y zzXNcc;CI18!aKeTVrHQ=excd>loxJnj(z&4Cs-mqgs2dhKogfK+aDVP#UUTwjiNu6 zVlwh{yd0=|-o0*5W7T_tR7yR#tJ#M-lY}3#wA102hG@|E>gjf>fDh)LtM&4@Gq|+; zx|yWe+?6?N1Jfay;{UafP z>kGjJ`Pat^@$J+TWbQYM>woV%jqzmh5b{7sVRJ=O*HUn;w7Io_O(Ct0nnIB>C6?c< zT&J`+B&}koj*|KLD#e(g8{x5TbtG$6*K7KNrKIh0?HedNh_jc^A#q@-_;;4sq$p<~ z_^W*K8L91fO7IJ)`CkJ~!6;Wc6x)pWP_EvD(4uh*IdG2hbaxtfQY2gkOi(bjYf zwT`i>ECf+ADe~RG+E%3(v&$&WK2&^HbqYPZB5btyi5x%23rC^c3Hiq1VJz?dRVj!~ z0#p=7vk^@h3DkMBn>r8NALa}uRM@K>?0x>qfto=tMt#*}nXQe4e+cw`Px6zG ze9cWS5>^CxYJig@3Dnz$v$+9H6=Hbw1J2I`g1Vfrm4-2NmJtaIS_(uzx`y=pj+vn# zG_Kd(2T>`cVPIhtus(hiwAykkQf-Q)+}avSFD|-7BVWw|y>t>NK?j-$*!8lBX#A?= z!(R8-7y@|!lQCN0u{vIp{0+a{y(#T<*?DX3dlUDo9Xa44P({u1NPtltlNXaC7WL&2 zG(=M+%<-ZknfZsq;Ah~ZvTg*jw9lw1eyDs0G#HB0D6@UQ0U*+MFH*FV{2H_XJdF}x z^D<7`(bzus9?ov_z%XN3Wt7C zz*&QO1y=w|_*&CbTnJj>G%r7vGfcTHzIf&nvHHwj(a-Kr3cZn~xfl+r^)AN{dt-%7 z?_<+f}FJ}X&&8O2i4 z9&x>czjwlfF7S9R%M8_A#Cr89{ARo+%cN28sb2CW4Dz%I$!st(o{*dU_81g#b*ekU zFq^0t=%oajQIIIPm?z1YuCj=P?GIO?k)21hAvV`OiDj(3!WIvw+%)d0T~>3-AMa^c zZNB%}bzX`%+aW9Mab5*l-rL2(Os(ZKF8C~zx9NggA7LavGhApk8*@*WD0Ukvxpsft zh`em)W2J{SCfwRxDlDB-M{ZDiuTC4SreT2D#}6_>n) znw&K@rLY(RgSqvyzlQ~>&CnNVklq-Kme?*8d5 zCoOklx&zx0oGn(GxshIvt)9M5q}7bU_T&{5c>#Nmn{k#m$|P?+>|U~riK>c0DT9i_ zG@Oa^M+@NTGlOxN<;(nsIWx&ho2Hv^dE&32Fss41fDq(l%~vRh3>fyoKoNtC9dZwX z`W+LiKhx2{{2KAOA)eN(+~CvUgo#cBaoKzLp#q^7LTgt0RemiDVB7&vsN=yHz9da* z5MDw#Aatp*4E+Tv1o24x*tj3;B?1~mVA}c^R|d6}kS~|1sM-4lu6DE+2`5F%;sT+m zkY#I*CN79XLKiFL14$(b@$|E=fX&&LksQPg(C58l$u|8yVN6A|IE1_V%c*27Hh3+` z^DP;SQNaeFTBUaKh9k>|X&DJ>vT~iuNnk9jye;GFY+z*w3b8q9w*riHt${EsNukNl z?=(DzwBz5psXAvhgk?W{C6p)ww0AU4c*%y0Wm%RDfM9f~RB#dqpxg1zP9a1v6%F)(gVa02N-d>-jQ&*$gPchosDlJC39v#9Fm%UiHZez<8Z zHRY8Ha}W{#RZ*yTgE})BTEb!~{(j>J(}#^dGmDm57#}}p@lq+5;zN}sZOkQ1mrgId*^xyn|HQg3AV1g z51m$;q6U~k$xZ2f6ZMZcl^W|Rn4eX}|AH@gv-Z+$7E6+js0qV#TG3SNr`kmH?4;#Y3esT3yUO~i=m#@ zDXrW!HHm(Gid#t}u|fqRQAEQlGFxA1R2x;`e<=9m3=s=>%EahKeik)I9I@um{81#E zHF(N5CTTtml174(n{j#f)SkCBL?t1V`^DwZqMcYZggQmA1bJr0YNaIXEiCDCVm5zY zKR^&3dwPU5zNS<6PF!%dED<6k=69zbq7Epm|rObm8x}Rnp*L_FO90& zB1ulIr+|qR_?x*iaH|H5g2{RhH7Ju zQp#h?K7Md+mjUeD3?HZy{=*~Qw_|Y3EL$*&X`wf#t1-k}!>^08#E^!meIBgdBI9Pg zdvLdKDI&`H`iKv+UkS_jkz?#&zRF^~=kvsC)CEHl#^F?xm51Aevevtor`Kt>i=*G> zJ`RyA@n@&r(LNnvXxV0Lox6O{yKX-O?3~1aW0R`30r-pEep_>*TRfFHM8@_tyQ z@*Q`4^=pH&o5xSn)%%Id*gg^@sY~!rt0@a_H+;dx$x!KhH|>deO8$t$(~X#WIc6q=IrT zS6W@AlFvUv>E|aE)yq|xR8c+IjJ$7=;z@`o#HQ1(Yy_?8ECI zVCLSEId%rd1^wNqCb1_)%|HQtLBN|W)~PoSD~n)~$x4BHmQgKMnKChj+;R%lF956b z|D37->12BM1#mucgTx;1_MsuU?KE$+c-$|Nu55FAt~On7&yE{!fwKi-^!;iQlZIUL>8+O065P+g)e=# zLlRa0g|iqT6ol4(8M%ehtXAaCOM3 zsJR8s4Iu%maY0McIS_8O|0+JVJ4Ce`fv|-TX0_cOMvXohMI)8nk@gLJinj`!pOlhz zlu$qPVra8TLhZER6&J?FY@3fi_;9dMxxcI_9Jz@SB)=R#{fOuoptpH{%i{QYpwl4{e7RGy~qZTT?v^GAwN(kgT0Edi<2Ho1N<+aekFdoCO&)fO+R***t>_vRS zlJ{M^N@ZH0ByuZ7wopMcdUO;Lp>ZfX~tQIBo= z(w*m=(vqIv+#v37e02Bg9Y=S8wTz#bzg&@TIbH7@i$@M$6TcOaO|2Psf}8omW`-=4 z@_pXjXy)Md;x@3{(PIggL|)Hr6HDi>*0VkoMhp4TjvV|Nz1>EIUuf=8d1fHa{l#|I4lTy#Stm&|h5NkiPxJ`Ntn2FS3cq7ASu;Z+v#&O@nO|o;k_>=5Aj!*9M2<;4`CQ0>PSj6E320=IW3W ziv5~s`lP=m0B9Av)qstHElTL@yNmsn*!@j0(i)jG?^-@^xU4|#Zmeq4`7gR0?f4?& zw(F>mj`hG1*q~62d5*{Az{f_O5z5j zMhIB}tPXSmwBlxUkkjUyzaDlJ3W9Gj*t5uuQu>GF|H6abg$wOXb&OOmw+I;#O%Cl| z9r8vhXYx5MDCu4YXhni?@edb$#wKPGWoAl}oz4LzJ^1rZAEy17eEh~TNS!w zXcB)()^TSHO-H9fNFWh-lV6-8h32>B(qbV;vZ4as`4%%`290_V@2s#bxePwHpJfXs zI;KXlVTO0_%Ztu=N+(jnwy|TE_cQ;=w)AF4Vm@d z$H23T!T*F6R}(#fQ8z@7E?Y?2luj&VuOgiwsjRg|IYSB=px0y#G7s<zb4-KS#2>H*dT1pk4Nkn-JStGuDaW~6 zuqt=|Kro1llIH{T|6PT_?+i-hcx`Cm{U-c2HLq1DeFF1eA?jDqOP)Le~d3V5w z=Y4-PSYS znh(={Vv+bvf_1ND_Oiv0II)3t4i|qJQy5ow%GuU%Y175QbS7$Ahh$dARMqSY9cbD1 zsY0ba&nygPjNtyFm@qw=yrA%eqLOvAS(~;e;oXqweMhfL%VV^M**iMGdL&~++-c6+ zbVSB%st_aw3Rj1!ahisY7itzugXhN#jH)eYVQ8ZTqZUuI!uG(jdqtD`<@ZsQ#VieJ zf1S@hsQQLO#8dSse{Y>%M-J3$2+CDRWQzh=nJ;8`9ZT?#=R*|I>VdczBC6#pnY#r6 zr1-9isBe1crs@^FmhFe;!r0A@I#J0{B?`ukC@1^^LW~!Is^LUe8_}NkSIOpwvq9ES zUwot8Z&-}7At=%CtAsC@fX_IN5fY>b{s@t-4zK*uIyaQH=)tbg-8Kq|U}{>n z^7?C6Pw?1UmlKQ41GzNjBp|V|<82VXv(S*vj-|F9h07h;c)cSZf=sL`dgppJg{Ns2 z6HDTo%3%#&yh(*47qgfI&kVM6?@txtkp85BT1_R1)vmR)W$~CH|eT+vl%3nM>`&IX!m8@x}1ya{6@9zq11A13a5-*$&%eGzDT z{~52q{-&6K%k*uH^{QxQ&ZQy4jqioj7rurIKc^p^L8yV0GtBniNeFRC*{xoYi@60- zzAsCgw-{X^snSLTPZ!O`3qbwIK?ZX`1)49twWF_ZT70>%?tKTguo}o@isFo4BF&yj zX&vy@J~#cmjMVM)S2!hN36+E!K9wJZGy3bK_+{?(;=Ym$*Ef8&HF?}?f>F37}I)d zn$YUa&)~Te4KY>fLCj4Fr_MGOyoaFlVFJf!$uhj*{z+lIU_D1XN=4}g2jK~MA}H-I zOJBnyXV@`}4X=!du06DJ+k4U*}aOl)O|#R3CatPyoV0>1-m!`JzhU&!&W{ zJ}^dPx7lbK8He3Fl~0hUENtEHbER=yifZFXe;SSwrJJ8*wX&WS{0c?zMRUZEu@!|^ zU3SO#0JIvr=bX@;MiRUK3G-$I+T{cRVr5>L&H`E=mi`*~oKaira`)wUxGoUaF)9nG?^!NCh>_|=lSY%$TN$%GpYToNz}o3 zL)HNq?Dw4bv(>G4>oxIK#}T3G%Dg$U`RXnxGHPT3IpPE{sKu!rOWgd~aBVNIn6(pRJq6to6kI zI{SK|_T^_w$5RzsTcanRI{Ct<+0;#HOnsBS}>|S*@d>^>&)4wlF79Rb3K2&E3tL0QdE8?cQ_6=JTb@lWU)_!}Mvo&qW z@u+qkeB=y2eA-wh*7wPVbDW*hx)t)bUt!^Q%HOv*r+eefEn$wo0H`VSVF*6VzhI=x z4IYUrp#)qi`yB#VlCGktB^t_UZ!6))0Uk@laSZs}<@o8UtErd6Z*jVKb~o&%)lE97 zuLooMXN@{P?AH?)Igd6WgFvg}v(439Tka&hRPs76IS&EbC;iFw%l+0_FS`|?7lLTB zUj9(Fib|(CYe<}nwa4fn=EeaF)EoVclhFfZtusBDyOg}qq&I_E(CYclPSX5;^=$0P zD(o*|>JZ8`M(zS)7kjeL&Nj1Nyd6vGW`l{m5zL?1DW53NK3|u&mjF+TWAi;K|5!%1 zc_yx;$qmE)8|L%(Q8ESRyJYCb2?v%>RFi*r{G=MkV#MPvp9I@D@wz{TqKdKy2Jtmz zP0ZRyVw89}|Bx{N{yq?;>_sxjjPv05(Ft%5K~55fiRNdu)<8~n5Fs1#%nS{XlTwjZXs9&nTR>OLvxHL-ww zoLB^I@x8G-Z;sp|21$s|`!>l~!vgtCft&k*m5jf$*gzPFbIqw~w$I!GsY)s4A5;E# z27Rb^LB6*n=tX|@ReEr~eJttYZ^-n%7;cJ8wX;O7bKLwNzTN_;uBC|yV@6}(mt4>j;4zu^{nV#;|t5@%VYrt#C z)vTKH-oKB5U2ha&-2efrx9JRuSGjax`; zqtUonE2wmPsz0o}4>Q;bXEFo*hZ!T!UI4%Qn@Ep`g=D;#=LnU~cPqHXLF&0maCcc5 zye~bd+3N`Sj}8?`bl7{~(7;*6S&$y`Hf*GGb*Lw(3v{JXg6>&MMAwr)cMWB*hvv3v)u5JZ&F6;ZNoAV2gp_#2h%IjFHn(w z{Uk$tDt`_!;1C67#G`M3KpWoo}el zC+98wzHv7e|M3F;IBnN}ti{`I4L$RP zUx%arJCW(YUI?|{EuFcep7j3_ymj%lB^!xXTMB1XSlV~7r#B)0a|8d(>|YfS7J?qk zJsn8j(}MnbaBUDLp@-IW)BW}6rO47B;o?6pyAmI9Wz@5T{=8 zf0RC^iTU4y5paR6pMUS`QqF0WZ~*`3G5&}$|3qj0_wj<))|J3hV2S(hpZ~QA{<>F@ za3C&;d_lc<_kVoz_x=5*VLtJB1k$^I-Q@oN8!r;)ElCf-Gx-+WpI*%W^W*M!AoP;a zifCd6s$Q|2^M2c_24+q;z6z(*#5f{Xvm!askV zHQ}8-n3gvz1vPTSr*_nLFqfi_zyyv>KHmc1^Aqj!^&b86<7n&ER_iNLAj5BJ#vh~O zWf7jG`@brs9aQjC4ZBOH{aU(+eoa$eDDsm|d&(b7&uCRf=@E1ekO;P#(s5DREI7aG z`^%I2V-6545k;aoAdctl`z-kig7-SQ5|SBxAg>`2aL0jwBs!x5R!4T|jd%1vH~Zh$ zdFDkBLD#Mp^Vvv|!}&z1!TPpE5MOa z?s&Fv?%ud&mhpHNFik?4WHKf40L<_!;jcyMkO!5ppeY4&E+5Wq_^I%Gn<;`U8dD1% z(*$7A>Paxii4m4_@1)S=ZR-9t6@Sc#CzRJ4sGohjUhM7NT?yb62slrY2x%sPLV=jT z-gFW|)`gd>5BO`8|Hli&iy{D4=5ZI7(^WW2Y6ub`N4E^@%3nqo^T!Bp`Vrj%s(6}ABA*OjIqew_lCCmix?`U zAh{foE7JNJ7l29e0_>VYBSDg$5R{uy9=h}=@dp%e@(+|dgI}mg|BXxpI6#%Go-ty7 zwxp1U0g4CSPdm3Zkm$O-K66m3R`Q1>QW#hjDYwWM^z4qT;9ERD;7(PoS}gz4TQrQp zcQr}f1rZ{SBi;c8GsXj~*56h=3FC58x$Hg9mXYGR=5qQO_kjwlTqR9MUrbl|TvDj=R zXb%BzDo3^(MlNVebZ@3~@#^8Qe4oP{h3gSM`;+?+{Qw;WjfFi2kg0Cpp4M55`Skaq=XDt%0GjSYyJE*Q@-$gYY|W z5Z+vccw%@9ItPkY-hx4+Gzs0Y^&chPg^o?6;t8l6v^=GI{vRPFn08jb71)Wc5i=8 zYMuydXXecK(2mQMwkZB!0f64vw4`#!)Y*&}@QAtzI&khxPW8XxbYO&B0Ai52#k?Pj z_fD<#*iH)HlDe|fyY&12^IkykwXW9<16K_5LKlte1VBM65Kc~ z&u^3(D3OHc3;~u=(73G1=AKXWWR8_YaXMvBZlOJH+(L27&FD<542RtqjdrJ#W@^xr zqE+T~ngU&e3n)Q2TpZUYD^gh5`?ZE(rgOD=2xd!7k|)y5O|2wY5&0;l6t6!-klP!sv$)BR`wMLRsI5iTo%W zyclSA%QyRM4tEahk6P{!(fO?!4g4C;@Ds%YN!w;cw|us#_VIHKW~|g&^f_wzt;ei1h^10IiWJ?HK$Rk!wXH=>F55-FxMwHN zQS$dsnjb4MW>I>`V|ZB6XkwI@;jWQp-?k_rZFMa}tcBT79MT zT=1^{AO#5z*x@u?Xjxcf;vg}I?fbtywcqQRmIDlr!7nVOK#K?!r|xu#SkSLCpRZX} zzCinj0@}u1e83CW7et{maeedAf_ zLspG`eZxHFg?U$d=X%1ru(Y;>JuV-0CHJ7fEMD8QFQQ46SWK4AT!Sc}Xd2&G*My1d zHhtPKujx(Gi+)-^hf?TLHL$^I1i_-0OTAt{c9kLg9&tN`e@!fkDWi#5+xuMD- zcFg^pPzKE)SBe0N`<4eeB)>dg)jAA%sTF4R+Z)Z z=zO5{B}WEyAfH(W(AaKJne-*v2RBph)A~uP!nxpFj(~y$dgf}2)X>zkn%S=R0ivJD z--Tog*0k(=N7_nEMHTq0j(%fJXWuScNUyKuuDpG^)cnZ{6`bV8`i)r&#A+$jGy`y^ zgkY453|TyHG%yNjOtSTa?>3{j87CLD>^S^NQ>Z5mm0!9HB9giFc6C*&JI&+Tcu4M|GiH^Y?;8Xl9&cek{7$GT-sR>;9=^Jd$)ve*)Lj?I zkr_XrL2O%mvaw@I3W*%3$d%H9mV|906Uears+r)uk;}M}Y;HVR&d#6wtqRW&FE`@ zat1AduC6cLOs*#xAEBZbV#uQ!um%;n=dD}CT;#bljgwjKBPMdB@PpUnU`WKYq|<3{ z^Ch$toTY^$(&&^VA~^$H9D5wZJo>Tt)G6MsUx{uakcsbv6a{2dTwhn$C#kED3@xr8 z?RWq@B7&}tZFrC5~=`u0A2(kR|AmLYU1|ldP5GLZemgzq}+kGN~4|^h_q!nA|VTF5HPopX~sxY zGn9rtaU`bX#|K7YeRgeFhTSCrxvn=HV~!4?_btv@O!xy9=W%Bt&Tex$T`-F%eKjRn z3ph7w-nq@?fD7q>~m3rw4aH zsmOCG5O;H~3zR4Nq6+Y+r!&KWJ-4;%mHAn#wD78v1|fFc+S2mTY$N9#+_m_wJ)&R2 z9xbY@vf@R|yl)LT>6(1rDLbty=?o`=JD8oB)1w9mhB3GCg1jhFT==2Iuq`P9;Bp~7aGLT^SM2}Y- z=*jt>z`@a1anXk!JU5-f7_mO;NQLpR2nfe!zQx|D+)c@Pp>G|{do31QVP#^^@D71T z!Q!$pBT&va9HVnFX-Z`_?+`ga|Df)pCuSps^jW1>^14y*RZNZMt)i4JD`^;*we||T z?XrC<$w-Vx;Sn3nD?q80b|j@hGnhfVb?^YHcM?gEA?NdeRN{l?m2GQHKrXotqa%h# z=qH4NNs%KJjiGC~(#b!_fPnJqMGvH6yQ?IvGDAD(0!4L-0y%iXKou-NzI5y+hOi*2kOdfVuN26%s*vGw@5GI9Q7DJ0Witcrkm#;eCR3dpNOo(> zf!(`zW5rJ*{f@i|X71Of|DJgOnm}Afv@nt~yhQ`Q#Fj&ZkQ8lqIgupf3o;nM`2NmT zD1rTf&;if5fkL=Q4(C>_a;T%xUU$H(;LrI4z%MyuiRoL z@}+B5?#AGy=z_^|l}1_-iA4j+y*Z*KEwe)$IngB0ETmV{-&h`Sd49N#zmr*)LX??Q zYp-X)zFd67MzK!TH#0z59#3uC=kAfWTKh^@pE1y}0fnM&FIytwCVp*+G(P$F)Gkz$ z50?;!p1zF^U}okUAG`W|W7ktnfV?vp_i`hl*?IIZu}yhnQhuR~G87;wN$e~Pilc+j zkFWCzNIGcfy-wlQpiA6kdPh)6QSUN;eOs3QC>e@09wsVCq^krXnt6Y$Obir3%8L#n zIU@|eq77=N1X@6W+z*9+<(_-}7xGJE26M2K+xaGC>S<2dL`MdB^ZN2>Yf!j`Idv@^ z)zHCiL!w4bSdI{SeBvT<`ZnHVcNmG*@9z4Q6-fa_4_|db0SOMSrD{q%dy*}$p5R~^ z$y@5VGi#_NkI3upf~eoOm0Iw@N8SFs(x1avtR7|Z{*K4*l*Zn5h(nGLfhdl$L-yL! zFQyLp@>*$`g}I!^xRvj*GkT3!H8R2uxm!-l{csrFI-lO-P^>&oPot`!`)~aGOBC-B`+59&Wm z_hu}Dv|aY7eHQKzdSvbP*iU>7(WD)#2~mx=C*tWQQnx{WP9-!7hiO5PZPK`1`jgQ! zZr(yccI&H>ygT<7iQjqRgk4{y-)Q{bD47rMo3Q+6G&XxhpN~Brmv&m9u=8i62pyiX z*TCB2CDX*!#fl%RE!oK(nRTCazp}z^`qYykMwTyTIdVv*t0$CE)Ei80$!)XgHvNoI zdzaQ9?L>0L3-;FDVC6$n%`YXcIVq=uaEf$Edf^0 z8U$|}`XnwX8xGfvh91~Bgm7r`ks=|is`zVQypRyX+%8M%OY4@eUu3hp^42gjInVdB z9Y-NV8ZmxnnklwSt}men%ZI%v$vc^?m)veo%J$d#1o@ar?U)j}O4B^W+4okbhYe|@ zSYV;8fuZGkjgy?V%pVBw;c>V%g?VjFAgLt)Q*mvbbArUOAwVw9TLCUeJ*-?Y7M!~?dgD(_aCLAv_hMkyTK8{MW?=ZLJqSD!2 zUkW@lnQfXsX#J%xnKa+#Ae~Ic#fk6-)_pTHL!-eWj+B4{RWLQy0i|alQ`U~#@;yfK$_0X@7j$*QO`Sk`(kv6189uI ziVfk&)MnQDei$wTunu6cuu1Lc620Evy>Z$MWs753Ppv42ZkglMaa`XpcS^L}RK3li z?7;U#cO^!r(aaML$C10*8<&>}AY1^64Y@j--q?*NyoR!FsKy}^o1mr&S4%2^!SN_z z21>a$)%QrW%H*xG=AR!Kc=oh&yXlW4&uQbPofMB9q0P34TEvm72nvdqtQnUbQ&${R zr~P_||0H4@?z^3qvg4jM0Cw%Z?bxy9xFmkY`gr;4^9ss;UV0m&5T3vTlhaG{Bm91K zaaU^N?VeU9{Z=?0ax!ugR0Bcx*iOV4hBfh68AEn=sb*cjf}9FGTnbw8%;0EVdTpcG z;pxhe=|a6}`l4gTw#C`kGaN<(iPKtFWyKb10v`%b77Q24`sm!%ayzACIgJOK_F3E= zZl@tk{7G(%lnhe>C3(Z4h?$8kNpCVnXn1kLNYDLP+6`zF#sRz@$zA+`4A&FhO>JBr zs*BeLq`mfZTYU*!7grok8wrjyd)5&km3CaG=fa3dvr`~@uK~zq*$RiJb+yxqrqk4% z+~%JN?~(_ATH?=^KgDgF+NT#YlS}F=U3*d>OueGzU;E7Br0nc4Py!hpmGd>M|I7{V z8ziy>o;n<}+ZFF0(BQ^hHv9Aesx`+zGcM`O%hq?MocnxBM7ljLlCuqy1)h2b&5sC& zwNimkKsyrM2#q{OQw@A%I(tl7iR`#?bxNLmsZ{1-g9V!v_x*4BO^BgQWMmPv@joDp z7VIS7X^f!>6J$8ZR8@dDBYFIFK<8+36SvE z!lj?D@;n84#z(Z4P^NBPpeQmyd1GZvqP5 zsod6@y9yiO$?g*#?Rs2aiUCQY1fl5cQIE*q&?IvRC&9sv*J(@L9Sns=Q9&*o!#COd zDFafF<<2RL`@XCYm1)x1woQlI5g5T}7n{)ymHxwhWgSAe%kskX6%$+Q8AP4&xfln* z2+G+UEkT@A8i#)#3_tCd>~XV|lZf$z>-}{JA|?+WXWsDB3^M-DqH$@s2nHe9*qGUa zpE6nzPJF%BZ$hhH(nBi}2KV8AAh+ebW{mq31mv|{!T(Bd>!XOHG+;UBz!t%3N}KBS z`zDt^#^v_lm~XJ!K+pyaG9XiBOUJSG#p~f+Szu1F2y01Wkv7#@SAV6h?sY*D{Giaz zXfa%>QpYZ69~1uJt)RUA6e3&`72PPSoBo~poqSXN&_XEvnQMgt;sm0)yHdF(qsgLDmG+?j|e3P>U?WAAn<>B`#Q;3M_DQYPG}9JTu&xBQfx@128jK5*Rp_bc+@ z0vn4nnZF`DpWyi-Bd46~*=Xc$@#j1IH^r# zyist7Mq+{|nF;M<3YujF%{~u~XRGzV($7tTp=E%Pm~%bA5=ICBSwEEBTkhizJG~f6 z&=Ni{XhYeR+}PlO-qI+KV{F2;RMoD#n)BfB=Kwk6^$$@uAMYZ#Z$`4GG}W;hs)CGZr={0yMA&yJHT|0@iZc z>SBdvWcas{M;*PJhI|(M7McJvU#uKvc;&mwO5Or9~{xGe90`$ z5P4UlkH{6uT~rFp$YRaQ90Nk1U+L!?#A;~?St=l6!|A2gtbQ|HqBuSQPh0kh#XS13 zXHIM)S5$QQvwKlx+-C7cZcB+|G%T%MfP>?~m5R8U^v9-vStQ!?JSofKq5<gHVew>mIN4kRX_q5Zl9KqPh553h=KBLwG;TR%rv&y3@xmU^L1CHpJJw{WUY%9)_sj{yEHXE(jF z4~0C5>JrXBIh?vGQq|0}&-YK=3n9ilOiv3x5_jmb(x#@n@UUOkx0HvN9ljP3DU0ho%ghy`A$ zNE9DBl9Pg25m+L{6jQZO4!K+NnI6d#~ zjUMC$G;9!l#Zs!NYhQT!PATYZxG0h-MLF4-p=>|Lx!}LE<15HpUqv+!BPh& z29bxlCWB21$u-q04c$;+FL&R5B4EEcoQ^D#r!d90mgm8;)M!$wpt?R?7Bz85v|Y*5 z>vqvF3ECM+oIYvY1rTyt~i{p)gz)%EL&M!ku* zf3d~!@Z{lhmT7O?gbQ=EUea@B1>|t78UZ4>MDmIgB~CMO z8K2fDL`!SNIn?kc_0BXAu9|m(r9SD*P<_)n-cCgnh4XjCV-a2-K%zOH z{27qQu=wj?I#+7O4JJEQUIuyxxe4Od!0(H?Cu}}n7n>~_%;+%cE#5HvVt-avSaV~k$C-YXD z`yz)E2mXK)lXnDX_S#GKZPF{08#wT&e1|`{fY*2dxI{nPk#bKaa`DpB?xfzSgb=#X z$$cx4%ay6Kd;U;wlXG4n?rSg3hCE)B8tbQ&>!J@z69CyAFGxDyoMivnXio^N*gXUB zbW(F6wunmcF#C!Uxv;FQC;eGl*an~)j-FrQDNAW)xm=6eIluV^5^{SY8au6_ESzg^ z1W4lvhN3MnL+Dbc^|ZmP(Kg;XUk!rHkcCq8BoOmGKN#E9Phedd&@_2?{(7I&rjML{ z7QwrYsAvi?ZaNG5zPNtK(o!>1XIpA;;QOgL=4IQ|Qb@|dsq)Gdcs)iMD7&5{0;4~Q z!}KEQ*hSQP%=+SRZxExU7Vhdb)~Etf>-LGD+DP|Eh;6XxcM%|z9O$4TyC~IqAmDBIPy*qk;E?DiXY~(N zs^m9dSrV~r`EPQ~9~2;7f}dB9(;OSC50Yy8ApsYVkW+Nn-aQ~F`UoQ!^zsN{h)*t( z>`2B%X}>ic{y;l9qTEbr{2^=#69ZVS6mvPe78 zNGfQBqOS{@L%*q#|08f^{AMD4WoyPgn$@D607wq|J{IrXHJ@}p2+ui_erN%-yeS0$ z7NgDQvEMRoz(O1=({SFK$fc|_JD4IT7x251&k{uHC6l^CJ2@<5+*|kdkKuY0?Z(^x zOAFw$e8JbZ3yt=zSbC=|&jc4@ut{_}F=x>>et`ZWW$#cjEBf~6DTgk~OzStfi0DRs z-wU}ib#|)58>3W=H9*OByihLRm$8qv0MNbx`oNPf0N5drhC_cQ=6{0UTUBElmDEk7 zj7S71UK4(y$@^IrGtP%MX6vj3Z*!$mlK{;ZYV2F*q(jd(hv6jL{k?HL8zDAt7aiw{GtH%<9SGP4CHDUGf2#vNOQYGa-G*EUaqi-!i%A>xTVNl8H3 zEvL!R`g53$rm$o(OBA=znEUFBx6RQW^u7&?WYQEMoW#C*p$S@nnv; z`?_Hcgz9=?kqq?#aj~A22F|2Dr$4pa<_S-_H6}bOF^zyi51aK4p+w14j#%}&B01p_ z>)x=`jfNHK({&)y2Pn_MjyqI|1=YW#EQZ+vu_@WSyPpRDVHegaD702 zDMN8pDpe&+-7@Nnkcxs>cD@p*h--a)QOgHUe4|3|aAxtq7a@@6k>lw?|Bd1LXxx`V z(Pg%zHiwUb+3beg6N}Lx8IK`g)1V;C+j)J2r*u+T4RE{f%8Q>z+g~mVy*&ZZ%}j-t6v1A zc*5)F_sQDmZPmxi(ZwD4l)rgR1gKzlS__$ya_8>qe#(sj&dVO;*dF`@f;q5xn^)~@rQ9~iui32M)F-(nuLJz zS5c>#I|3(-l_tl%o7B%aA|aNWtr3`C@cbnBh|+&7KPqb$WQL+B8Q);BTgkWJ2{ayT zN~?}iq*iD06*cZH=aND^8(p?{*O{v5-_v*=F%% zKQaH1LLm)7!qlHRd_S8i8l*D}WLJMP=sMY>%b{qMTzX-bHHkcXv7i3gLTUzueJ0 zgWN>Zkt5ix^jYJ{nQ?}lm5(opk(iIq)(Ne`6Y|X(X|9H+LAKxPzNfqs^P)*CzAM5Rpv?B4!n0Q z4<JgZPK8Eq%%Ia+o0kz!R_!9Hk z7RuoSZj~rblaE0uvRz-d#kLD|Iv*x%kj=EgIE=@jR9khTNf_l<$FPtFT>2s+liGmd zN!;t>MU4cko|sNAWlUDLD56@AT`!1LnbVc!s~AA61uaJvHx-xF?iZc&Q10a}MCMGr zwY2CD$!EhnJQHM}F%Q|$3j2s0a34j5nGzL!`kC8h`$6LnK!Q@@Nta=|K&Csl5D%&e zXjAmH$}rS6dC*H4JkjRiN+l4xdJ&VUfU>tUombs@Y{RFPgh+HlkiYQ6RhhN?sr0w;`drJ=qy0wg@h%Er>pRY0an4jYRvf=+`6}PE@yyjrd z)vO!8;-4*=$JYcZ3-NISd+FVm7UA0w`6@$ZlgT{a?(b&lV@Wa#5@TOIMUx7#y^Oi{ z0(Ay6vo#?-twyJd}EKZ%-Vp+eSvAFHgoMN=D@tbs+=f~R^ zw<0QRfyDW`(>Hu5FklQOcW0g-qPDilf^3`%`D(NzsfM^Y$#EXOIk?E!lIG; zlE|*MaM>ic3w#lwrqR$AzdD{20E$?^bX1EKnO!xqB<^4M7ij8zXqnV|^TJEJyb8Ra zi;?fz8HuZBmw-X3^M-*A5A}dRH3ET7yUPcCCeU5`Zfbd@Qi52AJvL}d?e+wldY1y| zaxhge?l1q7*KPhC(K_Jt=r8O`>sU(K(FZW+v;0rD{clR|pK0i|-(3HtO_VX!2qiHV z^9NBx*l>A8-1YDRfk){=?e-^C7?{{(mgka1UIEYoh1A4XwpB$dK!gO)FX3Bin;MC2 zYiF-k!5f>X2nUbm+qQ?^@w31zBZ@feN{FS5R}xSQvp-ov0}UF~;UeO!^ue!BX^BSn zz)>Y>7h&~s*qWFy3f>JfK<5o7d%HMhYSfis3*3$Kvx{VfAGmwuWb+meO{RG?lYe4) z$*Gd$`L^Yt;h~R7$<*Ex_VLRYlw4jx>#LiBDeXGv?{+BR1|*eG{%3%^%(xKL!y3_8 z^6M@B=s;wCY37t8{%&_>%svY`YmxN!s`EGS0eLpN9gE`=3N{@|$2 z+2nwK*B$-s;jSWS%ZaVhUpDrtbqv97y?sJ!KPqtwMJO-~HIc8>r(Yi=D_o3U66ko1 zKo&eX3w81M#ylq5_+r?-#VEUwmuMR6myR zaAgCs3PIcr>VcHK=f5AY2TFm_Ap^qg%?zp z?K@pelDILFD)EUN(IcVXT1bDFiToJ@cp#l20ZY~eL{&Yk12*3zq{a@bd6DnJCF#Zs zhK4$R0;qC}-Cu0S5{0nO)|k%P-ekW^->Yr}5fE_%UO?{1=MV6{@%ZkkRm5!SR*k4< zI#VJyTwpxT&k-t!H~a15T%6}BskCmC&&k4zQ4ZG& z%qsBwov|#qm>z2$xHQ|r5(p&7uYrA;XT{cod3Ir6g?<*|ecvuTSeqy8u9A=W)ccL)Ompys0giCg9z(BGI!!5w zHc~foj+4l}Rv!V)AHr;Z&%vK&^9=g!E~{j^-&#)#-W#5;Fz7=-x0juXpje*0GQ57p zGF3vJGwi8+Y5U;Xm~XZDpB+V(8r??)U}mR(lWlE|;QX(~EWB^ti=+bzHb4lpy+V!M zLGR#DTIF<2(ZNYe)>TuiftQuRYIXVozAV9BItx)Ncr?c+bu0rrDNIc9wav7=B`-)F zC$-ugfM&zKU(L6z&^%@m7!Ix7M=CkAzBDk45gu^%1ANhTxG|bqx^!Dox^jh!_;Kl2 zuEFe%ws__Q0Ev5%v^LV}xy&n7HEME@B8s>5nX~ljtSvSjT=25~D~kM&>GI_D2&~}E z*xSUQ`*t{<9c?kTk`#u+F6HkN{baUiZ|pA36ATq#wrHDIsO6q1>{oRnPQZa1mMx4h zxqX71>Do`LmKPH?1(m;!*TZ7|q9UzwQpjkffrq?aBX!qI&_Q|yHpxs36>;W%GXkz| zvSGS;iO%;G5j96lA?J1_hC(*S8%iXeqFLNr7W9okgFK5&%inD_Fw9sF_^TA zspbf?_uk5d*X2@vbOPfd79m{8W=CBb2v=toLD|M_C94qzvV=}-jsSF0T+!_ys z%9&f#_@KmTg^9cN90uddk*U>~VlA6zio6_DM&oJ2N59-=rE-S`=zgip!NK2}DwK2c zrDvp30yMuRq2()8d@tUn%(z&xi-X9^6rU-6pztI?=cMM%3B zYCe@zb6JYMyVh#3$z^k0*j(46MTy;}P@pK>*n3xNwXI5dBC|b!hSgo~JUvo?72W(D z(8Mb#k=~_7JUg&r2*IeAc<)08<^9nBi_DBzJZ?;s-uXtI(o8x7#*D61xuQeS_HviD zcH*XMDA8KzkOg}hNQcA`iyNW|!|wwT)ce$MgGt1v=c-Y3Wfc-y%1ay)U8mv1UPkAU z{4(K6z}*baWREIz^=^H=u^DEbP24@WyoVW9(A<}_x)Q_ohSyBSa-4ucKo{`<{pZ3f z0Fi}2pyj^~0Ijs}S4?IVns_XO5;o7k!aOeWcBS$paMp9>3*$=$gj-pq5^y-3x`B9= z(&^@a)Gr)^TC;fZE0t=sM0wB+7NMbuvr&RkSC^H6_~G~R`(~4TCBm#<5$u;R{NY6P z=BsZVf9%{(y{Y(qhA53bb1+@x02krG2Q{VuU#kkQNO!SZp}9dSIF4`i2yE87DeP!i z*qIEo9{;MYmS)JSEN0=Asc*PNa~AQrWO&kkKW0bmGa9OR zk?C&bMr-=4>(e*y9PPTlJPC38+g~5x>K0(p+nlaEn=n8U^6E^=ov!hPXP7DKuZUL} zxf9r(u9%i7ehY{e@N+7w5{n&pjL~@q9r5PB7w;L=PfCL`_tKH?HmrPb=9q&$X;i_=$WuPrPiX7M+M59!lP+4UMms`%@nT+GMO%jy!$?i z-|Uw5b(wC~y5);3|5zd1o9a_!4yXmM1%XVfwk>fdAhFI-Bln|L^P;6He z)*T!VEurm;^l|vo3(gzuEB5)%l}}s}HYXF}HTeq3@b+cFDPmg-wXXTfmHPG5Gnf;E z<5>8W3dK7cK-8`qf2H45Gh1jaEpuCwWP;jdI{SiLtXNvpv_+S+6Vd)$($GPSG-(RI z>%J9$pQUmcaLHqM^tb`Udtm&EK!cGkK*MahuY|*ad!+SbqvRtVOG+5NpMwkGmnPE9 z?%XI%N7_DT&a6SpiJVb~)Z!n@&cP(1t3Mlv(%CF!C8B53T3A$TzSF!%J#%0@aJ7{B z?=A*5H6kY%YL8bh!$i_r;zWUtEB&FAQe*h;2ui|{-w!9(hpV4P!GJd8V8-)u*XIdq zhuZ`v}3%>ChaY%ZT zD>jALMO&HDh2kv5(jv`9@7cM}r?7}XYd$42FSfjHm8uMr$GJ!XQDkX#X6xr>sotw3 z@ieTC%gn#~P!Rnu0-3(Q?}mM98N{S#;=@JNy{dVOv6PL1(R z{<*irpr3)b5(?W#?nO%FSsU;Rm-YX*y+c(0Jxzm*6AM5SO^Pu%(~%TumoB6L1V9}H z4f}I#Y#K!x62h<;rIM2nCk}0y2V^vP!yZE{MI1UDW)sbAq^eQ31N`_~v&l3n!&|Pp zfm5N+Kt9lzTd;gzZZ+O(6RVddyv9Tk1!xTYbHj>#I+!Dl=3X5T%NNCp(iEP3X(B-u zF$dCjnuvh(jC`81)M7UiAjv~fq`_$9R`TvG-J$obj^w;uj3ooe9+y(69Dyfkb6X>C z@B^(PuxVzCxvc!Oz#oiWmCLbO<%LdB#FB_H4+O;^SVZZ) zjc)NGaf2kL!fdLfy<_vGUzwVH>(^j!DVL7izUBABE$`4#kd=))oNx3;^2+r&s#l3r zPZ|s=C=w9*Bz&CY39#dm`jplDE00F2g#gF(N+yzIxauOwVX3e{52tI?+G(vM9Z;>YLG1tAb5q{NZP7sG>14H2W=P(ssZ4B{XAD`?31sl`!;Q zhs4@CzQxV=E^RL;XYa4P3B5sRgJRx3BfSkZT0aymB6{@eg0{sSeNijRcRwYh_Gzg0 z^z#qlON=vjy>_6U*tJ$qZb6M=OUTd%h+$Y(8x9whEv9kUE?hK;ClU}SyUX7osi<&b z;caOLh4Crd2IHcqKAcRm0LDAb?Sb5ryx|=_H3L1|s7FFd@m_7{V&wl}lHWi(Kz+ry z{HYxT%932W+ONv8-o@D{xvK5xi-wXlM;Lp_KT>2jKBF_m#aNI0@-O>=mI9hA9-%tlJZCIumJNv$Ou~HRx56SR zk701(3pIIxWP2}4h(+Ox!F1eNZvyva<%e=>oIoMu=fmvz^gy%EElQAy+!`k1#fxeP zH*bYE_W(s&9>-J{;B*W9rDTJ~llG*t_TrA{TXAZCimq1Yo)2H+Fz!WQU-tdsC%Zkq zHUnh>PdJ$}UY)q8VRTzFa84erRP3hV;(-P5uvm3zk2l`pnSw^qC}-JlbTYjl1OX63 zT~C_*N0-E(UgR$Xs3*tcq&fSwpC05Z(#VS@Nyv}u?OltU|jAF7e?Q9a{_I^ zAup<2%J!YX!Ifn&f}gGy5`mADkK#+NfN?wFwmvz3OYAZJ-XHE09?SPT)e_BSDJ(8K z3A_Lv*ldNCh;zqC0lV}P<#mHr&sUtUoSanuxW2#gL2J;nW!757aB`$!KPL>q=+)Mb zuOcd%pBF+M3DqX2B~sW(}5`d72^ToXXb_RgyQ}VhLxW z&WgN)?G1hvRlf^&)tevk8`8lS90T5aTGOSGW^!uX2naiuzntgaT`p+xeFJcXJ(_F{ z`^S0IWGwzuH^8z>}8s9Ki_g11H=GAJwt9%u@+_JE}&1&gwxkImPv6mT9oj( zTsqBLdkrSDE(*1~g&qg@>uX3ACc|GWABJc*9(Z00%)UySe8g(qpv>*Q9Lc@owfT4TDOY zdan_q=`dIPB-OgQ#g$OSt3Ow4#q)(FDL0EjoL@}_V%60cokUk;DwfUTxpBLWaF9w9 zJIVfk?7d}BT;0|!oZt`~g1dVM2pS|fL4$h}+}+*XLI|#b0158exYIbn(`ayahi^aU zJ?A;+-uu1(Z{2#U__4d7dhcGWUUSYd#~Aa^P_lt~CCNh~Cv{*RY{|nP#9??lgA2m8 zUk{7jmD){>jv`y&M#lH15uDp9OW2MHiHIMr2zcHO%a=L+HhTsK9P&)v7_+pqx&V{^LT9IFVb1z(%jJH!LPqL+c|tU62nF9Q@)}*;#`+ zM}L(HTT~p7y?)~;`8l;v8@Dq=_j+8EuZ+uL?1@GpMWEIK=hCp!sC*8!&S?M20M2$& zgx#AeTu<8TS(C|7H{Lv5m+(40bNjpIr5p{Qy*56b;#n_sXwaKeZx>pSf^Iq7*zSLw zbeLdjhIOc_yNIS}fjQ{q+Wz<-sT_N>UJc({6?E4BY)$PB;fR3DIf>rByKXVSE}SiY z))h|m@3@O7LB5tbGaZjSsG*2GA(a(w5h?Z>7wP$a;-=Rh5Cgiq+M|jm7iAVPtWwAl z^qXZ7v2|3o`HX5HqLjs7<12&181JJ@WI*ux@_1SYNTPByEYxw8uK6l}VUtNw#%C?; z9IZS+5XJ{euCQR1{lC7x|Me5ps_+7Y571(3dnsJ5J$DyR4=H%+Z8Z)5xDaz<8t!a^ z^r>eZ=56gOrcRjV*+bM#79nO4JHL(;Z{cz~i?h;$2+!6E7<54~-q|`@1$XsQ)%mxV zEV%+gV*LtfMbb!hm6F0*g`eEtD-j`9f?<0!?Y=|q+$iEGHRXn2Hvu+EfxChXaFVfs zsk=)1uji|Kpw8NaYCWKtm@n+l(IT`B?lo3EZ$3zIp0#~QnfN(Z089NY8;-i&C|8(T zD62ipLqQM<=)5f=O&I;BG_+mF#*vt{58gS-QnhPV*$#}sjAwr%rl1(lzB2l=`pv|8 zIEgjW@^Xu`VqIIILNU_m&Nuy@@7ys*p{KrZE>~m&2XlnWa@yiNmBrp%WQv~;J75c> z&q2U$z~16+OC7dZcV%q70Fm~8JS<6j9D;LW&j@om!k*iI9O9M&Go#eoE_Wu+90?=$ zekFDxuRJ&0?EDDTDkm&0HyDX>9ne{oX&Zq*n%Fk@Wj^HuvPsj_sRA=CEIw6 z(eMVpOE~HtwII_yaa%-jPhG^!wg0;HI%a5EbT(#|>>628MgjW+X@`nlRFGy66NBTC z5xYDgMQ6Bw7l4@n)G zR;EA0)jRpJRJ!6wKgd550x6y1w)yIdGYiT&aSzn`0y7`Y292b$xCr+KzLZ!#w*m<< zubLT?fWI)xO?Hk_MT@CV9Eu4Po7^A7n8BI#^sUCdb=)DM6ni;4|4N<$gQtASW~pOc@^-jHjPWY9D7a zI1ZwW_lz>W@r_{v9lV~fI{V3wAl-Assil7W zQMp0;xAcZ=G*mZ6y%rEMzlS05O@ZLJlc^>}_Zb#d{#*@YT>zy}8w2&AWngADz!#;2*P0K;I=wedsNQk0rG%Q0P@*U%YH{`)6=0tY`b z8#6w3h`Kd2Ygb?>FJ1#{Y-U)4v^V3@(93IN=S(F#M8Gox>9~_7>|7W0z#l*<$LYQj z)KfCAwQ_(BRG=EL$B{#0Z@YHQ*67Yq-unIDb0yxr6U&-{C8(~sOZE4G^~5a~yh%Aq z*pHw1J@gt6aa}|nyIc6EKgaqOH?D9vVKX@S%?6Zb?C}=K@J47&x(=aTW7+0FETV_^ zf8)P(93M?G*r;0*@hSi-ZchH#pM>KaUd<}lciW6MyI}C&-l5=geea=Ro`BEgWY=9q z>6t4)Y&@ixB?L7N?>$57;T%tayIrHj>83u~0P*?L4ySOesTU4U!+E^i zW!7!el`56zUB^PQZu4o-l7bf|@M&AjEK3u@a{F^x*jkhiWtLQ97vCU8q6S(hiuZ>e zp2*&i(JH^5J02;5g+uZ(CyZ~e1VqeN7sC`&*5|9b+WvSYm}$P=d^^^*Y>2sV`Fi;i zzgEKF_8yG)Bx0bHH?oPRL{lBM;grQ++@wUvp;H(g*!NqR(96wlcF9=@ZjyfWRsj!2 z(nS6rm2jsp{U>jnw{|xf(PH1FIN!2)omog`zU#&VhhIQj?cahs&BxL!31?!C^88_J zD(I{yHQ*vwCzT>o&Qh`#)LlYe)@j*Jy~9I_Lx>~^WKiP!dp0UN8M89d)^Du1p}2OD z=OL-SDA;%Ol%NFm?>pxGFEY+|#e>o`i^^9{;~BkSTzh>RTuwi`f%;4MaP^&RrJJgX%=MC`DIm+I~JG_WdSGG{6=m(>R4G+@Pxm zb0BtKbqS6+M}{Dfq8gF9CDREFKF;sQHQuzB)vdnug*17k4UxgfJE*o(5o1wOFyaiT(j?n4qi6fZxj=FK7;XEz$3eOt+QQc3sxK{WOv*K7gKEFA9^wg=vl2&==jK zxca!(&mtw(>lM$!b+-{bJ1QA^HGlUk)i5zo=&YR>JzrqyML?Ad0%{1RPGgmSB_uC# zQDoqXutPgXl~PV`FN}XxwHyE537TU4mZF7&V&iEw7p~1~ACzMwdmOvX^<6_89ny*2 zF$4@CDA~WuzbuK3D8vBk-XW@NAKth(DzR`LTncKGoB6uB9)MKrQ8x{>1NwhbAx)R_DTr^oqjv7T;du z%v?AT7mPv-7wc>-qlm9ss5;j(rW@Su8wGdy1H_h#iI{qZ9G?`cH-i<4?ysdowGx*J z;1&6a;hGatw#{xngNNa#e=1a+HP~K~fqJ*kHo?wiB;ptq@^($&>)|F7dnC>pYqN3g z-g14W^U19YXY3b@A!%wVm|ycoIgE|p#KC-o2j&17`;4MSD&IEIn{OFp{8CHbXa`)m z7SCJGHbTAFrCRfe>?7~>JgUPim)l%y<}C0e4SrKDX7x(9#k=4}vb;nyQK9cY`*-lb zHb)rYHzq1Pv@rR~S^HC2gm^{2ZmQMPm&1;Te9SP<5``F0*W-kI_jt_^Aoh9syVB3n z%k(7^!EbtrR_2Xk`C*V&>5R?K;Y=wS?&LF9F|k2KhsPb2dNB0F2$ih|9cpIN)v#+Y zsnDrH0*dSXg#|Ww-S%rqM9&n zj;-3$nnQh~UPE@x4KI`E9woamMq#A-?04HOWlw*^P^Ca~SV%1&y4p+qq}fI>3V%A=rBrhK=@KLKz=t@v-75 zo>ltW&Ira1F4MurM1AF-BRzv^94ydYT?tmjx);jks7Lc`XKAj~Lso2a)OIw9(_v4d zSQ3#Tf{yh9a(SYYYIC^^m$ib#bpkDY)cp&@L$5>l^uH8%0R}6T!o}I?`yp<2f`^PO zUPPlGGeu9eQa_};jZ!MPeZ6aD<^IgAwb;k*LOykuMPrvx90sJQKE_n=M4>bGZQ+BH zZ>A%E3)BuBS&hfkS*gN!#D)&UqprT6(GD65v&Gp`N~eb{go!45Z7ZA7u4kA@Bun#| z2blXuzByL3H33Ut4`O>v=E7279`)2nG-G}WEpZ}NudFNg+m~FL1KR2$oCcYKHulxB0f z3U4AUB9FD&^*?mvgxDX($USKk=-Q#07Qy!D|oZkoRG;KmxjH{eF}K`~Kj z>i`+tY4j1s9zFG#y-R{WO8DVe<)Ms`YGVBENEwdP=WIE5;9%amylbVsTrIvY?ZdyZ z86%k&2GQ7|?S)xy(7q=b^)>>z7V|h4EKDY~)$6=SDO#J8>~N%n2?@1#B`ONmv>R1# z>OF&SM{_?&%Sn35&R{pUv|I(qhHit#Mh_JeIW@}$4Yxwv9`?VdT`%8QRpb+mAy;UT zQ;zMhC zUxK6W=FhZcejpLBJ^$N7--IeWO$2$wyNBJ6yXd;{bOf`}Bu381*cmkcoFHY^!6pyq z4wv|BQRi?P$g#0MVz=9!fb5vX-+LCAV@^Tv&wNB^0^v0FA_hzPZQTIf`J8lZ=Eq=!oF;3Ovsezl zrv{-O%9cqiS{N0vUjn?_c>nv<5_W#n4QXwmVr*L z>opLDa`kP4y^5FFiKl0Ep?sz;onpp3n)dU;L*Y79QID*Ydx-*bj)BCg0Tt#B&*>NN z^U&& zBLUnOFInDuRD{TwJSzYGOv&i53}FvoJ6-=bHWqZB`U1?0_Gi4DfeaDSw>73`8D+R~ zEI?&Hbo4H=7}00%XQ*|&=(hhUBuzK{n(DN7;}pSomnjJpjN(i|F8_5b_L#o zGsHT}Pw)}tG&}8!1vaFKsz|IJH8ue-fpJXMp3_C&yIo05d1Yj(x8gr|+pb2=jWqD{ z5!OE$vD_NtxQ#okw9Aj^RsF&p!&fw@nJoG`62C^ z5Au$7!?_ZRlv3??OSkdY*`16>vWRyPI~0tKuL9r>VheZZvE&n^?6A##OLBgF z>B9GFvmVunUoeH|{MD?vOhPt()78U*(t?``>9T-okt+d;TYB0UPAMv__#%e5XLUZJ zznd6sHQ!F~Rd&m)Qi1KS7j2+iWHGLl40gZ`b_pv2M|sj4F?F1D@O< z4TqZmp+bcoIKnZj`KxJ-KRlct2b_hQW>x%4MwKsiWRHgvcq}s<^r_sC0viE;9x3Up}pFp}b!#RhwS!_Qo z-nKY2`0w9zM^B0ovYm-`XDIhI#L^(1>k7Dt>K$lR-P_NB`|=F7+l?0^a#5y}*b@%A zftvbBt!Q*IM&z7UCP2MF=B_N#IDFT@*qyXi3tna9485-MEA$(Lm($t8m5oJcjh-i9 z&Dn+z#kW$Jr+kEpo&cmrE9gea+3MC! z`jV+!ksFV5p;){?+UPL`GLG#_Stp_ZtYa^qlTW(Z={#NkP9(4Kf^N9x8;&ZF6RN{7 zyD{M=#?y8m*Dq~Zl;n|}v=x`V7Lzp{ z{h}gtu3%hR#EDnr6W8)nt5ePXVt%Nk+!C~2vS2d)mEAy`Q0p8uNd(vZm<8e*_q;0M z1V~LQYm{D@YbhMK^zLAV`hN>=3O|a5kBG&2f(&Kt)o%mdmg0T75+aLyuDzZ*jSdQ> zxn`Ng<5|VL5p$z}EAIEyEXF5H$&v@ItE`5CsU*A2Jq+*33|fx)G}P&`%``ga&5Ou# zR?Tt`d@QU7N6A>ffR+^)!ug`4!H#(uMhN_>uhh(eoghOSD3!x-Ta8c%#5ia(To+yW zm6I?e<0;oSX>vIpJE?2V9-Gp9c;0xiQ{p;n(yJ>gbMDQaU%;7y%%t)3+!Ex-p|`+i zcosu2?T~9>c`#tOfzXqD&Ox!0hwC^RD~A_x;<`r8jDxM#1GPRXVnS@WK>&7EE?}taG~W&=GZb z25q@LgG=m1kBvLDRxf30PX@~@`d9$4CtUR)`12)WGKFf#;JnFwY?I1-kt&Me8IEs< zVobR4>ICO}GH!U$SQ=}}U}_Qi?ljZsE1EQd@9NaI#d)#i;c0S3UEBrT(8Au5dQuJE zUU<*6&BPq%$V>QRIM=A@WtJW9pVQ{Tg4sY@Ve|0Yb)5ajN2EAa1IjL)fuwFuA%>8- zyHfeN-M2FO)h+dm2iO}J8R*g(%PKy%@1fFQyY%s}1j;6yJl;VcrUd}6)UhDEy z%d`*fHLPu(XO_Q{inmSQ3`F4jPUSgk)vyO!05-du)dm{Z3C$`q!=M=ap`;8QF~tJtUi1t_0x3!xc4`gO<>5k2 zIm%_*A7@3bYoyM)GKG&YBu83n3(2iCW4bwpw2oM943((U5)!j{ny~b6j-x|$8Ke>nIDXn zlJd>Sc?9H#c6LT3ny1MAvoQ8y$0Eew@%RHGkqVw=0mS@AUB zLBs2c8c^>nP+4)|O}O2Eoz!-l8J+cOhyLwfV;gwfnqbC=MYS zm~P2zgy9_V_2q1Z$_M;va>lbYo%vcDJx~hvVy9Eg0Yz!slB4}ngLSB)F`&*Tfq;8T zF+H<)+-+3Q@1Z#}&u;i5Hnq0tgl$K!vpgWbHvFTajsE;v7G7c%lX8eKxzj}(CNi1^ zQ-&_cy%yhQ59fVRWF7@K=ld2(1$fmRkN|& zb7>LH68T}a*aG>qiOhGyo}`y}+R<>$1irWAqb|@UV+sNv!>?nt5N^OD6Qz&a|7L~# zsa?MKKC={NjM-IXU(O<;kvUElxdVGwf5nWK92edv6I!N=<=GAO zm+pK0#fd$KCFiK#@)HNEW9_ae;-`xBz~V6m5=?BCN~-s(#YU{@pm|wpgzUcn3qq&K zpRi2vzF9}>*u;pVuRTcDs>|fk4RZOy*i2M4=>!A37hZXg;USLXzc7*=@Sb@(crXu~ z0sXKxFbcR{GKZt@6hZc~hC3z7fieLc)RjGqm=VFDn!VJq`#KQzn|H%ym#!ygeL6CefAgrgsDbVA=oQki4l4H(Nnu+q#9tUM(ENxM#GEfQEsDMi!!%FS{~ zUA=N|m)8Ph5%8_C(nHUE5p&td!NY;E#>}tFiCcf$67OgYmp6fQ07IW^)MnvunTL;E zt)*#axw-xfj_cfw1SS9-;usIv1kj;Bra@VF#fNZ15^V1rYh#qIJi{jKLi9XlWGLh4 zb>+wB%~dV=OyU=o&{Zg}IfOV0Z0yIC11^(&e@1#=%{PA{6E8WE*E!@ha`%OElFZ z_Be$X!sOsT2nm1j`IZqL9xJ7xw0aJpzvcr`Ha`U%gA7x<{M`%$%L8b4xPCC&ktoOF z`LyY~GdP}a1+1?ZLf4}&kh)_&(lEPI&%3J|MmvhEX1m>pW$AXQ`fLDDq= z464(fqp&!7Ooi6RstU!d``T>|+4x3{G9efI_8-pg6~4bZBk6n$Zt`g2<&+I+gF374 z$9?dsgP*%~P`&VZI33umKl{4ZPLw1VQqA5n67>4W1ck84zYC2ji- zrpnirad_Oj_YQEfZ3M6`Z>eBz8nLjSJFMJ#n$ISIlnFG#1OAhZOo0jKp-0r}b9U|L z`IPsbLE)sywX~hx{=q}uyZN!$zi#D8^yho&vwSB?3XaVGljQi1ZYb7kAl-eLbtFUS zC)si}>}cy$K*>iN2&+Q*C#Haq9hiii+MF~vTTrGW?d6;Pn#ih*8@i`q-mX-TbtNJ} z0tOVt%x>gaN&m?;dk;*!x$$TEQKR&)-0AQ3{Xd)dRg3_~=Rg^URQ6vb(0{gP|JvJV z08dF=EZObLf4=d5T!#t+&u$bDJ^%i{yT`}B*!gHBWl8@ImH)YdvQ!7CABkSwtMwxN zxA*vqFZA)FS@z$_|NnMjqzo|I=5uLyKmT|4_&*5@|Lanq_yC`?`w71~=zn|bzh8c% z0+tG2U5D{M`cD6`4xiu8;{l)ZsyGtk?|S{;xW+$jhJpwz)wjH1!G8yqzaz{)F1mAo z&silZWlQzn-Gd1r4!%OvnEelg?e7Qt>jL}gFQUdHvoZYG_4|{5c>(-)_Y)TbmhFGG z2g3g!zvch&?eRZb;(tzze;oP$J~94hOZ?B4_`hjO1j+QYhy5?1@cma*TUdh{ndnmi zzsC__BDI8|3NSiyPLRW(g{^&|#voZZ;3wW@|4t0NgCezc?eTzo>hon0(!hWh>cJn96ivuwr0z6O(ll5Uj)CoiIs&T52&K12_UxL`O3vYPz z5Eioz#mM~S#GzlO-Y(jQ2akL~02;C=PO@95QOtVS{i@63*UzSs@2SFRxw^C-#hByB zGA0CMg`_~*!%KD5*B3(wMZQ8~y|?=LD}hX4O1C`|5XCUYpdKr$x9Ycf$K4#%dU?29 zt1Qk1!6Qr7nvWJndi2?}I`9T48jZxD{7VT))r8O=|JW&yFL4-!_s%DP{GGozgxmJ3 z7W@eu0mxpc67q>HOUO%(&7eV5BbiC1y9&Sm>G=6+T<{n2Sjt@z%&Dwax{xoB3CWRo4W|=-MFdKBVK&FkBYHR|I z@#MJUS@-&F!(Xl_o&nvh>xnXOF|#HjcydoF!QE*=BKG|Wz5#Y$y+pm~ewi^=`(~Bkg zfc?efd8Z%&p%U}Ff^}9>0v%S?%XC@71DG+5_3;kJ?eGp5&@N5Igv_LI4lfS3x{UF6Sc zku#jWtVMqncY6Nj7Fr?y{kyoEPJzCodbH@$wKUoo+%j7wJdoaGA1@*=5;z_YA z2&*myBg(r@*@(Ce^H)3dpiKe;(pdu*FVfd5JDzG%(t)hm@h`~DgLi!}k#>FlbVGz* zvqGP~#tgcZL^&PHX)>ULRau)&qceC)w4q8-@W?`x_edAV%&LWQyXvuC zSj0iPJf$B%J;i;>qK|CRgdiH#B3b_x?7(8}R(FHGAWBRfyKq9&>_r~QYU3~sEm2Rw zYylS~Ksu(vW{5MHj1_i&LC)rgRd07*RL_p5LVL2Db}bbIciZd2RYA>ejqtEIm9$soS-O_w>)OuXLPdO(bOl^<|$ZxcP14oC+7q3*y=Sd~Q zhLBC03fOsc#(mLd<`F{KtXdqmSia?fq`v&E*R(}xRv}0j1Wp?w=H{G#izKk&;0R6i zN$db2+piA;x8-_`eLBQxlqmp51HX@qSvV`*X}y~r#OSQTYj)7ypOU(5>bs$#<)dv`xP5AtHoy($CXQ@J;bvR$GhPXct5a-=STJlW> z;-mcTucFSk$73vV{WSlKWy{xVRC;7?5KU0{5|5X9gkO;f?I$&8sL=O9zwtBo6ZLK zL~!8J$x?L!mRH&9do!gpCjGJbd63!lK9_`R+?{%N@rK<&9OcyCT43g5JY?WcM0)}0 z?#0^RK251<_N=aX@IR_%DqmFn=CjP1COQtMKGz_dxr*KIvJ{F5R1X1CROcT_o+`Uv zF%f|lfDr=ta}|&DZAT_6wRyj==^R!t$4;qyrJCZ1h!1RNST4nOctW~pU))Bn!Ot8E zHQHP>Z%@TU9d|I2@K{;7wfY{nq2>>6*TO$x$JyWq?X0aOQHedsHpUEdHrKzyzwK(7 zdqZ+mw_96yty=r>t=bGRTlfuo{PwG5#0TPG800qB+x>=>aG?bdzYkk16eNOa&4n#V zk-FaNL*9pSx&$^D23$TUzu7mroa<$_0zF?vcq;mK(=z(Y)o_^KW9R`PYFh5DVM|qep zCEt6G$5;ZRGayEecqD5mNfTQNe^|PLB}ugePFPULUA<^(+&! zh&=lSr3sluYZ0@>Qe0O2&>Vf1f6p-Wo489V~ZuM zA8+5ZD7M_Zrd+)`m_GKA-9)m^VMVOZ^SnfxV-@)sFe5+^+65HDRe(a5KomL|p}?w9 z^ZkcmN6rjG*;0o?<(-A4!(0Tn`3;H*F#j9Z=FebWRR)hW@Dh6+FxlDk#5|M5u>(fj zgI|6=yw`8H{Yk`S8i}59fOAw;`Szz;%<0C!&{~Ger%7nOb2<6fZnJ^0?D9iGknWD! z_TA+^WNagP6Iynr@L!tqP`;NCh?S3eyYDSG1i>bTd`SKqhsXLrZf|k!7@YT4=(^di*;j2$b$qpVes^Wj8g(w)9<#DGk+tyS5jAG=w-^WsyqH8c@z&ij z^wB?8r}sO4)2*U@Uy5xZV1JNI_6k^6ua-DM1`QNZZ+&g}-RvCAk-ah%qmtvWa+3`hsj^IdIL`>4?m})$h6x;a6AN}<^%E9yq+owa{plgNoB=hjC&&gHc$BFTDBs9$Z{K0bKLY4-N6?Ya+0 z;(l@5T_66O-+NrxHeVRaUdHuu_IbW)q=ZLcK(Jb2m5J;X=_I8=H^!X(sb&D2)dTIX z=FD8R9`$qh=0|iWp&rTNH8}?rn*}vA4C55{Jep;e)Ik>AW7(|;!M`LNgDs4x1={p0 zzPdeOmmbA{Qlekiw3btx;*7`ZH#;uD@On^;Pa{ceFG2 zee&`eHzxol4OL%lYa*Y)`$?D9gcs-Bigp=3>dU}ky?goGkw#%G$f{XZ7f%6J4tqt2 zt5K@?np@=I<0c4PBYANITRLTh#go62gld+T9Uex33XwH{$vnLSl;chpG&QMMro=hw z7$k2maL-2$sm^#j3%&(n!(nkT-ank*rpTHbt`yqv?VhISJW*lzpJ=Gzb6>|)w%F$r z4R^U5$A!oBmZeh;QHJIOES|LYM5Yy0{oL<+k0uHu9@gGQwDAY5qb5H#3|Mu{BO?sn zU6S4i9|Om|ix4vQjc%hoZ5wpf+bdq`QkPYugTXSJ$(GcXpM||eQGn2G(!(mx)ZVOVMaYY4 zzkx>U;8nic5KjNL`3CzXWLetDCVk+Ky6wE-kew$iu zElsc3E^vqjE&}c|Ci;FQtMH#T;Q?lq7R z*LFV&bZLvRta_sC)D9PF!IpHLDj}d~FQPZNv;%~h{VhX&@LI0E;52&h zybZ|<9@7+iOqF%QxVljg$!Y3M#%Eq;$0mhd-~py=8E;k3a34pxZa%Z%BbE{euH zT+$J(nb?{$tYnR0v)^^(^o^6Ne4bwu|`0O%{6VzRV;SLJgF2Mn?;eM((>Fs>^h$yt}nyPIRLPbY2CT3#O!Ef)mz-~~ZJ$i{{jlMMjgt|H5 zb+XlruG><>@k~O5#!YCd-jejTZVaVpdrOWRJzzRD^MPl;sVr$Ze$jfi>}S$H8xTUh z2+QO=%YcL{O|4Fr8svlq^0iyuW0RoNk0dCs-e3@jHg>0ekz;DN=!$!?b7G;_V1JF% zw~ArXAElTpQhp}cbD9=zCR~_u`Xk@^(`1{EA!zbt4$Vm~v-X>gv*Ag%b*VPJYyXfvXL~Fi@Q<)1`B_ zU-?lqw?BK}{OZeSk;yTtg6vPC=q)_R@> z@PpItd%vtE^Nd_5RnV`2F`vXd-Vp63%b&|%T>$a4&->L$pQ`bymCdWecaWC%nmpcn$w)ZDuZjs|@4b^hTn~-_Et)27`WDNYvqevD@!xT0hsnXf{%~+Kuh0SP`CEFzdE$8cuq5}7~9DD zSJwM*yPf-zZ)_;D*J=as2|xpe7PH=}qD~gdze{A>!WeH2-_7Al1QsbBKq)jq^*yoQV>SHa_Tu(KQn6mw z#@b|ln{EcLjR`6>5RoF?`S=dDvyBSMc*P)lOT`hf^82U`FDt3DSPwC-j(#}NToO}tcx0@D^U(_+Ul8d}~^!kvq0zhfn#{8i>K)w~dH^U;+ za__o3CCfnG*ILkDQq$DWS+g5M52@UjW&MIEetHl@NL!{g&XM2EYBu9w|NB1aPpzbA z0M(Bks!teX6MF8oBb6yQln1rr9oTrwx%nxlWqR~&_=g40;*w-s7Y3k7^lRk3$qxw~XnL5Ei52z#TTXr+OD%Hx%VVj@*l?pu|k zxfZcwN&v3`Z z&U8Vs#p{pz%ITL=3uaJE;H_B-KW(vGF8fv=`?%Ui0QwdAsz~wG zYj5In!2e1QH1DbM?0O`#hqxq!)SF21wkn~X7@nqa>9j zKH3gTmCx8t^dw@d5|;0fswN=`^I2+8SQirTxH^FQXKJtT%#8#cA}4e zmPGy%N>RPU0#H&40+z4}Q^6f!u4n@0vX_Q*5wfQL^XH?}N$j66P*~&_gRRP6<9C7c!<;A)-n8u5q3NXqPz$5`CM`HAN|f zr_sVvw3$mFVl2^~sZCjl_9G>VQU_kKeE=}JCK#dX&b%cY9aPp| z+>Lb<;U69B_cwSqd;9DuG8o`Rwc_{_BzJ3p7ApUnH8L9w@W$rIFeK@R%A-d0NlY^w zg?i*+XsQGwG#6Bvdp(-A}((_gigV_N~uB|;4I49uf zmEMyg1ubMh`6%*x?^cZ6!q1)POa9494_1B!iKNMO|JuN zH!wr=>znoKaT%4~DK3m0w?$m&GRhNr^#NS^!^}(S8vrBYCqSjA9UzzH#)C1iYOozQ z@f>wfhurDzul}sFNRTWItWZU!NKQTR%OW!=?H&+_|#XTtVBRw_-+_2QwSyVc+LU8;H*q;_{yC zQ@CY#ZDwjVS!o|VKfMOP913GVLtyppHj~%xY*-9J&%O0!POdqS!z$S@?uPL(f-HFe zKLMA{a|$d(2i;0VJC_e_<~~R6o-`R4(a;psWLtr*9X))0$elsTcD!ROm=hQc=r~Wt z4!W-IS^^n~uYgw5xy+`df_W3H-v=~Sa42nee%OiKt=9y^aRP!`y{{(cGCNcZJ}PJN zAGtlIux{T5lfV5~7WrQRcLxDG1UbjJwf)-n`%m@L*8`7vt?MFCEe#Wh4fr`|%EzP&P~3V*Q)xSRP-LVcvRGtU7y5UR3C1IfZcfQ?es(aER$ zvU3|eU-Qe=i~qE3zJu%bYxQcbz5^0{v=T6JHqu?KUh#*`cqdM`!LlrgI`eobpwk-9 zUG3KMyaO!0 zXpy6DWI=*slV+ZOP#hBYYColn9Ca{C3=P^!C`@1O%{V>eRqTfqQwVWTTXs7hO_1|q zhg4m-SK4!dwGetL3mrRbZ&`K)Hg_w?WQDu@ul|hZ&Yh9fAFuctdV{^wTGDS7n!PVe zv^YJR*R<=~E{vVBU9HT9yga>LsYGMM0%t`mxQJ-Dv85K69xb4yNx#t zdtXLpBAwEKR#>UF%8#8tACw8vYS-1AWgYAg&wN@m_8^X#7Q+|8Gjs?e zl$^#*^#@-kDkio>IU~P{4jU!r`i$LEE+xO~ddlA8Jb${~ZfX88W#1LLmWZs(sePLY z9STBvc%J^ik-*GAFl~czde*6tkQWsM$c^GHMHeptuiN4#c!6X*Wa)i6vz!56<4bTJ*`DwH24EcAx^h6KmcODV|Q$Mnz;$nT`oK0z+zIT-Em z+EUoO*=1X3S&>osiOziNu2=ssiMK86;8a&sbW(> zwssEl7%1IF3Vo{pYS$iNoz1DZHdtu$0@{~-SqY*V!K6o`LLp4CR^nMnfqXz5K}uK& zcs3Aen$r=8Y)7klQ8e*G#BM&zj@k!1OmjB6LG*7ui+v1otQ?mQ4bO^|H6jV z$3nT+E?Nrz#c=!0h+r=VVrNt~Y1~?QI4+Vkjl{6uJP~!gjpnI=KB9dK(P~YOQ+lE= zctsuDV1oARAjdk$-h*#=tvm4E!&q{)oU@j^CPD}WQftV@8br^9JHgtu!dicV-v6-1> zEq9>Gs0R|Qlp{1m(uTw8yfw00FG5Cm00sk&_*dRd61`s# zU`f&1!7}2!b@kK`wc8?NO1L{X0y(lDOgL2G@hzjYMMn?HyPzC@6w0$c0>0LH;ga7z zD&)#IS~}JU`&`HHk<|ZxbbV!19a-0AkRZX`-Q9w_g%B)2aQEO27Z2|4?oJ>;aCd^; z;O_43HkG&Ep6;3X=Kf%@SeH{(r;hBiAK5?r9*jg1)ELw-!Ofsm=ff2>iEcijjqA?rPD zfy`}fS!TcDB%>;nk&L`Dszo3>Y-3Z0gj0~oj>$?cEftRSyCt8uaZ$ds}qd*Y!4rt+vqHC}vV zXi6Vdz`f@DS25c2tzzYUI#WV%n%hc7JvfcC+;hByG`Lo;P}OyBN*ZQZhe_&w1c>AB z6k$rQLF6yzyV>MEFOc%n5_Bxov-mCnhe#A(514b@cdLfRh2&TSGQqrmg_R(5kr>yx zSv_08rS?{JKe2pfNO4B9{O5hKzHcieMynx^;WT&Ne!OWp)iHX#A~}m9WO_(ZL23pA zh2J!dhto6!()kkKQTU=^kJnl6*CeVol~Be}N?TzKz(pmJhZdS(CBa|zf1i{|~l91RW0 z;lCU{(2a>{PY~;A)t{hZ>CzDt(vNP!_ofehceunITjBV0LMm79Lxq z+~{Pv+Uk~GXTANYYVgilN82M;1aqa?I#`8Rhn0<<1@Oh}%miqq{8rxg+c+%a1*)TI zl_&&OO6^a>NvuI8Ao98xKg12BkqlntsWO#ebes0rWt!uel68VSvA%b@^K}-}R~Ass z#|HR3b~&7gKWAZFv45Kw&i!yr&@X5Ky)Xvy$rf$L5`Vy;mi@dDn@Uo-AB03u_9kJo zKUz(0O5o_&da}8W_VaHJK3M>v+`>8DOftij_>RRu0nK2gRbXepGG^*^yili1!I37G zBEK7#CUxpYL!RLd{0AwQ$y`Oav8!N&Q}^9h^b)j{Jv7nC~mur&!* ziFp+<%rWpUcR*zl%z;XJjywpveqo%8FciceURcidY1U)G{kGPL5?oOeOC>)XD@Z}` z1vpn`S6CQ2oM<*!(AQhd7iGzm{Lp*N&C{$4ff%VE!VPV?K4NphODLcV%j-5XCbS4p z3N1D0#Iy#=mRvn%HQT-zzIpLZ{1_Dm1@RddF@h&{37ca6fsu@u2- zu0vQ10(u|OI$ePDfy14V?K;A>2{=g(F;3_GO$! z*=fI`c8X9q;zKlXp}@dIv5f6^@yB3fJVt+X?z)R3cuykjdZ$oY-HMdDcfvLY?Kf#* zux9K(U^~5zs(Cg5lss9yklR@R7H+5mG%lvt>||bkQbKKKRU~<`Yrz_SJWW}nCLew2uv%mfL$XC4GC!@+^qUDB`C6%>;Jzr~9 zHzUdEy8`DAdt1lV!^viH5Ij>OXy1?}c=O!vgqM}G0}Cuwep<_W)99(srU}wnjH#;| zJGCSf;eYyqoM!DgiOB69wjSDV_wdbwFCk^KviIev>1>|)WD&+@Ve#bIx(@;HmL+{4nXClqY)m6(TUVE+8>v~#EXnPbf zqgbQXv@FF=Z7U?7&Scd~@Im(V<=Hr+(c9F%>it;PqU|mEaa$HY!>CzYcoqIs8WQfgd|37-LDlR513&GyCX zHcd|J2S&UDiiXYC1ILaG9P0&lA#7w4uHU+uOJkRlue;gc5P>GghlEf@ifSumz@fkJ zBhGr)L`d2zbmC-SQB2Q z4s-xF7^;%4c+ z#@dWNc`47JA39-}bd2`H?JL%Jeya4mR*lcWHCk>QBrC>Xp~nioQ=VwwERWi#lA?Tv z@3|hdhFuYO9Q~Hu0F->LxdZv1V4|}}?oBtU?wtN!R@2vW2NF`$vWF`o!$`h8WzugE zfcbS;k=)9UPf>XMiX+d)?a{k)OuY1q9!Auf5G80*yAQ&%-;WLBKu6UB#)AQfs#xL4 zjIKlwHUMuf&ol5KPr8;X&!TP?a7#`8ejGE%(#K#utX6@2Q<XGB4ec(v1p}}*r zPSO7WI$LkX1t9;Qh=h(Yk2ggl^=!{OtrwhHR zvarrl<(6UY?#o)-_*rCgrC3JOuVwd}lt)C>W{bqI8P1Sh>gI}cwyi5~`KP;hPZwmR z@W$x+U%PlGI>3phbD3uC-pc9rc2p3M_$)y|o_%1naKf6F`?_3~h<_x?;`p~oKIv!e zDARhoaizCLb7$j*L4^Ai5}}5W`EQ7Yy~Wxhq}24;;JMA0M517x>!y!wmhMot_Euy} za9&#>bCHpU8+AfzjZP56ROi_XM=T5zy!ZPyl43Y+MBKZ# zhtsu=9=xM1&_B3T=;mh~)CA)?qw49Ss-QmU=(*BQf=Lnvp07;9U53h~T@#nuy^7T; z*zJJCVQF?6q+Vba{2x9qr=`aUPQ&-pnc4yC*YQy)#(Lsq{_qzl@)J7H_H=Ci>jp7M zJ}hIlN>A+;K|o&4aIVvzrXvx#i6o6xYMb%ivo6rN8g#GRG3&UN80gufxyDoWd z;SG${D3LX*1Q`9%=p^1e1I^wPmUm|c{t{r_=P#M4+L;>$qL}GdT`4)B@ckQoeSclS z7C5-$mm?cNkEc4a=0jF;`c~>-L?UyQ>izOjhZyGF&jM|$C)A%384VKaXr2V~y@rOK zEjV6j_5{WNL5C+i$Q$jx(g+z|AO?D)FS6KggyOeAkreTH#MPw;@sHyiR2hoeqo_2o zer+J}p50lJmF**R>bvJ)$^V7`_{W(AE+OTdln5PdcsfY7*mH2Lx-i&L)8Y|qqR$b@g zf^*p>z=?9?;cM6-r^;=err}A(K3OP&+II^o-cC`2ayR+iOB*4N-!Xf-;WMIh=qd%j zh0JDNYhAI=R57h_eIVdtPi&x}_v4r$AS9tNMWGSiN}^#@cXht860-V%g(0C78+QVH zUUpK4k1}gr6_!25kU+ZL!I{7(_1QL~1?;w)xjyl!kMPnAG~;=G&)Rh+;}c{Sjpl?v zt1zV>WU$Z=*~jzVq!ExNqDmL`7wUzugov~(a4eHX*4(WX63Wo-4G!=L&DZQk*>X3M z{4GEHMgr;EUS2KplPqDR7D`_OLUd< zrlOu-G4s7aF(X)5$ff?>CA0GK44W7euOep}FLhC7kQmCQh`5nzt+-;b`$hP7(PH|$ zXMVf07}DSFtN?j10$^Q>y0KWRVlZq^iSCg>_RqSM!iTapp_c&AibAy^aOg zsFU;7n9YhCZ7z}=!76kUebSfA1~}#PDZDIt?E>DC%>h5Lvc1kvd_yyU>e>{bm94Qv z8HZ9wdlg7iE0hM3Si=BI+sB(szc~k7Q>*Z$u7@wRE=RvNr7>RYZu;E(-+1ck+`3)v zB#9kp#H3jHUGuYL3wAxAXCI#pF6B~7^<2Y387)!dX0+gOn8{>IWluaBID|H=;Fe6j zyc~MZLOp03@(1vOW+y(97NX77Tc@9}w-TRo!zA5hahO}ZH@6}Xrt2rbF!x-VDU;m% z=2LD*TbPbJ`;TsmG!VV9*@>=r*C*mdSq@TJbFFR->{tMlc8+4bx%ei!zyL2=)%H>% z(RWAj^W(bM!{&R;Lg)pt3}(-`Zq`e!90TV)J=#3n2V{lsH5MO)Jjo4E56+}Z%ySh( zBV$=X7pbB6;$3J++r7@WXQOC?j!}8cft{fX-j3G9?gOLf zf5kDtpXmJF1Q7D$3Yu<}ul^`Z0Chv9a8D66VFo}o+0*M{;5Rf zo7UdS^2IL-5&Es}YzYnGkM&vBtFxO1<`Wf>5xDo?Yug6AT&WnUgo}-2V?@{~0YNWb z7N$$+e{iY)C~!Aq)?$N6LNGY?O46FfhEW^f&RdsE2PPeCH~IcbJvjl6)m{t55WUcz z*NHgSh&4h4%$0;fo^vz=;`_E8ZT@vTh|>)LkN>By6{$a$8ouqK`EvNU=bwvh!utn`2DH!uIjrIT-1}e6 zF>^wq+#Hb6C}d3p2TGIu*Z+VVvmb=@f0v!=G!f~{vfE_m(IzJU({TY3-@0zayL-j{ zy6XS?GZ`PiEs_`Tw$K0Xrv4UL15I?Re@s#T3r74umD7LjZ%v7!6W53=V;o_ZWpI46 zcxv}2U@opXmWk11w=M?fQS-N&o|4H|e^srkKL6Gw6#-}LQ84H7;BkW4@?Dm2w3lH! zckBneOHsUnSRZ7)A7vT;S>15D{Qg~s1?V<1VDP+AeKKf2{msVfd1w4hX-VRrvHO2K zUko}RaCgkCW2pRSvRx4S)Kt3{o%oCLzl&@EE~GB8w#%-+wUkB+qWAj?v-;v>o&GUy z^G&KC2Wq5fw)pzRpNED3m;GPQOfLP6t)E;v-ueG6F7sFa0nK9eD~*&IO8v7w{OkXL zwtyzOQzS2YOF3rn|29SV&ye`{%_p-1uVrRl&}lEAuK({_{=eT)XQAx3?zz6&aQ)5! z`HbcSFlY3?%kljCvYmfyG|40K2jn%?Q~rNK`Nk=r!&*Q=C?sZ{)iTe9yXXJ&Q+|^J zE-Q&&te}ir%Ys<+KR@W265w%gw1L-d_tIVHUFqie@5lT1@7TT!74QE)9T8Ik{YNP# zlUx}G_CXtI%Ed{{ znKrQi$NbP8NVQQ2KeKBVhvP7if4BXbmh*t&xVtK)5HqS(e?V<}Y!|=vE0cpQ-As!T zmEG5T(rmc9Z`kSnFr6!bT`d{TZPo4_v7ozbJMO*z3YFuI?8I?Y)cO%|afMg(vZt~P z-4|A$4E7OHUpu&n?7OOmF+AxCQnkOm05Hsw0ZEFK3T|zciML*Fmyd=C(AO36+$c$Z z4=`XN{cyO})M#}w@k1$>D?cPMI2s(!m(RSz5~hMTl1ZwJk<+R-C$6(pEo4nvWAMk4 z2=8o0O3!$#_?!!=h|w>fDRlf33QZulJLQo$pB4K%!`4NK>%mWrtUTPPcKPE21Mx}+FC|E7WAd8jvCyV2YA`hekFHDTV zSzZp-fi+|GpD|WFZ^LC34GAhFYl9}*b;>`xnlufk0D14>yRFOL|NFHl3Hja}YsxrY z4|F7dS8w|8Av&{wdp&S-UDwqE3j-93$(RCmB;fe(_et@g$4YbKEN5*U%1%Hs1m>Ts zl5|;AYEt6CfB|(n(E3*l)8~K25UM2P{lyNpXY0}L+#Y$CgD<6pPNf@Td>H@zN=yRu zSQals2!&~dQlF)4oDBNMFOa1eb(~!PUR+`};4}DL<$(xJj7jM_U@{sIAq6W7C=O)_ zdT0%tnzKQb35Rdhc}w(r9weysQaK zPC@Sr|2S~N{i!lzUi@GeAa)oZHFlT`&k#9mY)QAF9MDzxMK@T?OOOkC!my-lQp?8D zDVq)X`m+%`scG8>-qe)qH59$Ku*f!CA2t5NE3ar$l_taE4UwJhP}N9`Xqjp$ zX(Yi6P%9qcpn3V#?b_qGIr3t0&%Vay*>sn0Fjg=f2xxvfeeQARFm0XCksCbpQAj28 ziG0;cmeNt7SBuf}At~N?`@9QhIQ=F2w4=c?gO9>lXFbI}(`W+Tu`P!!BfEKaI_BZp z;?OArxse^2M5wH?#pjAw$muR*C5&viPzEcWAdP#xRl5aRTjz&eX+%1Yq#o=*6p`xz zLyT}$dywUH2{|q^Q5lht!x!&Iql^lT7Ezazg?!f@PQ_M`fw`l2ZOC$PEFa5D)Uem+ zREe$z5NRm%mmPj>2vdBE_HJoiCHxl;ApgyoeV#VE!028SKv1G%SsozXCsWf+6)loi z=7?d|pa%3SPQyDE92Cl|2%Hv2e#MeIeZnE8iLGDF|MYn}fX9v+A;U8L}$r+`;pVi~c z_ryEfl{P!(6yeAg@rb4*mFql0xJ01Ao+#}!^W8xU1ja&uV@H~MQv~s!RoZAc?SC%^ zAJ^hYza?kQu0C|4sf>uO2l{m7nLJE>mys&f8hAxCJiEio)73sR+~b;Z{0J{8GhOUH z9X8RV%>T6H}I|``X=|t5q0Zj@J zgX4V0tsTzdL8G`!{jlz=f1pvdvCpVmBev2(n~=&r7uCKtc^VpA0IKf3xNZ<0?wB-7 zCwC8R7ys#Y+XJpEc^f;L?e)5O`q1~=l3(H%P^9cYVRzAHW+SX6qh%FUafOh5L`U|9 zgok$0bR_oGlqE*WW0l~PtfdhdvDo_HtEQuT+KK%rKK9dRb+c*W zbcqhss#6831EJ?Te~7BL8LKG4(IJi z^D%9{d<@Le@w%;ynPE2~{K};Y*eo(BTpv?u)EHmT8L5(YEqY@gE&7PGzwV>V1LZx< z#q@A?_}-ibleB7u$Fp?sO`)aB84!}3C@&#^g`PvUM&pOAE;Pg>Ue9;g*zGUAWP4Vu zwN@&Fi9AU$mpx{Wm41k{^K5tj63G-_eeAt-i`s?By6h3U|8#qME=7XcKr=U%>9L{h z^L2IU_2VKW&J5H%9w+7G%4|a4<-{1u4nz_#swQ`y-pX86r#siaU*&H!JNK8mtt6_m z`&#dq`V*Qbf!+MR*7-ki`hO%nA7`ME>A=Yi3AldAGPvH`LAMc$?N1LStmI2#OzQ5) zs+lW5)<$iwxdso1RQWgn1!h`18HSf#LBpN?!^70FY4}o@P={h~F9&#iRy|+K*G!mW za#zKt!hf($ZF|r6g_A(%1|S!U5diDb0Ps~PPpj}PXU?bEs?89?M%}evU-j}EQC4uD zVzb!5C*jfr&^ zd%f&c2-^X-tb8Wdj`jVACmCCK0ZeQE0m7l?;dBAU=f1I@hiv;mhJVkjphakj!X@*> ztYn^K_@k%Z$Y*kDXwe4WNMIIeKJIPCYF0QUZ@W*RhIpk>%RO2p{vG=Ou_`GZMrZi@ zmk^e+pv7fZ1)N{`xNPLIMA{CC^LW`4DNp@|d^Y=I97g4@a zLDpyP_lj)XK{&^{2d#7QES*M1dym;XyKh$td>zLvKj)%goc>H4aBO3~S%JTR4S|WJX2X!}E{GPhm=cp(p88^LkzF;+Q z(dgn#>lnVP)jn2&qE}$*+u&M3JU9KR85MTZ)idA1SoVM$x&rUnjjSz_DP%K zDCxXD~WHK}`V!sDetnRKM9k)*M-)k<({iM`kR8Xg3;Hl!=o- z#GQ89xcDc;_(Vu6dl;4e!Nhw-L15kQlYpKp`BwO$dtE?B@K7JM!{fGj_U{mqs8`|xw`BQztJ8Kc@4dh7{%Jvv z^qQ`hm%eZtf`D}~Em%u^@`JO=s^-4ak5}{uFMAOrB%&afPiB#uEAn<-f=3PBmk;?4YaUD%msGxsx5b-Cc9xfw_>0zXx0m>;- zI8IBYRkXg(BmvPhB!}C|*D%);sp_irveH;IhseN0m$UmiQ4+NlxWd(Hw=z}KL=k47 zb#0NjrHW-ch--M*h*TS!My3X$8~81(g(-gTdGBzTY*)(?WMwd?M(Q7PpRf^x12O{P z^$!cQrbLjnD^en|?UME{oF7gUgu3}gY&knWT$xK*K{gB!H?2xx_EeK*5kRXNg=1H- zN}KW8U%cN9#7T86J8X6NUN%-2e2F-2*m)T}a8Bqz-<0z04ag+uYOHCX?zc2jI%gg( zu$qW{lkns=LcqC4Cg8e1F|nnRT}$KGK&v+b>8u8O+<{oVFP~XXbf7AwE?S|j2EqM1 zas-3N7GHlnHfAZ^z5(HB)w;AvADFy%6JFW%_IggaA=?mGcofQh$-z(%nwdoF`++?E z)b-f%m~lyjBPnCWeN%YB%o*fy6)NTNE{pAhRhQsojT;5km@{{uaTNMoTTmVU+b2p? zSR`@&$i$}R04L>*Dzv^Bl&T8khM%gC)+Wlb2w>c>%q1qWRUF}`tCmfT$kPF{Y;W~4 zI)l~Tpq}UG-NA1-S$1QBSuL9?Mq`t-<-vF2Ib#OiHpBc?$ z1R>xlki_!>=cV{5L-m{#^@g8qU3koJRfH_#1Eq`s68mv|1siqS0y1K6L{W{6;H`(J zForI62(#&At_JwqNdhr7KrzC}c(ih?4Y8ol0JgFtE|Q9oOH}VzC%U@dGG~ zY9KDH@eO1a+=gdv{{BnYd#q{RlKfsqmf?aRaD7W@;Vd-`iXn_d*KIK3?<}sA-(A@% zmFg?=KZOT*B}99xdN?;sLfWP;+bhSYK`^OnPx7YUBq+f$-%JI#MI#zD+@{>{f+nX+ zuswX5PH(@S2f>|k=E6y^@Q>kC;%M1%4SajV*obGlZF_sF>A@aE^LT@3_nZ`8E`!Cb zQmXE{a(mx3!y`QxUBPaA}gJgKpD8ZuSW`QN00y`W+7oy1; z+D`P_fITDvZvN>;8gDx;_0SonbWKb9c+}COls_qe2(3W{`2uiwwy8iZ0@esljCrnK z^(+hImWM`x+u}w`OB>P zW4glNaIs+kQRjw!ptY9*^~pQ~j$gUC$9zQUJVG6hqO4mi>L|xNGQl zm0shrb~bHs7pKL;u{|RKJb7M=?a5(t4!Suc-bwn?{l%UOWFp66CuWwG z*0dWCjTgTYBYePOKJBL0)n{cWfmY#o@1AYJ@Fjf54SojJN33QZj4a_Z6Q*KT4+Pie z$ecF0f>)C#uoJ!~u38a&f<#v2v6%-i-1SVvej`H8wzvpa{-Jcrjh`#lU|D294KVIe zDbI0+sX%BeKoPONAo?^a;<9pa5je{dbdhGT;d`@LzlR1uxq|f-ssF@9N&*lV#neKu ze#xX1DF!~!ZO8+%sAqOJ)x$;7j`zq>ZyxvQzrC)$jv;DzR?jz0!Z#r=Ia4_a;?H6U zv{RRdQWnWRvoLo3CPaWNB=9eeAVL_~YDso;*oAYqAq|rIB06F=l6LCI(scOFOi&AT z9L*U^aU&J2U+nr#M86ySZX@Y$nyI`l{#>tjKRcKP$w95~Ad;^nwM-dgHFL0j(a!=6 zf@iXsCxwFAn2W6Zg-oBDhiSIm#oAepF5k$nXTTC(W(htzFAWq3ybQM;2M7x?yZQ-u z|7#hAP=JKL?&kBNc1g)LgBrP_d~@m_9TXkY2NBdM+DNDQGlyd{(@gT$dk$b0NqkpZ z1(TWOTsXK6J)Fz|UbXBOpT>ZqkMMH&O>A$wi>yNke#pAX0J4h+SQ=U1e)T zMop@N_+aRh3F`rmECF68@!R!T;r>TB9#o5ik`z^wN@lu{@}Lw+>dvQ2mJTdcyX!5x znArBpmHRbiKz!`GU^!D#29>xA&X$6e9B@WlFP^0UgUA6Rp7{9a-13k|?5`lgcSbw~ z!aY9#bO-TrJ|gEH?q{_A3GiMWOgVL$tqNNo*LQ_XpuX$xspiamo^w5{KbFUW!zK=s zd}s}rFfAC8h<7b`*i%7)*5b%O=Sb!<|1w*iVg^+1^WBVXa_x5P_0FPzp*%?x*}3^7 zQ@LX38%362t?}IhTHKv&*r5a_ci~tA=b)VthZk6H$p_*%|4C21fp3JRFgfJGxUfsc%l>iStJxEZg|e2z z^hRWYY5rKq+{KGvf9@RF?P|Puds3kzVVVAnCKB2YO+;iFM;DhCEXXag z1CGvo%Q)w~^f%*Xocs+7l@TDdVUzBV>CNHA*+C`gNTjk8A0wR4#J4){{neV%iCVE` zh&y36U~1_)iR3NG)*Iy@F}&sQhU|tyX*N+XpCZl1!w5k^5pgkzMK3b2xT}09ioq1T zvRl`4fn^C97MHR;hO1li9BN2gE8!LqXcYgFh5du4l7zxLNB;-j&Cw9Kv+fZe1_hDa z%J`Abe7R9zDL{kVBR%n5;CH+D(kV>|c^qzSGTbW!HXGH*@!yACFxxqD8{3&E4CCGJ z{X{wA`fGXyaS#@nSe32E(Er?nh~Xjdluz^G4^D%Z5gv;1nlxj*Fw9WZPNw{lNo_eD zAqV#+jLykyG>anJn@h~bW>y*o#ap4pgvOtFpd8_F);0m4DGt?`RCgHmm*pTc zHpfz?yPhmjVN6}WH@^_JsHc;EPN$^tjr=QReTQ12+|pbtKk}pt{48O%%;g(i0`$aDTD1A`fvVj4V~;;E*BiCjn$w>HXJm2!@6y5Loo8> z`57(E?)>6r&DDA4=`F%`BzMqYr!P32T>BrK zMx=)7o8GC%b))hbIkG=1>(gY_Hutqy;GG(32dQ6^|NGerUEW{CRaGEs-e>f$4?*i} zaH=RQ6wrtL)tz>K6zmwFXe!w93PZ)zb?39WZ?L+h=#tJ$>41QSuBt8aS5ejZHrB7h zgS47L6Cg+1%#iQ>tPG$xwA+A;1eX||&8nxLX$%_O_W_}6EodjwXY=NNhNYb&a$x9W>{UQW#Xrl%9$t^t2$ol!cv$Hvz{*G5%sgH*$?NPRSj{vpe5{B${YDA<^ zB~eZ8Ai8a%lDCMT924$^&d{z7KwBB@>nVDKJn9bHBg`Rh@3lLG&K_Kc@n%+!jYq49qpW>PBD&IqmO+osjs zJ~)oj(F{(#7uAj|g7sg7GJWq%wk!E<0gJ@?UYWJ!Wxl>p-nZwSK>kuYJNQJU7P{ZP zk4ht$g-g3w9;s9%zh)Atm9SFP{x@}L?ziydN~d1jk*pw`{saqPb8TMo4v)q`)YT?K zm&;-oM@?9UdpBE}D(F8y%3T)&)X2HV^xq3=KI^s$UgqAgv+v7)k1xyE=w^|E(S+Ygk5`VkV(f=N4A9J9RBZ(_bkc| zN3gu-Y}M>haKn*h6EE=4W@D?={bN;AkRpU>+|lb*kA0S9R-%XT4mepV3v>mMZHYmw z)Y$*3N}`47|2Zs_f;Ha45N{ zz4^q_-kTRp?sQZe(F~?>TE6W-blP`FcSu}j2*+lMy+^?QwIiohP@;5RMW?2!S#O11 z#lkx|mdasfJeJ{B^Xa*{#bMflx73>`@SSe1m|N4!1GP{!9%}KL&+DV?>v^M#bwoN- z>)MLfEroLClDCJ{waDOhMLM)$tX%eaw$);@oJ+aUuO!a$1X>O4O4}Lru+sIS`oP5~ zgFrpc)28Bdoy7QldtZ)&%G{!(_LF9HZj<7ePoy>rVOZtH*9V+KST507LG9|*hJXRC zjMT`_;o$gw-lt3Od5uBO2i^k6tsP}O3Bc?{v7#zAE=-aYz&w#8HL8`a?Ni_3wutqW z@H6!#de|@X)fF1{y@PNaXrC-970yX!oa}f42si2qNV@G$oD;H{{)DO`hRSF`iJp1X z;XJux_S-|VpZsC2q1jfioJ**ozVX`w!u0om0?o*1EaiDwMwZW4nr^0Ql@y@qs!R^= z#sRL0pDuUg-d1j&)Gs|65zQ{%!N;2f^1U2e42n=+!;(FGuCp)!8sWkYnQb2o-?_h@ zrM~>L4Uv|@7s#b^t>}DApLqn!K+_Q4bz>|Lm<72vHRw&r8ljroSUI;%sX|Kd^fZjT zs6T|GnIxZoD!z0a!Ii&X_~w0aA96aiDtPGWBI(1w>Qbv9VuAUwxOKn^A+cE4#YHE# zV6p_K(xZ8m8Q>%*uMWtbp`DMspqc<_Wu~f*Yu_QhoZ97Qr>b->f2Q+p91@xAnPe?m zXNisGp*Y_78_Ijn-djynN4<2OEauDwX(B3+=m?T=qfLu*o66e?meQb~&s3Y!1?!`M9y`wldLZQ4z*+F~?Mm*eY9=SmPw8Ykc>r(B6ATi<7n(pj zklozV>v^avv`*i7fOU_zWf5;0Ah*{&;(9LJbR5lL27#KHlv>5t{p+6v9haWpnGg|& zvIsq<$fjIX0qg|ZjLR@SR^vv=+7A;I(t^*fPPEq#4E~LdP`V&(Z?)MU9UggQ7dhR= z*r*fei+oEHnsw=F-Ve?Z(Q610RhrXtWSO~H46d)wnJoajCSUNtbX1qr=p_wvSBgX# zrS}pLbAJ;l=%f8o4SK$P}qq>J?RTuAOoL}@UZA)t=pt@h({@$=!UF$h~j5# zMF9B)tn1~}^a2(r?H!)>n{(228D_FBJk3XOd(jNQV_Nlo)Zui}kDsmNJFQdGO9(M`S(8p0ln?U*#mO=FTea%(-o^ zCU38h8M@mxL>DeNW1Ut_yZ4jR4TQ9EOjbPfihy$;l#FlgkZh~=FRrtkoZux#{^3+V!h#x19%*v3k`C^iZE z{EGs=&QLtE%bigymRxM-E)Iu=hqb087TYY)Y@pHNPq%XHLAz{BW^EA^uq>?)BFajM zw@Al9S@};?7_^>VZJDpjrmr6}aFo>ynezMi*&b{w`Im0*42Z;G9k+){vo%mhDXc-n z8*WjS`%m`>%|smUIVY_9iiHr_`uG_u?LjyFw^GS>KO=v!0f=x_cGsPvaPb2hRRbSR zZ2Q2*WW!cFxZawLjmW;8bj}DKMHS#9$l)*c>t)Eo{Kfe+0^jN?k3G@|)+P>@pw1Pv zdq&U(kv`@EI%q@z=Ll9Vqkcwv5Bc1YG8M~$QzHe6QXe`-#hQR-vRjKP>MesuE!(#8 zfM~U9{2C%*Z*7~?rDZAO62<(qm$={b1hOE`1D#vN_QgZV}{H0;_g_hTGD`6~?|G;p)-snJn zu*T;#*|V1)F6wN@&lRUNLLbb4c5X?fzadH(1$ewo0_JevV53{bgOPciUv2M-fx|Jj z+s%BjK>5o?cM-!n;>2=$&T>(t6KNTgxBMwq6f9s)kXznSwZN9Gz!P$JrIIR zHHrNp_$8A;gs{W8M=5+_f2z*JTtR_1fnG<;mRTmZ*I6eN=jo-%4QfVp9YIzlUgfj5 z!i@@2phW6v%o9vtK;NZ`8zB6oGrmFeX%U_YEU`bf>CD~jB|x5{t$7z)HF=F$%sP8Lwkk`D8ZZZQmwEL)f5T1gd)Dl|KCpxpy|YzesdA^+Gu~?s&RL zr-%STfJ#z`*JBl5%Sb5#iDEUF_MG4!Y4lQ&4?1!wXHnQeLZ55xh3+LHaFXYVzcx8+ zTexlfGRq{dYQOM%Z-L1#PrOKSu&kCHEzmQSo7yz; zq73$1gNlOw<>#1(mxKNen~~o$4zW7$+nz2C7kbcV7jIXim1dQ!7}LLI>6NF;{&VE{ zo0dw+=8IcL7Rqnm#2IY>)!%ApnQE;o!(WM^d-6Q*b?Y*lC34r-sh(Jtn2_RJr7>Nd zoso!&K3P0~Y<;xyJ)DAFUky$fFlw7Q?;t+DjJaWcmpjBG$=Z4n#m~2o``I71qf+2U zrHcd`X8TTB4Iee$1gbYc67n<7Tg>3CKBwYap6LQGK(mF04(S*l@bdCjKbLQCTrWoDbp9i9~Kwg2+y83QoOxAJ)b|!0h9B3V%3L5;5A|rd#H1{&S<~|vTGDzsq@P! zvH08>Bt*Xn|3w3=N07LXPwT##?g-#HNSh>f?x zci9S0i9nya>NWkHiuxygAL9hQ$CdiGeQe$d0} zjbuE@u?YY>W>UZ0o0Xwgc^X+8P#CHOTEcPJ1{oO6WeZ1(!Kt}jZp~EtvPz5(199~U z9x2I&+A`lP@3H=1E}C{VlIXer+kS97e>Cv~oGn8y!qvMmK*mnyi+gASxc|h`y~5{SiQZNYHzuF&>!; zV!c!S;c8!weGDA}yNxj#T{(h?_ef8j!{o?hnjXtpn|=$)-ROsGCZD()%5q5{-DAp; z7kO-u8DS8Z%x)i_vvzxL+$ht6ZP-vr9hkHkrfi;=)eSanb!}mRs^YK>PFz8$fc<@D zt<+>uWX4Z^2nYmX9p8#Z%N_X^tCuIY%;|i37cJ7BGZ|TlA0D^NbxTacQ%6yzxO6w- zc1kP;6`*rNi}D5o7dfMp(g|FFH=}0*pRgFpIul;1?0?*#_kYd~T@RKf;} zuv0REJNp_?{WVi>5BbHc3^Ga)>{suChM}zAbV=nKo#?gVI6R>; zv?7xy^qVv?9k^33dLzorRxmMqa~e%#s;wjkMKt)NV+M=dJHuu+Ca~{b+%TKkaw2E( zu*2tU=#cO1y=?7}`47bM-_tiK;3wXZNo}3Yd_Qxnft$>zLp*a%e%4&AO)sxxh~Jt3 zg9wt(@;+dr=}#~x*?@;EZD`qcVX{blvP(uaK4~gWZav#z&~)Nh5icShv4LBsJ zX}dkqa?m-}6k7Ien3!GlWN}@cR2F))D#*%OEEf5e2*g-CS{wz@NFPs)sl_10dHI)D zqxvW0iQIGY5`ab?Id_=3!JciK%`hd|o~Gb)RD_TI-2 z|MQT&`m>pR!_`E^K7$mtS%Thw+!N=cb*I(2Hye6itUgV@a-N<6sra#Tam)k zJjEyCOxA*t;Qkk7Zy8lp(1(2sk^<5t4blxF-AH#z9U7!Tq&uXgyGugifHWLRx~01X zq&pAsZ0^$gxu5sLyOvAO2iKgv_sr}w^PlVbUFr%Fb#u$^4-93R<>9M=&n}T9&~u$m z3w+PJp3Ndw+hhU8fYH?*@Js%cCrGbFbPK)8`0=zSP-;}vZO~j;o|N=#r3m${ih?th003FU!}XvhU0o^af2v06`Wd-+h07$M_OQ`5Uq{ zCieM-bOfHlWJVNShwmc^=i)dE855d{`SKln)B6tCo6T*Nl|FsfC5ow1 zE!-i>55bRf)gB0?6DyYhXmdvaqXt8Ka1i6Yka}RwXTMWHvIebI=%JkZTw2g7xN1YZ zT#~Ga9^x^q6E9!08NOMXQJ41C@^!ZFTlwM2In7}Xoe@AHVwjxvn*5bH4UEVVkjXOU ze}7+F{b5XXQ~{_Rz0_E4c5MR|&1?lqNzbs2BzEsSTvAg1cect%0`3j0T>FWpuM?*W zfh#I7Ke+k!K**tz;8r~B&KC>z&E?{KAal?~H!b>)#T51p)5FCZM;EE%d(WetLx7Ri z_EV-A8{>IPS8=(niABGJ0H?25cbT@+l7>eG)Ee>g8GhgPe&Dqs=&jQh6WAsbQS`uo zt91Q0yKgsrAcfCx8ds8RV#U;gOKJaeHh&R1mtiimOrvlE?~BYQ|Bh3ACckUHIJzK> zpyx#_CKbfO{CtsJddq`R*?XhsLJ(Y4Z87?7?fXP#+WxakMRNgcLs#*^y^CdlGiShF zE!R}qS=q}f<+V97y|**|)j7C+Bu!B2t=;0j_RCbcwKeV=GRNNAi0PY)1x&ktg^~RA za+k(>QVYN}Bf-T0zXyEffNzN+m3>3lVoA{kSnQH|6@!^{Y9uVEdNgPF`R3l`Wl8RX z7xO`}4*<$DYcl=VEMOlN zZd~}@^V8{8iojST=N@^jqCS5|6O^?^=6fj5v5HfJB{=2KSp;p#d432mpEowmZIgg3 zi~gts{!e(2FCVwgv*Q_OFMVCB|pV_)Pq8-{OQ(rZ;@ zsDcljIc#s`nWY6zOnogUZV^Zxt?OTDa=RYF-rPseQskl$O-D?8(W$lSOA+qfx$}`# z=YW-NcHu^bcmbJ8+chvJrNC~dKs3-J4OxP_22)nA)bJqZ`IlEy@mI@_(pT^SCiQk# zGJ{FOwK<4O8EC;mwkTq zcF!J9QosGeU=;B%cFKx?HV6!aHI=xTEo@Fl)fcb%eTM7ge+1St?bt7Z_i{x8n<`$d zx4r0?o`sU3#sd(~Z@Fa}ua&YEznc$xq!z3f!DG`5Kb+_%F=|OJ6TA`0^4jIqhwrKa zu0li~k$N)}t3e)>52-8Vcl+QgE~^~l2xraZPb)|?*bOaCu8l{_%>|!!;ywaPC~7(u zjpb+)6323#DFz_=NaF0y<09Gp|1tB$ftf$B(gI|f+oEV8zYcB=--zYr21Ax$)+gG_+m{=9Q# zP!NU>TSo`VAjnaR`B=i!lUxs*LU>y^>UD6~U{Uo^U^6;v-*ScQE{;f6e&7pwL(7VB6t?I&ht zLZ-PjR0kX)1mr?gOV8eA3;qFh&;i@eUq_=Vz0bM$IP!Og6Cw12%s|FuGX1zVyH zq2#(|^uhZu1Gt^|6Da|kGx=}6Bp_%2^Iru&{>^yGDmyiKfv+6YXRqI#DWPS9rCK8^r39^Ez z*q+wKq_>(P$O<>7n`qY3l($&2{B$7%>d>nrEn_*B{)8O{)6X_Yl5I}gcDRPV(Lxr~ zvN-6?Q6gU`e%=8fcX(_z)8RCY7kEQtZ$m^b5a)y6e@s_D_He$EW#81sC5vTMzDJ$| ziXR9&9@{NV;dZVrvCyNo1$$?=9w2W??($%Pwq&wxU$(3ZP_{t~@ea<2%DnAp{e!TO z&h@`E1=_kMSGBDyd2_B@`JXBf)>!XyST2^**xqW?gg%ndFCIAjy$~he#AZ7g%*7^W z!c6)pCI3q5!}#o5pK*2XpVAu}b(+kt5WPa8_NF%~{P)6SpRw-3qx2vID^2!ven1V- zJX!BPr;S|7$m1CSa-VpBA9=TPH~5n)hCNo+1eZ!yftus6=nnpu%b%_#I>{q{5V6vu zioI72HA-<|#BVRX-+SWCPZ>_D!u{elS9)c5`ecCrzkhrE4S{?uc&e@g zpk?4@s+KUYQDM$7dC0Jd4PD09yCZgtv<9c(FialPR?ka-Y#!A@c}t5- z*pevAR1pJLl7~U3*N~I;Ywh{Y+!>}|AiX*Jz2zi0%hsf4l=Nz8v+>LSW0mw8BNAxA zFsK3O4DvA`Kr35GGV=QU!?j77{DWt>&3uivlC*z=6F~8gD+RMKU+n4A0xLg-^VTZS z|G6H2xe%Z+xoy2uX~5`08AC0Hw==%^_P)d4C|CvV$5ooyuQ)yjQq;dRME@*mh~xqC zucQyfa~QyVh#?sUs{jy}f7d(y^FbmGXkB{TRR8ZDF~Xj0wr{gptkGc5q@&##KM9Gc zg!vdELF;gPPN)sIm&&y3MW3vcmWf)PYhv&PW)VGcTyk3yM*z~O%+yw#7JY`J7kT4{(-$r1G=A{pv5i4xETuMD5S5Q%eMW%qDn=g2x=+EvGfG--xo=)vrba z{jP1Nx$Ms4es;*k`Un15l2<5sE| z%tD5FZ2$|vk3R9aetk7^U5xepU%qkrCj(`I_rt8`pXVw)1~*>VBKB$$oS7HxpV{Vx zM9lYTmUlLMCdnDNiGPg20zKH1vnz$h-odQDzg&Fv?&d6@K|hvl=tuJ6{!nT)UzB#i z?Pnw9Fr)2B_b^jkp5Bm`B(Xqk816vG`2KaTQ&<#LvB2!>B zv9ASrZ$z(=m-4MN%CtYOzbK1@-*xckmC&?1;C8F|k|@K_{s0V`jPK||KLO+wOYsbM z>9sK#DpagU?wg0_;zT$7w&LpNPr+&3r$_Xh=O?N7OQjV9bYJE0;)!(LjNLmOSxi^0 z7;HRFmy*oaCcTn~Jf!WcC3(};Hf3>tUFt3j0GVx@(ZaNS zfSD5{Uwo8VZxBAQWV_%xFze`BS~r6z3s6iL>~essN3jO!8q$p^SrC#K(OtbnNT?oXixE*d(-;<|sC{cxq8 zozGDOC91SVX|NVOcF`ll7%vBW{cbq+ark3B7AG1C5_cV&xsy(tPyAge&D-`HRMX?Q zn}^;Nv+r1NOTpH#>pRiH(bK4;M-$?*IMIU1F7C|+o>zw`bdhsrN*SSOs?oQLy_Mz% z2!yB0w2lJq4no_E>@Ttb53S}8Be`b&6WP(sdg)-swXs15eLqa0O#2q?S6q;hB7$3! zk%*QKZ+)^BI=*?cyRXs2g1g$Z706WbHw!brvem0qTY0ZLc`gheop)ckLo?T8KYU|Z zI{f|gR3u2m6H;1?Hj;Q2+BajchLXN1U3|m+>Qv+sCli)*!sYhnCDgRQ$H#37rYAo! z=Ax*HHQVQ|!*3>a=79!%<{r4G*sxu~&9|KhUgWbY|1>WOxmNMaSXDx8k6P~?N1ij( zf{7N1a&?}@hx3#5^p?=XzSBA4DA3;bT0vMU>o?TT6fzEll2iO+cr+i$$BWb;No>(p zez;)4+*5FRmnOlVWbwFe3{#|{0~<)J-p3eh!T7b@ z2);N{k!%YVLm7MY;hTL~!@Z?Ql)X9i!Z@KHdN|C7as=%bmq|a6xo=^I_okI#4>If4 z4-SG~KDqzAeq%!-Y|``LV(Jt8f|EBy-k!BJ3gr<%OjZ?)i6E)14=k0}rpZ^bzC|FR zr96Up(UmWv1+Q)&XurGq?%!}OK$23rCz!+i9j#`{LOX@;831fdozv#f;RbRD{w84F zRn{yAZBOZ`p8}8{EWqE0eZ}j?y@H-ENn4DrmKgdQue%-PM?O^NZY#u~HaT|5uuxu$ zccvoqsI`LAtaV_SnC**`;UQu(d4)xMG>1Rx6$BGBbno}oc4F#Y!2ZFQe^6$F)W>mk z6)1%Q7HWfG4QGN;#i9T<>`q}QA8<7$pS9AKNDmPO@*N(#dxfB<{D2nrs=@ZS!0Wyo z{nvF^yLgOvGRv^|0* zc!J=F&P`%iEdVcAW8P(Tm4XoPo6(}M&Rvz}e!&GZ(^`&q-GzoyXdTmZ%5m*Ovwf1k zSrNQq(i$Ni`3VLma!=q;OSh-J{l*A{PmJ$4JcuQ7$sG~vo@72OP;}zSy{FaBm-M=x zK~J;$UJrvk)Uig1qy7{FR*zPH}^cPt!^W5=y9AOgaTQcumUL>~7K> zM8gDKA)X1eB*BwE$caZ%c$Fl5_y>%jYmbtBMBvNlvs0Vh&|l-ny<9Fz^$X`*rWNd{ zrFYp)J*!o4f!j(7T>D%cl?HTw>TT0m1Wy}XGYp8a-R`Dm!{-s8Uf~9l8fZ1AfzY?x z`X7gs#nEPXniJ?i5}wv;sYa-rKCQkdppf?w6m_nyd!}zgYV?W^XSAL0_{3d>In2v%Ugr znKr&+3;T(liwuhRJ#L$6$|6EdLE9c;E*nTLDai7Q{hx!aSr~Xxya6D=1sKCgaqdfm z9LiMkTScKu|BNXFmc7U1e)uhnoCU4|er%t<#fJ-qukgp|h zu>cjWah;x?Mav});1a8#Zeno0U_I&~rxEE3P3lJ_edVsb|4XzkfmT{XFSlqKa zs4}{Ru@5s8-2#Mk(7(MmtrJz}{K0if#K_)ay?r_FIf(Ru02Gqq zJ8N!JfCbsWUVQ|R?IWD@_E~xi!OQnL(%cuu$W>UBQC06Ev{Fp_23gx4`sq>pj{yH5 zLvB?8UUwzTjP~HxmtPdP+dl~)@-4w}X$S8SnpXBd_A@V)_GaE9!a6CA35Ui3e#F6| zQlfBs8)<6rbFIKpj3P_YB6(kytC99U^ zsABN%-3bNat)`0iYy0gE0on7b^GvmE#I6= zn`NQSjs>W6UGD2H@o{+tD+5`-y+y<{1YGCk_?z_9 zeIf}mrILGY+TGS*$M2njD-Dv7Gt?5DPnN)+ioA_F$ay^hXjCshwWadKEns2CwJ*#J zI<#n2()2iFPe^-%-D-ygu%x4a>Vduhhi=_zC8N<~Kre+dyT^KDK=G;@~qA#4Xn&i{6BQ-hRx#FRY<9pc3*=*To6ZLbVHqc_tFHpT^fusb^Jpl#c-MnZT6LiNv!BxyimP=n3AMw488RY z4x_f=Cz32Cf;O>g)g`1Yakl7dWPDPvrn{*-;9s}lChR!mR%2;omMcxwf37Wh0eMqr z0lw;b#&a3u1x~qbeC_{Bk^Hhhd(kru>3%SA$#vjF;zOLm-=h{;1ZlR>I@)HbNqkfw zcy62xN^Wd1N9o@@2JpN2wU4K(;9)JqDQWj95|iaeFw#?7x!aJ_xJ+06W3s~Tg+NDz(WXN1 z9HP;o^{jcd4jQhcSBHqXCr<8&&N@*x+s{_e!sU^0G3}IyescmhQH8cZzWlxz*gIfz zjn~4aH)_cza3Gz7-?iH#z;5BtU?0V$E%i2T0dOO>k0#hmo5FR#^<4uSPi~rhbmVI) z0Psc`&q88RSmVGZ^Kax>OaPJyYC6x3K`Og->k1+5bCMDNh>k548iN@8Ly-%Kj|;it zA(^}6lV%@>MRsgzu-+wvBvb{$fG+cy??`HHK?TZX7Qs8|p2(O;{*%MXY*&{K(+71Z z_zVP}v{3mwBV!~(1&Sd(dA6aRc=AjVWUKHWBr+VJW(E^=h=w^Pft6N4RnHO85g5{b zEr?;>LioZ6J`rG9qGHPjnWR9dgk0{fkyGzRKc#Y92wNC`zMkB$C(~W*tQ7zWqKk70 z2(xm@ZKQpDE7Y6(^Ca_mQHbga{q1L=?K zTeW)_O6644e8uId`;m>muRhEXr_6vbVbz!^1wV)$|G+HmP40ca_k-RLQvmWov^NVo zyEvW8W@7k)e@fitL+(D&Fh`xAWdv_-jU|~>rIB-?kmq=~!vd%C>nk&{qlgI$#Srh% z5cT87C(B>z%|t1zo27B^6i~|Xuzi_kk$sf&SsxzOeWEEG^tLqi8q{cGcp{=E;QzDr ztJ>9arO{MGT-ez{WiFA2At9FW+eM4%~4tV)M^ihhRxvuL21%;!*Ty3BMiY3lVw zC5;6&GPaAMqnoN|aph-#!feEoRK=tn$0Cmqd`P&^LhBNm-0#GCVoYjR>F7#gP68W9 zsA(QNo@)D$R47nqG5l)5)pzrLlZ@YIafK%PPC0t&u+96+t@@TlH;cUizEAkxez|5D zP?1|7rBEi4X+iTpTBH8Zl>@j3lOR4c!NmgC>RM|!_!!9#H04Hv4w`ddHXA4!?FM-7hYt>?eE z--PcVju(%5$=~<&o*#eXy8muI&!p4kiNLGIolKb$He82zbVQTL&@-}cZqM(sAJnoN z4&s#$`%WQ#4%gjWdD4#PE3*FMZN$rE3KXrqM_xC#Ue=iU~`sq0GwtKKEaIo z!>c7yrM07ot8DG(@F2@T0O2)ic{M!sIejhHYm{o+TTZv@n=|S2c|=D*^6rsXwefY% zaRoBabhdv+wyWI8Jnh~WOV!*wAZLF6EmCVBHow<6N(yZQ&BcKFSG(iE0_^sM!mb?D zrM!k`yMkNYXsU&5`rz!##RpRC-L?c~)fL8M4l2@xukXk&zB=>73Z^kmvIPXVSMMlY z+7M0+(dmkT`-4SLT`$2wFOI%>vUfC;63it zO+<28%$Tp@IGI}>{C6kW>&WPijlrxAyXk#+m{R}b&UNZ4@5aLYDVRYiClg*S@nUwQ zw$O5%KA-yyut<96ygAv`E*H_Sd&h?~HeMmVhc)^vCI=ohrS$Us)_MR+;TxzORzTrPD1;(5fol>b?`fn48#rv)H1D8RPY$Au80cgty0J(%{f*ab?>K|5O=zBjLojmf0L1-~O) zaB&1o@uVZCO*&H<f7Kb2H0a1DGe@<$!Ibht`AlH?+XBDFvzL5XqSf!fQ` zBG@-_;+{pHp`wA)WNR@ePl3l9fr{{?Wf}fz@llABUg`W`M;jh;?4!Ot|Gt(1cRk|_ zuo9*Za{%l4#u5dm{auEPOvFO79At{Jp053ecA5F=Qvf38E#J5{QQ%Pvr7d14uX9r(^t{B&G%n>mwP03SMI}r+o)39afIpK*iVaCO-awn zr-KgmmAA36rC)bZUNE5*p#$#_c3YZm_LMqcxuA^H_0WwaYnepxpGrIomb4~Z1+&4( z&geM6ngjT3BTX_-6e*C8bMdGHZ}l)}IS=IaW$p7x;>=27zInq`H`?*QtHK~W6SuVz z?fmcB&PmjAI8Nh$W6l+ho0hXnqPprcvpq-b!j#=i>Qls0 zWQn23oRMHQtQ5ms(H5N%()>g=1kw3}+@KABpJ-o87yP5aM~VKZN}3>Q;=$iI0PD;# z`aeB%2NG?TaYS7sW*v-sCfMVK_SXXL1x*aAfnA5Zhopp?$+46UMpL1UrA$7AlRSZt zwbC#|`(*y3s*E>YiIFrLYP#q5$5Mt+Uln?thL~4zt`b{Ay8Bd>TpBPHEwJlabYy!M z0r+D8LQ9iWcn&^`cl2V9Di@(Agm!@>bG|>+UUEmsD8-^}8uT{C{qEVAN@38Qz?2hE1SWM0hdqG-Gy>r~L>-$`ZjyC%0-R zk1JG49%;neP&YNpVXBA#)xlR8m3by-BuGBjzzzw~k?5 zj16XF^j4{CFPUw#kqm9c;(pjIR=uqWApIdi`o4*)(~6ejbE*0_a!!lk8I5+LvoTsw zT*fX)lo%*X>}SS&yFWyEXz^Gq3F@b4LLH&#>RU;O3EBv`hQ}Pra8gql-c5v`ac&6S z9)qQZ>y*5|En4+pGB~vAxJUb^ZPN7F+O%zQTkZXq?;`F!Y1fv%4_w*+u0aX$X0#*= zA1EBe0fn2}8hDCQ5ZtJ%8SaK-I2Ml)4;YY5Cd&_0*QOExO_ zD?L>#Hu#DB#D!v2hOa7LarLOJamTZfi{x)p8?J=VfiY9g9B$-hzY=nER`I%P(X+{Q znlz}8r(#W;p<*65_jC4?RPVVgol>5W@}w;joKP&5L(Tl;xw)1W?DxE9j02U^8!~z? zH2MGN5QQhNGAj)?-VROzN`WCXoYF<2m|AnTL^ zJHPc9ZY35lWt@;L-{_rT>T!Trv_PPE^& z+vpTPpeSCp9{0HC`Wj3ra<=`@8~Xvj8H;$N>6mXELOk!B@+K&K2H||do7>$SSxJN# zF>0{xlW0S&&9bzC6o@pWPyDXxK}nE!`?hKUiTaE6tv|GBKwsbf0#kn9+cEmn?yM%M znpiC1rG-7WB{XH-Nl{+#5{r_i!oLTt;taDy8KLveCIajiKQij=QX5VfY#}AM8M_U( z%P^?ybfQp5yAG=ll|fV*+tpz7s0*xXcCjOyoZw9G_S|Ph8skx( z{`U?snd-@TwTfxe+#Gp-f4F7Ydh~oVo#XZ>Ac7QD8j8{yO(9rwjAU56UO23K!6uEQ z`xyma8H9O~E&2gCeHd6UY9m$`KRPWjazT?}N z;;>l9dO?Wj6_%(w!gdCk-!u|6L4l<-I+WRWzx^e$@A zPF8~SsCe2Q;7-Y>Zk8hV5VOjNrjBKhz7Q~^bK6%GA4};T2P^`;*t;;ZrBg~!dQ%tC*2gj$Q389|0=UDN1;i6V3q;}CD#MJ$9%K zag=OUX!e0L;IkxDYSSOw#=IRBcJ+BPLv`O0htrFpAXLV`i4BNAPDd%C|9Wu)a6~K- zfqe8yrnKXzSpd=b5^``M_ZFh_1fzC>b4%@Elm3rZMCAMlQX^zi_ix4ilTgHg5bOFP zx-Dbw1moBzGYu2HdduiHL9m|~Mxfi}#%TVxn*KhbK2z=5thld#c*alQ++O_HPQB$V= zXa=4k>t(@ll%!atP)`WzQhjg>Gv>el8fAqQ1ZwjA2m7 z{miytO2s9QlO$+;XCbSQ8!~G5&+B=u_9Xw^2{Wqy`zu&TVBbw*^jBmq81NzDL{s@= z%-+YoWDQ!KS8ify*|!z{n;=6k+A^J6KLXpSWkan z!xIk^phI9NjsOblU%yY(cAaz-vw4}fN5b?P94LYWwS)fEPjN{il}Dw93uB(mL!VK$ z?@52UUwB(4bHR^h_X@GEq)X>V-sGT;_X&rvzSkH@=%@qMH-@7BIbwLaD*xiA454R? z4{R&0zf%BNHirnk-GBG`pWBcQ1BS&7uQjb=MiPl9ItrxQFqNY)zQ$5b?iE)pfZw)) zvtkhD=SYX66e0JVj>Ihftnsum^#j)@VjM4w zwMpfa-kntcA)w6uKUoR?e1xh@fJ~-B&Gpx;j1fRDZ-af48c430KjukKFJ~8P5r3jp4uklU^_!AX|7{ zq?noX(M(R}o4S|Wu`4o@ew`|CX3YAuvURc&s|NPbG8@6pU*C_3J-8SNJ+<(f@o7R% z?HF48nGEEcShI~z(Rb&AAH1J+N8rXzbH7oDMdY--wb5>HOFe{MC=kh}mdZ(N^4Ngj zPq*WB?OG`q4uW*)yK2mGjCs7s_>*w&)BYUCpNF8Q3KvBz$N*qBCVOjuG7Ju5WVu=v>lV)oTm}p-qoHBO zMkEu9gjJ&`GTJj$tR5Db|97X340|1rh8Xr3uG zEPH1=rI9>zkki`I0^M8Qk_q6EzEy*p81hX=lcrcc&J|IRvgmyOu@(bxAuQ*Mvv8PI zm0ywyyiVoN-?WiC4EsE=anQL&^@$sx53&MKO4>%()O+A%63PZlIZT9s-9P>IkHM|_ z)*q4VHwyrNqPG~yiU;DCXpY}%0P#z5Jp(c9oGM$|TdAhfm}aCAAINjPR5(6kF;G&- z#%j}xZ^fme_RGg=r^-sNcudz`YFn&$k7SN)E+3vf`1FE24*(AmwCzgh&}RhNTKe%b zBD^u-u+%fqCyd@mXV1Do5#szOm4yh9JYmEn1mcSNi>mi@k>t}z1*(lxuO@xzrf;K< zNDLYr#&|zBC%O{6V@E_p0VW8Li2~rEL-4arE+-LeI;W9 zV!bLcfozW$#Gv4_em31de8_K(a$MR!D*PRl2<`TTJtv5GoQbjc&r3oCHtm^27Qk2= zY<-TndCV%cIsR4HSx|uQ#jjDuXdzYI@vkkkVmfUk{1hmy+6=-+%S8rBmCpq}eB2&j zx!P_~`liXk7=}e1UP%Uc77A|+n3J-v_FX|AroencL2eW=lG)vnR=5&S6s z0LX#Ki0)0*`cL+R^z=^`lhtP4osZs};RC38-0wacn)e)`mq_u0>9j?Y?!`@8I4We) ze69A6qDy5?y>CS=R;m{$!@N6S(IqBZwp~JHQqb(@!jCq8m2jp}_hf3G43?WK*v#fc zEjlS#je0L7E~wiTgYlW;4Sep08)+<6ixSq~p~!Sfi3@Ymz+pss9^4LpydBb(f;4PI z@lW0-$bHi)A^(iBiIRf4LhAeJaJWBvBqrPCKqc3=wF~ z=(2EdsyY(|n~U5FYV;_dAiEs(vX?BmazLY3@|(6y9IYJQ2iH*#B&EQRmeP7airsDL^y@dfaMHUMo5^^?uXq;}U`*auyTto3 z_gF}v|8f{C&068BEiIj6s!rWnOS+`i+v*-Iwnf>k@p1+YTZIgg1~zWj|=+a^{!fT%||(2>}PVk zcY7*DH|ftojx?!k2l20E^5$KEzp?la7U3uGF2GkiEc9`-uH0dhO*3a-pl_VEB2`6~ z>TxyRbAEv{elnQO<_rk^Q6jI>V7H@mhr0Ii8%KM)K|Yl)RBHJ0Ie(y_em+DHV({By z2t>ZZqHB9pF?dWjn-{0PQGE+_K3zXkD#gEU?yRk5JQfrBbuczEm^WvBHC6ZyyF|G) zVGhR*St$nd=*tY}^Cs@RY$sW^g8vRnpwU9%ip?v!JibS`y% zK!ow#tIIww`qZB{-fF7`06zrk=VpEt@7ix{+1^6?mRP>;VWcdrz|YPI;S+PFlt1{i zc%9CbIVc>7!ioa`bxZzotxFYLaXg}D7#fC0(ar3*L*&$4PUjlEZA>WJt>;0!0v1Q1b!^(dXSW7tXZ}Ud;F426B%R1xf~C<2XF58ZO+yMN3%uiO2AwJ%gI`Fq?rV*+I>tPFp}B?#@Ww{-x*2NzLi*pnkG|JBRmD`;d@u z4JUEcP|S!ha@&m$2Wi;{`;lRwjG(-%b-mnv|7r-21++#Ro-2Hn5^xZ0J2!b{BC#FE zXLfzWG;suEh1^T9UsVUWU^w?grehs@P2W|HsR8#`<}LIaaRwJwg>LAMCdkonzIIs1^N0@N(P0cW#O)UL+Fv`qBQ?#^Ef?pXF3c`!Efg_2JA&1RGp& z(E?X8K=74a?=te;1&}JT!L6Jy0 zU9Lk8biA4SRchkgqWlDIT+s5G5k5ZZ#z)VFj(ju9j=LG2b)>ctJ)y+w`*JkM6E14x!s_JEunl<`=Rc{arpP zhh)vHJT$V>y&zMnM?=$tDJ{@X1u9RLL!U3)+VIXKS;|+*$O`ZJ=q1#s#5VX*#c0H= zY@zq)Ziu&#;Y!~5K4Ivm}QDmGBM?ZQP9QTJ|TBe+pOkHs|IVxI!ugAR& zXOqz@U9exYS2<)u;P(?20DTedvOWKxPeK}f*2UHB&F8ty-GuyJ{@yFf;CJ<0vK&?@ z3(U&so3R6b=pW%O5UH#RTZb`M6jwdwN zX#0KXTV4d{>z;$At<}MFT;DVu<|*X+AeFp{nlQ^&kDfQ~!m$Xfrx^4xD1Eb}iF-Y^ zNbdrajZxiwj^of_en5V0Y*yuCE$tJFWP1te#!L*<*(ZB%bB|1a1EMr#dG2$BA(3cq z|>VduZ0EtKHEI z`e8&b(R3m$DcC-(`vdYA`5;;>5{a|=O*00EwP(lbEd?1)l&wuKTI^6+?{tghZS)A1 z$*lzP_Z~K%puxFO+(Y^dR=D>j7*wk}II%51zqY8WQieBFjbl?-)vO=KLo z>^eWiyA!(?F@h8FV!c!mBl)p-qSx-Q^a6K?_BrTiM}XU{0*~>8uP}H6%_~1yD65|& z_*er93fp0)Y!((P{~~I7aAR!Y?_l30tTz!t_KjgwaANumwi_P8sh7*EDjg_u5)PUrGYFbj~=_PoGP5Ox#frui@ z!gR&TuQEIpndQOcvk^;|H`hj9+2xu2toVa zWu|Mhlzz#cr?Xu2v1V>mE?KdhguQbKAMX94Q_pnSl3{q(Aau$pJd6WK9azEBRcr{V zY6Azk@cz8Ep2YbTz&Mj4{|!y6+At|kg-YC zu!{&5YCXa9lAd$w_dG@`a$|>d!TjqtZz<|9hCQ8+h>x3*^H{RbO+d!^b3N3^5>KAM zVl0!tTuz}TaQgzrDPIrpt&`Y2i|N&t&A@z{3Jk)@>_ac{nN;2cMJ5~UQr;)J;tF-d zkz0%=&3z)W+Zovjs-#@luN*lwyA!86aG?{&VaySe`a+_{`!n+XX4^n9g<_X&P>+o< zKL{0aylfRnn{|NcUpZ=sFxKG)5>@xJ2&=MP@Mc~8fC!EhL5Rk5vj`d>e=C-NX3C@V z8kYMqnbB1lc=B6rW7_j30f-yVuU7DDh;#)+*PVi1B76tRB#83`d)21>zifn^=M56- z+7N$!7^j`MKYU(IRdF(m@y@~afDoNbE6;;1`t@RRivQSbA#>{ywVFi|#&+O^*?jjF z7HOcb_GqWKUj#6}W`VmYPCi)%uub*fUL)a+|w~Zo(APfYh zB!BVcsbxg!aNOwtOc13)L!=gKis0z`EGTRQL7{?alG{mL=hh;$eGr*7h~|7R+t-U! z;e&MqqUR-vvODT{3~WBO{d&tMcK!jZw!E;xv1OC#RXR@vuE8-Ck$uh-H*M zNJG+e=JA3(i01>A)W*Q?#0w#|kiyZDUv`ik-U@?`v|d48n%kpK zwd^=&6i*tx6*uYVGMJz-3I0@AoNSSh{FUUv!v0f)?&aHrVXk*`nMx?5*ydMV5(sQa zrGlR5>eWW($;R(qd^@fYh@(4e@)xd1@R9vd8;2!;x6e%qIOlD`z-!GN$3kCO79yQ}RqI2Jz zYQEJwdYuA4%XOxMW4S9i$OK(Dd^{OVc=OIy-d7|_ zXV4((&Qas$j}&hOwjMxQ=Z?l@xMaUejV*>WWFDjM&hyR`z-ojQ%M{daw07Az(RBC} zxI)H$Zee`$l5noV@$`kbyh>Wn+0WO=h~375Z>T1tt~ z&lqH&ob^=E%6RQZID9PXQzGbjImohEUaj_-ngSeKrD2uC^J^&}#vFA`yQdQ_3#ooS zrXCUWcB>na^Hq~m96V7z3C*XgrB;ZQ^l4;FtD(`?-`ul7|A)P|jEbX6+eV*25=cmJ z4=zE11PJaD+}$O(yEN9pg1dVN!QI{6A-G!_cZa6oE8g#&$uo0iex0?>ud^0E8dgzN zUA1f1y|4QcxQ>YZn!{h2V0RTUb<<3E{OiMI)=sa3mw3ORo!1BZ9>2}HNq?A*y&Uy1 zCMH+M%lN{gZJ0U3^#VKx%qrzpVor&R27|IL9L%M-qo&7qjAs;i!eyWxP;#4#FPAXQ1+v0?mdr@2%O=Wr|6Y zQP<~{)0Q(mDIXKK;vad#c)p_X_O;!+cas|EdE}k(E0$;40>|!#sAjTxOTWT=9jc+3 zZFyu#Fv}@&&C{T_MAa@oSPu^85JC@Pg%f{Rs5|}(2Dr6o6cbXBx_C;!qHX6Gv~R41 zL{O>5l4mJpb+M6YMV_B*d>Iola}B-!GUj5juiSxQ*>=gSlrO+%`tB?)ogX}*aZO97 zRUz7X=U(tIciY^&AX+z?v1Mb@g?huhe?1=bey1{ za7CntT&hnb3`pMI%*c#epVU?al$Wrh7a}YUjCvW@RcRb?+s#pNEQA-I)eW^(&HPZi z*8tJs`%z!t@}}{Ob92u!;sd|#MWoLI(Qeo zA>||a&qWe{HEKOge75{eE1{!`B)5AGLJ;QKsqvV1gAEKjQni&f47BKy`Q5_tswQMP z=2S6!IXfsNfVs{Q(d~ka%IITP3;u)`w$>`l&mv6i!}dt(2dPB=V8osfoWv_au6s2(R@t=K<2{r-fSbHi@l%Zyr^Z4h5@Yj@ z@eN!S*SEu#Ym<<8B`&+QDD7r9g)vFxa{UK&PEl&EaH-8^yje=j;W>m?A_48#5E!fB zmD&Q!tBfzU$U7yS@txcKl8Hw|Z8nEX3_VlQF0~^thpcLwiMdjTLH_nlqh21<7VUX- zSw|DrGK4U4TcMZI`CkXIM;5MntcdQ{Mji{vaIl>-Bs$auT7_R@xd%alrnZio#I` z`WKPHg30aIrLnL|3-;CeU9nrc4O1>XwP>N+ocY*DP9h?XRXM)YoYc)8@Y0Rk&%V&H zxh8JNvIp|cJb8ME8Mf6iI=9vEGNA}lAIo@)LP_6NOK>uGmFSmTk?ZnyrCjOR>2IcE zN9oHW7hKIhPdxF3H#{~A%=d(9(x4DBAUU@L+eNA%YENJtwEkIgH7qa-KXf-wJ2^&H z$eBVFfqOV+-#C^(JuCBFRY_RmqwP1+PZM(^k~_#{!ova<5$xW-OwZbM$5IXawY&>6 zR*$ELt4|05*iSYir(ey!pTgRme#F}n^Swn%*silz7DuJ3VHg9zN z@RYZFEj8y3I#mSkrh|;jEEyVfLg$fhVoamNWp!oH#OqxwAj=O$c;mr6M6vc65Wnjx ze`x?hg)u5EE@{Wc0DnlMUq2&r9^b)!X9ZdeQYpRXzGeLaQn&vBB_H%!D4%ZzDW&0J zAPB4#HE}V>r^{lx3G~bRF|l)-kp69 z{Da0LG4s1v!4*Xx)(3FAi4bXQj$i#=N#SwMHP6+vGu(${3p_N(@Of!&tCO!l`_Tr| zBvroVEuBOgUaB+c{#J3^&M44miD?OKzV4u~_fT8U*md z-F|_yf!~R>ipwG73yK9@?w1y6WXFuwd~*-89Upg9SzUM3g#GBfpXi&sI67&(-Q8{( zlp0M0BamVF1ND6z)#5Btmq!RW<6O{nZ>kBCCN}*Bh8B+Jc}ZT~&{KZxZz%B#URmm6 znTr>vbGYvj+Al*28~UnUA4!uRr(^8AI?m5{o#p0Luh;RfEX0TE2CO5Zo-yHhF26io zH;mUB#&r{kyB?|zAN|!DSn&uoUM9I;6O!o--~Vb!-lsr~0tgduKZ4)A147&x24W}^-^=l!vA^tA_$Lh7O#^rG z!x!h7ZjwHLn9~=v=~2BnUoA7aA1d4fC)VtJ9`gqIh<~B#?ez{PD*Es)ZZ`UKMFR$N z7i6e}syyPw1x_)=dswB$v%iK;h4v(RQmh{Ts;FsZGJB4k6Oe^MB^?ji%Xzh;`;rFDOK4*a@j*oZrg{@NF%B23~hqM$GV#hufz9CTM9sBp3ptHDx6_57grgw)a`P)zvbr=_G zO&+}n%^Wt?XbU7~wx-_d$YW{~;SKaIi-Gp4dovP|Cv)VH7xi%cHt5mgSE0x6_Kq&N z_3x9vE&`s5B7qC`m$r*yd$VUqIqZu7z3M5jydikW_i|TnI7*{xyFUz2 zd@a@6|7!K}Xj6ED|4Du{jc-$}59?2d`r~s{Y}4%tUk-Q$%ZS&({)qaaUy^Q%gcoy7 z)J%hu@u3$-xu4mTf)mx7-_Qf?X*o=mA1YbCC`q{$mEFUZyp7Ih=Vl!T-l(4vJ}|zn zqyCn7=u*-Gmi8DGLir6JZ)5+$CCsejjXTIm=m|VMN>`yi2 z>~Tahpbu(Ec2Up}I8kO%N@km*6n#07ZCoYl?bW{D9kvKySGrkn9mm=7E2*rO%2yt$cFU)Vk=vBC z-;%#0jtKq33qVQF3-W?KKM7_hTNL*_43BLYCYV&BMs?NP^YLZSGNY5Z>9_1eTnv#T zkRJVOs*oCshx$>@tvX@4Vu5xy#bDxEL8HE^Kps|*ascTfE3S76Z~$x#S`uI zzL!BN73S#~7CA569p41;BQo})5a=6$T{ks$3tIQBcBE6&%r1hYtT{b9+In#d3cn?rK-1v(8Idhx{Adr>Hjsxu~E1M z{4LL6(Lc^m5@@t!9exkfl(k)@u>45pKa3iTFWS*lQKi#jPxDw35;-@A3r4pepkBd}(X&RkXXSd<6-q!v3#BDn zW>e|%1mN3G3CNV92p8+5@Z%5Izz=X|?eHCsUl-cIZ40JNX5YG>))%8E?&Vo6(siHaoJ7y!GSg$l4 z(}lEpNirAet=P-#&w|xQjJ8sf2g2cJL0#WuDz#ug9?y?>?{L1{!>Q)W3#N|^!-#RO z9=B2{D_xKIU_~B*$SdHa&BU!&7`Z#pI;UN9#nQB~7mGyjotD@|5-(g})lx7?ckMRJ z-vhB)l5y1UdqSbBWmn$O^qYyzFMx0oAE3`awL2NZr^7{~Qs_TbN?61134Q=Y(rMKy zdaGo0<0!+e5)9@%wk|b%%5Ku(v|MZC18fgr8#T78e7wqO+fB#bYj2t~s^0HK<~ofw z(xfR~-_GhWCiv~O^7)tw>x2RXO^J9~EQ-`K+Tebxj6Au_X>c_*b(9{gGWa;0%zO1+ zokLnyBLmv$nhSc6)bq8&lU_^40pGH=_-WcVzr2G#ZgvkTdqLJAlqpGUZtFXLBwep_jS|RhT~CeZzcvn zzHhv6=3(o$sca<~@)BjoN?BY;U7!!^OQ^C7zfbQZMbxd1_cTN~D9;*XQkZ4N?#$Uc z-PW>zSod0rL670u{O3&db;90SGC<2j&i#My>YW zg}3^jm`I<vt4!Lm8u(nzP!e$=+p}{@6)=vN(aAS=*z}Pkhb|4EuRgi7)HVJhEdCPF0d9!$~ZHUKA*I{S)o;G zOnM#g7HZ=c_`Vs%oB|aOuCAc|4op5dUL;ela~OzhT-^g?W2$ci;9aWsQ90v-6J5(T zdrX!ey7HW^@qR61r3OxTjiqyo-L*V4I;MrEvO)bT-qoP`4aQ%#9Jub!$Sl-amYtd1 z(?Eveov#Ra-NTgg)h9H>vY?^MQ&LyVI@e}?Vvz7>@J@IVkKC@_qKQgB>y=uQn1nU! zENB4puxaP5%rnT)&{rS7xcx%LvcP#0MuZEnu*pbDPSMerM_b5d#;z4#iEcaK;doT6 zR-mXN+5#=DYPE*4nJ-u7R6Pt#6)4ur8{oknpv>O)fsL&{xXM&ZG$jD!D$ON#JYRu) z_tN3mPA_EbT(V4;OD@^mGU0m3F%lhRWs)qppbLGo-A%u&ZAC5^qpcq(9xfYUW8Ve+ ze(HKX>ct)$X3^zI#FND>zc{d5ZH52UI5O$(F-i4q2#N9Lal=oqK(vKhbZhi2K{o%D zpMoSNJ?0TEs=-|=n*d26NaH2tl5{e&ZbMn##p3<_Ij_;=7QLpO)``j-3^FTFLZWfB z8J@bom&aADqMJ~%BxAHas>5}S-A%w)q|j2xr^ongps1zk=zPp{(Bl}|P3-}Nm!x*T z1oL7oMl?1WhvovrLtN7)LV#WL7XSS!o%Rjqb&+CWLkUH*=$u5uzANnK?w?SQ!P{d?0)Sn@hZi2@4;r8L)cyiPADYr zoi9u;Tq(q?sVN-x*z_*=_K{D$#oTW*rB!zpC z!isq)QZn$WvRXhpXAs`3e_XszvSsHP&(IqKRT8tKIK z!zEsj?1=Q(jjb=q7NQ*0Nn2EAhv8*|ZFW!?t8b3F+zz_W74X!I7CK|$V&CeUm^-U` z@dRZk@NQ-9*^X?|GTj4It*KR|0SdKItM!M{kYP8n#h06+`50$Y>)!t5_~L&B=a0!R zr`BJIfw{9dbZ3Jb;bp8)C)~;jIipWDNi%Tn4}Cl^Vn5XjCy-C7UED~4dw zOtr+k7wweH?|l)CS$skuT;C2uQWcGRGj0E45hcTrB0lgWs7;NW{QmQ``VkD(H`HV2 zV4fdn8LI{#0QqijZ?2O)9_O_Cu8%7=shomnc!Uh4oS0X~^z8tj=W=3y=<3A|3&5gxod(AP(D!?Kf^pHx1YD1RAyE~(bJ)O-gc+zvGy zwKRASA+8~H)LQOEPRcK2f^}m`$M|soas|^2^)u_aD0br=En5U$qZh1}xoEvt2g}(H zlQ5lNJcG$Z+xf|%l+nd?=CBipU9O>; zQ<4jOg);ZDf;Ecz+ac;-z-ye^urK`W_VTyGa?V0M@|b|>n+LDUX*Gy9q#!{p`0J}4 zU9Zcx8t1}EiKL@=z9`LmYxYLZ%Vy(lSe^Go#83sf5#vTu>ZZZCUkFtjog`oz6`*s~ zOR*g$*e}2dUmTBPd>apMhI#9eEbt}zuf-qg!5cllQ_iYCZT5ympA_Gl-S60e)XDHW zrV1vFFOoh!o2{nTtS*;y^N=86J6dT`14038*3bYL z$^MYw9PtcoP!=Re0!Lv1XA@av$2$K=rdio|$lW$>lw97hxx5b$WIXC^@&)g)FHJK*fOfa0xMe|mz&UK=V(Zq+5yYj@4%0@()2R&FPS%!a%_d+WVeUTHZi7eP{EzA0+z0fawvfrbq>qsa|QhbSfEq5voE0YD3+@)5PL?ISs2(u;y7U1a0a=MOLaHX|?67Vq-qm2vYtsqU4^9ZJ z$raqg$;VSl1o~O(=;>B@dCV{3kt5ii!snIn$@VBcveyzf5an@?D#RIl3S#xMdV6U( zv)srjLP79|gN2PZyfyZy$p>>vv5EO$iVWeLk#i_Sl{D0&SEmLNOmfr$k>8Q_gkVkg zef{t-PHON&E0U;e&(fdYQzm_WmCinb*S*z3)p)AkfGZA@5vv@CD^?_1d3VPpo64tD zs8;qvNC%1I5b%Is>#LI4$3B}e2&NOGqP5qxyx81Gr z2YCl{1%OBx_4$u2rA(5jrTbXzyfg0-EvO&KlzoSq-r&2kwp@R|@)I$Zl(SMh2_@oK z<7o>OAuRnt{U$r~i2Ofa8Js9t-oXnsIbClkY!*N>1p+!O@v{|dwtBq^Z602zHD3=>oFnd(y*?U~qNn`UGIz*Hqdrd6IR_J~ z<1y?d5Q}p^nwLH6#t6TUh#Vit5=P)?m{=j^Wza@W*YC?N~PsYVKiV&U#Xz$EfAND6gX|AfFV3Y+j9r$u$uV$uY8^>zjt zU|>WxH=YHb&ep$dk~&i5yuU?T-OxfFlBL51Syf)-@K>un2qU z0FBQ^@l*bP^^%-)L=f|Dxp^25M@?8IIPcHwo*iX^UH0VNP6YKvKefHJ$dOHN^j2ZD zm_F0uFWp!&@VGv_EDFR~$O*+sA9KbE(QLZvl1(L^?tZ50qIa|u)Am&^UvOtT`z=0) z%*fA|vOVDXs88l!wgJ=)F?8OA)&>*RWj0ILl9jX*-a9p>P@A$>wd6HpOlQ+xIQyM= z?EAM6U94OTNv-A^t!2V9M^@{FxhhLH^%~3lAt{%O9i}mtBT0V_#>NT$JGUbZJ?gS$ z>^_@`dx@sOvSybf;5%)Fwhq+V2bzJ$SzRCsSp(X^-5faD9(5 zTu78>!34}at6#a&u>T@3?m58 zwk;a$)_t#5gz`?Dgex}$R7;oqm@lh#Gg85->*2jo%Fd4u?LX9jq;_ZZooDg0^o8Hj z^m>m}Sma-F%K{d`&Pr~1q@L&-l%0yvD9GOi1f=WpOYLB>jCvQ2$332DSq%)ON*n*G zGl>#_d*b6DYa0;~xoOP_@IBr;nu>sqn&gP+Fy>MR&2dLM3E3Uqe0Fqgc^5Mw+|J^Q zb{1wmw|4%m<7R#OayKt?=Zf3)KzY&Ywx>|}6qMG`XW$Sxe@R(_|lbZwSlzn&q_2n*kLGu78o>xmX{jXNh@IqQVu$$n|l$5M;&FOfH zyUX$@jo-8^w}XJYIHO9D@-I*ibEE%Ql0)xv)ZQq}R+as{VT*By0V`HrN*Jq9*8xZG zQ4?bwjXT3+9!lEyZP?(Daj|I`UoNgwu(m-GFH*2Cxuxmoottz9v&u;@5HsYU{2H@A zPsCyM0T6Qr50_3@V4T~3+iDcCJX!j;K}Mnr$c3;G7*?ftJ-7z8NtuDjuxKJI^bSV7 zTIs^sd^Eeb#K%?wo5cZ)l@rBgQm;B6Gmze-GfqX;22O3f}X3~#h z*(GF9@j_SQza@E#OGJco4@`@L?D-cEFOu%LI z6@88@OFTK9Ts;L3pWF=FhQ-1AbMM0g)HZKk$Mw!(3}Dca&S6gAOvhbLFiFP}#urpP zDo$35^*ty~YIm)zo6~{L+E?szWt+9nQ)|O-Q%=q%J9Jy{*x9 zAg1R9vH#rR(rv7jS`{vXR%7R@3aD9Up95qMi}(EJ$X^*UZ}F$5Gs(rgC%?=^F4iJB ze8qc)RE5{daxX)pbtULAi#UX%^1#6;Wi4Z?Q)XjrUSc927M3IF>}r$}y!_bV7wCKB zH;)gW%p2X|AKCkqSc8>cpyw4sVXzAC9WSUFwLh#Lnb^434K?ANWA?hMcCTuAJXObB zI@}a7eFAYhlK$?s{M1Eg2gd@;Dh?MNrb+Uc6BJ@T?Z^Ybeur6(a?^I{XRvNxn0Jy6 z+T}+#z-IJC_ZE=N#C_F(YsosZ3X8`@Es6G#o

s}m!q%TT5W>F!PJIShu6I4x&}_gY7a-wWhtVa&)SaeRK&&fjg3*O#zx4P_cn#vYEXaokrQx4Ds5dH&X|Buv)LyqKZz+&UR;NJD+T>StB z*Q-{}Y}XZ#*jTCAO!Uc0ElvhlmsYLtCm$B1>A1CueL0R<=sgn1%{!hJpvZay6HF*J$46R_9wK z0QKqGWb}72d0BEI?8J|;cya|fl9nPkutcj(5Sj3^MVUx>YL#ZcGfv?{POQ&E`uipD?*v$+3x|MziBhtPx9OIZZ~h# z9l&9nJ3aD*u%ve=pBIX~%E+;oh$+!yumCcX>!=-b(#;duvm0}!uydq0fHruOs$AaCN zI_w4`@jn^JEioQnGq8A^y+^N4C7+9>#H3N|SKc>H7*0;VjLsJ2Hb~6(U%Obpk&smc z=Vht5{YG02`zibld6tDvGu0WbQ>PBcLxPxcYpP_npAjLYsl=m2wPa6r`WyVh05Xp%s%1%xe^H^Y$yIH|Y6fuE^=Eo0X0l zwm0*3(`TNtF9O!DB%oFCrN(5UMvJlFQQWYNEs{rZma~`pxOtoEYsy=tJa1B|`0}gG z$VG?^YiiD-P45#Nv$VPOcM8(~%mbP2gpV+#uuN0+GF`9aPX<2-xID!(|9YNbLW}v) zcrY%8!HX-I&#MvW_=wMx!USyt5mibwSwW3TRLMN{F^<1nDIy*yuU;ECa=Bnd>4S%p zCSVeynVgWh|dZz5o&{m&-x)C3h9D zpa0;qP6OFh9#Z}c3U(Byo+&qg$Gbej^=d>+_B;#j%Tcc&lT1gSuLgzEXn#7Jxt%K&!9&2V(Q$aOSa-A0cop#1DWw<7#TT#dOHo z3Y2)~5LI)b8l*|&eM9Pl8c1M#k|7yQ){v>c<&^tcrASq^p(kZ1jwW*eXbDU?PO4^j z7p6BN9LIi8E2Ta)2|P1__RhB%1#iSq$=Ni8eur?O%*JR`ZN28nUsIW_wH5=M>ucj^ zRkW|$ET*q%yXRfKb<;^77Zz<%67n*p^(omMzgS-$Qp;BjW+W6Vyw}uCCkS zt|A}l?5UV1LEIwfZ&n5il-tc<{@L_Fgm)#)hC%N^3-miJ+T*xIjjFMYQ!E@R*3sgu z8O(;mpeuhp&mW@@Y=?@q8JZ&zzmfX}D=VZV{^Yskosl7EJz#Xy9!>H3>_ge^30K~) zZ22E2 z6Fr<0v$|F$QZ;MMXmuL>?Q0j0mYehzs14}JJl1bVe^r}r+Km7Ky=4KyQFg}!F#JLxGCg|{*2(8%lkaTbBv7t0u&ZPYsp)!4$knc` zxK;Jy74B~kfj#y5&QXWS79zY3+3Jbm@M`C+Ww@JG;wDBfj0d-DpHIGoZUa`~PF<9L z=jOtZ=#zzgOeRiyUGBdbK?CnYW4O4HQ^blDcWSS-yf955hJ4LdOtqIcY$+_gf50Es zGPo6~Yt==iai+B&d=72kzoc1BCXstmHg%$q8|zssIt4`oxj=sd{x^}T7%x+3ZQ)c?obx*;@;b$Qer{Li?KJxf2{iD ziq-VpYYpbrBHp-%*`~lHP+nV9qGG_LN|~w(r20+^q@lzeC zs|K(ldJbx5D3G8GBR!~G-7llfV#)T*YR=RL5`JQV_?VRU?!o(dt)6&Z%K*T%lgN(r zmZY?UWe<|r1byZ1o&H<{=wjuZ@f0(FA1>AE&xO^^SNU3gs+Qm{482~Cqt=ifJJ>x! zU(bUKCB|2oj=pwCXn4C~RhG{*Q>ruBB2h*qm7we~`Ba4?6F>Br$MS>u1`?0GO$kZk z0aR0-$XczYFTHAA^x15s)ON(L3PTT8(r;}z?^AFyf{(@N_>YmnjBAa}xzB3~_q
    W#WFxp%CdA2oxrOcAmH~03->9panS4YFcDv4bcPcNAilEhjhF@J&Z_V=mn z(R8UJ#ZE(`gR(Y;BCp7z@)ni_OGY4 zMUu|;f8X?xWyXLuk}EK%){wX<{PYZvtKIKFR!n9A7oAUD$;`aHOTX*?mSy`YvVG(V zH~XW@CQz$D)9fow*Coq0qY6sFv|AOQKkR3g?#Y+ zE1y4#k$+o&ppyX$kXNUL=V^Z=um9={0T_9C{Oe!-?1IDq{d=E!XY&z{<-gXU|2RqU zlc&?W(fax~N7TRX>}dxd6F!!4kNrQBGyi8hbe^a4d%!07>%SZz>FEH&`=eF=`TP4k zmjkjgeA`|i{{66j`;7kn=>X@A=f(f|0+4h(nc}S3K7;$`HUF0nKK>HG0sh}!0+-E3 zsz&l?2A#+2ReuoG$$vW;AApeWvgu1Dxy1i8a7aj>Y(+K;sU#c% zDcVQ@o|_#f=?W5$>3_LJ^1#kD61jC!nl2XNKRotBKFk)=)BPI;_)nJD{VV_mR+XB? zPy|y5FT^!&-QNy5+}ct`FctqI>n8i#iv_3z|M7G03jC9!&QQ~_BrPDtAFtU~5r-r@ zf(I`59~(9855`3Cpz?qsC1v6_8{y8}vz;%0!! z)*>9U6fJJk24ibDtX&lHRCW>jmy+Yp^|xaIt~rtbbQDv9vPrTZ#s1(7QskflA?yki zg=6ttOvKfK zc$bg&evhVIgSkMvu`26T_`iIQF9E=6m_+?pe#Jn8Id`UWE)%R5-Nv97YdKq5$iD>B zL-%#+koj|Ve;*wJaQ1P)e~W7;84aK#@x{=hhtL1R)n^0s*(M0fOdRdWI5qX-G2`({ z7<-aOk&f-)&bi*9giDv9H0Vc%;hW3c@FwEYzuPNC3FN=}c4ANAiZPu6`t zF>KbF0MV;HPBjyVFauTIo{zN|wYM9@kbQm)R9uO1jB28Q>|kp?As!e~_D|Vw^8$Q0wvYD@pze5Eo zTY%3unA&BJ*M~@_SD~rv4PoUi1dxWIrTI*$v)?Nl0tD|GS<=7{w_@JvO@gZVj`J<1 zWogP|J*(hH`rWv-XmT-^0`-bC1cv7OMdjEkKni@7mY|jK3o*fi?B}T!hV`n z5T0h0O)mSmKhF><4IH^x#3@EkIYu*avlU>h$b2(W2V=_-kJbh(SB}(hLgM`Z*Vuc; zwizA4u%OV+xwglL{j@#ny5+67#}5Jh834{;F#Bp720aG%Ut7*dj^L|ozWr8Z3Qkmm zUL?w}`%`RjKgVK%hV9YO}G(83vu$dyjsn!=%1R5y{%B>RxyMSs1LI#D1; z9Wa#0D3$CmojwLWZRX~=EOzwfw^>9x!IVm5Sg$J)@bUECw+9(-%kfK9VIT4WK3Mgn zcg_bFzMf2m5gr(`7}P#_nHJ=hty*O8ch~la_x09IRx4-ORW`2+6mzFm6VJD0V}bm` z6pLgI`w%4)px!0%Mh#*iGK7LT(fEV@c|%Y$5!H!4>UP!giZ|D0kJ_9cx6Q({#_j_$ zPD@LC-L?p?P9A%CQSi-oucRzb9n?Fc+GNA6Q7k^so{J%4H#i`5%$4>XkD?lp?NmFU zLIQ$VCku|Q?>@u;K3}Hk0sd0oPd6iJ7`r@vk7wyQ^&4z?u=F6|0)BOIE{ELQh5_ty z`>G?5^*~EWfTH(8_v^`ft%g01g*`xF4T7V!1mF_uHBK4$72UfuxbhpPA&OUsWni>xnWXDP zSKHoq!!6UmQs~0`j@fvV$z-7(y6)cnd^GKL+1#IEv*Q3*R*!B;Ph21ho$daVh~9HLilw*?~2t)#T$6aN7lJ#;zdIHe5y?}(gY!X|1I;ky>7kvd9Rhr`>C%A@f>@WAd z07##tAMfJu7Zx*U(DX}hSnJmCgLEATd=XeFONhXDSzBW9p1>-d3Eerrc z9S0BQ&mrwTewtr99!|_?73-s>${x!zf=I|FoOdL&ZQw**}Gp|W@Bq+VP! zy3I+L)F)qCU)mU$7Noqo*c?!*XmWg@zTRh7KWc^g|F z0CJ*KOC!pQJ;{UG%e|R|Tsc0X@{;*-i%Wc7rTs{Lo7t6AjV;Yl(FX-GmhOyb@^49`J2|{ACr^O} z(y#-hWKPFY44aUX1zp~zyTd>%K%KWQt&`r#6q1pNcGAga zi|guZ+KZIh9m@j*a>ytqO`6T}_E+dmAF*qHqMjG|V%@P*tC*eMOreHO_+d2S=Wz;& z7!DYM6uCbB{9ze_1h@WzBT%EEx*|S*45c{=izWZNC4T+q=L|ZH6vE#k;y#DAT@pNd zPUYEKU|0QWQ*6lV8ggcZqd=*9M&-q$GKRfl6Iw3knLw{c>9|cl&bW*C%=uljR@h%J z%Nly55+pKP;g?3zoc+a{ANCv^z>57NRx=2s0#?iiw$Glw}N8zTj1&z8s7kQl_eUm!+bNvE+nakQ! zVwr?fVy}Z)Oj!C?H=E< z^>8Dvi?-kKv~xPIY3uw!L$|u#KD)STfv2@i7jG~M5CTTTR^2wZo-nv9U1pfpTdMu) z=$Uz4i?{IbCurl?`cN76;8!2v5ffAMBY?!8TL?9ailfz7n(u`J-lv_EsOqh9_0n!z zf8M78aM(U|E8gsY6V!3Z8D#J#plIE=VaRcr@`opn>a2H9bT;1!X3v4P{M@q)Bb~-( z>cofpNn7ysAW#r|!{`B!&ysVQLf7~6)kd6w9}B{TxIk0Adnq7@f#a;*58P6mBbvq9hk6;gAUhUE8|%690f3Nn(=qG4 zb>TW)wH!a!*h9oH1YFElaexL5eJ5bs{GV&m`)kA}&)Q#9&bx|s3Ik5nYx{uNkyV98 ze4z^MfD|KdndvP{d@B1R^|HssjDf9NRwkQQt<(O(yj8>$IuYOdpUB+GiU#`Di?vbr zw?{a?>4fq(J+UgUx1!b?`GW)u`((qEgUJ2!OBs2{8yq3O4BTAii~_GpUJy4QsFiB= z;q6n=+IvX9VId^5jc4-DXSJQ_M#Ceq8cx5CCx6~Sz86T$CJq^dj4-%^wuUu!`P*^s zz9rt1+Fm98$_e3yTTLig{8@Gu0*^6=V92ld!+7pD64r+}EKmJIZYO*H##S%Ai1BR6 zZQhTWb-HzVi843M4R6Sb|HA zKR6t>;9ZUb-rw)tR$jjKmMRl&#^Z6T4|~V0C`X_l3BF2WtU*XGD1yn$fYxtizl>ru zbdVsoUO#a7aA0Y4F&W5xGN&lfVs{jgg(lycbz*Ke{;&qBqKnH$sHho3MvnQU=|B<+ zU#H1o5BryHGhWB+(jW*Jm|bLvpKTmRv9FF36(h!ezTxOpBb`b)7ZE=l#>*5*;(y!l zD*H<`%sX5%(@F}MC_R7>0&EYfd6qv*_$6hyegk7V$g*qdkL|PmiPf|_0RvgHufMl@ zEfEY9;1Mn^F?z;d`u0mLXNoB-0`eglre8w_w&6|-3G_;uD+Eh&uW@L9*Ti&moiOmX{eCOT!>OtuA3|h}PhggIavvvUd!oADGg=4)p z(0`{$&jbyY5oKsx5}A_`ay!Sx!(#Fi1rfO<(W1Z0?)S9eoOYmW;Duhwp7Hmsr+azSFp(hd2Q ziyC>Zt;CoB{ve5-?jjrgPmRg|f z8;xU-sOz$w-b!Esow^Q7Pn;<78jNc_UecpiSSq65e_Bjo1B)s9eRsQ0f2K)PdbkfN ze8lG)ubxqA)zUqK3LPu+3w)!Pt}>Oua+4>mQv%RK-qZwIjm`pgP)1q^ z;%iigFjuT@6hvB|2za@I6xKpc-lI7*K5$}{0#?+=rDoj4%L7b;Z$Wp(zYM0TInO@Nx9 z)oT;fgcD!fozEE1qm`=JG9>?i&ofco%jz|S@#JaYWk6>;w}F7`B2>u*=VT*;$?&D? zrHqzb8b^gBpDu?sZW`WSJc(E8OAHO2HMO>n184$v3`qqalbnIz20nR-`_ls55{L5K ziIrMwD6*;a&d=T0aNnzdpU_(I8rfw(pJMqAxMwej4q{nMT})@Q#UeS4U7uxTJDP$^ zTXLe#hpiE-{Ob%RO%9FLzSpHC6D%Kou_QDc%oFm5$1_>BTDnm%WqJO2k^HN_ zJ`1cDz9hC2RxI**Q2hff1R%s988{Y`QTs-*9VG+^&FNhBWg1u0*Ap%elL7qcqQa=W ztr!|5^$7AN>+feY5KepfUP4t(2w0533#;u*EWK)4yzkZJpb69)O8I*oNwx zcp$w7={t!7#^FL()_E7xTBQv&}gyx?c@Nru`) zD%qRn1(Jjm)F%Bt`Y8TPeX{3s!5FQM4#%;qmimlh)vQ0@2hm=YAySGY(el-s&f3lu zn`+&=6tuaHKLH&-ju5iNo+x3#rH0GC!C$u4I2~?81Q0KitI7mdnpalbbBXD) znWYYR2U!@C2p@W=M79UxS=5zUM&yQ-$NuU>L;HoB`puN`2b5Xt%F0`q!|}>>Oaq9r zcN1d%z}R@b0u|Vn?0Qpm1H^nVq@)fB~Og49HePI7N7I>@jjs|BtOxlt9)scrd< z15n8bru_XybII%SG>xtrG^|k-xolTvLl)I0&Lp|K?!s86f<(yk4CrtUn3glyZGQq@ z9=(W$fPV39z1&83#kmEX!4Kd8y2!K5)ODxBpz_*S8bX8|X1`~|<@_SvUJ>9R+#805 zR6DOtwtfswb9C@F5PE5tq{2#8A zZhrjpoUgUX{#DgS$YCcwCLfrnI&9`D4tnqGmE~BkG*%Yl)aADoShIhLbVM7U_{KnE zk}nl=-13cA>v&1xDxK3&MDz~6uHZ>)a0K8|;_Bow3DtM_T5l$f{CMEF4C_Q*-V_mE z=6}?7GzoFC4NQz;qA-a>GKn0J{YGqM<-+aps1NO)VRMU_E>=(h7P;R|kJIkvaqi@k z22Pe5&SL=5+8kH)7BBJ#IrI9^+H}es;P4;44@8djKBL z-$Vzk)~4FS$UR!G=HO0YzbDPzP2?Pr-AqU?7N9BCKw~>jG+c{gjOPdnPy@wJH$F>a zFgzhE)IRm6{8j`3T?T((Z~<+quN24SDn&Emk-BAj%qCyY(R+#Lbc9X>Z*=Q$^z4Oh z*{JOg0LIEi>k(M@n0-zTQ}{-^BxO2B-^VjXh4Mx&5zvnwqA?~+VD$omwtWI@7LJ)v zlEj2bb*z{s-R48aYs8)~ z;F_5~KKMoB$D|{SuJ9{z_bFymSfB@+Hu2CqZh7w_;sI$U%0i+IW`w5zmJ+>ki~@mS zMbEGIn5A;5(3&P*?0St!8NJuw@ZR`YWh?y}5#m2**a>DjCwu=H(L^lbeyfhNaplYZ zq3x}r;@Y+?(1hR+EWsT@g1b8*f#B{IJh;0BcL?qf65OG1cbDK^P-t-1Lf&GZyHEDH z@7(wIfRAd`RxQ?)nsbgm`sm#^fUa%SrUa;>KdU}ph-!S|&`DqWUI=G`fW5EF#OiTx zneuZoFTq{?NRkf|;5>H<2|ki6imzm<4vLI=eA-L`04O;5*Lv$eHg<7DCi3@Z}U>E&5ES%?*%2Bm1?u%eCD8}J|N`e_Hy*zqVcueyfhK<4=?LJ z8I_HpCYu`JY*y|y6=mdF1pC<3I zx*3&tHn0LZW7WRY`2EP(gGZKrzd|)W`p@^uXl;b!JDekAe(6AM`1ZpPu9T(2;R(Bd zJ%E3OwrV)sI+MrvdI%VsB7Xh*3j`8ZAV6j7v?jl^p5I-MS4A?fo%cIv^@1MAcpKM! z-LD%rgab*F)V)F|X#Pa0fn5iu_50aQR)k3V%1;QHlG8+p>`xpuslt&~n-W+E0 zyB)q7KE@xmJYk=c&hEHfeWGD#i2_-MQpo7fKUp{7exxO@tL2!(vr#^!4|ANk)1>bo zW8ccGHK2dUxjFlpq=!oUo}U#S6K+kGtSkHhMaM&8v&hMjoBI7HQT@XtT^Gb;C>~-` zKoSO4Y!gE+w&3PPr_};wXrq8s0TA!*YZVzGfoRu-QiB){CT*0F5i>lFMY5RB%(J*)iG!J0*+HP#T4{D{BGvLKAU~<|fQONXN@vl}dDbttR z_XbyB1CPAVlph*9Y|va5$c;wm)DjPTYlTNx&BNc1j>b?|TTL#R1!~*_rBL{z{^(ad z1ujCiore`biKtcj{d=8$2R%mL?dZxnFbNH%xYw#%6vpW-I$janhe;Cs{{A%6G!K)0 zfUSGD4MEU98zDbJG^vj$l6f>pjDXrI)_b5*VDPU*h?Y!3&@N(NBd#VLc z*t~{{d8_I-VO$g z?rYl1tCgd)dB^+P$X#6mh&CJl<8iFz^wy5;Sn|BkFD%gaHG*})>X$HUj}NnBWwGKU zh3j>rfE0>vfOz{ANTHL@C6nb(to%(F7M50(o(Z%a;1<}-V^lAl%&Y+vjFKVpE#`L) zt#V-L`1S}oHq|n~>8rinx1MS;G>YQ2E>xs~Ft)Q>gPP09scMlzOBlP^lDx?<>RDO07q`4yl$!fv?XdhNhEWH-pe1ZYY1Mtx4^;t*j|WmY!au{HoavZc0*q19J6IK^ zF@U@&+(`kj8}xN-G8z7!5bZw6Uy0FN4|Z$5n}*?P8>&r5#QK&vY@_)ZLDD#Fre%&{ z1T8L`ZAY{KGAgiI;FJkgVIxiwvE35y%oj&fGLxxc2O7#k_?^BNO|^Gh_$gQExPCdvN<<$GWC{0AF&aa#;Nc@NL(3`m$JT zswN`K%(nLqmLNnS+*83^Hfc;?Q9`rEb3&sIrAHZ&1QvCp-cKZ*#oz^2mIo5^<%4qr zGxG2$J8|sVer9J9VBSNdsANDf7f{QVq z`{f%ev~urUqa+IuLmS)l;=OH9fg;8VzzA}y>qjjnaHK<({q~0x;J5Vev(W6V(NnB6L3V& z2MHwlYU7gdY3p?yYjG792kpsZ-THrqSE<`mIr85_>~itBwk`=3f!1UY*OTq&S z(~GQa?o4;$uvq9e*lgB?xb2Jp)#l_(ZS2A%J8nJ^4nbaIMeB_R_aEejCVy!Oh({BH zxHgAVmSsJpfHKp5a6CiP_Pw#qXFnhy7Qy7^QT07lyy~pCpxVD2)K)JwWTjb)=L+_>$HL4isz~0QS54%2f1Vc`$7*EZy2o& zHY_z)%a|t#b4Vojj5mh^DZfw@F<*8-I{i~E!@$oV6^dR(ZNjyb168oOnlhiQbUqjM zg3DdEgb1^{PY~JpVy%^0rbyEpwK5_gz7SFwh>VR`XjO1juuD0r=S;lREWS$yi0u49j5c-iApliqga<`uq^ zSBOLiWVXWkYMDWfA3Q0!{)p^%u)fg!O`pG<>??vjnAz^R-*s2!!KBvV?h;tC>7FM$ z6Heyni-ip`mtm8^+fQUE7;{Z7<K6PJaer>y8 zA1OAeFg*UD4*_5Dr^_uUdbFVh+04*9&BIANkNgzAwA;sbMONc1}GR8;;$Jj$TqqT=pw^g zeWe8o5ToJ@>LS|;(E1f>)nprGU+4_+|9%2tMl`6z@8vcA9BH$?H&sUNH zUNYYSA6WAdR_5`#>@e2YSZ{EYCFvw10S6~ns%MZqP-*}q6g;NM#^r8-YS0&{oGG^; zNwC&1y3U%~{_ba6{B0fQMTi>!22vFkt)1~4Y!oWlYyHHTs9j$6!?&+VVH?=xe*zh> zI048T33^xI^pD8+wQqi=C%YI?5pe9TpYg?$582*iXUj}C>2a);UHaW?aNpM*BR-RDjcfb67+-C`V7FtMr2B7oUEj zlpf2TBhO#|8|OJ%uC)5Lf9apBvadR3`*FQopC>hdXad!88gGM@`HN0zg`2s$!tL+f zxrSnek_Llbrfr<>u*LEyy|w8{kpmJhW3bFi$2uI87FQ(Ea||N%^u*{WzPlqDY);uE z%SQ?3)3kdkj5>&~88p=aj)_$dg=nL93$N}v7Er6daQbT`iAk}iYxo@yP*kiXH7F0! z13e8Z3s4AQghVV!{xMQg&)DE+id2?wVXJJw&2Cdes~Ntpjq#r@7kS)YCL_E)Wp6s9K(vvmK-Lb3k~>H!L%&yg|!WFo*jXio4C zN$p?MOx3XVz2{WA`!}Uv_EbC&r2nI zfp+HD;>`b_4gHRkgX|vtrn###Fnj|z@K?Jh3wpWZ-<&{WS&b)tp}o6j*LW+JX?{`a zxD(5I9QYt{EnQ{t-~Zog*0Vqs|M7$jV9-x0P8`! z%4i@yId!x)Ryg)eg~Xa9 zO0onzR7nn|7o*mVRnyqS(QbB!4K1} zuPFH1+FmZ{vKGT(wO#+_B;QOm<74>i*u)XSz9R2KB0S%&;ON2jBybgq5Oj(XFim3d z$Ot{g;C9#&y0KrnQZ-ItHI6^Laz>WxDWQ_UtoOXBin#Igqt|Pg`KkA0A{~VuxTcJk z#%dL{ztGahYrP#+O65{UL}K-6mu~CvyQdpvuGiw0M9a{=9{I+|Kk7IVL*-ALy1${Ck!GEq|F-+h4>%Xt3O+%3xg=SUG7rI_Csr=%PE- z>34LSoJW4bd*V8Rgc9%8V_Iy-v5*Z(K6HZYA!{F@w?Wj>`tW zSyzp}k3CnecOP+|Z}2Erammn}j?G}+x+9Ip2xK|)boa7&7WeQ0z6%bJx*ey&C}diHd9$MFH;=L6ey z#LwrWgVTElFSZm(v?6VanE(gJBtSx0vNr;M-^%wbj29ZLCh1TQlP_R8q8vrvS1NYy zcB&6KVe&2;RDzWD&x^& zGD4ubR23TMU3J>)C6L*fEE5bIO8!b1eoBSqt+t?%NtjzR0X%OL&Dgdy(kAj`DCGn% zziP4g&PqSr#gr{3yR9R}(<>-vx>g6flk2_9RYqM%<)B@u_dojttR$Si2Yk+2*;bCd zlhH!d7=2%W_avS)7}PUoF97!xcsb0 zONdX~tAtx%s_#U#rjsYS4!`Z{cO9bV7YHrau{BZIi9QVXiv)f*#bG>oFMXkVQ)R}b zsRsD0B)jucxDGl$M$fjHsBJ*iWs52QsE5eLXh&V7jq?9MEeD!Yuir(=d5K35D93Gb z)atlXl_Q%`(JJ;+rcm*tI)noT0x1W~uyNsZgY2vz)-K!&;d`(r{uC4x1}en6^{ywe zw-7!j^-+{vzFb&BS635dwi#%+h2zgr{fckD(3hZ)dfXR0&X(ns^ct)P_Q`~rZ;7@q z#j+5VP+YBVo2(5{6jvN7OW(~w8EX61A5Nsm^zX%^^>E0Ey=E^`s9r4JT%Q!!^;2g7 zp;3cO&oiwG+n5?CkSk-*-%rA4+u!9azSh6ZI?dc;i<={S@hM#ZN*Y-CjJp!?@Adnu z4Gf?2VGPZr$xx7tR;h%yJa2d^Hz02Lexk4|BbH5^a|K&_rO|m=EQMuH4WQV!r7Mxs zd@jfbl3EjMQDTZeTrugGhQWd;?_+UCWSg6bK$iy zKtsdmBX2CWTq*^ zr8Fj1HK`I&6YiG`Q3x2xlQz(JxxZ;4fX&a! zAB_dzuOVRLEVbWJp^p3U5k<0iO9rNuQLYH7#$-&RVDto8gaAG&R}v>7FOu+6;Aw8D z!Eolg8lOfCjvFl!? z#r&h0yT^-xOorlw?aRLAx2elud4grt+0x_$)5KC22L`j{UFYbkQNh*bz3o1*7lLI> zqr8aIO0fk;x^8SXnDCukX5uW^ePy=~Fv&f5*q?9G%Ot%nYSArxuhFo5czM}a{-I#( zxoHLlmTBQ0@+*5HAt}XB4o*rSzh?b!&>SbM*ZiNyb{`qr<(2Q=n;1V>^4gphEew zY7VyurkEAC(lz|m18=C|i4d#tEq!s}E&pfZjo)^=8GO6J0j~%7ppp77-XEY{g};0H zsw1^Zr%X7%bfcs+?mqraQ+x1Ij3Q$_%N}}ZWu=emd*LB8K4e(`W-Z)|PNOJ5R&$8u zr%W9myj~S-!mS_Nd`UpP{IX%==ZK1cY^fz+zi&t!?$J0P2Ojs4U*ou_{ecY9ZF8-3pHz?UQOsLGNg;LIk1ZO1O2(a1`jrW<-8&{*$&>Dyr4r>nApNu~P0Zk*Sb z{Y#w4Vq%QVXnZaA)HADZiheiSr|yFeC79F@MQp3%*d98&LRT_Ii4Oj0Vjs=3r$=V# zm|-#x@aA_cXQ>u0wiZbj-{C46H7Sbu2zI1cm9A@#slBHb)Zr`M ztL_-l9on%lxXNrPGj_~#y9Z{H*U0XH2`SmP=9^J*mP7F+6yTf`i!SbE7TO?J#I4&3 zi>gysZftoNIi$vaBy+}{igFrNfHVVBBcJtR^j7HJ!0)cfGg6%M8lrJIawRlmV{RYq z?L~4y`>YGP6D%=le0WD4j0&RA-qxx+s1|fh6mWc8juq0|H<0zd>UzD`@P$M?-;RH{ zJv{oXp>PPcQM=Z&CgS#d53h%V@K~e%x17_Fb1aYu=mu_crg`Dgre7<4ruhr|hurlq ztS8x~N`AoiPo>&o&S&+ja`-S}TRi@YJZW^g;HPZc1QILm@{K|_F z&im4Ctmbop+5Dt2h?G=3SNgn|E1o`g?9C4sb+?$vN9Wk{U3_f1*+b#4NOjJJvoM2u zXiSw0QYda&o}9Yy)Hc+5BsSEzXkAYCqfJ?^=Nwuy!}bbDX>W&wk{EHd`@pwt?~%Gm zqe=G0-=yyir?9%a+K3pTzST`|JIc17LekA+Q2f!)O?93cWItbJ9HS)pnyqIRX+oJ4vIq+oOf<*G~37XpLs_9ogxgl-w#H=`A1b6r}A`-rW`3 zFpSSrDwVG42KN`fyjT_g#--=gQQV(5Af6{Zi#KgsYy^9^kwGwttN9;%5S~Yy{^5B zFH9iayjOyM%{+0SLhGska>4cRE;&VU1>+hn*La9?rX(Jt#$E94>$%q@zDE|Ik|4o{ zt^N3JlK~#OHuT%4K&R2MYKZ*#)m$UjtU}q3vywZtpw_o5(s-ufpX+ZtgyzNbWFC7K zbjuG%PVZgTOWFSc1PW8b9^b<-;RH_0cLXN@hl`6f#IB*64;P&#ttJ->>L2%;l?C~)^`Le=zpza` zk*roP=tfarU)+@lc4jDXtneu-sP$yurHkfVp02Ffe4ex3LaZQ~*h?FOMIy;uk$$yz zG?6pTpo^ZbcT7+O$xYr=_9)EOBlWxs^g3K64k54kh>UxznN;`k)w@T7I>I}W*0$n! z2BnFpX=flTbaS}wyKI0S_R&nV9j$|lP%K(;zBD?TQ(o-Nod15r+YAh}fDL-+qARh_ zwI}K8yFW?6BB>UMXwY*@nl94lX>5g*z8K$XK^Fq)&XKdin->s@jx{b%iqIC3dvbHt zqbU|a;e*ZubVE@V; zeciuc@W`VB3+&lNOO7{aSLziwQ64Y4tn|Egc=mqf{R-WF4cjD7Pc-&CWU)Svw`f~z zJ?O#;f&q3WrLp1jx_@ZBwy_8@*fyg7jnd3g19`Ao$qVG*$FoCXk$z=>^Fa$QVCrP$ zE~o5gnS0XLq~n=;5rLx`09Ka{-P!79s-IoCKURV^z(m)54rIA=Wp(cS;DrYzp(zeB zT<2fQ?5gkz+1JD)$?dM34MwXlU|Hbf#d3JxDZ&z#^Ku#W7ue;DL zb{m=`786*25btkn=s8Av_zcR}RqaQXeZM1N7bo*l1Kf}3a`QO`AnunGUvUB}UfLNS z?k#kL(c~Gft$xhQcdu;cv^xIU!JP9MmC1EyItTDB514Lh2-@s`s}v@z1*rbN!c23| zR7=i@egt@;p0U88HZPG!#p3kH!)s?A-hcDr-(a}2Lc*~#I7Xf5^0s}3I%Kg_k=Z9N z;mXc`Gxwra?ZsfnIxNZO))KRIEkG2DNIBeqIvk=rMrS8^_5lo7m{%Y!ko_|A_`ay4 zDox`6_JcoD2&3-8 zt&as`<%B2L^uU=ak>{ZJL})<+30DAeV?$Q0+z4Oasbv}abW99wp7Oc^WXqziH$)JR zHz(`I61C$c;$Y}LeE-i34kSUxP36Hn!RMRqMUiWRv!*eEwg^>VUnr0u-)n0 z+=Z9JcG1;bc&}CBiIP?x%xLZQ;dyI-f{@oV3%-M=byd|wIodLqXLS3HhoZ&3DUMnm zvsddE!pc z$WZKpvkAz)KX$Zd3;8)}fo-?!S5x1}*+TE5B+IEYxh| zt|A!`&7)wdCM?{KWAC+2Tv2m z$3XeYmb^AyaV*`gu;keV?ilp3%@MIN* z9wfs10r82)o7OExXI`09_K5~11P|g+0HyFKNu#Ty9!FWj>t@Rb3?)T_P)VruJOwo; z_+ak&hU_!`s3yRWuUho0kWj&=1d%i5={pNrIa&1G9sa1(55>*3H+Rjby`K%P*trl+ zy3P#3N0T{1cF;roxahK91sW}M70By=_MIoTS5WE&k!KylcW#59nHHa^WG!-5Bl$km z0JMfqi${6*O!6(1v$mn1KEl&Zzm0wGVn}oqc=ec2Hlj)FT|SK*8 zf7~|2Pg{L35$MR#0*}v`+BNTsPEFLhkHBZdvq_@%+KO4#(T(xxse;6e(M8(TG%V?| zKVaI_dUu(ili!Zw$TVZuoMJkzcm``U@M>xwHyF}$brKFNwuBVfm8Nz%0Od=BQ<&?g zV8kMFJLP(#0Rusc(ei6EKF{v6hdZ?%%PG}bX(SK4;!y`i3w+VRtHZ^kC8A?qSQb6q ztm9se7W%Pb@-ANjlc_18_Eq=Zi1Jocwkq?*Iwo`VYF!sbuVtIjDs4GUSpMmZoawsn z9+R78vVL*}Ge4nT69=tNCDdKJxXDW9caPW?!u{_w;uR0Zf?j$_93xGUMM3=WOc3JL zG(QX?Vt-~!L#9t#cE;t{b1%~)?EutP{=KkwV0^6$=7}F|U~>yu7Sv#btbpnmtEpfZggAI#iB>g%DJtQMDezQya)^D%#)E` z2XPB_fn8TlPYYCYe^3QcP}h>se!*_^^+7Juk9weQBUmU5MkZC6jp5ACmwNCVrwC*; zWVB=2j2yP=F=sn#$@P*cFa~QK?@zKswM++ko{OTCY{~2ELAwED&hNo+Lcf5fdG2$^ z@NLMcotw?~hwECvwHa#pUiIuj8LXT~%pS0R4@K8_emin7dhY8#uG{EwAM$C`Yk3Az z8k9nUX?XO#2>L7Lr=N53^T*}Ovjms^kn!McZUdcLR_XX3FHh!}m1h@Q*7m$7oorLs zpTt?t+J zU}7F_ObY2J`&;d=`3NS&)A0HxGwE)3)i)`EPji7r*<{NsZ^TRXU?dIT$*@n z!G3FfwpJmLCTQrBH=|TARTh~24GhT_>$KEOoRh_J+YJoliRSvKLKQ2BT@e)z!_BI4 znGt9j>o!7`(L)}T-`4w_QbI!F$w@o!$YEgCEHAWCr_x1(UM!=Jd7)a}tMq6EkO7Lo zP*{EXWWj8PV02Q@T6$oUP6?A8fN>|*kCAEtiO|2#7D$QD3_7}d8Fd@+&JWY3VV@l} zb1meHVBYvDy768^YhG0WdVJHqQHq1X6Y zAlnar3am!F_2D)676L3VwYkzMok!M~m=8DZ#cwKC2*J15QJ!Z6NVS4`x;5ZkKdzZi z9BD_ihV_*o`m&DBz_UeA3~wD&tId(+`Vqw>12+=dJ8a79_A101hROzQU*Hdh@=Z7y ziL9Tk`{ljg^qFtjT7<;b-nR}k3z6Z}$4IGGi!et2i#gPr@ryu1U>0=6k(pngop*jyusXqPk|JHyHrV3!Yd!Weci=YWPM-`_bzpGYrBWx^}4^E8E<{#Q#wAYi3TT59ll&Hx1}GkfW5f!FzLQb zE}ku^E2dVh{&s%o)YjLHeJPgk3uK(?=9>d}5dSzO3EvZhn|PZjaR>V&%R97?ycm&QgI zO6t%uU0^yZ8H2%A>q`>ZN9swpy0zG`&zf%cu%6!iEtp8BG-!H5Scz$n$c4_|jq3sI zvd(O)#`V+Wwkd3)hbnP2u57L^;0@2b@0U&Ox*ORRtw#NVIXf&qF3KsH?B?v9&IU@o z5A)bo)hLV|zrt|^Z+SwQ>Ktlhhgm}J>!f;=7@<1 zl=(6z2Ot-?cAhf(1H`Demm8tM3$VLwu#hTx9~0`?&odJ1fmK;*X*;^~Pq~;ctju@o z3<75;hAS~kdo#WXp~qpM+U7|<{9wpcA1v{Aex*EA=jqlULvmL({#Z{9AZfQhg3&bm zb?g#8p}nHzO6XQPPddxWi_->dVqH67QbMoiCmTUbjL$V&X1z_eT0JQ`3NFCX^Sbxv zfc_jUwmKKzUM|?Q@3{@^u<4(?*NxRLZ!=|a;By{LpBJdgIEk^xU(A=zT31jlU+wV( z3Ym7Nzb4k>FdG)8K?1M3V#Y{_nz;Fx z4Sz60Lm}ibDdB*fwi4=wxIg|JJCMQf&H$AH+KpY{sA~_rwOw{p?0fGuWz*U*rx454 zwGlylwowx6NLD}!|5$a(!rP#ptMh{B7`ek(^uiqbKpRnE(sUR!n}OuYhUT>{u(Q1V z5x?kgvhCrLp_T7S{Banv+!|yvmQmN=;L*N!42~R*8)6jN3P*BYU^ux52%V^Vl;08h z?Ucgp7NiyP?NkvDT#`qIr)a!H0|rJW{7R`E{tpjAc!Em4%u+(6!o!ME@OsM7B3rcH;ROhvAp@ROs_m6Plmu1)<>K_U8m`4 z;es;zBMh6MYFuxfdwhrFiNVMH=156ylnbgNiFGahg>y&&6WqNWK>)wQWuLj^m#-mM zdWj05%@@x&!z!r?Po_lsUjaIAY*orivU}#}%K$mXLR7o!N8q71CtRW1z zB`tTmh<5H09g`b-HgD&x#4|>woFEb8GxhocEP-=2KGzz84;yqhYDgUguQDUz@`qz* z!ME*4PPmp$i#4rXV7Pmdi=BEr5+Mx!Ry=4%@T``Wp^y$x>^$73UElmSiM3x$9YXC2!2>)P1g8n+waEacJX zTb@p7ur%AB4BwxJ+gKjax#H@|gAF_I4#TU0aMxUfWJB$Urh?ZI_~LRv#9L129XBtq znI^2RnKA=?=x_QKh(Y!w0P|5prGJkq0@6|c?i@tDf!tu&jP(h-TztFZ--H1Gn~i0E z?uyAj14YP)fs`fzmkGpCZ+RKAvL?|5*=4xpO#d(C?mmowbe=S;TQ9heGVCCTkYY>Q4C}) zYTZwiuQR!S3*E#qX7)F(^Wdexw}D5egVpN`r?EV|`&jimx-G{O@b)r&p5UVmoDOO7 zzQg%;qZUQoYjW5agtC4{)M~XarT1|Tp^{7yB_{8_CDb)mvAstxQy?Oor5@F@!=~K# z3)6v>V`W3b>MmDhAV_P{G;W7diwAJxZ?@5S+f6u4VYtss8qFa9&mm=m*EYRu_ZF=O ztE&aGY+Ja(lh>tQX|<*>nDKptHh|^Rk}SGcDvJvs zd+y1+L6@CS+*!V$T2euKWjEBL1`{%W@Nfz3!hv9667uxC>mZ~5CU!zK-bs= zLK8ny@?lftK_~MS+IInv8Zu1$BhO>e70vwEgD$$bu+v<+4YFRh;i3v|&f&>TrLu-In;> zAk3h~LiI%J(W(wiPrO)jPF<(u7Ae9l+$1B%(!7uU`zkDi=GZxb` zoFmcU^M@@grec>uT%Ac(nsSomw@MhDwhbcpSy)6ut))k7(V9PUtr2w9$@%Zuo$?57 zuao*v=uO5vD#UShrgYy6?*j(QEoHCrsNwa`8%*7IWW19;8|hjAVJ%K~x8`}@1RkIZ zV)NlcfJH`N{AXTATfJyk!%@F4i|yMU9%ZRdzmicvoz)Y% zKxJ)7Iv^I61_bOh!(KCfK8jplzd=FgRFbblVUGEZ^vjT5B#VSI>JK&mFiNHhm4ZEg zO%*P;=~8??tsuWQT8L<N!x%cdo(fns&tZk#fCT<;F7!NwUrF zc(n#RNR6MoDF3mkOxlup%N-eKyp}Xm^M{!-4Vi_ShAz`IzFHd^H_EgA=(v23W_Acw zQR8XUL|%)O@W_g1Vyntz4N`3*Ovy zOz7UKd5^4WI1CSq7kBHqH3?Wc)@84VrAFqzFA^-#!=Y@&jZOa&R)YP&;~MUGu*4XY zkKdfpj9+%KAi2@u#A1KWjs6@Fy6VYZpBq(eHa$frrNgx#q^#p{EZxm4**(lx9_0R> zy-PX)(5;S5n}Fidyl8+wXQ`AKoq^LV-IRT<#&1zJ18 zQ2w4xDBh9#@Fmggeq5cYCiMZzInOf|y4O z3;Q&7g)v;z4%8mz@O^)QL)FTo)1u^X2kl@qE@j=&>kdd{oseR12m$ipUDmp`iv>y< zRCkaN|5c2177yTHOz)oMzUwwo_3_nEvFdip(B~tr7N#?DZat@NML-@aqUBoDgEjOFdSuNWt}py{Uegf1nYTU-plw< z%Quw#xRMb(^?2m9xyQ;~Rwk8y;R34zf3Y!^wW2{h8~5xvDmi~aP$Q9wt`Z=mjSH(J zw(5jhhmsHB=5slygk_z2DFVk7l1*U3$(O^w{L32kOmw(dUYCp|XXh9~Y2I6l8mkpb z(;9&6WA#x70bQ)JJDj0Ag}=c;!yvTHYu$oxbclS~$F^I_OqGh!0FQQ}18;ZUXZ8DL zPhfV=Zy!sB!F9trC+k&692cvL`Yy*}U?>%{C*Dls=7G7Z$twqklF8%l_d>D-$)As_ zh7#!VKd<@8m$7wY^LgLp7%5%JsXC1yXo`>ZWz|7h?n=pP-{F~1F8$yS!DxGF6{hU@ z0u$w6(C^cma}Ub&F<-I@^~WnnrN{mp5>MeMLRm~YkAwOcuu(zSiC&0uFlpE4JY`og{#W^vIUW2oA8tleADt6iiAab-;IJ4x&na1nD`gSg?OJ+gs!<*aW!uEcuI zyV(#P2`d8n%4Ge6Wepei_g?XmMMh6*vRnK<^Fa+{dB^g%wyiK0#j4!(X&o{3FstUU zy{Hkg=9V*?UVaT;-k!K%AWIyIyIce zoH%tm@Bo|vyI=YeGDNst(M6>_RI2{j4*rL>#R{Z-`57qtB5Djcuyw!bVaSc7B@GVG zKhfBqofckuu>bWD-)kxiUA=+p1C;20=#T#kaNvq>&yq^g_<@HOHh_QIcHPK>d6j;^vgVwe>{-mJ)VoJthJ%|0N*JcOe zpd`5s)Q<(84AdABiovW*L^{bVx+!|)!U0PDm-cqZ6C(rWSu{8G_NZa%nv}f)+)edE zc_IJbJc~DuFeaYNb+xyPQC`EWvqVfTCK+nwA7yF{x5&LN%%@=a70fQ|3m$@h$i@e2 zm^E|jW_|A~3GTRJ_;3fPGJN}rME2`U31Fhdy&e0e;ehL2J(XT4YeTKo7{I|s>j$0u z*Tb(pQEo}MpXqJLWi^oHKQ@uEY;zn}QgZ#N-2aQK`i2k&qYq%okw+5DBn7rH#4u>p z72~s^4*;~JH(s}nil0V8H0$M`Tl_Y zJCEwsQ;0`Dn%)<^RG^nhMFa#26LMsUv-8?y3-fMsl(*oBLH{~QVqrX=TpTLZcmOoz zK@kqz-!$IUrWjvv0<%d$b~bm?8lJ_WdCFwKI9WJ43)l;$>C@ zR?kGf9Aye?9%jAR|8(2`8u_R5?vn&h?@hDqNf;5I-OTB0f9gaYB~3%RxPL=i|LcWB zsPJIURLb1t=I{j>6i-k>fxec#LFgY5{l8hlKw*g`(2%k$(qUn@!#gUAmn=O`{KEhG zC-I~nySI(|1CJs!R}~~@uqE;Lr1}$A{p;4k699#L|Mo@zKo=4uGs1qTPqc2#^Zoa3 z2?5F+_iqzyPbO685`w@6&0PQQUU+(kOaq{uJKwYG0o+(gJ3Qm6`onpN|9(phWW!k| zm~LGETe~~NDwVpO&_eNF2kbf4F;J+UOe(x2{r~?R!UHH^{?-Ec z|9Bp9>?-lZ08Fd1P6FzfBzBbxa{z*%y*MZ+mWr*xt)tfx zD})P}0c$S>(EriI6VcCu)jU-|?a1D0248LBQiFZBoM`NTLK1@>L-^xe->%htV@;0a z=4oiz_LE)*OICEGe)_w*Za*Y;Q3<*LQS~-9VW(d`U3LnZjQ`=`~u0| z*5EG3p7Hi!i>fLBuV6(m<4BVZKfF0VXWV3|G9Q62P64Rn((#>hxkE_@g5N3aXd-yr zs9x&>eA#tQ|G0N@?`ZUgo3EM$_#V`;s1A3bR*)Z5uY1`e_M~h*-R2N^0HsyK@K2YP z8n1>yoMP+RtS}94&jBH!vT2E(iH1m{VcWQtQCOrm@W6v&b!iw7{`)~cwWI%=22DF% zBJpD9S6N_|02X*F=T~rmcme~(^A%WCYK3*f&65{&--OChy&-0^#D9~dZRnI512Rk~ z!rDi5JOn;eR)viNtUw8X7M*~rGWa3TZ1o$LRe7DDoFF~_O6cx+Wu*w<8i4bL8Rri~ z^{;vFyCL@7DD=l^_fG+%&0QzOq7N*E8b9srMOW+EEM~;qf_{8<=O{VGr=ou1M`1k? zV2~b(Dg*!b*9zn4f4IR*Na_4;gJAhW1AM*_*r#IVx8LgTcwTWz)&Aqm9t-;%Rx`R( z2XF;qEUs5Z8o}UtM|9BWNaN|%h3ag=pCzA?ooS1A9pJCPy?`Z64B@TgHmO&KpM0=g za^cqHE+6^4a8$gHz2|1zqZaeM7IGVLAmIP`#8tK@!FVK=>UYt2zO9Z&!QMvUr2Xx% zPo)XAqj&`IuP9sXxPo^qf=Q^arQd!A;zFtRcfm6M(1_QAy(Hy+bL7q3OACBXqv>}u zz>b~pNnG5EE{4bjB5&0JKvgr@Xevk^LmGM=N*x`nT&O5vl&P)inZ9Vio4wa}3Z+ z((2kDKFN`(Gc|eMgmHj2JbNy`rDmWreVNPu9yXv15PTaNWSz1iGMpaI zjwG?mchrgnZa4Xp%;kFQb8~glwph4%uKIX&UAKqtWiG4&)8#X13&gYPkc%<_;tGN? z`mSb;dkeOZ`Rt+B<&bG@NlLEyCbFuBd~ec$VjK^^)d9MYR@03;TAj^gr~3@|-b&Na z*J1fFKY)C{ILn;L&|q(pq&3Z9YjM9Icx31$i_y0aJl}u`2XymFNVevkddG<_>4KiU z(byKhW5k6J&LgE#-dNQs2Wd$Jk$~(7H~z)GYz#$iGmE8ggx3hFv@DC$t>q1LMdh+# z_R4sXFRf7G|Do$GprY)$_Hjidq)`x1I#jy5K|#8^yFt2JLRz}Jq-*HzZWtK4Vd#|p z-+s@dzTfwI|7*FHvtXDx_c{CQv(MhwzOFallDnJpIvu>+c%HCDwmATv#QDSDQ_*?Sj~lod|4h$ zxenR5HYPl~IYm4s!jBDCVV9CKld zPJ>$fL5*URcX2X*3}kU_hI_F)%dj`LjWYC__D0@yVYVNWNbglsnvePVeFSgxu$a9K zUMX?4acoletF<3=LOeG~kOs@ZvMjOCU|MKZMno8UEuTaZt=)9J9i9>FLW5=gMZ1g3 z&O;-)SBt1IgB$*W_U&>Y!w8*VZt-}HuMEdtSANpZ1*<1* z4x=K>*#KC~#8sjQx%igEh#tXgAFBf`@#UR$R{_rE7(tMx7p*2OkY4F4Dt7?R)CY;rmL8$;+^aI=7 zI?jcT*FwCBc5|}1=#?9nO9^etRqd>w%`-$CSocR}qgMw1sh2^di&3|mXAcvB=$w!M zS4OkkB@IH>XQ&LXBI zfp5g$I3WIZNGH_9bKBY;P|9x{AF)&T&M`WIlJWVz$ORz3Ay~iTJo4K#+xldE<*nY_ zatc79c>~@DtYc9}j;<&9visgX??_GVzMS{3B@H*%Vq!-r3rSBx^|n!c6KOT=i@UPge=0Jgoc;!w-1?Vx{tK#n>9%` zi_2PQZ75_bMCkXD-4#AX!O3mp7f2&~kJQm9Z&*J#J(7OctRGo&73cY&*Q$Bbt)&_Y(M}x0;-Gc&!2qn``AqcJ0YoNiE zh31+4d>Npe#JW|5NRTC%4d;+62{i&8`^PgQPx{Y3L)KDzq_lpAT;%^X8%^nBI3Ic= zk&#JJegG$HYjy>TJEn`tEKsnU>%eVt0$DEauEzQ>I?u6cGpn;$lMGu~>x2ZtqY}hP z$6O%^zrf)xPFfy+-xJbUBYQtiz-}VFEmdMEh2!e~+VG(M>lw@_BXTw)ir|P!Cj^;p z0>J+g8BW_UI-d*dt2J6>yPwWE4?^K?1&Q1}cAk*SeM(SNh4HpxV~zQsso}Z+QT4DM zGK=*qzZ?v`o;g;z&w|PdaF-OzY+sbMDuYD=MX&-2bw#Oef->9fwrJ#*e3^R_?6uVQ-A91I}r%d1pKTq8}H(6-MPva>+5hC-V*lQY zK4+{+X$5ym0L5;|SyQ;DK7rFCd?@WTIqxc-AHwPROEedrx0gOxbV^{(Ky)=7aFC?yW}@M+OE905&@oFJvqbV?WjBser)Zs>V4o68=xsf{@P zEnfMH>!0$uwl3}P*gJMOLWPoann77;mpghWOdombB3{#7EBAOXje9=uQ+q*ce0U+x z({2)FDg;#rv(Zi#Z)?9c=Yi zLZinW4=H$BCi4KkF%j-5iAO^X&ymWL|Ux-}&TzIRtcS!LuQ!KN&G zsSS66e!#$C*#G_W4fph9r`yo5(8eucKA0}<7!Eh5D{OC?a;{V3}r#Df4TYt_z8fD+lg`~cjNOD?f%i~fQ^VIqE& zTfeRfz{6r+TmmH|0SjKsAeQZFpowZu6+P(nJl?w(W1?3LLbYAhh=TQ_lND3xm9XJ6DVsnLBRVEU6U_|E3@y*orS_nV<6@k;mN zD_Hc=K4t`TG%9IdG!f()Ze9TFJ-)CXRVw#kAR8tTu*-8W_G_xwF;6lNS|3N|^uCWw zio<$7>im?UGO1xYX7#Nw_hXB_d@=6g8mz0isJKl0eRbX31V>9_Rbbs{Wlqqg#?)_2 z2QMI)M!SAw#C7iko=5npo1d7$@OM@84K)B8Ij$wvr`~844IjM}!9!Fi--t%x^9~^H z%rCwAM8`lql>%O^=WT8Ry_t3RK$_4XZ*ph8%h-6mqs=DdU(Qwlt99OpM|Zvj(wRix zU-Gym7vJzX-Vbxh{kFOPgP4e&hPG64o55G}adIUX+608^t|e`F{YmRO&1B^K(-BO4j{ZH%cnSw0JS|_x5J&zQjs&O24pKA4V`#Z^Cwm3ZY zqpU#X2i*Js^z>Iz)A4eR&%u1^|IZ?iZUokp1Zli3)fE&_(hf9G0)%wfpIvCmwB z?gr$AAgiPd+ME?AbnVBe$6K-9nM^=|Yht=#(kA}i$dpMxdAIUdhk?i6 zp0`Clk>C@~kL^Kvs!b}tTeWnzBdent6VFYoHA&=lDWk}rq-Ku7lPf6Mfy6!>-0JSC z4x}n~EgtYZ-%8c+Y07oqlW^NM;f_$jruyp`X=GIziWMZDPTW{1l|Y3YWlaE`AOb_> z;%`PP);n*TPzT}IV}{m=XsK3$b$x)|sL6E<#e|BgwY_`BbJu7@?@H#{+=6DHC<95r&eUkL`0qb1XZhqQWslV^1KJZ47B zT|YU2Tr^7+-kOT3Krc06ylIn#pz~P8SM*zRpKmO%Mr86NJiBEXSFeckcwB zH@7B08GWcpN$swwja+|dG;eWKzpO$qdo~(uwg#*1sE9H*0f&mii&(}Cj?y0oq^~Ix zv|pb!P%D&s&swtC`t4#=dp#1jK0QHu8+vKG07ZwF9Oc#S2szXS!f(UoK^dNAo|w%F z{dZFmzPH7+7lwr7|Vifp{82M0w7ogi-A#7{HzSKBNt8#PNrd-DNTtvd`c1IWo&5%t}P zT>J0he{+sD@<@+wHms&}y7vT!c89ouu&6-o1ZKD~(xAr69E8VYw56s#AJ)M*}3qDAa&S_p4ZLx7rmi(u)GOLCpWM*ZC6Th;#muS*veL8!PV@k=XXe}FTQX~y%tvFX zrG>Set0JY6sOL!7st#BCF0B`XlzLxjlm*Uai{_LvS^(IUgj!955F=NfR2r*Co&}^q z^LbDh!5Y2(mc>QLfmTrqwQ8m4)uE<-^_%quZP7xG|K7~mEjmCh zOZWt5Gx0y#L@1uZ4ts7lkMr_$tRy-}HnZR2!5GZomNHYII|~i>uv_+MK%ax%zX1IX zMK3yD7(bGRbLPRaP>sefZKJl#DI*lJak}7TUCgH3`sd@sZtVpSgC|-a_lA13o&Lwj0zVBJs@yYqLMQLY<6fdpRGQc+hNK zKenY5)HtX}@e@iu^KcWz@SRPLHPB*hE{HJ{RB!7r)Q?QBet7jVzaK~-@?}#@28p9F zp5gUuko&0IobL*ka=9ek!dF`BQNB!W;V5hGnz9D5JRtsjq{Df%lwhS%X>pHrzg>(_ z@`BV_lX~2uMgq{IuARr9h|B$hbSN=+EI;NT4;qyUTaSAjkg@q1=yO7xh2_)^aL#A> zjRU8pDk%NS^&SsWOUTlP`y1ZPNr$%mL|)(~ z0nw28j8)_n-$O_Cewa%8n6PsQ(fbQPYt(SP$FkCM(g@I0Z=~63cQXLLIU1y*yR6~) zF|;zC>3q@kak;X<`(~cL=|_IjPa>I48RkhL__YuMP_^+9Z|n41rU0!Qn*Drl)U>1g z1gKHfV#0GQ0$v}rhU#$=SnjKp;B4)@tHRZgtuPp92uayW#dPm#79>ovxypye+l4Ey@*(?x@aG=_+TEtQGaGFU=q4 z4u6sYWH=AtTkg}dQM5{7hAy}wjl%D)-rA9dgUTwmZjZsKw4C#NmR$cEfpn!a1_Z`?(o1T_j&J?vV)l+&;2Y-F>`Wc@H%;B1 zi5$yTGp&0f13*$w9jY2z+iV*2JQkpCVS)zaR~d#`O@7?u!4}n^whkLIn=*XyY-xbd zW=M38+PKG?6KcG5Q37$|a6)tD0XQS!z#mwV84Hf)IN=tpuwIw>e&g95Tv2@WrV*e_ zz*k4SSXkP@W)U()n$2{3Oyz-I$RM7~3+pqyRYh{B%cCa_q!XeR1aJfTOhYQOFZ(ck z9;ssJEInihvL?II^B_XS#0z?BD9$N8UlT{87M})9=$N@qCSF--RMYW7k+<*h)9+U* zUJ?Q=Xl4EV+1(ID^!r9mr$|$IuNbE#7fIf`WBlurz_RJ%9zEh`xXr}yXt-w3qb!{WU>Cnxftwin%1|I2A=)tfR+_0NP%xbt82qP)xf`}r4;m*OLM zfoApn>N?Qcz(8zp_%oDIMeC~q-iL|S>=e>+)RVE4uk!K1(A?HrYK>krV3A5$E7CRu z+U`H6aNne5xT!X)DY~5YYni<;V@bPf*p%0sZhM_JSyX8_sKkXYvDO*ETM=>vy{(=q zR$bH*$qG7Ue#1yM?{Hf)`)KS^?DA?2VAuM$Zs<#*)1q_K%)73v!A1{YV=sx_QTzn? zH~EFX+Fa^(-RkGeM$`4_uu$(sbU%U*>?uw+L#nG*Jgn7H)y8$Hh#V7rGKXz(E-zc~ zNZ1T-ETB~`XRLO1;0g3}k~85^v*5W4_qo}Z6k^K{Lb(8Oh2h!;F2Tnlr9z!DA>?a= zA$GULL@>aO#3G-?w5-9_JQvAld81)hmHHrpD1(k6#4M`gTu?FL7BI-X$zN$8a zqPl6mLyzB$on5-Xd684g5x=_sj61G6lHo8pSZ}1GAR=T{&Th&^%o4%bN^Oz?g`3-z zsPbUGd6rtwrQ4nF%MX!-0*Wx}i}x=_VjWA4%8MWsb`kCwYoJtFn5apjjr$5RG4w7A zM|d=X)A!$HgPB==C{~`a7AA;qmCH3h)$$?b#mQD~rX?=b`TpF}Zi}vR&u^9hd0W3{ zNZasyyQQ1ExwY1i6RY@c=BDOgcru2b708Huam86%@Ve<{11aMNTxtTi)jY*wnVC*1 z8-U887%Xpun=^ihTh$!GJ#xAHcxHlukn;E z-+PA}RO{dzn%F@zGI)bBu9fS`r%_w?uBM%->eI!Jx$JhJexhRcCev5&czRfEI+D>n z5;pA#&X!uW%~Lx&mB~6dJeC6U*?LtTwtR`?mz|x5Q-w@>4-X5pfx;nNSplwY20i0q z!Fpd0lDuy3>C&|_9NSeWI$XSZD?cpoBx2}nh==?}y&9FfeQBFC z`veVjwFAdRMb(ovpIXg!ZY=?18ZYWJOC>%B%MdW25BOs2bj$@BiA~)IH^X+eWiKOj z7O$LP4a9;1NG+EtC5hk?x7>GypLoI^q#rLD6yXFQkcfKeVs$mvxtjX#Q~}UiG+Aox zC2n6tgtdVcOSyVIRW~CZWZ45Qodo`vzIZk{`gL}H+3M+PsRR87oNu2Im5SAjL*mdG zJ++&r@U*_+7;!lrRdvIhaVK9S?b70_55kkSmRHFvzhXhkz#T+-L!5E_2F^@*?fcHc zsuYX)EuP!Ycd*T0LmsMOz}tOMnm~fGkU|Z-PVC*5UE$fRMo^P0$0W;p*T%zmcR%jP zOA*~xPm>b>mk#vRBN}qSc92k`!#RG`!mMM9(9|#j^0T$v*vnX zyxPQiV$oJ5IEr^W7r#6Z5uJx*wAxg(o~q}{er@);YaGxn?2pVm0(TSrz?LPEXCQCU zl&^P-;YTFyW8QzMe9}!MI9(#Ym=ufM>u*fd)Jt>|BZ~^Z9jnu*vDmqFfsk36M2weg ziK5&f)EIYqbUN)Q!8(+38<-?flJ(6l9yk&Qxu$mwA$f}O{cjSvG$fDJr2(XDu_~u5 zdYaSdxtpqO0TlM?3~BV@veB#Hd542npsV{~Johhnu2^Z-WUPbsR43a}mk{;JRsLJPwt1pAH3t>H?DfUYLUf?{7^8F*DfvaJzRUX*K+UkihvYHNC}&` z$OWc39&G3}((A#pnd1TZ%RC^()939e4TgS+b>7Il%`w|Nb-X~Aqo=kkJ)KaiG4cz1 zA3&^kpXM^vnJj*kAdC29r}l%`N|HdU4eCXxOTqeyNF#KyiIz&~{rWiVD`CjcPeTNg zr4n=aG6&eor$XrPzwoSf_Aamzfh(|Z5Ct1Ad5wEKY$DRwJTR;Jy6TrDH z9_?;m3?hVegoK(d>AgubT$nJ8dFMB|g3n?h%GZvRo7^tH4WzAdJT;}Q@oOvlkgCy^ zF4@i?wU)FzgiXCdLG-nq$vNLK?X*0uIU-&gS0PUTxLQh&bkD9+GFz)oPXE_IHXgA= z8G!?dn-h8Ac)Z42&%d*FKd<8EPF*oP)48J6x(ztJh{+!+kG~1VHCQT!-xYj*rfO(5 zZS$J8stBEcjuK-4`R=H=Q$wWjP`=}J*Qfl&EN)Pm=Nd7Z4`u8i3l;)h7qgq`SQQK% zUakHrnEiE$I%{ma_P)7`0S6>|7P_P%l;(BGP&WIdt15F3Z?w2{6Rc*$lFiM{&Jc># z5jhzN?+Mfxb`~}o2q5P2%4#1GOO9a}13UGS&*oNVj5Gta)?D9k=9K-!y!!EBw3k@S z`;=7|z9~x8oiH5cQPJzDSc#Ah(ail=0{mKcCHGHqAP6@bd7EUy1bb$Wc6_ zC>=$4i?mcI&GX=0ZT^zSe|`?GmX`}aZKIx`cDM*#tb3oSZlv_HPO;YAOADxXx_AXn zI2}gcA>_!=t{j`8^t&JY@H*h{7R{#SMW6`Y%oUF>b}wFkiHnQ>lR2lS%QnCHR$7f^ zqaXj)%ef_q(PWH@@o~1GEp0MivU>fiZhBcu5>a~S!_fSFqjj&Czy* z){o#@n$+j|_bLN`MF3LjaaS)$s^QS%(Z~|?@v7z20r#ykfV8{e@}>22eewERBeCg- zBSnpa^X^+-ci~_IT(Vlc1|v9dcu|WUcng5HP)m~{P(F{{SwkKm7^>);gLl?{3rM$} z=E;$^g5S-+~SiUxj6<>vXFBBE^*UabJ2RNttlCP)EX z#F2H$qT5AJaX6j%pWlAsW)0Qy-oZ_7(WMZ9wF|6Obeztsdw~~jj1b_O`3O*SRR*AJ zE8LGJGP#ZQa%Wxw(Ng(o(6w}>@u(4EAdVc)>j1~jBHKVbUPOWCOpROp(M%g|EE=;E zP2e$FgdISUQbFie?(2fbl!FY)(bxR(X`mm^E-O$cP{Wkk5%#QePpkdj_mN|FGQX-0 z-mR=F3$e73PcjbK~?h8Kl|w3OIXc!8q7t zz)_Z)#4T3e6rc}53BsYpjCtO1WZKIigX=_pT-ce{&|NjpRW)}(or~1;#A~5@+tYTKn?K6SL`RBP7{e1Fp0U|rR0F9R0 zLv>0~qYga3Ba3&x7^nXx3I4n$di85$FKS?$q%$VVjO7IMiCt?7)Pa-+n+B&CKI##x@fHTuHu;$fE1KL(Hsv1!*1EX+^lSfq}D;K4J}>+f{nC z<8U;We1Rg_{5`t4#d6EN?ZchR=UB?vjo~+id2lTZRVQR>CKl|!ZggarFf9+t*+{|F{aP$C37w;*W~C!~DpN5QbE-HD3=BuSHx9i2kfcXA6Ax9v(UFO^x&a`>J^ZtSBdV|L>6g`3caT z^gfh)aB@WIkCyKL`Ft6ib(a61VUa~r{{pwh<)^0p|2=pHeUJGc1NuKNh*SZnU>vnH z7nkIJe{3>--O$uCi7#J_wRzCTLVh8j`;UA(7_^Q z2g;=p#Qy}m2K%<{G8r38C!x%20~C+s-&m`n{{F2FFcprepM-^9eH)A~OK|@eYyZDS z@ITKYnguMFCSQfAoQO)Hczh|vxcsoy|3SR}nWuzb@6C^5l~ky!5kJRaDnrZr??yt@ z|Lgnjr}D(#kcXzW+NhRl=%!SVxpi0R|2xLWA%ql;vveTm_z6?3E-2(R9eEW~`{5rZ z2r^-ql-Sc7RbZp$<|wX#pOc5;^SLP58jVJxkG_To|GUDUf;#k${?r>NNCJWp}!&@zn>7HmL4! zB@i;b-yDp+RGXO%m=w!MX1Zciy%~MM@#QpTy7XU|q@2L?T&0Wtg(h`a8D(AAZzCbX z#cXrxs$+hk54TvHI63IknGfhP7XcW|=KyS|q$2ijb*qlTn~wKYpKreV&KL%V=UPn7 z`opt8g9ZO!+tVr2GOWbSh_@Vj@;FR`XK8+BBXd=+0W=WO-RAsCC)vMA zEB_GhhstcIo#*ZstJ%%EbLo}!aeV-c8}niP3cBGqYN{nQZB)s?VoCh>A1Y!-bCk*W>rLXrQN|K$Qx= zyB?sh|7XvmlRUkD-pk>Rc|FpEjuu7jG%55BvNR`ww;(6NXi(R`=E6b{P)Gm(qX4lR z7R%kq+-R#S`^ZW|@d4o2RzLWSnZX)a%Tlz6!R*x{JJTH>)l5CVv33>EgA^`xJYIm2;T{NtH1Nhbd z%%+d96u(zHL+jm6lkifFvpMftXiZ&q6hIn=v4V?%mZrb_$le|PCBRwF?}qQgbde&E zV_z(Nu{qc`MtpB^#DM%1=$6oJs#y2=vpLq{z0ce-w0aq7LkaY?3i71$ecQJN3Sdss zt6)yG6`2$}{Pq_(=(y;U|1~RMAYN(851?#uqeS!BXZ4K^7l4*1fuRHLKAJH(HH~#1 zG)-KoMMr%KP&cKLHFZGVXa~sW5zC6UCj>Hp>*X(c36aAv?QZG>0M=cFb~m+-CI(1P z0mMvEEaFB!fsmYZW0g55n-$2PrwaM+QPCga&Bq{N9f3J2xGogIK)66cBBxd*zcmyk z%V;QOJgk<;DEJ}fOOD))$&25uG5lHYyy>WoBtF=Sew~*-zZWyXf3L{+@R5OjuKY_# zV66%O!0^cWp;eRB?6KV9=a_`}+iag=j=@%=89^5J8&d3c7e?z-I})N1{vOY3DNkB5*uT=%HnCfaVsboLb6S0NcD0FS==56m)ZYe{w%`U!7<~lgfP)Q3k z4>wP@*wJa%?>9cF{p&eD!&ogpT|Cuu{eHq zHA~W^Lh29G`8ml$bI}BGkn--PiP#nHUg*zN8D8_bZ9k>>SW&Y|N-a9_ zojggVACQ_989MoaGiKgkB+H<^{v7LqZ5sh@h$BP~mB{G10QvS?r}oyCC7xy$mPIBW zQzxSWT-mH6}|E;oR;zr!OnW)M^&m1{nS@XUk|J?ttaiiBOFS zb_0cNTOwmK^oM-gf4i?pen&*`2rXBb(t^9Ada2gT3|m#rlz^f_jJtLzXHRh6DNsj2 z{Q8wmyi~2ZC*Hww9q$LeQ;*7Ip5$Ev9?MMhOo>_msPn~N8+_tZ&X+T9PybNvm+Anu zs4r*6*Vi>r`O_m8$>{Tbq||_sGo!t&ASZ{+DE&tamxAOgOd2O-!7*Du>`Ieyy)v`$ zHF5zGt+{jDD!n+NhCycYse;|v#z6`$hlR1S&JZl@WJf$Ut5@^btjpT2k5PpfGfxKpI1$k{a&$wR)jhI-MxahVxPqfED#dRc zs(XnY$3}tlrrKm{c+2;WF#7R3pvur)Sp0!DuB`QXBp#`xi2_#G3Ec9XHW7zrfp2qQo^i%jvuDg5!Fss*iwzFVovG7U zIZF)=;@U0Yb8q%bYJ2+t0de>7=SZ*ni^^KxQ7M(4Z|W@9gh6u-8pHv5hU5p2`zb_L5HB>Mzx-9T|G7Ua07KcaPTEheX#*Y~6I>Gt1WM`^T~P zPzpWK=WAN%C3s0va;+f6`~x*!E>AEPi1PkDiE(GjEVpi#X~UAQEBRjS{h zj+D6eeEBLIY7}bP=#V;mMZ8=g#5GqS{w|Thj5(CugocP zMM8d9OAC9d7~h=T=6vu}2RN1ERI9d=JO=9oTG4`kKS%)#EdAwVMMj!bWz&Eu^7VUx z&F>LFUwbz4p9)kBiMNk%!sH+vh9W9wO(X8)_Ra zYiP+4{yr-*_wO*@K6T^I3@>KStc2;?v!GX3vNXYk69k<-yAY43jolhf4oozvC-&Y9 zHJ^FqM!m2ZuIrtY5O(YYF?74Wj$a;@D4huVsfaCAD#^M_LX*UXW&K#mnF}(X{gF$e zg`BUnSlQgY0z82;so^p&4|gv(-AgJKi)ta=j&>y^D?5BH^Hkh{Ite2-=BseLF z+5MjV`*4lqAAnsD2zyYJhUZl)b79Ufl_=A$v(X)}l>1Mm9wI(O9Sitg9N+M3=8|>6 zVN#Isx4DY~BqY!x5iVzsZ$WwJpJ+RVAG|_R%-t7B6H?9DM-%7_;HJJ14Oj*y3=ofy6?o9*4*8Qf{Bf>&2s+-G z;w1W%m<9?W`pOD}3`D+``ywDfpUIj-i>3;f8+Bj$Yr_8;_9H?}OiT~~*!mTGk29zf z={h506A!-MK<-no3dMQB?-tJKm?v-)x~TN{)&A3g#C`;*>@8ECSt{^~d(>429RZM* z+NMe&hR0ABT%<5K<}H zcxvU5#XV0)NqPEUgV6i(!5hJC&woP2l86wydgL%7Rf(ro0y62^<=9A>PCavJ$u%cx z-dO&zSA{ib0`A|hP!NK_k*V`jedCA9PMq+))Gk%us>hMPK^@^#**?Z5U^fDD6maaRc$w;!ughQ@T?U7s}@WODSbL<%l{ z3BPn{aN&{eDoxRhm%_VTLFiQJ3vG|WMqYxc?@7g|QpPu+VbJIy0*%hhC^h3u#HcJMT<|$%( z$hsUTM}|_B`H@-*IbfKl#?^;2($wlhS~Sz%<4-w~xXk=}1Lrry6unT5-{*)QI8(GV zEq;4E)bj>uycY15=3lgWY}PW%cB+OkO^40jZo)2{d@G6`!alE|n}_F`;jBq6XwCp> zTod6oN~zbb4-&*}fB@_PmI)T1ZhLNR^V zs+pIK1q5~A_GaWhVrkWBhmoe%PSv;hW=a?v5>Ce)@e)A{Vz~6#=$a2KZEs>1e3N-Y z3GK0JHW!D+2JaQU)oYa6wB%67-b_+XG^jA2jLp{{BJbe-ZLv%+IMlExGFU>DuCe0p zlpv0w%rH0pog<`SLhwV5$v6syO&+|FwP&F4Ndm z^Co2)Oxt-)8X<&Fegn_Io|Z8QoQzE!{+_1GV+;puH$MUwb$N~q3>`gWIQznB0zG%0 zs^<9VO^Y%ua|HCHB@Lxb>hQM$ejlc1!3QE)?kdYKcgue_nZ8KnA3I~-N!d3pD)QsJ z<@n*!DB&QJUsZY9&5zAZ z&uwr~J{aXVc)&eDxlW$)SiMAxN?8TX&e_EY5e#Ph_XLsYM%0nfMblcGlvF~ymN3W& zdDeayIA5)=j%s>uUKv+}0v_iawdwQvk$>=Myh5_jvy!nf&Gg?*-zNfdLp`Ll?3)&4 z@+@%1wAj+T*h5B@-5#4pZDW_ka$cc`ioC~DZwKin=U(J(oFkX%V1vV4U$*uzGN@m>{5<`2K!r((hrIFv)3DPB!{1+woI zo1GK7jve#LE@3qu!}{J|8EHfs3pz9X=wjMe4F@xZP|ZeZ_5Y;sh!VW-C2A%CCFtD9 zN#p8)Oo_o7WJa|B9D($88!=*qI6LG6bHe?KfN;_U9FLEQainF$1rpGGa9q_SC^WU_ zCp^5tV)Y(GpNz{ABETt|I$*|?g!YX%J5aOiejHpnX)mK0NfRqg9)Qo;BS-v6^|wo0 zgSB7D^c%~Vlq8ET7y)5X@C^9*?q!ci!S@@f z!QDE(apa6Tpy#Mv7=f71f;bm_4XP#d)n6M5Y=V#{f(`aLIeRp0hyH{*Kc6AG(L?nr z;9nI~86I#pE`W3gOLVb-{7(CqvBp2r`&gm>nl+I& zB6w7hE9_xy&V-Nfz*X{sFG=M$7l6Ja&O~IDeojqkR;vEqt4ZD;^6=X^hIsJP0#eU5 z6qE*SG0eGiD?q!K-6N3gtW&Gv%L7|TKwMEuk6Ji5&I#&w3bK-wjRKBW=Z_%#R~e}7 zG*>SMAF}m_^4k9-Sox8kI-%>L8R)|fGDb>M=tIfB&w{4Ys6v3#)VAC@|KQRWcUVn-h<@%d#c%i(+ zZLY_(Cuv!r6XVRaiIxKkAvp*VOj5=;Cge8%ZrGf#dLeGsz7nUw%H|+*iAJ6$%<_$6 zVv+G+p=NEdz`bjw$q8mHc#zFPhOc~qHs^nj{_D%OC!+9;R_m85qVOXEjA}-Z3V2mD zLWKa%okFXuhk{gtFDhL9=PJJ`VPhrv(*t(`Q^P09%D2-kd!;(_IL#wL&`2e>AO_Si z*Nv!v!?o^^%%m#ASetK|CwCyk+~mRURWC_( zw!}4B&e=N7$BG*WK|AJU)CXkG1imDM(WY|s)g5^v%VA8BBiVZ5u4*)!+}))NRg)>uwGpF@}sz&)=OOtA(1$tuZUA%I8axbPjVWYeNR z__r=d$(Lt%4r56)B1}pt%2T7k))z9X`^ZPE4Fw{-;jVR>4W>NsOP+9Iy!Zr3~v^UlXs`##8;o#X@ z9d?c6QVp53@CqVI>OUj^1+QQ(+j{YdATDlWegYLGucyA?S3{rRF@laS8 zc@UUurPt#XDQ+mjRotg2C1Q0y_I;ZsU$TP)C40}Qe?mk3Q8~9kSrlFjqDeNDRXYjg zK+;Rm+evnEwu}e0^uxecLN;OApB+_k`jZAZo3jF=oosr%q-z{UZcT6#fK``H-Jos*iH)Df(K* z(@OF^az$RTH#nh%8kL@fDUh_(S?m~nwpQfC~QLC~zrc-!CR7z8Wl zvbAeB!82NJt1JbXVBd+*u?a1v7k@XLV)HY#slt1}4T|lNk9VhMR4j8T4e_-@CRW@| z;OriY^K$lhZx>KVS5&M8@!MA;t#vICeOA>qaa8mofgLiEmcd%hF_p>F<(eTii3d4N za@6l5G1;GaH_*sw#VgA8Ewm_~6R+{2|MdUt>`+9V4Bw|raHc`>_}tmgg6*@A{V{ht zTP`t7;r?uY9T?fPdXQ1&b{S`=1-xqiyq7`joH(+jshjyMURgBa(pf|C)4DFPXHCY2 z4n{7d9YZcihCED%CW}p93Pry^8V_sl@0xh6*a*Ep5K6T5juthp7l837Rc_3iQh-nT zAn#^~ElA2EtxQzB5IbndD@N#Ru+PYaKc!qU3k38ue#Hak`vb!L!0!HvY4|6~Vx;hX ziUiloY4e#?W(H1CYn{Hi;KEEQWCX{4euQNKS$KTTeOZ9ee1{_9L? zO363x)J+p-Vh2q*8XB^kQ<;Cr@0rPRrRlYKIX0)Fq7R#4%vVCHCar|5Qj?uF7u^|M zN@l^+XoEsp4$P?0l1fs&DvW}~e{a=zPmwj6+J98+pz2j~7c)%PT=?3{5R$zl3jtZo zm4m7jwW2Hyi$<#fRx2QjCF6kT&-12pgk8}ET#@b}T7(jI0IK#pXprOSGf|DC8Di`# zrmrz9cH0tz2&Ca^haH_poytm&| z)fu_w^LonR@u!8Z)z*&%Z49d=3=dR|v=YZz0=O6D<|Z~$uG>c#*t=!mYwBc_f_Qf_ z868^}8fuI{>RGNeQPdzy*nWzl!0<&jNs;l%%dCMPmeWWf+4D$~wS!*{s<3u&szg1{ z$5mW*Rz>c=$I-AwCR^X6r=y7vrSoZza6M)v?CExv#xA?(d%m)mZxwW*+SG+Z#SJ-=^4nUmDH|A2Cb%K?**s_8vTZqgdv}Lpx zS(#GM8q^a$B^Q}ynxb8z`ix59Q?)>xhTwBcRj$-;Qm^KO8H4xsdFxGga*Lqz6WsXv zVjW#f#>3yQ_=tMk#k6kG11_{Y!>)d%IzM&s?^i12rhSU#09N4`tmV3dyb7ldkMW5gRzu; zAHkcLZ9>ZgI7So67W0C9a`-FYQ%;0%TPu*(*fo5r@~cJxkjifbhv_i>%I)G)8ID_A z^L$H=zIk;zB5Noi{rPigZqe3a!Qqb~VIEvf_V)r>*ixL9%D|mxw>;OQv?)2zN(Og; z38frLAZsyyFMI#qwclW>w5NaL;KttG;y$B#K1ZKAC|vP!YI1a7Gae=c7_W#!q)~31=98_O%@_)sg4lI8SITtgMC}B15ng^v}St;zkRs5 zEMVj@@|SBNbc4a~P;JG;tE9O(7|XefZ4MV)PUno%2hVYtay;*ECTgXv=Parua|_x?SlOm%^o$i{j?4ii68qiumDJgw#Rhu_VpZU&y=8;PAM>}BB$_r>0-vGX zOBhj!DyJuBHf+`Z7)i##-T!E`Bz(OW7U&#QEv0`Dk$MH2`Z(6ISoVz0V?26{HL3!t zfBK1yy3Vn4!*^iu(%0a7Y$3`)U{C*f$KJG-iU(+5(bH(Y)O_^7xQ?wSwz%gykkCa! zihZ@N4y^-ueZlpt#EB%jes>GQ7INSOH-JXy==b)lEPHK_LMnwUKm7~n?i5<~#lfan z(_?z`v@AO!w9wBE`aLrqsFO5f$d@L*l*ZZEof~0iUk+Gi7CdaY)xru1u6wf$ONwa2 z-&voHZ~QUaYE#Ba4WO(&Q$FCRyY327L* z5$PDDJEUQ#0frdjyL}#U@8kEq+rMr(VCKGRUFTZoxz-X7{UN^Dt$#20j2b9>zHwjb z{1ur#OSm8;_S`zoe7k-#)mMRQtvT`eQbsx#A*ZMN9g+En1n$nsg-99Nc-G?DcJ?Uz zKpo?BbVavLW@*7BF`wwqP3_|He17 zC7s3R{zGB^hJWcG*baj8ox_#Fxyq1-W-nSQph3?z{t&Pd>30Xr%r#TG-M?B%9cEPC zbQg$MRWw$)YyQG?>+OhS3fsr73?1u^2`=Z#)Mv>BqJeSvL&5tKw#%DwYq?;Zd# zv~0tzdirt(!zSgkr8+ckrAvcq9)r{^+`jjEmPy&$mx`drNxXl~w=f=CJRQwf@;0tN z7-U5?8S-NeWvJb&>yOWPP=ol0?t6i>8}a%c6S1hDGREg;!aI$tIg!;C$DydMaiXQ5 z4%)bFZJUL$P;&cZ*u`E43_DDOAScB$svgr%iR$h_^t9kor?sV$*z~8==y{*Hl3?48 z4wp<7)pb505~t&65O|M6?Hcj`LRgCsyBtJ>^YXNC_Q+wS1Zn8HCr(2UP=^C08Mn6m z6~x#^0y)nDq4+O-Z!Oqt!%|lVZ6v?Gk5+&l_YV%ApCxrCaj5-$)`Wj4#m)qY;g%Od z1mCPfRZ7!^KHo#uCo8nsR9a_E@-6=f+SAeERU<}CPSPB24cP;HQx!YYPB+9|pttur zBL(VV1fiqRgp1#xC6@g+`SH+-aCALi&m>1IYph0cCA6fxJFoGX+1P}Iw)7By)<#dk z9WInMLL-$rUcyA$Hez{R+C=NQzOu|D5vTE5lbvpv5uY>rb>f#M z^8$nP(NJOMCW=4e&ZLQesl-VP`(+_P*wbymhKz{<76rXxiaDx%BfG?$|vGl7teo3`!(N1sdx*wD_u`&;odRVFbVtal9 zf8CuFq-hf5A#O&DJXNc*Y`v!h)TZ$<$-h9+UFI5AcrMnolTa~MatYtNVs1-)Q{GY3 zOZx~Y-3e#1;j$#(Ul^kn`T@fh=HRUagO*kHar~&DK^$sh$)$p_#c*+jVJ`H5RCqT<4s4@A^d7{< zLSQ4em6w9-(kAZCaKO?BtwLQG+0iw-S75g5x+QT54`x5Ev&Pbk9ovO?9xuq;$Qf~s z8|sObq6yWHE=$Q zgV|V9e%1S;)qbe2s=Jes<{n$uOY_06r3PBR+*gT$9Bmf*;skadf1oUCk`AMFD#-D# z(o8*p!A(Odni4f4=bM1KpzXs(7TJ`e`c0KpWRpaoIUZp)y?O!AgCt$DIK9f}E<=}` zf^q+%-zB2!)Dd6Bn&r2s%6^7TzAiLDN$bF+<#~t-oEQ=+ zGwt6&23ibz#@FefC1r!&1`J7Vgjt+|G4b7TZ_OtUo_^DNoZPi>=F)o-aC3?R=cGjH za3p(WOF#+wES?SeOt{88Fs$^c>Soe;RY_I+cC7PG!%+{=)fQ<*4Us#+L3yXgj=M#N z7Nm4y9nC!>=$q9Jx`AF9{6PPyZo3xldEQsm{^%$+gkP4$8He0tzC^ga{uwHVw*3TR zXV|r{S=_KB*dlg*;Mme=VBF_zQ0Yj}-#FXgaT~qwv31A~gqy}S!1K-LCc)46;^p~f z$07doCddz7BSJ3Y?=DyzZ+V}tn{jcleqd|-%#chUVPig7ZqZv%2~-x-sN|LvCHd6X z3+BHHT)ckSOk9JlWf(r?NHmJT6tY5B)Msw+=QLpc_y=r(gBBuYM*B zsxYLJl}q}z31C|6AoI_nHpnrk{9-Qx@ZIpNpr98yVbl!`4fD+}%x4P{4|bZIL%NfA zt?HMfLm%>nFw5XTY@g{=zvhI1r>svQzl9Npe5Ye%Sj~JX`a)KB^ltz<*+_z^s`zMJ z*xGtCA>U8&OTXZHkqgY)Vrl9>zz)^p!Ylh2#%UW^-Wo!|gDik;j}^mq$RPmo#J1fi z=kL5+idK{|3$V6|^ZWG)=N7NkXau#(_-s{v&?kJdBnmruqx#+V9SQ$Xu4#6+4Bp*0 z{g8wVQ7$zm`cb}>Rt7+|%vl?LO0>EEA6l0CQ5vF64qRP}hW9IE7RpV*sTG8R?`%zN zja!rhc!Gt?Pw`w&?X>x8TiI@o|$gj641dO`sgrd@E1 z6Hc)3kwh^POzkk$o`M+66Se}5HRf8No^i;01{de@Njk8BxJc|H7%%`c9P%Rmj}3Qf-- z2Z#Np%`U=Sy;^Xijl_RtRZMI*+-S0eHA@lCsfF{5s{k6@qE7U0d^7K!9SmY4mb{8T zRa?M8>9X+o?NZ1j=KIewM8v9R2l?Q({SP$`T%k6u`Y)M5}KU2}_%b6_N_y%=al-sgv*L;a$Opre=P5DVEL#21hqEW}j*gHu%l+Ef}wVqRtppO#fkWa{wYJ-0d$a?ahdXP048#+G^;9y;&utgIIy*{Tz68r z-BBj)D-(lb0~v&}538mf@SmD)7FMCAUigGxjL}V)#zXsh3r$ggyXVe*I+qIBUZJmZ z-PuN*eP=j!Yh{QtRsfnn&2azXBMlEG5=Y*3M^CK2Ji1GHK8^49C`PF8(5DD~=bz8N;;A%t6 zwXWmGZ2q1e!;20v_R5b8ii_)ZYB&_^WZ)^gb2)d=J;k*shZ9u3?b-5YMAL4D7U4;T zesNHKaqWv}kkq;H64Z6}5bEaU`1Kt9npET{eKQn*AF zhbzTZ)5U#WGVnBvx6XV10@Dq2IOr3M;Se7=1sK>)9!2p-xKwTl;*?0h`?IW|HLlqx zF&x9VGyEGdy$$<7Xy70iAs-lp#2`@aOBvn1<+2^9}$)Jh+ER0p1Co z5!Q8g_=c6x=sua_WVF;uVKULtD-Q1^mBd|@T|P;cKnk;m<+ebT`-siH!5%}#I|F$! zMirTFZS;D071WCz5`UPt+H@#Px#$Yk6_pK5ASvUO-)jstQdo|K>B{KjC{Wuvx`Tzn zv3i(DZq~nQB^;N+?=MZGPqe$xab7k>b+#S_vj=_LmZo%$b)EI-n>w4T{Pg^^q+`3T zi_uv8E_k5$`m&XP^ zMi3&d`!*$#`0o3m8p{i~JJHb!B6_FE6jm~{&L5|pJFFcLGp@%Tl0lW$Q{8JnGqDNF z)9kvt7`vuc&V*ez?yL53|GVXGFnllrsuMJOH2PjWTKntZRanBx&k#-H3K+ zIxit~$dkBG)XOnibcNp3V14`hR7-Gf$2ldxMXKlJey@O8MCG!TCc>v~1`i}&X4?lk zSMsxk_r`5hFB`hYB0$JWQjl<-PJ%akPlYY|m`i)B(5${JYez!9)#$Mr5}I}0qVrdC zs>bJ1CE@PKj-Ry;pT)L{>A-tawd_(C2txak`DIUqG{6Jm5Hyu-B}E*iwATAuMf)p# z{2uTu$yOXtmnkm%r&mF%Lh(H#o$v`qIZDUX#0fjfq|hD8sgZOG9R#n{7t`~@#hf_w zl;sEUCwKYW7iCz2r^%!NbW zzP2hEdY=slmON|ivt5V}xn{(^(TEbg&A73cy&@khpo}+ugfGjV$v*ed+Vl5ouTv{c zehNDpIrUrL}FBn!fkYOH7YVMeSgb82P>?Q z?U%lOZ2t!Ra?7we4a#uqzNO|8R&$p|S|-O_X-@LwBw&S-s|j1H&n5>;B{E!$G0Gb$ z$|oga@(Ci0>dzdCrcZbcyQixu%nU&XI^Q>Vn8zGLjifM!9ZCmf%+YUsS||GgLOG|H za*WyTQIFEo@BWf4cHWT` z0b+Td2tsrxJQ1rN@8Wk>5f}f`mPhli7U*2O}h2-ax~7U(;(NwCkWw*4aj_P`vCDo^6^Z(HBhm3Kj_L z)|31cZ5z17T;(_D;h@XhFP0|{mV!e+&-4XplKYpSz^;B_rC^f60Q9bl92Gr1{VdKY zuVq0;-+umrSF;Mhf8^F;b27bgr~2UmJJf9;vc6eF81K7zn z8=#9AZ=8m>ES8AXIw--qx(+HQ-LTdxOzTQO#ytyZ_F-EX=lkQY??)X;JtW2N= z2V*eP&ZaXS=3Dpn(Qn2wVu|bzkXmTIp0o)w`J~b4eZmc63TFA*bBCKzBXy{gD1>?w z+>|k1r####WI2l4`pWeprBB1c6KzP7%o{ajl}`RvzKFhe{`!?ZhT24C^ZPHSNj!Ml zUy-B&cKxc&XDoawqgBqfmELHv{M@AH`5a@=66o_H4dGdv3^9h>V1Cs-dINB{Ez(J- z;T!lo*NbqIk=j@ZPx(q7_L>fmlRhL}xZ{BP{9EPv1}^kn9-lv0^?rC-rPdYwp_cSv z*~w#P^ofetNEYiC(;q#HCP)n*hT$2*O$CPwAc^0w;XU3ms)l$mGVMvMm<)w*mJv}| zO-W(_QP6LKgFRG;>#3ac(DQ1~4$$SSE_ZC~<-$WW!k?XGWuVl2^^sy@c&E5eRSAz#p zlm}Y*09~cfn0;bAHtADV7wIi@{#pjt3jrD^4Q-z&hq7va8rI-UGiK9py|Hx_7v_EP zLGm3RZren&8Bs5)&~`#V{3?IKx0@ji@jlc)wMx=7=u|g9+dd@rezx%)FGXG^VQr4w z-XcVmlNgYbIJZXLQ&ksqeudL+Fr;CHtKv43+IO#Mi=p!$_d~7!WNS*ix;Iezp)$~{ zSQ|8EF&P$YnUrWj|EUH%v{~oHtu7RT^^$l*ivh)*SnL%($syE^LRA{&{Grp+O7PYs zp3YGzO>)HBG#(CZ(e7UzvRrJWSuv)*ViUQa4^Z}O`nr!l@tG9_oIIgArE%sp9Y2|3 zLow?zoy%9ifC(MaZEw7KBhn}HgmTl@?Hf~U(hqp_-AZZyd}svW^&@wDN=aiZIdUt_ zoY!$hddVe;bTc1sQ>fz{&hZ6_W$s8mO0Yu1!@Ta$G@Y{m!~P0`cWzZ< z6?~y_Tcy71^7+qC9bQVnYPR8(7pJ_YYI{P=dSqg@a?Xy!6*0Y|dA<`ydwQjw8^&cM zEMFTLgyU5=*2!f*Juv-iF7oJoRdK&;&o{n*vH0fd%-;rR-e;*T%Ya=i;%jo`N**t+>@x6TucJQ*r*)wX`1)*=t`c7J zZeBkU1jC%t#iB`|(O%wa&$d#ll8bMzCiggI7#FZiKN7+H^BTW%SLT_Aecec#5h{Pi zc^xKkpAj22Ay#x-{$&jx>16tCbGETnxem>nIxqSDt7PV1Q=k?k@!0Gp6qKmD{u0as zm)oMd;S_RH$?&hxUn1+S9j+ghrbf4^LFUztx_4a+k-6xo^=ho3Hj1?GV_0b(gk!Jw zYJbn;jbl9#>bia5fV#SEnXhBqX+Eaqi7!S&eeOnYsH-Q2tdZy2xKbkiF5iD;oTENV zB&d8HpaL1LM#|()#9sUTRuYEy{#u}md(8&$0i4bgnMlz;y_$T#KZ^L0$*f`gQT6nl zfXRM-PHbO7Sm{|U?KtJ%rNhRa_EjmuX~C;)X8HR=k*D)v_fRFlNM#CN&pLIdkiF4A zJo9y8CQU&0^+ln~uOS5aPZfjT=S_l?(I9*RwWazy5cP_4K(8k62US~HcJ<5ALY}wT zA%7DY=zMMsaK(v_`H5Z`6_!`c;MI5V6%Xb7-0a~(l^&{BQHZcqmbS(l3ISoI*b!re z7@0VB?Ps#_#c_wkouyx{b~ga{0bTYwTmISRcbcnH-$KGkVmul*ugRF-`!Vt6%EyPJ z`uY#~H1#cq)2f;q8YPXnH5g!k6zP*rs9OG5_t~eOhD5{aKA;~GbA=V@o5J_JS-H75 zwikD8{sAw)@rx&egp8J$J~jGkSE8b?G4#cd=V;pRy|*Qu0AgyUiHQ<$aN9$R(XXle z{%qn9J^Kch=Vl4=0byLp(pnx}gCk`o6D+oInxRR&U*BX3rAo0$#Ju{u?QXuLGaryP z)4HxJ;S&32fGob(K%d;BO@VQoDo+V?j&^0xvzpFKI?sMg0=f{m!8=&etzrjF2Hf(L zg`%>g*U23viZr#raf)$5Z5yM7);8OdI>Ja!qqwV0N9>!{LJc)lTfe=p^Jj0;Io|*h z-l8}fk6&cVCo{m!`cQYuEoa_OR@rh)f#IR$=IV%J!qLv|SSypRK{XIj*+b=Go5Eqr zZexOwNWbxYJ>Rw59EMFN6{s=;CaNEsGOqef*hN^%oR;m1rvtGeZmCeq(VLs0xXfXVyYNY+>$nz5$sObeTILt42|bhMqfp*_&RL)+kvd zSHLyc4Nv@&yKc}-eE`{ZYcAllgG=9octq>nqy8SG|FR8v+}EevGuHDw8@u|qFguJN z*rNq%p7OX+5pqNi^)Uaf!+g#Yy9hwwjyerQ6 zw<-j+3qo%F|3BqA@hygHxS^D{y*S|OD+Dp$a7KJN3_mHg%bu5!`jMOU@NLDAq!|~4 z+!Sk`RelSMc84(g&(8d>5x%B-e0?lZ#XPL6P4KcAGgLHv3J`DQFACj7^6d_-qvM2L)7s)q9#3AORa0?R(t z|M53;h+Ei#U-G`d+192}cf{*dq(6jl&EEd+mHhkcE=iXs#SEh|pETp|$4^7PR0a$* z{i~OM=kvcGd_5nhDdc)?kYJP(ur2R{swS-dfAfF!`<)@c;1c&T`K~Pee^>eVZFzl( zf%%i*psM>{Wk@0@t^?QgqKx(b_zvG2^b#GKzIp;KSLFcyGj0E_0UZx87}NIf$-l$> ze;#hW{Sa1qqrndOKi?9B4PeO-+KT^Un*aHZ+cCf}Z+a|@|IfGV{e5vzh+F=36#iKJ ztNFOk1Pt@`!S~Xs|L5iY{x|U+fRTB;hyNIts~Nt63dBtBfMJrZeKM~4UvJ6%1eht% zS?>Ru>UU|tFj)uiJ8b^fTbclSsI>Q3>3>aingreemwFmm*v$WW%gzu9CD6vOLSXYb zk;r2prWURuiux?|)oP60ewitO%PSz{avL|1*D5Y@oke)3-b>oSx0|hC5L?gdQ2u1U zxan(a0Oi}ui}SC}o9e!+!iL|ExIw*)Wi2?*zUnv4mUS^u;&%7Jnk2UCE*Gk60fwUI z6IlwiM!z4DShL0M5@Jkb+msGl9M<2)&jns+#a(;WyaqrtDts9re2N*z6COIx!PwVx#& zI*yl(PsSbt)QB32hm(90YTvHgF-!y`W7dxyYE^Owx!rd6BA4_1=55<4TrO@(nJ#>P z#gzj;9QDeB8Y2_UQsZfT&a@u#+B^k9xr_lak4(z;pm>z60dp)p@z3Ic7+iOHAWej8 zj^5gs1sW;n02J2(@{BG!TX1RVyiN~#h1*Z2)4|iLC!XF&%Bv$QVffp|5f|&r@;otX zc9}XAHJ_=?f{p(8`o`ynK%0lt9Jd{gqN-p;az45E!&%QwUBua`GobTk(?}aCnaY%L z1!A5>fKj#{{FY8ji(!wP5R%~Bp<+nHkQa)nVBPbE*%>SG)o6%)Qa_VE#5VG z&%63_sJ%6iOfAkCt6<;3IbOXT!diy?tTLTbJc*8J|9O=qZl#Mc~q7d?l)lfj?8N&cDOhQrbP?i{^mh^>aB z_D+BRT7+Uc8EK>Xp~CJK7cw{0g0AiC=Q;7Zv;Q$F@7#XdwSB8>S@u0FfnMeSmCr2k zt2sT}YS(N;|p!%U=}=auL{ObV!AQ>E!|-vx7um#lP?I&I16k% z76`J`Dow|k%J}@S?%$Ze~5B+$A*Gj>J z!(dQH7ofFZavK852O``Y>b5GQ!KH00zI^~)PBu%q_s)GK@iGoT$T;1fNQ-pq`pYk~ z3~ZU5Hw*&?vy*L%PD`b_(F0*KN9X7%pgQ>m6JP~`LufDk=jDUlw1533V02w)xYiw> z9_&Obibd}b@PltRnd~WBOq6}51HPM= zU3N(ZvrIygfE8oaP9`J%tr&gC2cA#Y0`c{va$Y~jv}{^!@)iCTfOcJAcAiyOj@)it zjWynR#P%tLds)%fKi_(4bfsRXM?u({ah#D&$$TK~!25lJrSoI1Z?-e8cAZ~6pd=3w z4$H~5jjSY$3Q<6|Y!_Dt6?4<2m{Q~MFRL9f{TjR)HQP)t?GA;_hC}GdcFU1h9eV34CQjRJ&SyLL zIk`{zP)^@cTor7f#DnVkeYkcOQ`CRq>GeW3-a21FZX#nDjjBOKKC zkY~Rv6Fh6JvOWQaOg9EAB}m}dhI?!hKq?r73RYD=Stp~coc7(-&?N3-J103 zT^Xl?VmuEwM#?Tu$7RF+3r&|v0T(mUt{1`zw1MDI6yHx7Lx`fsU-b)NUiiQs-`|+p z%2tGJ_bhaHB^ZMX>>sX!5IIm6hqr_zXRn3NLmZcZS^u(tJ|WY0KarQVVI$LbDstMa z)ROZm3z<0*^YZA*RYII{+<2#3AG?don4DGLpHfJsS7DbAagXE8Jfri~Y;+V1OF8lr(S{zTKvK)tnHf!+kJ zq4>x>k`VZ&HO2XKutUe?3{ud&o3%%Uqki|xSOn4m^uk2ZO#6%J zmCuy3hUNpvaKL3VR%DQ}T--^Lf_#TfTa||P z$KyZsK{P-@OY7PQa1l={DQ4z z!KIaezoVWi-FTwWX8c`U#flnW9oRuyh z9Rq%Fd0+Z-r5+U3A)iVG>fK#bb?sAKalVbwS=+~zjVf+fd5bvNL<=im;v?s2f zaL@^E4M#r_Y%}25Se2hb9O%g()^%(d^L5~Sn_V5#6`zJkqt|DKFd~eYMSpJN|LPqb zbjyy}^s$gy`S$lm@h9SzocXuIK7#s^)H)}=KeF$l*Ju}ja(2#k_kJA|jp7_K+wC$- z;xFw}s8h7l`zCb_11A`-Y_g@5E=@dC6hEgjqxIQqTKm;wbQ(zU60%*I+;n#LtzE6b z$~#TB^Ux~K9P;B|XV04-xwVmwbqlWQJ+=F?VC`XZN#}?VCCYs(cQ#?B-!9PBY^}W? z_gqJo!0rMNiAp6Es&nxntDr_RfDp#m7~x$v*4yX+H8S|D;M&3tvC(-#wq6t=}W-mnYC9^juaO!*BGTj#xiZu59E-dLKULd0?;GTI4!b zvBmb-8K$07J2^MR)wbtc^$Uc>Y#j|#jH zh{v}jHq(1xIh<>Fe_t!Ir7V8|6cEy{+2X(j&Mo30CA=y}GUJ;LYR ze7H3*9$IFEK{yodr?5L zN2hi?4N~qci`RQSioRwnP`}-<>T-!o7Kcm$d5fegm@!g$A3s9T!tayzFq8(40Y`~( zHPtoJY$KF^E)tFq69oyr20IPP&gPup$Ar84%wTj_+{o*@pg2(f|Vw780ad*Y{k^4!pmosjm2Rm~J{Um>S zDWDMf>eDzvqpl5{_MgP}=^U5t1$KYSNnJv)wKlkx+Ft;Yag!ar@1l#1C;M6P`iH$O zrFLV`C-w9{ENNxy6-3sZID#?u!<~T*j@73dgPUQrm8Wsv@!180ZMx|1!yw!A?i=5J zrV^aZH!WvV8pTB#F&{((v+r-%H;G=1pU-j|7dM{}9kEi$xkK{;QEY5$lysY=2|I=caV7HIGfA`Zb@oZWwt5Bby%X7E0G~=bE=b9j8=~_`EY40e;@Xnj%n0wn_Hq(j(5Y>&cv2G)m?P(1xY8y+*`db_NcvvP- z_MYM?C$N35oN;DG7#(AZU5FVE^peAWu4+-)BKB=RW9#j>r04 z;5>S;Vf>%mB4oxE2=@ayyT`t{@NZq1v?|_(8r_wd_(YbvDOeD3xjd$S@8mk>SBl4m z!G!BvLp{Y^4PD;n?aVkWQiOudFt5GE@I9V>Ph$pqjCHG54te)N+A$(A=j3r`>s~e_ zXaSdeR>UJ?Vn-e;ZgqTNRC@;nKbrQ+UpIp#(AsF83V9woH}?7yiLK<+US!c+)dJYU zMpM3MCel1hBfv*{9rbWDeF|FE6^!eqaT%B70+&Zq+D@zWi)}jDW-k$p=|2X1 z_rHT?YQ4~U$e_S>NoASXZrAuLwQbhq;jTaCOOGrxnJM`>v%1e|ImN|b&v`E!7c$=r zcZq{q=*PjOfvuov%y|;n3#rZjYgG6wW3#x9yPY}U%U0!7`*d*5DYxs4a}&&k=j>pQ$lqexn~%onlEmHG5pGKxs&QmQXazj~P8*#Q!_a*Jv>afbnxo``%JMiK zs>Qbnv>AW`sVjL2la0NCReF_oRKEjG&*{-tn&c%arKNJ{7G^}(X67g})RB)(wcYuB zqo;c>TOVepuGRHRI;=WOQ$0j{oUU2&lh+~fO@X$OXV<8_ZfSnE@$q3CB*>RNaPK+L z>pOH+92Ydruh+#?IxLgM-vV`~I-r7PL%P8e+0<9DBi{}A-_~pQw3Px7`2IC|3j4B_nEn=sgS`LmxQ2THtFE-@3tNW(ggf5)&prb+5Za3A}{ z#gr?op;vmP)JHdfiEnOplr8^92wpGS`!bUt@hAA=A@d2}Bbmy%liiK^?t*Md$;dEa z&$Mx6Yf64LQP#R_Es+`G5u+bFN{y9K++l+8n5FIrw1jg)tpl^5q7by5T7yWO&G(vH z0Zoc2_nC(3G!)%cQx7&ubnJHL7yF3o2<>??OPXQ=E(z+|*rc?#tW9Xz-eh>h-Xo%_ zRy2nnU-ubS3UrrAc(=c@%%O37e)NXhK;lwDN#LzNl}0trv2QT#n9l|2=7(JCEe!f+ zLbxbnN*x&$-?k#Zr&6T`?ZKXu=$hEenMfHLhMfe0VS8GilM((4Hksjx>u~Q3V0GF= zZI7P3Jq0oa@b7N|8e4s_J^}x&-byJVFfTa@VfPeSw<0+>M@7=$a&6 zJPU@_q{nI|eO<8KSFdq;(Gf=dVq~(M9p+Wli@c06-`8WaP2qK{>`mp70VR>!8n&W@ z2-@1{eZxxi+V}3?cXA=$_#WpwT&O*!(I)-#A%lEU!JBX@;LD>C9JbnK`8bxwMr053og9^Vu39Cvm&^eH}~V${%chH8kS7hPXaCs%r# zer>6coY3aatR#}=G3!5|id4xyGu12G^)0?_W}rDf;NFwl|s|KFgSC8oJw#I0Rrz%2l#XN31gk?biw2D@O9vSmnD1 zP4^&kC$S1QFiYz^7Jw|f!wWY4Yx~aYI)$NFd#E=|(*u@CB)p>|#Hxic?l>-zLwDTM zwFyOV-iV7e!ZYQ2k%7?8K4OXcxQeP{q+C^`{2Ts!`i=q!R^;E=U@mtbki_cKFLss9 z1B& zR+7;DIWgn?#HsoDVwJhDCJn^%>-=Y8PM)tynSDkt7nsIoe>2J7f`9kio^cY+g zW`>FA(0DKHrEU1J`M26nSMU68x<>+5C&V=pO7A~imSxgC&&Wu!iG($sl|6FUuvm3- zFF$Yg4|l_*Qc3vvSssNsn(KDdJ9&GLWWO-0lv?Pc@Mps!+10`G*a*E6gGU)3?rvco zlO1_Vn7s&YW;aKtvW)j!l;5$hY>Y44`)8h=&ao91Oy;}{t=Y@BL(yBZ@rHq246MZk znuRjE>Uti+1KQHwzK`XPzZ1QoM;y!l=wdr83Ds4hTKBI5o(PZ z$4VhtnzuL_4)X2@UCjBND^1^3>mSK4@D%FDMaHW}UbjsW*;rd#zTD=tEqo4dMShr2 z8K1%~4R|U3LybYZ)P@C?*Fegu-u7)i6edIEp0ablb!FomV@u5#*q4eBSQG)S+zFg4 z$nM8Ww_u(qvzwY19$OWc`=BnvH-!-WIQCbp`U-V=p-7mOYb&P447NM_(qd6ke}J*x zz;JDVl^%-mNj%hD=)Ye+^`h2(QQG@wp4#3~kiOO@lI_EAhrDg)uGO7TrqyUU(8vY~ zg4Oer^@)iG6p8p$=Oj3^U(;C+<%_OjlCc(7!7*n9CPN9~e^plkFcJ~A*k!zh^mG0B zX&A?Likwaghc6;~KZYEDXoQ=rCJ_5&Yd&tIK9(kXFFR=o}pULW}hL_D45_^e7x8Bp}^&qgN7d0Fmj>{k{0yMM{%F}V?wN3%ZXVIZ!q(K8$D?l z)k(pG_NG^pGuUCS*w&T@k{4YL9#OUgT!e-oRI`8BfvH1e^OORxkS}WH{oqT05sjtw zKGqV$An|Q2SSK+{Lx?_a8L6f8z-@(ZCo6qD#r%9z+veyne0;RBDdysKbn%yE01Pf; zj->gDw4xd0J$c|D#NH5TV&{R-1Ps4-+sagIGZDM~-f^skUi^_b`Jz|+FY9L4WU`Dk zZnPXYa2?bwv9+>Mhl?JfpfqnH|BufgVG3{;1!bKw=104&L?|lbyXWgU`oE}*Eg$El zbTfY0T9yueaFc_hmF+Q7HrCSI+WWHs>>-u)WMzMyXdbePeYg+0TrS$O6dFJOl4Ht7L78f4k zcn)N2w8oK6Do?yw40N*PA-UI)CMiS7IKjcalmW2l;HIOaWbDXF8U`O<2P4fi_c)w zM!`0-+6tFv`yBgmi*rj4_GO|0g=GSdNFukLa9p>mB6|mpO;f_(LKbwsUl^LRVo2ph zKA}q#sXb0o3rjYo{o-9C2YP` z9F3@MN^a{7rz)15ldkAX)YeW}PnfSlX;c(3g)o{&pHSiZm)k&D+5LF?#sgIT3PI$; z(GLLXJWU{dP$^PWjr86yN2k3lry6eDd7o5eul{Lw)3yncQWYd2@ryZzq2} z2gd3)Wz8p*J=S%hBXV(^J5c0>o`~1Gv?l~I&Mp{awbK=#gZwtXi11r4TthD#LJ&i; z91Wmg5HFP3U#=12AjoI;NNv<>8}0A@ALREtLMGBQTJa@Aej}JA9_f_K&jE|Y#*Ns* z_S>HGk>{0dVwVRolz5ZDU6TT|rKj0_&QGd9=j686>X)Kemwj4oBABK& z7q>+srz3$2*14>qv$&OBaoOB@!?b({t<7D*##W@oGuc3%4dYmzsha-2*t`T;Or5?JGBV5cyE?|wv<8u0j$T{)>>E+veuK{?fGzT ze!4vh%-`&5-&e8RlSm}C_JM_qazGTpbRyRj?{MDm^XnVq%je!FrWvrJwI9vCm=pO~ z?q8Sn%kAEhS~q%PO3Ux^C{rd?34?e5V>*p!`%P0g6Cq+YZOOL}X;%kI7X$3MZLXtr z&vr)QF>S53wIc#>AN2l&oJlub^iHZy7(p}Bh8JN6eM+&NM1bHqyep;hKhzqoZ{D74 zm4{CLZ9&=k3S@~C5$b55_Be{QzHhX&3NOwn^u)ZN3U>QKIducHYdu*Wm26wvGNnmX zD?{s;aYt<34gRqpaJX1s(fe$VY{xo6Gs^D+pUa#V*=79#XZ1mumC44ih2BKB8tXC; zLRc9yOIjPX=9&8eb)8`aSjT9fZbJe+|FhmHO2*_WRc|I(B0sr0NMJw2r)No=p{II} zWGT@M7QfSUsT3x-^4emqvG62aNFNYkLns6{D4K#hs4>OOiMEXrymIr_;riZ(p{|#y z_A4jc1NBfi7k>UW9^$ewthC!I9Q%RdLu;TeBO}ZqfFHhpflCutv!XX^g_!AGEhy?u6-799 zu7?a{8`o?W-$j@H6!+OwH#hr_0a!TOnOy}x67?88*=-(rL+^;y95~u< zTDx`^uV}$Hi-eOCCqny(H}_CLEan8BzE@*Dl6*=!+GgIMiN+zh3qBf@1nwrTQOmy?d+`VSK>Q42sk*?66sn}&G$ zF?ZG2BZ1V@hf4%PhXk6ojfXZN0(`6;#KK^a6iaf>v?d*w^}G8TC&?me0yy8l_wnm; z5K!{%<}XOR%OSsA%H_!?6`2Ti?P?=)gO3;-t(&Fp6IO-VBwKH6G5Vuk_T1HN055R? zISG`-TpV};QyBZ(jE_NqH*X@NT@HQ*;;JP!l^5f)x#G*d^^0GqarhK7Hze?R7mHl> zyWKSRWA6tF2%4D&ttSG-;HFq&9t|LKHADn`{F-Pg`U&g-l}$6Mowc}n{8?B`2hp0g z%o##}<1KXuU_QPidY!yYVULT_bL=A>nRQR7n-ynUuE2K(Ry&;{q-HiXuL@N2RSVX9>Cj?$E2LZO(c19Tv8v{1Y28UGv|BY{}! zCUL6u_C5ircEWP6l*%+K@)!tJpmBfDD()hGC=UW511;sm*iC1aE}uls&VxF}cw>L2 zqZ-T#!eWTUrS8+y_<)hWVDYA0n)LOfhWAdob6d_s-WqPvQ%R6_ z8CGghfT^DT>ZY-4Zsh(IcloA=;C;2)jlgk|@Hq}qh)Z~W@cVc9x$3G)itKALq$!`O z=J<371(%v7v=e4r_g<~`jA-buG#GNdzGZ7FWj)j zTT$soiXM@eT@JXeZ`Ai!0)^18ILh<$$Qz%^-X(-W#PDZcTRxuIp(pP@JZk?PgXy4w zsP#n3av`mX-8S#EEiMK0UWRzRCoFcLA$jGS8u21+1_qLFY&}%^?1C0E;doV z*Df&In&lP0M9*8t+dpUp$^Y8a*L0NFqdYxBgR@#fksGzn%RI^v0c?awJm2c-YRz|H zB4I$;?Ppq@?Qd`=62u%!HKy}SThlfQ;;7w+_hx$;d)I2vldQy(?fOvh1<~jpQYek}6A85uYW8&m+*!*f9!$ZP5fn!eRXlq7XUp^9*8}zBy4AuyH$EVN2 z)ru-^y5K&Vzww()tpamt6IL^=-fBOk(5$cwby^#k#iYFYDgO$GM-$*Dmc3SiQ!>VO z&+3Wu=IEQYmlaVXR(^@4N=@&1zn=SK2Y zkRWey^*}TBc;yW!(0&`}vfSIK3qOxra4R+IOuGRgYT*hHtqHhOqk}_m8t=3|$ZT_s zR;}Nb&FBX0S2Sg1FcY+vJjEKw*&;pkrx|-f3pM0p?ajbXayIEPe<*ImsCc2W?K6Mx zSrm{xerzE>FM~TPaylqm3{l{Uu{A}%cf_Kaf}0@@gX14Iyuf#pZ~_YD3bSPHxOL83 z!)m-28RJYhC~a65MeqyzoEqZ~s2ookd@)(pb(NE0#?2XLjuE&9m(YJtAZ~6j)6e6jfH`1N;V_B5!P)rgNK7^|82$A|N}nVbC^tq$y08S{T-Sil35 z$&G11qPU^9L}NTof{k&kPS=8i(v!2K9@rA=Gn2JUt`UC>YEFJ8-f!1*=4bBK<@m!9{2g~RYBUnbMOHh1-%xpyn-P$ zy8J(;=%3$ncmW{i6oV|KD=ht2Z}IN~kN^wl${O__p|6s${@+icWBjc%!Bc#Q>WrO!{f)F1yLCvtSS4-yJHeG3&a0@N`oXsM%30hoP3e*6Rwr5#yZs z>VLjJ{g!jqWc>%SSlkO$GqV5CF8>bLgybJJjTl9}VI_La+JH*{nN9OuI&)Je_98Tq zah60_JdmPHC-4IDzFpii3l?GMI2nU~%ibVqGv2M98__aD3HTgFG#e6eXGpBS7c@lL zLzlNM*ewwfC_WE|fK&1B9dC__Ut4ionf=yv`WX-awAVW;qkJDj)9ME#pW^0kzgRWUtUhj3_V? zAfM$-5_Fw{EVmQ%y*RcnGxL@+I|4FgIF{XQLRT0w;qJf}QBY68G7X;Rp|2kv6{Hg3 z9$oZ{fI=ucZI##S_Ud%7c74P7%xsLY21I=azKqgqZjez@+Lg2i{Jz+%7hr%kZ8skQ znr*D!UecZ)+zX7zzGlSm+*QX|j!d)V=;X&a;|Sdx!;a@C2wIO>q9afM$3be7sT`1E%&B^#aMRmsMVulDsb03 zPt%YF_CCV{?9V;D%Vs{rmo=~1;K1Rz3Q+zr&dU(R+DF8c?Y z`p$<+XW|tl)kOlqb8K`skt4YrdO2a#vQJo0ZSqP`FTQ_uK!w@$AwpGDooK!O{N+6N z+xe0hTr;JgqUamA+QEH-)#^TQ>$Xq*$EzM%6azYrQCfLuErspI8D> zKrLNYZg9qV3K>yMx{duUo)AZpv+e~;|D(SC#v=y}A1SJF%FYm+gqs)Gy}dDc4vR$6 zpXfA7iYCLMVZK8>4b*hBny(q#B+$y@2!6YIyxK8!H}zdfP5|;DI?*wU{|(N3$4oc_PjEPz`idj^YH5GJ{EUnTXmTYUfPCRWa?D^5ljp&>*w|d~ zj7@j-5|)7Me2;%*&D5p~^3kTleFDPSqeKW1-~36yYFFeLq;`KfPm(=Ppt}i(4XXBJ zS9eBP@YuH~ZY1Gz4llbi`T@PBO}1|1)G64qa^zBO6rgb1Pu@hP+0u}=)68;1ZDB1!fxOp>P`e0D*-jKv;F9vAa_Hqo*T14b{J@H*>QFY(g!6Ec~kK>0prf;3?v~D%b z8VTa`?#!+l{EHXU+t*JcGW3pGtkNRz&dLGF*CdZ56nPx$hbM~P6&H{*gLSi#*4?^U zjl14{!=17*_t!;|497PQHZm~sCQfuei8h6{;irKZ`RqeJnIjAf5*nhy!6M_+dEejE zb{_-Z>V6n%Xb@Zp>gvjYDXu7GGWQu*MESC}V23zn#|7!>sun)M!>BnJFUg+J*of)P zk$|s`7^}{U_Lnobkb6jm3HZEDj$Ss6yBz}&x@hqS;t~0WrCagSU2}|sL1NR5JA0fU zOFAeF-ZuQFSLsLRGAy!V=bKS~R*3!DdVf5?j~FvxrK7dad2npdN%yQ|K2;#Z75fxL z5dhbYV2J7s2)jr{rOJs~aYJG6c`WVz=DXW_4GyjmyCeNUD5Zshenkz6FLmYT#1^f1~{#{yAA zh{(*^=BliBarc7U@$&hiIP*_f;5bTKz+X1{6EreJU!{D7AzS~jze?;d>&KIQ#JXDyr|MMkO^G%@af7F?5tAc+M0SBM-{(HL=3)v zvN@_DU){ZXd3m+&&F&qQmtk#{JK5cO5AH^9{&-+0x~%f;y&BMED#Th+>Rsv5U9n{y zVvCdA<%d{or?6>CzCB{s?D~zKDBUZepyQRcj%p2(pVNNV{*E*cJEWcfY6gLv-@WiJ9bo<&dE!FAD{o0*~e2GYJ5UGMF8FJ;4E} zTrPcWr0s2xUhBoiRx?+A&)0_-j8kO7;P0a(T8nO1Zu6{sqkEBJ&seHMj1sY3JCeB~ za2m=wGSNCmkVj#^jU3dfb>;g79NM0E@Os}jA(F64)CwDb-`&2%Q9?vw)aH9>(|pr# z=z9evlDvq`9Nypg+(z`O>Eq|mpV{6S*xU&Szv_?S>cRhVYiMSOR< zHeo2(T#WVAf~z?aRu}Kg-n#1bZeG$9SYO4*GKG_oQ3>(x1>I3CnzRGw@-rQFPR=P~ zI>?nncX%4{L2moBSFT{hFWBtYMXxB(>G<^HMIx5#kITbFn$|l`4S@%8rtB9YuuX0Y zNH_8K>hL`f6x{b5WMqwv#hT?^1$z@G;y9T-?-l*6`yi$ZffN^c$~i7=MoYJKihM;` z=EK&PWa@ZEa4<;5G6{@a;t>>lA5x2~(;AERMCpd7!e-bucTJ0cKt~6LPbI<_ayhJ> z=YB<8*7s6q!R4;{btzSl$gRxL5Kf^9M(q1# zwf-fOxG2RX+$L~nu@2sY0N=yHxboLem9=AlN#X~1KliIcj*bgf$#6n2T-1xB!baij zz2(*f>_HIxMkSr$*bGVkJ*LZXJpre(%l_c>dG7PeK|)mI`Raa|)4{4Y>er=^8J}ls z?Lta6`s{Xtc*4j&7FiOu?cX6T8?BXrg$=b7utbRM3BAnQOR&o^m&^j@Y9!nq6Lcx< zM9vFlU$nDzvN@rDn86%wSqfg+^8JTV`!m0k(uPLNk=0JuGY<(3jWtg{cvnsFF!oj0 zldw5P&pRt{v)3OAut;`Z7)0THj(bp@b)SR6gzZ_pi$e8Ge|d#@MB!IsYSA@UW7v`dYl|UU4?xu}ekDP{pDMil;47w8I7ilFf-Kv$%hRfGhD z8C=#j!Veih^%P@bqr3`S_4K?wk<9i-2=nhwn59+WYPkEn1INrBqzZ>&1(-ZoTx?*_ z3C=bv>ug3>x(C%GIr53`7-?!I`UR3C=r%gjL)n*1GV_3=)darVZQxaIL$f6{IEa# zjLE2^^lqTq<$s55K9y^dwke#&I!^NEoKM>Q_)~|Lm``1@9r4l8jp96MQ~7Mi_SC1W z0woGpwV`_V$u@+KgI*t|o(-e~`O*SG?1q5pw_yr$@*>t@{)cEa`R^-~h;1snfU4I4 zjh0oiwS^x8(+j;}D6EQ#l8wqtpi$A-nRz&~lyaQ%M^x7LOeJ)NL}pQ)*9cHSc5V2^Hfg+1Bt=;;O(m$sBr3E=rN~A6lfN zTW!b9=#SDYuAD+c`3yPYm~{K-c(FjMA(xjiH0!M|dUCEn_GzF&y?#g>jpiNLk#BB) zqF!ZAU{X6*Hf?Lvtn|1#!xORp*Dmnu;*HFw%-A>m4BFq(rYhLI@hwZET^` z72nhWZ^6bze27MNU1)MmIc>;4U~|2{J>kwKWrBglv9nOvfys2}TDggiY|Da67ccX; zIi;H5n4KdvYYTzzwl$W#b|7+JqK7>BSpC8Z)u`q6ls>I#d5O6=?%=2+wm0LAP|1@F zYck6z2By0*8EQ`Dov%MxDX*6s90QB=Tb1G&?nJLd>r5KW=lVg*W-c4m^vYF1bGVs- zO?Q|36}tY5*0UA5_c!J_$hw^dO=pV@7qPwXJ-e3NkDk(dppkE~E7Yoy&mSl&Z@PCt zo}*M!$=r`D6R>v?9zdIGYBp(PTGWz65F^!QXm5V~N?I5?h@$6lkGId4DPo{#9kJyL zi`-jt!0W)@#J}vdk0go0tq*b4`ShPIxc`}0(!e`LFqxjf^x0qj!OP+Pxc{cy*TwUc3mw!Tf^xMyDs2qgM`F zBz6bZ5HX|uJ<{bOI|e(4A2G(-^XE`dP&};@K;9HdEq53ms!d$%+Htq z1&i5Dr61bZ#d3ALVl#_wTcgf{#Uqq-0ky{Mf+x*yd6#^)!W07}^=0^MxK@RH7J2tZ zUXW3Wd>Dr_QRp3o$jfOUY0LYx11~&GyPU>vGUmk(DJcOOy`D4>hiZ8r@LZm}+-}}N z3TA6pvI&^ye2d(|S)B0^v-2a7J|vU*x9UwP(i=FIShkxPw=}$)hR`L;qiG2m8L^$jaho}14|I2JRwjnTnAH#;N7e7buf@JkGf`Z zS}l=ZlkyGDP(-}q62}9&YjPXP^w{957iku6g<*7|ZcUY~F6PqqhJUVKw@)Rz9qNrd zUz|xSUnlAe+1YuwJ8AjpG4z|O^3)3vG(!xpjMX%0U%-F$of*g-#|cQQ-e33~2B5at zwBEG|{G@|aNH`!PA;lSzgsNvI>A#rC%T_B;Rg#XQ%V!Qw)kFQ53L00S=>xJ{2fb2Z z-vDjhd$6THsrSqbilBPBVV0w^SK*1a;nC8a`lMAHz4qpCYa#v z1={ZY!D8cZDxGj)DiaPp_JhFT?0VDq&Qa^|^vz&VTMcwO18%M zN4NIyXr_>_@A3$`X$^G^WR3ck6-6X7rS@HZhwLI`W|-ad^nS59%IlJq;Xlf#KjA-0 zB&cPtdaA+d*D~lBJ&|O-q4+l~0EDI%`28`gK6vJ^>=_n|BNthu+bFyuZhUHCs38m) z@<0uYH1P!Tdxwk!eOFx`M-QdGSlWE?s5tuRzMpSYiqsL6))hqgrUQ8!Fm^^ubg@A(`KbjS=3v<{5A3&%<#M|^$8_zJgp5W&fOhuH52*$DQ3Y1^9X} zvwNkRGOfFKoc2mio*^aJw1)H(U5)jZ?#nSD;4l@5a7OGcsp9KT$z@uqnJYnfG;9&AR>-7l(@-fX$d8vs7Il(X+Ov?Gr0x zBrfN|ohw)1T@R)3us4dcxsEb;T=y#c;{fhl5`lUe$`bGe@Lrb!S~$nSF1%bshU!WQ zBj!-G+YVO{Kw*AqT-rRR1@G6krF!LF3E5{eFj!#96b%fI4#p6?>@jv~<`6-wc)=`H zmOl1;)&90F%8f=vXMP^?sfJ7Mh+M5uU08`gtf1}aVtS2&4x3pkSJ1Ya$8zZva9 zvJ?VWdFwXk+(02l=dpm4+4dCNy03h<7Bc*j%sLA@EuIM?b8K7t{(ai9%eh$^uXC(Q zoG>{MCmi+iIpWTy zvbnsG@w#*GD@S=reF%4$Dbero#2A{vOVhFPb608zQ>;7?rv(ql`$k1YUA|9X6WO8m zVAZ2Ki5TWK;~~wMRk2EsRS>|VxH)q_dqE)#x2YT{N zB8Ag7Ge#X&4e-{En#lLeEAg40ZE_6(4K{c*bCA>MY-{lBwHVCSSXoAJmuN(Be;(1w zE7u}yx9g9iFN~QsqxiZVu;ZeXb_0dju~pCW4_)rWQ%FJy}5fuHaqHLpi4HW8@Z0kniw;nid`A5*{nY{<*t#F5#qLA&)kl}6_;frRw ziWnLN?MYPp=A_cQrZDqAyO{h%uQP_)^R>OTWNAkg9;w-mN+6%BZzlwWFZ-j|nCrwk zVMb(RB!R9(bSSUKE2bo;kC_W^*p@G-E#^Rm2V_rAjIt2|&&}?@NK>o+*GdAAT0H0z zDl*@ZfHF1~WMpI~%#Nn8ZuYhzU-RMg$>HqCxeGSUgXc+O0Yq@a;%PC^&&~=*T2l3x zBg7zRXlOhWj4xkGEi=E)@uRBh0(=zS+ptqQDp-k@6qS{+1b^Wv6_K}xOV5wlS+A1c zkm_i3*qgwk{l+(nP^e20HNNi_;5^Ki5`tr#Ef#E~=TCW6kJ=T?XEi#Spz2~31a>CV zBz2?mH|&7gQ1=yPDAb)-k>Z?gYkgxV*+F#*1G9@5t#F`&N2n}SX5r7(;YlGm@BMHv zW*EOLo3b%8IqenhtM%sJ=Ar)%+af=}JV)_53fLlMG5AW1;{_@bz-A?N_La(Rz)n$i5cG9UP-P@X? zp%W8*bP@O{=NCT0!|8Vp#FQHzcBl94bwFU;;8!=h-Jpjj2$nm{+rN+-a#~2Kg;eU} z={Ek-iL5Gmy9nW7-((K9KibB`oe(sHFjpu5(7-=j8x_mIty&WTJYxkEaT5v~Q{({Z8x3n2B z%<4R?u^j#E12NwEXMNK4A(HVYCsB>8hoWsXM}(UfN~$IU^NyT>pinv)Do4(WM;}!6S>EF zUn>KuumHs+@D?1!UES*EW|T_0B(#y%E$X(GIP9KAXVGMp-gz`LL4LKF{I#YQ_@K6M z8@pI?>_cZsh@4Wz60zE+1|rPe8)@}hJj936LX=&@E4~Q&?)E8$4{ZZvE--xF*ICWvrw`8<%Qt#PdZS5 zp3i2VY=AW$*Y18if!NsVm?07lrM|7iT*`wOH}N`EVLBz@fHHClo-|6pi6ZTy|ASWj z`!LXat^P7PC;s;$r#1$SmZDeQ$6mBb6)w@sbSR0Z&dshff3~9dbJp|0_)O-0 zQp#+^;Nb2W8RPuC|Bx&*f7Lw+ghdPFFr;qHClWZWq76GlHs$-p%s!KhK7qA=TRI0SfUn_nat z!7+Ykk@Cd9yz1_@?pM3kKY^o1HtI7aX(WvTvFrJ$q-XkWc{edeXl9NTKu(SQ`;-1* z5(Nq6^u=xNIw{B;ep=8o7b5_x7OD1Hl5l1dK?9@fl#Zn#{ohaZhpZ!V0H!aFnOV2N zDGq3L(FdH-lza@b`e?zIAz1X(fLN?8)S`?)S$;V`(;yFvbOztY6NUF@a-18{!t=j#2p|M>N5ILymXZX-jg|DHa- zK2gYxM#zw%S_)ctu>9BZb7PKb@6-efGaER-`_) zCsJUm|FRnWwO0PP$aa4GP9PXtcfYTuKi=^gy$7V(--k(2<_STbt+%fwdqGPdhQGKz6Y)4?T=FZ|CsL30I~%<*k_GC`qQGfSp2Y5c z&%SHfN}vxf`THYf!$1w|d(hgd>4|>J$};^KRL*8|Pae{BP^;mD*_EsC@J8U~{6*n1 z5QR8kb^-m|D1j2T>AQ6Il41b+S;F^i2eOo?7#jILQItjIi~VU^i*A^_sC=9Pjx3I%CH%C z$u43O5bTe|>&Xvg2r>iv;bcZo(7KoFTMNUm`zMD#doiXOJyw1gMo_KZB%6Dltcgcl z5){M1!9}_sme6q9Z;hycuATD$oj(fk_|4p~F)97;tLOF$FdJ3f2p_BPTI`mZTsOAy z#y2}qB&Db})7Gpjy>1o@@UK>-Zq`Ch`qm4R)XbR)Uw`c3Vbrert|kExuP(Nkz|qZQ zwYzEZaR2mFdfOU&cC}1@cUm*nj(>-qI{ytY`#0ma^dqB*^Y?@4?}<9B21jB{nmMDWmNn-Isu?cKq$lSuguA zA5{-owZ%BLR;ytQp(0tdt>t%06oDDlGV8`-tx7!rImqcw1TTAM(7+-SPUHcP5g$n6 zHS$WU8}JL+u?0XOwai>wXi6~fPGLZKx?-C~#CxxHRihfgtn{r8s2S;)(z%3gzb%`8 z2^!+H+zUgtb_lz4mBXvAj%T`)$!NK?bLaL1hc-k^o(>DrZ@$y4b9ncVs_iX0u1m6* zAtNNjM5|E}I#Hmed(fmgm{U3qUsCT0k-s@=)kO#L-^~ts${gvqFyko6^pz?BZK z?Vc~I;4O=2I8lhAUJTh-!v)6SxJ&AA>P?ClbFIz8^!pS(*JMmO%!G_m!wxX`Xxm4$Z6YmHJi>*Jy z)^>bZ^eCyIAiIfddz!7+JjZmf1!FWqu;_*$hU+Z4=KTTx_(U>`%K7lgl^NjQ)l&4f zRru<&=7|wxh{^N6G!=~5iO4BjHdq^3ZCqg-{v{xHX zPY-z)bL=*ybKkh4V*N!+;jF5@rmR&+^el+)>2u`ry*Z$5T|ZZage7`yJNP1{7-BV5 zRNizifr7JT=4qB#yY1s_cp7Ulw$UPS+C`vCD6Rlkx-JNoH5&vE%5K};-iEwk6W(kt zRNh2RaD(8#6*zeZ-9DpWCmZ|~4(n1JH>K3ccCko|r;$+$9O(FE!ObZX1zNXs>1naK znWO5nrkE?vCduYA-=;6dm-qjG)MN`n4;}LE9tn(Z5)p9|Cgg!6Y)bqP3ajS*f@5^W z%9Z<65Zj^JnGOG^7^21kaY^D1jH158bwX^PJ4iUXJy`3uJ5DZ+K-BTn>`yO%eQvsW zPAVbD&gi2vI)N5T1@KN>906&v?k(dAk&vN9@Fmd(`4_``KhW+dx&aW)hoUi? zP1+Z9;G|?{bey)kM04Gm4{!DxfcAUS@Zc~-I71xr6;Tw<%5?Uh!Y-Mkzwh_fa?t01 zhxE4BB}gly6u}1HCcafj;082a-rx#w(S49@OWpf-x9_mX@k(8Z`CP;Va0Rq$k#?44 zL_`pF?UL9`&(XZSlp6H`Qx+63q0dAQnkbB+KTD9H{T4%HW?u>-&>km7Rmzy1egZY~ z{o=CQH2OQe18~I;l|Dbdc3#V!k*JE>BdiLuK^{C-qoVTzNVo-{cV7c10hQ*9}qM03L-{RMeZ3+o*%ovWXvD zAHgGecmt9h&fCvV#_N=xYBhkE`McLT`vEVvt_GmrZ`sFkCedv7^U>GCe5#aYVQ$bQ z51+973V=3yZww5q6O_z{B`l&wU5*`(E(`chrR;m7C>jp~KTfv!23jic!+jRKzpmuF zb0Dxp-k1bS`hRaH%|;^PQ4DNIMh2|N3PbUD_1rx?3t-xu-JH1OdYyiHwzoWZ1$f-{ zZa3Tl0yQbX=fq+5-u&K&YHIh>ee)#yyARDVi$keV33^_-fu`Za?0PTi#6vo_F)N?z z(Wffid9vz5ZrE@je+$EaUnvDYyIfG-ly=hVP?R}ZdU#P%D08>LKgtuLe0vw|@|%p| z2s&s0^?U=~x2ZJesj|IMv*9DZ1r@VUH06~8c9CH5E^7Jf8T3xYKtfqS0Pt0uGF3Db z=xodQWzK9M(e$7KK7w!qjvau$VV*scZuL6S+)w+l+U@ zs7vZu65b|``-}TlC-Dk{d$BOjHhO(XP1AEc`!c+e^ZiLCnsa})0)kM0=-NY|D{VoO ze!_9o4af*k{kHK%^W% zy<2_T`mz%m;_qa{sJ6XN<`QJ0!8^a_DIBG% zPmY=P7gxAL>B-5f*AD5bUu1R!7TXyi^vC8Ul$WIG+o2uH@`oSH3tFZfF1r7q%X%Wb zdokUx!xQB9r_|tQLmd)w!*^GiScXG3>U#Nf#hXSD^tU^np1|k6$3Pj=?7UJJyXC{0 zy?Ql29}anDxoQ9XEAa#hv7g~Y841!gA2kw?^IeRh*NidXJ+1)l#5$NBMLNg~HE9va zk$MA@8w6D6NnV7CP6l9>Wd3Nmw9m-c{yoCVR{+9b+H*v53X07nb6#x?QREU~U+`-; zEV3jXJ5jv-*{nw6?}--OwpZ0ev{Cf^i$F}5S4gM+a(gLz80ckCE)llImord*2UnU6 zIP=XG!mPTP6~|3jhok;z(&DHt1nT>1bf3wkabv%XT*lN5e^z?H7t~j2{!M4jgMaqJ z*==w$A-(%0$dS-(vk$9q31n@d_Pw2=PxO7ssfQ@}b-qe#dGe1|-NrgUh`K2o_7_+9 zhew{?HN?>OljpRbN?s0W)5Hu}HE$l;1&9EJRP2>%`FqGjjItzr>{5DEQa4HX47_94 zaOJu3@uK@5@`2&blB^_l$fd$E(CvoyDF361U$Pf?R_sprnym>q{4E6`xc?AcC}(`K zX+>7?PTPI|?0C##JU6aCJaEYxRd9>Mp0PA1Cw7*hp#!&zrJuQkVwp10gK2$$0tC``MN^3rp+KXv( zjf;k680HCB;1$`4Jfk@Qs0vh3kP*$+&U9_w@HZKe>GNtvUz3j@w^Nf_dttp=^NjtC znc-%;hkIY;Rxcu5T_7TF%5RrIV?ywiTo^ITa==>+Mj}`!J}u-J6NxD~uM{NW;VZSz z##_bawM<^*AWP=3_9z9vQq;5OZY}|MXywx)1f=IrU0~h*cPvfzL$MEYEgdLQs|T=G zMnp0^FinP6W_w-0YrQ*x3tytfhqhzU`x_j+%b5$5Ov8&K>u-Wc_?(qncr3@Bg-cZg zqaT0(Mtz>`?MvfWD@kGZ>r@U4X#Yo&e~OPTVdjECVX2Pt3WrIjPH)=8LP2Wb*h$z$ zpBqm^Jho!RyE zrgbcLj)3{F@K5270F+y3&Ir2@F1ZK|09a50flNFgfs;HMa{)w%5kGhTMcr>w-E5_h zs0viP;Ze0c+pb|IiL zK~)Hq0)mmey2z24?DxKj&rrw-yTgc9$E&og>T5txk3B#l1Te$Qt+&!NN3~ZnVRnj& zk&d%lgCvnS8Qxty3g1z2HySFbz*9i*?)|Y8ph6=V-><`fx-9t0TL3lRPn|DHDHIN} z@7{OU_&Mo15=Qn&yZ9Qi2O>)o`gYGx74w?hfx{|Hc1hf*;XDq~Eto64Q&ywiqLAgb z-d_TTFX}9_0LgY@$iTE$unte-?_^%~6ChoreKPA6%%D{vwXe(PZIFpTT5^1C-#gh@himjSg5G`d&!kx+Db zS~{=4@$Y(n%yc1RQ1#cPmcXram%XdCtnIH}RA_H+FD@V#7b&un{ug&I4ErZ{Ux^&V zppq*;F8YI%lvHT@TqOGP5Awd1k#(HPb-8Jp?~H|2#54q8@5iTqcrMv1@ZUYA3Kb5! z8FMKJ7{e+zQ=s-le$d5v(zvoLU#a&BkI0@J^9(0cI(rk#iN)-xQd{G7+cB#Ti^|O2 zhl`E%@Nc+a$Q;nx{vWh3_ri~G#4%x{c|R^+PZM4{bJ#H^2FChW z@ptKlaIu@~4Mj4&vU!`r50gnX(9=zAD`bw#o>$rjmy5Lf6Tu4K1+C^B682{l0R2TA zPzR^$tzM#&l|p*HXz$FgE8*j;VnbryCwRa7AgcsqqW>cCiP7JcD7AXsI^r;B8Wr$| zqtT(vm9`E=iNf+bw~EQv0yMt1v0L-J=yN2HRT>I0Tf6bLNa3jZagD^=1Og_Y?4T3D zTc_I1%4gLF2y_zwJx8QUo_wPY%D0gW?|a792Z7-nVj9$BfGTB}EaGSQbrd77Y!vA3ML&8Yn8a!vPblE<(|9j3(!jNCOB|N@wX-iy zYPsJ}BYj9x{XfX9j!7N^fmJP&e{u8JBu>LGnrkRblPYrf54%7Tnp2sB4qZ&1W?V8Z ziFfDYvg0O13K;@f_H0&jP2$J2T5JLdQvhFI3r2GFu1hVW+TcP0n$xMDb-^3=Oiujb z>z@|a*SODKpuCJ_8d z+S5Lg_9db&OSQ}QD{Nh<*A)`yNPi{mmxBOLk)|xvntf~3`vymE$-pa4n^EOJf@FcT zMu!-^X4oA+$o`botI4U8)IK@q^LGn}`KTYywj)p?#MiuxF{tG)QS;)N^~sF8yi6J> z!9ll%T~44G(=SserLF2aw}!@ox0 z|GjIwR@@uKPdI74WC4WA9g?AOYn@-{Mhm`86lv+|L-`EuD0O5@4h#6C8uTzvmUp~R z&XQ2WqDxTO{;FN|F256#oR}{w)IX3!#b?OHo(?D*FpG=oM=&ftt`b>dbUCO$g4yfG zkE$$CSZ?Eag%P7)=OV15z{$#ZfoBjvA%gi@kXIB@G6R#6PdjYdnMWUKS8&h%|gI0Ua;2lh+sj&n~3^GyUUM>KBAP5`=d>mYA zcCXW*Cj&i$*KkcC-SDGG&HZ9xCEa}H764^t@J=y05`w#*T$DB9EAFMXysK>8Oz%nHuKt^gy%X}KWR87Cvgabo|Kiv$PvlZK zqj{>2Ce)5M0PHEMSu3HmR$?QbJs8x6p03XI-i4`o!oJOeT0hN`X<#|??(BVc=ZUf+ zzj31>5k&yXR3i@$zlzIto65)AYo+jZbeNIST(3;~<=O4dN>>|2eT#*<%x0d^O+!&w z3*n&_&J8T~=TMkO_RS5|a&i4=KEzX3Bcg*kdxO2f7$@A*ryIc!L&;9|0Ia(^O!3=P z{q*oil-H;lOoVBPh^`i!Aex?c!V)XLCg0`Tvkg~}k_y^Qr|a&JT=xPn`P2KSuW50n zs%ATQ23`d3OBS$5*i4l!8whKXdfg7+TLlOeNx}OX;1Cm+WT<`oSeAvmWkuvtGjnkS znym!SVkEnusX8)M(ul=gHEABMYBC=?E~t z(EC{DWKD%2`nA$s4$K#v5Pp5N@%>a;6_yrLXXL~yG7sqp*#FG40m5_#-p`JyQq^Wg$5&_^iljlNfo&C{juN6y|(2FM`s zeq#K0)$?ZuIbhS7^Js!^l+7M=m&PcxUcSe(Ok+12h}Gb6R)KGu)^f}p`PTVVU2~=3 zf?xvKhrjvIC^Pw2GKiNWD&+|#QH;-~aJCIPdoDn78?ny)_nSq2 z@&+U_o7LMy>+FVw$`6QwN#N>kYCyPAsqFDN64;YkIT zfLTGwGQypaSZ@x*A@_WL{ee1iA*9V~@y!h2 zQ2v7+>Vw!yzZp!+<(V59=imNH;MePCPyx!|PU98w-yy<3UQtd4h@n{Oq5c0w@Bz0# zF!nmZt%LjTFY6bb`#F{x;3;#0&)k0B>Hc`dOBi4o_4%GN{jN^0Ht?!p@ z(8K)?fAESr)e3WiPwOt@{k=eT9gA746vVeLk@bRthka=cA-}ctyUssxt=H^O-lJGw z%W(&vHGxdMw-U51WmpXd;EXNhA6IU?g}hwHo^$1Q5WpjA1|CBNWvv zJm4FoKEHJ`f|fiKss*ieq5uBWAhbfJyZr4PeH-NWLJQK*KQUMuMAjs!58xVyjQU>V zS0>0U7JJB|84f(*M1k4f5?JMSJe-*OMDu?mfeXEF&7n_Vm zK7HaUisq$MUo0a<5}Pf-Sk#l*#f z!8H2TtyvYug6druuZeAS$ip)t0h3Uvt$`%UIz=*C^`>_ofp6=xIb{Th@o2d#%oB8= z>Z44pfx(R&r|QKcY`y?w@lcXy(wb zZFx7^u3YslYX4G3fe4xKH6_rx&bdA(OR^*9Lp>XlV{Q8zKXf6WR;tvd+Q{DyJ(i}Y z;UIAbcecv45bN**payEm12*+ILT)=LA)!y`@(9vhn3!132MBSDZ)>z<=(a}Fh zZmQgD1jkhDZ_7Z9Qd4A=h4fo4w4{w0% zAlq#4{k4(Xx#_dNY`a2x7{F|XYF!7BM6zz@EA&37N0PVW5L?2A)TyV-88FXf;e zA`9(YtduguDo_>i4>VP8?^R11KAc)L&@>y0o?VOLW^cJXL0l=f&;(M7H-X_vw&i^) zz<|9L?wufO+D~=crqd{RIjfeaWrqjzq_Sk*rj)_;Y{Rw*Q;eVDOPu02-Bd}f+dD(W zY;?@fZk<8=Ntm_S;t!}qNb{$g{nBmFH(1Cq_CMkli~I6LZtN=b{aM#|m6~2RSz!!6 zTjuECjzNzbB+Q9*#uZrY4u@|dH$+2=Qf^AL#I^i|xx(94J4U2j4ckw{Gy&PDNfX3-s1ASX%p)0!@%Q zd1`TxL4q%%XykY3;NXB2P|dX*y_qPJP(+e_U7oawAA`6jd06~Ti_N^;#pLxwTKO#+cD+yG#Ch zwDt=97{iJ-Jp3se?3jqsmE@RYY_!#Y1;aunbjpk-xXYV(u}&iq*HCr!lZ z91lJ=6Du7dhLaAJH45@2;=WJ!5*}6drfPj}B+G-uJ(L8SOA+_Wmm|h<;I--dmBtXS zI!T)pI-w_QS13`lcf<#0iwLG2_gfuE=TTw0@p`U~XzvB7}u(zFfE37gsn}2yZ;n%>qa$5DTh=7d~G_E~2I)M7(7A zF^5`=@cg)`AD}5O4(GbRhB|7w7OGY_yQqJrI@S8DW_=-w4N#@QB?T9z;^gjFUhgzX zmvQ*sROBy+TqkY^x(7GfEVze~BED!U^E%TV!+>0OAF^WMjA03m5KFuA+lV$WcVp7HB^RV)Ix~tMpeXaP-TkxkgR*Q90>NOe6WT!W z{2)maFC0fFdpmnK?mU1Y%TyjTPAZicPE4(2WtF!Aw_HM8sOKI8J7AB8` zgNLb^?NJ?Ooss3IcOXpfvbHP2qew#}Wch%jSH!8VC~4rYLrvRDb6N_2iBcoMFt_3w zKfJe7mczE-crNYs#!oY_OkQJPttVCWc~5o6{T`$L`xT)I)Z>A%bcTf0?7vRc|GHk$ z2PA_x(`PI1{~@~lS17F<1cdXL(1ZQ|>vOJgfN=i)g0Bw$e}(gy`cLZ|sJ#@^4UcOj?=YPVX5qG7P1R|_ zq|hkQk#BinXNE4d4g)mpq@1lZi}9GXf0K6GHb$yn24f;EXr~;&uEeS_e$Bpi`VYwz zsW5a@!Z%ZJrs6ERttPyo20Ex;0@x&u9`E;34FM}IFC;$hR;$AGC{aTDWoS+mY^Ukp zPzzw!S-}0gNz-9ESHgIX*vRTZC=MMCv-0Lrww}(e^psSlU&4@qQINkR?YF%H05kj~ z<@oDl?zx+~0a^Fsy$C-)@9ghA>vL!57?g{7pOxexI5~3G{`=0<`^MnDyaTf{8iQq< z8cKYn)VFc->R8kw{+WOIE67gW$KQ858LUA|)UyO_TruB3(pk=t%FO_ujh{r3C@$NZ#Sz&-?V^`~R)?va+&va(2$_J-f`A z*)zWpU|IZ7BiL^ar)ZK2%{y%yVd5v>yLm&3Uog>z(^o|0(SDHmQ~Nc*AA5pz?#1Z= zBxA|r*?a=(v;`h@6*=%$)U;?+I6lSw%<%oI#PDKu-1LfYU>FSsOan@U^78R&efwHw zH-OV;d<)~3r-p{9@uFcFl)W*hkr*%&HIOR?PfHV-Z*(KrPqEe4MRsNxOk}<^BfLq~ z$K?BIZ!%dZ>VgRnqjc)Fe@ly*MZinF!redamy~LM-lPxzZBT=506 zpks9Dyq*}|IJG?fUL>`hKn8;(yA7a&eJg$)UITLH)?T>X`Y`h`i{)b-vTyOSkp&gcn)6;K%?=wM8hMjFc&A>K|#IC82WOr!zdk{Sv94{b7u%6vZaWpbf3w{ zU(2|P=LU4>0_|vW>#QJ+hc>4+kIU+T$)Qm%^w??Onbwg3&YsicdQYWaL5T+?cmJpScCm!e z+D=kM7*Wr0VBrmHbCJM<`>tg=*s7{PmC4CS(EO-B<;P)B-fW?|{xpG>h(=tS&6Ua7 zKGdJuKI$&+Gso=EA@a zqD50Elpl(Z-xn{m)5(Y{dqg{$k`%yYDKW2+V9IKe&e?r0p?|+g16N3gTYd>s^uLs< zbL7;2tf*JI6nahjv%!3Ha5yJZEJbIc%Joc5P$o)%bh;O!pSSToi%>e%h7KaYa@n4j zuOcKN{8k043+NEAC8conLWnqiRo4x3EI;7BrA}if1_r? zSSnGQ7hLN!xf4x-?j#WM&K(P6S_WlNv>}bGqqx(D5>1pTFMWbpoZdlas8}9RH$tPh z`FDN9_(rkE9d-3X6Qe3oj#nGAE`VFEQ?Nzq-(iWBiT{XBCSfcRt%zWS9Rt9I#@trm zL}E@g<=YGHQ5BAX5w_1tDAEmkQ`WN0WqqL`H@Bs?%8kYIhzz&fl*WP!?fk)!01C^fRnO?t03Cp=oYkmS)dHu$olIe_)8q92gukWHdlt*3|g#?93-}LUaIO6t(8F9Nb+YOb00Y|tM&Id&paXIbu zv(!!dMqoqwW}SP@5UTHeA-5 zolnUH-|uovx8(CH+gLm8S2)yqKfkN$LO~yBM|7+mr6zM(GWu8p0Vg|Cj=b(ZVUjk= zO>s^vvHZBF{2-KIC-L6atsAf_{zTq);?p>_E0mnknht#-nCcAa+FQ2!! zQJY+cGqGSu;t`M6W;Um_vfqch1#7iR(N%7VS>UZ&$G6kNYp68(h>`=`4~KUPK&DAa z9{-m9-6&KH&(YTW{uB6>8i8PE;>JDo0S}KmkV(33yiJtCS!;C4q!HtQD5sNNJg7_muc?N}kx7A9t-?yy}*_ zU-*zZ!apD9r_vkWYgj53q~K^t+4Ny=#D=pIT~Lf{b1JKGNjI;M(8>h`l0;|a;-~tm z4?^|Ui_dWyc1@}uNSWwNd6i~rgE4gYpRoLyEh-2d=Owq9J-wA{Wh4aZ2x}SqEq%OIG-(C2x$mFnjO7&n5$_iMF9&wG!-1IR6Q}8V(S?C z-3F|ZU~|1@wl>#iE&Qvly|2w#i&Kw^M-jD0B_8doI^~4;ISu;OL1wyET}S&V#?uJ` zDk0)@9OE2t{;hFXSfYV6C$G$rJV~xGy8gP6#F!eCnwxJwVwXB=1B>Ghz%nGHZwd9wUeJ82rCoOf z@1;E+x#^m{ubL!_y{4tj-YvSbJ}Z_h0imOFr2*CJGFy_?$|?_SdJM#sUC(9 zyS0n#G$jwRA=ar3l=yGJ9~Y&t=cE&*PXcvU$bT6DeTTjZ^@{lEq-+ z#Yr0g;yRN{4-EA-cUu=4h}SS=9-`%in^cBU?N1A!NZZRD+`u37ApL_9Q&XualmPZ^ zskuRko*myF!C7^`2ALsOIdmRpH3HgF=qiUarjOXy2YJm;kEOY#NM*XclKSBG?A3FZ zy_Tmo%U@Ndq_dU%=ba{Q-GGH;jn!Wc!kJpx+Dv=5fS=f8N;Sr1eHi0=)Zn}=6;sUU zvi)d0cPIpbw9ug^kfnI(7w>Xe#dWz`;`9zU!2Ud*mAZ1YnXDPhD5;%;5}9U{7nZx6 z0NzDh=dmJT41)vyiZ5|lWUm;;F0+h|U1e+wbERKMfK5}RF%DTOD9vwR48O33sA!J} z6HZrxyVggoRKJ9OQE`usfahhbzB|aapO*B=pNjA6Korrue13tsQ+GaRPmzDXl=FdC z9G`iYA*6+C=-!qf-jXs6FHJ=K2Vz0qS64By%a;5{-`%6jtaCdw30Lbearf9wax|G* z2K(FZa5!69eW?n(xnb=1?SY(eL4$6?(%uHm?ht3Y7oB}gj{3Qgpi_z&QzM})zRDZ1 zMfDHw7$A>&S+usx+zy;I2~8EWtMlRQQSBRGn=E?;?GK09>;S&Yn!bT6R~+I^L{%og zOPy972`WfqUauIwoaEo3v*Yuf7qPqsHe`)jz!)t1*iq9Cjky zwlHWTb%dB}EsiO9N4~J1MH-OYpZb2Q7~RtXk@H4z*VwyJG?IEh0g*Z!n30gjAwm!` zT^Um4WHw)H=g4o~mKZw3#sozNz)vey;i%_Vo1vNU*U5>{-*q%Q;z=-!?IIQGSb7Or zy0`Sq(Zm#a!?)^VFBw~0tdgG|DCj4+(u&cU zE)X#Bk#*pPT3ue5s3O(v*oN)-A=5qy`5T&D^bv6@*%Xok`M@L@qaF9m2-!c{X-Gc8 zFaa>V*Q`9L`TGGpP znuEEz_=Mwj)zgDZbm}XVmAkT>JWQXA(4w#at{8~2WHoR)1!Eyg7B`SfQzLgFHMWvV ztYR*$;8L6;mmRhf2j0W$dMQ;b_2Hy`TM{$8@Dq{#^H4ETBvkjcL~AM60;BuT;iltFGc@y0PM3-)u+d_B}HuLHey|9U8)>iAr>5 zIy<7bU&jEh`7HU~QM*z8lS&)5vD=J}oqBG@YZ{|)sNqG9F=Rcdvb%aV+0Z~CZi0RK zXd}@z1OqABVuQY?wehl2YK7WScY=JyKdNniPE~5cRT>kUK>T=~E37-{v^w~#h6^5- zXmWm)Ntfz>^6!};i}w`qyh3bIFu}~9O$tpk32u*524GYz8b=VWfgiycM2eW2o#562 z*rF|(&h+gl-kcV0p8sKArULV3&7{B=KnYktFxML`SH?#?9U8GN;5-s^QI}rv06>Xa z!bEd}-xc2tG9j&A_1Eb3xgVmlf>SN_x}z8R9=F=-=t&**Nw{> z62M{2NE2DQiJaof^vz%fja~f1y)@U&ni`nOLiqmU=40|9c#Nm}YEEBjIzlcngP(yV z`TO42;C%hPayj=8V4ehpkoan*m5Aop_||xEaGTw|t@4h#e|h4a7RUZY8uMNH5=t^sBdZXI9o|gm^7b=1Du2HfSF>xca=nT& zRjQ#DgKE*7Y+h>@P#WDY0Q9Zuf}EP+s?N@X2-Z-nRMT^kvZiWxbTn1|gyz_JrhZ7e zhW-BHB*8pZIma~k$@=jto4A{sDEKYU;0~i|gJ3h#u}p5kqWMo}is%=tjIe_&@nt0P zHTQUTJOa(mjSr*rc@Xa=@thw=c2`XC?h`kB;TSK!`M{FzKp3rkANpU8 z$nu*jdPTl+yA8|e6U7=FRUT+|ma<*tN5Z5b=S;K-sUiwP(*k;MpzHn|zNc>vHW2v77i0cLGvo8QDBmU>;ib#zF^Te87T^_37-JG^4cBH51ij)n=<*7ISg4L8kxE+HE8jy0-SM;g?!4IM>iKq*^%7@vthY%i)&?ok91F>Bun67#blb_ zy=Jw{zG2UAw-dHcFE+L_c%)_5e|Wz>F%Mnw?I3;eUi%;@AZ7oRQ@D=ZwHbT%^(uuE z`8zs6_C1-gvKvc(OWxfvGT+~#Evl2Jf?8SB{;L+a@~L@i#4WT+{2?KTC-yA^2Rox3 z76=t&?f`3kp8Sy}nj6LE2)gcT=gZc2$c@DD82} z%wdsQd@vQ;JR+ouuTEK;mwq+oWyfx`eGVryg+Wui7PuPbRaU691o90UdrDT;8&7}lkD>|zEWrFbQ5C1|M_|r@1H{5d3^Iaf^_(Q z|MI`}FU2?=wlin&4V2{XYX5BQ|Al^?^hHdd$9i99w=_^4zwP3i5M!Q)>2*BE`XYf( zd4G2GuQ4lq=DU%<5$pgW!1vd9#4=OJNkfk@(N^F9#`x!;)-nwo6)NkKz#W}{8tBhL-3am(W3r8L=HTF+0FR6F0Y z8D*~E{ML-;eyc7!bIDhg{-^mTg&2wv@x4f6_;G0UD%=KY;CYos+1JKdWy!*dHT%L*!4Kp)@;C7m8_~c0 z>jGqE?bzO{H;%{|hpES4h4(#gr5x8qNSOrF&}fIiN=`{tH7j BU)KNt diff --git a/docs/reference/transform/images/transform-alert-summary-actions.png b/docs/reference/transform/images/transform-alert-summary-actions.png new file mode 100644 index 0000000000000000000000000000000000000000..3ec2bd86f3735e754fc59451ee91607540b76707 GIT binary patch literal 142958 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 htl83xX(B4_QgP6 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

      <<@ouKPnUw%Z6hWty%|KTR1hiExl_pXW~ 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. [role="screenshot"] -image::images/transform-alert-actions.png["Selecting connector type",500] +image::images/transform-alert-actions.png["Selecting action variables",500] // NOTE: This is screenshot is automatically generated. Do not edit it directly. After you save the configurations, the rule appears in the *{rules-ui}* list @@ -92,3 +99,44 @@ The name of an alert is always the same as the {transform} ID of the associated {transform} that triggered it. You can mute the notifications for a particular {transform} on the page of the rule that lists the individual alerts. You can open it via *{rules-ui}* by selecting the rule name. + +[[transform-action-variables]] +== Action variables + +The following variables are specific to the {transform} health rule type. +You can also specify {kibana-ref}/rule-action-variables.html[variables common to all rules]. + +`context.message`:: +A preconstructed message for the rule. For example: `Transform test-1 is not started.` + +`context.results`:: +The most recent results, which you can iterate over by using the +https://mustache.github.io/[Mustache] template array syntax. For example, the +message in an email connector action might contain: ++ +-- +[source,sh] +-------------------------------------------------- +[{{rule.name}}] Transform health check result: +{{context.message}} +{{#context.results}} + Transform ID: {{transform_id}} + {{#description}}Transform description: {{description}} + {{/description}}{{#transform_state}}Transform state: {{transform_state}} + {{/transform_state}}{{#health_status}}Transform health status: {{health_status}} + {{/health_status}}{{#issues}}Issue: {{issue}} + Issue count: {{count}} + {{#details}}Issue details: {{details}} + {{/details}}{{#first_occurrence}}First occurrence: {{first_occurrence}} + {{/first_occurrence}} + {{/issues}}{{#failure_reason}}Failure reason: {{failure_reason}} + {{/failure_reason}}{{#notification_message}}Notification message: {{notification_message}} + {{/notification_message}}{{#node_name}}Node name: {{node_name}} + {{/node_name}}{{#timestamp}}Timestamp: {{timestamp}} + {{/timestamp}} +{{/context.results}} +-------------------------------------------------- +-- + +For more examples, refer to +{kibana-ref}/rule-action-variables.html[Rule action variables]. \ No newline at end of file From 10dcb8e8bd4d845fbaa232c60fb8326471ee5b8b Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Tue, 12 Mar 2024 07:35:02 -0700 Subject: [PATCH 08/34] Add systemd native access (#106151) This commit moves systemd access to the NativeAccess lib. relates #104876 --- .../jna/JnaNativeLibraryProvider.java | 3 +- .../nativeaccess/jna/JnaSystemdLibrary.java | 31 +++++ libs/native/src/main/java/module-info.java | 2 +- .../nativeaccess/AbstractNativeAccess.java | 5 + .../nativeaccess/LinuxNativeAccess.java | 10 ++ .../nativeaccess/NativeAccess.java | 2 + .../nativeaccess/NoopNativeAccess.java | 6 + .../elasticsearch/nativeaccess/Systemd.java | 55 +++++++++ .../nativeaccess/lib/NativeLibrary.java | 2 +- .../nativeaccess/lib/SystemdLibrary.java | 13 ++ .../jdk/JdkNativeLibraryProvider.java | 3 +- .../nativeaccess/jdk/JdkSystemdLibrary.java | 65 ++++++++++ modules/systemd/build.gradle | 4 + .../systemd/src/main/java/module-info.java | 2 +- .../org/elasticsearch/systemd/Libsystemd.java | 38 ------ .../elasticsearch/systemd/SystemdPlugin.java | 50 ++++---- .../systemd/SystemdPluginTests.java | 113 +++++++++--------- 17 files changed, 281 insertions(+), 123 deletions(-) create mode 100644 libs/native/jna/src/main/java/org/elasticsearch/nativeaccess/jna/JnaSystemdLibrary.java create mode 100644 libs/native/src/main/java/org/elasticsearch/nativeaccess/Systemd.java create mode 100644 libs/native/src/main/java/org/elasticsearch/nativeaccess/lib/SystemdLibrary.java create mode 100644 libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkSystemdLibrary.java delete mode 100644 modules/systemd/src/main/java/org/elasticsearch/systemd/Libsystemd.java diff --git a/libs/native/jna/src/main/java/org/elasticsearch/nativeaccess/jna/JnaNativeLibraryProvider.java b/libs/native/jna/src/main/java/org/elasticsearch/nativeaccess/jna/JnaNativeLibraryProvider.java index a513e89b6a3b3..7d43cb2e3d4bb 100644 --- a/libs/native/jna/src/main/java/org/elasticsearch/nativeaccess/jna/JnaNativeLibraryProvider.java +++ b/libs/native/jna/src/main/java/org/elasticsearch/nativeaccess/jna/JnaNativeLibraryProvider.java @@ -10,11 +10,12 @@ import org.elasticsearch.nativeaccess.lib.NativeLibraryProvider; import org.elasticsearch.nativeaccess.lib.PosixCLibrary; +import org.elasticsearch.nativeaccess.lib.SystemdLibrary; import java.util.Map; public class JnaNativeLibraryProvider extends NativeLibraryProvider { public JnaNativeLibraryProvider() { - super("jna", Map.of(PosixCLibrary.class, JnaPosixCLibrary::new)); + super("jna", Map.of(PosixCLibrary.class, JnaPosixCLibrary::new, SystemdLibrary.class, JnaSystemdLibrary::new)); } } diff --git a/libs/native/jna/src/main/java/org/elasticsearch/nativeaccess/jna/JnaSystemdLibrary.java b/libs/native/jna/src/main/java/org/elasticsearch/nativeaccess/jna/JnaSystemdLibrary.java new file mode 100644 index 0000000000000..f06361e8807c5 --- /dev/null +++ b/libs/native/jna/src/main/java/org/elasticsearch/nativeaccess/jna/JnaSystemdLibrary.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.nativeaccess.jna; + +import com.sun.jna.Library; +import com.sun.jna.Native; + +import org.elasticsearch.nativeaccess.lib.SystemdLibrary; + +class JnaSystemdLibrary implements SystemdLibrary { + private interface NativeFunctions extends Library { + int sd_notify(int unset_environment, String state); + } + + private final NativeFunctions functions; + + JnaSystemdLibrary() { + this.functions = Native.load("libsystemd.so.0", NativeFunctions.class); + } + + @Override + public int sd_notify(int unset_environment, String state) { + return functions.sd_notify(unset_environment, state); + } +} diff --git a/libs/native/src/main/java/module-info.java b/libs/native/src/main/java/module-info.java index dbbbebf5fd393..ea049ff888cb3 100644 --- a/libs/native/src/main/java/module-info.java +++ b/libs/native/src/main/java/module-info.java @@ -14,7 +14,7 @@ requires org.elasticsearch.base; requires org.elasticsearch.logging; - exports org.elasticsearch.nativeaccess to org.elasticsearch.server; + exports org.elasticsearch.nativeaccess to org.elasticsearch.server, org.elasticsearch.systemd; // allows jna to implement a library provider, and ProviderLocator to load it exports org.elasticsearch.nativeaccess.lib to org.elasticsearch.nativeaccess.jna, org.elasticsearch.base; diff --git a/libs/native/src/main/java/org/elasticsearch/nativeaccess/AbstractNativeAccess.java b/libs/native/src/main/java/org/elasticsearch/nativeaccess/AbstractNativeAccess.java index 5f69101696884..fa23966dbeb79 100644 --- a/libs/native/src/main/java/org/elasticsearch/nativeaccess/AbstractNativeAccess.java +++ b/libs/native/src/main/java/org/elasticsearch/nativeaccess/AbstractNativeAccess.java @@ -24,4 +24,9 @@ protected AbstractNativeAccess(String name) { String getName() { return name; } + + @Override + public Systemd systemd() { + return null; + } } diff --git a/libs/native/src/main/java/org/elasticsearch/nativeaccess/LinuxNativeAccess.java b/libs/native/src/main/java/org/elasticsearch/nativeaccess/LinuxNativeAccess.java index f990dbdf2d9de..64f13c12f7735 100644 --- a/libs/native/src/main/java/org/elasticsearch/nativeaccess/LinuxNativeAccess.java +++ b/libs/native/src/main/java/org/elasticsearch/nativeaccess/LinuxNativeAccess.java @@ -9,9 +9,19 @@ package org.elasticsearch.nativeaccess; import org.elasticsearch.nativeaccess.lib.NativeLibraryProvider; +import org.elasticsearch.nativeaccess.lib.SystemdLibrary; class LinuxNativeAccess extends PosixNativeAccess { + + Systemd systemd; + LinuxNativeAccess(NativeLibraryProvider libraryProvider) { super("Linux", libraryProvider); + this.systemd = new Systemd(libraryProvider.getLibrary(SystemdLibrary.class)); + } + + @Override + public Systemd systemd() { + return systemd; } } diff --git a/libs/native/src/main/java/org/elasticsearch/nativeaccess/NativeAccess.java b/libs/native/src/main/java/org/elasticsearch/nativeaccess/NativeAccess.java index 5091c75041786..77b638690d1b9 100644 --- a/libs/native/src/main/java/org/elasticsearch/nativeaccess/NativeAccess.java +++ b/libs/native/src/main/java/org/elasticsearch/nativeaccess/NativeAccess.java @@ -26,4 +26,6 @@ static NativeAccess instance() { * @return true if running as root, or false if unsure */ boolean definitelyRunningAsRoot(); + + Systemd systemd(); } diff --git a/libs/native/src/main/java/org/elasticsearch/nativeaccess/NoopNativeAccess.java b/libs/native/src/main/java/org/elasticsearch/nativeaccess/NoopNativeAccess.java index 2bc06f21c9775..6eb6145699fe7 100644 --- a/libs/native/src/main/java/org/elasticsearch/nativeaccess/NoopNativeAccess.java +++ b/libs/native/src/main/java/org/elasticsearch/nativeaccess/NoopNativeAccess.java @@ -19,4 +19,10 @@ public boolean definitelyRunningAsRoot() { logger.warn("Cannot check if running as root because native access is not available"); return false; } + + @Override + public Systemd systemd() { + logger.warn("Cannot get systemd access because native access is not available"); + return null; + } } diff --git a/libs/native/src/main/java/org/elasticsearch/nativeaccess/Systemd.java b/libs/native/src/main/java/org/elasticsearch/nativeaccess/Systemd.java new file mode 100644 index 0000000000000..4deade118b788 --- /dev/null +++ b/libs/native/src/main/java/org/elasticsearch/nativeaccess/Systemd.java @@ -0,0 +1,55 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.nativeaccess; + +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; +import org.elasticsearch.nativeaccess.lib.SystemdLibrary; + +import java.util.Locale; + +public class Systemd { + private static final Logger logger = LogManager.getLogger(Systemd.class); + + private final SystemdLibrary lib; + + Systemd(SystemdLibrary lib) { + this.lib = lib; + } + + /** + * Notify systemd that the process is ready. + * + * @throws RuntimeException on failure to notify systemd + */ + public void notify_ready() { + notify("READY=1", false); + } + + public void notify_extend_timeout(long seconds) { + notify("EXTEND_TIMEOUT_USEC=" + (seconds * 1000000), true); + } + + public void notify_stopping() { + notify("STOPPING=1", true); + } + + private void notify(String state, boolean warnOnError) { + int rc = lib.sd_notify(0, state); + logger.trace("sd_notify({}, {}) returned [{}]", 0, state, rc); + if (rc < 0) { + String message = String.format(Locale.ROOT, "sd_notify(%d, %s) returned error [%d]", 0, state, rc); + if (warnOnError) { + logger.warn(message); + } else { + throw new RuntimeException(message); + } + } + } +} diff --git a/libs/native/src/main/java/org/elasticsearch/nativeaccess/lib/NativeLibrary.java b/libs/native/src/main/java/org/elasticsearch/nativeaccess/lib/NativeLibrary.java index 39a4137aeb0f2..cf2116440a8bc 100644 --- a/libs/native/src/main/java/org/elasticsearch/nativeaccess/lib/NativeLibrary.java +++ b/libs/native/src/main/java/org/elasticsearch/nativeaccess/lib/NativeLibrary.java @@ -9,4 +9,4 @@ package org.elasticsearch.nativeaccess.lib; /** A marker interface for libraries that can be loaded by {@link org.elasticsearch.nativeaccess.lib.NativeLibraryProvider} */ -public sealed interface NativeLibrary permits PosixCLibrary {} +public sealed interface NativeLibrary permits PosixCLibrary, SystemdLibrary {} diff --git a/libs/native/src/main/java/org/elasticsearch/nativeaccess/lib/SystemdLibrary.java b/libs/native/src/main/java/org/elasticsearch/nativeaccess/lib/SystemdLibrary.java new file mode 100644 index 0000000000000..3c4ffefb6e41f --- /dev/null +++ b/libs/native/src/main/java/org/elasticsearch/nativeaccess/lib/SystemdLibrary.java @@ -0,0 +1,13 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.nativeaccess.lib; + +public non-sealed interface SystemdLibrary extends NativeLibrary { + int sd_notify(int unset_environment, String state); +} diff --git a/libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkNativeLibraryProvider.java b/libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkNativeLibraryProvider.java index 48364bce57fdb..b808dc3151058 100644 --- a/libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkNativeLibraryProvider.java +++ b/libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkNativeLibraryProvider.java @@ -10,12 +10,13 @@ import org.elasticsearch.nativeaccess.lib.NativeLibraryProvider; import org.elasticsearch.nativeaccess.lib.PosixCLibrary; +import org.elasticsearch.nativeaccess.lib.SystemdLibrary; import java.util.Map; public class JdkNativeLibraryProvider extends NativeLibraryProvider { public JdkNativeLibraryProvider() { - super("jdk", Map.of(PosixCLibrary.class, JdkPosixCLibrary::new)); + super("jdk", Map.of(PosixCLibrary.class, JdkPosixCLibrary::new, SystemdLibrary.class, JdkSystemdLibrary::new)); } } diff --git a/libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkSystemdLibrary.java b/libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkSystemdLibrary.java new file mode 100644 index 0000000000000..682b94b6f4f74 --- /dev/null +++ b/libs/native/src/main21/java/org/elasticsearch/nativeaccess/jdk/JdkSystemdLibrary.java @@ -0,0 +1,65 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.nativeaccess.jdk; + +import org.elasticsearch.nativeaccess.lib.SystemdLibrary; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static java.lang.foreign.ValueLayout.ADDRESS; +import static java.lang.foreign.ValueLayout.JAVA_INT; +import static org.elasticsearch.nativeaccess.jdk.LinkerHelper.downcallHandle; + +class JdkSystemdLibrary implements SystemdLibrary { + static { + System.load(findLibSystemd()); + } + + // On some systems libsystemd does not have a non-versioned symlink. System.loadLibrary only knows how to find + // non-versioned library files. So we must manually check the library path to find what we need. + static String findLibSystemd() { + final String libsystemd = "libsystemd.so.0"; + String libpath = System.getProperty("java.library.path"); + for (String basepathStr : libpath.split(":")) { + var basepath = Paths.get(basepathStr); + if (Files.exists(basepath) == false) { + continue; + } + try (var stream = Files.walk(basepath)) { + var foundpath = stream.filter(Files::isDirectory).map(p -> p.resolve(libsystemd)).filter(Files::exists).findAny(); + if (foundpath.isPresent()) { + return foundpath.get().toAbsolutePath().toString(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + } + throw new UnsatisfiedLinkError("Could not find " + libsystemd + " in java.library.path: " + libpath); + } + + private static final MethodHandle sd_notify$mh = downcallHandle("sd_notify", FunctionDescriptor.of(JAVA_INT, JAVA_INT, ADDRESS)); + + @Override + public int sd_notify(int unset_environment, String state) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeState = arena.allocateUtf8String(state); + return (int) sd_notify$mh.invokeExact(unset_environment, nativeState); + } catch (Throwable t) { + throw new AssertionError(t); + } + } +} diff --git a/modules/systemd/build.gradle b/modules/systemd/build.gradle index 0f5c2a4c2fb19..351211ffd3c0e 100644 --- a/modules/systemd/build.gradle +++ b/modules/systemd/build.gradle @@ -11,3 +11,7 @@ esplugin { classname 'org.elasticsearch.systemd.SystemdPlugin' } +dependencies { + implementation project(':libs:elasticsearch-native') +} + diff --git a/modules/systemd/src/main/java/module-info.java b/modules/systemd/src/main/java/module-info.java index bd92851fde3a6..b3f5b64ff312f 100644 --- a/modules/systemd/src/main/java/module-info.java +++ b/modules/systemd/src/main/java/module-info.java @@ -12,5 +12,5 @@ requires org.elasticsearch.xcontent; requires org.apache.logging.log4j; requires org.apache.lucene.core; - requires com.sun.jna; + requires org.elasticsearch.nativeaccess; } diff --git a/modules/systemd/src/main/java/org/elasticsearch/systemd/Libsystemd.java b/modules/systemd/src/main/java/org/elasticsearch/systemd/Libsystemd.java deleted file mode 100644 index ba34a18c83e37..0000000000000 --- a/modules/systemd/src/main/java/org/elasticsearch/systemd/Libsystemd.java +++ /dev/null @@ -1,38 +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 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 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.systemd; - -import com.sun.jna.Native; - -import java.security.AccessController; -import java.security.PrivilegedAction; - -/** - * Provides access to the native method sd_notify from libsystemd. - */ -class Libsystemd { - - static { - AccessController.doPrivileged((PrivilegedAction) () -> { - Native.register(Libsystemd.class, "libsystemd.so.0"); - return null; - }); - } - - /** - * Notify systemd of state changes. - * - * @param unset_environment if non-zero, the NOTIFY_SOCKET environment variable will be unset before returning and further calls to - * sd_notify will fail - * @param state a new-line separated list of variable assignments; some assignments are understood directly by systemd - * @return a negative error code on failure, and positive if status was successfully sent - */ - static native int sd_notify(int unset_environment, String state); - -} diff --git a/modules/systemd/src/main/java/org/elasticsearch/systemd/SystemdPlugin.java b/modules/systemd/src/main/java/org/elasticsearch/systemd/SystemdPlugin.java index e3dca57472ade..947d1fa58e963 100644 --- a/modules/systemd/src/main/java/org/elasticsearch/systemd/SystemdPlugin.java +++ b/modules/systemd/src/main/java/org/elasticsearch/systemd/SystemdPlugin.java @@ -14,6 +14,8 @@ import org.elasticsearch.Build; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.nativeaccess.NativeAccess; +import org.elasticsearch.nativeaccess.Systemd; import org.elasticsearch.plugins.ClusterPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.threadpool.Scheduler; @@ -26,6 +28,7 @@ public class SystemdPlugin extends Plugin implements ClusterPlugin { private static final Logger logger = LogManager.getLogger(SystemdPlugin.class); private final boolean enabled; + private final Systemd systemd; final boolean isEnabled() { return enabled; @@ -44,18 +47,21 @@ public SystemdPlugin() { } if (isPackageDistribution == false) { logger.debug("disabling sd_notify as the build type [{}] is not a package distribution", buildType); - enabled = false; + this.enabled = false; + this.systemd = null; return; } logger.trace("ES_SD_NOTIFY is set to [{}]", esSDNotify); if (esSDNotify == null) { - enabled = false; + this.enabled = false; + this.systemd = null; return; } if (Boolean.TRUE.toString().equals(esSDNotify) == false && Boolean.FALSE.toString().equals(esSDNotify) == false) { throw new RuntimeException("ES_SD_NOTIFY set to unexpected value [" + esSDNotify + "]"); } - enabled = Boolean.TRUE.toString().equals(esSDNotify); + this.enabled = Boolean.TRUE.toString().equals(esSDNotify); + this.systemd = enabled ? NativeAccess.instance().systemd() : null; } private final SetOnce extender = new SetOnce<>(); @@ -77,19 +83,25 @@ public Collection createComponents(PluginServices services) { * Therefore, every fifteen seconds we send systemd a message via sd_notify to extend the timeout by thirty seconds. We will cancel * this scheduled task after we successfully notify systemd that we are ready. */ - extender.set(services.threadPool().scheduleWithFixedDelay(() -> { - final int rc = sd_notify(0, "EXTEND_TIMEOUT_USEC=30000000"); - if (rc < 0) { - logger.warn("extending startup timeout via sd_notify failed with [{}]", rc); - } - }, TimeValue.timeValueSeconds(15), EsExecutors.DIRECT_EXECUTOR_SERVICE)); + extender.set( + services.threadPool() + .scheduleWithFixedDelay( + () -> { systemd.notify_extend_timeout(30); }, + TimeValue.timeValueSeconds(15), + EsExecutors.DIRECT_EXECUTOR_SERVICE + ) + ); return List.of(); } - int sd_notify(@SuppressWarnings("SameParameterValue") final int unset_environment, final String state) { - final int rc = Libsystemd.sd_notify(unset_environment, state); - logger.trace("sd_notify({}, {}) returned [{}]", unset_environment, state, rc); - return rc; + void notifyReady() { + assert systemd != null; + systemd.notify_ready(); + } + + void notifyStopping() { + assert systemd != null; + systemd.notify_stopping(); } @Override @@ -98,11 +110,7 @@ public void onNodeStarted() { assert extender.get() == null; return; } - final int rc = sd_notify(0, "READY=1"); - if (rc < 0) { - // treat failure to notify systemd of readiness as a startup failure - throw new RuntimeException("sd_notify returned error [" + rc + "]"); - } + notifyReady(); assert extender.get() != null; final boolean cancelled = extender.get().cancel(); assert cancelled; @@ -113,11 +121,7 @@ public void close() { if (enabled == false) { return; } - final int rc = sd_notify(0, "STOPPING=1"); - if (rc < 0) { - // do not treat failure to notify systemd of stopping as a failure - logger.warn("sd_notify returned error [{}]", rc); - } + notifyStopping(); } } diff --git a/modules/systemd/src/test/java/org/elasticsearch/systemd/SystemdPluginTests.java b/modules/systemd/src/test/java/org/elasticsearch/systemd/SystemdPluginTests.java index c2d0983e4f825..712483e9c603c 100644 --- a/modules/systemd/src/test/java/org/elasticsearch/systemd/SystemdPluginTests.java +++ b/modules/systemd/src/test/java/org/elasticsearch/systemd/SystemdPluginTests.java @@ -21,16 +21,14 @@ import java.io.IOException; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import static org.elasticsearch.test.hamcrest.OptionalMatchers.isPresentWith; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; @@ -104,83 +102,68 @@ public void testInvalid() { } public void testOnNodeStartedSuccess() { - runTestOnNodeStarted(Boolean.TRUE.toString(), randomIntBetween(0, Integer.MAX_VALUE), (maybe, plugin) -> { + runTestOnNodeStarted(Boolean.TRUE.toString(), false, (maybe, plugin) -> { assertThat(maybe, OptionalMatchers.isEmpty()); + assertThat(plugin.invokedReady.get(), is(true)); verify(plugin.extender()).cancel(); }); } public void testOnNodeStartedFailure() { - final int rc = randomIntBetween(Integer.MIN_VALUE, -1); - runTestOnNodeStarted( - Boolean.TRUE.toString(), - rc, - (maybe, plugin) -> assertThat( - maybe, - isPresentWith( - allOf(instanceOf(RuntimeException.class), hasToString(containsString("sd_notify returned error [" + rc + "]"))) - ) - ) - ); + runTestOnNodeStarted(Boolean.TRUE.toString(), true, (maybe, plugin) -> { + assertThat(maybe, isPresentWith(allOf(instanceOf(RuntimeException.class), hasToString(containsString("notify ready failed"))))); + assertThat(plugin.invokedReady.get(), is(true)); + }); } public void testOnNodeStartedNotEnabled() { - runTestOnNodeStarted(Boolean.FALSE.toString(), randomInt(), (maybe, plugin) -> assertThat(maybe, OptionalMatchers.isEmpty())); + runTestOnNodeStarted(Boolean.FALSE.toString(), randomBoolean(), (maybe, plugin) -> assertThat(maybe, OptionalMatchers.isEmpty())); } private void runTestOnNodeStarted( final String esSDNotify, - final int rc, - final BiConsumer, SystemdPlugin> assertions + final boolean invokeFailure, + final BiConsumer, TestSystemdPlugin> assertions ) { - runTest(esSDNotify, rc, assertions, SystemdPlugin::onNodeStarted, "READY=1"); + runTest(esSDNotify, invokeFailure, assertions, SystemdPlugin::onNodeStarted); } public void testCloseSuccess() { - runTestClose( - Boolean.TRUE.toString(), - randomIntBetween(1, Integer.MAX_VALUE), - (maybe, plugin) -> assertThat(maybe, OptionalMatchers.isEmpty()) - ); + runTestClose(Boolean.TRUE.toString(), false, (maybe, plugin) -> { + assertThat(maybe, OptionalMatchers.isEmpty()); + assertThat(plugin.invokedStopping.get(), is(true)); + }); } public void testCloseFailure() { - runTestClose( - Boolean.TRUE.toString(), - randomIntBetween(Integer.MIN_VALUE, -1), - (maybe, plugin) -> assertThat(maybe, OptionalMatchers.isEmpty()) - ); + runTestClose(Boolean.TRUE.toString(), true, (maybe, plugin) -> { + assertThat(maybe, OptionalMatchers.isEmpty()); + assertThat(plugin.invokedStopping.get(), is(true)); + }); } public void testCloseNotEnabled() { - runTestClose(Boolean.FALSE.toString(), randomInt(), (maybe, plugin) -> assertThat(maybe, OptionalMatchers.isEmpty())); + runTestClose(Boolean.FALSE.toString(), randomBoolean(), (maybe, plugin) -> { + assertThat(maybe, OptionalMatchers.isEmpty()); + assertThat(plugin.invokedStopping.get(), is(false)); + }); } - private void runTestClose(final String esSDNotify, final int rc, final BiConsumer, SystemdPlugin> assertions) { - runTest(esSDNotify, rc, assertions, SystemdPlugin::close, "STOPPING=1"); + private void runTestClose( + final String esSDNotify, + boolean invokeFailure, + final BiConsumer, TestSystemdPlugin> assertions + ) { + runTest(esSDNotify, invokeFailure, assertions, SystemdPlugin::close); } private void runTest( final String esSDNotify, - final int rc, - final BiConsumer, SystemdPlugin> assertions, - final CheckedConsumer invocation, - final String expectedState + final boolean invokeReadyFailure, + final BiConsumer, TestSystemdPlugin> assertions, + final CheckedConsumer invocation ) { - final AtomicBoolean invoked = new AtomicBoolean(); - final AtomicInteger invokedUnsetEnvironment = new AtomicInteger(); - final AtomicReference invokedState = new AtomicReference<>(); - final SystemdPlugin plugin = new SystemdPlugin(false, randomPackageBuildType, esSDNotify) { - - @Override - int sd_notify(final int unset_environment, final String state) { - invoked.set(true); - invokedUnsetEnvironment.set(unset_environment); - invokedState.set(state); - return rc; - } - - }; + final TestSystemdPlugin plugin = new TestSystemdPlugin(esSDNotify, invokeReadyFailure); startPlugin(plugin); if (Boolean.TRUE.toString().equals(esSDNotify)) { assertNotNull(plugin.extender()); @@ -198,13 +181,29 @@ int sd_notify(final int unset_environment, final String state) { if (success) { assertions.accept(Optional.empty(), plugin); } - if (Boolean.TRUE.toString().equals(esSDNotify)) { - assertTrue(invoked.get()); - assertThat(invokedUnsetEnvironment.get(), equalTo(0)); - assertThat(invokedState.get(), equalTo(expectedState)); - } else { - assertFalse(invoked.get()); - } } + class TestSystemdPlugin extends SystemdPlugin { + final AtomicBoolean invokedReady = new AtomicBoolean(); + final AtomicBoolean invokedStopping = new AtomicBoolean(); + final boolean invokeReadyFailure; + + TestSystemdPlugin(String esSDNotify, boolean invokeFailure) { + super(false, randomPackageBuildType, esSDNotify); + this.invokeReadyFailure = invokeFailure; + } + + @Override + void notifyReady() { + invokedReady.set(true); + if (invokeReadyFailure) { + throw new RuntimeException("notify ready failed"); + } + } + + @Override + void notifyStopping() { + invokedStopping.set(true); + } + } } From e42e47e803001bf6881d9586774698e456f5ac6f Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 12 Mar 2024 09:07:20 -0700 Subject: [PATCH 09/34] Rename LocalExchangeSink and LocalExchangeSource (#106245) These should not have the term Local. --- .../compute/operator/exchange/ExchangeSinkHandler.java | 6 +++--- .../compute/operator/exchange/ExchangeSourceHandler.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkHandler.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkHandler.java index d1a2b8710cd23..945fdff50d31c 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkHandler.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkHandler.java @@ -49,10 +49,10 @@ public ExchangeSinkHandler(BlockFactory blockFactory, int maxBufferSize, LongSup this.lastUpdatedInMillis = new AtomicLong(nowInMillis.getAsLong()); } - private class LocalExchangeSink implements ExchangeSink { + private class ExchangeSinkImpl implements ExchangeSink { boolean finished; - LocalExchangeSink() { + ExchangeSinkImpl() { onChanged(); outstandingSinks.incrementAndGet(); } @@ -155,7 +155,7 @@ private void notifyListeners() { * @see ExchangeSinkOperator */ public ExchangeSink createExchangeSink() { - return new LocalExchangeSink(); + return new ExchangeSinkImpl(); } /** diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSourceHandler.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSourceHandler.java index 859b1fc73c3e1..7492fa8c19385 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSourceHandler.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSourceHandler.java @@ -44,10 +44,10 @@ public ExchangeSourceHandler(int maxBufferSize, Executor fetchExecutor) { this.outstandingSources = new PendingInstances(() -> buffer.finish(true)); } - private class LocalExchangeSource implements ExchangeSource { + private class ExchangeSourceImpl implements ExchangeSource { private boolean finished; - LocalExchangeSource() { + ExchangeSourceImpl() { outstandingSources.trackNewInstance(); } @@ -95,7 +95,7 @@ public int bufferSize() { * @see ExchangeSinkOperator */ public ExchangeSource createExchangeSource() { - return new LocalExchangeSource(); + return new ExchangeSourceImpl(); } /** From 39779b73dd4fbd4383dd9c00c70f21298d8821ea Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Tue, 12 Mar 2024 17:11:17 +0100 Subject: [PATCH 10/34] Fix testUpgradesLegitimateVersions (#106248) DefaultBuildVersion has an assertion to only accept versions that are >= 0. This change updates a test generator to create versions accordingly. Closes: #106213 --- .../src/test/java/org/elasticsearch/env/NodeMetadataTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java b/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java index 46d6beb56138b..f60812977d578 100644 --- a/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java @@ -33,7 +33,7 @@ public class NodeMetadataTests extends ESTestCase { // (Index)VersionUtils.randomVersion() only returns known versions, which are necessarily no later than (Index)Version.CURRENT; // however we want to also consider our behaviour with all versions, so occasionally pick up a truly random version. private Version randomVersion() { - return rarely() ? Version.fromId(randomInt()) : VersionUtils.randomVersion(random()); + return rarely() ? Version.fromId(randomNonNegativeInt()) : VersionUtils.randomVersion(random()); } private BuildVersion randomBuildVersion() { From bfbb155985e6ccf50af87c70231460ae44361e08 Mon Sep 17 00:00:00 2001 From: Kostas Krikellas <131142368+kkrik-es@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:38:39 +0200 Subject: [PATCH 11/34] [TEST] Set logging level in SnapshotResiliencyTests (#106238) * Set logging level in SnapshotResiliencyTests Some tests check that the expected INFO message gets logged, so they require setting the level to INFO for SnapshotsService. * spotless fixes --- .../org/elasticsearch/snapshots/SnapshotResiliencyTests.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java index edde9f0164a6e..0a53db94b9aaf 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java @@ -188,6 +188,7 @@ import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.BytesRefRecycler; import org.elasticsearch.transport.DisruptableMockTransport; @@ -1390,6 +1391,7 @@ public TransportRequestHandler interceptHandler( safeAwait(testListener); // shouldn't throw } + @TestLogging(reason = "testing logging at INFO level", value = "org.elasticsearch.snapshots.SnapshotsService:INFO") public void testFullSnapshotUnassignedShards() { setupTestCluster(1, 0); // no data nodes, we want unassigned shards @@ -1469,6 +1471,7 @@ public void onFailure(Exception e) { ); } + @TestLogging(reason = "testing logging at INFO level", value = "org.elasticsearch.snapshots.SnapshotsService:INFO") public void testSnapshotNameAlreadyInUseExceptionLogging() { setupTestCluster(1, 1); @@ -1519,6 +1522,7 @@ public void onFailure(Exception e) { ); } + @TestLogging(reason = "testing logging at INFO level", value = "org.elasticsearch.snapshots.SnapshotsService:INFO") public void testIndexNotFoundExceptionLogging() { setupTestCluster(1, 0); // no need for data nodes here @@ -1571,6 +1575,7 @@ public void onFailure(Exception e) { ); } + @TestLogging(reason = "testing logging at INFO level", value = "org.elasticsearch.snapshots.SnapshotsService:INFO") public void testIllegalArgumentExceptionLogging() { setupTestCluster(1, 0); // no need for data nodes here From 68b0acac8f9520037db0150f4863ea91ab4c45ee Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Tue, 12 Mar 2024 10:11:55 -0700 Subject: [PATCH 12/34] Add retrievers using the parser-only approach (#105470) This enhancement adds a new abstraction to the _search API called "retriever." A retriever is something that returns top hits. This adds three initial retrievers called "standard", "knn", and "rrf". The retrievers use a parser-only approach where they are parsed and then translated into a SearchSourceBuilder to execute the actual search. --------- Co-authored-by: Mayya Sharipova --- docs/changelog/105470.yaml | 5 + .../query-dsl/text-expansion-query.asciidoc | 74 +-- docs/reference/rest-api/common-parms.asciidoc | 31 ++ docs/reference/search.asciidoc | 2 + docs/reference/search/retriever.asciidoc | 225 ++++++++ docs/reference/search/rrf.asciidoc | 228 ++++---- .../search-application-api.asciidoc | 50 +- docs/reference/search/search.asciidoc | 11 + .../semantic-search/hybrid-search.asciidoc | 103 ++-- .../10_standard_retriever.yml | 519 ++++++++++++++++++ .../search.retrievers/20_knn_retriever.yml | 73 +++ server/src/main/java/module-info.java | 4 +- .../elasticsearch/plugins/SearchPlugin.java | 50 ++ .../elasticsearch/search/SearchModule.java | 24 + .../search/builder/SearchSourceBuilder.java | 47 +- .../search/collapse/CollapseBuilder.java | 5 + .../search/retriever/KnnRetrieverBuilder.java | 171 ++++++ .../search/retriever/RetrieverBuilder.java | 234 ++++++++ .../search/retriever/RetrieverParser.java | 30 + .../retriever/RetrieverParserContext.java | 42 ++ .../search/retriever/RetrieversFeatures.java | 30 + .../retriever/StandardRetrieverBuilder.java | 229 ++++++++ .../searchafter/SearchAfterBuilder.java | 5 +- ...lasticsearch.features.FeatureSpecification | 1 + .../KnnRetrieverBuilderParsingTests.java | 82 +++ .../retriever/RetrieverBuilderErrorTests.java | 102 ++++ .../RetrieverBuilderVersionTests.java | 80 +++ .../StandardRetrieverBuilderParsingTests.java | 112 ++++ .../searchafter/SearchAfterBuilderTests.java | 31 +- .../search/vectors/KnnSearchBuilderTests.java | 2 +- .../retriever/TestRetrieverBuilder.java | 93 ++++ .../rank-rrf/src/main/java/module-info.java | 4 + .../xpack/rank/rrf/RRFFeatures.java | 24 + .../xpack/rank/rrf/RRFRankPlugin.java | 5 + .../xpack/rank/rrf/RRFRetrieverBuilder.java | 134 +++++ ...lasticsearch.features.FeatureSpecification | 8 + .../rrf/RRFRetrieverBuilderParsingTests.java | 88 +++ .../rank/rrf/RRFRetrieverBuilderTests.java | 151 +++++ .../test/license/100_license.yml | 38 +- .../test/rrf/300_rrf_retriever.yml | 331 +++++++++++ .../test/rrf/400_rrf_retriever_script.yml | 342 ++++++++++++ 41 files changed, 3609 insertions(+), 211 deletions(-) create mode 100644 docs/changelog/105470.yaml create mode 100644 docs/reference/search/retriever.asciidoc create mode 100644 rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/10_standard_retriever.yml create mode 100644 rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/20_knn_retriever.yml create mode 100644 server/src/main/java/org/elasticsearch/search/retriever/KnnRetrieverBuilder.java create mode 100644 server/src/main/java/org/elasticsearch/search/retriever/RetrieverBuilder.java create mode 100644 server/src/main/java/org/elasticsearch/search/retriever/RetrieverParser.java create mode 100644 server/src/main/java/org/elasticsearch/search/retriever/RetrieverParserContext.java create mode 100644 server/src/main/java/org/elasticsearch/search/retriever/RetrieversFeatures.java create mode 100644 server/src/main/java/org/elasticsearch/search/retriever/StandardRetrieverBuilder.java create mode 100644 server/src/test/java/org/elasticsearch/search/retriever/KnnRetrieverBuilderParsingTests.java create mode 100644 server/src/test/java/org/elasticsearch/search/retriever/RetrieverBuilderErrorTests.java create mode 100644 server/src/test/java/org/elasticsearch/search/retriever/RetrieverBuilderVersionTests.java create mode 100644 server/src/test/java/org/elasticsearch/search/retriever/StandardRetrieverBuilderParsingTests.java create mode 100644 test/framework/src/main/java/org/elasticsearch/search/retriever/TestRetrieverBuilder.java create mode 100644 x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFFeatures.java create mode 100644 x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilder.java create mode 100644 x-pack/plugin/rank-rrf/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification create mode 100644 x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderParsingTests.java create mode 100644 x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderTests.java create mode 100644 x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/rrf/300_rrf_retriever.yml create mode 100644 x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/rrf/400_rrf_retriever_script.yml diff --git a/docs/changelog/105470.yaml b/docs/changelog/105470.yaml new file mode 100644 index 0000000000000..56425de6c88e4 --- /dev/null +++ b/docs/changelog/105470.yaml @@ -0,0 +1,5 @@ +pr: 105470 +summary: Add retrievers using the parser-only approach +area: Ranking +type: enhancement +issues: [] diff --git a/docs/reference/query-dsl/text-expansion-query.asciidoc b/docs/reference/query-dsl/text-expansion-query.asciidoc index cb0a7c6ea9c01..927b5d0a85886 100644 --- a/docs/reference/query-dsl/text-expansion-query.asciidoc +++ b/docs/reference/query-dsl/text-expansion-query.asciidoc @@ -155,47 +155,55 @@ GET my-index/_search ---- // TEST[skip: TBD] -This can also be achieved by using sub searches combined with <>. +This can also be achieved using <>, +through an <> with multiple +<>. [source,console] ---- GET my-index/_search { - "sub_searches": [ - { - "query": { - "multi_match": { - "query": "How is the weather in Jamaica?", - "fields": [ - "title", - "description" - ] - } - } - }, - { - "query": { - "text_expansion": { - "ml.inference.title_expanded.predicted_value": { - "model_id": ".elser_model_2", - "model_text": "How is the weather in Jamaica?" + "retriever": { + "rrf": { + "retrievers": [ + { + "standard": { + "query": { + "multi_match": { + "query": "How is the weather in Jamaica?", + "fields": [ + "title", + "description" + ] + } + } } - } - } - }, - { - "query": { - "text_expansion": { - "ml.inference.description_expanded.predicted_value": { - "model_id": ".elser_model_2", - "model_text": "How is the weather in Jamaica?" + }, + { + "standard": { + "query": { + "text_expansion": { + "ml.inference.title_expanded.predicted_value": { + "model_id": ".elser_model_2", + "model_text": "How is the weather in Jamaica?" + } + } + } + } + }, + { + "standard": { + "query": { + "text_expansion": { + "ml.inference.description_expanded.predicted_value": { + "model_id": ".elser_model_2", + "model_text": "How is the weather in Jamaica?" + } + } + } } } - } - } - ], - "rank": { - "rrf": { + ], "window_size": 10, "rank_constant": 20 } diff --git a/docs/reference/rest-api/common-parms.asciidoc b/docs/reference/rest-api/common-parms.asciidoc index a5ab70ae85181..6757b6be24207 100644 --- a/docs/reference/rest-api/common-parms.asciidoc +++ b/docs/reference/rest-api/common-parms.asciidoc @@ -1281,3 +1281,34 @@ Default: 1, the primary shard. See <>. -- end::wait_for_active_shards[] + +tag::rrf-retrievers[] +`retrievers`:: +(Required, array of retriever objects) ++ +A list of child retrievers to specify which sets of returned top documents +will have the RRF formula applied to them. Each child retriever carries an +equal weight as part of the RRF formula. Two or more child retrievers are +required. +end::rrf-retrievers[] + +tag::rrf-rank-constant[] +`rank_constant`:: +(Optional, integer) ++ +This value determines how much influence documents in individual +result sets per query have over the final ranked result set. A higher value indicates +that lower ranked documents have more influence. This value must be greater than or +equal to `1`. Defaults to `60`. +end::rrf-rank-constant[] + +tag::rrf-window-size[] +`window_size`:: +(Optional, integer) ++ +This value determines the size of the individual result sets per +query. A higher value will improve result relevance at the cost of performance. The final +ranked result set is pruned down to the search request's <>. +`window_size` must be greater than or equal to `size` and greater than or equal to `1`. +Defaults to the `size` parameter. +end::rrf-window-size[] diff --git a/docs/reference/search.asciidoc b/docs/reference/search.asciidoc index aa5b4c9aac9b7..b39afff876eed 100644 --- a/docs/reference/search.asciidoc +++ b/docs/reference/search.asciidoc @@ -52,6 +52,8 @@ include::search/point-in-time-api.asciidoc[] include::search/knn-search.asciidoc[] +include::search/retriever.asciidoc[] + include::search/rrf.asciidoc[] include::search/scroll-api.asciidoc[] diff --git a/docs/reference/search/retriever.asciidoc b/docs/reference/search/retriever.asciidoc new file mode 100644 index 0000000000000..a872f1866f42c --- /dev/null +++ b/docs/reference/search/retriever.asciidoc @@ -0,0 +1,225 @@ +[[retriever]] +=== Retriever API + +preview::["This functionality is in technical preview and may be changed or removed in a future release. The syntax will likely change before GA. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features."] + +A retriever is a specification to describe top documents returned from a +search. A retriever replaces other elements of the <> +that also return top documents such as <> and +<>. A retriever may have child retrievers where a +retriever with two or more children is considered a compound retriever. This +allows for complex behavior to be depicted in a tree-like structure, called +the retriever tree, to better clarify the order of operations that occur +during a search. + +The following retrievers are available: + +`standard`:: +A <> that replaces the functionality of a traditional <>. + +`knn`:: +A <> that replaces the functionality of a <>. + +`rrf`:: +A <> that produces top documents from <>. + +[[standard-retriever]] +==== Standard Retriever + +A standard retriever returns top documents from a traditional <>. + +===== Parameters: + +`query`:: +(Optional, <>) ++ +Defines a query to retrieve a set of top documents. + +`filter`:: +(Optional, <>) ++ +Applies a <> to this retriever +where all documents must match this query but do not contribute to the score. + +`search_after`:: +(Optional, <>) ++ +Defines a search after object parameter used for pagination. + +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=terminate_after] + +`sort`:: ++ +(Optional, <>) +A sort object that that specifies the order of matching documents. + +`min_score`:: +(Optional, `float`) ++ +Minimum <> for matching documents. Documents with a +lower `_score` are not included in the top documents. + +`collapse`:: +(Optional, <>) ++ +Collapses the top documents by a specified key into a single top document per key. + +===== Restrictions + +When a retriever tree contains a compound retriever (a retriever with two or more child +retrievers) *only* the query element is allowed. + +===== Example + +[source,js] +---- +GET /index/_search +{ + "retriever": { + "standard": { + "query" { ... }, + "filter" { ... }, + "min_score": ... + } + }, + "size": ... +} +---- +// NOTCONSOLE + +[[knn-retriever]] +==== kNN Retriever + +A kNN retriever returns top documents from a <>. + +===== Parameters + +`field`:: +(Required, string) ++ +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-field] + +`query_vector`:: +(Required if `query_vector_builder` is not defined, array of `float`) ++ +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-query-vector] + +`query_vector_builder`:: +(Required if `query_vector` is not defined, query vector builder object) ++ +Defines a <> to build a query vector. + +`k`:: +(Required, integer) ++ +Number of nearest neighbors to return as top hits. This value must be fewer than +or equal to `num_candidates`. + +`num_candidates`:: +(Required, integer) ++ +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-num-candidates] + +`filter`:: +(Optional, <>) ++ +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-filter] + +`similarity`:: +(Optional, float) ++ +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-similarity] + +===== Restrictions + +The parameters `query_vector` and `query_vector_builder` cannot be used together. + +===== Example: + +[source,js] +---- +GET /index/_search +{ + "retriever": { + "knn": { + "field": ..., + "query_vector": ..., + "k": ..., + "num_candidates": ... + } + } +} +---- +// NOTCONSOLE + +[[rrf-retriever]] +==== RRF Retriever + +An <> retriever returns top documents based on the RRF formula +equally weighting two or more child retrievers. + +===== Parameters + +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=rrf-retrievers] + +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=rrf-rank-constant] + +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=rrf-window-size] + +===== Restrictions + +An RRF retriever is a compound retriever. Child retrievers may not use +elements that are restricted by having a compound retriever as part of +the retriever tree. + +===== Example + +[source,js] +---- +GET /index/_search +{ + "retriever": { + "rrf": { + "retrievers": [ + { + "standard" { ... } + }, + { + "knn": { ... } + } + ], + "rank_constant": ... + "window_size": ... + } + } +} +---- +// NOTCONSOLE + +==== Using `from` and `size` with a retriever tree + +The <> and <> +parameters are provided globally as part of the general +<>. They are applied to all retrievers in a +retriever tree unless a specific retriever overrides the `size` parameter +using a different parameter such as `window_size`. Though, the final +search hits are always limited to `size`. + +==== Using aggregations with a retriever tree + +<> are globally specified as part of a search request. +The query used for an aggregation is the combination of all leaf retrievers as `should` +clauses in a <>. + +==== Restrictions on search parameters when specifying a retriever + +When a retriever is specified as part of a search the following elements are not allowed +at the top-level and instead are only allowed as elements of specific retrievers: + +* <> +* <> +* <> +* <> +* <> +* <> +* <> diff --git a/docs/reference/search/rrf.asciidoc b/docs/reference/search/rrf.asciidoc index b162083ebb926..813d1f6557bed 100644 --- a/docs/reference/search/rrf.asciidoc +++ b/docs/reference/search/rrf.asciidoc @@ -32,28 +32,20 @@ return score ==== Reciprocal rank fusion API You can use RRF as part of a <> to combine and rank -documents using result sets from a combination of -<>, -<>, and/or -<>. A minimum of 2 results sets -is required for ranking from the specified sources. - -The `rrf` parameter is an optional object defined as part of a search request's -<>. The `rrf` object contains the following -parameters: - -`rank_constant`:: -(Optional, integer) This value determines how much influence documents in individual -result sets per query have over the final ranked result set. A higher value indicates -that lower ranked documents have more influence. This value must be greater than or -equal to `1`. Defaults to `60`. - -`window_size`:: -(Optional, integer) This value determines the size of the individual result sets per -query. A higher value will improve result relevance at the cost of performance. The final -ranked result set is pruned down to the search request's <>. -`window_size` must be greater than or equal to `size` and greater than or equal to `1`. -Defaults to `100`. +documents using separate sets of top documents (result sets) from a +combination of <> using an +<>. A minimum of *two* child retrievers is +required for ranking. + +An RRF retriever is an optional object defined as part of a search request's +<>. The RRF retriever object contains +the following parameters: + +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=rrf-retrievers] + +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=rrf-rank-constant] + +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=rrf-window-size] An example request using RRF: @@ -61,19 +53,27 @@ An example request using RRF: ---- GET example-index/_search { - "query": { - "term": { - "text": "shoes" - } - }, - "knn": { - "field": "vector", - "query_vector": [1.25, 2, 3.5], - "k": 50, - "num_candidates": 100 - }, - "rank": { - "rrf": { + "retriever": { + "rrf": { <3> + "retrievers": [ + { + "standard": { <2> + "query": { + "term": { + "text": "shoes" + } + } + } + }, + { + "knn": { <1> + "field": "vector", + "query_vector": [1.25, 2, 3.5], + "k": 50, + "num_candidates": 100 + } + } + ], "window_size": 50, "rank_constant": 20 } @@ -82,10 +82,17 @@ GET example-index/_search ---- // TEST[skip:example fragment] -In the above example, we first execute the kNN search to get its global top 50 results. -Then we execute the query to get its global top 50 results. Afterwards, on a coordinating -node, we combine the knn search results with the query results and rank them based on the -RRF method to get the final top 10 results. +In the above example, we execute the `knn` and `standard` retrievers +independently of each other. Then we use the `rrf` retriever to combine +the results. + +<1> First, we execute the kNN search specified by the `knn` retriever to +get its global top 50 results. +<2> Second, we execute the query specified by the `standard` retriever to get +its global top 50 results. +<3> Then, on a coordinating node, we combine the kNN search top documents with +the query top documents and rank them based on the RRF formula using parameters from +the `rrf` retriever to get the combined top documents using the default `size` of `10`. Note that if `k` from a knn search is larger than `window_size`, the results are truncated to `window_size`. If `k` is smaller than `window_size`, the results are @@ -94,13 +101,12 @@ truncated to `window_size`. If `k` is smaller than `window_size`, the results ar [[rrf-supported-features]] ==== Reciprocal rank fusion supported features -RRF does support: +The `rrf` retriever supports: -* <> * <> * <> -RRF does not currently support: +The `rrf` retriever does not currently support: * <> * <> @@ -112,42 +118,48 @@ RRF does not currently support: * <> * <> -Using unsupported features as part of a search with RRF results +Using unsupported features as part of a search with an `rrf` retriever results in an exception. -[[rrf-using-sub-searches]] -==== Reciprocal rank fusion using sub searches +[[rrf-using-multiple-standard-retrievers]] +==== Reciprocal rank fusion using multiple standard retrievers -<> provides a way to -combine and rank multiple searches using RRF. +The `rrf` retriever provides a way to combine and rank multiple +`standard` retrievers. A primary use case is combining top documents +from a traditional BM25 query and an <> +query to achieve improved relevance. -An example request using RRF with sub searches: +An example request using RRF with multiple standard retrievers: [source,console] ---- GET example-index/_search { - "sub_searches": [ - { - "query": { - "term": { - "text": "blue shoes sale" - } - } - }, - { - "query": { - "text_expansion":{ - "ml.tokens":{ - "model_id":"my_elser_model", - "model_text":"What blue shoes are on sale?" - } + "retriever": { + "rrf": { <3> + "retrievers": [ + { + "standard": { <1> + "query": { + "term": { + "text": "blue shoes sale" + } + } + } + }, + { + "standard": { <2> + "query": { + "text_expansion":{ + "ml.tokens":{ + "model_id":"my_elser_model", + "model_text":"What blue shoes are on sale?" + } + } + } + } } - } - } - ], - "rank": { - "rrf": { + ], "window_size": 50, "rank_constant": 20 } @@ -156,17 +168,31 @@ GET example-index/_search ---- // TEST[skip:example fragment] -In the above example, we execute each of the two sub searches -independently of each other. First we run the term query for -`blue shoes sales` using the standard BM25 scoring algorithm. Then -we run the text expansion query for `What blue shoes are on sale?` +In the above example, we execute each of the two `standard` retrievers +independently of each other. Then we use the `rrf` retriever to combine +the results. + +<1> First we run the `standard` retriever +specifying a term query for `blue shoes sales` using the standard BM25 +scoring algorithm. +<2> Next we run the `standard` retriever specifying a +text expansion query for `What blue shoes are on sale?` using our <> scoring algorithm. -RRF allows us to combine the two results sets generated by completely -independent scoring algorithms with equal weighting. Not only does this -remove the need to figure out what the appropriate weighting would be -using linear combination, but RRF is also shown to give improved +<3> The `rrf` retriever allows us to combine the two top documents sets +generated by completely independent scoring algorithms with equal weighting. + +Not only does this remove the need to figure out what the appropriate +weighting is using linear combination, but RRF is also shown to give improved relevance over either query individually. +[[rrf-using-sub-searches]] +==== Reciprocal rank fusion using sub searches + +RRF using sub searches is no longer supported. Use the +<> instead. See +<> +for an example. + [[rrf-full-example]] ==== Reciprocal rank fusion full example @@ -179,7 +205,7 @@ to explain. ---- PUT example-index { - "mappings": { + "mappings": { "properties": { "text" : { "type" : "text" @@ -234,26 +260,35 @@ POST example-index/_refresh ---- // TEST -We now execute a search using RRF with a query, a kNN search, and +We now execute a search using an `rrf` retriever with a `standard` retriever +specifying a BM25 query, a `knn` retriever specifying a kNN search, and a terms aggregation. [source,console] ---- GET example-index/_search { - "query": { - "term": { - "text": "rrf" - } - }, - "knn": { - "field": "vector", - "query_vector": [3], - "k": 5, - "num_candidates": 5 - }, - "rank": { + "retriever": { "rrf": { + "retrievers": [ + { + "standard": { + "query": { + "term": { + "text": "rrf" + } + } + } + }, + { + "knn": { + "field": "vector", + "query_vector": [3], + "k": 5, + "num_candidates": 5 + } + } + ], "window_size": 5, "rank_constant": 1 } @@ -351,10 +386,11 @@ use `_rank` to show our top-ranked documents. // TESTRESPONSE[s/: \.\.\./: $body.$_path/] Let's break down how these hits were ranked. We -start by running the query and the kNN search -separately to collect what their individual hits are. +start by running the `standard` retriever specifying a query +and the `knn` retriever specifying a kNN search separately to +collect what their individual hits are. -First, we look at the hits for the query. +First, we look at the hits for the query from the `standard` retriever. [source,console-result] ---- @@ -407,7 +443,7 @@ First, we look at the hits for the query. <4> rank 4, `_id` 1 Note that our first hit doesn't have a value for the `vector` field. Now, -we look at the results for the kNN search. +we look at the results for the kNN search from the `knn` retriever. [source,console-result] ---- @@ -460,7 +496,8 @@ we look at the results for the kNN search. <4> rank 4, `_id` 5 We can now take the two individually ranked result sets and apply the -RRF formula to them to get our final ranking. +RRF formula to them using parameters from the `rrf` retriever to get +our final ranking. [source,python] ---- @@ -478,4 +515,3 @@ truncating the bottom `2` docs in our RRF result set with a `size` of `3`. We end with `_id: 3` as `_rank: 1`, `_id: 2` as `_rank: 2`, and `_id: 4` as `_rank: 3`. This ranking matches the result set from the original RRF search as expected. - diff --git a/docs/reference/search/search-your-data/search-application-api.asciidoc b/docs/reference/search/search-your-data/search-application-api.asciidoc index 29624a5bcd83a..0b7510d20658d 100644 --- a/docs/reference/search/search-your-data/search-application-api.asciidoc +++ b/docs/reference/search/search-your-data/search-application-api.asciidoc @@ -215,31 +215,35 @@ PUT _application/search_application/my-search-app "lang": "mustache", "source": """ { - "sub_searches": [ - {{#text_fields}} - { - "query": { - "match": { - "{{.}}": "{{query_string}}" - } - } - }, - {{/text_fields}} - {{#elser_fields}} - { - "query": { - "text_expansion": { - "ml.inference.{{.}}_expanded.predicted_value": { - "model_text": "{{query_string}}", - "model_id": "" + "retriever": { + "rrf": { + "retrievers": [ + {{#text_fields}} + { + "standard": { + "query": { + "match": { + "{{.}}": "{{query_string}}" + } + } + } + }, + {{/text_fields}} + {{#elser_fields}} + { + "standard": { + "query": { + "text_expansion": { + "ml.inference.{{.}}_expanded.predicted_value": { + "model_text": "{{query_string}}", + "model_id": "" + } + } + } } } - } - }, - {{/elser_fields}} - ], - "rank": { - "rrf": { + {{/elser_fields}} + ], "window_size": {{rrf.window_size}}, "rank_constant": {{rrf.rank_constant}} } diff --git a/docs/reference/search/search.asciidoc b/docs/reference/search/search.asciidoc index cd7d496d89d7b..5dae64916dd6b 100644 --- a/docs/reference/search/search.asciidoc +++ b/docs/reference/search/search.asciidoc @@ -558,12 +558,20 @@ Period of time used to extend the life of the PIT. (Optional, <>) Defines the search definition using the <>. +[[request-body-retriever]] +`retriever`:: +preview:[] +(Optional, <>) Defines a top-level retriever to specify +a desired set of top documents instead of a standard query or knn search. + [[request-body-rank]] `rank`:: preview:[] This param is in technical preview and may change in the future. The syntax will likely change before GA. + +This parameter is deprecated and will be removed. Use <> instead. ++ (Optional, object) Defines a method for combining and ranking result sets from a combination of <>, @@ -731,6 +739,8 @@ preview:[] This param is in technical preview and may change in the future. The syntax will likely change before GA. + +This parameter is deprecated and will be removed. Use <> instead. ++ (Optional, array of objects) An array of `sub_search` objects where each `sub_search` is evaluated independently, and their result sets are later combined as part of @@ -752,6 +762,7 @@ with a top-level <> element. ---- // NOTCONSOLE +[[request-body-search-terminate-after]] include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=terminate_after] + Defaults to `0`, which does not terminate query execution early. diff --git a/docs/reference/tab-widgets/semantic-search/hybrid-search.asciidoc b/docs/reference/tab-widgets/semantic-search/hybrid-search.asciidoc index f7d9ee1ad6443..47403df450bd2 100644 --- a/docs/reference/tab-widgets/semantic-search/hybrid-search.asciidoc +++ b/docs/reference/tab-widgets/semantic-search/hybrid-search.asciidoc @@ -1,36 +1,41 @@ // tag::elser[] -Hybrid search between a semantic and lexical query can be achieved by using a -`sub_searches` clause in your search request. In the `sub_searches` clause, -provide a `text_expansion` query and a full-text query. Next to the -`sub_searches` clause, also provide a <> clause with -the `rrf` parameter to rank documents using reciprocal rank fusion. +Hybrid search between a semantic and lexical query can be achieved by using an +< as part of your search request. Provide a +`text_expansion` query and a full-text query as +<> for the `rrf` retriever. The `rrf` +retriever uses <> to rank the top documents. [source,console] ---- GET my-index/_search { - "sub_searches": [ - { - "query": { - "match": { - "my_text_field": "the query string" - } - } - }, - { - "query": { - "text_expansion": { - "my_tokens": { - "model_id": ".elser_model_2", - "model_text": "the query string" + "retriever": { + "rrf": { + "retrievers": [ + { + "standard": { + "query": { + "match": { + "my_text_field": "the query string" + } + } + } + }, + { + "standard": { + "query": { + "text_expansion": { + "my_tokens": { + "model_id": ".elser_model_2", + "model_text": "the query string" + } + } + } } } - } + ] } - ], - "rank": { - "rrf": {} } } ---- @@ -43,36 +48,44 @@ GET my-index/_search Hybrid search between a semantic and lexical query can be achieved by providing: -* a `query` clause for the full-text query; -* a `knn` clause with the kNN search that queries the dense vector field; -* and a `rank` clause with the `rrf` parameter to rank documents using -reciprocal rank fusion. +* an `rrf` retriever to rank top documents using <> +* a `standard` retriever as a child retriever with `query` clause for the full-text query +* a `knn` retriever as a child retriever with the kNN search that queries the dense vector field [source,console] ---- GET my-index/_search { - "query": { - "match": { - "my_text_field": "the query string" - } - }, - "knn": { - "field": "text_embedding.predicted_value", - "k": 10, - "num_candidates": 100, - "query_vector_builder": { - "text_embedding": { - "model_id": "sentence-transformers__msmarco-minilm-l-12-v3", - "model_text": "the query string" - } + "retriever": { + "rrf": { + "retrievers": [ + { + "standard": { + "query": { + "match": { + "my_text_field": "the query string" + } + } + } + }, + { + "knn": { + "field": "text_embedding.predicted_value", + "k": 10, + "num_candidates": 100, + "query_vector_builder": { + "text_embedding": { + "model_id": "sentence-transformers__msmarco-minilm-l-12-v3", + "model_text": "the query string" + } + } + } + } + ] } - }, - "rank": { - "rrf": {} } } ---- // TEST[skip:TBD] -// end::dense-vector[] \ No newline at end of file +// end::dense-vector[] diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/10_standard_retriever.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/10_standard_retriever.yml new file mode 100644 index 0000000000000..23682a19ea6f7 --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/10_standard_retriever.yml @@ -0,0 +1,519 @@ +setup: + - skip: + version: ' - 8.13.99' + reason: 'standard retriever added in 8.14' + - do: + indices.create: + index: animals + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + properties: + type: + type: keyword + name: + type: text + fields: + raw: + type: keyword + color: + type: keyword + count: + type: integer + + - do: + bulk: + refresh: true + index: animals + body: + - '{"index": {"_id": 1 }}' + - '{"type": "domestic", "name": "cow", "color": "brown", "count": 1}' + - '{"index": {"_id": 2 }}' + - '{"type": "domestic", "name": "cow cow", "color": "spotted", "count": 2}' + - '{"index": {"_id": 3 }}' + - '{"type": "domestic", "name": "cow cow cow", "color": "spotted", "count": 3}' + - '{"index": {"_id": 4 }}' + - '{"type": "domestic", "name": "pig", "color": "pink", "count": 4}' + - '{"index": {"_id": 5 }}' + - '{"type": "domestic", "name": "pig pig", "color": "pink", "count": 5}' + - '{"index": {"_id": 6 }}' + - '{"type": "domestic", "name": "pig pig pig", "color": "spotted", "count": 6}' + - '{"index": {"_id": 7 }}' + - '{"type": "domestic", "name": "chicken", "color": "white", "count": 7}' + - '{"index": {"_id": 8 }}' + - '{"type": "domestic", "name": "chicken chicken", "color": "brown", "count": 8}' + - '{"index": {"_id": 9 }}' + - '{"type": "domestic", "name": "chicken chicken chicken", "color": "spotted", "count": 9}' + - '{"index": {"_id": 10 }}' + - '{"type": "wild", "name": "coyote", "color": "gray", "count": 10}' + - '{"index": {"_id": 11 }}' + - '{"type": "wild", "name": "coyote coyote", "color": "gray", "count": 11}' + - '{"index": {"_id": 12 }}' + - '{"type": "wild", "name": "coyote coyote coyote", "color": "white", "count": 12}' + - '{"index": {"_id": 13 }}' + - '{"type": "wild", "name": "rabbit", "color": "brown", "count": 13}' + - '{"index": {"_id": 14 }}' + - '{"type": "wild", "name": "rabbit rabbit", "color": "spotted", "count": 14}' + - '{"index": {"_id": 15 }}' + - '{"type": "wild", "name": "rabbit rabbit rabbit", "color": "white", "count": 15}' + +--- +"standard retriever basic": + - do: + search: + index: animals + body: + fields: [ "name", "count" ] + retriever: + standard: + query: + match: + name: "cow" + + - match: {hits.total.value: 3} + + - match: {hits.hits.0._id: "3"} + - match: {hits.hits.0.fields.name.0: "cow cow cow"} + - match: {hits.hits.0.fields.count.0: 3} + + - match: {hits.hits.1._id: "2"} + - match: {hits.hits.1.fields.name.0: "cow cow"} + - match: {hits.hits.1.fields.count.0: 2} + + - match: {hits.hits.2._id: "1"} + - match: {hits.hits.2.fields.name.0: "cow"} + - match: {hits.hits.2.fields.count.0: 1} + +--- +"standard retriever single sort": + - do: + search: + index: animals + body: + fields: [ "name", "count" ] + retriever: + standard: + query: + term: + color: "spotted" + sort: [ + { + name.raw: "asc" + } + ] + + - match: {hits.total.value: 5} + + - match: {hits.hits.0._id: "9"} + - match: {hits.hits.0.fields.name.0: "chicken chicken chicken"} + - match: {hits.hits.0.fields.count.0: 9} + + - match: {hits.hits.1._id: "2"} + - match: {hits.hits.1.fields.name.0: "cow cow"} + - match: {hits.hits.1.fields.count.0: 2} + + - match: {hits.hits.2._id: "3"} + - match: {hits.hits.2.fields.name.0: "cow cow cow"} + - match: {hits.hits.2.fields.count.0: 3} + + - match: {hits.hits.3._id: "6"} + - match: {hits.hits.3.fields.name.0: "pig pig pig"} + - match: {hits.hits.3.fields.count.0: 6} + + - match: {hits.hits.4._id: "14"} + - match: {hits.hits.4.fields.name.0: "rabbit rabbit"} + - match: {hits.hits.4.fields.count.0: 14} + +--- +"standard retriever multi sort": + - do: + search: + index: animals + body: + fields: [ "name", "count" ] + retriever: + standard: + query: + bool: + should: [ + { + term: { + color: "spotted" + } + }, + { + term: { + color: "pink" + } + } + ] + sort: [ + { + color: "asc" + }, + { + count: "desc" + } + ] + + - match: {hits.total.value: 7} + + - match: {hits.hits.0._id: "5"} + - match: {hits.hits.0.fields.name.0: "pig pig"} + - match: {hits.hits.0.fields.count.0: 5} + + - match: {hits.hits.1._id: "4"} + - match: {hits.hits.1.fields.name.0: "pig"} + - match: {hits.hits.1.fields.count.0: 4} + + - match: {hits.hits.2._id: "14"} + - match: {hits.hits.2.fields.name.0: "rabbit rabbit"} + - match: {hits.hits.2.fields.count.0: 14} + + - match: {hits.hits.3._id: "9"} + - match: {hits.hits.3.fields.name.0: "chicken chicken chicken"} + - match: {hits.hits.3.fields.count.0: 9} + + - match: {hits.hits.4._id: "6"} + - match: {hits.hits.4.fields.name.0: "pig pig pig"} + - match: {hits.hits.4.fields.count.0: 6} + + - match: {hits.hits.5._id: "3"} + - match: {hits.hits.5.fields.name.0: "cow cow cow"} + - match: {hits.hits.5.fields.count.0: 3} + + - match: {hits.hits.6._id: "2"} + - match: {hits.hits.6.fields.name.0: "cow cow"} + - match: {hits.hits.6.fields.count.0: 2} + +--- +"standard retriever filter": + - do: + search: + index: animals + body: + fields: [ "name", "count" ] + retriever: + standard: + filter: + bool: + must_not: + term: + color: "spotted" + query: + match: + name: "cow" + + - match: {hits.total.value: 1} + + - match: {hits.hits.0._id: "1"} + - match: {hits.hits.0.fields.name.0: "cow"} + - match: {hits.hits.0.fields.count.0: 1} + +--- +"standard retriever multi filter": + - do: + search: + index: animals + body: + fields: [ "name", "count" ] + retriever: + standard: + filter: [ + { + match: { + name: "cow" + } + }, + { + range: { + count: { + gt: 1, + lt: 3 + } + } + } + ] + query: + term: + color: "spotted" + + - match: {hits.total.value: 1} + + - match: {hits.hits.0._id: "2"} + - match: {hits.hits.0.fields.name.0: "cow cow"} + - match: {hits.hits.0.fields.count.0: 2} + +--- +"standard retriever filter no query": + - do: + search: + index: animals + body: + fields: [ "name", "count" ] + retriever: + standard: + filter: [ + { + match: { + name: "cow" + } + }, + { + range: { + count: { + gt: 1, + lt: 4 + } + } + } + ] + sort: [ + { + count: "desc" + } + ] + + - match: {hits.total.value: 2} + + - match: { hits.hits.0._id: "3" } + - match: { hits.hits.0.fields.name.0: "cow cow cow" } + - match: { hits.hits.0.fields.count.0: 3 } + + - match: {hits.hits.1._id: "2"} + - match: {hits.hits.1.fields.name.0: "cow cow"} + - match: {hits.hits.1.fields.count.0: 2} + +--- +"standard retriever search after": + - do: + search: + index: animals + body: + size: 3 + fields: [ "name", "count" ] + retriever: + standard: + query: + bool: + should: [ + { + term: { + color: "spotted" + } + }, + { + term: { + color: "pink" + } + } + ] + sort: [ + { + count: "desc" + } + ] + + - match: {hits.total.value: 7} + + - match: {hits.hits.0._id: "14"} + - match: {hits.hits.0.fields.name.0: "rabbit rabbit"} + - match: {hits.hits.0.fields.count.0: 14} + + - match: {hits.hits.1._id: "9"} + - match: {hits.hits.1.fields.name.0: "chicken chicken chicken"} + - match: {hits.hits.1.fields.count.0: 9} + + - match: {hits.hits.2._id: "6"} + - match: {hits.hits.2.fields.name.0: "pig pig pig"} + - match: {hits.hits.2.fields.count.0: 6} + + - do: + search: + index: animals + body: + size: 3 + fields: [ "name", "count" ] + retriever: + standard: + search_after: [ 6 ] + query: + bool: + should: [ + { + term: { + color: "spotted" + } + }, + { + term: { + color: "pink" + } + } + ] + sort: [ + { + count: "desc" + } + ] + + - match: {hits.total.value: 7} + + - match: {hits.hits.0._id: "5"} + - match: {hits.hits.0.fields.name.0: "pig pig"} + - match: {hits.hits.0.fields.count.0: 5} + + - match: {hits.hits.1._id: "4"} + - match: {hits.hits.1.fields.name.0: "pig"} + - match: {hits.hits.1.fields.count.0: 4} + + - match: {hits.hits.2._id: "3"} + - match: {hits.hits.2.fields.name.0: "cow cow cow"} + - match: {hits.hits.2.fields.count.0: 3} + + - do: + search: + index: animals + body: + size: 3 + fields: [ "name", "count" ] + retriever: + standard: + search_after: [ 3 ] + query: + bool: + should: [ + { + term: { + color: "spotted" + } + }, + { + term: { + color: "pink" + } + } + ] + sort: [ + { + count: "desc" + } + ] + + - match: {hits.total.value: 7} + + - match: {hits.hits.0._id: "2"} + - match: {hits.hits.0.fields.name.0: "cow cow"} + - match: {hits.hits.0.fields.count.0: 2} + +--- +"standard retriever terminate after": + - do: + search: + index: animals + body: + fields: [ "name", "count" ] + retriever: + standard: + filter: + bool: + must_not: + match: + name: "cow" + sort: [ + { + count: "asc" + } + ] + terminate_after: 3 + + - match: {hits.total.value: 3} + + - match: {hits.hits.0._id: "4"} + - match: {hits.hits.0.fields.name.0: "pig"} + - match: {hits.hits.0.fields.count.0: 4} + + - match: {hits.hits.1._id: "5"} + - match: {hits.hits.1.fields.name.0: "pig pig"} + - match: {hits.hits.1.fields.count.0: 5} + + - match: {hits.hits.2._id: "6"} + - match: {hits.hits.2.fields.name.0: "pig pig pig"} + - match: {hits.hits.2.fields.count.0: 6} + +--- +"standard retriever min score": + - do: + search: + index: animals + body: + fields: [ "name", "count" ] + retriever: + standard: + query: + script_score: + query: + match: + name: "cow" + script: + source: " $('count', -1)" + min_score: 1.5 + + - match: {hits.total.value: 2} + + - match: {hits.hits.0._id: "3"} + - match: {hits.hits.0.fields.name.0: "cow cow cow"} + - match: {hits.hits.0.fields.count.0: 3} + + - match: {hits.hits.1._id: "2"} + - match: {hits.hits.1.fields.name.0: "cow cow"} + - match: {hits.hits.1.fields.count.0: 2} + +--- +"standard retriever collapse": + - do: + search: + index: animals + body: + size: 15 + fields: [ "name", "count", "color" ] + retriever: + standard: + query: + match_all: {} + collapse: + field: "color" + sort: [ + { + count: "asc" + } + ] + + - match: {hits.total.value: 15} + + - match: {hits.hits.0._id: "1"} + - match: {hits.hits.0.fields.name.0: "cow"} + - match: {hits.hits.0.fields.count.0: 1} + - match: {hits.hits.0.fields.color.0: "brown"} + + - match: {hits.hits.1._id: "2"} + - match: {hits.hits.1.fields.name.0: "cow cow"} + - match: {hits.hits.1.fields.count.0: 2} + - match: {hits.hits.1.fields.color.0: "spotted"} + + - match: {hits.hits.2._id: "4"} + - match: {hits.hits.2.fields.name.0: "pig"} + - match: {hits.hits.2.fields.count.0: 4} + - match: {hits.hits.2.fields.color.0: "pink"} + + - match: {hits.hits.3._id: "7" } + - match: {hits.hits.3.fields.name.0: "chicken" } + - match: {hits.hits.3.fields.count.0: 7 } + - match: {hits.hits.3.fields.color.0: "white"} + + - match: {hits.hits.4._id: "10"} + - match: {hits.hits.4.fields.name.0: "coyote"} + - match: {hits.hits.4.fields.count.0: 10} + - match: {hits.hits.4.fields.color.0: "gray"} diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/20_knn_retriever.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/20_knn_retriever.yml new file mode 100644 index 0000000000000..66f88315032c3 --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.retrievers/20_knn_retriever.yml @@ -0,0 +1,73 @@ +setup: + - skip: + version: ' - 8.13.99' + reason: 'kNN retriever added in 8.14' + - do: + indices.create: + index: index1 + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + properties: + name: + type: keyword + vector: + type: dense_vector + dims: 5 + index: true + similarity: l2_norm + + - do: + bulk: + refresh: true + index: index1 + body: + - '{"index": {"_id": 1 }}' + - '{"name": "cow.jpg", "vector": [1, 1, 1, 1, 1]}' + - '{"index": {"_id": 2}}' + - '{"name": "moose.jpg", "vector": [2, 2, 2, 2, 2]}' + - '{"index": {"_id": 3 }}' + - '{"name": "rabbit.jpg", "vector": [3, 3, 3, 3, 3]}' + +--- +"kNN retriever": + - do: + search: + index: index1 + body: + fields: [ "name" ] + retriever: + knn: + field: vector + query_vector: [2, 2, 2, 2, 3] + k: 2 + num_candidates: 3 + + - match: {hits.hits.0._id: "2"} + - match: {hits.hits.0.fields.name.0: "moose.jpg"} + + - match: {hits.hits.1._id: "3"} + - match: {hits.hits.1.fields.name.0: "rabbit.jpg"} + +--- +"kNN retriever with filter": + - do: + search: + index: index1 + body: + fields: [ "name" ] + retriever: + knn: + field: vector + query_vector: [2, 2, 2, 2, 3] + k: 2 + num_candidates: 3 + filter: + term: + name: "rabbit.jpg" + + - match: {hits.total.value: 1} + - match: {hits.hits.0._id: "3"} + - match: {hits.hits.0.fields.name.0: "rabbit.jpg"} diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index 9c142d18034c0..83b8606da2997 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -358,6 +358,7 @@ exports org.elasticsearch.search.query; exports org.elasticsearch.search.rank; exports org.elasticsearch.search.rescore; + exports org.elasticsearch.search.retriever; exports org.elasticsearch.search.runtime; exports org.elasticsearch.search.searchafter; exports org.elasticsearch.search.slice; @@ -415,7 +416,8 @@ org.elasticsearch.cluster.service.TransportFeatures, org.elasticsearch.cluster.metadata.MetadataFeatures, org.elasticsearch.rest.RestFeatures, - org.elasticsearch.indices.IndicesFeatures; + org.elasticsearch.indices.IndicesFeatures, + org.elasticsearch.search.retriever.RetrieversFeatures; uses org.elasticsearch.plugins.internal.SettingsExtension; uses RestExtension; diff --git a/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java b/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java index e985e279770e9..73927037b1f81 100644 --- a/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java @@ -37,6 +37,8 @@ import org.elasticsearch.search.internal.ShardSearchRequest; import org.elasticsearch.search.rescore.Rescorer; import org.elasticsearch.search.rescore.RescorerBuilder; +import org.elasticsearch.search.retriever.RetrieverBuilder; +import org.elasticsearch.search.retriever.RetrieverParser; import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.search.suggest.Suggester; import org.elasticsearch.search.suggest.SuggestionBuilder; @@ -111,6 +113,13 @@ default List> getSuggesters() { return emptyList(); } + /** + * The new {@link RetrieverBuilder}s defined by this plugin. + */ + default List> getRetrievers() { + return emptyList(); + } + /** * The new {@link Query}s defined by this plugin. */ @@ -256,6 +265,47 @@ public Writeable.Reader getSuggestionReader() { } } + /** + * Specification of custom {@link RetrieverBuilder}. + */ + class RetrieverSpec { + + private final ParseField name; + private final RetrieverParser parser; + + /** + * Specification of custom {@link RetrieverBuilder}. + * + * @param name holds the names by which this retriever might be parsed. The {@link ParseField#getPreferredName()} is special as it + * is the name by under which the reader is registered. So it is the name that the retriever should use as its + * {@link NamedWriteable#getWriteableName()} too. + * @param parser the parser the reads the retriever builder from xcontent + */ + public RetrieverSpec(ParseField name, RetrieverParser parser) { + this.name = name; + this.parser = parser; + } + + /** + * Specification of custom {@link RetrieverBuilder}. + * + * @param name the name by which this retriever might be parsed or deserialized. Make sure that the retriever builder returns + * this name for {@link NamedWriteable#getWriteableName()}. + * @param parser the parser the reads the retriever builder from xcontent + */ + public RetrieverSpec(String name, RetrieverParser parser) { + this(new ParseField(name), parser); + } + + public ParseField getName() { + return name; + } + + public RetrieverParser getParser() { + return parser; + } + } + /** * Specification of custom {@link Query}. */ diff --git a/server/src/main/java/org/elasticsearch/search/SearchModule.java b/server/src/main/java/org/elasticsearch/search/SearchModule.java index 5b17203ded132..97b747c650c1b 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/server/src/main/java/org/elasticsearch/search/SearchModule.java @@ -86,6 +86,7 @@ import org.elasticsearch.plugins.SearchPlugin.QuerySpec; import org.elasticsearch.plugins.SearchPlugin.QueryVectorBuilderSpec; import org.elasticsearch.plugins.SearchPlugin.RescorerSpec; +import org.elasticsearch.plugins.SearchPlugin.RetrieverSpec; import org.elasticsearch.plugins.SearchPlugin.ScoreFunctionSpec; import org.elasticsearch.plugins.SearchPlugin.SearchExtSpec; import org.elasticsearch.plugins.SearchPlugin.SignificanceHeuristicSpec; @@ -227,6 +228,10 @@ import org.elasticsearch.search.internal.ShardSearchRequest; import org.elasticsearch.search.rescore.QueryRescorerBuilder; import org.elasticsearch.search.rescore.RescorerBuilder; +import org.elasticsearch.search.retriever.KnnRetrieverBuilder; +import org.elasticsearch.search.retriever.RetrieverBuilder; +import org.elasticsearch.search.retriever.RetrieverParserContext; +import org.elasticsearch.search.retriever.StandardRetrieverBuilder; import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.GeoDistanceSortBuilder; import org.elasticsearch.search.sort.ScoreSortBuilder; @@ -305,6 +310,7 @@ public SearchModule(Settings settings, List plugins) { registerSuggesters(plugins); highlighters = setupHighlighters(settings, plugins); registerScoreFunctions(plugins); + registerRetrieverParsers(plugins); registerQueryParsers(plugins); registerRescorers(plugins); registerSorts(); @@ -1039,6 +1045,13 @@ private void registerFetchSubPhase(FetchSubPhase subPhase) { fetchSubPhases.add(requireNonNull(subPhase, "FetchSubPhase must not be null")); } + private void registerRetrieverParsers(List plugins) { + registerRetriever(new RetrieverSpec<>(StandardRetrieverBuilder.NAME, StandardRetrieverBuilder::fromXContent)); + registerRetriever(new RetrieverSpec<>(KnnRetrieverBuilder.NAME, KnnRetrieverBuilder::fromXContent)); + + registerFromPlugin(plugins, SearchPlugin::getRetrievers, this::registerRetriever); + } + private void registerQueryParsers(List plugins) { registerQuery(new QuerySpec<>(MatchQueryBuilder.NAME, MatchQueryBuilder::new, MatchQueryBuilder::fromXContent)); registerQuery(new QuerySpec<>(MatchPhraseQueryBuilder.NAME, MatchPhraseQueryBuilder::new, MatchPhraseQueryBuilder::fromXContent)); @@ -1198,6 +1211,17 @@ public static List getIntervalsSourceProviderNamed ); } + private void registerRetriever(RetrieverSpec spec) { + namedXContents.add( + new NamedXContentRegistry.Entry( + RetrieverBuilder.class, + spec.getName(), + (p, c) -> spec.getParser().fromXContent(p, (RetrieverParserContext) c), + spec.getName().getForRestApiVersion() + ) + ); + } + private void registerQuery(QuerySpec spec) { namedWriteables.add(new NamedWriteableRegistry.Entry(QueryBuilder.class, spec.getName().getPreferredName(), spec.getReader())); namedXContents.add( 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 72fd84cda760b..741a0e680b522 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -39,6 +39,8 @@ import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.rank.RankBuilder; import org.elasticsearch.search.rescore.RescorerBuilder; +import org.elasticsearch.search.retriever.RetrieverBuilder; +import org.elasticsearch.search.retriever.RetrieverParserContext; import org.elasticsearch.search.searchafter.SearchAfterBuilder; import org.elasticsearch.search.slice.SliceBuilder; import org.elasticsearch.search.sort.ScoreSortBuilder; @@ -70,6 +72,7 @@ import static java.util.Collections.emptyMap; import static org.elasticsearch.index.query.AbstractQueryBuilder.parseTopLevelQuery; +import static org.elasticsearch.search.internal.SearchContext.DEFAULT_TERMINATE_AFTER; import static org.elasticsearch.search.internal.SearchContext.TRACK_TOTAL_HITS_ACCURATE; import static org.elasticsearch.search.internal.SearchContext.TRACK_TOTAL_HITS_DISABLED; @@ -120,6 +123,7 @@ public final class SearchSourceBuilder implements Writeable, ToXContentObject, R public static final ParseField SLICE = new ParseField("slice"); public static final ParseField POINT_IN_TIME = new ParseField("pit"); public static final ParseField RUNTIME_MAPPINGS_FIELD = new ParseField("runtime_mappings"); + public static final ParseField RETRIEVER = new ParseField("retriever"); private static final boolean RANK_SUPPORTED = Booleans.parseBoolean(System.getProperty("es.search.rank_supported"), true); @@ -1285,6 +1289,7 @@ private SearchSourceBuilder parseXContent( } List knnBuilders = new ArrayList<>(); + RetrieverBuilder retrieverBuilder = null; SearchUsage searchUsage = new SearchUsage(); while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { @@ -1353,7 +1358,15 @@ private SearchSourceBuilder parseXContent( ); } } else if (token == XContentParser.Token.START_OBJECT) { - if (QUERY_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + if (RETRIEVER.match(currentFieldName, parser.getDeprecationHandler())) { + if (clusterSupportsFeature.test(RetrieverBuilder.RETRIEVERS_SUPPORTED) == false) { + throw new ParsingException(parser.getTokenLocation(), "Unknown key for a START_OBJECT in [retriever]."); + } + retrieverBuilder = RetrieverBuilder.parseTopLevelRetrieverBuilder( + parser, + new RetrieverParserContext(searchUsage, clusterSupportsFeature) + ); + } else if (QUERY_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { if (subSearchSourceBuilders.isEmpty() == false) { throw new IllegalArgumentException( "cannot specify field [" + currentFieldName + "] and field [" + SUB_SEARCHES_FIELD.getPreferredName() + "]" @@ -1611,6 +1624,38 @@ private SearchSourceBuilder parseXContent( knnSearch = knnBuilders.stream().map(knnBuilder -> knnBuilder.build(size())).collect(Collectors.toList()); + if (retrieverBuilder != null) { + List specified = new ArrayList<>(); + if (subSearchSourceBuilders.isEmpty() == false) { + specified.add(QUERY_FIELD.getPreferredName()); + } + if (knnSearch.isEmpty() == false) { + specified.add(KNN_FIELD.getPreferredName()); + } + if (searchAfterBuilder != null) { + specified.add(SEARCH_AFTER.getPreferredName()); + } + if (terminateAfter != DEFAULT_TERMINATE_AFTER) { + specified.add(TERMINATE_AFTER_FIELD.getPreferredName()); + } + if (sorts != null) { + specified.add(SORT_FIELD.getPreferredName()); + } + if (rescoreBuilders != null) { + specified.add(RESCORE_FIELD.getPreferredName()); + } + if (minScore != null) { + specified.add(MIN_SCORE_FIELD.getPreferredName()); + } + if (rankBuilder != null) { + specified.add(RANK_FIELD.getPreferredName()); + } + if (specified.isEmpty() == false) { + throw new IllegalArgumentException("cannot specify [" + RETRIEVER.getPreferredName() + "] and " + specified); + } + retrieverBuilder.extractToSearchSourceBuilder(this, false); + } + searchUsageConsumer.accept(searchUsage); return this; } diff --git a/server/src/main/java/org/elasticsearch/search/collapse/CollapseBuilder.java b/server/src/main/java/org/elasticsearch/search/collapse/CollapseBuilder.java index c77a5c3c09f81..05dc86f4d80d8 100644 --- a/server/src/main/java/org/elasticsearch/search/collapse/CollapseBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/collapse/CollapseBuilder.java @@ -195,6 +195,11 @@ public int hashCode() { return result; } + @Override + public String toString() { + return Strings.toString(this, true, true); + } + public CollapseContext build(SearchExecutionContext searchExecutionContext) { MappedFieldType fieldType = searchExecutionContext.getFieldType(field); if (fieldType == null) { diff --git a/server/src/main/java/org/elasticsearch/search/retriever/KnnRetrieverBuilder.java b/server/src/main/java/org/elasticsearch/search/retriever/KnnRetrieverBuilder.java new file mode 100644 index 0000000000000..fc2d4218ea1ec --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/retriever/KnnRetrieverBuilder.java @@ -0,0 +1,171 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.vectors.KnnSearchBuilder; +import org.elasticsearch.search.vectors.QueryVectorBuilder; +import org.elasticsearch.xcontent.ConstructingObjectParser; +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.Arrays; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * A knn retriever is used to represent a knn search + * with some elements to specify parameters for that knn search. + */ +public final class KnnRetrieverBuilder extends RetrieverBuilder { + + public static final String NAME = "knn"; + public static final NodeFeature KNN_RETRIEVER_SUPPORTED = new NodeFeature("knn_retriever_supported"); + + public static final ParseField FIELD_FIELD = new ParseField("field"); + public static final ParseField K_FIELD = new ParseField("k"); + public static final ParseField NUM_CANDS_FIELD = new ParseField("num_candidates"); + public static final ParseField QUERY_VECTOR_FIELD = new ParseField("query_vector"); + public static final ParseField QUERY_VECTOR_BUILDER_FIELD = new ParseField("query_vector_builder"); + public static final ParseField VECTOR_SIMILARITY = new ParseField("similarity"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "knn", + args -> { + List vector = (List) args[1]; + final float[] vectorArray; + if (vector != null) { + vectorArray = new float[vector.size()]; + for (int i = 0; i < vector.size(); i++) { + vectorArray[i] = vector.get(i); + } + } else { + vectorArray = null; + } + return new KnnRetrieverBuilder( + (String) args[0], + vectorArray, + (QueryVectorBuilder) args[2], + (int) args[3], + (int) args[4], + (Float) args[5] + ); + } + ); + + static { + PARSER.declareString(constructorArg(), FIELD_FIELD); + PARSER.declareFloatArray(optionalConstructorArg(), QUERY_VECTOR_FIELD); + PARSER.declareNamedObject( + optionalConstructorArg(), + (p, c, n) -> p.namedObject(QueryVectorBuilder.class, n, c), + QUERY_VECTOR_BUILDER_FIELD + ); + PARSER.declareInt(constructorArg(), K_FIELD); + PARSER.declareInt(constructorArg(), NUM_CANDS_FIELD); + PARSER.declareFloat(optionalConstructorArg(), VECTOR_SIMILARITY); + RetrieverBuilder.declareBaseParserFields(NAME, PARSER); + } + + public static KnnRetrieverBuilder fromXContent(XContentParser parser, RetrieverParserContext context) throws IOException { + if (context.clusterSupportsFeature(KNN_RETRIEVER_SUPPORTED) == false) { + throw new ParsingException(parser.getTokenLocation(), "unknown retriever [" + NAME + "]"); + } + return PARSER.apply(parser, context); + } + + private final String field; + private final float[] queryVector; + private final QueryVectorBuilder queryVectorBuilder; + private final int k; + private final int numCands; + private final Float similarity; + + public KnnRetrieverBuilder( + String field, + float[] queryVector, + QueryVectorBuilder queryVectorBuilder, + int k, + int numCands, + Float similarity + ) { + this.field = field; + this.queryVector = queryVector; + this.queryVectorBuilder = queryVectorBuilder; + this.k = k; + this.numCands = numCands; + this.similarity = similarity; + } + + // ---- FOR TESTING XCONTENT PARSING ---- + + @Override + public String getName() { + return NAME; + } + + @Override + public void extractToSearchSourceBuilder(SearchSourceBuilder searchSourceBuilder, boolean compoundUsed) { + KnnSearchBuilder knnSearchBuilder = new KnnSearchBuilder(field, queryVector, queryVectorBuilder, k, numCands, similarity); + if (preFilterQueryBuilders != null) { + knnSearchBuilder.addFilterQueries(preFilterQueryBuilders); + } + List knnSearchBuilders = new ArrayList<>(searchSourceBuilder.knnSearch()); + knnSearchBuilders.add(knnSearchBuilder); + searchSourceBuilder.knnSearch(knnSearchBuilders); + } + + @Override + public void doToXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(FIELD_FIELD.getPreferredName(), field); + builder.field(K_FIELD.getPreferredName(), k); + builder.field(NUM_CANDS_FIELD.getPreferredName(), numCands); + + if (queryVector != null) { + builder.field(QUERY_VECTOR_FIELD.getPreferredName(), queryVector); + } + + if (queryVectorBuilder != null) { + builder.field(QUERY_VECTOR_BUILDER_FIELD.getPreferredName(), queryVectorBuilder); + } + + if (similarity != null) { + builder.field(VECTOR_SIMILARITY.getPreferredName(), similarity); + } + } + + @Override + public boolean doEquals(Object o) { + KnnRetrieverBuilder that = (KnnRetrieverBuilder) o; + return k == that.k + && numCands == that.numCands + && Objects.equals(field, that.field) + && Arrays.equals(queryVector, that.queryVector) + && Objects.equals(queryVectorBuilder, that.queryVectorBuilder) + && Objects.equals(similarity, that.similarity); + } + + @Override + public int doHashCode() { + int result = Objects.hash(field, queryVectorBuilder, k, numCands, similarity); + result = 31 * result + Arrays.hashCode(queryVector); + return result; + } + + // ---- END TESTING ---- +} diff --git a/server/src/main/java/org/elasticsearch/search/retriever/RetrieverBuilder.java b/server/src/main/java/org/elasticsearch/search/retriever/RetrieverBuilder.java new file mode 100644 index 0000000000000..c9b12f03beb53 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/retriever/RetrieverBuilder.java @@ -0,0 +1,234 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.SuggestingErrorOnUnknown; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.xcontent.AbstractObjectParser; +import org.elasticsearch.xcontent.FilterXContentParserWrapper; +import org.elasticsearch.xcontent.NamedObjectNotFoundException; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentLocation; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** + * A retriever represents an API element that returns an ordered list of top + * documents. These can be obtained from a query, from another retriever, etc. + * Internally, a {@link RetrieverBuilder} is just a wrapper for other search + * elements that are extracted into a {@link SearchSourceBuilder}. The advantage + * retrievers have is in the API they appear as a tree-like structure enabling + * easier reasoning about what a search does. + * + * This is the base class for all other retrievers. This class does not support + * serialization and is expected to be fully extracted to a {@link SearchSourceBuilder} + * prior to any transport calls. + */ +public abstract class RetrieverBuilder implements ToXContent { + + public static final NodeFeature RETRIEVERS_SUPPORTED = new NodeFeature("retrievers_supported"); + + public static final ParseField PRE_FILTER_FIELD = new ParseField("filter"); + + protected static void declareBaseParserFields( + String name, + AbstractObjectParser parser + ) { + parser.declareObjectArray((r, v) -> r.preFilterQueryBuilders = v, (p, c) -> { + QueryBuilder preFilterQueryBuilder = AbstractQueryBuilder.parseTopLevelQuery(p, c::trackQueryUsage); + c.trackSectionUsage(name + ":" + PRE_FILTER_FIELD.getPreferredName()); + return preFilterQueryBuilder; + }, PRE_FILTER_FIELD); + } + + /** + * This method parsers a top-level retriever within a search and tracks its own depth. Currently, the + * maximum depth allowed is limited to 2 as a compound retriever cannot currently contain another + * compound retriever. + */ + public static RetrieverBuilder parseTopLevelRetrieverBuilder(XContentParser parser, RetrieverParserContext context) throws IOException { + parser = new FilterXContentParserWrapper(parser) { + + int nestedDepth = 0; + + @Override + public T namedObject(Class categoryClass, String name, Object context) throws IOException { + if (categoryClass.equals(RetrieverBuilder.class)) { + nestedDepth++; + + if (nestedDepth > 2) { + throw new IllegalArgumentException( + "the nested depth of the [" + name + "] retriever exceeds the maximum nested depth [2] for retrievers" + ); + } + } + + T namedObject = getXContentRegistry().parseNamedObject(categoryClass, name, this, context); + + if (categoryClass.equals(RetrieverBuilder.class)) { + nestedDepth--; + } + + return namedObject; + } + }; + + return parseInnerRetrieverBuilder(parser, context); + } + + protected static RetrieverBuilder parseInnerRetrieverBuilder(XContentParser parser, RetrieverParserContext context) throws IOException { + Objects.requireNonNull(context); + + if (parser.currentToken() != XContentParser.Token.START_OBJECT && parser.nextToken() != XContentParser.Token.START_OBJECT) { + throw new ParsingException( + parser.getTokenLocation(), + "retriever malformed, must start with [" + XContentParser.Token.START_OBJECT + "]" + ); + } + + if (parser.nextToken() == XContentParser.Token.END_OBJECT) { + throw new ParsingException(parser.getTokenLocation(), "retriever malformed, empty clause found"); + } + + if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { + throw new ParsingException( + parser.getTokenLocation(), + "retriever malformed, no field after [" + XContentParser.Token.START_OBJECT + "]" + ); + } + + String retrieverName = parser.currentName(); + + if (parser.nextToken() != XContentParser.Token.START_OBJECT) { + throw new ParsingException( + parser.getTokenLocation(), + "[" + retrieverName + "] retriever malformed, no [" + XContentParser.Token.START_OBJECT + "] after retriever name" + ); + } + + RetrieverBuilder retrieverBuilder; + + try { + retrieverBuilder = parser.namedObject(RetrieverBuilder.class, retrieverName, context); + } catch (NamedObjectNotFoundException nonfe) { + String message = String.format( + Locale.ROOT, + "unknown retriever [%s]%s", + retrieverName, + SuggestingErrorOnUnknown.suggest(retrieverName, nonfe.getCandidates()) + ); + + throw new ParsingException(new XContentLocation(nonfe.getLineNumber(), nonfe.getColumnNumber()), message, nonfe); + } + + context.trackSectionUsage(retrieverName); + + if (parser.currentToken() != XContentParser.Token.END_OBJECT) { + throw new ParsingException( + parser.getTokenLocation(), + "[" + + retrieverName + + "] malformed retriever, expected [" + + XContentParser.Token.END_OBJECT + + "] but found [" + + parser.currentToken() + + "]" + ); + } + + if (parser.nextToken() != XContentParser.Token.END_OBJECT) { + throw new ParsingException( + parser.getTokenLocation(), + "[" + + retrieverName + + "] malformed retriever, expected [" + + XContentParser.Token.END_OBJECT + + "] but found [" + + parser.currentToken() + + "]" + ); + } + + return retrieverBuilder; + } + + protected List preFilterQueryBuilders = new ArrayList<>(); + + /** + * Gets the filters for this retriever. + */ + public List getPreFilterQueryBuilders() { + return preFilterQueryBuilders; + } + + /** + * This method is called at the end of parsing on behalf of a {@link SearchSourceBuilder}. + * Elements from retrievers are expected to be "extracted" into the {@link SearchSourceBuilder}. + */ + public abstract void extractToSearchSourceBuilder(SearchSourceBuilder searchSourceBuilder, boolean compoundUsed); + + // ---- FOR TESTING XCONTENT PARSING ---- + + public abstract String getName(); + + @Override + public final XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startObject(); + if (preFilterQueryBuilders.isEmpty() == false) { + builder.field(PRE_FILTER_FIELD.getPreferredName(), preFilterQueryBuilders); + } + doToXContent(builder, params); + builder.endObject(); + + return builder; + } + + protected abstract void doToXContent(XContentBuilder builder, ToXContent.Params params) throws IOException; + + @Override + public boolean isFragment() { + return false; + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RetrieverBuilder that = (RetrieverBuilder) o; + return Objects.equals(preFilterQueryBuilders, that.preFilterQueryBuilders) && doEquals(o); + } + + protected abstract boolean doEquals(Object o); + + @Override + public final int hashCode() { + return Objects.hash(getClass(), preFilterQueryBuilders, doHashCode()); + } + + protected abstract int doHashCode(); + + @Override + public String toString() { + return Strings.toString(this, true, true); + } + + // ---- END FOR TESTING ---- +} diff --git a/server/src/main/java/org/elasticsearch/search/retriever/RetrieverParser.java b/server/src/main/java/org/elasticsearch/search/retriever/RetrieverParser.java new file mode 100644 index 0000000000000..d2703d8a81260 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/retriever/RetrieverParser.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; + +/** + * Defines a retriever parser that is able to parse {@link RetrieverBuilder}s + * from {@link org.elasticsearch.xcontent.XContent}. + */ +@FunctionalInterface +public interface RetrieverParser { + + /** + * Creates a new {@link RetrieverBuilder} from the retriever held by the + * {@link XContentParser}. The state on the parser contained in this context + * will be changed as a side effect of this method call. The + * {@link RetrieverParserContext} tracks usage of retriever features and + * queries when available. + */ + RB fromXContent(XContentParser parser, RetrieverParserContext context) throws IOException; +} diff --git a/server/src/main/java/org/elasticsearch/search/retriever/RetrieverParserContext.java b/server/src/main/java/org/elasticsearch/search/retriever/RetrieverParserContext.java new file mode 100644 index 0000000000000..21c24647753cb --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/retriever/RetrieverParserContext.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.usage.SearchUsage; + +import java.util.Objects; +import java.util.function.Predicate; + +public class RetrieverParserContext { + + protected final SearchUsage searchUsage; + protected final Predicate clusterSupportsFeature; + + public RetrieverParserContext(SearchUsage searchUsage, Predicate clusterSupportsFeature) { + this.searchUsage = Objects.requireNonNull(searchUsage); + this.clusterSupportsFeature = clusterSupportsFeature; + } + + public void trackSectionUsage(String section) { + searchUsage.trackSectionUsage(section); + } + + public void trackQueryUsage(String query) { + searchUsage.trackQueryUsage(query); + } + + public void trackRescorerUsage(String name) { + searchUsage.trackRescorerUsage(name); + } + + public boolean clusterSupportsFeature(NodeFeature nodeFeature) { + return clusterSupportsFeature != null && clusterSupportsFeature.test(nodeFeature); + } +} diff --git a/server/src/main/java/org/elasticsearch/search/retriever/RetrieversFeatures.java b/server/src/main/java/org/elasticsearch/search/retriever/RetrieversFeatures.java new file mode 100644 index 0000000000000..ad664616f4564 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/retriever/RetrieversFeatures.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.features.FeatureSpecification; +import org.elasticsearch.features.NodeFeature; + +import java.util.Set; + +/** + * Each retriever is given its own {@link NodeFeature} so new + * retrievers can be added individually with additional functionality. + */ +public class RetrieversFeatures implements FeatureSpecification { + + @Override + public Set getFeatures() { + return Set.of( + RetrieverBuilder.RETRIEVERS_SUPPORTED, + StandardRetrieverBuilder.STANDARD_RETRIEVER_SUPPORTED, + KnnRetrieverBuilder.KNN_RETRIEVER_SUPPORTED + ); + } +} diff --git a/server/src/main/java/org/elasticsearch/search/retriever/StandardRetrieverBuilder.java b/server/src/main/java/org/elasticsearch/search/retriever/StandardRetrieverBuilder.java new file mode 100644 index 0000000000000..4694780770617 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/retriever/StandardRetrieverBuilder.java @@ -0,0 +1,229 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.builder.SubSearchSourceBuilder; +import org.elasticsearch.search.collapse.CollapseBuilder; +import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.search.searchafter.SearchAfterBuilder; +import org.elasticsearch.search.sort.SortBuilder; +import org.elasticsearch.xcontent.ObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +/** + * A standard retriever is used to represent anything that is a query along + * with some elements to specify parameters for that query. + */ +public final class StandardRetrieverBuilder extends RetrieverBuilder implements ToXContent { + + public static final String NAME = "standard"; + public static final NodeFeature STANDARD_RETRIEVER_SUPPORTED = new NodeFeature("standard_retriever_supported"); + + public static final ParseField QUERY_FIELD = new ParseField("query"); + public static final ParseField SEARCH_AFTER_FIELD = new ParseField("search_after"); + public static final ParseField TERMINATE_AFTER_FIELD = new ParseField("terminate_after"); + public static final ParseField SORT_FIELD = new ParseField("sort"); + public static final ParseField MIN_SCORE_FIELD = new ParseField("min_score"); + public static final ParseField COLLAPSE_FIELD = new ParseField("collapse"); + + public static final ObjectParser PARSER = new ObjectParser<>( + NAME, + StandardRetrieverBuilder::new + ); + + static { + PARSER.declareObject((r, v) -> r.queryBuilder = v, (p, c) -> { + QueryBuilder queryBuilder = AbstractQueryBuilder.parseTopLevelQuery(p, c::trackQueryUsage); + c.trackSectionUsage(NAME + ":" + QUERY_FIELD.getPreferredName()); + return queryBuilder; + }, QUERY_FIELD); + + PARSER.declareField((r, v) -> r.searchAfterBuilder = v, (p, c) -> { + SearchAfterBuilder searchAfterBuilder = SearchAfterBuilder.fromXContent(p); + c.trackSectionUsage(NAME + ":" + SEARCH_AFTER_FIELD.getPreferredName()); + return searchAfterBuilder; + }, SEARCH_AFTER_FIELD, ObjectParser.ValueType.OBJECT_ARRAY); + + PARSER.declareField((r, v) -> r.terminateAfter = v, (p, c) -> { + int terminateAfter = p.intValue(); + c.trackSectionUsage(NAME + ":" + TERMINATE_AFTER_FIELD.getPreferredName()); + return terminateAfter; + }, TERMINATE_AFTER_FIELD, ObjectParser.ValueType.INT); + + PARSER.declareField((r, v) -> r.sortBuilders = v, (p, c) -> { + List> sortBuilders = SortBuilder.fromXContent(p); + c.trackSectionUsage(NAME + ":" + SORT_FIELD.getPreferredName()); + return sortBuilders; + }, SORT_FIELD, ObjectParser.ValueType.OBJECT_ARRAY); + + PARSER.declareField((r, v) -> r.minScore = v, (p, c) -> { + float minScore = p.floatValue(); + c.trackSectionUsage(NAME + ":" + MIN_SCORE_FIELD.getPreferredName()); + return minScore; + }, MIN_SCORE_FIELD, ObjectParser.ValueType.FLOAT); + + PARSER.declareField((r, v) -> r.collapseBuilder = v, (p, c) -> { + CollapseBuilder collapseBuilder = CollapseBuilder.fromXContent(p); + if (collapseBuilder.getField() != null) { + c.trackSectionUsage(COLLAPSE_FIELD.getPreferredName()); + } + return collapseBuilder; + }, COLLAPSE_FIELD, ObjectParser.ValueType.OBJECT); + + RetrieverBuilder.declareBaseParserFields(NAME, PARSER); + } + + public static StandardRetrieverBuilder fromXContent(XContentParser parser, RetrieverParserContext context) throws IOException { + if (context.clusterSupportsFeature(STANDARD_RETRIEVER_SUPPORTED) == false) { + throw new ParsingException(parser.getTokenLocation(), "unknown retriever [" + NAME + "]"); + } + return PARSER.apply(parser, context); + } + + QueryBuilder queryBuilder; + SearchAfterBuilder searchAfterBuilder; + int terminateAfter = SearchContext.DEFAULT_TERMINATE_AFTER; + List> sortBuilders; + Float minScore; + CollapseBuilder collapseBuilder; + + @Override + public void extractToSearchSourceBuilder(SearchSourceBuilder searchSourceBuilder, boolean compoundUsed) { + if (preFilterQueryBuilders.isEmpty() == false) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + + for (QueryBuilder preFilterQueryBuilder : preFilterQueryBuilders) { + boolQueryBuilder.filter(preFilterQueryBuilder); + } + + if (queryBuilder != null) { + boolQueryBuilder.must(queryBuilder); + } + + searchSourceBuilder.subSearches().add(new SubSearchSourceBuilder(boolQueryBuilder)); + } else if (queryBuilder != null) { + searchSourceBuilder.subSearches().add(new SubSearchSourceBuilder(queryBuilder)); + } + + if (searchAfterBuilder != null) { + if (compoundUsed) { + throw new IllegalArgumentException( + "[" + SEARCH_AFTER_FIELD.getPreferredName() + "] cannot be used in children of compound retrievers" + ); + } + + searchSourceBuilder.searchAfter(searchAfterBuilder.getSortValues()); + } + + if (terminateAfter != SearchContext.DEFAULT_TERMINATE_AFTER) { + if (compoundUsed) { + throw new IllegalArgumentException( + "[" + TERMINATE_AFTER_FIELD.getPreferredName() + "] cannot be used in children of compound retrievers" + ); + } + + searchSourceBuilder.terminateAfter(terminateAfter); + } + + if (sortBuilders != null) { + if (compoundUsed) { + throw new IllegalArgumentException( + "[" + SORT_FIELD.getPreferredName() + "] cannot be used in children of compound retrievers" + ); + } + + searchSourceBuilder.sort(sortBuilders); + } + + if (minScore != null) { + if (compoundUsed) { + throw new IllegalArgumentException( + "[" + MIN_SCORE_FIELD.getPreferredName() + "] cannot be used in children of compound retrievers" + ); + } + + searchSourceBuilder.minScore(minScore); + } + + if (collapseBuilder != null) { + if (compoundUsed) { + throw new IllegalArgumentException( + "[" + COLLAPSE_FIELD.getPreferredName() + "] cannot be used in children of compound retrievers" + ); + } + + searchSourceBuilder.collapse(collapseBuilder); + } + } + + // ---- FOR TESTING XCONTENT PARSING ---- + + @Override + public String getName() { + return NAME; + } + + @Override + public void doToXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + if (queryBuilder != null) { + builder.field(QUERY_FIELD.getPreferredName(), queryBuilder); + } + + if (searchAfterBuilder != null) { + searchAfterBuilder.innerToXContent(builder); + } + + if (terminateAfter != SearchContext.DEFAULT_TERMINATE_AFTER) { + builder.field(TERMINATE_AFTER_FIELD.getPreferredName(), terminateAfter); + } + + if (sortBuilders != null) { + builder.field(SORT_FIELD.getPreferredName(), sortBuilders); + } + + if (minScore != null) { + builder.field(MIN_SCORE_FIELD.getPreferredName(), minScore); + } + + if (collapseBuilder != null) { + builder.field(COLLAPSE_FIELD.getPreferredName(), collapseBuilder); + } + } + + @Override + public boolean doEquals(Object o) { + StandardRetrieverBuilder that = (StandardRetrieverBuilder) o; + return terminateAfter == that.terminateAfter + && Objects.equals(queryBuilder, that.queryBuilder) + && Objects.equals(searchAfterBuilder, that.searchAfterBuilder) + && Objects.equals(sortBuilders, that.sortBuilders) + && Objects.equals(minScore, that.minScore) + && Objects.equals(collapseBuilder, that.collapseBuilder); + } + + @Override + public int doHashCode() { + return Objects.hash(queryBuilder, searchAfterBuilder, terminateAfter, sortBuilders, minScore, collapseBuilder); + } + + // ---- END FOR TESTING ---- +} diff --git a/server/src/main/java/org/elasticsearch/search/searchafter/SearchAfterBuilder.java b/server/src/main/java/org/elasticsearch/search/searchafter/SearchAfterBuilder.java index 249f2c95ddc7f..f5c3727f56676 100644 --- a/server/src/main/java/org/elasticsearch/search/searchafter/SearchAfterBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/searchafter/SearchAfterBuilder.java @@ -211,7 +211,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - void innerToXContent(XContentBuilder builder) throws IOException { + public void innerToXContent(XContentBuilder builder) throws IOException { builder.array(SEARCH_AFTER.getPreferredName(), sortValues); } @@ -277,7 +277,8 @@ public boolean equals(Object other) { if ((other instanceof SearchAfterBuilder) == false) { return false; } - return Arrays.equals(sortValues, ((SearchAfterBuilder) other).sortValues); + boolean value = Arrays.equals(sortValues, ((SearchAfterBuilder) other).sortValues); + return value; } @Override diff --git a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification index a42c1f7192d49..71b6aacd56ea7 100644 --- a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification +++ b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification @@ -12,3 +12,4 @@ org.elasticsearch.cluster.service.TransportFeatures org.elasticsearch.cluster.metadata.MetadataFeatures org.elasticsearch.rest.RestFeatures org.elasticsearch.indices.IndicesFeatures +org.elasticsearch.search.retriever.RetrieversFeatures diff --git a/server/src/test/java/org/elasticsearch/search/retriever/KnnRetrieverBuilderParsingTests.java b/server/src/test/java/org/elasticsearch/search/retriever/KnnRetrieverBuilderParsingTests.java new file mode 100644 index 0000000000000..cbbbe7d86f4e2 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/retriever/KnnRetrieverBuilderParsingTests.java @@ -0,0 +1,82 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.RandomQueryBuilder; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.usage.SearchUsage; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.search.vectors.KnnSearchBuilderTests.randomVector; + +public class KnnRetrieverBuilderParsingTests extends AbstractXContentTestCase { + + /** + * Creates a random {@link KnnRetrieverBuilder}. The created instance + * is not guaranteed to pass {@link SearchRequest} validation. This is purely + * for x-content testing. + */ + public static KnnRetrieverBuilder createRandomKnnRetrieverBuilder() { + String field = randomAlphaOfLength(6); + int dim = randomIntBetween(2, 30); + float[] vector = randomBoolean() ? null : randomVector(dim); + int k = randomIntBetween(1, 100); + int numCands = randomIntBetween(k + 20, 1000); + Float similarity = randomBoolean() ? null : randomFloat(); + + KnnRetrieverBuilder knnRetrieverBuilder = new KnnRetrieverBuilder(field, vector, null, k, numCands, similarity); + + List preFilterQueryBuilders = new ArrayList<>(); + + if (randomBoolean()) { + for (int i = 0; i < randomIntBetween(1, 3); ++i) { + preFilterQueryBuilders.add(RandomQueryBuilder.createQuery(random())); + } + } + + knnRetrieverBuilder.preFilterQueryBuilders.addAll(preFilterQueryBuilders); + + return knnRetrieverBuilder; + } + + @Override + protected KnnRetrieverBuilder createTestInstance() { + return createRandomKnnRetrieverBuilder(); + } + + @Override + protected KnnRetrieverBuilder doParseInstance(XContentParser parser) throws IOException { + return KnnRetrieverBuilder.fromXContent( + parser, + new RetrieverParserContext( + new SearchUsage(), + nf -> nf == RetrieverBuilder.RETRIEVERS_SUPPORTED || nf == KnnRetrieverBuilder.KNN_RETRIEVER_SUPPORTED + ) + ); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new SearchModule(Settings.EMPTY, List.of()).getNamedXContents()); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/retriever/RetrieverBuilderErrorTests.java b/server/src/test/java/org/elasticsearch/search/retriever/RetrieverBuilderErrorTests.java new file mode 100644 index 0000000000000..0e5490e989a2d --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/retriever/RetrieverBuilderErrorTests.java @@ -0,0 +1,102 @@ +/* + * 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 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 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.builder.SearchSourceBuilder; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.json.JsonXContent; + +import java.io.IOException; +import java.util.List; + +/** + * Tests exceptions related to usage of restricted global values with a retriever. + */ +public class RetrieverBuilderErrorTests extends ESTestCase { + + public void testRetrieverExtractionErrors() throws IOException { + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"query\": {\"match_all\": {}}, \"retriever\":{\"standard\":{}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("cannot specify [retriever] and [query]", iae.getMessage()); + } + + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"knn\":{\"field\": \"test\", \"k\": 2, \"num_candidates\": 5," + + " \"query_vector\": [1, 2, 3]}, \"retriever\":{\"standard\":{}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("cannot specify [retriever] and [knn]", iae.getMessage()); + } + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"search_after\": [1], \"retriever\":{\"standard\":{}}}")) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("cannot specify [retriever] and [search_after]", iae.getMessage()); + } + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"terminate_after\": 1, \"retriever\":{\"standard\":{}}}")) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("cannot specify [retriever] and [terminate_after]", iae.getMessage()); + } + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"sort\": [\"field\"], \"retriever\":{\"standard\":{}}}")) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("cannot specify [retriever] and [sort]", iae.getMessage()); + } + + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"rescore\": {\"query\": {\"rescore_query\": {\"match_all\": {}}}}, \"retriever\":{\"standard\":{}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("cannot specify [retriever] and [rescore]", iae.getMessage()); + } + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"min_score\": 2, \"retriever\":{\"standard\":{}}}")) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("cannot specify [retriever] and [min_score]", iae.getMessage()); + } + + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "" + "{\"min_score\": 2, \"query\": {\"match_all\": {}}, \"retriever\":{\"standard\":{}}, \"terminate_after\": 1}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("cannot specify [retriever] and [query, terminate_after, min_score]", iae.getMessage()); + } + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new SearchModule(Settings.EMPTY, List.of()).getNamedXContents()); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/retriever/RetrieverBuilderVersionTests.java b/server/src/test/java/org/elasticsearch/search/retriever/RetrieverBuilderVersionTests.java new file mode 100644 index 0000000000000..593b43bc26597 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/retriever/RetrieverBuilderVersionTests.java @@ -0,0 +1,80 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.json.JsonXContent; + +import java.io.IOException; +import java.util.List; + +/** Tests retrievers validate on their own {@link NodeFeature} */ +public class RetrieverBuilderVersionTests extends ESTestCase { + + public void testRetrieverVersions() throws IOException { + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"retriever\":{\"standard\":{}}}")) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + ParsingException iae = expectThrows(ParsingException.class, () -> ssb.parseXContent(parser, true, nf -> false)); + assertEquals("Unknown key for a START_OBJECT in [retriever].", iae.getMessage()); + } + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"retriever\":{\"standard\":{}}}")) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + ParsingException iae = expectThrows( + ParsingException.class, + () -> ssb.parseXContent(parser, true, nf -> nf == RetrieverBuilder.RETRIEVERS_SUPPORTED) + ); + assertEquals("unknown retriever [standard]", iae.getMessage()); + } + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"retriever\":{\"standard\":{}}}")) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + ssb.parseXContent( + parser, + true, + nf -> nf == RetrieverBuilder.RETRIEVERS_SUPPORTED || nf == StandardRetrieverBuilder.STANDARD_RETRIEVER_SUPPORTED + ); + } + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"retriever\":{\"knn\":{}}}")) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + ParsingException iae = expectThrows( + ParsingException.class, + () -> ssb.parseXContent(parser, true, nf -> nf == RetrieverBuilder.RETRIEVERS_SUPPORTED) + ); + assertEquals("unknown retriever [knn]", iae.getMessage()); + } + + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"retriever\":{\"knn\":{\"field\": \"test\", \"k\": 2, \"num_candidates\": 5, \"query_vector\": [1, 2, 3]}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + ssb.parseXContent( + parser, + true, + nf -> nf == RetrieverBuilder.RETRIEVERS_SUPPORTED || nf == KnnRetrieverBuilder.KNN_RETRIEVER_SUPPORTED + ); + } + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new SearchModule(Settings.EMPTY, List.of()).getNamedXContents()); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/retriever/StandardRetrieverBuilderParsingTests.java b/server/src/test/java/org/elasticsearch/search/retriever/StandardRetrieverBuilderParsingTests.java new file mode 100644 index 0000000000000..bec534d89cc03 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/retriever/StandardRetrieverBuilderParsingTests.java @@ -0,0 +1,112 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.query.RandomQueryBuilder; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.collapse.CollapseBuilderTests; +import org.elasticsearch.search.searchafter.SearchAfterBuilderTests; +import org.elasticsearch.search.sort.SortBuilderTests; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.usage.SearchUsage; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContent; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.function.BiFunction; + +public class StandardRetrieverBuilderParsingTests extends AbstractXContentTestCase { + + /** + * Creates a random {@link StandardRetrieverBuilder}. The created instance + * is not guaranteed to pass {@link SearchRequest} validation. This is purely + * for x-content testing. + */ + public static StandardRetrieverBuilder createRandomStandardRetrieverBuilder( + BiFunction createParser + ) { + try { + StandardRetrieverBuilder standardRetrieverBuilder = new StandardRetrieverBuilder(); + + if (randomBoolean()) { + for (int i = 0; i < randomIntBetween(1, 3); ++i) { + standardRetrieverBuilder.getPreFilterQueryBuilders().add(RandomQueryBuilder.createQuery(random())); + } + } + + if (randomBoolean()) { + standardRetrieverBuilder.queryBuilder = RandomQueryBuilder.createQuery(random()); + } + + if (randomBoolean()) { + standardRetrieverBuilder.searchAfterBuilder = SearchAfterBuilderTests.randomJsonSearchFromBuilder(createParser); + } + + if (randomBoolean()) { + standardRetrieverBuilder.terminateAfter = randomNonNegativeInt(); + } + + if (randomBoolean()) { + standardRetrieverBuilder.sortBuilders = SortBuilderTests.randomSortBuilderList(); + } + + if (randomBoolean()) { + standardRetrieverBuilder.collapseBuilder = CollapseBuilderTests.randomCollapseBuilder(randomBoolean()); + } + + return standardRetrieverBuilder; + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + + @Override + protected StandardRetrieverBuilder createTestInstance() { + return createRandomStandardRetrieverBuilder((xContent, data) -> { + try { + return createParser(xContent, data); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + }); + } + + @Override + protected StandardRetrieverBuilder doParseInstance(XContentParser parser) throws IOException { + return StandardRetrieverBuilder.fromXContent( + parser, + new RetrieverParserContext( + new SearchUsage(), + nf -> nf == RetrieverBuilder.RETRIEVERS_SUPPORTED || nf == StandardRetrieverBuilder.STANDARD_RETRIEVER_SUPPORTED + ) + ); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + @Override + protected String[] getShuffleFieldsExceptions() { + // disable xcontent shuffling on the highlight builder + return new String[] { "fields" }; + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new SearchModule(Settings.EMPTY, List.of()).getNamedXContents()); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/searchafter/SearchAfterBuilderTests.java b/server/src/test/java/org/elasticsearch/search/searchafter/SearchAfterBuilderTests.java index ff963835f55f6..96ebc5642fde7 100644 --- a/server/src/test/java/org/elasticsearch/search/searchafter/SearchAfterBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/searchafter/SearchAfterBuilderTests.java @@ -29,6 +29,7 @@ import org.elasticsearch.search.sort.SortAndFormats; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; @@ -36,8 +37,10 @@ import org.elasticsearch.xcontent.json.JsonXContent; import java.io.IOException; +import java.io.UncheckedIOException; import java.math.BigDecimal; import java.util.Collections; +import java.util.function.BiFunction; import static org.elasticsearch.search.searchafter.SearchAfterBuilder.extractSortType; import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; @@ -47,7 +50,10 @@ public class SearchAfterBuilderTests extends ESTestCase { private static final int NUMBER_OF_TESTBUILDERS = 20; - private static SearchAfterBuilder randomSearchAfterBuilder() throws IOException { + /** + * Generates a random {@link SearchAfterBuilder}. + */ + public static SearchAfterBuilder randomSearchAfterBuilder() throws IOException { int numSearchFrom = randomIntBetween(1, 10); SearchAfterBuilder searchAfterBuilder = new SearchAfterBuilder(); Object[] values = new Object[numSearchFrom]; @@ -71,11 +77,14 @@ private static SearchAfterBuilder randomSearchAfterBuilder() throws IOException return searchAfterBuilder; } - // We build a json version of the search_after first in order to - // ensure that every number type remain the same before/after xcontent (de)serialization. - // This is not a problem because the final type of each field value is extracted from associated sort field. - // This little trick ensure that equals and hashcode are the same when using the xcontent serialization. - private SearchAfterBuilder randomJsonSearchFromBuilder() throws IOException { + /** + * We build a json version of the search_after first in order to + * ensure that every number type remain the same before/after xcontent (de)serialization. + * This is not a problem because the final type of each field value is extracted from associated sort field. + * This little trick ensure that equals and hashcode are the same when using the xcontent serialization. + */ + public static SearchAfterBuilder randomJsonSearchFromBuilder(BiFunction createParser) + throws IOException { int numSearchAfter = randomIntBetween(1, 10); XContentBuilder jsonBuilder = XContentFactory.jsonBuilder(); jsonBuilder.startObject(); @@ -97,7 +106,7 @@ private SearchAfterBuilder randomJsonSearchFromBuilder() throws IOException { } jsonBuilder.endArray(); jsonBuilder.endObject(); - try (XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(jsonBuilder))) { + try (XContentParser parser = createParser.apply(JsonXContent.jsonXContent, BytesReference.bytes(jsonBuilder))) { parser.nextToken(); parser.nextToken(); parser.nextToken(); @@ -128,7 +137,13 @@ public void testEqualsAndHashcode() throws Exception { public void testFromXContent() throws Exception { for (int runs = 0; runs < 20; runs++) { - SearchAfterBuilder searchAfterBuilder = randomJsonSearchFromBuilder(); + SearchAfterBuilder searchAfterBuilder = randomJsonSearchFromBuilder((xContent, data) -> { + try { + return createParser(xContent, data); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + }); XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values())); if (randomBoolean()) { builder.prettyPrint(); diff --git a/server/src/test/java/org/elasticsearch/search/vectors/KnnSearchBuilderTests.java b/server/src/test/java/org/elasticsearch/search/vectors/KnnSearchBuilderTests.java index a077367604e5e..c650f54321060 100644 --- a/server/src/test/java/org/elasticsearch/search/vectors/KnnSearchBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/vectors/KnnSearchBuilderTests.java @@ -220,7 +220,7 @@ public void testRewrite() throws Exception { assertThat(((RewriteableQuery) rewritten.filterQueries.get(0)).rewrites, equalTo(1)); } - static float[] randomVector(int dim) { + public static float[] randomVector(int dim) { float[] vector = new float[dim]; for (int i = 0; i < vector.length; i++) { vector[i] = randomFloat(); diff --git a/test/framework/src/main/java/org/elasticsearch/search/retriever/TestRetrieverBuilder.java b/test/framework/src/main/java/org/elasticsearch/search/retriever/TestRetrieverBuilder.java new file mode 100644 index 0000000000000..40cc1890f69ed --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/search/retriever/TestRetrieverBuilder.java @@ -0,0 +1,93 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.retriever; + +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.plugins.SearchPlugin; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; + +/** + * Test retriever is used to test parsing of retrievers in plugins where + * generation of other random retrievers are not easily accessible through test code. + */ +public class TestRetrieverBuilder extends RetrieverBuilder { + + /** + * Creates a random {@link TestRetrieverBuilder}. The created instance + * is not guaranteed to pass {@link SearchRequest} validation. This is purely + * for x-content testing. + */ + public static TestRetrieverBuilder createRandomTestRetrieverBuilder() { + return new TestRetrieverBuilder(ESTestCase.randomAlphaOfLengthBetween(5, 10)); + } + + public static final String NAME = "test"; + public static final ParseField TEST_FIELD = new ParseField(NAME); + public static final SearchPlugin.RetrieverSpec TEST_SPEC = new SearchPlugin.RetrieverSpec<>( + TEST_FIELD, + TestRetrieverBuilder::fromXContent + ); + + public static final ParseField VALUE_FIELD = new ParseField("value"); + + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + NAME, + args -> new TestRetrieverBuilder((String) args[0]) + ); + + static { + PARSER.declareString(constructorArg(), VALUE_FIELD); + } + + public static TestRetrieverBuilder fromXContent(XContentParser parser, RetrieverParserContext context) { + return PARSER.apply(parser, context); + } + + private final String value; + + public TestRetrieverBuilder(String value) { + this.value = value; + } + + @Override + public void extractToSearchSourceBuilder(SearchSourceBuilder searchSourceBuilder, boolean compoundUsed) { + throw new UnsupportedOperationException("only used for parsing tests"); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public void doToXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(VALUE_FIELD.getPreferredName(), value); + } + + @Override + public boolean doEquals(Object o) { + TestRetrieverBuilder that = (TestRetrieverBuilder) o; + return Objects.equals(value, that.value); + } + + @Override + public int doHashCode() { + return Objects.hash(value); + } +} diff --git a/x-pack/plugin/rank-rrf/src/main/java/module-info.java b/x-pack/plugin/rank-rrf/src/main/java/module-info.java index a28e907610c26..4fd2a7e4d54f3 100644 --- a/x-pack/plugin/rank-rrf/src/main/java/module-info.java +++ b/x-pack/plugin/rank-rrf/src/main/java/module-info.java @@ -5,6 +5,8 @@ * 2.0. */ +import org.elasticsearch.xpack.rank.rrf.RRFFeatures; + module org.elasticsearch.rank.rrf { requires org.apache.lucene.core; requires org.elasticsearch.base; @@ -13,4 +15,6 @@ requires org.elasticsearch.xcore; exports org.elasticsearch.xpack.rank.rrf; + + provides org.elasticsearch.features.FeatureSpecification with RRFFeatures; } diff --git a/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFFeatures.java b/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFFeatures.java new file mode 100644 index 0000000000000..816b25d53d375 --- /dev/null +++ b/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFFeatures.java @@ -0,0 +1,24 @@ +/* + * 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.rank.rrf; + +import org.elasticsearch.features.FeatureSpecification; +import org.elasticsearch.features.NodeFeature; + +import java.util.Set; + +/** + * A set of features specifically for the rrf plugin. + */ +public class RRFFeatures implements FeatureSpecification { + + @Override + public Set getFeatures() { + return Set.of(RRFRetrieverBuilder.RRF_RETRIEVER_SUPPORTED); + } +} diff --git a/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRankPlugin.java b/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRankPlugin.java index 135f8907faa9b..4d7c60f00ec1c 100644 --- a/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRankPlugin.java +++ b/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRankPlugin.java @@ -41,4 +41,9 @@ public List getNamedWriteables() { public List getNamedXContent() { return List.of(new NamedXContentRegistry.Entry(RankBuilder.class, new ParseField(NAME), RRFRankBuilder::fromXContent)); } + + @Override + public List> getRetrievers() { + return List.of(new RetrieverSpec<>(new ParseField(NAME), RRFRetrieverBuilder::fromXContent)); + } } 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 new file mode 100644 index 0000000000000..ea8255f73af88 --- /dev/null +++ b/x-pack/plugin/rank-rrf/src/main/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilder.java @@ -0,0 +1,134 @@ +/* + * 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.rank.rrf; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.retriever.RetrieverBuilder; +import org.elasticsearch.search.retriever.RetrieverParserContext; +import org.elasticsearch.xcontent.ObjectParser; +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.Collections; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.xpack.rank.rrf.RRFRankPlugin.NAME; + +/** + * An rrf retriever is used to represent an rrf rank element, but + * as a tree-like structure. This retriever is a compound retriever + * meaning it has a set of child retrievers that each return a set of + * top docs that will then be combined and ranked according to the rrf + * formula. + */ +public final class RRFRetrieverBuilder extends RetrieverBuilder { + + public static final NodeFeature RRF_RETRIEVER_SUPPORTED = new NodeFeature("rrf_retriever_supported"); + + public static final ParseField RETRIEVERS_FIELD = new ParseField("retrievers"); + public static final ParseField WINDOW_SIZE_FIELD = new ParseField("window_size"); + public static final ParseField RANK_CONSTANT_FIELD = new ParseField("rank_constant"); + + public static final ObjectParser PARSER = new ObjectParser<>( + NAME, + RRFRetrieverBuilder::new + ); + + static { + PARSER.declareObjectArray((r, v) -> r.retrieverBuilders = v, (p, c) -> { + p.nextToken(); + String name = p.currentName(); + RetrieverBuilder retrieverBuilder = p.namedObject(RetrieverBuilder.class, name, c); + p.nextToken(); + return retrieverBuilder; + }, RETRIEVERS_FIELD); + PARSER.declareInt((r, v) -> r.windowSize = v, WINDOW_SIZE_FIELD); + PARSER.declareInt((r, v) -> r.rankConstant = v, RANK_CONSTANT_FIELD); + + RetrieverBuilder.declareBaseParserFields(NAME, PARSER); + } + + public static RRFRetrieverBuilder fromXContent(XContentParser parser, RetrieverParserContext context) throws IOException { + if (context.clusterSupportsFeature(RRF_RETRIEVER_SUPPORTED) == false) { + throw new ParsingException(parser.getTokenLocation(), "unknown retriever [" + NAME + "]"); + } + if (RRFRankPlugin.RANK_RRF_FEATURE.check(XPackPlugin.getSharedLicenseState()) == false) { + throw LicenseUtils.newComplianceException("Reciprocal Rank Fusion (RRF)"); + } + return PARSER.apply(parser, context); + } + + List retrieverBuilders = Collections.emptyList(); + int windowSize = RRFRankBuilder.DEFAULT_WINDOW_SIZE; + int rankConstant = RRFRankBuilder.DEFAULT_RANK_CONSTANT; + + @Override + public void extractToSearchSourceBuilder(SearchSourceBuilder searchSourceBuilder, boolean compoundUsed) { + if (compoundUsed) { + throw new IllegalArgumentException("[rank] cannot be used in children of compound retrievers"); + } + + for (RetrieverBuilder retrieverBuilder : retrieverBuilders) { + if (preFilterQueryBuilders.isEmpty() == false) { + retrieverBuilder.getPreFilterQueryBuilders().addAll(preFilterQueryBuilders); + } + + retrieverBuilder.extractToSearchSourceBuilder(searchSourceBuilder, true); + } + + searchSourceBuilder.rankBuilder(new RRFRankBuilder(windowSize, rankConstant)); + } + + // ---- FOR TESTING XCONTENT PARSING ---- + + @Override + public String getName() { + return NAME; + } + + @Override + public void doToXContent(XContentBuilder builder, Params params) throws IOException { + if (retrieverBuilders.isEmpty() == false) { + builder.startArray(RETRIEVERS_FIELD.getPreferredName()); + + for (RetrieverBuilder retrieverBuilder : retrieverBuilders) { + builder.startObject(); + builder.field(retrieverBuilder.getName()); + retrieverBuilder.toXContent(builder, params); + builder.endObject(); + } + + builder.endArray(); + } + + builder.field(WINDOW_SIZE_FIELD.getPreferredName(), windowSize); + builder.field(RANK_CONSTANT_FIELD.getPreferredName(), rankConstant); + } + + @Override + public boolean doEquals(Object o) { + RRFRetrieverBuilder that = (RRFRetrieverBuilder) o; + return windowSize == that.windowSize + && rankConstant == that.rankConstant + && Objects.equals(retrieverBuilders, that.retrieverBuilders); + } + + @Override + public int doHashCode() { + return Objects.hash(retrieverBuilders, windowSize, rankConstant); + } + + // ---- END FOR TESTING ---- +} diff --git a/x-pack/plugin/rank-rrf/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification b/x-pack/plugin/rank-rrf/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification new file mode 100644 index 0000000000000..605e999b66c66 --- /dev/null +++ b/x-pack/plugin/rank-rrf/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification @@ -0,0 +1,8 @@ +# +# 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. +# + +org.elasticsearch.xpack.rank.rrf.RRFFeatures diff --git a/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderParsingTests.java b/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderParsingTests.java new file mode 100644 index 0000000000000..d63e8a14b59d5 --- /dev/null +++ b/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderParsingTests.java @@ -0,0 +1,88 @@ +/* + * 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.rank.rrf; + +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.search.retriever.RetrieverBuilder; +import org.elasticsearch.search.retriever.RetrieverParserContext; +import org.elasticsearch.search.retriever.TestRetrieverBuilder; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.usage.SearchUsage; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class RRFRetrieverBuilderParsingTests extends AbstractXContentTestCase { + + /** + * Creates a random {@link RRFRetrieverBuilder}. The created instance + * is not guaranteed to pass {@link SearchRequest} validation. This is purely + * for x-content testing. + */ + public static RRFRetrieverBuilder createRandomRRFRetrieverBuilder() { + RRFRetrieverBuilder rrfRetrieverBuilder = new RRFRetrieverBuilder(); + + if (randomBoolean()) { + rrfRetrieverBuilder.windowSize = randomIntBetween(1, 10000); + } + + if (randomBoolean()) { + rrfRetrieverBuilder.rankConstant = randomIntBetween(1, 1000000); + } + + int retrieverCount = randomIntBetween(2, 50); + rrfRetrieverBuilder.retrieverBuilders = new ArrayList<>(retrieverCount); + + while (retrieverCount > 0) { + rrfRetrieverBuilder.retrieverBuilders.add(TestRetrieverBuilder.createRandomTestRetrieverBuilder()); + --retrieverCount; + } + + return rrfRetrieverBuilder; + } + + @Override + protected RRFRetrieverBuilder createTestInstance() { + return createRandomRRFRetrieverBuilder(); + } + + @Override + protected RRFRetrieverBuilder doParseInstance(XContentParser parser) throws IOException { + return RRFRetrieverBuilder.PARSER.apply(parser, new RetrieverParserContext(new SearchUsage(), nf -> true)); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + List entries = new ArrayList<>(); + 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() + ) + ); + entries.add( + new NamedXContentRegistry.Entry( + RetrieverBuilder.class, + new ParseField(RRFRankPlugin.NAME), + (p, c) -> RRFRetrieverBuilder.PARSER.apply(p, (RetrieverParserContext) c) + ) + ); + return new NamedXContentRegistry(entries); + } +} diff --git a/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderTests.java b/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderTests.java new file mode 100644 index 0000000000000..229f900ef3d15 --- /dev/null +++ b/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderTests.java @@ -0,0 +1,151 @@ +/* + * 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.rank.rrf; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.retriever.RetrieverBuilder; +import org.elasticsearch.search.retriever.RetrieverParserContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.json.JsonXContent; + +import java.io.IOException; +import java.util.List; + +/** Tests for the rrf retriever. */ +public class RRFRetrieverBuilderTests extends ESTestCase { + + /** Tests the rrf retriever validates on its own {@link NodeFeature} */ + public void testRetrieverVersions() throws IOException { + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"retriever\":{\"rrf\":{}}}")) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + ParsingException iae = expectThrows( + ParsingException.class, + () -> ssb.parseXContent(parser, true, nf -> nf == RetrieverBuilder.RETRIEVERS_SUPPORTED) + ); + assertEquals("unknown retriever [rrf]", iae.getMessage()); + } + } + + /** Tests extraction errors related to compound retrievers. These tests require a compound retriever which is why they are here. */ + public void testRetrieverExtractionErrors() throws IOException { + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"retriever\":{\"rrf_nl\":{\"retrievers\":" + + "[{\"standard\":{\"search_after\":[1]}},{\"standard\":{\"search_after\":[2]}}]}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("[search_after] cannot be used in children of compound retrievers", iae.getMessage()); + } + + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"retriever\":{\"rrf_nl\":{\"retrievers\":" + + "[{\"standard\":{\"terminate_after\":1}},{\"standard\":{\"terminate_after\":2}}]}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("[terminate_after] cannot be used in children of compound retrievers", iae.getMessage()); + } + + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"retriever\":{\"rrf_nl\":{\"retrievers\":" + "[{\"standard\":{\"sort\":[\"f1\"]}},{\"standard\":{\"sort\":[\"f2\"]}}]}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("[sort] cannot be used in children of compound retrievers", iae.getMessage()); + } + + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"retriever\":{\"rrf_nl\":{\"retrievers\":" + "[{\"standard\":{\"min_score\":1}},{\"standard\":{\"min_score\":2}}]}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("[min_score] cannot be used in children of compound retrievers", iae.getMessage()); + } + + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"retriever\":{\"rrf_nl\":{\"retrievers\":" + + "[{\"standard\":{\"collapse\":{\"field\":\"f0\"}}},{\"standard\":{\"collapse\":{\"field\":\"f1\"}}}]}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("[collapse] cannot be used in children of compound retrievers", iae.getMessage()); + } + + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"retriever\":{\"rrf_nl\":{\"retrievers\":[{\"rrf_nl\":{}}]}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("[rank] cannot be used in children of compound retrievers", iae.getMessage()); + } + } + + /** Tests max depth errors related to compound retrievers. These tests require a compound retriever which is why they are here. */ + public void testRetrieverBuilderParsingMaxDepth() throws IOException { + try ( + XContentParser parser = createParser( + JsonXContent.jsonXContent, + "{\"retriever\":{\"rrf_nl\":{\"retrievers\":[{\"rrf_nl\":{\"retrievers\":[{\"standard\":{}}]}}]}}}" + ) + ) { + SearchSourceBuilder ssb = new SearchSourceBuilder(); + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true)); + assertEquals("[1:65] [rrf] failed to parse field [retrievers]", iae.getMessage()); + assertEquals( + "the nested depth of the [standard] retriever exceeds the maximum nested depth [2] for retrievers", + iae.getCause().getCause().getMessage() + ); + } + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + List entries = new SearchModule(Settings.EMPTY, List.of()).getNamedXContents(); + entries.add( + new NamedXContentRegistry.Entry( + RetrieverBuilder.class, + new ParseField(RRFRankPlugin.NAME), + (p, c) -> RRFRetrieverBuilder.fromXContent(p, (RetrieverParserContext) c) + ) + ); + // Add an entry with no license requirement for unit testing + entries.add( + new NamedXContentRegistry.Entry( + RetrieverBuilder.class, + new ParseField(RRFRankPlugin.NAME + "_nl"), + (p, c) -> RRFRetrieverBuilder.PARSER.apply(p, (RetrieverParserContext) c) + ) + ); + return new NamedXContentRegistry(entries); + } +} diff --git a/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/license/100_license.yml b/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/license/100_license.yml index fb74c935c774c..c84c66f8aa31d 100644 --- a/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/license/100_license.yml +++ b/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/license/100_license.yml @@ -49,7 +49,7 @@ setup: indices.refresh: {} --- -"RRF Invalid License": +"rrf invalid license": - do: catch: forbidden @@ -75,3 +75,39 @@ setup: - match: { status: 403 } - match: { error.type: security_exception } - match: { error.reason: "current license is non-compliant for [Reciprocal Rank Fusion (RRF)]" } + +--- +"rrf retriever invalid license": + + - do: + catch: forbidden + search: + index: test + body: + track_total_hits: false + fields: [ "text" ] + retriever: + rrf: + retrievers: [ + { + knn: { + field: vector, + query_vector: [ 0.0 ], + k: 3, + num_candidates: 3 + } + }, + { + standard: { + query: { + term: { + text: term + } + } + } + } + ] + + - match: { status: 403 } + - match: { error.type: security_exception } + - match: { error.reason: "current license is non-compliant for [Reciprocal Rank Fusion (RRF)]" } diff --git a/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/rrf/300_rrf_retriever.yml b/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/rrf/300_rrf_retriever.yml new file mode 100644 index 0000000000000..ec7a31ffd9ceb --- /dev/null +++ b/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/rrf/300_rrf_retriever.yml @@ -0,0 +1,331 @@ +setup: + - skip: + version: ' - 8.12.99' + reason: 'rrf retriever added in 8.13' + + - do: + indices.create: + index: test + body: + settings: + number_of_shards: 5 + number_of_replicas: 0 + mappings: + properties: + text: + type: text + keyword: + type: keyword + vector: + type: dense_vector + dims: 1 + index: true + similarity: l2_norm + + - do: + index: + index: test + id: "1" + body: + text: "term term" + keyword: "other" + vector: [0.0] + + - do: + index: + index: test + id: "2" + body: + text: "other" + keyword: "other" + vector: [1.0] + + - do: + index: + index: test + id: "3" + body: + text: "term" + keyword: "keyword" + vector: [2.0] + + - do: + indices.refresh: {} + +--- +"rrf retriever with a standard retriever and a knn retriever": + + - do: + search: + index: test + body: + track_total_hits: false + fields: [ "text", "keyword" ] + retriever: + rrf: + retrievers: [ + { + knn: { + field: vector, + query_vector: [ 0.0 ], + k: 3, + num_candidates: 3 + } + }, + { + standard: { + query: { + term: { + text: term + } + } + } + } + ] + window_size: 100 + rank_constant: 1 + size: 10 + + - match: { hits.hits.0._id: "1" } + - match: { hits.hits.0._rank: 1 } + - match: { hits.hits.0.fields.text.0: "term term" } + - match: { hits.hits.0.fields.keyword.0: "other" } + + - match: { hits.hits.1._id: "3" } + - match: { hits.hits.1._rank: 2 } + - match: { hits.hits.1.fields.text.0: "term" } + - match: { hits.hits.1.fields.keyword.0: "keyword" } + + - match: { hits.hits.2._id: "2" } + - match: { hits.hits.2._rank: 3 } + - match: { hits.hits.2.fields.text.0: "other" } + - match: { hits.hits.2.fields.keyword.0: "other" } + +--- +"rrf retriever with multiple standard retrievers": + + - do: + search: + index: test + body: + track_total_hits: true + fields: [ "text", "keyword" ] + retriever: + rrf: + retrievers: [ + { + "standard": { + "query": { + "term": { + "text": "term" + } + } + } + }, + { + "standard": { + "query": { + "match": { + "keyword": "keyword" + } + } + } + } + ] + window_size: 100 + rank_constant: 1 + size: 10 + + - match: { hits.total.value : 2 } + + - match: { hits.hits.0._id: "3" } + - match: { hits.hits.0._rank: 1 } + - match: { hits.hits.0.fields.text.0: "term" } + - match: { hits.hits.0.fields.keyword.0: "keyword" } + + - match: { hits.hits.1._id: "1" } + - match: { hits.hits.1._rank: 2 } + - match: { hits.hits.1.fields.text.0: "term term" } + - match: { hits.hits.1.fields.keyword.0: "other" } + +--- +"rrf retriever with multiple standard retrievers and a knn retriever": + + - do: + search: + index: test + body: + track_total_hits: true + fields: [ "text", "keyword" ] + retriever: + rrf: + retrievers: [ + { + knn: { + field: vector, + query_vector: [ 0.0 ], + k: 3, + num_candidates: 3 + } + }, + { + "standard": { + "query": { + "term": { + "text": "term" + } + } + } + }, + { + "standard": { + "query": { + "match": { + "keyword": "keyword" + } + } + } + } + ] + window_size: 100 + rank_constant: 1 + size: 10 + + - match: { hits.total.value : 3 } + + - match: { hits.hits.0._id: "3" } + - match: { hits.hits.0._rank: 1 } + - match: { hits.hits.0.fields.text.0: "term" } + - match: { hits.hits.0.fields.keyword.0: "keyword" } + + - match: { hits.hits.1._id: "1" } + - match: { hits.hits.1._rank: 2 } + - match: { hits.hits.1.fields.text.0: "term term" } + - match: { hits.hits.1.fields.keyword.0: "other" } + + - match: { hits.hits.2._id: "2" } + - match: { hits.hits.2._rank: 3 } + - match: { hits.hits.2.fields.text.0: "other" } + - match: { hits.hits.2.fields.keyword.0: "other" } + +--- +"rrf retriever with multiple standard retrievers and multiple knn retriever": + + - do: + search: + size: 1 + index: test + body: + track_total_hits: true + fields: [ "text", "keyword" ] + retriever: + rrf: + retrievers: [ + { + knn: { + field: vector, + query_vector: [ 0.0 ], + k: 3, + num_candidates: 3 + } + }, + { + knn: { + field: vector, + query_vector: [ 1.0 ], + k: 3, + num_candidates: 3 + } + }, + { + "standard": { + "query": { + "term": { + "text": "term" + } + } + } + }, + { + "standard": { + "query": { + "match": { + "keyword": "keyword" + } + } + } + } + ] + window_size: 2 + rank_constant: 1 + + - match: { hits.total.value : 3 } + - length: { hits.hits: 1 } + + - match: { hits.hits.0._id: "3" } + - match: { hits.hits.0._rank: 1 } + - match: { hits.hits.0.fields.text.0: "term" } + - match: { hits.hits.0.fields.keyword.0: "keyword" } + +--- +"rrf retriever with multiple standard retrievers and multiple knn retriever and a filter": + + - do: + search: + index: test + body: + track_total_hits: true + fields: [ "text", "keyword" ] + retriever: + rrf: + filter: [ + { + term: { + keyword: "keyword" + } + } + ] + retrievers: [ + { + knn: { + field: vector, + query_vector: [ 0.0 ], + k: 3, + num_candidates: 3 + } + }, + { + knn: { + field: vector, + query_vector: [ 1.0 ], + k: 3, + num_candidates: 3 + } + }, + { + "standard": { + "query": { + "term": { + "text": "term" + } + } + } + }, + { + "standard": { + "query": { + "match": { + "keyword": "keyword" + } + } + } + } + ] + + - match: { hits.total.value : 1 } + - length: { hits.hits: 1 } + + - match: { hits.hits.0._id: "3" } + - match: { hits.hits.0._rank: 1 } + - match: { hits.hits.0.fields.text.0: "term" } + - match: { hits.hits.0.fields.keyword.0: "keyword" } diff --git a/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/rrf/400_rrf_retriever_script.yml b/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/rrf/400_rrf_retriever_script.yml new file mode 100644 index 0000000000000..90edcfbffd2b6 --- /dev/null +++ b/x-pack/plugin/rank-rrf/src/yamlRestTest/resources/rest-api-spec/test/rrf/400_rrf_retriever_script.yml @@ -0,0 +1,342 @@ +setup: + - skip: + features: close_to + version: ' - 8.13.99' + reason: 'rrf retriever added in 8.14' + + - do: + indices.create: + index: test + body: + settings: + number_of_shards: 5 + number_of_replicas: 0 + mappings: + properties: + vector_asc: + type: dense_vector + dims: 1 + index: true + similarity: l2_norm + vector_desc: + type: dense_vector + dims: 1 + index: true + similarity: l2_norm + int: + type: integer + text: + type: text + + - do: + bulk: + index: test + refresh: true + body: | + { "index": {"_id" : "1"} } + { "vector_asc": [1.0], "vector_desc": [11.0], "int": 1, "text": "term 1" } + { "index": {"_id" : "2"} } + { "vector_asc": [2.0], "vector_desc": [10.0], "int": 2, "text": "term 2" } + { "index": {"_id" : "3"} } + { "vector_asc": [3.0], "vector_desc": [9.0], "int": 3, "text": "term 3" } + { "index": {"_id" : "4"} } + { "vector_asc": [4.0], "vector_desc": [8.0], "int": 1, "text": "term 4" } + { "index": {"_id" : "5"} } + { "vector_asc": [5.0], "vector_desc": [7.0], "int": 2, "text": "term 5" } + { "index": {"_id" : "6"} } + { "vector_asc": [6.0], "vector_desc": [6.0], "int": 3, "text": "term 6" } + { "index": {"_id" : "7"} } + { "vector_asc": [7.0], "vector_desc": [5.0], "int": 1, "text": "term 7" } + { "index": {"_id" : "8"} } + { "vector_asc": [8.0], "vector_desc": [4.0], "int": 2, "text": "term 8" } + { "index": {"_id" : "9"} } + { "vector_asc": [9.0], "vector_desc": [3.0], "int": 3, "text": "term 9" } + { "index": {"_id" : "10"} } + { "vector_asc": [10.0], "vector_desc": [2.0], "int": 1, "text": "term 10" } + { "index": {"_id" : "11"} } + { "vector_asc": [11.0], "vector_desc": [1.0], "int": 2, "text": "term 11" } + +--- +"rrf retriever using a knn retriever and a standard retriever with a scripted metric aggregation": + + - do: + search: + index: test + body: + size: 5 + track_total_hits: true + fields: [ "text" ] + retriever: + rrf: + retrievers: [ + { + knn: { + field: vector_asc, + query_vector: [ 5.0 ], + k: 5, + num_candidates: 11 + } + }, + { + "standard": { + query: { + bool: { + should: [ + { + term: { + text: { + value: "6", + boost: 10.0 + } + } + }, + { + term: { + text: { + value: "5", + boost: 7.0 + } + } + }, + { + term: { + text: { + value: "7", + boost: 7.0 + } + } + }, + { + term: { + text: { + value: "4", + boost: 3.0 + } + } + }, + { + term: { + text: { + value: "3", + boost: 2.0 + } + } + } + ] + } + } + } + } + ] + window_size: 100 + rank_constant: 1 + aggs: + sums: + scripted_metric: + init_script: | + state['sums'] = ['asc': [], 'text': []] + map_script: | + state['sums']['asc'].add($('vector_asc', null).getVector()[0]); + state['sums']['text'].add(Integer.parseInt($('text', null).substring(5))); + combine_script: | + [ + 'asc_total': state['sums']['asc'].stream().mapToDouble(v -> v).sum(), + 'text_total': state['sums']['text'].stream().mapToInt(v -> v).sum() + ] + reduce_script: | + [ + 'asc_total': states.stream().mapToDouble(v -> v['asc_total']).sum(), + 'text_total': states.stream().mapToInt(v -> v['text_total']).sum() + ] + + - match: { hits.hits.0._id: "5" } + - match: { hits.hits.0._rank: 1 } + + - match: { hits.hits.1._id: "6" } + - match: { hits.hits.1._rank: 2 } + + - match: { hits.hits.2._id: "4" } + - match: { hits.hits.2._rank: 3 } + + - match: { hits.hits.3._id: "7" } + - match: { hits.hits.3._rank: 4 } + + - match: { hits.hits.4._id: "3" } + - match: { hits.hits.4._rank: 5 } + + - close_to: { aggregations.sums.value.asc_total: { value: 25.0, error: 0.001 }} + - match: { aggregations.sums.value.text_total: 25 } + +--- +"rrf retriever using multiple knn retrievers with a scripted metric aggregation": + + - do: + search: + index: test + body: + size: 1 + track_total_hits: true + fields: [ "text" ] + retriever: + rrf: + retrievers: [ + { + knn: { + field: vector_asc, + query_vector: [ 6.0 ], + k: 5, + num_candidates: 11 + } + }, + { + knn: { + field: vector_desc, + query_vector: [ 8.0 ], + k: 3, + num_candidates: 11 + } + } + ] + window_size: 3 + rank_constant: 1 + aggs: + sums: + scripted_metric: + init_script: | + state['sums'] = ['asc': [], 'desc': []] + map_script: | + state['sums']['asc'].add($('vector_asc', null).getVector()[0]); + state['sums']['desc'].add($('vector_desc', null).getVector()[0]) + combine_script: | + [ + 'asc_total': state['sums']['asc'].stream().mapToDouble(v -> v).sum(), + 'desc_total': state['sums']['desc'].stream().mapToDouble(v -> v).sum() + ] + reduce_script: | + [ + 'asc_total': states.stream().mapToDouble(v -> v['asc_total']).sum(), + 'desc_total': states.stream().mapToDouble(v -> v['desc_total']).sum() + ] + + - match: { hits.total.value: 6 } + + - match: { hits.hits.0._id: "5" } + - match: { hits.hits.0._rank: 1 } + + - close_to: { aggregations.sums.value.asc_total: { value: 33.0, error: 0.001 }} + - close_to: { aggregations.sums.value.desc_total: { value: 39.0, error: 0.001 }} + +--- +"rrf retriever using multiple knn retrievers and a standard retriever with a scripted metric aggregation": + + - do: + search: + index: test + body: + size: 5 + track_total_hits: true + fields: [ "text" ] + retriever: + rrf: + retrievers: [ + { + knn: { + field: vector_asc, + query_vector: [ 6.0 ], + k: 5, + num_candidates: 11 + } + }, + { + knn: { + field: vector_desc, + query_vector: [ 6.0 ], + k: 5, + num_candidates: 11 + } + }, + { + standard: { + query: { + bool: { + should: [ + { + term: { + text: { + value: "6", + boost: 10.0 + } + } + }, + { + term: { + text: { + value: "5", + boost: 7.0 + } + } + }, + { + term: { + text: { + value: "7", + boost: 7.0 + } + } + }, + { + term: { + text: { + value: "4", + boost: 3.0 + } + } + } + ] + } + } + } + } + ] + window_size: 100 + rank_constant: 1 + aggs: + sums: + scripted_metric: + init_script: | + state['sums'] = ['asc': [], 'desc': [], 'text': []] + map_script: | + state['sums']['asc'].add($('vector_asc', null).getVector()[0]); + state['sums']['desc'].add($('vector_asc', null).getVector()[0]); + state['sums']['text'].add(Integer.parseInt($('text', null).substring(5))); + combine_script: | + [ + 'asc_total': state['sums']['asc'].stream().mapToDouble(v -> v).sum(), + 'desc_total': state['sums']['asc'].stream().mapToDouble(v -> v).sum(), + 'text_total': state['sums']['text'].stream().mapToInt(v -> v).sum() + ] + reduce_script: | + [ + 'asc_total': states.stream().mapToDouble(v -> v['asc_total']).sum(), + 'desc_total': states.stream().mapToDouble(v -> v['desc_total']).sum(), + 'text_total': states.stream().mapToInt(v -> v['text_total']).sum() + ] + + - match: { hits.hits.0._id: "6" } + - match: { hits.hits.0._rank: 1 } + + - match: { hits.hits.1._id: "5" } + - match: { hits.hits.1._rank: 2 } + + - match: { hits.hits.2._id: "7" } + - match: { hits.hits.2._rank: 3 } + + - match: { hits.hits.3._id: "4" } + - match: { hits.hits.3._rank: 4 } + + - match: { hits.hits.4._id: "8" } + - match: { hits.hits.4._rank: 5 } + + - close_to: { aggregations.sums.value.asc_total: { value: 30.0, error: 0.001 }} + - close_to: { aggregations.sums.value.desc_total: { value: 30.0, error: 0.001 }} + - match: { aggregations.sums.value.text_total: 30 } From ff8650a864019274040f2d0d4af31e18fd77abd9 Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Tue, 12 Mar 2024 18:18:24 +0100 Subject: [PATCH 13/34] Support MatchType in EnrichExec (#106251) In preparation for supporting more ENRICH match types, this change includes the TransportVersion changes required for serialization support. Co-authored-by: Nhat Nguyen --- .../org/elasticsearch/TransportVersions.java | 1 + .../xpack/esql/io/stream/PlanNamedTypes.java | 18 +++++++++++++++++- .../xpack/esql/plan/physical/EnrichExec.java | 13 +++++++++++-- .../esql/planner/LocalExecutionPlanner.java | 2 +- .../xpack/esql/planner/Mapper.java | 1 + 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 418720284eda8..6ac2c24739805 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -142,6 +142,7 @@ static TransportVersion def(int id) { public static final TransportVersion ESQL_SERIALIZE_ARRAY_BLOCK = def(8_602_00_0); public static final TransportVersion ADD_DATA_STREAM_GLOBAL_RETENTION = def(8_603_00_0); public static final TransportVersion ALLOCATION_STATS = def(8_604_00_0); + public static final TransportVersion ESQL_EXTENDED_ENRICH_TYPES = def(8_605_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java index 384bfd164b0a7..515d6cb5c92b3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java @@ -512,6 +512,9 @@ static EnrichExec readEnrichExec(PlanStreamInput in) throws IOException { final PhysicalPlan child = in.readPhysicalPlanNode(); final NamedExpression matchField = in.readNamedExpression(); final String policyName = in.readString(); + final String matchType = (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_EXTENDED_ENRICH_TYPES)) + ? in.readString() + : "match"; final String policyMatchField = in.readString(); final Map concreteIndices; final Enrich.Mode mode; @@ -526,7 +529,17 @@ static EnrichExec readEnrichExec(PlanStreamInput in) throws IOException { } concreteIndices = Map.of(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY, Iterables.get(esIndex.concreteIndices(), 0)); } - return new EnrichExec(source, child, mode, matchField, policyName, policyMatchField, concreteIndices, readNamedExpressions(in)); + return new EnrichExec( + source, + child, + mode, + matchType, + matchField, + policyName, + policyMatchField, + concreteIndices, + readNamedExpressions(in) + ); } static void writeEnrichExec(PlanStreamOutput out, EnrichExec enrich) throws IOException { @@ -534,6 +547,9 @@ static void writeEnrichExec(PlanStreamOutput out, EnrichExec enrich) throws IOEx out.writePhysicalPlanNode(enrich.child()); out.writeNamedExpression(enrich.matchField()); out.writeString(enrich.policyName()); + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_EXTENDED_ENRICH_TYPES)) { + out.writeString(enrich.matchType()); + } out.writeString(enrich.policyMatchField()); if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_MULTI_CLUSTERS_ENRICH)) { out.writeEnum(enrich.mode()); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/EnrichExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/EnrichExec.java index 0bfaa2db2be5d..b803d0c20d9de 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/EnrichExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/EnrichExec.java @@ -21,6 +21,7 @@ public class EnrichExec extends UnaryExec implements EstimatesRowSize { private final Enrich.Mode mode; + private final String matchType; private final NamedExpression matchField; private final String policyName; private final String policyMatchField; @@ -41,6 +42,7 @@ public EnrichExec( Source source, PhysicalPlan child, Enrich.Mode mode, + String matchType, NamedExpression matchField, String policyName, String policyMatchField, @@ -49,6 +51,7 @@ public EnrichExec( ) { super(source, child); this.mode = mode; + this.matchType = matchType; this.matchField = matchField; this.policyName = policyName; this.policyMatchField = policyMatchField; @@ -63,6 +66,7 @@ protected NodeInfo info() { EnrichExec::new, child(), mode, + matchType, matchField, policyName, policyMatchField, @@ -73,13 +77,17 @@ protected NodeInfo info() { @Override public EnrichExec replaceChild(PhysicalPlan newChild) { - return new EnrichExec(source(), newChild, mode, matchField, policyName, policyMatchField, concreteIndices, enrichFields); + return new EnrichExec(source(), newChild, mode, matchType, matchField, policyName, policyMatchField, concreteIndices, enrichFields); } public Enrich.Mode mode() { return mode; } + public String matchType() { + return matchType; + } + public NamedExpression matchField() { return matchField; } @@ -118,6 +126,7 @@ public boolean equals(Object o) { if (super.equals(o) == false) return false; EnrichExec that = (EnrichExec) o; return mode.equals(that.mode) + && Objects.equals(matchType, that.matchType) && Objects.equals(matchField, that.matchField) && Objects.equals(policyName, that.policyName) && Objects.equals(policyMatchField, that.policyMatchField) @@ -127,6 +136,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), mode, matchField, policyName, policyMatchField, concreteIndices, enrichFields); + return Objects.hash(super.hashCode(), mode, matchType, matchField, policyName, policyMatchField, concreteIndices, enrichFields); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java index d7d2e99426a97..af66a1ea069aa 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java @@ -468,7 +468,7 @@ private PhysicalOperation planEnrich(EnrichExec enrich, LocalExecutionPlannerCon source.layout.get(enrich.matchField().id()).channel(), enrichLookupService, enrichIndex, - "match", // TODO: enrich should also resolve the match_type + enrich.matchType(), enrich.policyMatchField(), enrich.enrichFields() ), diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/Mapper.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/Mapper.java index e9fb028d7c520..fd0801d35958d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/Mapper.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/Mapper.java @@ -149,6 +149,7 @@ private PhysicalPlan map(UnaryPlan p, PhysicalPlan child) { enrich.source(), child, enrich.mode(), + enrich.policy().getType(), enrich.matchField(), BytesRefs.toString(enrich.policyName().fold()), enrich.policy().getMatchField(), From c33955e4907c9258ae5ddd09a4fdb24eebb8871a Mon Sep 17 00:00:00 2001 From: David Kyle Date: Tue, 12 Mar 2024 17:19:59 +0000 Subject: [PATCH 14/34] [ML] Make task settings optional when creating Cohere embedding models (#106241) --- .../services/cohere/CohereService.java | 3 +- .../services/cohere/CohereServiceTests.java | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java index 172a71bd45434..4f476e60ee2db 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java @@ -36,6 +36,7 @@ import static org.elasticsearch.xpack.inference.services.ServiceUtils.createInvalidModelException; import static org.elasticsearch.xpack.inference.services.ServiceUtils.parsePersistedConfigErrorMsg; +import static org.elasticsearch.xpack.inference.services.ServiceUtils.removeFromMapOrDefaultEmpty; import static org.elasticsearch.xpack.inference.services.ServiceUtils.removeFromMapOrThrowIfNull; import static org.elasticsearch.xpack.inference.services.ServiceUtils.throwIfNotEmptyMap; @@ -61,7 +62,7 @@ public void parseRequestConfig( ) { try { Map serviceSettingsMap = removeFromMapOrThrowIfNull(config, ModelConfigurations.SERVICE_SETTINGS); - Map taskSettingsMap = removeFromMapOrThrowIfNull(config, ModelConfigurations.TASK_SETTINGS); + Map taskSettingsMap = removeFromMapOrDefaultEmpty(config, ModelConfigurations.TASK_SETTINGS); CohereModel model = createModel( inferenceEntityId, diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java index 356da0ece08af..dae4c20d00d78 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java @@ -123,6 +123,34 @@ public void testParseRequestConfig_CreatesACohereEmbeddingsModel() throws IOExce } } + public void testParseRequestConfig_OptionalTaskSettings() throws IOException { + try (var service = createCohereService()) { + + ActionListener modelListener = ActionListener.wrap(model -> { + MatcherAssert.assertThat(model, instanceOf(CohereEmbeddingsModel.class)); + + var embeddingsModel = (CohereEmbeddingsModel) model; + MatcherAssert.assertThat(embeddingsModel.getServiceSettings().getCommonSettings().getUri().toString(), is("url")); + MatcherAssert.assertThat(embeddingsModel.getServiceSettings().getCommonSettings().getModelId(), is("model")); + MatcherAssert.assertThat(embeddingsModel.getServiceSettings().getEmbeddingType(), is(CohereEmbeddingType.FLOAT)); + MatcherAssert.assertThat(embeddingsModel.getTaskSettings(), equalTo(CohereEmbeddingsTaskSettings.EMPTY_SETTINGS)); + MatcherAssert.assertThat(embeddingsModel.getSecretSettings().apiKey().toString(), is("secret")); + }, e -> fail("Model parsing should have succeeded " + e.getMessage())); + + service.parseRequestConfig( + "id", + TaskType.TEXT_EMBEDDING, + getRequestConfigMap( + CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", "model", CohereEmbeddingType.FLOAT), + getSecretSettingsMap("secret") + ), + Set.of(), + modelListener + ); + + } + } + public void testParseRequestConfig_ThrowsUnsupportedModelType() throws IOException { try (var service = createCohereService()) { var failureListener = getModelListenerForException( @@ -955,6 +983,14 @@ private Map getRequestConfigMap( ); } + private Map getRequestConfigMap(Map serviceSettings, Map secretSettings) { + var builtServiceSettings = new HashMap<>(); + builtServiceSettings.putAll(serviceSettings); + builtServiceSettings.putAll(secretSettings); + + return new HashMap<>(Map.of(ModelConfigurations.SERVICE_SETTINGS, builtServiceSettings)); + } + private CohereService createCohereService() { return new CohereService(mock(HttpRequestSender.Factory.class), createWithEmptySettings(threadPool)); } From 9b16b5044c040f51a345ac209cfd1ba718abf24d Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 12 Mar 2024 10:26:55 -0700 Subject: [PATCH 15/34] Allow running node-level reduction (#106204) This PR executes an empty reduction plan on data nodes at the node-level. With this change, we have the capability to replace this empty plan with an actual reduction plan later. Relates #99498 --- .../operator/exchange/ExchangeService.java | 2 +- .../xpack/esql/action/EsqlActionTaskIT.java | 15 ++- .../xpack/esql/plugin/ComputeService.java | 103 +++++++++++++++--- 3 files changed, 100 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeService.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeService.java index 8065ac6f3086e..efb646daec0e5 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeService.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeService.java @@ -91,7 +91,7 @@ public void registerTransportHandler(TransportService transportService) { * * @throws IllegalStateException if a sink handler for the given id already exists */ - ExchangeSinkHandler createSinkHandler(String exchangeId, int maxBufferSize) { + public ExchangeSinkHandler createSinkHandler(String exchangeId, int maxBufferSize) { ExchangeSinkHandler sinkHandler = new ExchangeSinkHandler(blockFactory, maxBufferSize, threadPool::relativeTimeInMillis); if (sinks.putIfAbsent(exchangeId, sinkHandler) != null) { throw new IllegalStateException("sink exchanger for id [" + exchangeId + "] already exists"); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionTaskIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionTaskIT.java index 60f174773a1b8..5d022cd25cdab 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionTaskIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionTaskIT.java @@ -35,6 +35,7 @@ import static org.elasticsearch.test.MapMatcher.assertMap; import static org.elasticsearch.test.MapMatcher.matchesMap; +import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.emptyIterable; @@ -58,6 +59,7 @@ public class EsqlActionTaskIT extends AbstractPausableIntegTestCase { private String READ_DESCRIPTION; private String MERGE_DESCRIPTION; + private String REDUCE_DESCRIPTION; @Before public void setup() { @@ -73,6 +75,9 @@ public void setup() { \\_ProjectOperator[projection = [0]] \\_LimitOperator[limit = 1000] \\_OutputOperator[columns = [sum(pause_me)]]"""; + REDUCE_DESCRIPTION = """ + \\_ExchangeSourceOperator[] + \\_ExchangeSinkOperator"""; } public void testTaskContents() throws Exception { @@ -136,7 +141,7 @@ public void testTaskContents() throws Exception { assertThat(luceneSources, greaterThanOrEqualTo(1)); assertThat(valuesSourceReaders, equalTo(1)); assertThat(exchangeSinks, greaterThanOrEqualTo(1)); - assertThat(exchangeSources, equalTo(1)); + assertThat(exchangeSources, equalTo(2)); } finally { scriptPermits.release(numberOfDocs()); try (EsqlQueryResponse esqlResponse = response.get()) { @@ -233,12 +238,12 @@ private List getTasksStarting() throws Exception { .setDetailed(true) .get() .getTasks(); - assertThat(tasks, hasSize(equalTo(2))); + assertThat(tasks, hasSize(equalTo(3))); for (TaskInfo task : tasks) { assertThat(task.action(), equalTo(DriverTaskRunner.ACTION_NAME)); DriverStatus status = (DriverStatus) task.status(); logger.info("task {} {}", task.description(), status); - assertThat(task.description(), either(equalTo(READ_DESCRIPTION)).or(equalTo(MERGE_DESCRIPTION))); + assertThat(task.description(), anyOf(equalTo(READ_DESCRIPTION), equalTo(MERGE_DESCRIPTION), equalTo(REDUCE_DESCRIPTION))); /* * Accept tasks that are either starting or have gone * immediately async. The coordinating task is likely @@ -265,11 +270,11 @@ private List getTasksRunning() throws Exception { .setDetailed(true) .get() .getTasks(); - assertThat(tasks, hasSize(equalTo(2))); + assertThat(tasks, hasSize(equalTo(3))); for (TaskInfo task : tasks) { assertThat(task.action(), equalTo(DriverTaskRunner.ACTION_NAME)); DriverStatus status = (DriverStatus) task.status(); - assertThat(task.description(), either(equalTo(READ_DESCRIPTION)).or(equalTo(MERGE_DESCRIPTION))); + assertThat(task.description(), anyOf(equalTo(READ_DESCRIPTION), equalTo(MERGE_DESCRIPTION), equalTo(REDUCE_DESCRIPTION))); if (task.description().equals(READ_DESCRIPTION)) { assertThat(status.status(), equalTo(DriverStatus.Status.RUNNING)); } else { 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 7af37a3eeb114..ba3d8564e1334 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 @@ -592,7 +592,7 @@ private class DataNodeRequestExecutor { private final DataNodeRequest request; private final CancellableTask parentTask; private final ExchangeSinkHandler exchangeSink; - private final ActionListener listener; + private final ActionListener listener; private final List driverProfiles; private final int maxConcurrentShards; private final ExchangeSink blockingSink; // block until we have completed on all shards or the coordinator has enough data @@ -602,13 +602,14 @@ private class DataNodeRequestExecutor { CancellableTask parentTask, ExchangeSinkHandler exchangeSink, int maxConcurrentShards, - ActionListener listener + List driverProfiles, + ActionListener listener ) { this.request = request; this.parentTask = parentTask; this.exchangeSink = exchangeSink; this.listener = listener; - this.driverProfiles = request.configuration().profile() ? Collections.synchronizedList(new ArrayList<>()) : List.of(); + this.driverProfiles = driverProfiles; this.maxConcurrentShards = maxConcurrentShards; this.blockingSink = exchangeSink.createExchangeSink(); } @@ -649,10 +650,7 @@ private void onBatchCompleted(int lastBatchIndex, List batchProfi // don't return until all pages are fetched exchangeSink.addCompletionListener( ContextPreservingActionListener.wrapPreservingContext( - ActionListener.runBefore( - listener.map(nullValue -> new ComputeResponse(driverProfiles)), - () -> exchangeService.finishSinkHandler(request.sessionId(), null) - ), + ActionListener.runBefore(listener, () -> exchangeService.finishSinkHandler(request.sessionId(), null)), transportService.getThreadPool().getThreadContext() ) ); @@ -665,17 +663,94 @@ private void onFailure(Exception e) { } } + private void runComputeOnDataNode( + CancellableTask task, + String externalId, + PhysicalPlan reducePlan, + DataNodeRequest request, + ActionListener listener + ) { + final List collectedProfiles = request.configuration().profile() + ? Collections.synchronizedList(new ArrayList<>()) + : List.of(); + final var responseHeadersCollector = new ResponseHeadersCollector(transportService.getThreadPool().getThreadContext()); + listener = ActionListener.runBefore(listener, responseHeadersCollector::finish); + try (RefCountingListener refs = new RefCountingListener(listener.map(i -> new ComputeResponse(collectedProfiles)))) { + final AtomicBoolean cancelled = new AtomicBoolean(); + // run compute with target shards + var internalSink = exchangeService.createSinkHandler(request.sessionId(), request.pragmas().exchangeBufferSize()); + DataNodeRequestExecutor dataNodeRequestExecutor = new DataNodeRequestExecutor( + request, + task, + internalSink, + request.configuration().pragmas().maxConcurrentShardsPerNode(), + collectedProfiles, + ActionListener.runBefore(cancelOnFailure(task, cancelled, refs.acquire()), responseHeadersCollector::collect) + ); + dataNodeRequestExecutor.start(); + // run the node-level reduction + var externalSink = exchangeService.getSinkHandler(externalId); + var exchangeSource = new ExchangeSourceHandler(1, esqlExecutor); + exchangeSource.addRemoteSink(internalSink::fetchPageAsync, 1); + ActionListener reductionListener = cancelOnFailure(task, cancelled, refs.acquire()); + runCompute( + task, + new ComputeContext( + request.sessionId(), + request.clusterAlias(), + List.of(), + request.configuration(), + exchangeSource, + externalSink + ), + reducePlan, + ActionListener.wrap(driverProfiles -> { + responseHeadersCollector.collect(); + if (request.configuration().profile()) { + collectedProfiles.addAll(driverProfiles); + } + // don't return until all pages are fetched + externalSink.addCompletionListener( + ActionListener.runBefore(reductionListener, () -> exchangeService.finishSinkHandler(externalId, null)) + ); + }, e -> { + exchangeService.finishSinkHandler(externalId, e); + reductionListener.onFailure(e); + }) + ); + } catch (Exception e) { + exchangeService.finishSinkHandler(externalId, e); + exchangeService.finishSinkHandler(request.sessionId(), e); + listener.onFailure(e); + } + } + private class DataNodeRequestHandler implements TransportRequestHandler { @Override public void messageReceived(DataNodeRequest request, TransportChannel channel, Task task) { - DataNodeRequestExecutor executor = new DataNodeRequestExecutor( - request, - (CancellableTask) task, - exchangeService.getSinkHandler(request.sessionId()), - request.configuration().pragmas().maxConcurrentShardsPerNode(), - new ChannelActionListener<>(channel) + final ActionListener listener = new ChannelActionListener<>(channel); + final ExchangeSinkExec reducePlan; + if (request.plan() instanceof ExchangeSinkExec plan) { + reducePlan = new ExchangeSinkExec( + plan.source(), + plan.output(), + plan.isIntermediateAgg(), + new ExchangeSourceExec(plan.source(), plan.output(), plan.isIntermediateAgg()) + ); + } else { + listener.onFailure(new IllegalStateException("expected exchange sink for a remote compute; got " + request.plan())); + return; + } + final String sessionId = request.sessionId(); + request = new DataNodeRequest( + sessionId + "[n]", // internal session + request.configuration(), + request.clusterAlias(), + request.shardIds(), + request.aliasFilters(), + request.plan() ); - executor.start(); + runComputeOnDataNode((CancellableTask) task, sessionId, reducePlan, request, listener); } } From e14db090db26a3a2f6fc9b2336433f79e92b3efd Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 12 Mar 2024 10:27:57 -0700 Subject: [PATCH 16/34] Support fill existing LongArray with stream input (#106217) Today, we can't use LongArray#readFrom(StreamInput in) in ES|QL because the returned big array is not tracked with the circuit breaker. We can integrate the circuit breaker with ReleasableLongArray; however, we don't know how many bytes we should track: the whole BytesReference or just the slice. This PR adds an alternative method, where we create a big array manually, then fill it with bytes from a stream input. If we are okay with this approach, I can make similar changes to other classes. --- .../elasticsearch/common/util/BigArrays.java | 6 ++++++ .../elasticsearch/common/util/BigLongArray.java | 16 ++++++++++++++++ .../elasticsearch/common/util/LongArray.java | 5 +++++ .../common/util/ReleasableLongArray.java | 5 +++++ .../common/util/BigArraysTests.java | 17 +++++++++++++++++ .../common/util/MockBigArrays.java | 5 +++++ 6 files changed, 54 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/common/util/BigArrays.java b/server/src/main/java/org/elasticsearch/common/util/BigArrays.java index d1367f41d9d87..36451932edc1a 100644 --- a/server/src/main/java/org/elasticsearch/common/util/BigArrays.java +++ b/server/src/main/java/org/elasticsearch/common/util/BigArrays.java @@ -298,6 +298,12 @@ public void writeTo(StreamOutput out) throws IOException { out.writeVInt(size); out.write(array, 0, size); } + + @Override + public void fillWith(StreamInput in) throws IOException { + int len = in.readVInt(); + in.readBytes(array, 0, len); + } } private static class ByteArrayAsDoubleArrayWrapper extends AbstractArrayWrapper implements DoubleArray { diff --git a/server/src/main/java/org/elasticsearch/common/util/BigLongArray.java b/server/src/main/java/org/elasticsearch/common/util/BigLongArray.java index 1044b5fb78ee4..0e0abf812b248 100644 --- a/server/src/main/java/org/elasticsearch/common/util/BigLongArray.java +++ b/server/src/main/java/org/elasticsearch/common/util/BigLongArray.java @@ -10,6 +10,7 @@ import org.apache.lucene.util.ArrayUtil; import org.apache.lucene.util.RamUsageEstimator; +import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import java.io.IOException; @@ -134,6 +135,21 @@ public void writeTo(StreamOutput out) throws IOException { writePages(out, Math.toIntExact(size), pages, Long.BYTES, LONG_PAGE_SIZE); } + @Override + public void fillWith(StreamInput in) throws IOException { + readPages(in, pages); + } + + static void readPages(StreamInput in, byte[][] pages) throws IOException { + int remained = in.readVInt(); + for (int i = 0; i < pages.length - 1; i++) { + int len = pages[0].length; + in.readBytes(pages[i], 0, len); + remained -= len; + } + in.readBytes(pages[pages.length - 1], 0, remained); + } + static void writePages(StreamOutput out, int size, byte[][] pages, int bytesPerValue, int pageSize) throws IOException { out.writeVInt(size * bytesPerValue); int lastPageEnd = size % pageSize; diff --git a/server/src/main/java/org/elasticsearch/common/util/LongArray.java b/server/src/main/java/org/elasticsearch/common/util/LongArray.java index bd293a1356406..59321d1957f4d 100644 --- a/server/src/main/java/org/elasticsearch/common/util/LongArray.java +++ b/server/src/main/java/org/elasticsearch/common/util/LongArray.java @@ -42,6 +42,11 @@ static LongArray readFrom(StreamInput in) throws IOException { */ void fill(long fromIndex, long toIndex, long value); + /** + * Alternative of {@link #readFrom(StreamInput)} where the written bytes are loaded into an existing {@link LongArray} + */ + void fillWith(StreamInput in) throws IOException; + /** * Bulk set. */ diff --git a/server/src/main/java/org/elasticsearch/common/util/ReleasableLongArray.java b/server/src/main/java/org/elasticsearch/common/util/ReleasableLongArray.java index 44764ea1e1715..2980713e2e652 100644 --- a/server/src/main/java/org/elasticsearch/common/util/ReleasableLongArray.java +++ b/server/src/main/java/org/elasticsearch/common/util/ReleasableLongArray.java @@ -59,6 +59,11 @@ public void fill(long fromIndex, long toIndex, long value) { throw new UnsupportedOperationException(); } + @Override + public void fillWith(StreamInput in) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public void set(long index, byte[] buf, int offset, int len) { throw new UnsupportedOperationException(); diff --git a/server/src/test/java/org/elasticsearch/common/util/BigArraysTests.java b/server/src/test/java/org/elasticsearch/common/util/BigArraysTests.java index 946effda16a76..3512a50d5578c 100644 --- a/server/src/test/java/org/elasticsearch/common/util/BigArraysTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/BigArraysTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.breaker.CircuitBreakingException; import org.elasticsearch.common.breaker.PreallocatedCircuitBreakerService; import org.elasticsearch.common.io.stream.ByteArrayStreamInput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; @@ -245,6 +246,22 @@ public void testLongArrayFill() { array2.close(); } + public void testSerializeLongArray() throws Exception { + final int len = randomIntBetween(1, 1000_000); + final LongArray array1 = bigArrays.newLongArray(len, randomBoolean()); + for (int i = 0; i < len; ++i) { + array1.set(i, randomLong()); + } + BytesStreamOutput out = new BytesStreamOutput(); + array1.writeTo(out); + final LongArray array2 = bigArrays.newLongArray(len, randomBoolean()); + array2.fillWith(out.bytes().streamInput()); + for (int i = 0; i < len; i++) { + assertThat(array2.get(i), equalTo(array1.get(i))); + } + Releasables.close(array1, array2); + } + public void testByteArrayBulkGet() { final byte[] array1 = new byte[randomIntBetween(1, 4000000)]; random().nextBytes(array1); diff --git a/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java b/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java index 902e089679f49..8ed9073d45625 100644 --- a/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java +++ b/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java @@ -539,6 +539,11 @@ public Collection getChildResources() { public void writeTo(StreamOutput out) throws IOException { in.writeTo(out); } + + @Override + public void fillWith(StreamInput streamInput) throws IOException { + in.fillWith(streamInput); + } } private class FloatArrayWrapper extends AbstractArrayWrapper implements FloatArray { From eea35742b7566c2e9c9dc3c6b81ab457b3592119 Mon Sep 17 00:00:00 2001 From: Mark Tozzi Date: Tue, 12 Mar 2024 13:52:26 -0400 Subject: [PATCH 17/34] [ESQL] Copy BinaryComparisons Translation logic into esql (#105711) Bbuilding on the work @alex-spies did in #105230 to start to decouple the ESQL translation layer from the QL translation layer. I did this by straight up copying the QL binary comparison code from ExpressionTranslators.BinaryComparisons to EsqlExpressionTranslators.BinaryComparisons. --- .../planner/EsqlExpressionTranslators.java | 136 ++++++++++++++++-- 1 file changed, 122 insertions(+), 14 deletions(-) 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 8ba8efac981af..33f8b4a5eddef 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 @@ -9,8 +9,17 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.Equals; +import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.GreaterThan; +import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.GreaterThanOrEqual; import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.InsensitiveEquals; +import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.LessThan; +import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.LessThanOrEqual; +import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.NotEquals; import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.NullEquals; import org.elasticsearch.xpack.ql.QlIllegalArgumentException; import org.elasticsearch.xpack.ql.expression.Expression; import org.elasticsearch.xpack.ql.expression.Expressions; @@ -18,32 +27,33 @@ import org.elasticsearch.xpack.ql.expression.TypedAttribute; import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.BinaryComparison; -import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.Equals; -import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.GreaterThan; -import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.GreaterThanOrEqual; -import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.LessThan; -import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.LessThanOrEqual; -import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NotEquals; -import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NullEquals; import org.elasticsearch.xpack.ql.planner.ExpressionTranslator; import org.elasticsearch.xpack.ql.planner.ExpressionTranslators; import org.elasticsearch.xpack.ql.planner.TranslatorHandler; import org.elasticsearch.xpack.ql.querydsl.query.MatchAll; +import org.elasticsearch.xpack.ql.querydsl.query.NotQuery; import org.elasticsearch.xpack.ql.querydsl.query.Query; +import org.elasticsearch.xpack.ql.querydsl.query.RangeQuery; import org.elasticsearch.xpack.ql.querydsl.query.TermQuery; import org.elasticsearch.xpack.ql.querydsl.query.TermsQuery; import org.elasticsearch.xpack.ql.tree.Source; import org.elasticsearch.xpack.ql.type.DataType; import org.elasticsearch.xpack.ql.type.DataTypes; import org.elasticsearch.xpack.ql.util.Check; +import org.elasticsearch.xpack.versionfield.Version; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.OffsetTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import static org.elasticsearch.xpack.ql.type.DataTypes.IP; import static org.elasticsearch.xpack.ql.type.DataTypes.UNSIGNED_LONG; +import static org.elasticsearch.xpack.ql.type.DataTypes.VERSION; import static org.elasticsearch.xpack.ql.util.NumericUtils.unsignedLongAsNumber; public final class EsqlExpressionTranslators { @@ -108,18 +118,116 @@ static Query translate(InsensitiveEquals bc) { } } + /** + * This class is responsible for pushing the ES|QL Binary Comparison operators into Lucene. It covers: + *
        + *
      • {@link Equals}
      • + *
      • {@link NotEquals}
      • + *
      • {@link NullEquals}
      • + *
      • {@link GreaterThanOrEqual}
      • + *
      • {@link GreaterThan}
      • + *
      • {@link LessThanOrEqual}
      • + *
      • {@link LessThan}
      • + *
      + * + * In general, we are able to push these down when one of the arguments is a constant (i.e. is foldable). This class assumes + * that an earlier pass through the query has rearranged things so that the foldable value will be the right hand side + * input to the operation. + */ public static class BinaryComparisons extends ExpressionTranslator { @Override protected Query asQuery(BinaryComparison bc, TranslatorHandler handler) { - return doTranslate(bc, handler); - } - - public static Query doTranslate(BinaryComparison bc, TranslatorHandler handler) { + // TODO: Pretty sure this check is redundant with the one at the beginning of translate ExpressionTranslators.BinaryComparisons.checkBinaryComparison(bc); Query translated = translateOutOfRangeComparisons(bc); - return translated == null - ? ExpressionTranslators.BinaryComparisons.doTranslate(bc, handler) - : handler.wrapFunctionQuery(bc, bc.left(), () -> translated); + if (translated != null) { + return handler.wrapFunctionQuery(bc, bc.left(), () -> translated); + } + return handler.wrapFunctionQuery(bc, bc.left(), () -> translate(bc, handler)); + } + + static Query translate(BinaryComparison bc, TranslatorHandler handler) { + Check.isTrue( + bc.right().foldable(), + "Line {}:{}: Comparisons against fields are not (currently) supported; offender [{}] in [{}]", + bc.right().sourceLocation().getLineNumber(), + bc.right().sourceLocation().getColumnNumber(), + Expressions.name(bc.right()), + bc.symbol() + ); + TypedAttribute attribute = checkIsPushableAttribute(bc.left()); + Source source = bc.source(); + String name = handler.nameOf(attribute); + Object result = bc.right().fold(); + Object value = result; + String format = null; + boolean isDateLiteralComparison = false; + + // TODO: This type coersion layer is copied directly from the QL counterpart code. It's probably not necessary or desireable + // in the ESQL version. We should instead do the type conversions using our casting functions. + // for a date constant comparison, we need to use a format for the date, to make sure that the format is the same + // no matter the timezone provided by the user + if (value instanceof ZonedDateTime || value instanceof OffsetTime) { + DateFormatter formatter; + if (value instanceof ZonedDateTime) { + formatter = DateFormatter.forPattern("strict_date_optional_time_nanos"); + // RangeQueryBuilder accepts an Object as its parameter, but it will call .toString() on the ZonedDateTime instance + // which can have a slightly different format depending on the ZoneId used to create the ZonedDateTime + // Since RangeQueryBuilder can handle date as String as well, we'll format it as String and provide the format as well. + value = formatter.format((ZonedDateTime) value); + } else { + formatter = DateFormatter.forPattern("strict_hour_minute_second_fraction"); + value = formatter.format((OffsetTime) value); + } + format = formatter.pattern(); + isDateLiteralComparison = true; + } else if (attribute.dataType() == IP && value instanceof BytesRef bytesRef) { + value = DocValueFormat.IP.format(bytesRef); + } else if (attribute.dataType() == VERSION) { + // VersionStringFieldMapper#indexedValueForSearch() only accepts as input String or BytesRef with the String (i.e. not + // encoded) representation of the version as it'll do the encoding itself. + if (value instanceof BytesRef bytesRef) { + value = new Version(bytesRef).toString(); + } else if (value instanceof Version version) { + value = version.toString(); + } + } else if (attribute.dataType() == UNSIGNED_LONG && value instanceof Long ul) { + value = unsignedLongAsNumber(ul); + } + + ZoneId zoneId = null; + if (DataTypes.isDateTime(attribute.dataType())) { + zoneId = bc.zoneId(); + } + if (bc instanceof GreaterThan) { + return new RangeQuery(source, name, value, false, null, false, format, zoneId); + } + if (bc instanceof GreaterThanOrEqual) { + return new RangeQuery(source, name, value, true, null, false, format, zoneId); + } + if (bc instanceof LessThan) { + return new RangeQuery(source, name, null, false, value, false, format, zoneId); + } + if (bc instanceof LessThanOrEqual) { + return new RangeQuery(source, name, null, false, value, true, format, zoneId); + } + if (bc instanceof Equals || bc instanceof NullEquals || bc instanceof NotEquals) { + name = pushableAttributeName(attribute); + + Query query; + if (isDateLiteralComparison) { + // dates equality uses a range query because it's the one that has a "format" parameter + query = new RangeQuery(source, name, value, true, value, true, format, zoneId); + } else { + query = new TermQuery(source, name, value); + } + if (bc instanceof NotEquals) { + query = new NotQuery(source, query); + } + return query; + } + + throw new QlIllegalArgumentException("Don't know how to translate binary comparison [{}] in [{}]", bc.right().nodeString(), bc); } private static Query translateOutOfRangeComparisons(BinaryComparison bc) { From bef6363649edfde2a8f6061bdf3e20c4c8313a7c Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Tue, 12 Mar 2024 20:19:21 +0100 Subject: [PATCH 18/34] Fix typo in text_expansion example (#106265) --- docs/reference/query-dsl/text-expansion-query.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/query-dsl/text-expansion-query.asciidoc b/docs/reference/query-dsl/text-expansion-query.asciidoc index 927b5d0a85886..27fca2bb56375 100644 --- a/docs/reference/query-dsl/text-expansion-query.asciidoc +++ b/docs/reference/query-dsl/text-expansion-query.asciidoc @@ -253,7 +253,7 @@ GET my-index/_search "pruning_config": { "tokens_freq_ratio_threshold": 5, "tokens_weight_threshold": 0.4, - "only_score_pruned_tokens": false + "only_score_pruned_tokens": true } } } From c3bc2712de49bd84d7dda28bec303d6b5d1c3ca7 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 12 Mar 2024 12:20:16 -0700 Subject: [PATCH 19/34] Mute HeapAttackIT (#106263) Started failing after we introduced the node-level reduction. Tracked at #106262 Relates #106204 --- .../org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java b/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java index 8c87ef5977114..ffa817ed09677 100644 --- a/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java +++ b/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java @@ -10,6 +10,7 @@ import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.util.EntityUtils; +import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; @@ -57,6 +58,7 @@ * Tests that run ESQL queries that have, in the past, used so much memory they * crash Elasticsearch. */ +@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/106262") public class HeapAttackIT extends ESRestTestCase { @ClassRule From 3dc500862c5638fb90068142eac6f6f69423ae1f Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 12 Mar 2024 15:46:04 -0400 Subject: [PATCH 20/34] ESQL: Support partially folding CASE (#106094) This adds support for folding the arms of a `CASE` statement. So `CASE(false, a, b)` becomes `b`. --- docs/changelog/106094.yaml | 5 + .../function/scalar/conditional/Case.java | 56 +++++- .../esql/optimizer/LogicalPlanOptimizer.java | 26 ++- .../scalar/conditional/CaseExtraTests.java | 169 ++++++++++++++++++ .../optimizer/LogicalPlanOptimizerTests.java | 11 ++ 5 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/106094.yaml create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/CaseExtraTests.java diff --git a/docs/changelog/106094.yaml b/docs/changelog/106094.yaml new file mode 100644 index 0000000000000..4341164222338 --- /dev/null +++ b/docs/changelog/106094.yaml @@ -0,0 +1,5 @@ +pr: 106094 +summary: "ESQL: Support partially folding CASE" +area: ES|QL +type: enhancement +issues: [] diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Case.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Case.java index 17e096005fc1f..66756ffa14c60 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Case.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Case.java @@ -88,7 +88,11 @@ public Case( for (int c = 0; c < conditionCount; c++) { conditions.add(new Condition(children().get(c * 2), children().get(c * 2 + 1))); } - elseValue = children().size() % 2 == 0 ? new Literal(source, null, NULL) : children().get(children().size() - 1); + elseValue = elseValueIsExplicit() ? children().get(children().size() - 1) : new Literal(source, null, NULL); + } + + private boolean elseValueIsExplicit() { + return children().size() % 2 == 1; } @Override @@ -175,7 +179,6 @@ public boolean foldable() { @Override public Object fold() { - // TODO can we partially fold? like CASE(false, foo, bar) -> bar for (Condition condition : conditions) { Boolean b = (Boolean) condition.condition.fold(); if (b != null && b) { @@ -185,6 +188,55 @@ public Object fold() { return elseValue.fold(); } + /** + * Fold the arms of {@code CASE} statements. + *
        + *
      1. + * Conditions that evaluate to {@code false} are removed so + * {@code EVAL c=CASE(false, foo, b, bar, bort)} becomes + * {@code EVAL c=CASE(b, bar, bort)}. + *
      2. + *
      3. + * Conditions that evaluate to {@code true} stop evaluation and + * return themselves so {@code EVAL c=CASE(true, foo, bar)} becomes + * {@code EVAL c=foo}. + *
      4. + *
      + * And those two combine so {@code EVAL c=CASE(false, foo, b, bar, true, bort, el)} becomes + * {@code EVAL c=CASE(b, bar, bort)}. + */ + public Expression partiallyFold() { + List newChildren = new ArrayList<>(children().size()); + boolean modified = false; + for (Condition condition : conditions) { + if (condition.condition.foldable() == false) { + newChildren.add(condition.condition); + newChildren.add(condition.value); + continue; + } + modified = true; + Boolean b = (Boolean) condition.condition.fold(); + if (b != null && b) { + newChildren.add(condition.value); + return finishPartialFold(newChildren); + } + } + if (modified == false) { + return this; + } + if (elseValueIsExplicit()) { + newChildren.add(elseValue); + } + return finishPartialFold(newChildren); + } + + private Expression finishPartialFold(List newChildren) { + if (newChildren.size() == 1) { + return newChildren.get(0); + } + return replaceChildren(newChildren); + } + @Override public ExpressionEvaluator.Factory toEvaluator(Function toEvaluator) { ElementType resultType = PlannerUtils.toElementType(dataType()); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java index 7a5e39fea8f95..59f0d46bf618a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java @@ -17,6 +17,7 @@ import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.Equals; import org.elasticsearch.xpack.esql.expression.SurrogateExpression; import org.elasticsearch.xpack.esql.expression.function.aggregate.Count; +import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case; import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In; import org.elasticsearch.xpack.esql.plan.logical.Enrich; @@ -84,6 +85,7 @@ import static org.elasticsearch.xpack.ql.expression.Expressions.asAttributes; import static org.elasticsearch.xpack.ql.optimizer.OptimizerRules.PropagateEquals; import static org.elasticsearch.xpack.ql.optimizer.OptimizerRules.TransformDirection; +import static org.elasticsearch.xpack.ql.optimizer.OptimizerRules.TransformDirection.DOWN; public class LogicalPlanOptimizer extends ParameterizedRuleExecutor { @@ -120,6 +122,7 @@ protected static Batch operators() { new SplitInWithFoldableValue(), new PropagateEvalFoldables(), new ConstantFolding(), + new PartiallyFoldCase(), // boolean new BooleanSimplification(), new LiteralsOnTheRight(), @@ -1586,7 +1589,6 @@ private static LogicalPlan normalize(Aggregate aggregate, AttributeMap newChildren = new ArrayList<>(exp.children()); @@ -1606,4 +1607,25 @@ protected Expression nullify(Expression exp, Expression nullExp) { return Literal.of(exp, null); } } + + /** + * Fold the arms of {@code CASE} statements. + *
      {@code
      +     * EVAL c=CASE(true, foo, bar)
      +     * }
      + * becomes + *
      {@code
      +     * EVAL c=foo
      +     * }
      + */ + static class PartiallyFoldCase extends OptimizerRules.OptimizerExpressionRule { + PartiallyFoldCase() { + super(DOWN); + } + + @Override + protected Expression rule(Case c) { + return c.partiallyFold(); + } + } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/CaseExtraTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/CaseExtraTests.java new file mode 100644 index 0000000000000..19cc49c180802 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/CaseExtraTests.java @@ -0,0 +1,169 @@ +/* + * 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.conditional; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.ql.expression.Literal; +import org.elasticsearch.xpack.ql.tree.Source; +import org.elasticsearch.xpack.ql.type.DataTypes; + +import java.util.List; + +import static org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase.field; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.sameInstance; + +/** + * Extra tests for {@code CASE} that don't fit into the parameterized + * {@link CaseTests}. + */ +public class CaseExtraTests extends ESTestCase { + public void testElseValueExplicit() { + assertThat( + new Case( + Source.synthetic("case"), + field("first_cond", DataTypes.BOOLEAN), + List.of(field("v", DataTypes.LONG), field("e", DataTypes.LONG)) + ).children(), + equalTo(List.of(field("first_cond", DataTypes.BOOLEAN), field("v", DataTypes.LONG), field("e", DataTypes.LONG))) + ); + } + + public void testElseValueImplied() { + assertThat( + new Case(Source.synthetic("case"), field("first_cond", DataTypes.BOOLEAN), List.of(field("v", DataTypes.LONG))).children(), + equalTo(List.of(field("first_cond", DataTypes.BOOLEAN), field("v", DataTypes.LONG))) + ); + } + + public void testPartialFoldDropsFirstFalse() { + Case c = new Case( + Source.synthetic("case"), + new Literal(Source.EMPTY, false, DataTypes.BOOLEAN), + List.of(field("first", DataTypes.LONG), field("last_cond", DataTypes.BOOLEAN), field("last", DataTypes.LONG)) + ); + assertThat(c.foldable(), equalTo(false)); + assertThat( + c.partiallyFold(), + equalTo(new Case(Source.synthetic("case"), field("last_cond", DataTypes.BOOLEAN), List.of(field("last", DataTypes.LONG)))) + ); + } + + public void testPartialFoldNoop() { + Case c = new Case( + Source.synthetic("case"), + field("first_cond", DataTypes.BOOLEAN), + List.of(field("first", DataTypes.LONG), field("last", DataTypes.LONG)) + ); + assertThat(c.foldable(), equalTo(false)); + assertThat(c.partiallyFold(), sameInstance(c)); + } + + public void testPartialFoldFirst() { + Case c = new Case( + Source.synthetic("case"), + new Literal(Source.EMPTY, true, DataTypes.BOOLEAN), + List.of(field("first", DataTypes.LONG), field("last", DataTypes.LONG)) + ); + assertThat(c.foldable(), equalTo(false)); + assertThat(c.partiallyFold(), equalTo(field("first", DataTypes.LONG))); + } + + public void testPartialFoldFirstAfterKeepingUnknown() { + Case c = new Case( + Source.synthetic("case"), + field("keep_me_cond", DataTypes.BOOLEAN), + List.of( + field("keep_me", DataTypes.LONG), + new Literal(Source.EMPTY, true, DataTypes.BOOLEAN), + field("first", DataTypes.LONG), + field("last", DataTypes.LONG) + ) + ); + assertThat(c.foldable(), equalTo(false)); + assertThat( + c.partiallyFold(), + equalTo( + new Case( + Source.synthetic("case"), + field("keep_me_cond", DataTypes.BOOLEAN), + List.of(field("keep_me", DataTypes.LONG), field("first", DataTypes.LONG)) + ) + ) + ); + } + + public void testPartialFoldSecond() { + Case c = new Case( + Source.synthetic("case"), + new Literal(Source.EMPTY, false, DataTypes.BOOLEAN), + List.of( + field("first", DataTypes.LONG), + new Literal(Source.EMPTY, true, DataTypes.BOOLEAN), + field("second", DataTypes.LONG), + field("last", DataTypes.LONG) + ) + ); + assertThat(c.foldable(), equalTo(false)); + assertThat(c.partiallyFold(), equalTo(field("second", DataTypes.LONG))); + } + + public void testPartialFoldSecondAfterDroppingFalse() { + Case c = new Case( + Source.synthetic("case"), + new Literal(Source.EMPTY, false, DataTypes.BOOLEAN), + List.of( + field("first", DataTypes.LONG), + new Literal(Source.EMPTY, true, DataTypes.BOOLEAN), + field("second", DataTypes.LONG), + field("last", DataTypes.LONG) + ) + ); + assertThat(c.foldable(), equalTo(false)); + assertThat(c.partiallyFold(), equalTo(field("second", DataTypes.LONG))); + } + + public void testPartialFoldLast() { + Case c = new Case( + Source.synthetic("case"), + new Literal(Source.EMPTY, false, DataTypes.BOOLEAN), + List.of( + field("first", DataTypes.LONG), + new Literal(Source.EMPTY, false, DataTypes.BOOLEAN), + field("second", DataTypes.LONG), + field("last", DataTypes.LONG) + ) + ); + assertThat(c.foldable(), equalTo(false)); + assertThat(c.partiallyFold(), equalTo(field("last", DataTypes.LONG))); + } + + public void testPartialFoldLastAfterKeepingUnknown() { + Case c = new Case( + Source.synthetic("case"), + field("keep_me_cond", DataTypes.BOOLEAN), + List.of( + field("keep_me", DataTypes.LONG), + new Literal(Source.EMPTY, false, DataTypes.BOOLEAN), + field("first", DataTypes.LONG), + field("last", DataTypes.LONG) + ) + ); + assertThat(c.foldable(), equalTo(false)); + assertThat( + c.partiallyFold(), + equalTo( + new Case( + Source.synthetic("case"), + field("keep_me_cond", DataTypes.BOOLEAN), + List.of(field("keep_me", DataTypes.LONG), field("last", DataTypes.LONG)) + ) + ) + ); + } +} 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 adcb1f611a343..1ce383f2327ad 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 @@ -3405,6 +3405,17 @@ public void testPushdownWithOverwrittenName() { } } + public void testPartiallyFoldCase() { + var plan = optimizedPlan(""" + FROM test + | EVAL c = CASE(true, emp_no, salary) + """); + + var eval = as(plan, Eval.class); + var languages = as(Alias.unwrap(eval.expressions().get(0)), FieldAttribute.class); + assertThat(languages.name(), is("emp_no")); + } + private LogicalPlan optimizedPlan(String query) { return plan(query); } From 24df2215dfae734382cd7d6c19c00cbd3e458e38 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Tue, 12 Mar 2024 13:29:07 -0700 Subject: [PATCH 21/34] Make ES|QL tests compatible with stateless (#106136) Co-authored-by: Elastic Machine --- x-pack/plugin/esql/qa/server/mixed-cluster/build.gradle | 1 + x-pack/plugin/esql/qa/server/multi-node/build.gradle | 4 ++++ .../xpack/esql/qa/multi_node/EsqlClientYamlIT.java | 8 +------- .../xpack/esql/qa/rest/FieldExtractorTestCase.java | 6 ------ 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/build.gradle b/x-pack/plugin/esql/qa/server/mixed-cluster/build.gradle index 51c4a0250a74d..09397710bb856 100644 --- a/x-pack/plugin/esql/qa/server/mixed-cluster/build.gradle +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/build.gradle @@ -6,6 +6,7 @@ import org.elasticsearch.gradle.testclusters.StandaloneRestIntegTestTask apply plugin: 'elasticsearch.internal-java-rest-test' apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.internal-test-artifact' apply plugin: 'elasticsearch.bwc-test' restResources { diff --git a/x-pack/plugin/esql/qa/server/multi-node/build.gradle b/x-pack/plugin/esql/qa/server/multi-node/build.gradle index 2f26003cf7ce4..5fbc4c57b39b7 100644 --- a/x-pack/plugin/esql/qa/server/multi-node/build.gradle +++ b/x-pack/plugin/esql/qa/server/multi-node/build.gradle @@ -1,5 +1,8 @@ +import org.elasticsearch.gradle.util.GradleUtils + apply plugin: 'elasticsearch.internal-java-rest-test' apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.internal-test-artifact' dependencies { javaRestTestImplementation project(xpackModule('esql:qa:testFixtures')) @@ -7,6 +10,7 @@ dependencies { yamlRestTestImplementation project(xpackModule('esql:qa:server')) } +GradleUtils.extendSourceSet(project, "javaRestTest", "yamlRestTest") tasks.named('javaRestTest') { usesDefaultDistribution() diff --git a/x-pack/plugin/esql/qa/server/multi-node/src/yamlRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlClientYamlIT.java b/x-pack/plugin/esql/qa/server/multi-node/src/yamlRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlClientYamlIT.java index a90cce0a566e7..d3ddae16e8af1 100644 --- a/x-pack/plugin/esql/qa/server/multi-node/src/yamlRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlClientYamlIT.java +++ b/x-pack/plugin/esql/qa/server/multi-node/src/yamlRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlClientYamlIT.java @@ -10,7 +10,6 @@ import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.elasticsearch.test.cluster.ElasticsearchCluster; -import org.elasticsearch.test.cluster.local.distribution.DistributionType; import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; import org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase; @@ -20,12 +19,7 @@ public class EsqlClientYamlIT extends ESClientYamlSuiteTestCase { @ClassRule - public static ElasticsearchCluster cluster = ElasticsearchCluster.local() - .distribution(DistributionType.DEFAULT) - .nodes(2) - .setting("xpack.security.enabled", "false") - .setting("xpack.license.self_generated.type", "trial") - .build(); + public static ElasticsearchCluster cluster = Clusters.testCluster(); @Override protected String getTestRestCluster() { diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java index 39c21651a7e02..d107f8a147fd6 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java @@ -1440,12 +1440,6 @@ private static void index(String name, String... docs) throws IOException { private static void createIndex(String name, CheckedConsumer mapping) throws IOException { Request request = new Request("PUT", "/" + name); XContentBuilder index = JsonXContent.contentBuilder().prettyPrint().startObject(); - index.startObject("settings"); - { - index.field("index.number_of_replicas", 0); - index.field("index.number_of_shards", 1); - } - index.endObject(); index.startObject("mappings"); mapping.accept(index); index.endObject(); From 8ebf5bf73d1b475f003c2901b85db4d7b57848aa Mon Sep 17 00:00:00 2001 From: Dianna Hohensee Date: Tue, 12 Mar 2024 16:56:47 -0400 Subject: [PATCH 22/34] Add stateless cluster state thread pool name (#106257) Relates ES-6834 --- .../repositories/blobstore/BlobStoreRepository.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 52cfa2fd5275f..7bc35de44ff9a 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -181,6 +181,7 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp public static final String STATELESS_SHARD_READ_THREAD_NAME = "stateless_shard_read"; public static final String STATELESS_TRANSLOG_THREAD_NAME = "stateless_translog"; public static final String STATELESS_SHARD_WRITE_THREAD_NAME = "stateless_shard_write"; + public static final String STATELESS_CLUSTER_STATE_READ_WRITE_THREAD_NAME = "stateless_cluster_state_read_write"; public static final String SNAPSHOT_PREFIX = "snap-"; @@ -2001,7 +2002,8 @@ protected void assertSnapshotOrGenericThread() { ThreadPool.Names.GENERIC, STATELESS_SHARD_READ_THREAD_NAME, STATELESS_TRANSLOG_THREAD_NAME, - STATELESS_SHARD_WRITE_THREAD_NAME + STATELESS_SHARD_WRITE_THREAD_NAME, + STATELESS_CLUSTER_STATE_READ_WRITE_THREAD_NAME ); } From 22d015b550108fbd80ac92e7ec8851af0b0d8bec Mon Sep 17 00:00:00 2001 From: Dianna Hohensee Date: Tue, 12 Mar 2024 17:45:56 -0400 Subject: [PATCH 23/34] Blob repository related comment improvements (#104478) --- .../elasticsearch/common/blobstore/BlobContainer.java | 2 ++ .../org/elasticsearch/common/blobstore/BlobStore.java | 4 ++++ .../blobstore/support/AbstractBlobContainer.java | 2 +- .../common/blobstore/support/FilterBlobContainer.java | 10 ++++++++++ .../repositories/RepositoriesService.java | 10 +++++++++- .../repositories/blobstore/BlobStoreRepository.java | 2 +- .../blobcache/shared/SharedBlobCacheService.java | 3 +++ 7 files changed, 30 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java b/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java index 77c225f5d94cb..ae48de05a620b 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java @@ -25,6 +25,8 @@ /** * An interface for managing a repository of blob entries, where each blob entry is just a named group of bytes. + * + * A BlobStore creates BlobContainers. */ public interface BlobContainer { diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java b/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java index 8fd04906b1803..4db5a7dd083da 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java @@ -15,6 +15,10 @@ /** * An interface for storing blobs. + * + * Creates a {@link BlobContainer} for each given {@link BlobPath} on demand in {@link #blobContainer(BlobPath)}. + * In implementation/practice, BlobStore often returns a BlobContainer seeded with a reference to the BlobStore. + * {@link org.elasticsearch.repositories.blobstore.BlobStoreRepository} holds and manages a BlobStore. */ public interface BlobStore extends Closeable { diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/support/AbstractBlobContainer.java b/server/src/main/java/org/elasticsearch/common/blobstore/support/AbstractBlobContainer.java index ece00bf970037..dd60fc27c814c 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/support/AbstractBlobContainer.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/support/AbstractBlobContainer.java @@ -13,7 +13,7 @@ import org.elasticsearch.common.blobstore.BlobPath; /** - * A base abstract blob container that implements higher level container methods. + * A base abstract blob container that adds some methods implementations that are often identical across many subclasses. */ public abstract class AbstractBlobContainer implements BlobContainer { diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/support/FilterBlobContainer.java b/server/src/main/java/org/elasticsearch/common/blobstore/support/FilterBlobContainer.java index d231e5046e1c8..571ef6c434fdc 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/support/FilterBlobContainer.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/support/FilterBlobContainer.java @@ -25,6 +25,12 @@ import java.util.Objects; import java.util.stream.Collectors; +/** + * A blob container that by default delegates all methods to an internal BlobContainer. Implementations must define {@link #wrapChild} so + * that the abstraction is complete: so that the internal BlobContainer instance cannot leak out of this wrapper. + * + * Inheritors can safely modify needed methods while continuing to have access to a complete BlobContainer implementation beneath. + */ public abstract class FilterBlobContainer implements BlobContainer { private final BlobContainer delegate; @@ -33,6 +39,10 @@ public FilterBlobContainer(BlobContainer delegate) { this.delegate = Objects.requireNonNull(delegate); } + /** + * Wraps up any instances of the internal BlobContainer type in another BlobContainer type (presumably the implementation's type). + * Ensures that the internal {@link #delegate} type never leaks out of the BlobContainer wrapper type. + */ protected abstract BlobContainer wrapChild(BlobContainer child); @Override diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 095f70a3e5966..f7a2a605a18bd 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -69,7 +69,15 @@ import static org.elasticsearch.snapshots.SearchableSnapshotsSettings.SEARCHABLE_SNAPSHOTS_REPOSITORY_UUID_SETTING_KEY; /** - * Service responsible for maintaining and providing access to snapshot repositories on nodes. + * Service responsible for maintaining and providing access to multiple repositories. + * + * The elected master creates new repositories on request and persists the {@link RepositoryMetadata} in the cluster state. The cluster + * state update then goes out to the rest of the cluster nodes so that all nodes know how to access the new repository. This class contains + * factory information to create new repositories, and provides access to and maintains the lifecycle of repositories. New nodes can easily + * find all the repositories via the cluster state after joining a cluster. + * + * {@link #repository(String)} can be used to fetch a repository. {@link #createRepository(RepositoryMetadata)} does the heavy lifting of + * creation. {@link #applyClusterState(ClusterChangedEvent)} handles adding and removing repositories per cluster state updates. */ public class RepositoriesService extends AbstractLifecycleComponent implements ClusterStateApplier { diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 7bc35de44ff9a..80d6729c6812b 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -165,7 +165,7 @@ /** * BlobStore - based implementation of Snapshot Repository *

      - * This repository works with any {@link BlobStore} implementation. The blobStore could be (and preferred) lazy initialized in + * This repository works with any {@link BlobStore} implementation. The blobStore could be (and is preferably) lazily initialized in * {@link #createBlobStore()}. *

      * For in depth documentation on how exactly implementations of this class interact with the snapshot functionality please refer to the diff --git a/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java b/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java index d4c7c04c5b26e..5b767f2461f6b 100644 --- a/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java +++ b/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java @@ -62,6 +62,9 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +/** + * A caching layer on a local node to minimize network roundtrips to the remote blob store. + */ public class SharedBlobCacheService implements Releasable { private static final String SHARED_CACHE_SETTINGS_PREFIX = "xpack.searchable.snapshot.shared_cache."; From 20f8e2f90aaf39ecb133c72112ac70211512de05 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Tue, 12 Mar 2024 14:55:10 -0700 Subject: [PATCH 24/34] Chain mrjar source sets (#106092) When building MR jars we may have code for several java versions. For example, we could have main21 and main22. This commit adjust the sourcesets to chain together, so that eg main22 can utilize code from main21 (which in turn can already use code from main). --- .../gradle/internal/MrjarPlugin.java | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) 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 8c5d671e00fe7..46fa38a44f564 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 @@ -30,6 +30,9 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -56,24 +59,41 @@ public void apply(Project project) { var javaExtension = project.getExtensions().getByType(JavaPluginExtension.class); var srcDir = project.getProjectDir().toPath().resolve("src"); + List mainVersions = new ArrayList<>(); try (var subdirStream = Files.list(srcDir)) { for (Path sourceset : subdirStream.toList()) { assert Files.isDirectory(sourceset); String sourcesetName = sourceset.getFileName().toString(); Matcher sourcesetMatcher = MRJAR_SOURCESET_PATTERN.matcher(sourcesetName); if (sourcesetMatcher.matches()) { - int javaVersion = Integer.parseInt(sourcesetMatcher.group(1)); - addMrjarSourceset(project, javaExtension, sourcesetName, javaVersion); + mainVersions.add(Integer.parseInt(sourcesetMatcher.group(1))); } } } catch (IOException e) { throw new UncheckedIOException(e); } + + Collections.sort(mainVersions); + List parentSourceSets = new ArrayList<>(); + parentSourceSets.add(SourceSet.MAIN_SOURCE_SET_NAME); + for (int javaVersion : mainVersions) { + String sourcesetName = "main" + javaVersion; + addMrjarSourceset(project, javaExtension, sourcesetName, parentSourceSets, javaVersion); + parentSourceSets.add(sourcesetName); + } } - private void addMrjarSourceset(Project project, JavaPluginExtension javaExtension, String sourcesetName, int javaVersion) { + private void addMrjarSourceset( + Project project, + JavaPluginExtension javaExtension, + String sourcesetName, + List parentSourceSets, + int javaVersion + ) { SourceSet sourceSet = javaExtension.getSourceSets().maybeCreate(sourcesetName); - GradleUtils.extendSourceSet(project, SourceSet.MAIN_SOURCE_SET_NAME, sourcesetName); + for (String parentSourceSetName : parentSourceSets) { + GradleUtils.extendSourceSet(project, parentSourceSetName, sourcesetName); + } var jarTask = project.getTasks().withType(Jar.class).named(JavaPlugin.JAR_TASK_NAME); jarTask.configure(task -> { From 026fab92b381c9490b3906077f2649931bfe52ac Mon Sep 17 00:00:00 2001 From: Volodymyr Krasnikov <129072588+volodk85@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:23:22 -0700 Subject: [PATCH 25/34] debugging, decrease log level in IT test (#106269) --- .../recovery/SnapshotBasedIndexRecoveryIT.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/SnapshotBasedIndexRecoveryIT.java b/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/SnapshotBasedIndexRecoveryIT.java index 8e506d6ac8b51..1465911490f61 100644 --- a/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/SnapshotBasedIndexRecoveryIT.java +++ b/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/SnapshotBasedIndexRecoveryIT.java @@ -70,6 +70,7 @@ import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.InternalSettingsPlugin; import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.test.junit.annotations.TestIssueLogging; import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.test.transport.MockTransportService; import org.elasticsearch.threadpool.ThreadPool; @@ -936,6 +937,10 @@ public void testDisabledSnapshotBasedRecoveryUsesSourceFiles() throws Exception } } + @TestIssueLogging( + issueUrl = "https://github.com/elastic/elasticsearch/issues/87568", + value = "org.elasticsearch.indices.recovery:DEBUG" + ) public void testRecoveryConcurrentlyWithIndexing() throws Exception { internalCluster().startDataOnlyNode(); String indexName = randomAlphaOfLength(10).toLowerCase(Locale.ROOT); From ad47ffbb2d5029ddba2305084d4d9df6df13ef6d Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Tue, 12 Mar 2024 15:41:09 -0700 Subject: [PATCH 26/34] Mute SearchTransportTelemetryTests.testSearchTransportMetricsQueryThenFetch see https://github.com/elastic/elasticsearch/issues/104184 --- .../search/TelemetryMetrics/SearchTransportTelemetryTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/search/TelemetryMetrics/SearchTransportTelemetryTests.java b/server/src/test/java/org/elasticsearch/search/TelemetryMetrics/SearchTransportTelemetryTests.java index 93712880f2ce4..0a9b498bc0562 100644 --- a/server/src/test/java/org/elasticsearch/search/TelemetryMetrics/SearchTransportTelemetryTests.java +++ b/server/src/test/java/org/elasticsearch/search/TelemetryMetrics/SearchTransportTelemetryTests.java @@ -81,6 +81,7 @@ public void testSearchTransportMetricsDfsQueryThenFetch() throws InterruptedExce resetMeter(); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/103810") public void testSearchTransportMetricsQueryThenFetch() throws InterruptedException { assertSearchHitsWithoutFailures( client().prepareSearch(indexName).setSearchType(SearchType.QUERY_THEN_FETCH).setQuery(simpleQueryStringQuery("doc1")), From 4e09a86e7100a3aa6e26e450193c66fb42ecf3d8 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 12 Mar 2024 15:47:11 -0700 Subject: [PATCH 27/34] Mute testProfile (#106275) Tracked at #106273 --- .../elasticsearch/xpack/esql/action/CrossClustersQueryIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueryIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueryIT.java index c9ee644040a43..ca93f8d090996 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueryIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueryIT.java @@ -121,6 +121,7 @@ public void testMetadataIndex() { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/106273") public void testProfile() { assumeTrue("pragmas only enabled on snapshot builds", Build.current().isSnapshot()); final int localOnlyProfiles; From 99719f2c8254e79daa182ce02ffde9c7f27baeba Mon Sep 17 00:00:00 2001 From: Dianna Hohensee Date: Tue, 12 Mar 2024 19:23:15 -0400 Subject: [PATCH 28/34] Shorten cluster state thread pool name (#106274) The thread pool name is used to report metrics, and MetricNameValidator enforces a max of 30 characters. --- .../repositories/blobstore/BlobStoreRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 80d6729c6812b..41e849b4d2ebd 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -181,7 +181,7 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp public static final String STATELESS_SHARD_READ_THREAD_NAME = "stateless_shard_read"; public static final String STATELESS_TRANSLOG_THREAD_NAME = "stateless_translog"; public static final String STATELESS_SHARD_WRITE_THREAD_NAME = "stateless_shard_write"; - public static final String STATELESS_CLUSTER_STATE_READ_WRITE_THREAD_NAME = "stateless_cluster_state_read_write"; + public static final String STATELESS_CLUSTER_STATE_READ_WRITE_THREAD_NAME = "stateless_cluster_state"; public static final String SNAPSHOT_PREFIX = "snap-"; From d471ccb5bb4e5226f843ae688b0b2b205a842131 Mon Sep 17 00:00:00 2001 From: Panagiotis Bailis Date: Wed, 13 Mar 2024 09:24:51 +0200 Subject: [PATCH 29/34] Adding support for hex-encoded byte vectors on knn-search (#105393) --- docs/changelog/105393.yaml | 5 + docs/reference/query-dsl/knn-query.asciidoc | 4 +- docs/reference/rest-api/common-parms.asciidoc | 2 +- docs/reference/search/knn-search.asciidoc | 2 +- .../search-your-data/knn-search.asciidoc | 21 ++ ...70_knn_search_hex_encoded_byte_vectors.yml | 163 ++++++++++ ...175_knn_query_hex_encoded_byte_vectors.yml | 162 ++++++++++ .../org/elasticsearch/TransportVersions.java | 1 + .../common/io/stream/StreamInput.java | 14 + .../common/io/stream/StreamOutput.java | 13 + .../vectors/DenseVectorFieldMapper.java | 286 ++++++++++++------ .../search/retriever/KnnRetrieverBuilder.java | 10 +- .../search/vectors/ExactKnnQueryBuilder.java | 31 +- .../vectors/KnnScoreDocQueryBuilder.java | 30 +- .../search/vectors/KnnSearchBuilder.java | 76 +++-- .../search/vectors/KnnVectorQueryBuilder.java | 72 +++-- .../search/vectors/VectorData.java | 168 ++++++++++ .../vectors/DenseVectorFieldTypeTests.java | 7 +- ...AbstractKnnVectorQueryBuilderTestCase.java | 53 ++-- .../KnnByteVectorQueryBuilderTests.java | 9 + ...a => KnnFloatVectorQueryBuilderTests.java} | 11 +- .../search/vectors/KnnSearchBuilderTests.java | 4 +- .../search/vectors/VectorDataTests.java | 199 ++++++++++++ .../AbstractQueryVectorBuilderTestCase.java | 2 +- 24 files changed, 1136 insertions(+), 209 deletions(-) create mode 100644 docs/changelog/105393.yaml create mode 100644 rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/170_knn_search_hex_encoded_byte_vectors.yml create mode 100644 rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/175_knn_query_hex_encoded_byte_vectors.yml create mode 100644 server/src/main/java/org/elasticsearch/search/vectors/VectorData.java rename server/src/test/java/org/elasticsearch/search/vectors/{KnnVectorQueryBuilderTests.java => KnnFloatVectorQueryBuilderTests.java} (56%) create mode 100644 server/src/test/java/org/elasticsearch/search/vectors/VectorDataTests.java diff --git a/docs/changelog/105393.yaml b/docs/changelog/105393.yaml new file mode 100644 index 0000000000000..4a4cc299b7bd7 --- /dev/null +++ b/docs/changelog/105393.yaml @@ -0,0 +1,5 @@ +pr: 105393 +summary: Adding support for hex-encoded byte vectors on knn-search +area: Vector Search +type: feature +issues: [] diff --git a/docs/reference/query-dsl/knn-query.asciidoc b/docs/reference/query-dsl/knn-query.asciidoc index e9aeea68c06f7..c11782f524950 100644 --- a/docs/reference/query-dsl/knn-query.asciidoc +++ b/docs/reference/query-dsl/knn-query.asciidoc @@ -87,8 +87,8 @@ the top `size` results. `query_vector`:: + -- -(Required, array of floats) Query vector. Must have the same number of dimensions -as the vector field you are searching against. +(Required, array of floats or string) Query vector. Must have the same number of dimensions +as the vector field you are searching against. Must be either an array of floats or a hex-encoded byte vector. -- `num_candidates`:: diff --git a/docs/reference/rest-api/common-parms.asciidoc b/docs/reference/rest-api/common-parms.asciidoc index 6757b6be24207..062f832b6f79d 100644 --- a/docs/reference/rest-api/common-parms.asciidoc +++ b/docs/reference/rest-api/common-parms.asciidoc @@ -597,7 +597,7 @@ end::knn-num-candidates[] tag::knn-query-vector[] Query vector. Must have the same number of dimensions as the vector field you -are searching against. +are searching against. Must be either an array of floats or a hex-encoded byte vector. end::knn-query-vector[] tag::knn-similarity[] diff --git a/docs/reference/search/knn-search.asciidoc b/docs/reference/search/knn-search.asciidoc index 136b53388baf9..7947c688a807c 100644 --- a/docs/reference/search/knn-search.asciidoc +++ b/docs/reference/search/knn-search.asciidoc @@ -121,7 +121,7 @@ include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-k] include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-num-candidates] `query_vector`:: -(Required, array of floats) +(Required, array of floats or string) include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-query-vector] ==== diff --git a/docs/reference/search/search-your-data/knn-search.asciidoc b/docs/reference/search/search-your-data/knn-search.asciidoc index ab65b834c0ce7..030c10a91d005 100644 --- a/docs/reference/search/search-your-data/knn-search.asciidoc +++ b/docs/reference/search/search-your-data/knn-search.asciidoc @@ -242,6 +242,27 @@ POST byte-image-index/_search // TEST[s/"k": 10/"k": 3/] // TEST[s/"num_candidates": 100/"num_candidates": 3/] + +_Note_: In addition to the standard byte array, one can also provide a hex-encoded string value +for the `query_vector` param. As an example, the search request above can also be expressed as follows, +which would yield the same results +[source,console] +---- +POST byte-image-index/_search +{ + "knn": { + "field": "byte-image-vector", + "query_vector": "fb09", + "k": 10, + "num_candidates": 100 + }, + "fields": [ "title" ] +} +---- +// TEST[continued] +// TEST[s/"k": 10/"k": 3/] +// TEST[s/"num_candidates": 100/"num_candidates": 3/] + [discrete] [[knn-search-quantized-example]] ==== Byte quantized kNN search diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/170_knn_search_hex_encoded_byte_vectors.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/170_knn_search_hex_encoded_byte_vectors.yml new file mode 100644 index 0000000000000..71f65220eba1e --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/170_knn_search_hex_encoded_byte_vectors.yml @@ -0,0 +1,163 @@ +setup: + - skip: + version: ' - 8.13.99' + reason: 'hex encoding for byte vectors was added in 8.14' + + - do: + indices.create: + index: knn_hex_vector_index + body: + settings: + number_of_shards: 1 + mappings: + dynamic: false + properties: + my_vector_byte: + type: dense_vector + dims: 3 + index : true + similarity : l2_norm + element_type: byte + my_vector_float: + type: dense_vector + dims: 3 + index: true + element_type: float + similarity : l2_norm + + # [-128, 127, 10] - is encoded as '807f0a' + - do: + index: + index: knn_hex_vector_index + id: "1" + body: + my_vector_byte: "807f0a" + + + # [0, 1, 0] - is encoded as '000100' + - do: + index: + index: knn_hex_vector_index + id: "2" + body: + my_vector_byte: "000100" + + # [64, -10, -30] - is encoded as '40f6e2' + - do: + index: + index: knn_hex_vector_index + id: "3" + body: + my_vector_byte: "40f6e2" + + - do: + index: + index: knn_hex_vector_index + id: "4" + body: + my_vector_float: [10.5, -10, 1024] + + - do: + indices.refresh: {} + +--- +"Fail to index hex-encoded vector on float field": + + # [-128, 127, 10] - is encoded as '807f0a' + - do: + catch: /Failed to parse object./ + index: + index: knn_hex_vector_index + id: "5" + body: + my_vector_float: "807f0a" + +--- +"Knn search with hex string for float field" : + # [64, 10, -30] - is encoded as '400ae2' + # this will be properly decoded but only because: + # (i) the provided input is compatible as the values are within [Byte.MIN_VALUE, Byte.MAX_VALUE] range + # (ii) we do not differentiate between byte and float fields when initially parsing a query even for hex + # (iii) we support expansion from byte to float + + - do: + search: + index: knn_hex_vector_index + body: + size: 3 + knn: + field: my_vector_float + query_vector: "400ae2" + num_candidates: 100 + k: 10 + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "4" } + +--- +"Knn search with hex string for byte field" : + # [64, 10, -30] - is encoded as '400ae2' + - do: + search: + index: knn_hex_vector_index + body: + size: 3 + knn: + field: my_vector_byte + query_vector: "400ae2" + num_candidates: 100 + k: 10 + + - match: { hits.total.value: 3 } + - match: { hits.hits.0._id: "3" } + - match: { hits.hits.1._id: "2" } + - match: { hits.hits.2._id: "1" } + +--- +"Knn search with hex string for byte field - dimensions mismatch" : + # [64, 10, -30, 10] - is encoded as '400ae20a' + - do: + catch: /the query vector has a different dimension \[4\] than the index vectors \[3\]/ + search: + index: knn_hex_vector_index + body: + size: 3 + knn: + field: my_vector_byte + query_vector: "400ae20a" + num_candidates: 100 + k: 10 + + +--- +"Knn search with hex string for byte field - cannot decode string" : + # '40af20a' is garbage :) + - do: + catch: /failed to parse field \[query_vector\]/ + search: + index: knn_hex_vector_index + body: + size: 3 + knn: + field: my_vector_byte + query_vector: "40af20a" + num_candidates: 100 + k: 10 + +--- +"Knn search with standard byte vector matching against hex-encoded indexed docs" : + - do: + search: + index: knn_hex_vector_index + body: + size: 3 + knn: + field: my_vector_byte + query_vector: [64, 10, -30] + num_candidates: 100 + k: 10 + + - match: { hits.total.value: 3 } + - match: { hits.hits.0._id: "3" } + - match: { hits.hits.1._id: "2" } + - match: { hits.hits.2._id: "1" } diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/175_knn_query_hex_encoded_byte_vectors.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/175_knn_query_hex_encoded_byte_vectors.yml new file mode 100644 index 0000000000000..9f850400a09cd --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/175_knn_query_hex_encoded_byte_vectors.yml @@ -0,0 +1,162 @@ +setup: + - skip: + version: ' - 8.13.99' + reason: 'hex encoding for byte vectors was added in 8.14' + + - do: + indices.create: + index: knn_hex_vector_index + body: + settings: + number_of_shards: 1 + mappings: + dynamic: false + properties: + my_vector_byte: + type: dense_vector + dims: 3 + index : true + similarity : l2_norm + element_type: byte + my_vector_float: + type: dense_vector + dims: 3 + index: true + element_type: float + similarity : l2_norm + + # [-128, 127, 10] - is encoded as '807f0a' + - do: + index: + index: knn_hex_vector_index + id: "1" + body: + my_vector_byte: "807f0a" + + + # [0, 1, 0] - is encoded as '000100' + - do: + index: + index: knn_hex_vector_index + id: "2" + body: + my_vector_byte: "000100" + + # [64, -10, -30] - is encoded as '40f6e2' + - do: + index: + index: knn_hex_vector_index + id: "3" + body: + my_vector_byte: "40f6e2" + + - do: + index: + index: knn_hex_vector_index + id: "4" + body: + my_vector_float: [10.5, -10, 1024] + + - do: + indices.refresh: {} + +--- +"Fail to index hex-encoded vector on float field": + + # [-128, 127, 10] - is encoded as '807f0a' + - do: + catch: /Failed to parse object./ + index: + index: knn_hex_vector_index + id: "5" + body: + my_vector_float: "807f0a" + +--- +"Knn query with hex string for float field" : + # [64, 10, -30] - is encoded as '400ae2' + # this will be properly decoded but only because: + # (i) the provided input is compatible as the values are within [Byte.MIN_VALUE, Byte.MAX_VALUE] range + # (ii) we do not differentiate between byte and float fields when initially parsing a query even for hex + # (iii) we support expansion from byte to float + + - do: + search: + index: knn_hex_vector_index + body: + size: 3 + query: + knn: + field: my_vector_float + query_vector: "400ae2" + num_candidates: 100 + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "4" } + +--- +"Knn query with hex string for byte field" : + # [64, 10, -30] - is encoded as '400ae2' + - do: + search: + index: knn_hex_vector_index + body: + size: 3 + query: + knn: + field: my_vector_byte + query_vector: "400ae2" + num_candidates: 100 + + - match: { hits.total.value: 3 } + - match: { hits.hits.0._id: "3" } + - match: { hits.hits.1._id: "2" } + - match: { hits.hits.2._id: "1" } + +--- +"Knn query with hex string for byte field - dimensions mismatch" : + # [64, 10, -30, 10] - is encoded as '400ae20a' + - do: + catch: /the query vector has a different dimension \[4\] than the index vectors \[3\]/ + search: + index: knn_hex_vector_index + body: + size: 3 + query: + knn: + field: my_vector_byte + query_vector: "400ae20a" + num_candidates: 100 + +--- +"Knn query with hex string for byte field - cannot decode string" : + # '40af20a' is garbage :) + - do: + catch: /failed to parse field \[query_vector\]/ + search: + index: knn_hex_vector_index + body: + size: 3 + query: + knn: + field: my_vector_byte + query_vector: "40af20a" + num_candidates: 100 + +--- +"Knn query with standard byte vector matching against hex-encoded indexed docs" : + - do: + search: + index: knn_hex_vector_index + body: + size: 3 + query: + knn: + field: my_vector_byte + query_vector: [64, 10, -30] + num_candidates: 100 + + - match: { hits.total.value: 3 } + - match: { hits.hits.0._id: "3" } + - match: { hits.hits.1._id: "2" } + - match: { hits.hits.2._id: "1" } diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 6ac2c24739805..a83b0ea0c90e5 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -143,6 +143,7 @@ static TransportVersion def(int id) { public static final TransportVersion ADD_DATA_STREAM_GLOBAL_RETENTION = def(8_603_00_0); public static final TransportVersion ALLOCATION_STATS = def(8_604_00_0); public static final TransportVersion ESQL_EXTENDED_ENRICH_TYPES = def(8_605_00_0); + public static final TransportVersion KNN_EXPLICIT_BYTE_QUERY_VECTOR_PARSING = def(8_606_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java b/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java index 7281616a8d25f..c4952b8cae51d 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java @@ -693,6 +693,20 @@ public byte[] readOptionalByteArray() throws IOException { return null; } + /** + * Reads an optional float array. It's effectively the same as readFloatArray, except + * it supports null. + * @return a float array or null + * @throws IOException + */ + @Nullable + public float[] readOptionalFloatArray() throws IOException { + if (readBoolean()) { + return readFloatArray(); + } + return null; + } + /** * Same as {@link #readMap(Writeable.Reader, Writeable.Reader)} but always reading string keys. */ diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java b/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java index 69a5135215eba..33fb000c1bca2 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java @@ -534,6 +534,19 @@ public void writeOptionalByteArray(@Nullable byte[] array) throws IOException { } } + /** + * Writes a float array, for null arrays it writes false. + * @param array an array or null + */ + public void writeOptionalFloatArray(@Nullable float[] array) throws IOException { + if (array == null) { + writeBoolean(false); + } else { + writeBoolean(true); + writeFloatArray(array); + } + } + public void writeGenericMap(@Nullable Map map) throws IOException { writeGenericValue(map); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index 47efa0ca49771..22b8549e14969 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -43,6 +43,7 @@ import org.apache.lucene.search.join.BitSetProducer; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.VectorUtil; +import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; @@ -69,6 +70,7 @@ import org.elasticsearch.search.vectors.ESDiversifyingChildrenFloatKnnVectorQuery; import org.elasticsearch.search.vectors.ESKnnByteVectorQuery; import org.elasticsearch.search.vectors.ESKnnFloatVectorQuery; +import org.elasticsearch.search.vectors.VectorData; import org.elasticsearch.search.vectors.VectorSimilarityQuery; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; @@ -80,6 +82,7 @@ import java.nio.ByteOrder; import java.time.ZoneId; import java.util.Arrays; +import java.util.HexFormat; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -88,6 +91,7 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import static org.elasticsearch.common.Strings.format; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; /** @@ -338,11 +342,16 @@ void checkVectorMagnitude( } @Override - public void parseKnnVectorAndIndex(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException { + public double computeDotProduct(VectorData vectorData) { + return VectorUtil.dotProduct(vectorData.asByteVector(), vectorData.asByteVector()); + } + + private VectorData parseVectorArray(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException { int index = 0; byte[] vector = new byte[fieldMapper.fieldType().dims]; float squaredMagnitude = 0; - for (Token token = context.parser().nextToken(); token != Token.END_ARRAY; token = context.parser().nextToken()) { + for (XContentParser.Token token = context.parser().nextToken(); token != Token.END_ARRAY; token = context.parser() + .nextToken()) { fieldMapper.checkDimensionExceeded(index, context); ensureExpectedToken(Token.VALUE_NUMBER, token, context.parser()); final int value; @@ -383,44 +392,49 @@ public void parseKnnVectorAndIndex(DocumentParserContext context, DenseVectorFie } fieldMapper.checkDimensionMatches(index, context); checkVectorMagnitude(fieldMapper.fieldType().similarity, errorByteElementsAppender(vector), squaredMagnitude); + return VectorData.fromBytes(vector); + } + + private VectorData parseHexEncodedVector(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException { + byte[] decodedVector = HexFormat.of().parseHex(context.parser().text()); + fieldMapper.checkDimensionMatches(decodedVector.length, context); + VectorData vectorData = VectorData.fromBytes(decodedVector); + double squaredMagnitude = computeDotProduct(vectorData); + checkVectorMagnitude( + fieldMapper.fieldType().similarity, + errorByteElementsAppender(decodedVector), + (float) squaredMagnitude + ); + return vectorData; + } + + @Override + VectorData parseKnnVector(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException { + XContentParser.Token token = context.parser().currentToken(); + return switch (token) { + case START_ARRAY -> parseVectorArray(context, fieldMapper); + case VALUE_STRING -> parseHexEncodedVector(context, fieldMapper); + default -> throw new ParsingException( + context.parser().getTokenLocation(), + format("Unsupported type [%s] for provided value [%s]", token, context.parser().text()) + ); + }; + } + + @Override + public void parseKnnVectorAndIndex(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException { + VectorData vectorData = parseKnnVector(context, fieldMapper); Field field = createKnnVectorField( fieldMapper.fieldType().name(), - vector, + vectorData.asByteVector(), fieldMapper.fieldType().similarity.vectorSimilarityFunction(fieldMapper.indexCreatedVersion, this) ); context.doc().addWithKey(fieldMapper.fieldType().name(), field); } @Override - double parseKnnVectorToByteBuffer(DocumentParserContext context, DenseVectorFieldMapper fieldMapper, ByteBuffer byteBuffer) - throws IOException { - double dotProduct = 0f; - int index = 0; - for (Token token = context.parser().nextToken(); token != Token.END_ARRAY; token = context.parser().nextToken()) { - fieldMapper.checkDimensionExceeded(index, context); - ensureExpectedToken(Token.VALUE_NUMBER, token, context.parser()); - int value = context.parser().intValue(true); - if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { - throw new IllegalArgumentException( - "element_type [" - + this - + "] vectors only support integers between [" - + Byte.MIN_VALUE - + ", " - + Byte.MAX_VALUE - + "] but found [" - + value - + "] at dim [" - + index - + "];" - ); - } - byteBuffer.put((byte) value); - dotProduct += value * value; - index++; - } - fieldMapper.checkDimensionMatches(index, context); - return dotProduct; + int getNumBytes(int dimensions) { + return dimensions * elementBytes; } @Override @@ -530,6 +544,11 @@ void checkVectorMagnitude( } } + @Override + public double computeDotProduct(VectorData vectorData) { + return VectorUtil.dotProduct(vectorData.asFloatVector(), vectorData.asFloatVector()); + } + @Override public void parseKnnVectorAndIndex(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException { int index = 0; @@ -566,23 +585,27 @@ && isNotUnitVector(squaredMagnitude)) { } @Override - double parseKnnVectorToByteBuffer(DocumentParserContext context, DenseVectorFieldMapper fieldMapper, ByteBuffer byteBuffer) - throws IOException { - double dotProduct = 0f; + VectorData parseKnnVector(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException { int index = 0; + float squaredMagnitude = 0; float[] vector = new float[fieldMapper.fieldType().dims]; for (Token token = context.parser().nextToken(); token != Token.END_ARRAY; token = context.parser().nextToken()) { fieldMapper.checkDimensionExceeded(index, context); ensureExpectedToken(Token.VALUE_NUMBER, token, context.parser()); float value = context.parser().floatValue(true); vector[index] = value; - byteBuffer.putFloat(value); - dotProduct += value * value; + squaredMagnitude += value * value; index++; } fieldMapper.checkDimensionMatches(index, context); checkVectorBounds(vector); - return dotProduct; + checkVectorMagnitude(fieldMapper.fieldType().similarity, errorFloatElementsAppender(vector), squaredMagnitude); + return VectorData.fromFloats(vector); + } + + @Override + int getNumBytes(int dimensions) { + return dimensions * elementBytes; } @Override @@ -607,8 +630,9 @@ ByteBuffer createByteBuffer(IndexVersion indexVersion, int numBytes) { abstract void parseKnnVectorAndIndex(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException; - abstract double parseKnnVectorToByteBuffer(DocumentParserContext context, DenseVectorFieldMapper fieldMapper, ByteBuffer byteBuffer) - throws IOException; + abstract VectorData parseKnnVector(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException; + + abstract int getNumBytes(int dimensions); abstract ByteBuffer createByteBuffer(IndexVersion indexVersion, int numBytes); @@ -699,6 +723,8 @@ static Function errorFloatElementsAppender(float[] static Function errorByteElementsAppender(byte[] vector) { return sb -> appendErrorElements(sb, vector); } + + public abstract double computeDotProduct(VectorData vectorData); } static final Map namesToElementType = Map.of( @@ -1158,66 +1184,120 @@ public Query createKnnQuery( return knnQuery; } - public Query createExactKnnQuery(float[] queryVector) { - queryVector = validateAndNormalize(queryVector); - VectorSimilarityFunction vectorSimilarityFunction = similarity.vectorSimilarityFunction(indexVersionCreated, elementType); + public Query createExactKnnQuery(VectorData queryVector) { + if (isIndexed() == false) { + throw new IllegalArgumentException( + "to perform knn search on field [" + name() + "], its mapping must have [index] set to [true]" + ); + } return switch (elementType) { - case BYTE -> { - byte[] bytes = new byte[queryVector.length]; + case BYTE -> createExactKnnByteQuery(queryVector.asByteVector()); + case FLOAT -> createExactKnnFloatQuery(queryVector.asFloatVector()); + }; + } + + private Query createExactKnnByteQuery(byte[] queryVector) { + if (queryVector.length != dims) { + throw new IllegalArgumentException( + "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]" + ); + } + if (similarity == VectorSimilarity.DOT_PRODUCT || similarity == VectorSimilarity.COSINE) { + float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector); + elementType.checkVectorMagnitude(similarity, ElementType.errorByteElementsAppender(queryVector), squaredMagnitude); + } + VectorSimilarityFunction vectorSimilarityFunction = similarity.vectorSimilarityFunction(indexVersionCreated, elementType); + return new BooleanQuery.Builder().add(new FieldExistsQuery(name()), BooleanClause.Occur.FILTER) + .add( + new FunctionQuery( + new ByteVectorSimilarityFunction( + vectorSimilarityFunction, + new ByteKnnVectorFieldSource(name()), + new ConstKnnByteVectorValueSource(queryVector) + ) + ), + BooleanClause.Occur.SHOULD + ) + .build(); + } + + private Query createExactKnnFloatQuery(float[] queryVector) { + if (queryVector.length != dims) { + throw new IllegalArgumentException( + "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]" + ); + } + elementType.checkVectorBounds(queryVector); + if (similarity == VectorSimilarity.DOT_PRODUCT || similarity == VectorSimilarity.COSINE) { + float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector); + elementType.checkVectorMagnitude(similarity, ElementType.errorFloatElementsAppender(queryVector), squaredMagnitude); + if (similarity == VectorSimilarity.COSINE + && indexVersionCreated.onOrAfter(NORMALIZE_COSINE) + && isNotUnitVector(squaredMagnitude)) { + float length = (float) Math.sqrt(squaredMagnitude); + queryVector = Arrays.copyOf(queryVector, queryVector.length); for (int i = 0; i < queryVector.length; i++) { - bytes[i] = (byte) queryVector[i]; + queryVector[i] /= length; } - yield new BooleanQuery.Builder().add(new FieldExistsQuery(name()), BooleanClause.Occur.FILTER) - .add( - new FunctionQuery( - new ByteVectorSimilarityFunction( - vectorSimilarityFunction, - new ByteKnnVectorFieldSource(name()), - new ConstKnnByteVectorValueSource(bytes) - ) - ), - BooleanClause.Occur.SHOULD - ) - .build(); } - case FLOAT -> new BooleanQuery.Builder().add(new FieldExistsQuery(name()), BooleanClause.Occur.FILTER) - .add( - new FunctionQuery( - new FloatVectorSimilarityFunction( - vectorSimilarityFunction, - new FloatKnnVectorFieldSource(name()), - new ConstKnnFloatValueSource(queryVector) - ) - ), - BooleanClause.Occur.SHOULD - ) - .build(); - }; + } + VectorSimilarityFunction vectorSimilarityFunction = similarity.vectorSimilarityFunction(indexVersionCreated, elementType); + return new BooleanQuery.Builder().add(new FieldExistsQuery(name()), BooleanClause.Occur.FILTER) + .add( + new FunctionQuery( + new FloatVectorSimilarityFunction( + vectorSimilarityFunction, + new FloatKnnVectorFieldSource(name()), + new ConstKnnFloatValueSource(queryVector) + ) + ), + BooleanClause.Occur.SHOULD + ) + .build(); + } + + Query createKnnQuery(float[] queryVector, int numCands, Query filter, Float similarityThreshold, BitSetProducer parentFilter) { + return createKnnQuery(VectorData.fromFloats(queryVector), numCands, filter, similarityThreshold, parentFilter); } public Query createKnnQuery( - float[] queryVector, + VectorData queryVector, int numCands, Query filter, Float similarityThreshold, BitSetProducer parentFilter ) { - queryVector = validateAndNormalize(queryVector); - Query knnQuery = switch (elementType) { - case BYTE -> { - byte[] bytes = new byte[queryVector.length]; - for (int i = 0; i < queryVector.length; i++) { - bytes[i] = (byte) queryVector[i]; - } - yield parentFilter != null - ? new ESDiversifyingChildrenByteKnnVectorQuery(name(), bytes, filter, numCands, parentFilter) - : new ESKnnByteVectorQuery(name(), bytes, numCands, filter); - } - case FLOAT -> parentFilter != null - ? new ESDiversifyingChildrenFloatKnnVectorQuery(name(), queryVector, filter, numCands, parentFilter) - : new ESKnnFloatVectorQuery(name(), queryVector, numCands, filter); + if (isIndexed() == false) { + throw new IllegalArgumentException( + "to perform knn search on field [" + name() + "], its mapping must have [index] set to [true]" + ); + } + return switch (getElementType()) { + case BYTE -> createKnnByteQuery(queryVector.asByteVector(), numCands, filter, similarityThreshold, parentFilter); + case FLOAT -> createKnnFloatQuery(queryVector.asFloatVector(), numCands, filter, similarityThreshold, parentFilter); }; + } + + private Query createKnnByteQuery( + byte[] queryVector, + int numCands, + Query filter, + Float similarityThreshold, + BitSetProducer parentFilter + ) { + if (queryVector.length != dims) { + throw new IllegalArgumentException( + "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]" + ); + } + if (similarity == VectorSimilarity.DOT_PRODUCT || similarity == VectorSimilarity.COSINE) { + float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector); + elementType.checkVectorMagnitude(similarity, ElementType.errorByteElementsAppender(queryVector), squaredMagnitude); + } + Query knnQuery = parentFilter != null + ? new ESDiversifyingChildrenByteKnnVectorQuery(name(), queryVector, filter, numCands, parentFilter) + : new ESKnnByteVectorQuery(name(), queryVector, numCands, filter); if (similarityThreshold != null) { knnQuery = new VectorSimilarityQuery( knnQuery, @@ -1228,12 +1308,13 @@ public Query createKnnQuery( return knnQuery; } - private float[] validateAndNormalize(float[] queryVector) { - if (isIndexed() == false) { - throw new IllegalArgumentException( - "to perform knn search on field [" + name() + "], its mapping must have [index] set to [true]" - ); - } + private Query createKnnFloatQuery( + float[] queryVector, + int numCands, + Query filter, + Float similarityThreshold, + BitSetProducer parentFilter + ) { if (queryVector.length != dims) { throw new IllegalArgumentException( "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]" @@ -1244,7 +1325,6 @@ private float[] validateAndNormalize(float[] queryVector) { float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector); elementType.checkVectorMagnitude(similarity, ElementType.errorFloatElementsAppender(queryVector), squaredMagnitude); if (similarity == VectorSimilarity.COSINE - && ElementType.FLOAT.equals(elementType) && indexVersionCreated.onOrAfter(NORMALIZE_COSINE) && isNotUnitVector(squaredMagnitude)) { float length = (float) Math.sqrt(squaredMagnitude); @@ -1254,7 +1334,17 @@ && isNotUnitVector(squaredMagnitude)) { } } } - return queryVector; + Query knnQuery = parentFilter != null + ? new ESDiversifyingChildrenFloatKnnVectorQuery(name(), queryVector, filter, numCands, parentFilter) + : new ESKnnFloatVectorQuery(name(), queryVector, numCands, filter); + if (similarityThreshold != null) { + knnQuery = new VectorSimilarityQuery( + knnQuery, + similarityThreshold, + similarity.score(similarityThreshold, elementType, dims) + ); + } + return knnQuery; } VectorSimilarity getSimilarity() { @@ -1349,13 +1439,15 @@ private void parseBinaryDocValuesVectorAndIndex(DocumentParserContext context) t int dims = fieldType().dims; ElementType elementType = fieldType().elementType; int numBytes = indexCreatedVersion.onOrAfter(MAGNITUDE_STORED_INDEX_VERSION) - ? dims * elementType.elementBytes + MAGNITUDE_BYTES - : dims * elementType.elementBytes; + ? elementType.getNumBytes(dims) + MAGNITUDE_BYTES + : elementType.getNumBytes(dims); ByteBuffer byteBuffer = elementType.createByteBuffer(indexCreatedVersion, numBytes); - double dotProduct = elementType.parseKnnVectorToByteBuffer(context, this, byteBuffer); + VectorData vectorData = elementType.parseKnnVector(context, this); + vectorData.addToBuffer(byteBuffer); if (indexCreatedVersion.onOrAfter(MAGNITUDE_STORED_INDEX_VERSION)) { // encode vector magnitude at the end + double dotProduct = elementType.computeDotProduct(vectorData); float vectorMagnitude = (float) Math.sqrt(dotProduct); byteBuffer.putFloat(vectorMagnitude); } diff --git a/server/src/main/java/org/elasticsearch/search/retriever/KnnRetrieverBuilder.java b/server/src/main/java/org/elasticsearch/search/retriever/KnnRetrieverBuilder.java index fc2d4218ea1ec..3c4355e56d21d 100644 --- a/server/src/main/java/org/elasticsearch/search/retriever/KnnRetrieverBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/retriever/KnnRetrieverBuilder.java @@ -13,6 +13,7 @@ import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.vectors.KnnSearchBuilder; import org.elasticsearch.search.vectors.QueryVectorBuilder; +import org.elasticsearch.search.vectors.VectorData; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentBuilder; @@ -121,7 +122,14 @@ public String getName() { @Override public void extractToSearchSourceBuilder(SearchSourceBuilder searchSourceBuilder, boolean compoundUsed) { - KnnSearchBuilder knnSearchBuilder = new KnnSearchBuilder(field, queryVector, queryVectorBuilder, k, numCands, similarity); + KnnSearchBuilder knnSearchBuilder = new KnnSearchBuilder( + field, + VectorData.fromFloats(queryVector), + queryVectorBuilder, + k, + numCands, + similarity + ); if (preFilterQueryBuilders != null) { knnSearchBuilder.addFilterQueries(preFilterQueryBuilders); } diff --git a/server/src/main/java/org/elasticsearch/search/vectors/ExactKnnQueryBuilder.java b/server/src/main/java/org/elasticsearch/search/vectors/ExactKnnQueryBuilder.java index d292f61dcb085..60b0d259961da 100644 --- a/server/src/main/java/org/elasticsearch/search/vectors/ExactKnnQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/vectors/ExactKnnQueryBuilder.java @@ -22,7 +22,6 @@ import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; -import java.util.Arrays; import java.util.Objects; /** @@ -32,7 +31,7 @@ public class ExactKnnQueryBuilder extends AbstractQueryBuilder { public static final String NAME = "exact_knn"; private final String field; - private final float[] query; + private final VectorData query; /** * Creates a query builder. @@ -41,13 +40,27 @@ public class ExactKnnQueryBuilder extends AbstractQueryBuilder PARSER = new ConstructingObjectParser<>("knn", args -> { // TODO optimize parsing for when BYTE values are provided - List vector = (List) args[1]; - final float[] vectorArray; - if (vector != null) { - vectorArray = new float[vector.size()]; - for (int i = 0; i < vector.size(); i++) { - vectorArray[i] = vector.get(i); - } - } else { - vectorArray = null; - } return new Builder().field((String) args[0]) - .queryVector(vectorArray) + .queryVector((VectorData) args[1]) .queryVectorBuilder((QueryVectorBuilder) args[4]) .k((Integer) args[2]) .numCandidates((Integer) args[3]) @@ -79,9 +68,15 @@ public class KnnSearchBuilder implements Writeable, ToXContentFragment, Rewritea static { PARSER.declareString(constructorArg(), FIELD_FIELD); - PARSER.declareFloatArray(optionalConstructorArg(), QUERY_VECTOR_FIELD); + PARSER.declareField( + optionalConstructorArg(), + (p, c) -> VectorData.parseXContent(p), + QUERY_VECTOR_FIELD, + ObjectParser.ValueType.OBJECT_ARRAY_STRING_OR_NUMBER + ); PARSER.declareInt(optionalConstructorArg(), K_FIELD); PARSER.declareInt(optionalConstructorArg(), NUM_CANDS_FIELD); + PARSER.declareNamedObject( optionalConstructorArg(), (p, c, n) -> p.namedObject(QueryVectorBuilder.class, n, c), @@ -108,7 +103,7 @@ public static KnnSearchBuilder.Builder fromXContent(XContentParser parser) throw } final String field; - final float[] queryVector; + final VectorData queryVector; final QueryVectorBuilder queryVectorBuilder; private final Supplier querySupplier; final int k; @@ -127,7 +122,26 @@ public static KnnSearchBuilder.Builder fromXContent(XContentParser parser) throw * @param numCands the number of nearest neighbor candidates to consider per shard */ public KnnSearchBuilder(String field, float[] queryVector, int k, int numCands, Float similarity) { - this(field, Objects.requireNonNull(queryVector, format("[%s] cannot be null", QUERY_VECTOR_FIELD)), null, k, numCands, similarity); + this( + field, + Objects.requireNonNull(VectorData.fromFloats(queryVector), format("[%s] cannot be null", QUERY_VECTOR_FIELD)), + null, + k, + numCands, + similarity + ); + } + + /** + * Defines a kNN search. + * + * @param field the name of the vector field to search against + * @param queryVector the query vector + * @param k the final number of nearest neighbors to return as top hits + * @param numCands the number of nearest neighbor candidates to consider per shard + */ + public KnnSearchBuilder(String field, VectorData queryVector, int k, int numCands, Float similarity) { + this(field, queryVector, null, k, numCands, similarity); } /** @@ -151,7 +165,7 @@ public KnnSearchBuilder(String field, QueryVectorBuilder queryVectorBuilder, int public KnnSearchBuilder( String field, - float[] queryVector, + VectorData queryVector, QueryVectorBuilder queryVectorBuilder, int k, int numCands, @@ -169,7 +183,7 @@ private KnnSearchBuilder( Float similarity ) { this.field = field; - this.queryVector = new float[0]; + this.queryVector = VectorData.fromFloats(new float[0]); this.queryVectorBuilder = null; this.k = k; this.numCands = numCands; @@ -181,7 +195,7 @@ private KnnSearchBuilder( private KnnSearchBuilder( String field, QueryVectorBuilder queryVectorBuilder, - float[] queryVector, + VectorData queryVector, List filterQueries, int k, int numCandidates, @@ -219,7 +233,7 @@ private KnnSearchBuilder( ); } this.field = field; - this.queryVector = queryVector == null ? new float[0] : queryVector; + this.queryVector = queryVector == null ? VectorData.fromFloats(new float[0]) : queryVector; this.queryVectorBuilder = queryVectorBuilder; this.k = k; this.numCands = numCandidates; @@ -234,7 +248,11 @@ public KnnSearchBuilder(StreamInput in) throws IOException { this.field = in.readString(); this.k = in.readVInt(); this.numCands = in.readVInt(); - this.queryVector = in.readFloatArray(); + if (in.getTransportVersion().onOrAfter(TransportVersions.KNN_EXPLICIT_BYTE_QUERY_VECTOR_PARSING)) { + this.queryVector = in.readOptionalWriteable(VectorData::new); + } else { + this.queryVector = VectorData.fromFloats(in.readFloatArray()); + } this.filterQueries = in.readNamedWriteableCollectionAsList(QueryBuilder.class); this.boost = in.readFloat(); if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_7_0)) { @@ -262,7 +280,7 @@ public QueryVectorBuilder getQueryVectorBuilder() { } // for testing only - public float[] getQueryVector() { + public VectorData getQueryVector() { return queryVector; } @@ -365,7 +383,7 @@ public boolean equals(Object o) { return k == that.k && numCands == that.numCands && Objects.equals(field, that.field) - && Arrays.equals(queryVector, that.queryVector) + && Objects.equals(queryVector, that.queryVector) && Objects.equals(queryVectorBuilder, that.queryVectorBuilder) && Objects.equals(querySupplier, that.querySupplier) && Objects.equals(filterQueries, that.filterQueries) @@ -383,7 +401,7 @@ public int hashCode() { querySupplier, queryVectorBuilder, similarity, - Arrays.hashCode(queryVector), + Objects.hashCode(queryVector), Objects.hashCode(filterQueries), innerHitBuilder, boost @@ -401,7 +419,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(queryVectorBuilder.getWriteableName(), queryVectorBuilder); builder.endObject(); } else { - builder.array(QUERY_VECTOR_FIELD.getPreferredName(), queryVector); + builder.field(QUERY_VECTOR_FIELD.getPreferredName(), queryVector); } if (similarity != null) { builder.field(VECTOR_SIMILARITY.getPreferredName(), similarity); @@ -434,7 +452,11 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(field); out.writeVInt(k); out.writeVInt(numCands); - out.writeFloatArray(queryVector); + if (out.getTransportVersion().onOrAfter(TransportVersions.KNN_EXPLICIT_BYTE_QUERY_VECTOR_PARSING)) { + out.writeOptionalWriteable(queryVector); + } else { + out.writeFloatArray(queryVector.asFloatVector()); + } out.writeNamedWriteableCollection(filterQueries); out.writeFloat(boost); if (out.getTransportVersion().before(TransportVersions.V_8_7_0) && queryVectorBuilder != null) { @@ -460,7 +482,7 @@ public void writeTo(StreamOutput out) throws IOException { public static class Builder { private String field; - private float[] queryVector; + private VectorData queryVector; private QueryVectorBuilder queryVectorBuilder; private Integer k; private Integer numCandidates; @@ -490,7 +512,7 @@ public Builder innerHit(InnerHitBuilder innerHitBuilder) { return this; } - public Builder queryVector(float[] queryVector) { + public Builder queryVector(VectorData queryVector) { this.queryVector = queryVector; return this; } diff --git a/server/src/main/java/org/elasticsearch/search/vectors/KnnVectorQueryBuilder.java b/server/src/main/java/org/elasticsearch/search/vectors/KnnVectorQueryBuilder.java index 7e65cd19638ce..149dedd59df46 100644 --- a/server/src/main/java/org/elasticsearch/search/vectors/KnnVectorQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/vectors/KnnVectorQueryBuilder.java @@ -37,7 +37,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -62,23 +61,19 @@ public class KnnVectorQueryBuilder extends AbstractQueryBuilder PARSER = new ConstructingObjectParser<>("knn", args -> { - List vector = (List) args[1]; - final float[] vectorArray; - if (vector != null) { - vectorArray = new float[vector.size()]; - for (int i = 0; i < vector.size(); i++) { - vectorArray[i] = vector.get(i); - } - } else { - vectorArray = null; - } - return new KnnVectorQueryBuilder((String) args[0], vectorArray, (Integer) args[2], (Float) args[3]); - }); + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "knn", + args -> new KnnVectorQueryBuilder((String) args[0], (VectorData) args[1], (Integer) args[2], (Float) args[3]) + ); static { PARSER.declareString(constructorArg(), FIELD_FIELD); - PARSER.declareFloatArray(constructorArg(), QUERY_VECTOR_FIELD); + PARSER.declareField( + optionalConstructorArg(), + (p, c) -> VectorData.parseXContent(p), + QUERY_VECTOR_FIELD, + ObjectParser.ValueType.OBJECT_ARRAY_STRING_OR_NUMBER + ); PARSER.declareInt(optionalConstructorArg(), NUM_CANDS_FIELD); PARSER.declareFloat(optionalConstructorArg(), VECTOR_SIMILARITY_FIELD); PARSER.declareFieldArray( @@ -95,12 +90,20 @@ public static KnnVectorQueryBuilder fromXContent(XContentParser parser) { } private final String fieldName; - private final float[] queryVector; + private final VectorData queryVector; private Integer numCands; private final List filterQueries = new ArrayList<>(); private final Float vectorSimilarity; public KnnVectorQueryBuilder(String fieldName, float[] queryVector, Integer numCands, Float vectorSimilarity) { + this(fieldName, VectorData.fromFloats(queryVector), numCands, vectorSimilarity); + } + + public KnnVectorQueryBuilder(String fieldName, byte[] queryVector, Integer numCands, Float vectorSimilarity) { + this(fieldName, VectorData.fromBytes(queryVector), numCands, vectorSimilarity); + } + + public KnnVectorQueryBuilder(String fieldName, VectorData queryVector, Integer numCands, Float vectorSimilarity) { if (numCands != null && numCands > NUM_CANDS_LIMIT) { throw new IllegalArgumentException("[" + NUM_CANDS_FIELD.getPreferredName() + "] cannot exceed [" + NUM_CANDS_LIMIT + "]"); } @@ -121,12 +124,17 @@ public KnnVectorQueryBuilder(StreamInput in) throws IOException { } else { this.numCands = in.readVInt(); } - if (in.getTransportVersion().before(TransportVersions.V_8_7_0) || in.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) { - this.queryVector = in.readFloatArray(); + if (in.getTransportVersion().onOrAfter(TransportVersions.KNN_EXPLICIT_BYTE_QUERY_VECTOR_PARSING)) { + this.queryVector = in.readOptionalWriteable(VectorData::new); } else { - in.readBoolean(); - this.queryVector = in.readFloatArray(); - in.readBoolean(); // used for byteQueryVector, which was always null + if (in.getTransportVersion().before(TransportVersions.V_8_7_0) + || in.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) { + this.queryVector = VectorData.fromFloats(in.readFloatArray()); + } else { + in.readBoolean(); + this.queryVector = VectorData.fromFloats(in.readFloatArray()); + in.readBoolean(); // used for byteQueryVector, which was always null + } } if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_2_0)) { this.filterQueries.addAll(readQueries(in)); @@ -143,7 +151,7 @@ public String getFieldName() { } @Nullable - public float[] queryVector() { + public VectorData queryVector() { return queryVector; } @@ -190,13 +198,17 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeVInt(numCands); } } - if (out.getTransportVersion().before(TransportVersions.V_8_7_0) - || out.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) { - out.writeFloatArray(queryVector); + if (out.getTransportVersion().onOrAfter(TransportVersions.KNN_EXPLICIT_BYTE_QUERY_VECTOR_PARSING)) { + out.writeOptionalWriteable(queryVector); } else { - out.writeBoolean(true); - out.writeFloatArray(queryVector); - out.writeBoolean(false); // used for byteQueryVector, which was always null + if (out.getTransportVersion().before(TransportVersions.V_8_7_0) + || out.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) { + out.writeFloatArray(queryVector.asFloatVector()); + } else { + out.writeBoolean(true); + out.writeFloatArray(queryVector.asFloatVector()); + out.writeBoolean(false); // used for byteQueryVector, which was always null + } } if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_2_0)) { writeQueries(out, filterQueries); @@ -326,13 +338,13 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { @Override protected int doHashCode() { - return Objects.hash(fieldName, Arrays.hashCode(queryVector), numCands, filterQueries, vectorSimilarity); + return Objects.hash(fieldName, Objects.hashCode(queryVector), numCands, filterQueries, vectorSimilarity); } @Override protected boolean doEquals(KnnVectorQueryBuilder other) { return Objects.equals(fieldName, other.fieldName) - && Arrays.equals(queryVector, other.queryVector) + && Objects.equals(queryVector, other.queryVector) && Objects.equals(numCands, other.numCands) && Objects.equals(filterQueries, other.filterQueries) && Objects.equals(vectorSimilarity, other.vectorSimilarity); diff --git a/server/src/main/java/org/elasticsearch/search/vectors/VectorData.java b/server/src/main/java/org/elasticsearch/search/vectors/VectorData.java new file mode 100644 index 0000000000000..a92644af1fcf5 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/vectors/VectorData.java @@ -0,0 +1,168 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.vectors; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; +import org.elasticsearch.xcontent.ToXContentFragment; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.Strings.format; + +public record VectorData(float[] floatVector, byte[] byteVector) implements Writeable, ToXContentFragment { + + private VectorData(float[] floatVector) { + this(floatVector, null); + } + + private VectorData(byte[] byteVector) { + this(null, byteVector); + } + + public VectorData(StreamInput in) throws IOException { + this(in.readOptionalFloatArray(), in.readOptionalByteArray()); + } + + public VectorData { + if (false == (floatVector == null ^ byteVector == null)) { + throw new IllegalArgumentException("please supply exactly either a float or a byte vector"); + } + } + + public byte[] asByteVector() { + if (byteVector != null) { + return byteVector; + } + DenseVectorFieldMapper.ElementType.BYTE.checkVectorBounds(floatVector); + byte[] vec = new byte[floatVector.length]; + for (int i = 0; i < floatVector.length; i++) { + vec[i] = (byte) floatVector[i]; + } + return vec; + } + + public float[] asFloatVector() { + if (floatVector != null) { + return floatVector; + } + float[] vec = new float[byteVector.length]; + for (int i = 0; i < byteVector.length; i++) { + vec[i] = byteVector[i]; + } + return vec; + } + + public void addToBuffer(ByteBuffer byteBuffer) { + if (floatVector != null) { + for (float val : floatVector) { + byteBuffer.putFloat(val); + } + } else { + byteBuffer.put(byteVector); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeOptionalFloatArray(floatVector); + out.writeOptionalByteArray(byteVector); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (floatVector != null) { + builder.startArray(); + for (float v : floatVector) { + builder.value(v); + } + builder.endArray(); + } else { + builder.value(HexFormat.of().formatHex(byteVector)); + } + return builder; + } + + @Override + public String toString() { + return floatVector != null ? Arrays.toString(floatVector) : Arrays.toString(byteVector); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + VectorData other = (VectorData) obj; + return Arrays.equals(floatVector, other.floatVector) && Arrays.equals(byteVector, other.byteVector); + } + + @Override + public int hashCode() { + return Objects.hash(Arrays.hashCode(floatVector), Arrays.hashCode(byteVector)); + } + + public static VectorData parseXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + return switch (token) { + case START_ARRAY -> parseQueryVectorArray(parser); + case VALUE_STRING -> parseHexEncodedVector(parser); + case VALUE_NUMBER -> parseNumberVector(parser); + default -> throw new ParsingException(parser.getTokenLocation(), format("Unknown type [%s] for parsing vector", token)); + }; + } + + private static VectorData parseQueryVectorArray(XContentParser parser) throws IOException { + XContentParser.Token token; + List vectorArr = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + if (token == XContentParser.Token.VALUE_NUMBER || token == XContentParser.Token.VALUE_STRING) { + vectorArr.add(parser.floatValue()); + } else { + throw new ParsingException(parser.getTokenLocation(), format("Type [%s] not supported for query vector", token)); + } + } + float[] floatVector = new float[vectorArr.size()]; + for (int i = 0; i < vectorArr.size(); i++) { + floatVector[i] = vectorArr.get(i); + } + return VectorData.fromFloats(floatVector); + } + + private static VectorData parseHexEncodedVector(XContentParser parser) throws IOException { + return VectorData.fromBytes(HexFormat.of().parseHex(parser.text())); + } + + private static VectorData parseNumberVector(XContentParser parser) throws IOException { + return VectorData.fromFloats(new float[] { parser.floatValue() }); + } + + public static VectorData fromFloats(float[] vec) { + return vec == null ? null : new VectorData(vec); + } + + public static VectorData fromBytes(byte[] vec) { + return vec == null ? null : new VectorData(vec); + } + +} diff --git a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java index c3d2d6a3f194b..27adc72fb5ed8 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java @@ -25,6 +25,7 @@ import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.DenseVectorFieldType; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.VectorSimilarity; +import org.elasticsearch.search.vectors.VectorData; import java.io.IOException; import java.util.Collections; @@ -179,7 +180,7 @@ public void testExactKnnQuery() { for (int i = 0; i < dims; i++) { queryVector[i] = randomFloat(); } - Query query = field.createExactKnnQuery(queryVector); + Query query = field.createExactKnnQuery(VectorData.fromFloats(queryVector)); assertTrue(query instanceof BooleanQuery); BooleanQuery booleanQuery = (BooleanQuery) query; boolean foundFunction = false; @@ -202,12 +203,10 @@ public void testExactKnnQuery() { Collections.emptyMap() ); byte[] queryVector = new byte[dims]; - float[] floatQueryVector = new float[dims]; for (int i = 0; i < dims; i++) { queryVector[i] = randomByte(); - floatQueryVector[i] = queryVector[i]; } - Query query = field.createExactKnnQuery(floatQueryVector); + Query query = field.createExactKnnQuery(VectorData.fromBytes(queryVector)); assertTrue(query instanceof BooleanQuery); BooleanQuery booleanQuery = (BooleanQuery) query; boolean foundFunction = false; diff --git a/server/src/test/java/org/elasticsearch/search/vectors/AbstractKnnVectorQueryBuilderTestCase.java b/server/src/test/java/org/elasticsearch/search/vectors/AbstractKnnVectorQueryBuilderTestCase.java index ad9c95b5b80c7..45ad9d514ba82 100644 --- a/server/src/test/java/org/elasticsearch/search/vectors/AbstractKnnVectorQueryBuilderTestCase.java +++ b/server/src/test/java/org/elasticsearch/search/vectors/AbstractKnnVectorQueryBuilderTestCase.java @@ -43,10 +43,12 @@ abstract class AbstractKnnVectorQueryBuilderTestCase extends AbstractQueryTestCase { private static final String VECTOR_FIELD = "vector"; private static final String VECTOR_ALIAS_FIELD = "vector_alias"; - private static final int VECTOR_DIMENSION = 3; + static final int VECTOR_DIMENSION = 3; abstract DenseVectorFieldMapper.ElementType elementType(); + abstract KnnVectorQueryBuilder createKnnVectorQueryBuilder(String fieldName, int numCands, Float similarity); + @Override protected void initializeAdditionalMappings(MapperService mapperService) throws IOException { XContentBuilder builder = XContentFactory.jsonBuilder() @@ -75,12 +77,9 @@ protected void initializeAdditionalMappings(MapperService mapperService) throws @Override protected KnnVectorQueryBuilder doCreateTestQueryBuilder() { String fieldName = randomBoolean() ? VECTOR_FIELD : VECTOR_ALIAS_FIELD; - float[] vector = new float[VECTOR_DIMENSION]; - for (int i = 0; i < vector.length; i++) { - vector[i] = elementType().equals(DenseVectorFieldMapper.ElementType.BYTE) ? randomByte() : randomFloat(); - } int numCands = randomIntBetween(DEFAULT_SIZE, 1000); - KnnVectorQueryBuilder queryBuilder = new KnnVectorQueryBuilder(fieldName, vector, numCands, randomBoolean() ? null : randomFloat()); + KnnVectorQueryBuilder queryBuilder = createKnnVectorQueryBuilder(fieldName, numCands, randomBoolean() ? null : randomFloat()); + if (randomBoolean()) { List filters = new ArrayList<>(); int numFilters = randomIntBetween(1, 5); @@ -120,11 +119,16 @@ protected void doAssertLuceneQuery(KnnVectorQueryBuilder queryBuilder, Query que Query knnVectorQueryBuilt = switch (elementType()) { case BYTE -> new ESKnnByteVectorQuery( VECTOR_FIELD, - getByteQueryVector(queryBuilder.queryVector()), + queryBuilder.queryVector().asByteVector(), + queryBuilder.numCands(), + filterQuery + ); + case FLOAT -> new ESKnnFloatVectorQuery( + VECTOR_FIELD, + queryBuilder.queryVector().asFloatVector(), queryBuilder.numCands(), filterQuery ); - case FLOAT -> new ESKnnFloatVectorQuery(VECTOR_FIELD, queryBuilder.queryVector(), queryBuilder.numCands(), filterQuery); }; if (query instanceof VectorSimilarityQuery vectorSimilarityQuery) { query = vectorSimilarityQuery.getInnerKnnQuery(); @@ -193,7 +197,8 @@ public void testMustRewrite() throws IOException { public void testBWCVersionSerializationFilters() throws IOException { KnnVectorQueryBuilder query = createTestQueryBuilder(); - KnnVectorQueryBuilder queryNoFilters = new KnnVectorQueryBuilder(query.getFieldName(), query.queryVector(), query.numCands(), null) + VectorData vectorData = VectorData.fromFloats(query.queryVector().asFloatVector()); + KnnVectorQueryBuilder queryNoFilters = new KnnVectorQueryBuilder(query.getFieldName(), vectorData, query.numCands(), null) .queryName(query.queryName()) .boost(query.boost()); TransportVersion beforeFilterVersion = TransportVersionUtils.randomVersionBetween( @@ -206,12 +211,11 @@ public void testBWCVersionSerializationFilters() throws IOException { public void testBWCVersionSerializationSimilarity() throws IOException { KnnVectorQueryBuilder query = createTestQueryBuilder(); - KnnVectorQueryBuilder queryNoSimilarity = new KnnVectorQueryBuilder( - query.getFieldName(), - query.queryVector(), - query.numCands(), - null - ).queryName(query.queryName()).boost(query.boost()).addFilterQueries(query.filterQueries()); + VectorData vectorData = VectorData.fromFloats(query.queryVector().asFloatVector()); + KnnVectorQueryBuilder queryNoSimilarity = new KnnVectorQueryBuilder(query.getFieldName(), vectorData, query.numCands(), null) + .queryName(query.queryName()) + .boost(query.boost()) + .addFilterQueries(query.filterQueries()); assertBWCSerialization(query, queryNoSimilarity, TransportVersions.V_8_7_0); } @@ -223,12 +227,11 @@ public void testBWCVersionSerializationQuery() throws IOException { TransportVersions.V_8_12_0 ); Float similarity = differentQueryVersion.before(TransportVersions.V_8_8_0) ? null : query.getVectorSimilarity(); - KnnVectorQueryBuilder queryOlderVersion = new KnnVectorQueryBuilder( - query.getFieldName(), - query.queryVector(), - query.numCands(), - similarity - ).queryName(query.queryName()).boost(query.boost()).addFilterQueries(query.filterQueries()); + VectorData vectorData = VectorData.fromFloats(query.queryVector().asFloatVector()); + KnnVectorQueryBuilder queryOlderVersion = new KnnVectorQueryBuilder(query.getFieldName(), vectorData, query.numCands(), similarity) + .queryName(query.queryName()) + .boost(query.boost()) + .addFilterQueries(query.filterQueries()); assertBWCSerialization(query, queryOlderVersion, differentQueryVersion); } @@ -245,12 +248,4 @@ private void assertBWCSerialization(QueryBuilder newQuery, QueryBuilder bwcQuery } } } - - private static byte[] getByteQueryVector(float[] queryVector) { - byte[] byteQueryVector = new byte[queryVector.length]; - for (int i = 0; i < queryVector.length; i++) { - byteQueryVector[i] = (byte) queryVector[i]; - } - return byteQueryVector; - } } diff --git a/server/src/test/java/org/elasticsearch/search/vectors/KnnByteVectorQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/search/vectors/KnnByteVectorQueryBuilderTests.java index 3bf92a60275d8..6c83700d0b29a 100644 --- a/server/src/test/java/org/elasticsearch/search/vectors/KnnByteVectorQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/vectors/KnnByteVectorQueryBuilderTests.java @@ -15,4 +15,13 @@ public class KnnByteVectorQueryBuilderTests extends AbstractKnnVectorQueryBuilde DenseVectorFieldMapper.ElementType elementType() { return DenseVectorFieldMapper.ElementType.BYTE; } + + @Override + protected KnnVectorQueryBuilder createKnnVectorQueryBuilder(String fieldName, int numCands, Float similarity) { + byte[] vector = new byte[VECTOR_DIMENSION]; + for (int i = 0; i < vector.length; i++) { + vector[i] = randomByte(); + } + return new KnnVectorQueryBuilder(fieldName, vector, numCands, similarity); + } } diff --git a/server/src/test/java/org/elasticsearch/search/vectors/KnnVectorQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/search/vectors/KnnFloatVectorQueryBuilderTests.java similarity index 56% rename from server/src/test/java/org/elasticsearch/search/vectors/KnnVectorQueryBuilderTests.java rename to server/src/test/java/org/elasticsearch/search/vectors/KnnFloatVectorQueryBuilderTests.java index 6b1166cdd16dc..eeb5244d57943 100644 --- a/server/src/test/java/org/elasticsearch/search/vectors/KnnVectorQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/vectors/KnnFloatVectorQueryBuilderTests.java @@ -10,9 +10,18 @@ import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; -public class KnnVectorQueryBuilderTests extends AbstractKnnVectorQueryBuilderTestCase { +public class KnnFloatVectorQueryBuilderTests extends AbstractKnnVectorQueryBuilderTestCase { @Override DenseVectorFieldMapper.ElementType elementType() { return DenseVectorFieldMapper.ElementType.FLOAT; } + + @Override + KnnVectorQueryBuilder createKnnVectorQueryBuilder(String fieldName, int numCands, Float similarity) { + float[] vector = new float[VECTOR_DIMENSION]; + for (int i = 0; i < vector.length; i++) { + vector[i] = randomFloat(); + } + return new KnnVectorQueryBuilder(fieldName, vector, numCands, similarity); + } } diff --git a/server/src/test/java/org/elasticsearch/search/vectors/KnnSearchBuilderTests.java b/server/src/test/java/org/elasticsearch/search/vectors/KnnSearchBuilderTests.java index c650f54321060..564c8b9d0db11 100644 --- a/server/src/test/java/org/elasticsearch/search/vectors/KnnSearchBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/vectors/KnnSearchBuilderTests.java @@ -106,7 +106,7 @@ protected KnnSearchBuilder mutateInstance(KnnSearchBuilder instance) { instance.boost ); case 1: - float[] newVector = randomValueOtherThan(instance.queryVector, () -> randomVector(5)); + float[] newVector = randomValueOtherThan(instance.queryVector.asFloatVector(), () -> randomVector(5)); return new KnnSearchBuilder(instance.field, newVector, instance.k, instance.numCands, instance.similarity).boost( instance.boost ); @@ -213,7 +213,7 @@ public void testRewrite() throws Exception { assertThat(rewritten.field, equalTo(searchBuilder.field)); assertThat(rewritten.boost, equalTo(searchBuilder.boost)); - assertThat(rewritten.queryVector, equalTo(expectedArray)); + assertThat(rewritten.queryVector.asFloatVector(), equalTo(expectedArray)); assertThat(rewritten.queryVectorBuilder, nullValue()); assertThat(rewritten.filterQueries, hasSize(1)); assertThat(rewritten.similarity, equalTo(1f)); diff --git a/server/src/test/java/org/elasticsearch/search/vectors/VectorDataTests.java b/server/src/test/java/org/elasticsearch/search/vectors/VectorDataTests.java new file mode 100644 index 0000000000000..feabc100d1007 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/vectors/VectorDataTests.java @@ -0,0 +1,199 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.vectors; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; + +public class VectorDataTests extends ESTestCase { + + private static final float DELTA = 1e-5f; + + public void testThrowsIfBothVectorsAreNull() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new VectorData(null, null)); + assertThat(ex.getMessage(), containsString("please supply exactly either a float or a byte vector")); + } + + public void testThrowsIfBothVectorsAreNonNull() { + IllegalArgumentException ex = expectThrows( + IllegalArgumentException.class, + () -> new VectorData(new float[] { 0f }, new byte[] { 1 }) + ); + assertThat(ex.getMessage(), containsString("please supply exactly either a float or a byte vector")); + } + + public void testShouldCorrectlyConvertByteToFloatIfExplicitlyRequested() { + byte[] byteVector = new byte[] { 1, 2, -127 }; + float[] expected = new float[] { 1f, 2f, -127f }; + + VectorData vectorData = new VectorData(null, byteVector); + float[] actual = vectorData.asFloatVector(); + assertArrayEquals(expected, actual, DELTA); + } + + public void testShouldThrowForDecimalsWhenConvertingToByte() { + float[] vec = new float[] { 1f, 2f, 3.1f }; + + VectorData vectorData = new VectorData(vec, null); + expectThrows(IllegalArgumentException.class, vectorData::asByteVector); + } + + public void testShouldThrowForOutsideRangeWhenConvertingToByte() { + float[] vec = new float[] { 1f, 2f, 200f }; + + VectorData vectorData = new VectorData(vec, null); + expectThrows(IllegalArgumentException.class, vectorData::asByteVector); + } + + public void testEqualsAndHashCode() { + VectorData v1 = new VectorData(new float[] { 1, 2, 3 }, null); + VectorData v2 = new VectorData(null, new byte[] { 1, 2, 3 }); + assertNotEquals(v1, v2); + assertNotEquals(v1.hashCode(), v2.hashCode()); + + VectorData v3 = new VectorData(null, new byte[] { 1, 2, 3 }); + assertEquals(v2, v3); + assertEquals(v2.hashCode(), v3.hashCode()); + } + + public void testParseHexCorrectly() throws IOException { + byte[] expected = new byte[] { 64, 10, -30, 10 }; + String toParse = "\"400ae20a\""; + try ( + XContentParser parser = XContentHelper.createParserNotCompressed( + XContentParserConfiguration.EMPTY, + new BytesArray(toParse), + XContentType.JSON + ) + ) { + parser.nextToken(); + VectorData parsed = VectorData.parseXContent(parser); + assertArrayEquals(expected, parsed.asByteVector()); + } + } + + public void testParseFloatArray() throws IOException { + float[] expected = new float[] { 1f, -1f, .1f }; + String toParse = "[1.0, -1.0, 0.1]"; + try ( + XContentParser parser = XContentHelper.createParserNotCompressed( + XContentParserConfiguration.EMPTY, + new BytesArray(toParse), + XContentType.JSON + ) + ) { + parser.nextToken(); + VectorData parsed = VectorData.parseXContent(parser); + assertArrayEquals(expected, parsed.asFloatVector(), DELTA); + } + } + + public void testParseByteArray() throws IOException { + byte[] expected = new byte[] { 64, 10, -30, 10 }; + String toParse = "[64,10,-30,10]"; + try ( + XContentParser parser = XContentHelper.createParserNotCompressed( + XContentParserConfiguration.EMPTY, + new BytesArray(toParse), + XContentType.JSON + ) + ) { + parser.nextToken(); + VectorData parsed = VectorData.parseXContent(parser); + assertArrayEquals(expected, parsed.asByteVector()); + } + } + + public void testByteThrowsForOutsideRange() throws IOException { + String toParse = "[1000]"; + try ( + XContentParser parser = XContentHelper.createParserNotCompressed( + XContentParserConfiguration.EMPTY, + new BytesArray(toParse), + XContentType.JSON + ) + ) { + parser.nextToken(); + VectorData parsed = VectorData.parseXContent(parser); + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, parsed::asByteVector); + assertThat(ex.getMessage(), containsString("vectors only support integers between [-128, 127]")); + } + } + + public void testAsByteThrowsForDecimals() throws IOException { + String toParse = "[0.1]"; + try ( + XContentParser parser = XContentHelper.createParserNotCompressed( + XContentParserConfiguration.EMPTY, + new BytesArray(toParse), + XContentType.JSON + ) + ) { + parser.nextToken(); + VectorData parsed = VectorData.parseXContent(parser); + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, parsed::asByteVector); + assertThat(ex.getMessage(), containsString("vectors only support non-decimal values but found decimal value")); + } + } + + public void testParseSingleNumber() throws IOException { + float[] expected = new float[] { 0.1f }; + String toParse = "0.1"; + try ( + XContentParser parser = XContentHelper.createParserNotCompressed( + XContentParserConfiguration.EMPTY, + new BytesArray(toParse), + XContentType.JSON + ) + ) { + parser.nextToken(); + VectorData parsed = VectorData.parseXContent(parser); + assertArrayEquals(expected, parsed.asFloatVector(), DELTA); + } + } + + public void testParseThrowsForUnknown() throws IOException { + String unknown = "{\"foo\":\"bar\"}"; + try ( + XContentParser parser = XContentHelper.createParser( + XContentParserConfiguration.EMPTY, + new BytesArray(unknown), + XContentType.JSON + ) + ) { + parser.nextToken(); + ParsingException ex = expectThrows(ParsingException.class, () -> VectorData.parseXContent(parser)); + assertThat(ex.getMessage(), containsString("Unknown type [" + XContentParser.Token.START_OBJECT + "] for parsing vector")); + } + } + + public void testFailForUnknownArrayValue() throws IOException { + String toParse = "[0.1, true]"; + try ( + XContentParser parser = XContentHelper.createParserNotCompressed( + XContentParserConfiguration.EMPTY, + new BytesArray(toParse), + XContentType.JSON + ) + ) { + parser.nextToken(); + ParsingException ex = expectThrows(ParsingException.class, () -> VectorData.parseXContent(parser)); + assertThat(ex.getMessage(), containsString("Type [" + XContentParser.Token.VALUE_BOOLEAN + "] not supported for query vector")); + } + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryVectorBuilderTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryVectorBuilderTestCase.java index b6e5c7161edc8..b327aee0931f9 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryVectorBuilderTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryVectorBuilderTestCase.java @@ -132,7 +132,7 @@ public final void testKnnSearchRewrite() throws Exception { PlainActionFuture future = new PlainActionFuture<>(); Rewriteable.rewriteAndFetch(randomFrom(serialized, searchBuilder), context, future); KnnSearchBuilder rewritten = future.get(); - assertThat(rewritten.getQueryVector(), equalTo(expected)); + assertThat(rewritten.getQueryVector().asFloatVector(), equalTo(expected)); assertThat(rewritten.getQueryVectorBuilder(), nullValue()); } } From d54593f158265d2142c1db8b766267efead5e77d Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 13 Mar 2024 09:01:29 +0100 Subject: [PATCH 30/34] Fix a downsample persistent task assignment bug (#106247) If as part of the persistent task assignment the source downsample index no longer exists, then the persistent task framework will continuously try to find an assignment and fail with IndexNotFoundException (which gets logged as a warning on elected master node). This fixes a bug in resolving the shard routing, so that if the index no longer exists any node is returned and the persistent task can fail gracefully at a later stage. The original fix via #98769 didn't get this part right. --- docs/changelog/106247.yaml | 5 + ...DownsampleShardPersistentTaskExecutor.java | 11 +- ...ampleShardPersistentTaskExecutorTests.java | 125 ++++++++++++++++++ 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/106247.yaml create mode 100644 x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/DownsampleShardPersistentTaskExecutorTests.java diff --git a/docs/changelog/106247.yaml b/docs/changelog/106247.yaml new file mode 100644 index 0000000000000..5895dffd685a4 --- /dev/null +++ b/docs/changelog/106247.yaml @@ -0,0 +1,5 @@ +pr: 106247 +summary: Fix a downsample persistent task assignment bug +area: Downsampling +type: bug +issues: [] diff --git a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/DownsampleShardPersistentTaskExecutor.java b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/DownsampleShardPersistentTaskExecutor.java index 6f110ace53fc9..883986887fc3d 100644 --- a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/DownsampleShardPersistentTaskExecutor.java +++ b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/DownsampleShardPersistentTaskExecutor.java @@ -23,6 +23,7 @@ import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamOutput; @@ -134,7 +135,7 @@ public PersistentTasksCustomMetadata.Assignment getAssignment( // If during re-assignment the source index was deleted, then we need to break out. // Returning NO_NODE_FOUND just keeps the persistent task until the source index appears again (which would never happen) // So let's return a node and then in the node operation we would just fail and stop this persistent task - var indexShardRouting = clusterState.routingTable().shardRoutingTable(params.shardId().getIndexName(), params.shardId().id()); + var indexShardRouting = findShardRoutingTable(shardId, clusterState); if (indexShardRouting == null) { var node = selectLeastLoadedNode(clusterState, candidateNodes, DiscoveryNode::canContainData); return new PersistentTasksCustomMetadata.Assignment(node.getId(), "a node to fail and stop this persistent task"); @@ -175,6 +176,14 @@ private void delegate(final AllocatedPersistentTask task, final DownsampleShardT ); } + private static IndexShardRoutingTable findShardRoutingTable(ShardId shardId, ClusterState clusterState) { + var indexRoutingTable = clusterState.routingTable().index(shardId.getIndexName()); + if (indexRoutingTable != null) { + return indexRoutingTable.shard(shardId.getId()); + } + return null; + } + static void realNodeOperation( Client client, IndicesService indicesService, diff --git a/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/DownsampleShardPersistentTaskExecutorTests.java b/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/DownsampleShardPersistentTaskExecutorTests.java new file mode 100644 index 0000000000000..06f6be27e9f3d --- /dev/null +++ b/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/DownsampleShardPersistentTaskExecutorTests.java @@ -0,0 +1,125 @@ +/* + * 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.downsample; + +import org.elasticsearch.action.downsample.DownsampleConfig; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.DataStreamTestHelper; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodeRole; +import org.elasticsearch.cluster.node.DiscoveryNodeUtils; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.routing.IndexRoutingTable; +import org.elasticsearch.cluster.routing.RoutingTable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.downsample.DownsampleShardTask; +import org.junit.Before; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; + +import static org.elasticsearch.cluster.routing.ShardRoutingState.STARTED; +import static org.elasticsearch.cluster.routing.TestShardRouting.shardRoutingBuilder; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; + +public class DownsampleShardPersistentTaskExecutorTests extends ESTestCase { + + private ClusterState initialClusterState; + private DownsampleShardPersistentTaskExecutor executor; + + @Before + public void setup() { + Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); + Instant start = now.minus(2, ChronoUnit.HOURS); + Instant end = now.plus(40, ChronoUnit.MINUTES); + initialClusterState = DataStreamTestHelper.getClusterStateWithDataStream("metrics-app1", List.of(new Tuple<>(start, end))); + executor = new DownsampleShardPersistentTaskExecutor(mock(Client.class), DownsampleShardTask.TASK_NAME, mock(Executor.class)); + } + + public void testGetAssignment() { + var backingIndex = initialClusterState.metadata().dataStreams().get("metrics-app1").getWriteIndex(); + var node = newNode(); + var shardId = new ShardId(backingIndex, 0); + var clusterState = ClusterState.builder(initialClusterState) + .nodes(new DiscoveryNodes.Builder().add(node).build()) + .routingTable( + RoutingTable.builder() + .add( + IndexRoutingTable.builder(backingIndex) + .addShard(shardRoutingBuilder(shardId, node.getId(), true, STARTED).withRecoverySource(null).build()) + ) + ) + .build(); + + var params = new DownsampleShardTaskParams( + new DownsampleConfig(new DateHistogramInterval("1h")), + shardId.getIndexName(), + 1, + 1, + shardId, + Strings.EMPTY_ARRAY, + Strings.EMPTY_ARRAY, + Strings.EMPTY_ARRAY + ); + var result = executor.getAssignment(params, Set.of(node), clusterState); + assertThat(result.getExecutorNode(), equalTo(node.getId())); + } + + public void testGetAssignmentMissingIndex() { + var backingIndex = initialClusterState.metadata().dataStreams().get("metrics-app1").getWriteIndex(); + var node = newNode(); + var shardId = new ShardId(backingIndex, 0); + var clusterState = ClusterState.builder(initialClusterState) + .nodes(new DiscoveryNodes.Builder().add(node).build()) + .routingTable( + RoutingTable.builder() + .add( + IndexRoutingTable.builder(backingIndex) + .addShard(shardRoutingBuilder(shardId, node.getId(), true, STARTED).withRecoverySource(null).build()) + ) + ) + .build(); + + var missingShardId = new ShardId(new Index("another_index", "uid"), 0); + var params = new DownsampleShardTaskParams( + new DownsampleConfig(new DateHistogramInterval("1h")), + missingShardId.getIndexName(), + 1, + 1, + missingShardId, + Strings.EMPTY_ARRAY, + Strings.EMPTY_ARRAY, + Strings.EMPTY_ARRAY + ); + var result = executor.getAssignment(params, Set.of(node), clusterState); + assertThat(result.getExecutorNode(), equalTo(node.getId())); + assertThat(result.getExplanation(), equalTo("a node to fail and stop this persistent task")); + } + + private static DiscoveryNode newNode() { + return DiscoveryNodeUtils.create( + "node_" + UUIDs.randomBase64UUID(random()), + buildNewFakeTransportAddress(), + Map.of(), + DiscoveryNodeRole.roles() + ); + } + +} From e944619e015030437c27008f42831c3b75dfa796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20FOUCRET?= Date: Wed, 13 Mar 2024 09:05:47 +0100 Subject: [PATCH 31/34] Fix typo in the LTR guide. (#106276) --- .../search-your-data/learning-to-rank-model-training.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/search/search-your-data/learning-to-rank-model-training.asciidoc b/docs/reference/search/search-your-data/learning-to-rank-model-training.asciidoc index fb026578bc00d..6525147839412 100644 --- a/docs/reference/search/search-your-data/learning-to-rank-model-training.asciidoc +++ b/docs/reference/search/search-your-data/learning-to-rank-model-training.asciidoc @@ -109,7 +109,7 @@ The FeatureLogger provides an `extract_features` method which enables you to ext [source,python] ---- feature_logger.extract_features( - query_params:{"query": "foo"}, + query_params={"query": "foo"}, doc_ids=["doc-1", "doc-2"] ) ---- From e0087e9b0dc70f7887d2ff50afda3bb4f21507fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Wed, 13 Mar 2024 09:34:28 +0100 Subject: [PATCH 32/34] [Transform] Do not log error on node restart when the transform is already failed. (#106171) --- docs/changelog/106171.yaml | 6 ++++++ .../common/notifications/AbstractAuditor.java | 6 +++--- .../CannotStartFailedTransformException.java | 16 ++++++++++++++++ .../TransformPersistentTasksExecutor.java | 15 ++++++++++++--- .../transform/transforms/TransformTask.java | 5 ++--- 5 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 docs/changelog/106171.yaml create mode 100644 x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/CannotStartFailedTransformException.java diff --git a/docs/changelog/106171.yaml b/docs/changelog/106171.yaml new file mode 100644 index 0000000000000..9daf1b9acd994 --- /dev/null +++ b/docs/changelog/106171.yaml @@ -0,0 +1,6 @@ +pr: 106171 +summary: Do not log error on node restart when the transform is already failed +area: Transform +type: enhancement +issues: + - 106168 diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/notifications/AbstractAuditor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/notifications/AbstractAuditor.java index 8371f018a6bde..d02fb85f46b1e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/notifications/AbstractAuditor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/notifications/AbstractAuditor.java @@ -103,15 +103,15 @@ public void audit(Level level, String resourceId, String message) { } public void info(String resourceId, String message) { - indexDoc(messageFactory.newMessage(resourceId, message, Level.INFO, new Date(), nodeName)); + audit(Level.INFO, resourceId, message); } public void warning(String resourceId, String message) { - indexDoc(messageFactory.newMessage(resourceId, message, Level.WARNING, new Date(), nodeName)); + audit(Level.WARNING, resourceId, message); } public void error(String resourceId, String message) { - indexDoc(messageFactory.newMessage(resourceId, message, Level.ERROR, new Date(), nodeName)); + audit(Level.ERROR, resourceId, message); } private static void onIndexResponse(DocWriteResponse response) { diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/CannotStartFailedTransformException.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/CannotStartFailedTransformException.java new file mode 100644 index 0000000000000..674af59f6ad2d --- /dev/null +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/CannotStartFailedTransformException.java @@ -0,0 +1,16 @@ +/* + * 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.transform.transforms; + +import org.elasticsearch.ElasticsearchException; + +class CannotStartFailedTransformException extends ElasticsearchException { + CannotStartFailedTransformException(String msg) { + super(msg); + } +} diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutor.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutor.java index 2fc001599c78d..ae9678893df9a 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutor.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutor.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.transform.transforms; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.util.SetOnce; @@ -62,6 +63,8 @@ import java.util.stream.Collectors; import static org.elasticsearch.core.Strings.format; +import static org.elasticsearch.xpack.core.common.notifications.Level.ERROR; +import static org.elasticsearch.xpack.core.common.notifications.Level.INFO; import static org.elasticsearch.xpack.transform.transforms.TransformNodes.nodeCanRunThisTransform; public class TransformPersistentTasksExecutor extends PersistentTasksExecutor { @@ -203,11 +206,17 @@ protected void nodeOperation(AllocatedPersistentTask task, @Nullable TransformTa ActionListener startTaskListener = ActionListener.wrap( response -> logger.info("[{}] successfully completed and scheduled task in node operation", transformId), failure -> { - auditor.error( + // If the transform is failed then there is no need to log an error on every node restart as the error had already been + // logged when the transform first failed. + boolean logErrorAsInfo = failure instanceof CannotStartFailedTransformException; + auditor.audit( + logErrorAsInfo ? INFO : ERROR, transformId, - "Failed to start transform. " + "Please stop and attempt to start again. Failure: " + failure.getMessage() + "Failed to start transform. Please stop and attempt to start again. Failure: " + failure.getMessage() ); - logger.error("Failed to start task [" + transformId + "] in node operation", failure); + logger.atLevel(logErrorAsInfo ? Level.INFO : Level.ERROR) + .withThrowable(failure) + .log("[{}] Failed to start task in node operation", transformId); } ); diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java index 8a78be8417020..ac81579e8dd71 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java @@ -240,9 +240,8 @@ void start(Long startingCheckpoint, ActionListener Date: Wed, 13 Mar 2024 09:03:42 +0000 Subject: [PATCH 33/34] Reduce usage of `SAME` threadpool name (#106279) These days in most places where we mention `SAME` we're immediately looking up the corresponding executor, and therefore can just mention `EsExecutors#DIRECT_EXECUTOR_SERVICE` directly instead. --- .../http/netty4/Netty4PipeliningIT.java | 9 ++- .../TransportFieldCapabilitiesAction.java | 11 +-- .../search/MultiSearchActionTookTests.java | 9 +-- .../TransportMultiSearchActionTests.java | 13 ++-- .../JoinValidationServiceTests.java | 4 +- .../cluster/service/MasterServiceTests.java | 3 +- .../CompositeIndexEventListenerTests.java | 9 +-- .../ClusterConnectionManagerTests.java | 4 +- .../transport/TransportActionProxyTests.java | 25 +++---- .../org/elasticsearch/test/ESTestCase.java | 17 +++++ .../AbstractSimpleTransportTestCase.java | 68 ++++++------------- .../saml/TransportSamlAuthenticateAction.java | 5 +- .../TransportSamlInvalidateSessionAction.java | 5 +- .../saml/TransportSamlLogoutAction.java | 11 +-- ...nsportSamlPrepareAuthenticationAction.java | 5 +- ...curityServerTransportInterceptorTests.java | 2 +- 16 files changed, 81 insertions(+), 119 deletions(-) diff --git a/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4PipeliningIT.java b/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4PipeliningIT.java index 653733b064ba9..130a1168d455c 100644 --- a/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4PipeliningIT.java +++ b/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4PipeliningIT.java @@ -42,7 +42,6 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xcontent.ToXContentObject; import java.io.IOException; @@ -243,9 +242,8 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli if (failAfterBytes < 0) { throw new IllegalArgumentException("[" + FAIL_AFTER_BYTES_PARAM + "] must be present and non-negative"); } - return channel -> client.threadPool() - .executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)) - .execute(() -> channel.sendResponse(RestResponse.chunked(RestStatus.OK, new ChunkedRestResponseBody() { + return channel -> randomExecutor(client.threadPool()).execute( + () -> channel.sendResponse(RestResponse.chunked(RestStatus.OK, new ChunkedRestResponseBody() { int bytesRemaining = failAfterBytes; @Override @@ -270,7 +268,8 @@ public ReleasableBytesReference encodeChunk(int sizeHint, Recycler rec public String getResponseContentTypeString() { return RestResponse.TEXT_CONTENT_TYPE; } - }, null))); + }, null)) + ); } }); } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index ad39066c253a5..e28434623601a 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.shard.ShardId; @@ -91,14 +92,8 @@ public TransportFieldCapabilitiesAction( IndicesService indicesService, IndexNameExpressionResolver indexNameExpressionResolver ) { - // TODO replace SAME when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 - super( - NAME, - transportService, - actionFilters, - FieldCapabilitiesRequest::new, - transportService.getThreadPool().executor(ThreadPool.Names.SAME) - ); + // TODO replace DIRECT_EXECUTOR_SERVICE when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 + super(NAME, transportService, actionFilters, FieldCapabilitiesRequest::new, EsExecutors.DIRECT_EXECUTOR_SERVICE); this.threadPool = threadPool; this.searchCoordinationExecutor = threadPool.executor(ThreadPool.Names.SEARCH_COORDINATION); this.transportService = transportService; diff --git a/server/src/test/java/org/elasticsearch/action/search/MultiSearchActionTookTests.java b/server/src/test/java/org/elasticsearch/action/search/MultiSearchActionTookTests.java index f682e75b89a07..fde0f190737e1 100644 --- a/server/src/test/java/org/elasticsearch/action/search/MultiSearchActionTookTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/MultiSearchActionTookTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.node.DiscoveryNodeUtils; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.Randomness; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.AtomicArray; @@ -31,14 +30,12 @@ import org.junit.Before; import org.junit.BeforeClass; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; -import java.util.List; import java.util.Queue; import java.util.Set; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -136,9 +133,7 @@ private TransportMultiSearchAction createTransportMultiSearchAction(boolean cont final int availableProcessors = Runtime.getRuntime().availableProcessors(); AtomicInteger counter = new AtomicInteger(); - final List threadPoolNames = Arrays.asList(ThreadPool.Names.GENERIC, ThreadPool.Names.SAME); - Randomness.shuffle(threadPoolNames); - final ExecutorService commonExecutor = threadPool.executor(threadPoolNames.get(0)); + final Executor commonExecutor = randomExecutor(threadPool); final Set requests = Collections.newSetFromMap(Collections.synchronizedMap(new IdentityHashMap<>())); NodeClient client = new NodeClient(settings, threadPool) { diff --git a/server/src/test/java/org/elasticsearch/action/search/TransportMultiSearchActionTests.java b/server/src/test/java/org/elasticsearch/action/search/TransportMultiSearchActionTests.java index d04e41c83699d..561bca2695337 100644 --- a/server/src/test/java/org/elasticsearch/action/search/TransportMultiSearchActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/TransportMultiSearchActionTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.Randomness; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.search.SearchResponseUtils; import org.elasticsearch.tasks.Task; import org.elasticsearch.telemetry.metric.MeterRegistry; @@ -37,7 +38,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -143,10 +144,10 @@ public void testBatchExecute() throws ExecutionException, InterruptedException { AtomicInteger counter = new AtomicInteger(); AtomicReference errorHolder = new AtomicReference<>(); // randomize whether or not requests are executed asynchronously - final List threadPoolNames = Arrays.asList(ThreadPool.Names.GENERIC, ThreadPool.Names.SAME); - Randomness.shuffle(threadPoolNames); - final ExecutorService commonExecutor = threadPool.executor(threadPoolNames.get(0)); - final ExecutorService rarelyExecutor = threadPool.executor(threadPoolNames.get(1)); + final List executorServices = Arrays.asList(threadPool.generic(), EsExecutors.DIRECT_EXECUTOR_SERVICE); + Randomness.shuffle(executorServices); + final Executor commonExecutor = executorServices.get(0); + final Executor rarelyExecutor = executorServices.get(1); final Set requests = Collections.newSetFromMap(Collections.synchronizedMap(new IdentityHashMap<>())); NodeClient client = new NodeClient(settings, threadPool) { @Override @@ -164,7 +165,7 @@ public void search(final SearchRequest request, final ActionListener { counter.decrementAndGet(); var response = SearchResponseUtils.emptyWithTotalHits( diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/JoinValidationServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/JoinValidationServiceTests.java index 9b1ce4611169b..5d2193c5ed3da 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/JoinValidationServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/JoinValidationServiceTests.java @@ -99,9 +99,7 @@ public TransportVersion getTransportVersion() { @Override public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) throws TransportException { - final var executor = threadPool.executor( - randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC, ThreadPool.Names.CLUSTER_COORDINATION) - ); + final var executor = randomExecutor(threadPool, ThreadPool.Names.CLUSTER_COORDINATION); executor.execute(new AbstractRunnable() { @Override public void onFailure(Exception e) { diff --git a/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java index 453d9bfecf2ab..6a24c8fc88078 100644 --- a/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java @@ -175,8 +175,7 @@ protected ExecutorService createThreadPoolExecutor() { masterService.setClusterStatePublisher((clusterStatePublicationEvent, publishListener, ackListener) -> { clusterStateRef.set(clusterStatePublicationEvent.getNewState()); ClusterServiceUtils.setAllElapsedMillis(clusterStatePublicationEvent); - threadPool.executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)) - .execute(() -> publishListener.onResponse(null)); + randomExecutor(threadPool).execute(() -> publishListener.onResponse(null)); }); masterService.setClusterStateSupplier(clusterStateRef::get); masterService.start(); diff --git a/server/src/test/java/org/elasticsearch/index/CompositeIndexEventListenerTests.java b/server/src/test/java/org/elasticsearch/index/CompositeIndexEventListenerTests.java index 10778cfb86e46..2ee721900b691 100644 --- a/server/src/test/java/org/elasticsearch/index/CompositeIndexEventListenerTests.java +++ b/server/src/test/java/org/elasticsearch/index/CompositeIndexEventListenerTests.java @@ -18,7 +18,6 @@ import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardTestCase; import org.elasticsearch.test.MockLogAppender; -import org.elasticsearch.threadpool.ThreadPool; import org.hamcrest.Matchers; import java.util.concurrent.TimeUnit; @@ -60,9 +59,7 @@ public void beforeIndexShardRecovery( listener.onResponse(null); } else { // fails the listener sometimes - shard.getThreadPool() - .executor(randomFrom(ThreadPool.Names.GENERIC, ThreadPool.Names.SAME)) - .execute(ActionRunnable.run(listener, this::runStep)); + randomExecutor(shard.getThreadPool()).execute(ActionRunnable.run(listener, this::runStep)); } } @@ -129,9 +126,7 @@ public void afterIndexShardRecovery(IndexShard indexShard, ActionListener listener.onResponse(null); } else { // fails the listener sometimes - shard.getThreadPool() - .executor(randomFrom(ThreadPool.Names.GENERIC, ThreadPool.Names.SAME)) - .execute(ActionRunnable.run(listener, this::runStep)); + randomExecutor(shard.getThreadPool()).execute(ActionRunnable.run(listener, this::runStep)); } } diff --git a/server/src/test/java/org/elasticsearch/transport/ClusterConnectionManagerTests.java b/server/src/test/java/org/elasticsearch/transport/ClusterConnectionManagerTests.java index 7ef22abf91b0f..c197e4c296ef6 100644 --- a/server/src/test/java/org/elasticsearch/transport/ClusterConnectionManagerTests.java +++ b/server/src/test/java/org/elasticsearch/transport/ClusterConnectionManagerTests.java @@ -469,7 +469,7 @@ public void testConcurrentConnectsAndDisconnects() throws Exception { final ConnectionManager.ConnectionValidator validator = (c, p, l) -> { assertTrue(validatorPermits.tryAcquire()); - threadPool.executor(randomFrom(ThreadPool.Names.GENERIC, ThreadPool.Names.SAME)).execute(() -> { + randomExecutor(threadPool).execute(() -> { try { l.onResponse(null); } finally { @@ -547,7 +547,7 @@ public void testConcurrentConnectsAndCloses() throws Exception { final ConnectionManager.ConnectionValidator validator = (c, p, l) -> { assertTrue(validatorPermits.tryAcquire()); - threadPool.executor(randomFrom(ThreadPool.Names.GENERIC, ThreadPool.Names.SAME)).execute(() -> { + randomExecutor(threadPool).execute(() -> { try { l.onResponse(null); } finally { diff --git a/server/src/test/java/org/elasticsearch/transport/TransportActionProxyTests.java b/server/src/test/java/org/elasticsearch/transport/TransportActionProxyTests.java index 29590cfca5ead..3346bd40aec5a 100644 --- a/server/src/test/java/org/elasticsearch/transport/TransportActionProxyTests.java +++ b/server/src/test/java/org/elasticsearch/transport/TransportActionProxyTests.java @@ -266,22 +266,17 @@ public void testSendLocalRequest() throws Exception { final CountDownLatch latch = new CountDownLatch(2); final boolean cancellable = randomBoolean(); - serviceB.registerRequestHandler( - "internal:test", - threadPool.executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)), - SimpleTestRequest::new, - (request, channel, task) -> { - try { - assertThat(task instanceof CancellableTask, equalTo(cancellable)); - assertEquals(request.sourceNode, "TS_A"); - final SimpleTestResponse responseB = new SimpleTestResponse("TS_B"); - channel.sendResponse(responseB); - response.set(responseB); - } finally { - latch.countDown(); - } + serviceB.registerRequestHandler("internal:test", randomExecutor(threadPool), SimpleTestRequest::new, (request, channel, task) -> { + try { + assertThat(task instanceof CancellableTask, equalTo(cancellable)); + assertEquals(request.sourceNode, "TS_A"); + final SimpleTestResponse responseB = new SimpleTestResponse("TS_B"); + channel.sendResponse(responseB); + response.set(responseB); + } finally { + latch.countDown(); } - ); + }); TransportActionProxy.registerProxyAction(serviceB, "internal:test", cancellable, SimpleTestResponse::new); AbstractSimpleTransportTestCase.connectToNode(serviceA, nodeB); diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 052b9a7165a6c..f1db2946aa572 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -73,6 +73,7 @@ import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.MockBigArrays; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.XContentHelper; @@ -168,6 +169,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -1196,6 +1198,21 @@ public static String randomDateFormatterPattern() { return randomFrom(FormatNames.values()).getName(); } + /** + * Randomly choose between {@link EsExecutors#DIRECT_EXECUTOR_SERVICE} (which does not fork), {@link ThreadPool#generic}, and one of the + * other named threadpool executors. + */ + public static Executor randomExecutor(ThreadPool threadPool, String... otherExecutorNames) { + final var choice = between(0, otherExecutorNames.length + 1); + if (choice < otherExecutorNames.length) { + return threadPool.executor(otherExecutorNames[choice]); + } else if (choice == otherExecutorNames.length) { + return threadPool.generic(); + } else { + return EsExecutors.DIRECT_EXECUTOR_SERVICE; + } + } + /** * helper to randomly perform on consumer with value */ diff --git a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java index 0f87ca4684c58..40c48a4d3fcde 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java @@ -928,19 +928,14 @@ public void onNodeDisconnected(DiscoveryNode node, Transport.Connection connecti public void testConcurrentSendRespondAndDisconnect() throws BrokenBarrierException, InterruptedException { Set sendingErrors = ConcurrentCollections.newConcurrentSet(); Set responseErrors = ConcurrentCollections.newConcurrentSet(); - serviceA.registerRequestHandler( - "internal:test", - threadPool.executor(randomBoolean() ? ThreadPool.Names.SAME : ThreadPool.Names.GENERIC), - TestRequest::new, - (request, channel, task) -> { - try { - channel.sendResponse(new TestResponse((String) null)); - } catch (Exception e) { - logger.info("caught exception while responding", e); - responseErrors.add(e); - } + serviceA.registerRequestHandler("internal:test", randomExecutor(threadPool), TestRequest::new, (request, channel, task) -> { + try { + channel.sendResponse(new TestResponse((String) null)); + } catch (Exception e) { + logger.info("caught exception while responding", e); + responseErrors.add(e); } - ); + }); final TransportRequestHandler ignoringRequestHandler = (request, channel, task) -> { try { channel.sendResponse(new TestResponse((String) null)); @@ -2225,30 +2220,15 @@ public Executor executor() { } } - serviceB.registerRequestHandler( - "internal:action1", - threadPool.executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)), - TestRequest::new, - new TestRequestHandler(serviceB) - ); - serviceC.registerRequestHandler( - "internal:action1", - threadPool.executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)), - TestRequest::new, - new TestRequestHandler(serviceC) - ); - serviceA.registerRequestHandler( - "internal:action1", - threadPool.executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)), - TestRequest::new, - new TestRequestHandler(serviceA) - ); + serviceB.registerRequestHandler("internal:action1", randomExecutor(threadPool), TestRequest::new, new TestRequestHandler(serviceB)); + serviceC.registerRequestHandler("internal:action1", randomExecutor(threadPool), TestRequest::new, new TestRequestHandler(serviceC)); + serviceA.registerRequestHandler("internal:action1", randomExecutor(threadPool), TestRequest::new, new TestRequestHandler(serviceA)); int iters = randomIntBetween(30, 60); CountDownLatch allRequestsDone = new CountDownLatch(iters); class TestResponseHandler implements TransportResponseHandler { private final int id; - private final String executor = randomBoolean() ? ThreadPool.Names.SAME : ThreadPool.Names.GENERIC; + private final Executor executor = randomExecutor(threadPool); TestResponseHandler(int id) { this.id = id; @@ -2277,7 +2257,7 @@ public void handleException(TransportException exp) { @Override public Executor executor() { - return threadPool.executor(executor); + return executor; } } @@ -2311,19 +2291,14 @@ public Executor executor() { } public void testRegisterHandlerTwice() { - serviceB.registerRequestHandler( - "internal:action1", - threadPool.executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)), - TestRequest::new, - (request, message, task) -> { - throw new AssertionError("boom"); - } - ); + serviceB.registerRequestHandler("internal:action1", randomExecutor(threadPool), TestRequest::new, (request, message, task) -> { + throw new AssertionError("boom"); + }); expectThrows( IllegalArgumentException.class, () -> serviceB.registerRequestHandler( "internal:action1", - threadPool.executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)), + randomExecutor(threadPool), TestRequest::new, (request, message, task) -> { throw new AssertionError("boom"); @@ -2331,14 +2306,9 @@ public void testRegisterHandlerTwice() { ) ); - serviceA.registerRequestHandler( - "internal:action1", - threadPool.executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)), - TestRequest::new, - (request, message, task) -> { - throw new AssertionError("boom"); - } - ); + serviceA.registerRequestHandler("internal:action1", randomExecutor(threadPool), TestRequest::new, (request, message, task) -> { + throw new AssertionError("boom"); + }); } public void testHandshakeWithIncompatVersion() { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java index df0f199f03aba..590757f9d7d32 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.TimeValue; import org.elasticsearch.tasks.Task; @@ -51,13 +52,13 @@ public TransportSamlAuthenticateAction( TokenService tokenService, SecurityContext securityContext ) { - // TODO replace SAME when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 + // TODO replace DIRECT_EXECUTOR_SERVICE when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 super( SamlAuthenticateAction.NAME, transportService, actionFilters, SamlAuthenticateRequest::new, - threadPool.executor(ThreadPool.Names.SAME) + EsExecutors.DIRECT_EXECUTOR_SERVICE ); this.threadPool = threadPool; this.authenticationService = authenticationService; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionAction.java index 44b4302b7f554..ade43984c6bab 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionAction.java @@ -15,6 +15,7 @@ import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -59,13 +60,13 @@ public TransportSamlInvalidateSessionAction( TokenService tokenService, Realms realms ) { - // TODO replace SAME when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 + // TODO replace DIRECT_EXECUTOR_SERVICE when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 super( SamlInvalidateSessionAction.NAME, transportService, actionFilters, SamlInvalidateSessionRequest::new, - transportService.getThreadPool().executor(ThreadPool.Names.SAME) + EsExecutors.DIRECT_EXECUTOR_SERVICE ); this.tokenService = tokenService; this.realms = realms; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutAction.java index fc73514f38ffc..f5a55da7b1916 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutAction.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -50,14 +51,8 @@ public TransportSamlLogoutAction( Realms realms, TokenService tokenService ) { - // TODO replace SAME when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 - super( - SamlLogoutAction.NAME, - transportService, - actionFilters, - SamlLogoutRequest::new, - transportService.getThreadPool().executor(ThreadPool.Names.SAME) - ); + // TODO replace DIRECT_EXECUTOR_SERVICE when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 + super(SamlLogoutAction.NAME, transportService, actionFilters, SamlLogoutRequest::new, EsExecutors.DIRECT_EXECUTOR_SERVICE); this.realms = realms; this.tokenService = tokenService; this.genericExecutor = transportService.getThreadPool().generic(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlPrepareAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlPrepareAuthenticationAction.java index 4458f3ac304ed..e18ca43c018f5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlPrepareAuthenticationAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlPrepareAuthenticationAction.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -41,13 +42,13 @@ public final class TransportSamlPrepareAuthenticationAction extends HandledTrans @Inject public TransportSamlPrepareAuthenticationAction(TransportService transportService, ActionFilters actionFilters, Realms realms) { - // TODO replace SAME when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 + // TODO replace DIRECT_EXECUTOR_SERVICE when removing workaround for https://github.com/elastic/elasticsearch/issues/97916 super( SamlPrepareAuthenticationAction.NAME, transportService, actionFilters, SamlPrepareAuthenticationRequest::new, - transportService.getThreadPool().executor(ThreadPool.Names.SAME) + EsExecutors.DIRECT_EXECUTOR_SERVICE ); this.realms = realms; this.genericExecutor = transportService.getThreadPool().generic(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java index d49c1be8a7e0a..6d5ba44ccabf7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java @@ -566,7 +566,7 @@ public void testProfileSecuredRequestHandlerDecrementsRefCountOnFailure() throws logger, TransportDeleteIndexAction.TYPE.name(), randomBoolean(), - threadPool.executor(randomBoolean() ? ThreadPool.Names.SAME : ThreadPool.Names.GENERIC), + randomExecutor(threadPool), (request, channel, task) -> fail("should fail at destructive operations check to trigger listener failure"), Map.of( profileName, From ab8f4351b2c76ebb4662c582272a85282249ebb7 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Wed, 13 Mar 2024 10:30:16 +0100 Subject: [PATCH 34/34] Additional logging for testRestoreSnapshotAllocationDoesNotExceedWatermarkWithMultipleShards (#106255) --- .../allocation/decider/DiskThresholdDeciderIT.java | 9 ++++----- .../allocator/DesiredBalanceComputerTests.java | 4 ---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderIT.java index d664d4ab352d9..fff708bbddc1e 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderIT.java @@ -164,7 +164,10 @@ public void testRestoreSnapshotAllocationDoesNotExceedWatermark() throws Excepti @TestIssueLogging( value = "org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceComputer:TRACE," + "org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceReconciler:DEBUG," - + "org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceShardsAllocator:TRACE", + + "org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceShardsAllocator:TRACE," + + "org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator:TRACE," + + "org.elasticsearch.cluster.routing.allocation.decider.AllocationDeciders:TRACE," + + "org.elasticsearch.cluster.routing.allocation.decider.DiskThresholdDecider:TRACE", issueUrl = "https://github.com/elastic/elasticsearch/issues/105331" ) public void testRestoreSnapshotAllocationDoesNotExceedWatermarkWithMultipleShards() throws Exception { @@ -291,10 +294,6 @@ public Set getShardIdsWithSizeSmallerOrEqual(long size) { public Set getSmallestShardIds() { return getShardIdsWithSizeSmallerOrEqual(getSmallestShardSize()); } - - public Set getAllShardIds() { - return sizes.stream().map(ShardSize::shardId).collect(toSet()); - } } private record ShardSize(ShardId shardId, long size) {} diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java index 4fb1093698430..989e810bbc2b8 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java @@ -1300,10 +1300,6 @@ public ShardAllocationDecision decideShardAllocation(ShardRouting shard, Routing }, DesiredBalanceComputer.class, expectation); } - private static Map.Entry indexSize(ClusterState clusterState, String name, long size, boolean primary) { - return Map.entry(shardIdentifierFromRouting(findShardId(clusterState, name), primary), size); - } - private static ShardId findShardId(ClusterState clusterState, String name) { return clusterState.getRoutingTable().index(name).shard(0).shardId(); }