You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by cp...@apache.org on 2017/01/31 19:01:18 UTC

lucene-solr:branch_6x: SOLR-9933: SolrCoreParser now supports configuration of custom SpanQueryBuilder classes. (Daniel Collins, Christine Poerschke)

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_6x 6a3d7bf37 -> c99b39fca


SOLR-9933: SolrCoreParser now supports configuration of custom SpanQueryBuilder classes. (Daniel Collins, Christine Poerschke)


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/c99b39fc
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/c99b39fc
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/c99b39fc

Branch: refs/heads/branch_6x
Commit: c99b39fca1551ce079ccea451ecec5dfc296bd0f
Parents: 6a3d7bf
Author: Christine Poerschke <cp...@apache.org>
Authored: Tue Jan 31 09:59:44 2017 +0000
Committer: Christine Poerschke <cp...@apache.org>
Committed: Tue Jan 31 18:22:39 2017 +0000

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   3 +
 .../org/apache/solr/search/SolrCoreParser.java  |  43 ++++++--
 .../solr/search/SolrSpanQueryBuilder.java       |  33 ++++++
 .../ApacheLuceneSolrNearQueryBuilder.java       |  12 ++-
 .../solr/search/ChooseOneWordQueryBuilder.java  |  62 +++++++++++
 .../apache/solr/search/HandyQueryBuilder.java   |  25 ++++-
 .../apache/solr/search/TestSolrCoreParser.java  | 108 +++++++++++++++++++
 7 files changed, 270 insertions(+), 16 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c99b39fc/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 2a3253b..c3b6e22 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -61,6 +61,9 @@ Optimizations
 * SOLR-9764: All filters that which all documents in the index now share the same memory (DocSet).
   (Michael Sun, yonik)
 
+* SOLR-9933: SolrCoreParser now supports configuration of custom SpanQueryBuilder classes.
+  (Daniel Collins, Christine Poerschke)
+
 Bug Fixes
 ----------------------
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c99b39fc/solr/core/src/java/org/apache/solr/search/SolrCoreParser.java
----------------------------------------------------------------------
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 4857b75..5619d87 100755
--- a/solr/core/src/java/org/apache/solr/search/SolrCoreParser.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrCoreParser.java
@@ -16,16 +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 <code>search</code> module
@@ -33,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 +64,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.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.queryFactory.addBuilder(queryName, queryBuilder);
+        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);
+        }
+      }
     }
   }
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c99b39fc/solr/core/src/java/org/apache/solr/search/SolrSpanQueryBuilder.java
----------------------------------------------------------------------
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 0000000..2dea85c
--- /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;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c99b39fc/solr/core/src/test/org/apache/solr/search/ApacheLuceneSolrNearQueryBuilder.java
----------------------------------------------------------------------
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 bbc081a..574a736 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")),

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c99b39fc/solr/core/src/test/org/apache/solr/search/ChooseOneWordQueryBuilder.java
----------------------------------------------------------------------
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 0000000..6e2112e
--- /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));
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c99b39fc/solr/core/src/test/org/apache/solr/search/HandyQueryBuilder.java
----------------------------------------------------------------------
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 c38fb6b..f76015f 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);
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c99b39fc/solr/core/src/test/org/apache/solr/search/TestSolrCoreParser.java
----------------------------------------------------------------------
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 3ef96c3..79740e6 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("<ApacheLuceneSolr fieldName='"+fieldName+"'/>");
+    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 = "<HelloQuery/>";
     final String rhsXml = "<GoodbyeQuery/>";
@@ -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 = "<SpanOr fieldName='contents'>"
+        + "<SpanTerm>rain</SpanTerm>"
+        + "<SpanTerm>spain</SpanTerm>"
+        + "<SpanTerm>plain</SpanTerm>"
+        + "</SpanOr>";
+    final String rhsXml = "<SpanNear fieldName='contents' slop='2' inOrder='true'>"
+        + "<SpanTerm>sunny</SpanTerm>"
+        + "<SpanTerm>sky</SpanTerm>"
+        + "</SpanNear>";
+    final Query query = parseHandyQuery(lhsXml, rhsXml);
+    final BooleanQuery bq = (BooleanQuery)query;
+    assertEquals(2, bq.clauses().size());
+    for (int ii=0; ii<bq.clauses().size(); ++ii) {
+      final Query clauseQuery = bq.clauses().get(ii).getQuery();
+      switch (ii) {
+        case 0:
+          assertTrue(unwrapSpanBoostQuery(clauseQuery) instanceof SpanOrQuery);
+          break;
+        case 1:
+          assertTrue(unwrapSpanBoostQuery(clauseQuery) instanceof SpanNearQuery);
+          break;
+        default:
+          fail("unexpected clause index "+ii);
+      }
+    }
+  }
+
+  private static String composeChooseOneWordQueryXml(String fieldName, String... termTexts) {
+    final StringBuilder sb = new StringBuilder("<ChooseOneWord fieldName='"+fieldName+"'>");
+    for (String termText : termTexts) {
+      sb.append("<Word>").append(termText).append("</Word>");
+    }
+    sb.append("</ChooseOneWord>");
+    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 = "<ApacheLuceneSolr fieldName='"+fieldName+"'/>";
+    // the wrapping query
+    final String parentQuery = (span ? "SpanOr" : "BooleanQuery");
+    final String subQueryPrefix = (span ? "" : "<Clause occurs='must'>");
+    final String subQuerySuffix = (span ? "" : "</Clause>");
+    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);
+  }
+
 }