diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index cf82501f9bd..6479c718645 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -63,6 +63,9 @@ New Features * SOLR-9240: Support parallel ETL with the topic expression (Joel Bernstein) +* SOLR-9275: XML QueryParser support (defType=xmlparser) now extensible via configuration. + (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 cf3fb421954..1e0e5bd8365 100755 --- a/solr/core/src/java/org/apache/solr/search/SolrCoreParser.java +++ b/solr/core/src/java/org/apache/solr/search/SolrCoreParser.java @@ -16,23 +16,54 @@ */ package org.apache.solr.search; +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.solr.common.util.NamedList; +import org.apache.solr.core.SolrResourceLoader; import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.util.plugin.NamedListInitializedPlugin; /** * Assembles a QueryBuilder which uses Query objects from Solr's search module * in addition to Query objects supported by the Lucene CoreParser. */ -public class SolrCoreParser extends CoreParser { +public class SolrCoreParser extends CoreParser implements NamedListInitializedPlugin { + + protected final SolrQueryRequest req; public SolrCoreParser(String defaultField, Analyzer analyzer, SolrQueryRequest req) { super(defaultField, analyzer); + this.req = req; + } - // final IndexSchema schema = req.getSchema(); - // lucene_parser.addQueryBuilder("SomeOtherQuery", new SomeOtherQueryBuilder(schema)); + @Override + public void init(NamedList initArgs) { + final SolrResourceLoader loader; + if (req == null) { + loader = new SolrResourceLoader(); + } else { + loader = req.getCore().getResourceLoader(); + } + + final Iterable> args = initArgs; + for (final Map.Entry entry : args) { + 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}); + + this.queryFactory.addBuilder(queryName, queryBuilder); + } } } diff --git a/solr/core/src/java/org/apache/solr/search/SolrQueryBuilder.java b/solr/core/src/java/org/apache/solr/search/SolrQueryBuilder.java new file mode 100644 index 00000000000..e8135128337 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/search/SolrQueryBuilder.java @@ -0,0 +1,34 @@ +/* + * 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.QueryBuilder; +import org.apache.solr.request.SolrQueryRequest; + +public abstract class SolrQueryBuilder implements QueryBuilder { + + protected final SolrQueryRequest req; + protected final QueryBuilder queryFactory; + + public SolrQueryBuilder(String defaultField, Analyzer analyzer, + SolrQueryRequest req, QueryBuilder queryFactory) { + this.req = req; + this.queryFactory = queryFactory; + } + +} diff --git a/solr/core/src/java/org/apache/solr/search/XmlQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/XmlQParserPlugin.java index cb6b45eae5f..ee8e062782a 100755 --- a/solr/core/src/java/org/apache/solr/search/XmlQParserPlugin.java +++ b/solr/core/src/java/org/apache/solr/search/XmlQParserPlugin.java @@ -25,12 +25,21 @@ import org.apache.lucene.search.Query; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.NamedList; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.schema.IndexSchema; public class XmlQParserPlugin extends QParserPlugin { public static final String NAME = "xmlparser"; + private NamedList args; + + @Override + public void init( NamedList args ) { + super.init(args); + this.args = args; + } + private class XmlQParser extends QParser { public XmlQParser(String qstr, SolrParams localParams, @@ -46,7 +55,9 @@ public class XmlQParserPlugin extends QParserPlugin { final IndexSchema schema = req.getSchema(); final String defaultField = QueryParsing.getDefaultField(schema, getParam(CommonParams.DF)); final Analyzer analyzer = schema.getQueryAnalyzer(); + final SolrCoreParser solrParser = new SolrCoreParser(defaultField, analyzer, req); + solrParser.init(args); try { return solrParser.parse(new ByteArrayInputStream(qstr.getBytes(StandardCharsets.UTF_8))); } catch (ParserException e) { diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-testxmlparser.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-testxmlparser.xml new file mode 100644 index 00000000000..40c39a18984 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-testxmlparser.xml @@ -0,0 +1,33 @@ + + + + + + + ${tests.luceneMatchVersion:LATEST} + ${solr.data.dir:} + + + + + + org.apache.solr.search.HandyQueryBuilder + org.apache.solr.search.HelloQueryBuilder + org.apache.solr.search.GoodbyeQueryBuilder + + diff --git a/solr/core/src/test/org/apache/solr/search/GoodbyeQueryBuilder.java b/solr/core/src/test/org/apache/solr/search/GoodbyeQueryBuilder.java new file mode 100644 index 00000000000..af258d49359 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/GoodbyeQueryBuilder.java @@ -0,0 +1,39 @@ +/* + * 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.ParserException; +import org.apache.lucene.queryparser.xml.QueryBuilder; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.solr.request.SolrQueryRequest; +import org.w3c.dom.Element; + +public class GoodbyeQueryBuilder extends SolrQueryBuilder { + + public GoodbyeQueryBuilder(String defaultField, Analyzer analyzer, + SolrQueryRequest req, QueryBuilder queryFactory) { + super(defaultField, analyzer, req, queryFactory); + } + + @Override + public Query getQuery(Element e) throws ParserException { + return new MatchNoDocsQuery(); + } + +} diff --git a/solr/core/src/test/org/apache/solr/search/HandyQueryBuilder.java b/solr/core/src/test/org/apache/solr/search/HandyQueryBuilder.java new file mode 100644 index 00000000000..14a8aac66f5 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/HandyQueryBuilder.java @@ -0,0 +1,53 @@ +/* + * 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.DOMUtils; +import org.apache.lucene.queryparser.xml.ParserException; +import org.apache.lucene.queryparser.xml.QueryBuilder; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Query; +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 HandyQueryBuilder(String defaultField, Analyzer analyzer, + SolrQueryRequest req, QueryBuilder queryFactory) { + super(defaultField, analyzer, req, queryFactory); + } + + @Override + public Query getQuery(Element e) throws ParserException { + final BooleanQuery.Builder bq = new BooleanQuery.Builder(); + final Query lhsQ = getSubQuery(e, "Left"); + final Query rhsQ = getSubQuery(e, "Right"); + bq.add(new BooleanClause(lhsQ, BooleanClause.Occur.SHOULD)); + bq.add(new BooleanClause(rhsQ, BooleanClause.Occur.SHOULD)); + return bq.build(); + } + + private Query getSubQuery(Element e, String name) throws ParserException { + Element subE = DOMUtils.getChildByTagOrFail(e, name); + subE = DOMUtils.getFirstChildOrFail(subE); + return queryFactory.getQuery(subE); + } +} diff --git a/solr/core/src/test/org/apache/solr/search/HelloQueryBuilder.java b/solr/core/src/test/org/apache/solr/search/HelloQueryBuilder.java new file mode 100644 index 00000000000..642047f32fa --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/HelloQueryBuilder.java @@ -0,0 +1,39 @@ +/* + * 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.ParserException; +import org.apache.lucene.queryparser.xml.QueryBuilder; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.solr.request.SolrQueryRequest; +import org.w3c.dom.Element; + +public class HelloQueryBuilder extends SolrQueryBuilder { + + public HelloQueryBuilder(String defaultField, Analyzer analyzer, + SolrQueryRequest req, QueryBuilder queryFactory) { + super(defaultField, analyzer, req, queryFactory); + } + + @Override + public Query getQuery(Element e) throws ParserException { + return new MatchAllDocsQuery(); + } + +} diff --git a/solr/core/src/test/org/apache/solr/search/TestXmlQParserPlugin.java b/solr/core/src/test/org/apache/solr/search/TestXmlQParserPlugin.java new file mode 100644 index 00000000000..3c4edae0edc --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/TestXmlQParserPlugin.java @@ -0,0 +1,78 @@ +/* + * 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.SolrTestCaseJ4; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TestXmlQParserPlugin extends SolrTestCaseJ4 { + + @BeforeClass + public static void beforeClass() throws Exception { + initCore("solrconfig-testxmlparser.xml", "schema-minimal.xml"); + } + + @Override + @Before + public void setUp() throws Exception { + // if you override setUp or tearDown, you better call + // the super classes version + super.setUp(); + clearIndex(); + assertU(commit()); + } + + @Test + public void testHelloQuery() throws Exception { + final int numDocs = random().nextInt(10); + implTestQuery(numDocs, "", numDocs); + } + + @Test + public void testGoodbyeQuery() throws Exception { + final int numDocs = random().nextInt(10); + implTestQuery(numDocs, "", 0); + } + + @Test + public void testHandyQuery() throws Exception { + final int numDocs = random().nextInt(10); + final String q = ""; + implTestQuery(numDocs, q, numDocs); + } + + public void implTestQuery(int numDocs, String q, int expectedCount) throws Exception { + // add some documents + for (int ii=1; ii<=numDocs; ++ii) { + String[] doc = {"id",ii+"0"}; + assertU(adoc(doc)); + if (random().nextBoolean()) { + assertU(commit()); + } + } + assertU(commit()); + // and then run the query + ModifiableSolrParams params = new ModifiableSolrParams(); + params.add("defType", "testxmlparser"); + params.add("q", q); + assertQ(req(params), "*[count(//doc)="+expectedCount+"]"); + } + +}