| /* |
| * 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.util; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.lucene.index.Term; |
| import org.apache.lucene.search.BooleanClause; |
| import org.apache.lucene.search.BooleanClause.Occur; |
| import org.apache.lucene.search.BooleanQuery; |
| import org.apache.lucene.search.DisjunctionMaxQuery; |
| import org.apache.lucene.search.PhraseQuery; |
| import org.apache.lucene.search.Query; |
| import org.apache.lucene.search.TermQuery; |
| import org.apache.solr.SolrTestCaseJ4; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.request.SolrQueryRequest; |
| import org.apache.solr.search.QParser; |
| import org.apache.solr.util.SolrPluginUtils.DisjunctionMaxQueryParser; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| |
| /** |
| * Tests that the functions in SolrPluginUtils work as advertised. |
| */ |
| public class SolrPluginUtilsTest extends SolrTestCaseJ4 { |
| |
| @BeforeClass |
| public static void beforeClass() throws Exception { |
| initCore("solrconfig.xml","schema.xml"); |
| } |
| |
| @Test |
| public void testPartialEscape() { |
| |
| assertEquals("",pe("")); |
| assertEquals("foo",pe("foo")); |
| assertEquals("foo\\:bar",pe("foo:bar")); |
| assertEquals("+foo\\:bar",pe("+foo:bar")); |
| assertEquals("foo \\! bar",pe("foo ! bar")); |
| assertEquals("foo\\?",pe("foo?")); |
| assertEquals("foo \"bar\"",pe("foo \"bar\"")); |
| assertEquals("foo\\! \"bar\"",pe("foo! \"bar\"")); |
| |
| } |
| |
| @Test |
| public void testStripUnbalancedQuotes() { |
| |
| assertEquals("",strip("")); |
| assertEquals("foo",strip("foo")); |
| assertEquals("foo \"bar\"",strip("foo \"bar\"")); |
| assertEquals("42",strip("42\"")); |
| assertEquals("\"how now brown cow?\"",strip("\"how now brown cow?\"")); |
| assertEquals("\"you go\" \"now!\"",strip("\"you go\" \"now!\"")); |
| |
| } |
| |
| @Test |
| public void testStripIllegalOperators() { |
| |
| assertEquals("",stripOp("")); |
| assertEquals("foo",stripOp("foo")); |
| assertEquals("foo -bar",stripOp("foo -bar")); |
| assertEquals("foo +bar",stripOp("foo +bar")); |
| assertEquals("foo + bar",stripOp("foo + bar")); |
| assertEquals("foo+ bar",stripOp("foo+ bar")); |
| assertEquals("foo+ bar",stripOp("foo+ bar")); |
| assertEquals("foo+",stripOp("foo+")); |
| assertEquals("foo bar",stripOp("foo bar -")); |
| assertEquals("foo bar ",stripOp("foo bar - + ++")); |
| assertEquals("foo bar",stripOp("foo --bar")); |
| assertEquals("foo bar ",stripOp("foo -------------------------------------------------------------------------------------------------------------------------bar --")); |
| assertEquals("foo bar ",stripOp("foo --bar -----------------------------------------------------------------------------------------------------------------------")); |
| |
| } |
| |
| @Test |
| public void testParseFieldBoosts() throws Exception { |
| |
| Map<String,Float> e1 = new HashMap<>(); |
| e1.put("fieldOne",2.3f); |
| e1.put("fieldTwo",null); |
| e1.put("fieldThree",-0.4f); |
| |
| assertEquals("basic e1", e1, SolrPluginUtils.parseFieldBoosts |
| ("fieldOne^2.3 fieldTwo fieldThree^-0.4")); |
| assertEquals("spacey e1", e1, SolrPluginUtils.parseFieldBoosts |
| (" fieldOne^2.3 fieldTwo fieldThree^-0.4 ")); |
| assertEquals("really spacey e1", e1, SolrPluginUtils.parseFieldBoosts |
| (" \t fieldOne^2.3 \n fieldTwo fieldThree^-0.4 ")); |
| assertEquals("really spacey e1", e1, SolrPluginUtils.parseFieldBoosts |
| (new String[]{" \t fieldOne^2.3 \n", |
| " fieldTwo fieldThree^-0.4 ", |
| " "})); |
| |
| Map<String,Float> e2 = new HashMap<>(); |
| assertEquals("empty e2", e2, SolrPluginUtils.parseFieldBoosts |
| ("")); |
| assertEquals("spacey e2", e2, SolrPluginUtils.parseFieldBoosts |
| (" \t ")); |
| } |
| |
| @Test |
| public void testDisjunctionMaxQueryParser() throws Exception { |
| |
| Query out; |
| String t; |
| |
| SolrQueryRequest req = req("df", "text"); |
| QParser qparser = QParser.getParser("hi", "dismax", req); |
| |
| DisjunctionMaxQueryParser qp = |
| new SolrPluginUtils.DisjunctionMaxQueryParser(qparser, req.getParams().get("df")); |
| |
| qp.addAlias("hoss", 0.01f, SolrPluginUtils.parseFieldBoosts |
| ("title^2.0 title_stemmed name^1.2 subject^0.5")); |
| qp.addAlias("test", 0.01f, SolrPluginUtils.parseFieldBoosts("text^2.0")); |
| qp.addAlias("unused", 1.0f, SolrPluginUtils.parseFieldBoosts |
| ("subject^0.5 sind^1.5")); |
| |
| |
| /* first some sanity tests that don't use aliasing at all */ |
| |
| t = "XXXXXXXX"; |
| out = qp.parse(t); |
| assertNotNull(t+" sanity test gave back null", out); |
| assertTrue(t+" sanity test isn't TermQuery: " + out.getClass(), |
| out instanceof TermQuery); |
| assertEquals(t+" sanity test is wrong field", |
| qp.getDefaultField(), |
| ((TermQuery)out).getTerm().field()); |
| |
| t = "subject:XXXXXXXX"; |
| out = qp.parse(t); |
| assertNotNull(t+" sanity test gave back null", out); |
| assertTrue(t+" sanity test isn't TermQuery: " + out.getClass(), |
| out instanceof TermQuery); |
| assertEquals(t+" sanity test is wrong field", "subject", |
| ((TermQuery)out).getTerm().field()); |
| |
| /* field has untokenzied type, so this should be a term anyway */ |
| t = "sind:\"simple phrase\""; |
| out = qp.parse(t); |
| assertNotNull(t+" sanity test gave back null", out); |
| assertTrue(t+" sanity test isn't TermQuery: " + out.getClass(), |
| out instanceof TermQuery); |
| assertEquals(t+" sanity test is wrong field", "sind", |
| ((TermQuery)out).getTerm().field()); |
| |
| t = "subject:\"simple phrase\""; |
| out = qp.parse(t); |
| assertNotNull(t+" sanity test gave back null", out); |
| assertTrue(t+" sanity test isn't PhraseQuery: " + out.getClass(), |
| out instanceof PhraseQuery); |
| assertEquals(t+" sanity test is wrong field", "subject", |
| ((PhraseQuery)out).getTerms()[0].field()); |
| |
| |
| /* now some tests that use aliasing */ |
| |
| /* basic usage of single "term" */ |
| t = "hoss:XXXXXXXX"; |
| out = qp.parse(t); |
| assertNotNull(t+" was null", out); |
| assertTrue(t+" wasn't a DMQ:" + out.getClass(), |
| out instanceof DisjunctionMaxQuery); |
| assertEquals(t+" wrong number of clauses", 4, |
| countItems(((DisjunctionMaxQuery)out).iterator())); |
| |
| |
| /* odd case, but should still work, DMQ of one clause */ |
| t = "test:YYYYY"; |
| out = qp.parse(t); |
| assertNotNull(t+" was null", out); |
| assertTrue(t+" wasn't a DMQ:" + out.getClass(), |
| out instanceof DisjunctionMaxQuery); |
| assertEquals(t+" wrong number of clauses", 1, |
| countItems(((DisjunctionMaxQuery)out).iterator())); |
| |
| /* basic usage of multiple "terms" */ |
| t = "hoss:XXXXXXXX test:YYYYY"; |
| out = qp.parse(t); |
| assertNotNull(t+" was null", out); |
| assertTrue(t+" wasn't a boolean:" + out.getClass(), |
| out instanceof BooleanQuery); |
| { |
| BooleanQuery bq = (BooleanQuery)out; |
| List<BooleanClause> clauses = new ArrayList<>(bq.clauses()); |
| assertEquals(t+" wrong number of clauses", 2, |
| clauses.size()); |
| Query sub = clauses.get(0).getQuery(); |
| assertTrue(t+" first wasn't a DMQ:" + sub.getClass(), |
| sub instanceof DisjunctionMaxQuery); |
| assertEquals(t+" first had wrong number of clauses", 4, |
| countItems(((DisjunctionMaxQuery)sub).iterator())); |
| sub = clauses.get(1).getQuery(); |
| assertTrue(t+" second wasn't a DMQ:" + sub.getClass(), |
| sub instanceof DisjunctionMaxQuery); |
| assertEquals(t+" second had wrong number of clauses", 1, |
| countItems(((DisjunctionMaxQuery)sub).iterator())); |
| } |
| |
| /* a phrase, and a term that is a stop word for some fields */ |
| t = "hoss:\"XXXXXX YYYYY\" hoss:the"; |
| out = qp.parse(t); |
| assertNotNull(t+" was null", out); |
| assertTrue(t+" wasn't a boolean:" + out.getClass(), |
| out instanceof BooleanQuery); |
| { |
| BooleanQuery bq = (BooleanQuery)out; |
| List<BooleanClause> clauses = new ArrayList<>(bq.clauses()); |
| assertEquals(t+" wrong number of clauses", 2, |
| clauses.size()); |
| Query sub = clauses.get(0).getQuery(); |
| assertTrue(t+" first wasn't a DMQ:" + sub.getClass(), |
| sub instanceof DisjunctionMaxQuery); |
| assertEquals(t+" first had wrong number of clauses", 4, |
| countItems(((DisjunctionMaxQuery)sub).iterator())); |
| sub = clauses.get(1).getQuery(); |
| assertTrue(t+" second wasn't a DMQ:" + sub.getClass(), |
| sub instanceof DisjunctionMaxQuery); |
| assertEquals(t+" second had wrong number of clauses (stop words)", 2, |
| countItems(((DisjunctionMaxQuery)sub).iterator())); |
| } |
| |
| |
| |
| } |
| |
| private static int countItems(@SuppressWarnings({"rawtypes"})Iterator i) { |
| int count = 0; |
| while (i.hasNext()) { |
| count++; |
| i.next(); |
| } |
| return count; |
| } |
| |
| @Test |
| public void testMinShouldMatchCalculator() { |
| |
| /* zero is zero is zero */ |
| assertEquals(0, calcMSM(5, "0")); |
| assertEquals(0, calcMSM(5, "0%")); |
| assertEquals(0, calcMSM(5, " -5 ")); |
| assertEquals(0, calcMSM(5, "\n -100% \n")); |
| |
| /* basic integers */ |
| assertEquals(3, calcMSM(5, " \n3\n ")); |
| assertEquals(2, calcMSM(5, "-3")); |
| assertEquals(3, calcMSM(3, "3")); |
| assertEquals(0, calcMSM(3, "-3")); |
| assertEquals(3, calcMSM(3, "5")); |
| assertEquals(0, calcMSM(3, "-5")); |
| |
| /* positive percentages with rounding */ |
| assertEquals(0, calcMSM(3, " \n25% \n")); |
| assertEquals(1, calcMSM(4, "25%")); |
| assertEquals(1, calcMSM(5, " 25% ")); |
| assertEquals(2, calcMSM(10, "25%")); |
| |
| /* negative percentages with rounding */ |
| assertEquals(3, calcMSM(3, " \n-25%\n ")); |
| assertEquals(3, calcMSM(4, "-25%")); |
| assertEquals(4, calcMSM(5, "-25%")); |
| assertEquals(8, calcMSM(10, "-25%")); |
| |
| /* conditional */ |
| assertEquals(1, calcMSM(1, "3<0")); |
| assertEquals(2, calcMSM(2, "3<0")); |
| assertEquals(3, calcMSM(3, "3<0")); |
| assertEquals(0, calcMSM(4, "3<0")); |
| assertEquals(0, calcMSM(5, "3<0")); |
| assertEquals(1, calcMSM(1, "3<25%")); |
| assertEquals(2, calcMSM(2, " 3\n<\n25% ")); |
| assertEquals(3, calcMSM(3, "3<25%")); |
| assertEquals(1, calcMSM(4, "\n 3 < \n25%\n ")); |
| assertEquals(1, calcMSM(5, "3<25%")); |
| |
| /* multiple conditionals */ |
| assertEquals(1, calcMSM(1, "\n3 < -25% 10 < -3 \n")); |
| assertEquals(2, calcMSM(2, " 3 < -25% 10 < -3\n")); |
| assertEquals(3, calcMSM(3, " 3 < -25% \n 10 < -3 \n")); |
| assertEquals(3, calcMSM(4, " 3 < -25% 10 < -3 ")); |
| assertEquals(4, calcMSM(5, " 3 < -25% 10 < -3")); |
| assertEquals(5, calcMSM(6, "3<-25% 10<-3")); |
| assertEquals(6, calcMSM(7, " 3 < -25% 10 < -3 ")); |
| assertEquals(6, calcMSM(8, " 3 < -25% 10 \n < -3\n")); |
| assertEquals(7, calcMSM(9, " 3 < -25% 10 < -3 \n")); |
| assertEquals(8, calcMSM(10, " 3 < -25% 10 < -3")); |
| assertEquals(8, calcMSM(11, "3<-25% 10<-3")); |
| assertEquals(9, calcMSM(12, "3<-25% 10<-3")); |
| assertEquals(97, calcMSM(100, "3<-25% 10<-3")); |
| |
| BooleanQuery.Builder q = new BooleanQuery.Builder(); |
| q.add(new TermQuery(new Term("a","b")), Occur.SHOULD); |
| q.add(new TermQuery(new Term("a","c")), Occur.SHOULD); |
| q.add(new TermQuery(new Term("a","d")), Occur.SHOULD); |
| q.add(new TermQuery(new Term("a","d")), Occur.SHOULD); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "0"); |
| assertEquals(0, q.build().getMinimumNumberShouldMatch()); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "1"); |
| assertEquals(1, q.build().getMinimumNumberShouldMatch()); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "50%"); |
| assertEquals(2, q.build().getMinimumNumberShouldMatch()); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "99"); |
| assertEquals(4, q.build().getMinimumNumberShouldMatch()); |
| |
| q.add(new TermQuery(new Term("a","e")), Occur.MUST); |
| q.add(new TermQuery(new Term("a","f")), Occur.MUST); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "50%"); |
| assertEquals(2, q.build().getMinimumNumberShouldMatch()); |
| |
| } |
| |
| @Test |
| public void testMinShouldMatchBadQueries() { |
| Exception e = expectThrows(SolrException.class, () -> calcMSM(2, "1<")); |
| assertEquals("Invalid 'mm' spec: '1<'. Expecting values before and after '<'" , e.getMessage()); |
| e = expectThrows(SolrException.class, () -> calcMSM(2, "1<x")); |
| assertEquals("Invalid 'mm' spec. Expecting an integer.", e.getMessage()); |
| e = expectThrows(SolrException.class, () -> calcMSM(1, "x%")); |
| assertEquals("Invalid 'mm' spec. Expecting an integer.", e.getMessage()); |
| e = expectThrows(SolrException.class, () -> calcMSM(1, "%%")); |
| assertEquals("Invalid 'mm' spec. Expecting an integer.", e.getMessage()); |
| e = expectThrows(SolrException.class, () -> calcMSM(1, "x")); |
| assertEquals("Invalid 'mm' spec. Expecting an integer.", e.getMessage()); |
| |
| e = expectThrows(SolrException.class, () -> calcMSM(10, "2<-25% 9<X")); |
| assertEquals("Invalid 'mm' spec. Expecting an integer." , e.getMessage()); |
| e = expectThrows(SolrException.class, () -> calcMSM(10, "2<-25% 9<")); |
| assertEquals("Invalid 'mm' spec: '9<'. Expecting values before and after '<'" , e.getMessage()); |
| } |
| |
| @Test |
| public void testMinShouldMatchAutoRelax() { |
| /* The basics should not be affected by autoRelax */ |
| BooleanQuery.Builder q = new BooleanQuery.Builder(); |
| q.add(new TermQuery(new Term("a","b")), Occur.SHOULD); |
| q.add(new TermQuery(new Term("a","c")), Occur.SHOULD); |
| q.add(new TermQuery(new Term("a","d")), Occur.SHOULD); |
| q.add(new TermQuery(new Term("a","d")), Occur.SHOULD); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "0", true); |
| assertEquals(0, q.build().getMinimumNumberShouldMatch()); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "1", true); |
| assertEquals(1, q.build().getMinimumNumberShouldMatch()); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "50%", true); |
| assertEquals(2, q.build().getMinimumNumberShouldMatch()); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "99", true); |
| assertEquals(4, q.build().getMinimumNumberShouldMatch()); |
| |
| q.add(new TermQuery(new Term("a","e")), Occur.MUST); |
| q.add(new TermQuery(new Term("a","f")), Occur.MUST); |
| |
| SolrPluginUtils.setMinShouldMatch(q, "50%", true); |
| assertEquals(2, q.build().getMinimumNumberShouldMatch()); |
| |
| /* Simulate stopwords through uneven disjuncts */ |
| q = new BooleanQuery.Builder(); |
| q.add(new DisjunctionMaxQuery(Collections.singleton(new TermQuery(new Term("a","foo"))), 0.0f), Occur.SHOULD); |
| DisjunctionMaxQuery dmq = new DisjunctionMaxQuery( |
| Arrays.asList( |
| new TermQuery(new Term("a","foo")), |
| new TermQuery(new Term("b","foo"))), |
| 0f); |
| q.add(dmq, Occur.SHOULD); |
| dmq = new DisjunctionMaxQuery( |
| Arrays.asList( |
| new TermQuery(new Term("a","bar")), |
| new TermQuery(new Term("b","bar"))), |
| 0f); |
| q.add(dmq, Occur.SHOULD); |
| |
| // Without relax |
| SolrPluginUtils.setMinShouldMatch(q, "100%", false); |
| assertEquals(3, q.build().getMinimumNumberShouldMatch()); |
| |
| // With relax |
| SolrPluginUtils.setMinShouldMatch(q, "100%", true); |
| assertEquals(2, q.build().getMinimumNumberShouldMatch()); |
| |
| // Still same result with a MUST clause extra |
| q.add(new TermQuery(new Term("a","must")), Occur.MUST); |
| SolrPluginUtils.setMinShouldMatch(q, "100%", true); |
| assertEquals(2, q.build().getMinimumNumberShouldMatch()); |
| |
| // Combination of dismax and non-dismax SHOULD clauses |
| q.add(new TermQuery(new Term("b","should")), Occur.SHOULD); |
| SolrPluginUtils.setMinShouldMatch(q, "100%", true); |
| assertEquals(3, q.build().getMinimumNumberShouldMatch()); |
| } |
| |
| private static class InvokeSettersTestClass { |
| private float aFloat = random().nextFloat(); |
| public float getAFloat() { |
| return aFloat; |
| } |
| public void setAFloat(float aFloat) { |
| this.aFloat = aFloat; |
| } |
| public void setAFloat(String aFloat) { |
| this.aFloat = Float.parseFloat(aFloat); |
| } |
| } |
| |
| @Test |
| public void testInvokeSetters() { |
| final Float theFloat = random().nextFloat(); |
| implTestInvokeSetters(theFloat, theFloat); |
| implTestInvokeSetters(theFloat, theFloat.toString()); |
| } |
| |
| public void implTestInvokeSetters(final Float theFloat, final Object theFloatObject) { |
| final InvokeSettersTestClass bean = new InvokeSettersTestClass(); |
| final Map<String,Object> initArgs = new HashMap<>(); |
| initArgs.put("aFloat", theFloatObject); |
| SolrPluginUtils.invokeSetters(bean, initArgs.entrySet()); |
| assertEquals(bean.getAFloat(), theFloat.floatValue(), 0.0); |
| } |
| |
| /** macro */ |
| public String pe(CharSequence s) { |
| return SolrPluginUtils.partialEscape(s).toString(); |
| } |
| |
| /** macro */ |
| public String strip(CharSequence s) { |
| return SolrPluginUtils.stripUnbalancedQuotes(s).toString(); |
| } |
| |
| /** macro */ |
| public String stripOp(CharSequence s) { |
| return SolrPluginUtils.stripIllegalOperators(s).toString(); |
| } |
| |
| /** macro */ |
| public int calcMSM(int clauses, String spec) { |
| return SolrPluginUtils.calculateMinShouldMatch(clauses, spec); |
| } |
| } |
| |