From 0a475cfafa2239865d0c5a03d0c75426608f805e Mon Sep 17 00:00:00 2001 From: Vishnu Priya Chandra Sekar Date: Mon, 6 Jul 2026 18:40:46 -0700 Subject: [PATCH 1/6] SOLR-17433: Set default http request timeout to infinite * Set the default request timeout to infinite by setting it to zero. This helps streams to continue streaming until they exhaust the content. * Prevent passing over the request timeout to JDK's HttpClient if the request timeout is zero. This is because JDK expects no value to represent infinite request timeout. * Updated ConcurrentUpdateSolrClientTestBase.testSocketTimeoutOnCommit to explicitly set a request timeout. The test previously relied on the default 1 ms request timeout (from idle timeout) to trigger an HTTP client timeout. After the default timeout was changed to infinite, the client no longer timed out, causing the test's 10-second timeout to fail first. This change restores the intended test behavior by overriding the request timeout explicitly. * Updated HttpJdkSolrClientTest.testTimeout to explicitly set a request timeout. Previously, request timeout was based on idle timeout. After the infinite request timeout, the client no longer timed out. Fixed the test by overriding the request timeout explicitly * Updated LB2SolrClientTest.testTimeoutExceptionMarksServerAsZombie to explicitly set a request timeout as the default request timeout causes the client to wait infinitely. --- ...-17433-changed-default-request-timeout.yml | 31 +++++++++++++++++++ .../ConcurrentUpdateJettySolrClientTest.java | 6 +++- .../solrj/jetty/HttpJettySolrClientTest.java | 5 +-- .../client/solrj/impl/HttpJdkSolrClient.java | 5 ++- .../client/solrj/impl/HttpSolrClient.java | 12 +++++-- .../ConcurrentUpdateJdkSolrClientTest.java | 6 +++- .../solrj/impl/HttpJdkSolrClientTest.java | 6 +++- .../client/solrj/impl/LB2SolrClientTest.java | 3 ++ 8 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 changelog/unreleased/SOLR-17433-changed-default-request-timeout.yml diff --git a/changelog/unreleased/SOLR-17433-changed-default-request-timeout.yml b/changelog/unreleased/SOLR-17433-changed-default-request-timeout.yml new file mode 100644 index 000000000000..53749319ed25 --- /dev/null +++ b/changelog/unreleased/SOLR-17433-changed-default-request-timeout.yml @@ -0,0 +1,31 @@ +# (DELETE ALL COMMENTS UP HERE AFTER FILLING THIS IN + +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc + +# If the change is minor, don't bother adding a changelog entry. +# For `other` type entries, the threshold to bother with a changelog entry should be even higher. + +# title: +# * The audience is end-users and administrators, not committers. +# * Be short and focused on the user impact. Multiple sentences is fine! +# * For technical/geeky details, prefer the commit message instead of changelog. +# * Reference JIRA issues like `SOLR-12345`, or if no JIRA but have a GitHub PR then `PR#12345`. + +# type: +# `added` for new features/improvements, opt-in by the user typically documented in the ref guide +# `changed` for improvements; not opt-in +# `fixed` for improvements that are deemed to have fixed buggy behavior +# `deprecated` for marking things deprecated +# `removed` for code removed +# `dependency_update` for updates to dependencies +# `other` for anything else, like large/significant refactorings, build changes, +# test infrastructure, or documentation. +# Most such changes are too small/minor to bother with a changelog entry. + +title: Changed default http request timeout to infinite +type: changed +authors: + - name: Vishnu Priya Chandra Sekar +links: + - name: SOLR-17433 + url: https://issues.apache.org/jira/browse/SOLR-17433 diff --git a/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/ConcurrentUpdateJettySolrClientTest.java b/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/ConcurrentUpdateJettySolrClientTest.java index df28d17f3b0d..e23f52bf0ad6 100644 --- a/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/ConcurrentUpdateJettySolrClientTest.java +++ b/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/ConcurrentUpdateJettySolrClientTest.java @@ -47,7 +47,11 @@ public ConcurrentUpdateBaseSolrClient outcomeCountingConcurrentClient( public HttpSolrClient solrClient(Integer overrideIdleTimeoutMs) { var builder = new HttpJettySolrClient.Builder(); if (overrideIdleTimeoutMs != null) { - builder.withIdleTimeout(overrideIdleTimeoutMs, TimeUnit.MILLISECONDS); + builder + .withIdleTimeout(overrideIdleTimeoutMs, TimeUnit.MILLISECONDS) + // override the infinite request timeout with idle timeout to ensure idle requests time + // out. + .withRequestTimeout(overrideIdleTimeoutMs, TimeUnit.MILLISECONDS); } return builder.build(); } diff --git a/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientTest.java b/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientTest.java index 11bf3e3c85a2..6ba37acc49f7 100644 --- a/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientTest.java +++ b/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientTest.java @@ -671,10 +671,7 @@ public void testBuilder() { public void testIdleTimeoutWithHttpClient() throws Exception { String url = solrTestRule.getBaseUrl() + SLOW_STREAM_SERVLET_PATH; try (var oldClient = - new HttpJettySolrClient.Builder(url) - .withRequestTimeout(Long.MAX_VALUE, TimeUnit.MILLISECONDS) - .withIdleTimeout(100, TimeUnit.MILLISECONDS) - .build()) { + new HttpJettySolrClient.Builder(url).withIdleTimeout(100, TimeUnit.MILLISECONDS).build()) { try (var onlyBaseUrlChangedClient = new HttpJettySolrClient.Builder(url).withHttpClient(oldClient).build()) { diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java index 1f88adc519fd..b9c9f26cc6a8 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java @@ -448,7 +448,10 @@ private synchronized boolean maybeTryHeadRequestSync(URI uriNoQueryParams) { } private void decorateRequest(HttpRequest.Builder reqb, SolrRequest solrRequest) { - reqb.timeout(Duration.of(requestTimeoutMillis, ChronoUnit.MILLIS)); + // JDK does not allow non-positive value for request timeout. + if (requestTimeoutMillis > 0) { + reqb.timeout(Duration.of(requestTimeoutMillis, ChronoUnit.MILLIS)); + } reqb.header("User-Agent", USER_AGENT); setBasicAuthHeader(solrRequest, reqb); diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java index fccc4357fcde..8dd5a237860c 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java @@ -580,10 +580,16 @@ public B withRequestTimeout(long requestTimeout, TimeUnit unit) { return (B) this; } + /** + * Returns the request timeout in milliseconds. + * + *

