Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
007d5a8
Introduce json_queries key in JSON DSL, pass-through to req.getJSON()
Copilot Jun 30, 2026
45aa9cb
Introduce IntervalsQParserPlugin ({!intervals json_query=...}), retur…
Copilot Jun 30, 2026
f64ca06
Improve TestIntervalsQParserPlugin: add json_queries pass-through test
Copilot Jun 30, 2026
a77c3eb
Implement IntervalsQParserPlugin term interval query from JSON DSL
Copilot Jun 30, 2026
0c356b3
Implement OpenSearch-style intervals JSON rule parser
Copilot Jun 30, 2026
d89632c
Fix intervals DSL parser warnings and finalize validation
Copilot Jun 30, 2026
0256e7a
Clarify fuzzy expansion constant in intervals parser
Copilot Jun 30, 2026
c14938f
Add interval rule support: term, phrase, regexp, range, max_width, ex…
Copilot Jul 1, 2026
b4e4394
Add Intervals Query Parser documentation page and references
Copilot Jul 1, 2026
f05b945
Fix spelling: analyse -> analyze in intervals-query-parser.adoc
Copilot Jul 1, 2026
ef325c3
Add SOLR-13764 changelog fragment
Copilot Jul 1, 2026
ac0e13c
fix changelog
mkhludnev Jul 1, 2026
671c3f9
Support df local/query param as field for IntervalsQParser; update tests
Copilot Jul 1, 2026
9e2c4cf
Update intervals-query-parser.adoc to document both df and legacy que…
Copilot Jul 1, 2026
a48de05
Remove legacy format support from IntervalsQParserPlugin
Copilot Jul 1, 2026
5631943
Rename SOLR-13764-intervals-query-parser-reference-guide.yml to SOLR-…
mkhludnev Jul 1, 2026
95f95ad
Change type to 'added' for intervals query parser
mkhludnev Jul 1, 2026
6bfa9aa
Revise intervals query parser documentation
mkhludnev Jul 1, 2026
e5e1dd7
Add Lucene intervals package link to intervals-query-parser.adoc
Copilot Jul 1, 2026
13e9309
Fix spotless formatting in TestIntervalsQParserPlugin
Copilot Jul 1, 2026
d25c401
Fix the guide
mkhludnev Jul 1, 2026
1476ffc
Change source format from json to text in intervals query
mkhludnev Jul 1, 2026
fe55aad
Add intervals vs xmlparser nested query comparison test
Copilot Jul 1, 2026
a6f8438
Clarify JSON DSL reference in intervals query parser
mkhludnev Jul 1, 2026
9e558f7
Fix typo in author URL field
mkhludnev Jul 1, 2026
1be68da
Fix tidy and checkSiteLinks CI failures
Copilot Jul 1, 2026
dbf88de
Add DistributedQParserTest: cloud-based distributed search test for l…
Copilot Jul 1, 2026
9ad5c02
Add testIntervalsQParser to DistributedQParserTest
Copilot Jul 1, 2026
cf16eaa
fix: reformat DistributedQParserTest.java per spotless rules
Copilot Jul 1, 2026
5d928f4
Fix Spotless formatting violation in DistributedQParserTest.java
Copilot Jul 1, 2026
0b3b44f
Potential fix for pull request finding
mkhludnev Jul 1, 2026
19b5262
Apply suggestions from code review
mkhludnev Jul 1, 2026
65dd5f9
multiterm analyzer and QueryEqualityTest
mkhludnev Jul 1, 2026
4917c07
fix tests
mkhludnev Jul 1, 2026
966c705
review
mkhludnev Jul 2, 2026
5055635
Delete solr/.vscode/settings.json
mkhludnev Jul 2, 2026
0b589ae
fix test compile
mkhludnev Jul 2, 2026
70645d0
check prefix len
mkhludnev Jul 2, 2026
f7e7ac0
change syntax to {!intervals df=title}$foobar
mkhludnev Jul 2, 2026
422b46f
strict field check
mkhludnev Jul 2, 2026
752d5b9
assertJQ
mkhludnev Jul 3, 2026
1343572
tiding manually
mkhludnev Jul 3, 2026
bcc0462
tidyng spaces. dunno why
mkhludnev Jul 3, 2026
82fe732
testing pure json requests
mkhludnev Jul 4, 2026
53f0147
wipe settings
mkhludnev Jul 4, 2026
25239fe
add vscode to gitignore
mkhludnev Jul 4, 2026
0a85931
format
mkhludnev Jul 4, 2026
1b86d60
review suggestions
mkhludnev Jul 4, 2026
045c0fe
forbiddenapi
mkhludnev Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ CLAUDE.md
GEMINI.md
.gemini/

