diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index eb4c3f84d74..39c4e48f311 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -121,6 +121,9 @@ New Features
* SOLR-8029: Added new style APIs and a framework for creating new APIs and mapping old APIs to new
(noble, Steve Rowe, Cassandra Targett, Timothy Potter)
+* SOLR-9933: SolrCoreParser now supports configuration of custom SpanQueryBuilder classes.
+ (Daniel Collins, Christine Poerschke)
+
Bug Fixes
----------------------
diff --git a/solr/core/src/java/org/apache/solr/search/SolrCoreParser.java b/solr/core/src/java/org/apache/solr/search/SolrCoreParser.java
index 3f6596b7cc2..0a2cf5891a1 100755
--- a/solr/core/src/java/org/apache/solr/search/SolrCoreParser.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrCoreParser.java
@@ -16,15 +16,20 @@
*/
package org.apache.solr.search;
+import java.lang.invoke.MethodHandles;
import java.util.Map;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.queryparser.xml.CoreParser;
import org.apache.lucene.queryparser.xml.QueryBuilder;
+import org.apache.lucene.queryparser.xml.builders.SpanQueryBuilder;
+import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.util.plugin.NamedListInitializedPlugin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Assembles a QueryBuilder which uses Query objects from Solr's search
module
@@ -32,6 +37,8 @@ import org.apache.solr.util.plugin.NamedListInitializedPlugin;
*/
public class SolrCoreParser extends CoreParser implements NamedListInitializedPlugin {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
protected final SolrQueryRequest req;
public SolrCoreParser(String defaultField, Analyzer analyzer,
@@ -58,14 +65,35 @@ public class SolrCoreParser extends CoreParser implements NamedListInitializedPl
final String queryName = entry.getKey();
final String queryBuilderClassName = (String)entry.getValue();
- final SolrQueryBuilder queryBuilder = loader.newInstance(
- queryBuilderClassName,
- SolrQueryBuilder.class,
- null,
- new Class[] {String.class, Analyzer.class, SolrQueryRequest.class, QueryBuilder.class},
- new Object[] {defaultField, analyzer, req, this});
+ try {
+ final SolrSpanQueryBuilder spanQueryBuilder = loader.newInstance(
+ queryBuilderClassName,
+ SolrSpanQueryBuilder.class,
+ null,
+ new Class[] {String.class, Analyzer.class, SolrQueryRequest.class, SpanQueryBuilder.class},
+ new Object[] {defaultField, analyzer, req, this});
- this.queryFactory.addBuilder(queryName, queryBuilder);
+ this.addSpanQueryBuilder(queryName, spanQueryBuilder);
+ } catch (Exception outerException) {
+ try {
+ final SolrQueryBuilder queryBuilder = loader.newInstance(
+ queryBuilderClassName,
+ SolrQueryBuilder.class,
+ null,
+ new Class[] {String.class, Analyzer.class, SolrQueryRequest.class, QueryBuilder.class},
+ new Object[] {defaultField, analyzer, req, this});
+
+ this.addQueryBuilder(queryName, queryBuilder);
+ } catch (Exception innerException) {
+ log.error("Class {} not found or not suitable: {} {}",
+ queryBuilderClassName, outerException, innerException);
+ throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, "Cannot find suitable "
+ + SolrSpanQueryBuilder.class.getCanonicalName() + " or "
+ + SolrQueryBuilder.class.getCanonicalName() + " class: "
+ + queryBuilderClassName + " in "
+ + loader);
+ }
+ }
}
}
diff --git a/solr/core/src/java/org/apache/solr/search/SolrSpanQueryBuilder.java b/solr/core/src/java/org/apache/solr/search/SolrSpanQueryBuilder.java
new file mode 100644
index 00000000000..2dea85c87b2
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/SolrSpanQueryBuilder.java
@@ -0,0 +1,33 @@
+/*
+ * 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.lucene.analysis.Analyzer;
+import org.apache.lucene.queryparser.xml.builders.SpanQueryBuilder;
+import org.apache.solr.request.SolrQueryRequest;
+
+public abstract class SolrSpanQueryBuilder extends SolrQueryBuilder implements SpanQueryBuilder {
+
+ protected final SpanQueryBuilder spanFactory;
+
+ public SolrSpanQueryBuilder(String defaultField, Analyzer analyzer,
+ SolrQueryRequest req, SpanQueryBuilder spanFactory) {
+ super(defaultField, analyzer, req, spanFactory);
+ this.spanFactory = spanFactory;
+ }
+
+}
diff --git a/solr/core/src/test/org/apache/solr/search/ApacheLuceneSolrNearQueryBuilder.java b/solr/core/src/test/org/apache/solr/search/ApacheLuceneSolrNearQueryBuilder.java
index bbc081a45ce..574a736406a 100644
--- a/solr/core/src/test/org/apache/solr/search/ApacheLuceneSolrNearQueryBuilder.java
+++ b/solr/core/src/test/org/apache/solr/search/ApacheLuceneSolrNearQueryBuilder.java
@@ -20,7 +20,7 @@ import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.xml.DOMUtils;
import org.apache.lucene.queryparser.xml.ParserException;
-import org.apache.lucene.queryparser.xml.QueryBuilder;
+import org.apache.lucene.queryparser.xml.builders.SpanQueryBuilder;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanQuery;
@@ -28,14 +28,18 @@ import org.apache.lucene.search.spans.SpanTermQuery;
import org.apache.solr.request.SolrQueryRequest;
import org.w3c.dom.Element;
-public class ApacheLuceneSolrNearQueryBuilder extends SolrQueryBuilder {
+public class ApacheLuceneSolrNearQueryBuilder extends SolrSpanQueryBuilder {
public ApacheLuceneSolrNearQueryBuilder(String defaultField, Analyzer analyzer,
- SolrQueryRequest req, QueryBuilder queryFactory) {
- super(defaultField, analyzer, req, queryFactory);
+ SolrQueryRequest req, SpanQueryBuilder spanFactory) {
+ super(defaultField, analyzer, req, spanFactory);
}
public Query getQuery(Element e) throws ParserException {
+ return getSpanQuery(e);
+ }
+
+ public SpanQuery getSpanQuery(Element e) throws ParserException {
final String fieldName = DOMUtils.getAttributeWithInheritanceOrFail(e, "fieldName");
final SpanQuery[] spanQueries = new SpanQuery[]{
new SpanTermQuery(new Term(fieldName, "Apache")),
diff --git a/solr/core/src/test/org/apache/solr/search/ChooseOneWordQueryBuilder.java b/solr/core/src/test/org/apache/solr/search/ChooseOneWordQueryBuilder.java
new file mode 100644
index 00000000000..6e2112ec47d
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/ChooseOneWordQueryBuilder.java
@@ -0,0 +1,62 @@
+/*
+ * 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.lucene.analysis.Analyzer;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.queryparser.xml.DOMUtils;
+import org.apache.lucene.queryparser.xml.ParserException;
+import org.apache.lucene.queryparser.xml.builders.SpanQueryBuilder;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.spans.SpanQuery;
+import org.apache.lucene.search.spans.SpanTermQuery;
+import org.apache.solr.request.SolrQueryRequest;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+public class ChooseOneWordQueryBuilder extends SolrSpanQueryBuilder {
+
+ public ChooseOneWordQueryBuilder(String defaultField, Analyzer analyzer, SolrQueryRequest req,
+ SpanQueryBuilder spanFactory) {
+ super(defaultField, analyzer, req, spanFactory);
+ }
+
+ public Query getQuery(Element e) throws ParserException {
+ return implGetQuery(e, false);
+ }
+
+ public SpanQuery getSpanQuery(Element e) throws ParserException {
+ return (SpanQuery)implGetQuery(e, true);
+ }
+
+ public Query implGetQuery(Element e, boolean span) throws ParserException {
+ Term term = null;
+ final String fieldName = DOMUtils.getAttributeWithInheritanceOrFail(e, "fieldName");
+ for (Node node = e.getFirstChild(); node != null; node = node.getNextSibling()) {
+ if (node.getNodeType() == Node.ELEMENT_NODE &&
+ node.getNodeName().equals("Word")) {
+ final String word = DOMUtils.getNonBlankTextOrFail((Element) node);
+ final Term t = new Term(fieldName, word);
+ if (term == null || term.text().length() < t.text().length()) {
+ term = t;
+ }
+ }
+ }
+ return (span ? new SpanTermQuery(term) : new TermQuery(term));
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/search/HandyQueryBuilder.java b/solr/core/src/test/org/apache/solr/search/HandyQueryBuilder.java
index c38fb6b81c1..f76015fde80 100644
--- a/solr/core/src/test/org/apache/solr/search/HandyQueryBuilder.java
+++ b/solr/core/src/test/org/apache/solr/search/HandyQueryBuilder.java
@@ -19,20 +19,22 @@ package org.apache.solr.search;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.queryparser.xml.DOMUtils;
import org.apache.lucene.queryparser.xml.ParserException;
-import org.apache.lucene.queryparser.xml.QueryBuilder;
+import org.apache.lucene.queryparser.xml.builders.SpanQueryBuilder;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
+import org.apache.lucene.search.spans.SpanOrQuery;
+import org.apache.lucene.search.spans.SpanQuery;
import org.apache.solr.request.SolrQueryRequest;
import org.w3c.dom.Element;
// A simple test query builder to demonstrate use of
// SolrQueryBuilder's queryFactory constructor argument.
-public class HandyQueryBuilder extends SolrQueryBuilder {
+public class HandyQueryBuilder extends SolrSpanQueryBuilder {
public HandyQueryBuilder(String defaultField, Analyzer analyzer,
- SolrQueryRequest req, QueryBuilder queryFactory) {
- super(defaultField, analyzer, req, queryFactory);
+ SolrQueryRequest req, SpanQueryBuilder spanFactory) {
+ super(defaultField, analyzer, req, spanFactory);
}
public Query getQuery(Element e) throws ParserException {
@@ -44,9 +46,24 @@ public class HandyQueryBuilder extends SolrQueryBuilder {
return bq.build();
}
+ public SpanQuery getSpanQuery(Element e) throws ParserException {
+ SpanQuery subQueries[] = {
+ getSubSpanQuery(e, "Left"),
+ getSubSpanQuery(e, "Right"),
+ };
+
+ return new SpanOrQuery(subQueries);
+ }
+
private Query getSubQuery(Element e, String name) throws ParserException {
Element subE = DOMUtils.getChildByTagOrFail(e, name);
subE = DOMUtils.getFirstChildOrFail(subE);
return queryFactory.getQuery(subE);
}
+
+ private SpanQuery getSubSpanQuery(Element e, String name) throws ParserException {
+ Element subE = DOMUtils.getChildByTagOrFail(e, name);
+ subE = DOMUtils.getFirstChildOrFail(subE);
+ return spanFactory.getSpanQuery(subE);
+ }
}
diff --git a/solr/core/src/test/org/apache/solr/search/TestSolrCoreParser.java b/solr/core/src/test/org/apache/solr/search/TestSolrCoreParser.java
index 3ef96c3faae..79740e68522 100644
--- a/solr/core/src/test/org/apache/solr/search/TestSolrCoreParser.java
+++ b/solr/core/src/test/org/apache/solr/search/TestSolrCoreParser.java
@@ -24,13 +24,18 @@ import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.MockAnalyzer;
import org.apache.lucene.analysis.MockTokenFilter;
import org.apache.lucene.analysis.MockTokenizer;
+import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.xml.CoreParser;
import org.apache.lucene.queryparser.xml.ParserException;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.spans.SpanBoostQuery;
import org.apache.lucene.search.spans.SpanNearQuery;
+import org.apache.lucene.search.spans.SpanOrQuery;
+import org.apache.lucene.search.spans.SpanQuery;
import org.apache.lucene.search.spans.SpanTermQuery;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.common.util.NamedList;
@@ -52,6 +57,7 @@ public class TestSolrCoreParser extends LuceneTestCase {
args.add("GoodbyeQuery", GoodbyeQueryBuilder.class.getCanonicalName());
args.add("HandyQuery", HandyQueryBuilder.class.getCanonicalName());
args.add("ApacheLuceneSolr", ApacheLuceneSolrNearQueryBuilder.class.getCanonicalName());
+ args.add("ChooseOneWord", ChooseOneWordQueryBuilder.class.getCanonicalName());
solrCoreParser.init(args);
}
}
@@ -85,6 +91,10 @@ public class TestSolrCoreParser extends LuceneTestCase {
public void testApacheLuceneSolr() throws IOException, ParserException {
final String fieldName = "contents";
final Query query = parseXmlString("");
+ checkApacheLuceneSolr(query, fieldName);
+ }
+
+ private static void checkApacheLuceneSolr(Query query, String fieldName) {
assertTrue(query instanceof SpanNearQuery);
final SpanNearQuery snq = (SpanNearQuery)query;
assertEquals(fieldName, snq.getField());
@@ -96,6 +106,7 @@ public class TestSolrCoreParser extends LuceneTestCase {
assertTrue(snq.getClauses()[2] instanceof SpanTermQuery);
}
+ // test custom query (HandyQueryBuilder) wrapping a Query
public void testHandyQuery() throws IOException, ParserException {
final String lhsXml = "";
final String rhsXml = "";
@@ -107,4 +118,101 @@ public class TestSolrCoreParser extends LuceneTestCase {
assertTrue(bq.clauses().get(1).getQuery() instanceof MatchNoDocsQuery);
}
+ private static SpanQuery unwrapSpanBoostQuery(Query query) {
+ assertTrue(query instanceof SpanBoostQuery);
+ final SpanBoostQuery spanBoostQuery = (SpanBoostQuery)query;
+ return spanBoostQuery.getQuery();
+ }
+
+ // test custom query (HandyQueryBuilder) wrapping a SpanQuery
+ public void testHandySpanQuery() throws IOException, ParserException {
+ final String lhsXml = ""
+ + "rain"
+ + "spain"
+ + "plain"
+ + "";
+ final String rhsXml = ""
+ + "sunny"
+ + "sky"
+ + "";
+ final Query query = parseHandyQuery(lhsXml, rhsXml);
+ final BooleanQuery bq = (BooleanQuery)query;
+ assertEquals(2, bq.clauses().size());
+ for (int ii=0; ii");
+ for (String termText : termTexts) {
+ sb.append("").append(termText).append("");
+ }
+ sb.append("");
+ return sb.toString();
+ }
+
+ // test custom queries being wrapped in a Query or SpanQuery
+ public void testCustomQueryWrapping() throws IOException, ParserException {
+ final boolean span = random().nextBoolean();
+ // the custom queries
+ final String fieldName = "contents";
+ final String[] randomTerms = new String[] {"bumble", "honey", "solitary"};
+ final String randomQuery = composeChooseOneWordQueryXml(fieldName, randomTerms);
+ final String apacheLuceneSolr = "";
+ // the wrapping query
+ final String parentQuery = (span ? "SpanOr" : "BooleanQuery");
+ final String subQueryPrefix = (span ? "" : "");
+ final String subQuerySuffix = (span ? "" : "");
+ final String xml = "<"+parentQuery+">"
+ + subQueryPrefix+randomQuery+subQuerySuffix
+ + subQueryPrefix+apacheLuceneSolr+subQuerySuffix
+ + ""+parentQuery+">";
+ // the test
+ final Query query = parseXmlString(xml);
+ if (span) {
+ assertTrue(unwrapSpanBoostQuery(query) instanceof SpanOrQuery);
+ final SpanOrQuery soq = (SpanOrQuery)unwrapSpanBoostQuery(query);
+ assertEquals(2, soq.getClauses().length);
+ checkChooseOneWordQuery(span, soq.getClauses()[0], fieldName, randomTerms);
+ checkApacheLuceneSolr(soq.getClauses()[1], fieldName);
+ } else {
+ assertTrue(query instanceof BooleanQuery);
+ final BooleanQuery bq = (BooleanQuery)query;
+ assertEquals(2, bq.clauses().size());
+ checkChooseOneWordQuery(span, bq.clauses().get(0).getQuery(), fieldName, randomTerms);
+ checkApacheLuceneSolr(bq.clauses().get(1).getQuery(), fieldName);
+ }
+ }
+
+ private static void checkChooseOneWordQuery(boolean span, Query query, String fieldName, String ... expectedTermTexts) {
+ final Term term;
+ if (span) {
+ assertTrue(query instanceof SpanTermQuery);
+ final SpanTermQuery stq = (SpanTermQuery)query;
+ term = stq.getTerm();
+ } else {
+ assertTrue(query instanceof TermQuery);
+ final TermQuery tq = (TermQuery)query;
+ term = tq.getTerm();
+ }
+ final String text = term.text();
+ boolean foundExpected = false;
+ for (String expected : expectedTermTexts) {
+ foundExpected |= expected.equals(text);
+ }
+ assertEquals(fieldName, term.field());
+ assertTrue("expected term text ("+text+") not found in ("+expectedTermTexts+")", foundExpected);
+ }
+
}