You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by jb...@apache.org on 2014/03/03 16:45:59 UTC

svn commit: r1573589 - in /lucene/dev/trunk/solr: core/src/java/org/apache/solr/core/ core/src/java/org/apache/solr/handler/component/ core/src/java/org/apache/solr/search/ core/src/test/org/apache/solr/handler/component/ solrj/src/java/org/apache/solr...

Author: jbernste
Date: Mon Mar  3 15:45:59 2014
New Revision: 1573589

URL: http://svn.apache.org/r1573589
Log:
SOLR-5720: Add ExpandComponent to expand results collapsed by the CollapsingQParserPlugin

Added:
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java   (with props)
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/DistributedExpandComponentTest.java   (with props)
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/TestExpandComponent.java   (with props)
    lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/ExpandParams.java   (with props)
Modified:
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/core/SolrCore.java
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
    lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/core/SolrCore.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/core/SolrCore.java?rev=1573589&r1=1573588&r2=1573589&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/core/SolrCore.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/core/SolrCore.java Mon Mar  3 15:45:59 2014
@@ -84,6 +84,7 @@ import org.apache.solr.handler.component
 import org.apache.solr.handler.component.RealTimeGetComponent;
 import org.apache.solr.handler.component.SearchComponent;
 import org.apache.solr.handler.component.StatsComponent;
+import org.apache.solr.handler.component.ExpandComponent;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.response.BinaryResponseWriter;
@@ -1237,6 +1238,8 @@ public final class SolrCore implements S
     addIfNotPresent(components,DebugComponent.COMPONENT_NAME,DebugComponent.class);
     addIfNotPresent(components,RealTimeGetComponent.COMPONENT_NAME,RealTimeGetComponent.class);
     addIfNotPresent(components,AnalyticsComponent.COMPONENT_NAME,AnalyticsComponent.class);