.vscode/
# WANT TO ADD MORE? You can tell Git without adding to this file:
# See https://git-scm.com/docs/gitignore
# In particular, if you have tools you use, add to $GIT_DIR/info/exclude or use core.excludesFile
10 changes: 10 additions & 0 deletions changelog/unreleased/SOLR-13764-intervals-query-parser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
title: Add Intervals Query Parser
type: added
authors:
- name: Mikhail Khludnev
nick: mkhludnev
url: https://home.apache.org/phonebook.html?uid=mkhl
links:
- name: SOLR-13764
url: https://issues.apache.org/jira/browse/SOLR-13764
10 changes: 10 additions & 0 deletions solr/core/src/java/org/apache/solr/request/json/RequestUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
import org.noggit.ObjectBuilder;

public class RequestUtil {
/**
* Top-level JSON key holding the map of named query definitions, referenced elsewhere (e.g. by
* {@link org.apache.solr.search.IntervalsQParserPlugin}) via a {@code $name} local param value.
*/
public static final String JSON_QUERIES_KEY = "json_queries";

/**
* Set default-ish params on a SolrQueryRequest as well as do standard macro processing and JSON
* request parsing.
Expand Down Expand Up @@ -254,6 +260,10 @@ public static void processParams(
SolrException.ErrorCode.BAD_REQUEST,
"Expected Map for 'queries', received " + queriesJsonObj);
}
} else if (JSON_QUERIES_KEY.equals(key)) {
// passed through as a parsed object for use by SearchComponent.prepare() at subordinate
// nodes; not processed here
continue;
} else if ("params".equals(key) || "facet".equals(key)) {
// handled elsewhere
continue;
Expand Down
704 changes: 704 additions & 0 deletions solr/core/src/java/org/apache/solr/search/IntervalsQParserPlugin.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public abstract class QParserPlugin implements NamedListInitializedPlugin {
map.put(VectorSimilarityQParserPlugin.NAME, new VectorSimilarityQParserPlugin());
map.put(FuzzyQParserPlugin.NAME, new FuzzyQParserPlugin());
map.put(NumericRangeQParserPlugin.NAME, new NumericRangeQParserPlugin());
map.put(IntervalsQParserPlugin.NAME, new IntervalsQParserPlugin());

standardPlugins = Collections.unmodifiableMap(map);
}
Expand Down
172 changes: 172 additions & 0 deletions solr/core/src/test/org/apache/solr/search/DistributedQParserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* 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.search;

import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.request.json.DirectJsonQueryRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.junit.BeforeClass;
import org.junit.Test;

/**
* Distributed search tests for the standard query parsers: {@code lucene}, {@code dismax}, {@code
* edismax}, and {@code intervals}.
*/
public class DistributedQParserTest extends SolrCloudTestCase {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have this... shouldn't this be only for intervals?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if the intent is testing distributed... well we have a whole test framework for that which is quite good: BaseDistributedSearchTestCase. Doesn't even require SolrCloud!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afiak SolrCloudTestCase is rather faster and convenient. It's a little bit redundant for local-only QP, but there's a https://github.com/apache/solr/pull/4582#issuecomment-4861727946 regarding transferring new JSON request entries. I've decided to double check.
We cad drop it if you wish.


private static final String COLLECTION = "distributed-qparser";

@BeforeClass
public static void setupCluster() throws Exception {
configureCluster(2).addConfig("conf", configset("cloud-dynamic")).configure();

CollectionAdminRequest.createCollection(COLLECTION, "conf", 2, 1)
.process(cluster.getSolrClient());
cluster.waitForActiveCollection(COLLECTION, 2, 2);

new UpdateRequest()
.add(sdoc("id", "1", "subject", "quick brown fox"))
.add(sdoc("id", "2", "subject", "lazy brown dog"))
.add(sdoc("id", "3", "subject", "quick red dog"))
.add(sdoc("id", "4", "subject", "slow green cat"))
.commit(cluster.getSolrClient(), COLLECTION);
}

@Test
public void testLuceneQParser() throws Exception {
QueryResponse response =
new QueryRequest(params("q", "subject:quick", "defType", "lucene", "fl", "id"))
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(2, response.getResults().getNumFound());

response =
new QueryRequest(params("q", "subject:brown", "defType", "lucene", "fl", "id"))
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(2, response.getResults().getNumFound());
}

@Test
public void testDismaxQParser() throws Exception {
QueryResponse response =
new QueryRequest(params("q", "quick", "defType", "dismax", "qf", "subject", "fl", "id"))
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(2, response.getResults().getNumFound());

response =
new QueryRequest(params("q", "brown dog", "defType", "dismax", "qf", "subject", "fl", "id"))
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(3, response.getResults().getNumFound());
}

@Test
public void testEdismaxQParser() throws Exception {
QueryResponse response =
new QueryRequest(params("q", "quick", "defType", "edismax", "qf", "subject", "fl", "id"))
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(2, response.getResults().getNumFound());

response =
new QueryRequest(
params("q", "brown dog", "defType", "edismax", "qf", "subject", "fl", "id"))
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(3, response.getResults().getNumFound());
}

@Test
public void testIntervalsQParser() throws Exception {
// match rule: "quick" appears in docs 1 ("quick brown fox") and 3 ("quick red dog")
String jsonQueries =
"'q1': {'match': {'query': 'quick'}}"
+ (random().nextBoolean() ? ", 'ignore': {'match': {'query': 'lazy'}}" : "");
QueryResponse response =
new DirectJsonQueryRequest(
"{"
+ "'query': {intervals: {df: subject, query: $q1}},"
+ "'json_queries': {"
+ jsonQueries
+ "},"
+ "'fields': 'id'"
+ "}")
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(2, response.getResults().getNumFound());

// a distinct match rule: "lazy" appears only in doc 2 ("lazy brown dog") — confirm the
// result differs from the "quick" query above
jsonQueries =
"'q1': {'match': {'query': 'lazy'}}"
+ (random().nextBoolean() ? ", 'ignore': {'match': {'query': 'quick'}}" : "");
QueryResponse lazyResponse =
new DirectJsonQueryRequest(
"{"
+ "'query': {intervals: {df: subject, query: $q1}},"
+ "'json_queries': {"
+ jsonQueries
+ "},"
+ "'fields': 'id'"
+ "}")
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(1, lazyResponse.getResults().getNumFound());
assertNotEquals(response.getResults().getNumFound(), lazyResponse.getResults().getNumFound());

// all_of ordered: "quick" then "fox" — only doc 1 ("quick brown fox") matches
response =
new DirectJsonQueryRequest(
"{"
+ "'query': {intervals: {df: subject, query: $q1}},"
+ "'json_queries': {'q1': {'all_of': {'ordered': true,"
+ "'intervals': [{'match': {'query': 'quick'}},{'match': {'query': 'fox'}}]}}},"
+ "'fields': 'id'"
+ "}")
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(1, response.getResults().getNumFound());

// union of two top-level interval queries: "quick" (docs 1, 3) or "lazy" (doc 2) — three
// docs match
response =
new DirectJsonQueryRequest(
"{"
+ "'query': {'bool': {'should': ["
+ "{intervals: {df: subject, query: $q1}},"
+ "{intervals: {df: subject, query: $q2}}]}},"
+ "'json_queries': {'q1': {'match': {'query': 'quick'}},"
+ "'q2': {'match': {'query': 'lazy'}}},"
+ "'fields': 'id'"
+ "}")
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(3, response.getResults().getNumFound());

// intersection of two top-level interval queries: "quick" (docs 1, 3) and "brown"
// (docs 1, 2) — only doc 1 has both terms
response =
new DirectJsonQueryRequest(
"{"
+ "'query': {'bool': {'must': ["
+ " {intervals: {df: subject, query: $q1}},"
+ " {intervals: { query: $q2}}"
+ "]}},"
+ "'json_queries': {'q1': {'match': {'query': 'quick'}},"
+ "'q2': {'match': {'query': 'brown'}}},"
+ "'fields': 'id',"
+ "params:{df: subject}"
+ "}")
.process(cluster.getSolrClient(), COLLECTION);
assertEquals(1, response.getResults().getNumFound());
}
}
11 changes: 11 additions & 0 deletions solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,17 @@ public void testQueryFuzzy() throws Exception {
}
}

public void testQueryIntervals() throws Exception {
try (SolrQueryRequest req = req("myField", "foo_s")) {
req.setJSON(Map.of("json_queries", Map.of("q1", Map.of("term", Map.of("value", "asdf")))));
assertQueryEquals(
IntervalsQParserPlugin.NAME,
req,
"{!intervals df=$myField}$q1",
"{!intervals df=foo_s}$q1");
}
}

public void testQueryBoost() throws Exception {
SolrQueryRequest req = req("df", "foo_s", "myBoost", "sum(3,foo_i)");
try {
Expand Down
Loading
Loading