If no request timeout is configured or if the configured value is non-positive, this + * method returns {@code 0}, indicating that requests will wait infinitely for a response. + * + * @return request timeout in milliseconds or {@code 0} if no valid timeout is configured. + */ public long getRequestTimeoutMillis() { - return requestTimeoutMillis != null && requestTimeoutMillis > 0 - ? requestTimeoutMillis - : getIdleTimeoutMillis(); + return requestTimeoutMillis != null && requestTimeoutMillis > 0 ? requestTimeoutMillis : 0; } /** diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateJdkSolrClientTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateJdkSolrClientTest.java index 13e790b60b2b..259e8180d1b1 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateJdkSolrClientTest.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateJdkSolrClientTest.java @@ -28,7 +28,11 @@ public HttpSolrClient solrClient(Integer overrideIdleTimeoutMs) { var builder = new HttpJdkSolrClient.Builder().withSSLContext(MockTrustManager.ALL_TRUSTING_SSL_CONTEXT); if (overrideIdleTimeoutMs != null) { - builder.withIdleTimeout(overrideIdleTimeoutMs, TimeUnit.MILLISECONDS); + builder + .withIdleTimeout(overrideIdleTimeoutMs, TimeUnit.MILLISECONDS) + // override the infinite request timeout with idle timeout to ensure idle requests times + // out. + .withRequestTimeout(overrideIdleTimeoutMs, TimeUnit.MILLISECONDS); } return builder.build(); } diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java index 73ff00474bb1..ef4edd8f63d4 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java @@ -175,6 +175,8 @@ public void testRequestWithBaseUrl() throws Exception { client.requestWithBaseUrl(intendedUrl, new QueryRequest(q, SolrRequest.METHOD.GET), null); assertEquals( client.getParser().getWriterType(), DebugServlet.parameters.get(CommonParams.WT)[0]); + // verify whether the default request timeout is infinite. + assertEquals(0, client.requestTimeoutMillis); } } @@ -211,7 +213,9 @@ public void testTimeout() throws Exception { SolrQuery q = new SolrQuery("*:*"); try (HttpJdkSolrClient client = (HttpJdkSolrClient) - builder(solrTestRule.getBaseUrl() + SLOW_SERVLET_PATH, 500, 500).build()) { + builder(solrTestRule.getBaseUrl() + SLOW_SERVLET_PATH, 500, 500) + .withRequestTimeout(500, TimeUnit.MILLISECONDS) + .build()) { client.query(q, SolrRequest.METHOD.GET); fail("No exception thrown."); } catch (SolrServerException e) { diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LB2SolrClientTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LB2SolrClientTest.java index 849d8953c0fa..28c757c3291e 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LB2SolrClientTest.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LB2SolrClientTest.java @@ -405,6 +405,9 @@ private class TimeoutZombieTestContext implements AutoCloseable { new HttpJettySolrClient.Builder() .withConnectionTimeout(1000, TimeUnit.MILLISECONDS) .withIdleTimeout(1, TimeUnit.MILLISECONDS) + // override the infinite request timeout with idle timeout to ensure idle requests + // times out. + .withRequestTimeout(1, TimeUnit.MILLISECONDS) .build(); lbClient = new LBJettySolrClient.Builder(delegateClient, nonRoutableEndpoint).build(); From 08359ce6a84fe0de8558892febefeee19d79528a Mon Sep 17 00:00:00 2001 From: David Smiley Date: Wed, 8 Jul 2026 22:06:56 -0400 Subject: [PATCH 2/6] Remove ASF License headers when possible. (#4609) These files are not "released" (not in the source or any other release anymore). --- AGENTS.md | 16 ---------------- dev-docs/README.adoc | 16 ---------------- dev-docs/dependency-upgrades.adoc | 16 ---------------- dev-docs/gradle-help/README.md | 17 ----------------- dev-docs/lucene-upgrade.md | 17 ----------------- dev-docs/pmc-chair.adoc | 16 ---------------- dev-docs/ref-guide/antora.adoc | 16 ---------------- dev-docs/ref-guide/asciidoc-syntax.adoc | 16 ---------------- dev-docs/working-between-major-versions.adoc | 16 ---------------- 9 files changed, 146 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e7542117d443..70433ba1adcf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,19 +1,3 @@ - # AGENTS.md for Apache Solr While README.md and CONTRIBUTING.md are mainly written for humans, this file is a condensed knowledge base for LLM coding agents on the Solr codebase. See https://agents.md for more info and how to make various coding assistants consume this file. Also see `dev-docs/how-to-contribute.adoc` for some guidelines when using genAI to contribute to Solr. diff --git a/dev-docs/README.adoc b/dev-docs/README.adoc index 17d64ccbb54e..49570d7a0a92 100644 --- a/dev-docs/README.adoc +++ b/dev-docs/README.adoc @@ -1,20 +1,4 @@ = Solr Developer Docs -// 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. This directory includes information for Solr developers. There are some other sources of information for learning about developing on Solr: diff --git a/dev-docs/dependency-upgrades.adoc b/dev-docs/dependency-upgrades.adoc index cbc16809aca3..1bfbabb5d204 100644 --- a/dev-docs/dependency-upgrades.adoc +++ b/dev-docs/dependency-upgrades.adoc @@ -1,20 +1,4 @@ = Dependency upgrades -// 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. Solr has lots of 3rd party dependencies, defined in `gradle/libs.versions.toml`. Keeping them up-to-date is crucial for a number of reasons: diff --git a/dev-docs/gradle-help/README.md b/dev-docs/gradle-help/README.md index 58df221401df..d3a43d90fae6 100644 --- a/dev-docs/gradle-help/README.md +++ b/dev-docs/gradle-help/README.md @@ -1,20 +1,3 @@ - - # Gradle Help Documentation This directory contains text files that provide help documentation for various Gradle tasks and project workflows in Solr. diff --git a/dev-docs/lucene-upgrade.md b/dev-docs/lucene-upgrade.md index c816c9f594cf..dd17ef42380b 100644 --- a/dev-docs/lucene-upgrade.md +++ b/dev-docs/lucene-upgrade.md @@ -1,20 +1,3 @@ - - # Lucene upgrade steps ## Read diff --git a/dev-docs/pmc-chair.adoc b/dev-docs/pmc-chair.adoc index 8c5f3b1ad238..24cb1db80459 100644 --- a/dev-docs/pmc-chair.adoc +++ b/dev-docs/pmc-chair.adoc @@ -1,21 +1,5 @@ = Tips & Tricks for PMC Chair :toc: left -// 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. Congratulations on becoming the Chair of the Lucene PMC! Thank you for accepting the role. diff --git a/dev-docs/ref-guide/antora.adoc b/dev-docs/ref-guide/antora.adoc index a363cb3fe46e..0b70cf7f27f3 100644 --- a/dev-docs/ref-guide/antora.adoc +++ b/dev-docs/ref-guide/antora.adoc @@ -1,21 +1,5 @@ = Working with HTML Templates :toc: -// 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. The Solr Ref Guide uses Antora to build the HTML version of the site. diff --git a/dev-docs/ref-guide/asciidoc-syntax.adoc b/dev-docs/ref-guide/asciidoc-syntax.adoc index 510bea5aec77..2b590e145ef8 100644 --- a/dev-docs/ref-guide/asciidoc-syntax.adoc +++ b/dev-docs/ref-guide/asciidoc-syntax.adoc @@ -1,21 +1,5 @@ = AsciiDoc Syntax Cheatsheet :toc: -// 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. The definitive manual on AsciiDoc syntax is in the https://docs.asciidoctor.org/asciidoctor.js/latest[Asciidoctor.js documentation]. To help people get started, however, here is a simpler cheat sheet. diff --git a/dev-docs/working-between-major-versions.adoc b/dev-docs/working-between-major-versions.adoc index 4c41fb8073a0..af61cda5e478 100644 --- a/dev-docs/working-between-major-versions.adoc +++ b/dev-docs/working-between-major-versions.adoc @@ -1,20 +1,4 @@ = Working between Multiple Major versions -// 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. Working between multiple major versions of `solr` is often a necessary part of committing code, due to backports and testing. For some versions, this is an even bigger issue because `8.x` and `9.x` use different build systems, ant and gradle, and different repository locations. From bca4e41395c5a6ad9f58e4ef866678cfe5c4474a Mon Sep 17 00:00:00 2001 From: Abhishek Umarjikar <35094694+abumarjikar@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:15:18 +0530 Subject: [PATCH 3/6] SOLR-18197: Add root document query shortcut support to NestPathField (#4512) Querying root (top-level) documents no longer requires the verbose existence-negation idiom: Before: fq=*:* -_nest_path_:* After: fq=_nest_path_:\/ NestPathField now extends StrField instead of SortableTextField/CustomAnalyzer. Lucene's query parser bypasses getFieldQuery() for "tokenized" field; switching to an untokenized StrField fixed it. It's also simpler. The field is now a bit more strict about misconfiguration that was previously allowed. It doesn't support stored=true anymore but it's not needed anyway. --- .../SOLR_18197_nest_path_easy_of_use.yml | 10 ++ .../org/apache/solr/schema/NestPathField.java | 107 +++++++++++++----- .../search/join/BlockJoinChildQParser.java | 2 +- .../search/join/BlockJoinParentQParser.java | 21 ++-- .../apache/solr/search/QueryEqualityTest.java | 24 ++++ .../update/TestNestedUpdateProcessor.java | 7 +- .../pages/dense-vector-search.adoc | 6 +- .../pages/searching-nested-documents.adoc | 17 ++- 8 files changed, 150 insertions(+), 44 deletions(-) create mode 100644 changelog/unreleased/SOLR_18197_nest_path_easy_of_use.yml diff --git a/changelog/unreleased/SOLR_18197_nest_path_easy_of_use.yml b/changelog/unreleased/SOLR_18197_nest_path_easy_of_use.yml new file mode 100644 index 000000000000..646fe64ae835 --- /dev/null +++ b/changelog/unreleased/SOLR_18197_nest_path_easy_of_use.yml @@ -0,0 +1,10 @@ +title: Add root document query shortcut support to NestPathField +type: added +authors: + - name: Abhishek Umarjikar + nick: abumarjikar + - name: David Smiley + nick: dsmiley +links: + - name: SOLR-18197 + url: https://issues.apache.org/jira/browse/SOLR-18197 diff --git a/solr/core/src/java/org/apache/solr/schema/NestPathField.java b/solr/core/src/java/org/apache/solr/schema/NestPathField.java index d34e532abbb5..bac7fa749979 100644 --- a/solr/core/src/java/org/apache/solr/schema/NestPathField.java +++ b/solr/core/src/java/org/apache/solr/schema/NestPathField.java @@ -17,13 +17,18 @@ package org.apache.solr.schema; -import java.io.IOException; +import java.util.List; import java.util.Map; -import org.apache.lucene.analysis.core.KeywordTokenizerFactory; -import org.apache.lucene.analysis.custom.CustomAnalyzer; -import org.apache.lucene.analysis.pattern.PatternReplaceFilterFactory; -import org.apache.solr.analysis.TokenizerChain; +import java.util.regex.Pattern; +import org.apache.lucene.document.SortedDocValuesField; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.util.BytesRef; import org.apache.solr.common.SolrException; +import org.apache.solr.search.QParser; /** * To be used for field {@link IndexSchema#NEST_PATH_FIELD_NAME} for enhanced nested doc @@ -34,32 +39,82 @@ * @see org.apache.solr.update.processor.NestedUpdateProcessorFactory * @since 8.0 */ -public class NestPathField extends SortableTextField { +public class NestPathField extends StrField { + + private static final Pattern ORDINAL_PATTERN = Pattern.compile("#\\d*"); @Override public void setArgs(IndexSchema schema, Map args) { - args.putIfAbsent("stored", "false"); - args.putIfAbsent("multiValued", "false"); - args.putIfAbsent("omitTermFreqAndPositions", "true"); - args.putIfAbsent("omitNorms", "true"); - args.putIfAbsent("maxCharsForDocValues", "-1"); + args.putIfAbsent("stored", "false"); // flip a default + args.putIfAbsent("docValues", "true"); // flip a default; necessary for old schemas + args.putIfAbsent("multiValued", "false"); // flip a default; necessary for old schemas + args.putIfAbsent("uninvertible", "false"); // flip a default; necessary for old schemas super.setArgs(schema, args); + // Doesn't support these flags; perhaps others too. + // note: we could support STORED if truly useful but why bother given docValues. + restrictProps(STORED | MULTIVALUED | UNINVERTIBLE | TOKENIZED); + } + + /** + * {@inheritDoc} Overridden to manipulate the indexed form (AKA terms), but not the DV form. The + * value may look something like {@code /childA#0/childB#23} and we want to strip out the + * pound-digits aspects. + * + *

The terms index is used for block-join and docValues is used for returning nested docs in + * the same structure as given. + * + * @see #toInternal(String) + */ + @Override + public List createFields(SchemaField field, Object value) { + // indexed or docValues or both may be false... albeit we strongly recommend both enabled and + // the testing of disabling either may be non-existent. + final IndexableField termField = createField(field, value); // calls toInternal + + if (!field.hasDocValues()) { // note: strongly recommended, however + return termField == null ? List.of() : List.of(termField); + } + + final BytesRef bytes = getBytesRef(value); // unmodified, unlike the indexed term + final IndexableField dvField = new SortedDocValuesField(field.getName(), bytes); + + return termField == null ? List.of(dvField) : List.of(termField, dvField); + } + + @Override + public String toInternal(String val) { + return ORDINAL_PATTERN.matcher(val).replaceAll(""); + } + + @Override + public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) { + if (externalVal == null) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, + "Field " + field.getName() + " missing value. Forgot `v` local-param?"); + } - // CustomAnalyzer is easy to use - CustomAnalyzer customAnalyzer; - try { - customAnalyzer = - CustomAnalyzer.builder(schema.getResourceLoader()) - .withDefaultMatchVersion(schema.getDefaultLuceneMatchVersion()) - .withTokenizer(KeywordTokenizerFactory.class) - .addTokenFilter( - PatternReplaceFilterFactory.class, "pattern", "#\\d*", "replace", "all") - .build(); - } catch (IOException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e); // impossible? + if (externalVal.contains("\\")) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, + "Field " + field.getName() + " query value contains backslashes ('\\')."); } - // Solr HTTP Schema APIs don't know about CustomAnalyzer so use TokenizerChain instead - setIndexAnalyzer(new TokenizerChain(customAnalyzer)); - // leave queryAnalyzer as literal + + if (externalVal.isEmpty() || "/".equals(externalVal)) { + return new BooleanQuery.Builder() + .add(MatchAllDocsQuery.INSTANCE, BooleanClause.Occur.MUST) + .add(field.getType().getExistenceQuery(parser, field), BooleanClause.Occur.MUST_NOT) + .build(); + } + + if (!externalVal.startsWith("/") || externalVal.endsWith("/")) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, + "Field " + + field.getName() + + " query value must start with a forward slash ('/') and cannot contain a trailing slash."); + } + + return super.getFieldQuery(parser, field, externalVal); } } diff --git a/solr/core/src/java/org/apache/solr/search/join/BlockJoinChildQParser.java b/solr/core/src/java/org/apache/solr/search/join/BlockJoinChildQParser.java index 0d7383dfc4f8..b53d7d816805 100644 --- a/solr/core/src/java/org/apache/solr/search/join/BlockJoinChildQParser.java +++ b/solr/core/src/java/org/apache/solr/search/join/BlockJoinChildQParser.java @@ -90,7 +90,7 @@ protected Query parseUsingParentPath(String parentPath, String childPath) throws } // allParents filter: (*:* -{!prefix f="_nest_path_" v="/"}) - // For root: (*:* -_nest_path_:*) + // For root: {!field f=_nest_path_ v='/'} final Query allParentsFilter = buildAllParentsFilterFromPath(parentPath); // constrain the parent query to only match docs at exactly parentPath diff --git a/solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java b/solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java index 6e7dbaf4c63e..6cd34424b951 100644 --- a/solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java +++ b/solr/core/src/java/org/apache/solr/search/join/BlockJoinParentQParser.java @@ -171,11 +171,11 @@ protected Query parseUsingParentPath(String parentPath, String childPath) throws if (parsedChildQuery.clauses().isEmpty()) { // i.e. all children // no block-join needed; just return all "parent" docs at this level - return wrapWithParentPathConstraint(parentPath, new MatchAllDocsQuery()); + return wrapWithParentPathConstraint(parentPath, MatchAllDocsQuery.INSTANCE); } // allParents filter: (*:* -{!prefix f="_nest_path_" v="/"}) - // For root: (*:* -_nest_path_:*) + // For root: {!field f=_nest_path_ v='/'} final Query allParentsFilter = buildAllParentsFilterFromPath(parentPath); // constrain child query: (+ +{!prefix f="_nest_path_" v="/"}) @@ -206,18 +206,19 @@ protected Query parseUsingParentPath(String parentPath, String childPath) throws * * *

Equivalent to: {@code (*:* -{!prefix f="_nest_path_" v="/"})} For root ({@code - * /}): {@code (*:* -_nest_path_:*)} + * /}): {@code {!field f=_nest_path_ v='/'}} */ protected Query buildAllParentsFilterFromPath(String parentPath) { - final Query excludeQuery; - if (parentPath.equals("/")) { - excludeQuery = newNestPathExistsQuery(); - } else { - excludeQuery = new PrefixQuery(new Term(IndexSchema.NEST_PATH_FIELD_NAME, parentPath + "/")); + if (parentPath.equals("/") || parentPath.isEmpty()) { + final SchemaField nestPathField = req.getSchema().getField(IndexSchema.NEST_PATH_FIELD_NAME); + return nestPathField.getType().getFieldQuery(this, nestPathField, parentPath); } + return new BooleanQuery.Builder() - .add(new MatchAllDocsQuery(), Occur.MUST) - .add(excludeQuery, Occur.MUST_NOT) + .add(MatchAllDocsQuery.INSTANCE, Occur.MUST) + .add( + new PrefixQuery(new Term(IndexSchema.NEST_PATH_FIELD_NAME, parentPath + "/")), + Occur.MUST_NOT) .build(); } diff --git a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java index ecad83f39de0..b8895fe7b18b 100644 --- a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java +++ b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java @@ -24,6 +24,7 @@ import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.NamedMatches; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermInSetQuery; @@ -2000,6 +2001,29 @@ public void testHashRangeQuery() throws Exception { "{!hash_range l='107347968' u='214695935' f='x_id'}"); } + @Test + public void testNestPathRootShortcut() throws Exception { + try (SolrQueryRequest req = req("df", "_nest_path_")) { + Query parsedQ = + assertQueryEqualsAndReturn( + null, req, "{!field f=_nest_path_ v=''}", "{!field f=_nest_path_}/"); + + var schemaField = req.getSchema().getField("_nest_path_"); + Query expectedQ = + new BooleanQuery.Builder() + .add(new MatchAllDocsQuery(), BooleanClause.Occur.MUST) + .add( + schemaField.getType().getExistenceQuery(null, schemaField), + BooleanClause.Occur.MUST_NOT) + .build(); + + assertEquals( + "The root shortcut query did not form the expected match-all minus existence structure", + expectedQ, + parsedQ); + } + } + // Override req to add df param public static SolrQueryRequest req(String... q) { return SolrTestCaseJ4.req(q, "df", "text"); diff --git a/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java b/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java index fca48d5765a9..d5482e52c3f2 100644 --- a/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java +++ b/solr/core/src/test/org/apache/solr/update/TestNestedUpdateProcessor.java @@ -690,8 +690,7 @@ private SolrParams parentQueryMaker(String parent_path, String inner_child_query "child_q", "(+" + inner_child_query + " +_nest_path_:*)"); } else { return params( - "q", - "{!parent which='(*:* -_nest_path_:*)'}(+" + inner_child_query + " +_nest_path_:*)"); + "q", "{!parent which=_nest_path_:\\/}(+" + inner_child_query + " +_nest_path_:*)"); } } // else... @@ -786,7 +785,9 @@ private SolrParams childQueryMaker(String parent_path, String inner_parent_query } else { return params( "q", - "{!child of='(*:* -_nest_path_:*)'}(+" + inner_parent_query + " -_nest_path_:*)"); + "{!child of='{!field f=_nest_path_ v=/}'}(+" + + inner_parent_query + + " -_nest_path_:*)"); } } // else... diff --git a/solr/solr-ref-guide/modules/query-guide/pages/dense-vector-search.adoc b/solr/solr-ref-guide/modules/query-guide/pages/dense-vector-search.adoc index fd9a916a88a7..c91a362f740f 100644 --- a/solr/solr-ref-guide/modules/query-guide/pages/dense-vector-search.adoc +++ b/solr/solr-ref-guide/modules/query-guide/pages/dense-vector-search.adoc @@ -539,7 +539,7 @@ Here is an example of a `knn` search using a `childrenOf`: [source,text] ?q={!knn f=vector topK=3 childrenOf=$allParents}[1.0, 2.0, 3.0, 4.0] -&allParents=*:* -_nest_path_:* +&allParents={!field f=_nest_path_ v='/'} The search results retrieved are the k=3 nearest documents to the vector in input `[1.0, 2.0, 3.0, 4.0]`, each of them with a different parent. The 'childrenOf' parameter must return all valid parents to guarantee the correct functioning of the query. @@ -560,7 +560,7 @@ Here is an example of a `knn` search using a `parents.preFilter`: [source,text] ?q={!knn f=vector topK=3 parents.preFilter=$someParents childrenOf=$allParents}[1.0, 2.0, 3.0, 4.0] -&allParents=*:* -_nest_path_:* +&allParents={!field f=_nest_path_ v='/'} &someParents=color_s:RED The search results retrieved are the k=3 nearest documents to the vector in input `[1.0, 2.0, 3.0, 4.0]`, each of them with a different parent. Only the documents with a parent that satisfy the 'color_s:RED' condition are considered candidates for the ANN search. @@ -605,7 +605,7 @@ So you should query a multivalued vector fields following the same syntax: [source,text] ?q={!parent which=$allParents score=max v=$children.q} &children.q={!knn f=vector_multivalued topK=3 parents.preFilter=$someParents childrenOf=$allParents}[1.0, 2.0, 3.0, 4.0] -&allParents=*:* -_nest_path_:* +&allParents={!field f=_nest_path_ v='/'} &someParents=color_s:RED In terms of rendering the results, you need the child transformer if you want to output them flat (you can choose to only return the best vector per result or all vectors): diff --git a/solr/solr-ref-guide/modules/query-guide/pages/searching-nested-documents.adoc b/solr/solr-ref-guide/modules/query-guide/pages/searching-nested-documents.adoc index 81220d51318a..879663b17acf 100644 --- a/solr/solr-ref-guide/modules/query-guide/pages/searching-nested-documents.adoc +++ b/solr/solr-ref-guide/modules/query-guide/pages/searching-nested-documents.adoc @@ -274,6 +274,21 @@ $ curl 'http://localhost:8983/solr/gettingstarted/select' -d 'omitHeader=true' - }} ---- +=== Root Document Query Shortcut + +When working with hierarchical nested documents, you may frequently need to isolate or filter your search results exclusively to top-level "root" documents (documents that do not belong to a parent path layout). + +Instead of using a verbose negative field existence filter (like `*:* -_nest_path_:*`), the `NestPathField` type supports an explicit root document query shortcut. Passing either a single forward slash (`/`) or an empty string (`''`) automatically constructs a query matching all documents while excluding any nested child paths: + +[source,text] +---- +# Targets only top-level root documents via the slash shortcut +fq={!field f=_nest_path_}/ + +# Alternative equivalent syntax using an empty parameter string +fq={!field f=_nest_path_ v=''} +---- + === Nested Vectors search through Block Join Query Parsers and Child Doc Transformer @@ -293,7 +308,7 @@ An example: [source,text] ?q={!parent which=$allParents score=max v=$children.q}& children.q={!knn f=vector topK=3 parents.preFilter=$someParents childrenOf=$allParents}[1.0, 2.0, 3.0, 4.0]& -allParents=*:* -_nest_path_:*& +allParents={!field f=_nest_path_ v=''}& someParents=color_s:RED& fl=id,score,vectors,vector,[child fl=vector childFilter=$children.q] From 739f3e9b5be0ac7371ab50c68b2dc9f29f810569 Mon Sep 17 00:00:00 2001 From: Vishnu Priya Chandra Sekar Date: Thu, 9 Jul 2026 11:54:40 -0700 Subject: [PATCH 4/6] Removed the comments in the changelog --- ...-17433-changed-default-request-timeout.yml | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/changelog/unreleased/SOLR-17433-changed-default-request-timeout.yml b/changelog/unreleased/SOLR-17433-changed-default-request-timeout.yml index 53749319ed25..e7821271e8dc 100644 --- a/changelog/unreleased/SOLR-17433-changed-default-request-timeout.yml +++ b/changelog/unreleased/SOLR-17433-changed-default-request-timeout.yml @@ -1,27 +1,3 @@ -# (DELETE ALL COMMENTS UP HERE AFTER FILLING THIS IN - -# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc - -# If the change is minor, don't bother adding a changelog entry. -# For `other` type entries, the threshold to bother with a changelog entry should be even higher. - -# title: -# * The audience is end-users and administrators, not committers. -# * Be short and focused on the user impact. Multiple sentences is fine! -# * For technical/geeky details, prefer the commit message instead of changelog. -# * Reference JIRA issues like `SOLR-12345`, or if no JIRA but have a GitHub PR then `PR#12345`. - -# type: -# `added` for new features/improvements, opt-in by the user typically documented in the ref guide -# `changed` for improvements; not opt-in -# `fixed` for improvements that are deemed to have fixed buggy behavior -# `deprecated` for marking things deprecated -# `removed` for code removed -# `dependency_update` for updates to dependencies -# `other` for anything else, like large/significant refactorings, build changes, -# test infrastructure, or documentation. -# Most such changes are too small/minor to bother with a changelog entry. - title: Changed default http request timeout to infinite type: changed authors: From 52bad08c17b469ba007fa019fc96f959d3bb2c38 Mon Sep 17 00:00:00 2001 From: David Smiley Date: Sat, 11 Jul 2026 12:39:42 -0400 Subject: [PATCH 5/6] ConcurrentUpdateJettySolrClientTest: don't set request timeout --- .../solrj/jetty/ConcurrentUpdateJettySolrClientTest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/ConcurrentUpdateJettySolrClientTest.java b/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/ConcurrentUpdateJettySolrClientTest.java index e23f52bf0ad6..50d568d53f1e 100644 --- a/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/ConcurrentUpdateJettySolrClientTest.java +++ b/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/ConcurrentUpdateJettySolrClientTest.java @@ -48,10 +48,7 @@ public HttpSolrClient solrClient(Integer overrideIdleTimeoutMs) { var builder = new HttpJettySolrClient.Builder(); if (overrideIdleTimeoutMs != null) { builder - .withIdleTimeout(overrideIdleTimeoutMs, TimeUnit.MILLISECONDS) - // override the infinite request timeout with idle timeout to ensure idle requests time - // out. - .withRequestTimeout(overrideIdleTimeoutMs, TimeUnit.MILLISECONDS); + .withIdleTimeout(overrideIdleTimeoutMs, TimeUnit.MILLISECONDS); } return builder.build(); } From e5c4d0b95a2f52d040bb008b2527478a390a7c4e Mon Sep 17 00:00:00 2001 From: David Smiley Date: Sat, 11 Jul 2026 12:43:54 -0400 Subject: [PATCH 6/6] HttpJdkSolrClientTest: remove testTimeout() We already have testRequestTimeout(). Idle is unsupported; can't be tested. --- .../client/solrj/impl/HttpJdkSolrClientTest.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java index ef4edd8f63d4..0727506d8e16 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java @@ -208,21 +208,6 @@ public void testAsyncException() throws Exception { super.testAsyncExceptionBase(); } - @Test - public void testTimeout() throws Exception { - SolrQuery q = new SolrQuery("*:*"); - try (HttpJdkSolrClient client = - (HttpJdkSolrClient) - builder(solrTestRule.getBaseUrl() + SLOW_SERVLET_PATH, 500, 500) - .withRequestTimeout(500, TimeUnit.MILLISECONDS) - .build()) { - client.query(q, SolrRequest.METHOD.GET); - fail("No exception thrown."); - } catch (SolrServerException e) { - assertTrue(e.getMessage().contains("timeout") || e.getMessage().contains("Timeout")); - } - } - @Test public void test0IdleTimeout() throws Exception { SolrQuery q = new SolrQuery("*:*");