SOLR-236: refactor field collapsing, pull out of SolrIndexSearcher

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1030220 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Yonik Seeley 2010-11-02 21:09:10 +00:00
parent 5b27b08048
commit cb1bc14e2a
4 changed files with 302 additions and 280 deletions

View File

@ -300,8 +300,8 @@ public class QueryComponent extends SearchComponent
boolean doGroup = params.getBool(GroupParams.GROUP, false);
if (doGroup) {
try {
cmd.groupCommands = new ArrayList<Grouping.Command>();
Grouping grouping = new Grouping(searcher, result, cmd);
String[] fields = params.getParams(GroupParams.GROUP_FIELD);
String[] funcs = params.getParams(GroupParams.GROUP_FUNC);
String[] queries = params.getParams(GroupParams.GROUP_QUERY);
@ -330,7 +330,7 @@ public class QueryComponent extends SearchComponent
for (String groupByStr : funcs) {
QParser parser = QParser.getParser(groupByStr, "func", rb.req);
Query q = parser.getQuery();
Grouping.CommandFunc gc = new Grouping.CommandFunc();
Grouping.CommandFunc gc = grouping.new CommandFunc();
gc.groupSort = groupSort;
if (q instanceof FunctionQuery) {
@ -343,8 +343,9 @@ public class QueryComponent extends SearchComponent
gc.docsPerGroup = docsPerGroupDefault;
gc.groupOffset = groupOffsetDefault;
gc.offset = cmd.getOffset();
gc.sort = cmd.getSort();
cmd.groupCommands.add(gc);
grouping.add(gc);
}
}
@ -352,7 +353,7 @@ public class QueryComponent extends SearchComponent
for (String groupByStr : queries) {
QParser parser = QParser.getParser(groupByStr, null, rb.req);
Query gq = parser.getQuery();
Grouping.CommandQuery gc = new Grouping.CommandQuery();
Grouping.CommandQuery gc = grouping.new CommandQuery();
gc.query = gq;
gc.groupSort = groupSort;
gc.key = groupByStr;
@ -360,26 +361,23 @@ public class QueryComponent extends SearchComponent
gc.docsPerGroup = docsPerGroupDefault;
gc.groupOffset = groupOffsetDefault;
cmd.groupCommands.add(gc);
grouping.add(gc);
}
}
if (cmd.groupCommands.size() == 0)
cmd.groupCommands = null;
if (cmd.groupCommands != null) {
if (rb.doHighlights || rb.isDebug()) {
// we need a single list of the returned docs
cmd.setFlags(SolrIndexSearcher.GET_DOCLIST);
}
searcher.search(result,cmd);
rb.setResult( result );
rsp.add("grouped", result.groupedResults);
// TODO: get "hits" a different way to log
return;
if (rb.doHighlights || rb.isDebug()) {
// we need a single list of the returned docs
cmd.setFlags(SolrIndexSearcher.GET_DOCLIST);
}
// searcher.search(result,cmd);
grouping.execute();
rb.setResult( result );
rsp.add("grouped", result.groupedResults);
// TODO: get "hits" a different way to log
return;
} catch (ParseException e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
}

View File

@ -19,6 +19,8 @@ package org.apache.solr.search;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.*;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.search.function.DocValues;
import org.apache.solr.search.function.ValueSource;
@ -27,27 +29,286 @@ import java.util.*;
public class Grouping {
public static class Command {
public String key; // the name to use for this group in the response
public Sort groupSort; // the sort of the documents *within* a single group.
public abstract class Command {
public String key; // the name to use for this group in the response
public Sort groupSort; // the sort of the documents *within* a single group.
public Sort sort; // the sort between groups
public int docsPerGroup; // how many docs in each group - from "group.limit" param, default=1
public int groupOffset; // the offset within each group (for paging within each group)
public int numGroups; // how many groups - defaults to the "rows" parameter
public int offset; // offset into the list of groups
public int groupOffset; // the offset within each group (for paging within each group)
public int numGroups; // how many groups - defaults to the "rows" parameter
public int offset; // offset into the list of groups
abstract void prepare() throws IOException;
abstract Collector createCollector() throws IOException;
Collector createNextCollector() throws IOException {
return null;
}
abstract void finish() throws IOException;
abstract int getMatches();
NamedList commonResponse() {
NamedList groupResult = new SimpleOrderedMap();
grouped.add(key, groupResult); // grouped={ key={
int this_matches = getMatches();
groupResult.add("matches", this_matches);
maxMatches = Math.max(maxMatches, this_matches);
return groupResult;
}
DocList getDocList(TopDocsCollector collector) {
int docsToCollect = getMax(groupOffset, docsPerGroup, maxDoc);
// TODO: implement a DocList impl that doesn't need to start at offset=0
TopDocs topDocs = collector.topDocs(0, docsToCollect);
int ids[] = new int[topDocs.scoreDocs.length];
float[] scores = needScores ? new float[topDocs.scoreDocs.length] : null;
for (int i=0; i<ids.length; i++) {
ids[i] = topDocs.scoreDocs[i].doc;
if (scores != null)
scores[i] = topDocs.scoreDocs[i].score;
}
float score = topDocs.getMaxScore();
maxScore = Math.max(maxScore, score);
DocSlice docs = new DocSlice(groupOffset, Math.max(0, ids.length - groupOffset), ids, scores, topDocs.totalHits, score);
if (getDocList) {
DocIterator iter = docs.iterator();
while (iter.hasNext())
idSet.add(iter.nextDoc());
}
return docs;
}
void addDocList(NamedList rsp, TopDocsCollector collector) {
rsp.add("doclist", getDocList(collector));
}
}
public static class CommandQuery extends Command {
public class CommandQuery extends Command {
public Query query;
TopDocsCollector topCollector;
FilterCollector collector;
@Override
void prepare() throws IOException {
}
@Override
Collector createCollector() throws IOException {
int docsToCollect = getMax(groupOffset, docsPerGroup, maxDoc);
DocSet groupFilt = searcher.getDocSet(query);
topCollector = newCollector(groupSort, docsToCollect, false, needScores);
collector = new FilterCollector(groupFilt, topCollector);
return collector;
}
@Override
void finish() throws IOException {
NamedList rsp = commonResponse();
addDocList(rsp, (TopDocsCollector)collector.getCollector());
}
@Override
int getMatches() {
return collector.getMatches();
}
}
public static class CommandFunc extends Command {
public class CommandFunc extends Command {
public ValueSource groupBy;
// todo - find a better place to store these
transient Map context;
transient Collector collector;
int maxGroupToFind;
Map context;
TopGroupCollector collector = null;
Phase2GroupCollector collector2;
@Override
void prepare() throws IOException {
Map context = ValueSource.newContext();
groupBy.createWeight(context, searcher);
}
@Override
Collector createCollector() throws IOException {
maxGroupToFind = getMax(offset, numGroups, maxDoc);
if (compareSorts(sort, groupSort)) {
collector = new TopGroupSortCollector(groupBy, context, normalizeSort(sort), normalizeSort(groupSort), maxGroupToFind);
} else {
collector = new TopGroupCollector(groupBy, context, normalizeSort(sort), maxGroupToFind);
}
return collector;
}
@Override
Collector createNextCollector() throws IOException {
int docsToCollect = getMax(groupOffset, docsPerGroup, maxDoc);
if (docsToCollect < 0 || docsToCollect > maxDoc) docsToCollect = maxDoc;
collector2 = new Phase2GroupCollector(collector, groupBy, context, groupSort, docsToCollect, needScores, offset);
return collector2;
}
@Override
void finish() throws IOException {
NamedList groupResult = commonResponse();
if (collector.orderedGroups == null) collector.buildSet();
List groupList = new ArrayList();
groupResult.add("groups", groupList); // grouped={ key={ groups=[
int skipCount = offset;
for (SearchGroup group : collector.orderedGroups) {
if (skipCount > 0) {
skipCount--;
continue;
}
NamedList nl = new SimpleOrderedMap();
groupList.add(nl); // grouped={ key={ groups=[ {
nl.add("groupValue", group.groupValue.toObject());
SearchGroupDocs groupDocs = collector2.groupMap.get(group.groupValue);
addDocList(nl, groupDocs.collector);
}
}
@Override
int getMatches() {
return collector.getMatches();
}
}
static Sort byScoreDesc = new Sort();
static boolean compareSorts(Sort sort1, Sort sort2) {
return sort1 == sort2 || normalizeSort(sort1).equals(normalizeSort(sort2));
}
/** returns a sort by score desc if null */
static Sort normalizeSort(Sort sort) {
return sort==null ? byScoreDesc : sort;
}
static int getMax(int offset, int len, int max) {
int v = len<0 ? max : offset + len;
if (v < 0 || v > max) v = max;
return v;
}
static TopDocsCollector newCollector(Sort sort, int numHits, boolean fillFields, boolean needScores) throws IOException {
if (sort==null || sort==byScoreDesc) {
return TopScoreDocCollector.create(numHits, true);
} else {
return TopFieldCollector.create(sort, numHits, false, needScores, needScores, true);
}
}
final SolrIndexSearcher searcher;
final SolrIndexSearcher.QueryResult qr;
final SolrIndexSearcher.QueryCommand cmd;
final List<Command> commands = new ArrayList<Command>();
public Grouping(SolrIndexSearcher searcher, SolrIndexSearcher.QueryResult qr, SolrIndexSearcher.QueryCommand cmd) {
this.searcher = searcher;
this.qr = qr;
this.cmd = cmd;
}
public void add(Grouping.Command groupingCommand) {
commands.add(groupingCommand);
}
int maxDoc;
boolean needScores;
boolean getDocSet;
boolean getDocList; // doclist needed for debugging or highlighting
Query query;
DocSet filter;
Filter luceneFilter;
NamedList grouped = new SimpleOrderedMap();
Set<Integer> idSet = new LinkedHashSet<Integer>(); // used for tracking unique docs when we need a doclist
int maxMatches; // max number of matches from any grouping command
float maxScore = Float.NEGATIVE_INFINITY; // max score seen in any doclist
public void execute() throws IOException {
DocListAndSet out = new DocListAndSet();
qr.setDocListAndSet(out);
filter = cmd.getFilter()!=null ? cmd.getFilter() : searcher.getDocSet(cmd.getFilterList());
maxDoc = searcher.maxDoc();
needScores = (cmd.getFlags() & SolrIndexSearcher.GET_SCORES) != 0;
getDocSet = (cmd.getFlags() & SolrIndexSearcher.GET_DOCSET) != 0;
getDocList = (cmd.getFlags() & SolrIndexSearcher.GET_DOCLIST) != 0; // doclist needed for debugging or highlighting
query = QueryUtils.makeQueryable(cmd.getQuery());
for (Command cmd : commands) {
cmd.prepare();
}
List<Collector> collectors = new ArrayList<Collector>(commands.size());
for (Command cmd : commands) {
Collector collector = cmd.createCollector();
if (collector != null)
collectors.add(collector);
}
Collector allCollectors = MultiCollector.wrap(collectors.toArray(new Collector[collectors.size()]));
DocSetCollector setCollector = null;
if (getDocSet) {
setCollector = new DocSetDelegateCollector(maxDoc>>6, maxDoc, allCollectors);
allCollectors = setCollector;
}
searcher.search(query, luceneFilter, allCollectors);
if (getDocSet) {
qr.setDocSet(setCollector.getDocSet());
}
collectors.clear();
for (Command cmd : commands) {
Collector collector = cmd.createNextCollector();
if (collector != null)
collectors.add(collector);
}
if (collectors.size() > 0) {
searcher.search(query, luceneFilter, MultiCollector.wrap(collectors.toArray(new Collector[collectors.size()])));
}
for (Command cmd : commands) {
cmd.finish();
}
qr.groupedResults = grouped;
if (getDocList) {
int sz = idSet.size();
int[] ids = new int[sz];
int idx = 0;
for (int val : idSet) {
ids[idx++] = val;
}
qr.setDocList(new DocSlice(0, sz, ids, null, maxMatches, maxScore));
}
}
}
@ -82,11 +343,11 @@ abstract class GroupCollector extends Collector {
class FilterCollector extends GroupCollector {
private final DocSet filter;
private final TopFieldCollector collector;
private final Collector collector;
private int docBase;
private int matches;
public FilterCollector(DocSet filter, TopFieldCollector collector) throws IOException {
public FilterCollector(DocSet filter, Collector collector) throws IOException {
this.filter = filter;
this.collector = collector;
}
@ -119,7 +380,7 @@ class FilterCollector extends GroupCollector {
return matches;
}
TopFieldCollector getTopFieldCollector() {
Collector getCollector() {
return collector;
}
}
@ -527,7 +788,10 @@ class Phase2GroupCollector extends Collector {
}
SearchGroupDocs groupDocs = new SearchGroupDocs();
groupDocs.groupValue = group.groupValue;
groupDocs.collector = TopFieldCollector.create(sort, docsPerGroup, getSortFields, getScores, getScores, true);
if (sort==null)
groupDocs.collector = TopScoreDocCollector.create(docsPerGroup, true);
else
groupDocs.collector = TopFieldCollector.create(sort, docsPerGroup, getSortFields, getScores, getScores, true);
groupMap.put(groupDocs.groupValue, groupDocs);
}
@ -571,6 +835,6 @@ class Phase2GroupCollector extends Collector {
// disad: blows up the size of SearchGroup if we need many of them, and couples implementations
class SearchGroupDocs {
public MutableValue groupValue;
TopFieldCollector collector;
TopDocsCollector collector;
}

View File

@ -983,238 +983,19 @@ public class SolrIndexSearcher extends IndexSearcher implements SolrInfoMBean {
return qr.getDocList();
}
private static final int NO_CHECK_QCACHE = 0x80000000;
private static final int GET_DOCSET = 0x40000000;
private static final int NO_CHECK_FILTERCACHE = 0x20000000;
static final int NO_CHECK_QCACHE = 0x80000000;
static final int GET_DOCSET = 0x40000000;
static final int NO_CHECK_FILTERCACHE = 0x20000000;
public static final int GET_DOCLIST = 0x02; // get the documents actually returned in a response
public static final int GET_SCORES = 0x01;
private void groupBy(QueryResult qr, QueryCommand cmd) throws IOException {
DocListAndSet out = new DocListAndSet();
qr.setDocListAndSet(out);
DocSet filter = cmd.getFilter()!=null ? cmd.getFilter() : getDocSet(cmd.getFilterList());
int maxDoc = maxDoc();
boolean needScores = (cmd.getFlags() & GET_SCORES) != 0;
boolean getDocSet = (cmd.getFlags() & GET_DOCSET) != 0;
boolean getDocList = (cmd.getFlags() & GET_DOCLIST) != 0; // doclist needed for debugging or highlighting
Query query = QueryUtils.makeQueryable(cmd.getQuery());
final Filter luceneFilter = filter==null ? null : filter.getTopFilter();
Sort sort = cmd.getSort();
if (sort == null) sort = new Sort();
List<GroupCollector> collectors = new ArrayList<GroupCollector>(cmd.groupCommands.size());
for (Grouping.Command groupCommand : cmd.groupCommands) {
// TODO: perhaps use some methods rather than instanceof
if (groupCommand instanceof Grouping.CommandFunc) {
Grouping.CommandFunc gc = (Grouping.CommandFunc)groupCommand;
Map context = ValueSource.newContext();
gc.groupBy.createWeight(context, this);
TopGroupCollector collector;
int groupsToCollect = gc.numGroups<0 ? maxDoc : gc.offset + gc.numGroups;
if (groupsToCollect < 0 || groupsToCollect > maxDoc) groupsToCollect = maxDoc;
if (gc.groupSort != null && gc.groupSort != sort) {
collector = new TopGroupSortCollector(gc.groupBy, context, sort, gc.groupSort, groupsToCollect);
} else {
collector = new TopGroupCollector(gc.groupBy, context, sort, groupsToCollect);
}
collectors.add(collector);
// for next phase
gc.context = context;
gc.collector = collector;
}
if (groupCommand instanceof Grouping.CommandQuery) {
int docsToCollect = groupCommand.docsPerGroup<0 ? maxDoc : groupCommand.groupOffset + groupCommand.docsPerGroup;
if (docsToCollect < 0 || docsToCollect > maxDoc) docsToCollect = maxDoc;
DocSet groupFilt = getDocSet(((Grouping.CommandQuery)groupCommand).query);
TopFieldCollector collector = TopFieldCollector.create(groupCommand.groupSort==null ? sort : groupCommand.groupSort, docsToCollect, false, needScores, needScores, true);
collectors.add(new FilterCollector(groupFilt, collector));
}
}
Collector allCollectors = MultiCollector.wrap(collectors.toArray(new Collector[collectors.size()]));
DocSetCollector setCollector = null;
if (getDocSet) {
// TODO: can callCollectors be zero length?
setCollector = new DocSetDelegateCollector(maxDoc()>>6, maxDoc(), allCollectors);
allCollectors = setCollector;
}
search(query, luceneFilter, allCollectors);
if (getDocSet) {
qr.docListAndSet.docSet = setCollector.getDocSet();
}
// TODO: make this a generic collector list
int numPhase2 = 0;
List<Phase2GroupCollector> phase2Collectors = new ArrayList<Phase2GroupCollector>(cmd.groupCommands.size());
for (Grouping.Command groupCommand : cmd.groupCommands) {
if (groupCommand instanceof Grouping.CommandFunc) {
Grouping.CommandFunc gc = (Grouping.CommandFunc)groupCommand;
Sort collectorSort = gc.groupSort == null ? sort : gc.groupSort;
int docsToCollect = groupCommand.docsPerGroup<0 ? maxDoc : groupCommand.groupOffset + groupCommand.docsPerGroup;
if (docsToCollect < 0 || docsToCollect > maxDoc) docsToCollect = maxDoc;
Phase2GroupCollector collector = new Phase2GroupCollector((TopGroupCollector)gc.collector, gc.groupBy, gc.context, collectorSort, docsToCollect, needScores, groupCommand.offset);
phase2Collectors.add(collector);
numPhase2++;
} else if (groupCommand instanceof Grouping.CommandQuery) {
phase2Collectors.add(null);
} else {
phase2Collectors.add(null);
}
}
// TODO: optionally cache docs and feed them back through rather than re-searching
if (numPhase2 > 0)
search(query, luceneFilter, MultiCollector.wrap(phase2Collectors.toArray(new Collector[phase2Collectors.size()])));
Set<Integer> idSet = new LinkedHashSet<Integer>(); // used for tracking unique docs when we need a doclist
int maxMatches = 0;
float maxScore = Float.NEGATIVE_INFINITY;
NamedList grouped = new SimpleOrderedMap();
for (int cmdnum=0; cmdnum<cmd.groupCommands.size(); cmdnum++) {
Grouping.Command groupCommand = cmd.groupCommands.get(cmdnum);
GroupCollector gcollector = collectors.get(cmdnum);
NamedList groupResult = new SimpleOrderedMap();
grouped.add(groupCommand.key, groupResult); // grouped={ key={
int this_matches = gcollector.getMatches();
groupResult.add("matches", this_matches);
maxMatches = Math.max(maxMatches, this_matches);
// TODO: refactor this
if (groupCommand instanceof Grouping.CommandQuery) {
int docsToCollect = groupCommand.docsPerGroup<0 ? maxDoc : groupCommand.groupOffset + groupCommand.docsPerGroup;
if (docsToCollect < 0 || docsToCollect > maxDoc) docsToCollect = maxDoc;
TopDocs topDocs = ((FilterCollector)gcollector).getTopFieldCollector().topDocs(0, docsToCollect);
// TODO: refactor
//topDocs.totalHits
int ids[] = new int[topDocs.scoreDocs.length];
float[] scores = needScores ? new float[topDocs.scoreDocs.length] : null;
for (int i=0; i<ids.length; i++) {
ids[i] = topDocs.scoreDocs[i].doc;
if (scores != null)
scores[i] = topDocs.scoreDocs[i].score;
}
float score = topDocs.getMaxScore();
maxScore = Math.max(maxScore, score);
DocSlice docs = new DocSlice(groupCommand.groupOffset, Math.max(0, ids.length - groupCommand.groupOffset), ids, scores, topDocs.totalHits, score);
groupResult.add("doclist", docs);
if (getDocList) {
DocIterator iter = docs.iterator();
while (iter.hasNext())
idSet.add(iter.nextDoc());
}
continue;
}
Grouping.CommandFunc groupCommandFunc = (Grouping.CommandFunc)groupCommand;
TopGroupCollector collector = (TopGroupCollector)gcollector;
Phase2GroupCollector collector2 = phase2Collectors.get(cmdnum);
if (collector.orderedGroups == null) collector.buildSet();
List groupList = new ArrayList();
groupResult.add("groups", groupList); // grouped={ key={ groups=[
int skipCount = groupCommand.offset;
for (SearchGroup group : collector.orderedGroups) {
if (skipCount > 0) {
skipCount--;
continue;
}
NamedList nl = new SimpleOrderedMap();
groupList.add(nl); // grouped={ key={ groups=[ {
nl.add("groupValue", group.groupValue.toObject());
SearchGroupDocs groupDocs = collector2.groupMap.get(group.groupValue);
// nl.add("matches", groupDocs.matches); // redundant with doclist.numFound from the doc list
int docsToCollect = groupCommand.docsPerGroup<0 ? maxDoc : groupCommand.groupOffset + groupCommand.docsPerGroup;
if (docsToCollect < 0 || docsToCollect > maxDoc) docsToCollect = maxDoc;
TopDocs topDocs = groupDocs.collector.topDocs(0, docsToCollect);
//topDocs.totalHits
int ids[] = new int[topDocs.scoreDocs.length];
float[] scores = needScores ? new float[topDocs.scoreDocs.length] : null;
for (int i=0; i<ids.length; i++) {
ids[i] = topDocs.scoreDocs[i].doc;
if (scores != null)
scores[i] = topDocs.scoreDocs[i].score;
}
float score = topDocs.getMaxScore();
maxScore = Math.max(maxScore, score);
DocSlice docs = new DocSlice(groupCommand.groupOffset, Math.max(0, ids.length - groupCommand.groupOffset), ids, scores, topDocs.totalHits, score);
nl.add("doclist", docs);
if (getDocList) {
DocIterator iter = docs.iterator();
while (iter.hasNext())
idSet.add(iter.nextDoc());
}
/*** values from stage 1
DocSlice docs = new DocSlice(0, 1, new int[] {group.topDoc}, null, 1, 0);
nl.add("docs", docs);
Object[] vals = new Object[collector.comparators.length];
for (int i=0; i<vals.length; i++) {
vals[i] = collector.comparators[i].value(group.comparatorSlot);
}
nl.add("groupSortValue", vals);
groupResult.add(nl);
***/
}
}
qr.groupedResults = grouped;
if (getDocList) {
int sz = idSet.size();
int[] ids = new int[sz];
int idx = 0;
for (int val : idSet) {
ids[idx++] = val;
}
qr.docListAndSet.docList = new DocSlice(0, sz, ids, null, maxMatches, maxScore);
}
}
/**
* getDocList version that uses+populates query and filter caches.
* In the event of a timeout, the cache is not populated.
*/
private void getDocListC(QueryResult qr, QueryCommand cmd) throws IOException {
if (cmd.groupCommands != null) {
groupBy(qr, cmd);
return;
}
DocListAndSet out = new DocListAndSet();
qr.setDocListAndSet(out);
QueryResultKey key=null;
@ -2025,7 +1806,7 @@ public class SolrIndexSearcher extends IndexSearcher implements SolrInfoMBean {
private int flags;
private long timeAllowed = -1;
public List<Grouping.Command> groupCommands;
// public List<Grouping.Command> groupCommands;
public Query getQuery() { return query; }
public QueryCommand setQuery(Query query) {

View File

@ -105,27 +105,6 @@ public class TestGroupingSearch extends SolrTestCaseJ4 {
);
}
@Test
public void testGroupingGroupSortingName() {
assertU(add(doc("id", "1","name", "author1", "title", "a book title")));
assertU(add(doc("id", "2","name", "author1", "title", "the title")));
assertU(add(doc("id", "3","name", "author2", "title", "book title")));
assertU(add(doc("id", "4","name", "author2", "title", "the title")));
assertU(commit());
assertQ(req("q","title:title", "group", "true", "group.field","name", "group.sort", "title asc")
,"*[count(//arr[@name='groups']/lst) = 2]"
,"//arr[@name='groups']/lst[1]/str[@name='groupValue'][.='author2']"
// ,"//arr[@name='groups']/lst[1]/int[@name='matches'][.='2']"
,"//arr[@name='groups']/lst[1]/result[@numFound='2']"
,"//arr[@name='groups']/lst[1]/result/doc/*[@name='id'][.='3']"
,"//arr[@name='groups']/lst[2]/str[@name='groupValue'][.='author1']"
// ,"//arr[@name='groups']/lst[2]/int[@name='matches'][.='2']"
,"//arr[@name='groups']/lst[2]/result[@numFound='2']"
,"//arr[@name='groups']/lst[2]/result/doc/*[@name='id'][.='1']"
);
}
@Test
public void testGroupingGroupSortingWeight() {