This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit 3fb538e9d927e68d45de4f35fd9cc9b646b2bdb3 Author: Serhiy Bzhezytskyy <[email protected]> AuthorDate: Thu Oct 8 19:11:47 2026 +0300 SOLR-18519: Port StreamTool to picocli (#5043) (cherry picked from commit 7d17c6500ef5425419b337a5f82e9da92284e4e3) --- .../SOLR-17697-picocli-experimental-cli.yml | 2 + .../core/src/java/org/apache/solr/cli/SolrCLI.java | 3 +- .../src/java/org/apache/solr/cli/StreamTool.java | 233 ++++++++++++++++++++- .../org/apache/solr/cli/StreamToolPicocliTest.java | 97 +++++++++ .../test/org/apache/solr/cli/StreamToolTest.java | 44 +++- .../modules/deployment-guide/deployment-nav.adoc | 1 + .../modules/deployment-guide/pages/cli/index.adoc | 1 + .../deployment-guide/pages/cli/solr-stream.adoc | 110 ++++++++++ 8 files changed, 482 insertions(+), 9 deletions(-) diff --git a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml index ffdd9051a75..58cd2b4142a 100644 --- a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml +++ b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml @@ -25,3 +25,5 @@ links: url: https://issues.apache.org/jira/browse/SOLR-18514 - name: SOLR-18516 url: https://issues.apache.org/jira/browse/SOLR-18516 + - name: SOLR-18519 + url: https://issues.apache.org/jira/browse/SOLR-18519 diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 8e5619c39f1..7141ede40bf 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -89,7 +89,8 @@ import org.slf4j.LoggerFactory; ExportTool.class, HealthcheckTool.class, PostTool.class, - PostLogsTool.class + PostLogsTool.class, + StreamTool.class }) public class SolrCLI implements CLIO, java.util.concurrent.Callable<Integer> { diff --git a/solr/core/src/java/org/apache/solr/cli/StreamTool.java b/solr/core/src/java/org/apache/solr/cli/StreamTool.java index da8beec6657..79870742578 100644 --- a/solr/core/src/java/org/apache/solr/cli/StreamTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StreamTool.java @@ -37,6 +37,7 @@ import java.util.Set; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; +import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.io.SolrClientCache; import org.apache.solr.client.solrj.io.Tuple; @@ -53,10 +54,25 @@ import org.apache.solr.client.solrj.io.stream.expr.StreamFactory; import org.apache.solr.client.solrj.jetty.HttpJettySolrClient; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.common.util.IOUtils; import org.apache.solr.handler.CatStream; /** Supports stream command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") [email protected]( + name = "stream", + description = + "The Stream command runs a streaming expression in Solr or in the CLI process and prints" + + " the results.", + footerHeading = "%nExamples:%n", + footer = { + " # Run a streaming expression against the techproducts collection", + " bin/solr stream -c techproducts 'search(techproducts,q=\"iPod\",fl=\"name,price\")'", + "", + " # Run a streaming expression stored in a .expr file", + " bin/solr stream --header -c techproducts stream.expr" + }) public class StreamTool extends ToolBase { /** @@ -76,6 +92,87 @@ public class StreamTool extends ToolBase { String collection, String credentials) {} + // --- picocli fields --- + // Named distinctly from the record accessors above where a bare name would shadow one. + + @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") + private ConnectionOptions connectionOptions; + + @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions; + + /** Where {@code --execution} runs the expression: in the CLI process or on a Solr node. */ + enum Execution { + local, + remote + } + + /** Accepts the values of {@link Execution} in any case, as the commons-cli path does. */ + static class ExecutionConverter implements picocli.CommandLine.ITypeConverter<Execution> { + @Override + public Execution convert(String value) { + try { + return Execution.valueOf(value.toLowerCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new picocli.CommandLine.TypeConversionException( + "expected one of " + Arrays.toString(Execution.values()) + " but was '" + value + "'"); + } + } + } + + @picocli.CommandLine.Option( + names = "--execution", + defaultValue = "remote", + paramLabel = "ENVIRONMENT", + converter = ExecutionConverter.class, + description = + "Where to run the expression, one of: ${COMPLETION-CANDIDATES}. 'local' is the CLI" + + " process, 'remote' is a Solr server. Default is '${DEFAULT-VALUE}'.") + private Execution executionOpt; + + @picocli.CommandLine.Option( + names = {"-c", "--name"}, + paramLabel = "name", + description = + "Name of the specific collection to execute expression on if the execution is set to" + + " 'remote'. Required for 'remote' execution environment.") + private String collectionOpt; + + @picocli.CommandLine.Option( + names = "--fields", + paramLabel = "FIELDS", + description = + "The fields in the tuples to output. Defaults to fields in the first tuple of result" + + " set.") + private String fieldsOpt; + + @picocli.CommandLine.Option(names = "--header", description = "Specify to include a header line.") + private boolean headerOpt; + + @picocli.CommandLine.Option( + names = "--delimiter", + defaultValue = " ", + paramLabel = "CHARACTER", + description = "The output delimiter. Default to using three spaces.") + private String delimiterOpt; + + @picocli.CommandLine.Option( + names = "--array-delimiter", + defaultValue = "|", + paramLabel = "CHARACTER", + description = "The delimiter multi-valued fields. Default to using a pipe (|) delimiter.") + private String arrayDelimiterOpt; + + @picocli.CommandLine.Parameters( + arity = "1..*", + paramLabel = "expr", + description = + "The streaming expression, or a *.expr file, followed by $1/$2/... substitution args.") + private String[] exprArgs; + + public StreamTool() { + this(new DefaultToolRuntime()); + } + public StreamTool(ToolRuntime runtime) { super(runtime); } @@ -94,6 +191,11 @@ public class StreamTool extends ToolBase { """; } + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option EXECUTION_OPTION = Option.builder() .longOpt("execution") @@ -103,6 +205,11 @@ public class StreamTool extends ToolBase { "Execution environment is either 'local' (i.e CLI process) or via a 'remote' Solr server. Default environment is 'remote'.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option COLLECTION_OPTION = Option.builder("c") .longOpt("name") @@ -121,9 +228,19 @@ public class StreamTool extends ToolBase { "The fields in the tuples to output. Defaults to fields in the first tuple of result set.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option HEADER_OPTION = Option.builder().longOpt("header").desc("Specify to include a header line.").get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option DELIMITER_OPTION = Option.builder() .longOpt("delimiter") @@ -131,6 +248,12 @@ public class StreamTool extends ToolBase { .hasArg() .desc("The output delimiter. Default to using three spaces.") .get(); + + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option ARRAY_DELIMITER_OPTION = Option.builder() .longOpt("array-delimiter") @@ -534,7 +657,115 @@ public class StreamTool extends ToolBase { @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + StreamParams params = + new StreamParams( + exprArgs, + executionOpt.name(), + arrayDelimiterOpt, + delimiterOpt, + headerOpt, + fieldsOpt, + collectionOpt, + credentialsOptions.credentials); + + String expr = readExpressionFromArgs(params.args()); + echoIfVerbose("Running Expression: " + expr); + + // Validate inputs before opening any connection to Solr. + boolean local = params.execution().equalsIgnoreCase("local"); + validateExpressionArgs(local, params.collection(), expr); + + // Mirrors the commons-cli path's call structure exactly, including which calls are skipped in + // local mode: resolveSolrUrl() is only invoked when actually needed, since (like + // CLIUtils.normalizeSolrUrl(cli)) it prints a stderr warning when no connection option was + // given. + var solrConnection = resolveSolrConnection(); + String solrUrl = local ? null : resolveSolrUrl(); + if (solrConnection == null) { + // No connection option given and none discoverable from a running Solr; fall back to the + // resolved base URL so expressions that need a Solr connection get a usable default. + solrConnection = + CloudSolrClient.CloudSolrClientConnection.parse( + solrUrl != null ? solrUrl : resolveSolrUrl()); + } + + runStream(params, expr, solrConnection, solrUrl); + return 0; + } + + /** + * The connection named by {@code --solr-connection} or {@code --zk-host}, or by the matching + * property when the option is absent, as {@link CLIUtils#getSolrConnection(CommandLine)} reads + * it; null if neither is set. + */ + private CloudSolrClient.CloudSolrClientConnection connectionFromOptions() throws IOException { + String solrConnection = + (connectionOptions != null && connectionOptions.solrConnection != null) + ? connectionOptions.solrConnection + : EnvUtils.getProperty("solr.connection"); + if (solrConnection != null && !solrConnection.isBlank()) { + return CloudSolrClient.CloudSolrClientConnection.parse(solrConnection); + } + String zkHost = + (connectionOptions != null && connectionOptions.zkHost != null) + ? connectionOptions.zkHost + : EnvUtils.getProperty("zkHost"); + if (zkHost != null && !zkHost.isBlank()) { + var zkSolrConnection = CloudSolrClient.CloudSolrClientConnection.parse(zkHost); + if (!zkSolrConnection.isZookeeper()) { + throw new IOException( + String.format( + Locale.ROOT, "Expected ZooKeeper connection string, but got: '%s'.", zkHost)); + } + return zkSolrConnection; + } + return null; + } + + /** + * Resolves the base Solr URL used for remote execution and for probing whether Solr is running in + * cloud mode; mirrors {@link CLIUtils#normalizeSolrUrl(CommandLine)}, including its warning when + * no connection was given. + */ + private String resolveSolrUrl() throws Exception { + if (connectionOptions != null && connectionOptions.solrUrl != null) { + return CLIUtils.normalizeSolrUrl(connectionOptions.solrUrl); + } + var connection = connectionFromOptions(); + if (connection != null) { + return CLIUtils.solrUrlFromConnection(connection, credentialsOptions.credentials); + } + String defaultSolrUrl = CLIUtils.getDefaultSolrUrl(); + CLIO.err( + "Neither --solr-connection, --zk-host or --solr-url parameters, nor SOLR_CONNECTION, ZK_HOST env var provided, so assuming solr url is " + + defaultSolrUrl + + "."); + return defaultSolrUrl; + } + + /** + * Mirrors {@link CLIUtils#getSolrConnection(CommandLine)}: an explicit connection wins, otherwise + * a running Solr is asked for its ZooKeeper, and null means it is not in SolrCloud mode. + */ + private CloudSolrClient.CloudSolrClientConnection resolveSolrConnection() throws Exception { + var connection = connectionFromOptions(); + if (connection != null) { + return connection; + } + try (SolrClient solrClient = + CLIUtils.getSolrClient(resolveSolrUrl(), credentialsOptions.credentials)) { + Map<String, Object> status = StatusTool.reportStatus(solrClient); + @SuppressWarnings("unchecked") + Map<String, Object> cloud = (Map<String, Object>) status.get("cloud"); + if (cloud == null) { + return null; + } + String zookeeper = (String) cloud.get("ZooKeeper"); + if (zookeeper.endsWith("(embedded)")) { + zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length()); + } + return CloudSolrClient.CloudSolrClientConnection.parse(zookeeper); + } } static String readExpression(LineNumberReader bufferedReader, String[] args) throws IOException { diff --git a/solr/core/src/test/org/apache/solr/cli/StreamToolPicocliTest.java b/solr/core/src/test/org/apache/solr/cli/StreamToolPicocliTest.java new file mode 100644 index 00000000000..3dd46ac101d --- /dev/null +++ b/solr/core/src/test/org/apache/solr/cli/StreamToolPicocliTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.cli; + +import java.util.Arrays; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.util.SecurityJson; +import org.junit.Test; +import picocli.CommandLine; + +/** + * Runs all {@link StreamToolTest} tests through the picocli invocation path. + * + * <p>All {@code @Test} methods are inherited; only the invocation strategy is overridden. + */ +public class StreamToolPicocliTest extends StreamToolTest { + + @Override + protected int runTool(String[] args, Class<? extends ToolBase> clazz) throws Exception { + // args[0] is the tool name used by commons-cli dispatch; strip it for picocli. + String[] toolArgs = Arrays.copyOfRange(args, 1, args.length); + ToolRuntime runtime = new CLITestHelper.TestingRuntime(false); + ToolBase tool = clazz.getDeclaredConstructor(ToolRuntime.class).newInstance(runtime); + return new CommandLine(tool) + .setDefaultValueProvider(new CliDefaultValueProvider()) + .execute(toolArgs); + } + + @Test + public void testExecutionIsAcceptedInAnyCase() throws Exception { + String[] args = { + "stream", + "--execution", + "LOCAL", + "-s", + getSolrConnection().toString(), + "--credentials", + SecurityJson.USER_PASS, + "echo(Hello)" + }; + assertEquals(0, runTool(args, StreamTool.class)); + } + + @Test + public void testUnknownExecutionIsAUsageError() throws Exception { + String[] args = { + "stream", + "--execution", + "elsewhere", + "-s", + getSolrConnection().toString(), + "--credentials", + SecurityJson.USER_PASS, + "echo(Hello)" + }; + assertEquals(2, runTool(args, StreamTool.class)); + } + + @Test + public void testRunEchoStreamRemotelyUsesSolrConnectionProperty() throws Exception { + // SOLR_CONNECTION reaches the JVM as the solr.connection property + String collectionName = "streamSolrConnectionPropertyCollection"; + withBasicAuth(CollectionAdminRequest.createCollection(collectionName, "_default", 1, 1)) + .processAndWait(cluster.getSolrClient(), 10); + waitForState( + "Expected collection to be created with 1 shard and 1 replicas", + collectionName, + clusterShape(1, 1)); + + System.setProperty("solr.connection", getSolrConnection().toString()); + String[] args = { + "stream", + "--execution", + "remote", + "-c", + collectionName, + "--credentials", + SecurityJson.USER_PASS, + "echo(Hello)" + }; + assertEquals(0, runTool(args, StreamTool.class)); + } +} diff --git a/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java b/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java index 27ae59cddb9..90d458eb7fd 100644 --- a/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java +++ b/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java @@ -43,12 +43,17 @@ import org.junit.Test; public class StreamToolTest extends SolrCloudTestCase { + /** Runs the tool. Overridden by the picocli variant of this test. */ + protected int runTool(String[] args, Class<? extends ToolBase> clazz) throws Exception { + return CLITestHelper.runTool(args, clazz); + } + @BeforeClass public static void setupClusterWithSecurityEnabled() throws Exception { configureCluster(2).withSecurityJson(SecurityJson.SIMPLE).configure(); } - private <T extends SolrRequest<? extends SolrResponse>> T withBasicAuth(T req) { + <T extends SolrRequest<? extends SolrResponse>> T withBasicAuth(T req) { req.setBasicAuthCredentials(SecurityJson.USER, SecurityJson.PASS); return req; } @@ -240,7 +245,7 @@ public class StreamToolTest extends SolrCloudTestCase { getSolrConnection().toString(), expression }; - assertEquals(1, CLITestHelper.runTool(args, StreamTool.class)); + assertEquals(1, runTool(args, StreamTool.class)); } @Test @@ -259,7 +264,7 @@ public class StreamToolTest extends SolrCloudTestCase { SecurityJson.USER_PASS, expression }; - assertEquals(0, CLITestHelper.runTool(args, StreamTool.class)); + assertEquals(0, runTool(args, StreamTool.class)); } @Test @@ -285,7 +290,7 @@ public class StreamToolTest extends SolrCloudTestCase { expressionFile.toString() }; - assertEquals(0, CLITestHelper.runTool(args, StreamTool.class)); + assertEquals(0, runTool(args, StreamTool.class)); // test passing in the expression directly args = @@ -301,7 +306,7 @@ public class StreamToolTest extends SolrCloudTestCase { expression }; - assertEquals(0, CLITestHelper.runTool(args, StreamTool.class)); + assertEquals(0, runTool(args, StreamTool.class)); } @Test @@ -335,7 +340,7 @@ public class StreamToolTest extends SolrCloudTestCase { expressionFile.toString() }; - assertEquals(0, CLITestHelper.runTool(args, StreamTool.class)); + assertEquals(0, runTool(args, StreamTool.class)); // test passing in the expression directly args = @@ -353,7 +358,32 @@ public class StreamToolTest extends SolrCloudTestCase { expression }; - assertEquals(0, CLITestHelper.runTool(args, StreamTool.class)); + assertEquals(0, runTool(args, StreamTool.class)); + } + + @Test + public void testRunEchoStreamRemotelyUsesZkHostProperty() throws Exception { + String collectionName = "streamZkHostPropertyCollection"; + withBasicAuth(CollectionAdminRequest.createCollection(collectionName, "_default", 1, 1)) + .processAndWait(cluster.getSolrClient(), 10); + waitForState( + "Expected collection to be created with 1 shard and 1 replicas", + collectionName, + clusterShape(1, 1)); + + String[] args = { + "stream", + "--execution", + "remote", + "-c", + collectionName, + "--credentials", + SecurityJson.USER_PASS, + "echo(Hello)" + }; + + System.setProperty("zkHost", cluster.getZkClient().getZkServerAddress()); + assertEquals(0, runTool(args, StreamTool.class)); } // Copied from StreamExpressionTest.java diff --git a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc index 25397dcfe32..074440d2a99 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc @@ -34,6 +34,7 @@ *** xref:cli/solr-start.adoc[start] *** xref:cli/solr-status.adoc[status] *** xref:cli/solr-stop.adoc[stop] +*** xref:cli/solr-stream.adoc[stream] *** xref:cli/solr-version.adoc[version] *** xref:cli/solr-zk.adoc[zk] **** xref:cli/solr-zk-cp.adoc[cp] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc index c542dbb975b..e7e950ea5e1 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc @@ -73,6 +73,7 @@ Any command not listed here is unavailable when the picocli implementation is en |xref:cli/solr-start.adoc[bin/solr start] |xref:cli/solr-status.adoc[bin/solr status] |xref:cli/solr-stop.adoc[bin/solr stop] +|xref:cli/solr-stream.adoc[bin/solr stream] |xref:cli/solr-version.adoc[bin/solr version] |xref:cli/solr-zk-cp.adoc[bin/solr zk cp] |xref:cli/solr-zk-downconfig.adoc[bin/solr zk downconfig] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stream.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stream.adoc new file mode 100644 index 00000000000..e62f5b1a1bb --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stream.adoc @@ -0,0 +1,110 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr stream +:page-toclevels: 2 + +[WARNING] +==== +This page documents the *experimental* picocli implementation of `bin/solr`, which is not enabled by default. +For what `bin/solr` does by default, see xref:solr-control-script-reference.adoc[]. +See xref:cli/index.adoc[] to enable this implementation and for its known limitations. +==== + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr stream [-hv] [--header] [--array-delimiter=CHARACTER] [-c=name] + [--delimiter=CHARACTER] [--execution=ENVIRONMENT] [--fields=FIELDS] + [-u=<credentials>] [-s=<solrConnection> | --solr-url=<solrUrl> | + -z=<zkHost>] expr... +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +The Stream command runs a streaming expression in Solr or in the CLI process and prints the results. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*--array-delimiter*=_CHARACTER_:: + The delimiter multi-valued fields. Default to using a pipe (|) delimiter. + +*-c*, *--name*=_name_:: + Name of the specific collection to execute expression on if the execution is set to 'remote'. Required for 'remote' execution environment. + +*--delimiter*=_CHARACTER_:: + The output delimiter. Default to using three spaces. + +*--execution*=_ENVIRONMENT_:: + Where to run the expression, one of: local, remote. 'local' is the CLI process, 'remote' is a Solr server. Default is 'remote'. + +*--fields*=_FIELDS_:: + The fields in the tuples to output. Defaults to fields in the first tuple of result set. + +*-h*, *--help*:: + Print this help message and exit. + +*--header*:: + Specify to include a header line. + +*-s*, *--solr-connection*=_<solrConnection>_:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=_<solrUrl>_:: + Base Solr URL, which can be used to determine the zk-host if that's not known. + +*-u*, *--credentials*=_<credentials>_:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=_<zkHost>_:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +== Arguments + +_expr_...:: + The streaming expression, or a *.expr file, followed by $1/$2/... substitution args. + +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-footer[] +== Examples +[%hardbreaks] + # Run a streaming expression against the techproducts collection + bin/solr stream -c techproducts 'search(techproducts,q="iPod",fl="name,price")' + +[%hardbreaks] + # Run a streaming expression stored in a .expr file + bin/solr stream --header -c techproducts stream.expr + +// end::picocli-generated-man-section-footer[]