+    addIfNotPresent(components,ExpandComponent.COMPONENT_NAME,ExpandComponent.class);
+
     return components;
   }
   private <T> void addIfNotPresent(Map<String ,T> registry, String name, Class<? extends  T> c){

Added: lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java?rev=1573589&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java (added)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java Mon Mar  3 15:45:59 2014
@@ -0,0 +1,337 @@
+/*
+ * 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.handler.component;
+
+import org.apache.lucene.index.AtomicReader;
+import org.apache.lucene.index.AtomicReaderContext;
+import org.apache.lucene.index.SortedDocValues;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.FieldCache;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Collector;
+import org.apache.lucene.search.TopDocsCollector;
+import org.apache.lucene.search.TopFieldCollector;
+import org.apache.lucene.search.TopScoreDocCollector;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.CharsRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.search.CollapsingQParserPlugin;
+import org.apache.solr.search.DocIterator;
+import org.apache.solr.search.DocList;
+import org.apache.solr.search.QueryParsing;
+import org.apache.solr.schema.FieldType;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.params.ExpandParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.search.DocSlice;
+import org.apache.solr.search.SolrIndexSearcher;
+import org.apache.solr.util.plugin.PluginInfoInitialized;
+import org.apache.solr.util.plugin.SolrCoreAware;
+import org.apache.solr.core.PluginInfo;
+import org.apache.solr.core.SolrCore;
+
+import com.carrotsearch.hppc.IntObjectOpenHashMap;
+import com.carrotsearch.hppc.IntOpenHashSet;
+import com.carrotsearch.hppc.cursors.IntObjectCursor;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+  * The ExpandComponent is designed to work with the CollapsingPostFilter.
+  * The CollapsingPostFilter collapses a result set on a field.
+  * <p/>
+  * The ExpandComponent expands the collapsed groups for a single page.
+  * <p/>
+  * http parameters:
+  * <p/>
+  * expand=true <br/>
+  * expand.rows=5 </br>
+  * expand.sort=field asc|desc
+  *
+  **/
+    
+public class ExpandComponent extends SearchComponent implements PluginInfoInitialized, SolrCoreAware {
+  public static final String COMPONENT_NAME = "expand";
+  private PluginInfo info = PluginInfo.EMPTY_INFO;
+
+      @Override
+  public void init(PluginInfo info) {
+      this.info = info;
+  }
+
+      @Override
+  public void prepare(ResponseBuilder rb) throws IOException {
+    if (rb.req.getParams().getBool(ExpandParams.EXPAND,false)) {
+      rb.doExpand = true;
+    }
+  }
+      @Override
+  public void inform(SolrCore core) {
+
+  }
+
+        @Override
+  public void process(ResponseBuilder rb) throws IOException {
+
+    if(!rb.doExpand) {
+      return;
+    }
+
+    SolrQueryRequest req = rb.req;
+    SolrParams params = req.getParams();
+
+    boolean isShard = params.getBool(ShardParams.IS_SHARD, false);
+    String ids = params.get(ShardParams.IDS);
+
+    if(ids == null && isShard) {
+      return;
+    }
+
+    String field = null;
+    String sortParam = params.get(ExpandParams.EXPAND_SORT);
+    int limit = params.getInt(ExpandParams.EXPAND_ROWS, 5);
+
+    Sort sort = null;
+
+    if(sortParam != null) {
+      sort = QueryParsing.parseSortSpec(sortParam, rb.req).getSort();
+    }
+
+    Query query = rb.getQuery();
+    List<Query> filters = rb.getFilters();
+    List<Query> newFilters = new ArrayList();
+    for(Query q : filters) {
+      if(!(q instanceof CollapsingQParserPlugin.CollapsingPostFilter)) {
+        newFilters.add(q);
+      } else {
+        CollapsingQParserPlugin.CollapsingPostFilter cp = (CollapsingQParserPlugin.CollapsingPostFilter)q;
+        field = cp.getField();
+      }
+    }
+
+    if(field == null) {
+      throw new IOException("Expand field is null.");
+    }
+
+    SolrIndexSearcher searcher = req.getSearcher();
+    AtomicReader reader = searcher.getAtomicReader();
+    SortedDocValues values = FieldCache.DEFAULT.getTermsIndex(reader, field);
+    FixedBitSet groupBits = new FixedBitSet(values.getValueCount());
+    DocList docList = rb.getResults().docList;
+    IntOpenHashSet collapsedSet = new IntOpenHashSet(docList.size()*2);
+
+    DocIterator idit = docList.iterator();
+
+    while(idit.hasNext()) {
+      int doc = idit.nextDoc();
+      int ord = values.getOrd(doc);
+      if(ord > -1) {
+        groupBits.set(ord);
+        collapsedSet.add(doc);
+      }
+    }
+
+    Collector collector = null;
+    GroupExpandCollector groupExpandCollector = new GroupExpandCollector(values, groupBits, collapsedSet, limit, sort);
+    SolrIndexSearcher.ProcessedFilter pfilter = searcher.getProcessedFilter(null, newFilters);
+    if(pfilter.postFilter != null) {
+      pfilter.postFilter.setLastDelegate(groupExpandCollector);
+      collector = pfilter.postFilter;
+    } else {
+      collector = groupExpandCollector;
+    }
+
+    searcher.search(query, pfilter.filter, collector);
+    IntObjectOpenHashMap groups = groupExpandCollector.getGroups();
+    Iterator<IntObjectCursor> it = groups.iterator();
+    Map<String, DocSlice> outMap = new HashMap<>();
+    BytesRef bytesRef = new BytesRef();
+    CharsRef charsRef = new CharsRef();
+    FieldType fieldType = searcher.getSchema().getField(field).getType();
+
+    while(it.hasNext()) {
+      IntObjectCursor cursor = it.next();
+      int ord = cursor.key;
+      TopDocsCollector topDocsCollector = (TopDocsCollector)cursor.value;
+      TopDocs topDocs = topDocsCollector.topDocs();
+      ScoreDoc[] scoreDocs = topDocs.scoreDocs;
+      if(scoreDocs.length > 0) {
+        int[] docs = new int[scoreDocs.length];
+        float[] scores = new float[scoreDocs.length];
+        for(int i=0; i<docs.length; i++) {
+          ScoreDoc scoreDoc = scoreDocs[i];
+          docs[i] = scoreDoc.doc;
+          scores[i] = scoreDoc.score;
+        }
+        DocSlice slice = new DocSlice(0, docs.length, docs, scores, topDocs.totalHits, topDocs.getMaxScore());
+        values.lookupOrd(ord, bytesRef);
+        fieldType.indexedToReadable(bytesRef, charsRef);
+        String group = charsRef.toString();
+        outMap.put(group, slice);
+      }
+    }
+
+    rb.rsp.add("expanded", outMap);
+  }
+        @Override
+  public void modifyRequest(ResponseBuilder rb, SearchComponent who, ShardRequest sreq) {
+
+  }
+        @Override
+  public void handleResponses(ResponseBuilder rb, ShardRequest sreq) {
+
+    if(!rb.doExpand) {
+      return;
+    }
+
+    if ((sreq.purpose & ShardRequest.PURPOSE_GET_FIELDS) != 0) {
+      SolrQueryRequest req = rb.req;
+      Map expanded = (Map)req.getContext().get("expanded");
+      if(expanded == null) {
+        expanded = new HashMap();
+        req.getContext().put("expanded", expanded);
+      }
+
+      for (ShardResponse srsp : sreq.responses) {
+        NamedList response = srsp.getSolrResponse().getResponse();
+        Map ex = (Map)response.get("expanded");
+        Iterator<Map.Entry<String,SolrDocumentList>>it = ex.entrySet().iterator();
+        while(it.hasNext()) {
+          Map.Entry<String, SolrDocumentList> entry = it.next();
+          String name = entry.getKey();
+          SolrDocumentList val = entry.getValue();
+          expanded.put(name, val);
+        }
+      }
+    }
+  }
+        @Override
+  public void finishStage(ResponseBuilder rb) {
+
+    if(!rb.doExpand) {
+      return;
+    }
+
+    if (rb.stage != ResponseBuilder.STAGE_GET_FIELDS) {
+      return;
+    }
+
+    Map expanded = (Map)rb.req.getContext().get("expanded");
+    if(expanded == null) {
+      expanded = new HashMap();
+    }
+
+    rb.rsp.add("expanded", expanded);
+  }
+
+  private class GroupExpandCollector extends Collector {
+    private SortedDocValues docValues;
+    private IntObjectOpenHashMap groups;
+    private int docBase;
+    private FixedBitSet groupBits;
+    private IntOpenHashSet collapsedSet;
+    private List<Collector> collectors;
+
+    public GroupExpandCollector(SortedDocValues docValues, FixedBitSet groupBits, IntOpenHashSet collapsedSet, int limit, Sort sort) throws IOException {
+      int numGroups = collapsedSet.size();
+      groups = new IntObjectOpenHashMap(numGroups*2);
+      collectors = new ArrayList();
+      DocIdSetIterator iterator = groupBits.iterator();
+      int group = -1;
+      while((group = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
+        Collector collector = (sort == null) ? TopScoreDocCollector.create(limit, true) : TopFieldCollector.create(sort,limit, false, false,false, true);
+        groups.put(group, collector);
+        collectors.add(collector);
+      }
+
+      this.collapsedSet = collapsedSet;
+      this.groupBits = groupBits;
+      this.docValues = docValues;
+    }
+
+    public IntObjectOpenHashMap getGroups() {
+      return this.groups;
+    }
+
+    public boolean acceptsDocsOutOfOrder() {
+      return false;
+    }
+
+    public void collect(int docId) throws IOException {
+      int doc = docId+docBase;
+      int ord = docValues.getOrd(doc);
+      if(ord > -1 && groupBits.get(ord) && !collapsedSet.contains(doc)) {
+        Collector c = (Collector)groups.get(ord);
+        c.collect(docId);
+      }
+    }
+
+    public void setNextReader(AtomicReaderContext context) throws IOException {
+      this.docBase = context.docBase;
+      for(Collector c : collectors) {
+        c.setNextReader(context);
+      }
+    }
+
+    public void setScorer(Scorer scorer) throws IOException {
+      for(Collector c : collectors) {
+        c.setScorer(scorer);
+      }
+    }
+  }
+
+  ////////////////////////////////////////////
+  ///  SolrInfoMBean
+  ////////////////////////////////////////////
+
+    @Override
+  public String getDescription() {
+    return "Expand Component";
+  }
+
+  @Override
+  public String getSource() {
+    return "$URL: https://svn.apache.org/repos/asf/lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java $";
+  }
+
+  @Override
+  public URL[] getDocs() {
+    try {
+      return new URL[]{
+          new URL("http://wiki.apache.org/solr/ExpandComponent")
+      };
+    } catch (MalformedURLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
\ No newline at end of file

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java?rev=1573589&r1=1573588&r2=1573589&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java Mon Mar  3 15:45:59 2014
@@ -55,6 +55,7 @@ public class ResponseBuilder
   public SolrQueryResponse rsp;
   public boolean doHighlights;
   public boolean doFacets;
+  public boolean doExpand;
   public boolean doStats;
   public boolean doTerms;
 

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java?rev=1573589&r1=1573588&r2=1573589&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java Mon Mar  3 15:45:59 2014
@@ -72,6 +72,7 @@ public class SearchHandler extends Reque
     names.add( StatsComponent.COMPONENT_NAME );
     names.add( DebugComponent.COMPONENT_NAME );
     names.add( AnalyticsComponent.COMPONENT_NAME );
+    names.add( ExpandComponent.COMPONENT_NAME);
     return names;
   }
 

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java?rev=1573589&r1=1573588&r2=1573589&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java Mon Mar  3 15:45:59 2014
@@ -140,6 +140,11 @@ public class CollapsingQParserPlugin ext
     public static final int NULL_POLICY_COLLAPSE = 1;
     public static final int NULL_POLICY_EXPAND = 2;
 
+
+    public String getField(){
+      return this.field;
+    }
+
     public void setCache(boolean cache) {
 
     }

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/DistributedExpandComponentTest.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/DistributedExpandComponentTest.java?rev=1573589&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/DistributedExpandComponentTest.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/DistributedExpandComponentTest.java Mon Mar  3 15:45:59 2014
@@ -0,0 +1,189 @@
+package org.apache.solr.handler.component;
+
+/*
+ * 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.
+ */
+
+import org.apache.solr.BaseDistributedSearchTestCase;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.junit.BeforeClass;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Iterator;
+
+/**
+ * Test for QueryComponent's distributed querying
+ *
+ * @see org.apache.solr.handler.component.QueryComponent
+ */
+public class DistributedExpandComponentTest extends BaseDistributedSearchTestCase {
+
+  public DistributedExpandComponentTest() {
+    fixShardCount = true;
+    shardCount = 3;
+    stress = 0;
+  }
+
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception {
+    initCore("solrconfig-collapseqparser.xml", "schema11.xml");
+  }
+
+  @Override
+  public void doTest() throws Exception {
+    del("*:*");
+
+    index_specific(0,"id","1", "term_s", "YYYY", "group_s", "group1", "test_ti", "5",  "test_tl", "10", "test_tf", "2000");
+    index_specific(0,"id","2", "term_s", "YYYY", "group_s", "group1", "test_ti", "50", "test_tl", "100", "test_tf", "200");
+    index_specific(1,"id","5", "term_s", "YYYY", "group_s", "group2", "test_ti", "4",  "test_tl", "10", "test_tf", "2000");
+    index_specific(1,"id","6", "term_s", "YYYY", "group_s", "group2", "test_ti", "10", "test_tl", "100", "test_tf", "200");
+    index_specific(0,"id","7", "term_s", "YYYY", "group_s", "group1", "test_ti", "1",  "test_tl", "100000", "test_tf", "2000");
+    index_specific(1,"id","8", "term_s", "YYYY", "group_s", "group2", "test_ti", "2",  "test_tl", "100000", "test_tf", "200");
+    index_specific(2,"id","9", "term_s", "YYYY", "group_s", "group3", "test_ti", "1000", "test_tl", "1005", "test_tf", "3000");
+    index_specific(2, "id", "10", "term_s", "YYYY", "group_s", "group3", "test_ti", "1500", "test_tl", "1001", "test_tf", "3200");
+    index_specific(2,"id", "11",  "term_s", "YYYY", "group_s", "group3", "test_ti", "1300", "test_tl", "1002", "test_tf", "3300");
+    index_specific(1,"id","12", "term_s", "YYYY", "group_s", "group4", "test_ti", "15",  "test_tl", "10", "test_tf", "2000");
+    index_specific(1,"id","13", "term_s", "YYYY", "group_s", "group4", "test_ti", "16",  "test_tl", "9", "test_tf", "2000");
+    index_specific(1,"id","14", "term_s", "YYYY", "group_s", "group4", "test_ti", "1",  "test_tl", "20", "test_tf", "2000");
+
+
+    commit();
+
+
+    handle.put("explain", SKIPVAL);
+    handle.put("QTime", SKIPVAL);
+    handle.put("timestamp", SKIPVAL);
+    handle.put("score", SKIPVAL);
+    handle.put("wt", SKIP);
+    handle.put("distrib", SKIP);
+    handle.put("shards.qt", SKIP);
+    handle.put("shards", SKIP);
+    handle.put("q", SKIP);
+    handle.put("maxScore", SKIPVAL);
+    handle.put("_version_", SKIP);
+
+    query("q", "*:*", "fq", "{!collapse field=group_s}", "defType", "edismax", "bf", "field(test_ti)", "expand", "true", "fl","*,score");
+    query("q", "*:*", "fq", "{!collapse field=group_s}", "defType", "edismax", "bf", "field(test_ti)", "expand", "true", "expand.sort", "test_tl desc", "fl","*,score");
+    query("q", "*:*", "fq", "{!collapse field=group_s}", "defType", "edismax", "bf", "field(test_ti)", "expand", "true", "expand.sort", "test_tl desc", "expand.rows", "1", "fl","*,score");
+    //Test no expand results
+    query("q", "test_ti:5", "fq", "{!collapse field=group_s}", "defType", "edismax", "bf", "field(test_ti)", "expand", "true", "expand.sort", "test_tl desc", "expand.rows", "1", "fl","*,score");
+    //Test zero results
+    query("q", "test_ti:5434343", "fq", "{!collapse field=group_s}", "defType", "edismax", "bf", "field(test_ti)", "expand", "true", "expand.sort", "test_tl desc", "expand.rows", "1", "fl","*,score");
+
+    //First basic test case.
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.add("q", "*:*");
+    params.add("fq", "{!collapse field=group_s}");
+    params.add("defType", "edismax");
+    params.add("bf", "field(test_ti)");
+    params.add("expand", "true");
+
+    setDistributedParams(params);
+    QueryResponse rsp = queryServer(params);
+    Map<String, SolrDocumentList> results = rsp.getExpandedResults();
+    assertExpandGroups(results, "group1","group2", "group3", "group4");
+    assertExpandGroupCountAndOrder("group1", 2, results, "1.0", "7.0");
+    assertExpandGroupCountAndOrder("group2", 2, results, "5.0", "8.0");
+    assertExpandGroupCountAndOrder("group3", 2, results, "11.0", "9.0");
+    assertExpandGroupCountAndOrder("group4", 2, results, "12.0", "14.0");
+
+
+    //Test expand.sort
+
+    params = new ModifiableSolrParams();
+    params.add("q", "*:*");
+    params.add("fq", "{!collapse field=group_s}");
+    params.add("defType", "edismax");
+    params.add("bf", "field(test_ti)");
+    params.add("expand", "true");
+    params.add("expand.sort", "test_tl desc");
+    setDistributedParams(params);
+    rsp = queryServer(params);
+    results = rsp.getExpandedResults();
+    assertExpandGroups(results, "group1","group2", "group3", "group4");
+    assertExpandGroupCountAndOrder("group1", 2, results, "7.0", "1.0");
+    assertExpandGroupCountAndOrder("group2", 2, results, "8.0", "5.0");
+    assertExpandGroupCountAndOrder("group3", 2, results, "9.0", "11.0");
+    assertExpandGroupCountAndOrder("group4", 2, results, "14.0", "12.0");
+
+
+    //Test expand.rows
+
+    params = new ModifiableSolrParams();
+    params.add("q", "*:*");
+    params.add("fq", "{!collapse field=group_s}");
+    params.add("defType", "edismax");
+    params.add("bf", "field(test_ti)");
+    params.add("expand", "true");
+    params.add("expand.sort", "test_tl desc");
+    params.add("expand.rows", "1");
+    setDistributedParams(params);
+    rsp = queryServer(params);
+    results = rsp.getExpandedResults();
+    assertExpandGroups(results, "group1","group2", "group3", "group4");
+    assertExpandGroupCountAndOrder("group1", 1, results, "7.0");
+    assertExpandGroupCountAndOrder("group2", 1, results, "8.0");
+    assertExpandGroupCountAndOrder("group3", 1, results, "9.0");
+    assertExpandGroupCountAndOrder("group4", 1, results, "14.0");
+
+  }
+
+  private void assertExpandGroups(Map<String, SolrDocumentList> expandedResults, String... groups) throws Exception {
+    for(int i=0; i<groups.length; i++) {
+      if(!expandedResults.containsKey(groups[i])) {
+        throw new Exception("Expanded Group Not Found:"+groups[i]+", Found:"+exportGroups(expandedResults));
+      }
+    }
+  }
+
+  private String exportGroups(Map<String, SolrDocumentList> groups) {
+    StringBuilder buf = new StringBuilder();
+    Iterator<String> it = groups.keySet().iterator();
+    while(it.hasNext()) {
+      String group = it.next();
+      buf.append(group);
+      if(it.hasNext()) {
+        buf.append(",");
+      }
+    }
+    return buf.toString();
+  }
+
+  private void assertExpandGroupCountAndOrder(String group, int count, Map<String, SolrDocumentList>expandedResults, String... docs) throws Exception {
+    SolrDocumentList results = expandedResults.get(group);
+    if(results == null) {
+      throw new Exception("Group Not Found:"+group);
+    }
+
+    if(results.size() != count) {
+      throw new Exception("Expected Count "+results.size()+" Not Found:"+count);
+    }
+
+    for(int i=0; i<docs.length;i++) {
+      String id = docs[i];
+      SolrDocument doc = results.get(i);
+      if(!doc.getFieldValue("id").toString().equals(id)) {
+        throw new Exception("Id not in results or out of order:"+id+"!="+doc.getFieldValue("id"));
+      }
+    }
+  }
+}

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/TestExpandComponent.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/TestExpandComponent.java?rev=1573589&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/TestExpandComponent.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/component/TestExpandComponent.java Mon Mar  3 15:45:59 2014
@@ -0,0 +1,193 @@
+/*
+* 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.handler.component;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.*;
+
+public class TestExpandComponent extends SolrTestCaseJ4 {
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig-collapseqparser.xml", "schema11.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 testExpand() throws Exception {
+    String[] doc = {"id","1", "term_s", "YYYY", "group_s", "group1", "test_ti", "5", "test_tl", "10", "test_tf", "2000"};
+    assertU(adoc(doc));
+    assertU(commit());
+    String[] doc1 = {"id","2", "term_s","YYYY", "group_s", "group1", "test_ti", "50", "test_tl", "100", "test_tf", "200"};
+    assertU(adoc(doc1));
+
+    String[] doc2 = {"id","3", "term_s", "YYYY", "test_ti", "5000", "test_tl", "100", "test_tf", "200"};
+    assertU(adoc(doc2));
+    assertU(commit());
+    String[] doc3 = {"id","4", "term_s", "YYYY", "test_ti", "500", "test_tl", "1000", "test_tf", "2000"};
+    assertU(adoc(doc3));
+
+
+    String[] doc4 = {"id","5", "term_s", "YYYY", "group_s", "group2", "test_ti", "4", "test_tl", "10", "test_tf", "2000"};
+    assertU(adoc(doc4));
+    assertU(commit());
+    String[] doc5 = {"id","6", "term_s","YYYY", "group_s", "group2", "test_ti", "10", "test_tl", "100", "test_tf", "200"};
+    assertU(adoc(doc5));
+    assertU(commit());
+
+    String[] doc6 = {"id","7", "term_s", "YYYY", "group_s", "group1", "test_ti", "1", "test_tl", "100000", "test_tf", "2000"};
+    assertU(adoc(doc6));
+    assertU(commit());
+    String[] doc7 = {"id","8", "term_s","YYYY", "group_s", "group2", "test_ti", "2", "test_tl", "100000", "test_tf", "200"};
+    assertU(adoc(doc7));
+
+    assertU(commit());
+
+
+
+    //First basic test case.
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.add("q", "*:*");
+    params.add("fq", "{!collapse field=group_s}");
+    params.add("defType", "edismax");
+    params.add("bf", "field(test_ti)");
+    params.add("expand", "true");
+    assertQ(req(params), "*[count(/response/result/doc)=2]",
+        "*[count(/response/lst[@name='expanded']/result)=2]",
+        "/response/result/doc[1]/float[@name='id'][.='2.0']",
+        "/response/result/doc[2]/float[@name='id'][.='6.0']",
+        "/response/lst[@name='expanded']/result[@name='group1']/doc[1]/float[@name='id'][.='1.0']",
+        "/response/lst[@name='expanded']/result[@name='group1']/doc[2]/float[@name='id'][.='7.0']",
+        "/response/lst[@name='expanded']/result[@name='group2']/doc[1]/float[@name='id'][.='5.0']",
+        "/response/lst[@name='expanded']/result[@name='group2']/doc[2]/float[@name='id'][.='8.0']"
+    );
+
+    //Test expand.sort
+    params = new ModifiableSolrParams();
+    params.add("q", "*:*");
+    params.add("fq", "{!collapse field=group_s}");
+    params.add("defType", "edismax");
+    params.add("bf", "field(test_ti)");
+    params.add("expand", "true");
+    params.add("expand.sort", "test_tl desc");
+    assertQ(req(params), "*[count(/response/result/doc)=2]",
+        "*[count(/response/lst[@name='expanded']/result)=2]",
+        "/response/result/doc[1]/float[@name='id'][.='2.0']",
+        "/response/result/doc[2]/float[@name='id'][.='6.0']",
+        "/response/lst[@name='expanded']/result[@name='group1']/doc[1]/float[@name='id'][.='7.0']",
+        "/response/lst[@name='expanded']/result[@name='group1']/doc[2]/float[@name='id'][.='1.0']",
+        "/response/lst[@name='expanded']/result[@name='group2']/doc[1]/float[@name='id'][.='8.0']",
+        "/response/lst[@name='expanded']/result[@name='group2']/doc[2]/float[@name='id'][.='5.0']"
+    );
+
+    //Test with nullPolicy, ExpandComponent should ignore docs with null values in the collapse fields.
+    //Main result set should include the doc with null value in the collapse field.
+    params = new ModifiableSolrParams();
+    params.add("q", "*:*");
+    params.add("fq", "{!collapse field=group_s nullPolicy=collapse}");
+    params.add("defType", "edismax");
+    params.add("bf", "field(test_ti)");
+    params.add("expand", "true");
+    params.add("expand.sort", "test_tl desc");
+    assertQ(req(params), "*[count(/response/result/doc)=3]",
+        "*[count(/response/lst[@name='expanded']/result)=2]",
+        "/response/result/doc[1]/float[@name='id'][.='3.0']",
+        "/response/result/doc[2]/float[@name='id'][.='2.0']",
+        "/response/result/doc[3]/float[@name='id'][.='6.0']",
+        "/response/lst[@name='expanded']/result[@name='group1']/doc[1]/float[@name='id'][.='7.0']",
+        "/response/lst[@name='expanded']/result[@name='group1']/doc[2]/float[@name='id'][.='1.0']",
+        "/response/lst[@name='expanded']/result[@name='group2']/doc[1]/float[@name='id'][.='8.0']",
+        "/response/lst[@name='expanded']/result[@name='group2']/doc[2]/float[@name='id'][.='5.0']"
+    );
+
+
+    //Test expand.rows
+
+    params = new ModifiableSolrParams();
+    params.add("q", "*:*");
+    params.add("fq", "{!collapse field=group_s}");
+    params.add("defType", "edismax");
+    params.add("bf", "field(test_ti)");
+    params.add("expand", "true");
+    params.add("expand.sort", "test_tl desc");
+    params.add("expand.rows", "1");
+    assertQ(req(params), "*[count(/response/result/doc)=2]",
+        "*[count(/response/lst[@name='expanded']/result)=2]",
+        "*[count(/response/lst[@name='expanded']/result[@name='group1']/doc)=1]",
+        "*[count(/response/lst[@name='expanded']/result[@name='group2']/doc)=1]",
+        "/response/result/doc[1]/float[@name='id'][.='2.0']",
+        "/response/result/doc[2]/float[@name='id'][.='6.0']",
+        "/response/lst[@name='expanded']/result[@name='group1']/doc[1]/float[@name='id'][.='7.0']",
+        "/response/lst[@name='expanded']/result[@name='group2']/doc[1]/float[@name='id'][.='8.0']"
+    );
+
+
+    //Test no group results
+
+    params = new ModifiableSolrParams();
+    params.add("q", "test_ti:5");
+    params.add("fq", "{!collapse field=group_s}");
+    params.add("defType", "edismax");
+    params.add("bf", "field(test_ti)");
+    params.add("expand", "true");
+    params.add("expand.sort", "test_tl desc");
+    params.add("expand.rows", "1");
+    assertQ(req(params), "*[count(/response/result/doc)=1]",
+        "*[count(/response/lst[@name='expanded']/result)=0]"
+    );
+
+    //Test zero results
+
+    params = new ModifiableSolrParams();
+    params.add("q", "test_ti:5532535");
+    params.add("fq", "{!collapse field=group_s}");
+    params.add("defType", "edismax");
+    params.add("bf", "field(test_ti)");
+    params.add("expand", "true");
+    params.add("expand.sort", "test_tl desc");
+    params.add("expand.rows", "1");
+    assertQ(req(params), "*[count(/response/result/doc)=0]",
+        "*[count(/response/lst[@name='expanded']/result)=0]"
+    );
+
+
+
+
+
+
+  }
+
+
+
+
+}

Modified: lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java?rev=1573589&r1=1573588&r2=1573589&view=diff
==============================================================================
--- lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java (original)
+++ lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java Mon Mar  3 15:45:59 2014
@@ -50,6 +50,9 @@ public class QueryResponse extends SolrR
   private NamedList<Object> _groupedInfo = null;
   private GroupResponse _groupResponse = null;
 
+  private NamedList<Object> _expandedInfo = null;
+  private Map<String, SolrDocumentList> _expandedResults = null;
+
   // Facet stuff
   private Map<String,Integer> _facetQuery = null;
   private List<FacetField> _facetFields = null;
@@ -119,7 +122,10 @@ public class QueryResponse extends SolrR
         _groupedInfo = (NamedList<Object>) res.getVal( i );
         extractGroupedInfo( _groupedInfo );
       }
-       else if( "highlighting".equals( n ) ) {
+      else if("expanded".equals(n)) {
+        _expandedResults = (Map<String, SolrDocumentList>) res.getVal( i );
+      }
+      else if( "highlighting".equals( n ) ) {
         _highlightingInfo = (NamedList<Object>) res.getVal( i );
         extractHighlightingInfo( _highlightingInfo );
       }
@@ -410,6 +416,10 @@ public class QueryResponse extends SolrR
     return _facetQuery;
   }
 
+  public Map<String, SolrDocumentList> getExpandedResults(){
+    return this._expandedResults;
+  }
+
   /**
    * Returns the {@link GroupResponse} containing the group commands.
    * A group command can be the result of one of the following parameters:

Added: lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/ExpandParams.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/ExpandParams.java?rev=1573589&view=auto
==============================================================================
--- lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/ExpandParams.java (added)
+++ lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/ExpandParams.java Mon Mar  3 15:45:59 2014
@@ -0,0 +1,31 @@
+/*
+ * 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.common.params;
+
+/**
+ * Expand parameters
+ */
+public interface ExpandParams {
+
+  public static final String EXPAND = "expand";
+  public static final String EXPAND_SORT = EXPAND + ".sort";
+  public static final String EXPAND_ROWS = EXPAND + ".rows";
+
+
+}
+