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 + + ""; + // 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); + } + }