Index: src/java/org/apache/solr/search/CollapseFilter.java
===================================================================
--- src/java/org/apache/solr/search/CollapseFilter.java	(revision 0)
+++ src/java/org/apache/solr/search/CollapseFilter.java	(revision 0)
@@ -0,0 +1,286 @@
+package org.apache.solr.search;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+
+import org.apache.lucene.search.FieldCache;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.params.SolrParams.CollapseType;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.schema.FieldType;
+import org.apache.solr.schema.SchemaField;
+
+/**
+ * @author Emmanuel Keller keller.emmanuel@gmail.com
+ * @version $Id:$
+ * @since solr 1.3
+ */
+public class CollapseFilter {
+
+  private class CollapseParams {
+
+    private CollapseType getCollapseType(String str) {
+      if (str != null) {
+        str = str.toUpperCase();
+        if (str.equals("ADJACENT")) {
+          return CollapseType.ADJACENT;
+        } else if (str.equals("NORMAL")) {
+          return CollapseType.NORMAL;
+        }
+      }
+      return null;
+    }
+
+    /**
+     * Field use to collapse results
+     */
+    private String collapseField;
+
+    /**
+     * Maximum number of visible documents for a collapsed result
+     */
+    private int collapseMax;
+
+    private CollapseType collapseType;
+
+    private CollapseParams(SolrParams p) {
+      collapseField = p.required().get(SolrParams.COLLAPSE_FIELD);
+      collapseMax = p.getInt(SolrParams.COLLAPSE_MAX, 1);
+
+      collapseType = CollapseType.NORMAL;
+      String t = p.get(SolrParams.COLLAPSE_TYPE);
+      if (t != null) {
+        collapseType = getCollapseType(t);
+        if (t == null) {
+          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unknown collapse type: '" + t
+              + "' -- must be: normal or adjacent");
+        }
+      }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o == this)
+        return true;
+      if (!(o instanceof CollapseParams))
+        return false;
+      CollapseParams other = (CollapseParams) o;
+      if (collapseType != other.collapseType)
+        return false;
+      if (collapseMax != other.collapseMax)
+        return false;
+      if (!collapseField.equals(other.collapseField))
+        return false;
+      return true;
+    }
+  }
+
+  public class CollapseCacheKey {
+
+    private QueryResultKey queryKey;
+
+    private CollapseParams collapseParams;
+
+    private final int hc;
+
+    public CollapseCacheKey(QueryResultKey queryKey, CollapseParams collapseParams) {
+      this.queryKey = queryKey;
+      this.collapseParams = collapseParams;
+      hc = queryKey.hashCode();
+    }
+
+    @Override
+    public int hashCode() {
+      return hc;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o == this)
+        return true;
+      if (!(o instanceof CollapseCacheKey))
+        return false;
+      CollapseCacheKey other = (CollapseCacheKey) o;
+      if (!collapseParams.equals(other.collapseParams))
+        return false;
+      if (!queryKey.equals(other.queryKey))
+        return false;
+      return true;
+    }
+  }
+
+  private class CollapseResult {
+    /**
+     * Contains final documents (DocSet)
+     */
+    private DocSet docSet;
+
+    /**
+     * Group list
+     */
+    private HashMap<Integer, Integer> collapseList;
+
+    /**
+     * Stores the reader version. Used to invalidate old cached results
+     */
+    private long readerVersion;
+
+    private long time1, time2;
+
+    private CollapseResult(long readerVersion) {
+      collapseList = new HashMap<Integer, Integer>();
+      docSet = new BitDocSet();
+      this.readerVersion = readerVersion;
+    }
+  }
+
+  /**
+   * Analyse of SolrParams
+   */
+  private CollapseParams collapseParams = null;
+
+  /**
+   * Results of collapsing (for cache storage)
+   */
+  private CollapseResult collapseResult = null;
+
+  private void adjacentCollapse(DocList docList, String[] fields) {
+
+    int repeatCount = 0;
+    int lastVisibleId = -1;
+    int collapseCount = 0;
+    int collapseId = -1;
+    String previousField = null;
+
+    long startTime = System.currentTimeMillis();
+
+    DocIterator it = docList.iterator();
+    while (it.hasNext()) {
+      int currentId = it.nextDoc();
+      String currentField = fields[currentId];
+      // By default, the current document will be added to the list
+      boolean bAdd = true;
+      if (currentField != null && currentField.equals(previousField)) {
+        if (++repeatCount >= collapseParams.collapseMax) {
+          // Document will be collapsed
+          bAdd = false;
+          collapseCount++;
+          if (collapseId == -1) {
+            // collapseResult.hasMoreResult.add(lastVisibleId);
+            collapseId = lastVisibleId;
+          }
+        }
+      } else { // !currentField.equals
+        repeatCount = 0;
+        if (collapseId != -1) {
+          collapseResult.collapseList.put(new Integer(collapseId), new Integer(collapseCount));
+          collapseCount = 0;
+          collapseId = -1;
+        }
+      }
+      if (bAdd) {
+        collapseResult.docSet.add(currentId);
+        lastVisibleId = currentId;
+      }
+      previousField = currentField;
+    } // while
+
+    if (collapseId != -1)
+      collapseResult.collapseList.put(new Integer(collapseId), new Integer(collapseCount));
+
+    collapseResult.time1 = System.currentTimeMillis() - startTime;
+  }
+
+  /**
+   * 
+   * @param searcher
+   * @param query
+   * @param filters
+   * @param sort
+   * @param flags
+   * @param params
+   * @throws IOException
+   */
+  public CollapseFilter(SolrIndexSearcher searcher, Query query, List<Query> filters, Sort sort, int flags,
+      SolrParams params) throws IOException {
+    this.collapseParams = new CollapseParams(params);
+
+    // Cache
+    CollapseCacheKey cacheKey = new CollapseCacheKey(new QueryResultKey(query, filters, sort, flags), collapseParams);
+    collapseResult = (CollapseResult) searcher.cacheLookup("collapseCache", cacheKey);
+    long indexVersion = searcher.getReader().getVersion();
+    if (collapseResult != null)
+      if (collapseResult.readerVersion == indexVersion)
+        return;
+
+    collapseResult = new CollapseResult(indexVersion);
+
+    // Do the job
+    DocList docList = null;
+    if (collapseParams.collapseType == CollapseType.ADJACENT)
+      docList = searcher.getDocList(query, filters, sort, 0, searcher.maxDoc(), flags);
+    else
+      docList = searcher.getDocList(query, filters, new Sort(new SortField(collapseParams.collapseField)), 0, searcher
+          .maxDoc(), flags);
+
+    String[] fields = FieldCache.DEFAULT.getStrings(searcher.getReader(), collapseParams.collapseField);
+    adjacentCollapse(docList, fields);
+
+    // Put the result on the cache
+    searcher.cacheInsert("collapseCache", cacheKey, collapseResult);
+  }
+
+  // Get the final docSet
+  public DocSet getDocSet() {
+    return collapseResult.docSet;
+  }
+
+  /**
+   * Contains document count per group
+   */
+  private NamedList<Object> namedList = null;
+
+  /**
+   * 
+   * @param searcher
+   * @param docList
+   * @return
+   * @throws IOException
+   */
+  public NamedList<Object> getMoreResults(SolrIndexSearcher searcher, DocList docList) throws IOException {
+
+    if (namedList != null)
+      return namedList;
+
+    long startTime = System.currentTimeMillis();
+
+    NamedList<Object> res = new NamedList<Object>();
+    res.add("field", collapseParams.collapseField);
+    DocIterator it = docList.iterator();
+    int id = -1;
+    FieldType collapseFieldType = searcher.getSchema().getField(collapseParams.collapseField).getType();
+    SchemaField uniqueSchemaField = searcher.getSchema().getUniqueKeyField();
+    String[] collapseFields = FieldCache.DEFAULT.getStrings(searcher.getReader(), collapseParams.collapseField);
+    NamedList<Integer> resCount = new NamedList<Integer>();
+    NamedList<Integer> resDoc = new NamedList<Integer>();
+    while (it.hasNext()) {
+      id = it.nextDoc();
+      Integer n = collapseResult.collapseList.get(new Integer(id));
+      if (n != null) {
+        resDoc.add(searcher.getReader().document(id).get(uniqueSchemaField.getName()), n);
+        resCount.add(collapseFieldType.indexedToReadable(collapseFields[id]), n);
+      }
+    }
+    collapseResult.time2 = System.currentTimeMillis() - startTime;
+    res.add("time", collapseResult.time1 + "/" + collapseResult.time2);
+    res.add("doc", resDoc);
+    res.add("count", resCount);
+    namedList = res;
+    return namedList;
+  }
+}
Index: src/java/org/apache/solr/search/SolrIndexSearcher.java
===================================================================
--- src/java/org/apache/solr/search/SolrIndexSearcher.java	(revision 551170)
+++ src/java/org/apache/solr/search/SolrIndexSearcher.java	(working copy)
@@ -698,6 +698,29 @@
     getDocListC(answer,query,filterList,null,lsort,offset,len,flags);
     return answer.docList;
   }
+  
+  /**
+   * Returns documents matching both <code>query</code> and the 
+   * intersection of the <code>filterList</code>, sorted by <code>sort</code>.
+   * <p>
+   * This method is cache aware and may retrieve <code>filter</code> from
+   * the cache or make an insertion into the cache as a result of this call.
+   * <p>
+   * FUTURE: The returned DocList may be retrieved from a cache.
+   *
+   * @param query
+   * @param filterList may be null
+   * @param lsort    criteria by which to sort (if null, query relevance is used)
+   * @param offset   offset into the list of documents to return
+   * @param len      maximum number of documents to return
+   * @return DocList meeting the specified criteria, should <b>not</b> be modified by the caller.
+   * @throws IOException
+   */
+  public DocList getDocList(Query query, List<Query> filterList, DocSet filter, Sort lsort, int offset, int len, int flags) throws IOException {
+    DocListAndSet answer = new DocListAndSet();
+    getDocListC(answer,query,filterList,filter,lsort,offset,len,flags);
+    return answer.docList;
+  }
 
 
   private static final int NO_CHECK_QCACHE       = 0x80000000;
@@ -1239,6 +1262,35 @@
   }
 
   /**
+   * Returns documents matching both <code>query</code> and the intersection 
+   * of <code>filterList</code>, sorted by <code>sort</code>.  
+   * Also returns the compete set of documents
+   * matching <code>query</code> and <code>filter</code> 
+   * (regardless of <code>offset</code> and <code>len</code>).
+   * <p>
+   * This method is cache aware and may retrieve <code>filter</code> from
+   * the cache or make an insertion into the cache as a result of this call.
+   * <p>
+   * FUTURE: The returned DocList may be retrieved from a cache.
+   * <p>
+   * The DocList and DocSet returned should <b>not</b> be modified.
+   *
+   * @param query
+   * @param filterList   may be null
+   * @param lsort    criteria by which to sort (if null, query relevance is used)
+   * @param offset   offset into the list of documents to return
+   * @param len      maximum number of documents to return
+   * @param flags    user supplied flags for the result set
+   * @return DocListAndSet meeting the specified criteria, should <b>not</b> be modified by the caller.
+   * @throws IOException
+   */
+  public DocListAndSet getDocListAndSet(Query query, List<Query> filterList, DocSet filter, Sort lsort, int offset, int len, int flags) throws IOException {
+      DocListAndSet ret = new DocListAndSet();
+      getDocListC(ret,query,filterList,filter,lsort,offset,len, flags |= GET_DOCSET);
+      return ret;
+  }
+  
+  /**
    * Returns documents matching both <code>query</code> and <code>filter</code>
    * and sorted by <code>sort</code>. Also returns the compete set of documents
    * matching <code>query</code> and <code>filter</code> (regardless of <code>offset</code> and <code>len</code>).
Index: src/java/org/apache/solr/common/params/SolrParams.java
===================================================================
--- src/java/org/apache/solr/common/params/SolrParams.java	(revision 551170)
+++ src/java/org/apache/solr/common/params/SolrParams.java	(working copy)
@@ -123,7 +123,28 @@
    */
   public static final String FACET_ENUM_CACHE_MINDF = "facet.enum.cache.minDf";
 
+  /**
+   * "normal" or "adjacent"
+   */
+  public static final String COLLAPSE_TYPE = "collapse.type";
 
+  /**
+   * Any field whose terms the user wants to enumerate over for Collapse Constraint
+   */
+  public static final String COLLAPSE_FIELD = "collapse.field";
+
+
+  public enum CollapseType {
+    NORMAL, ADJACENT;
+  }
+  
+  
+  /**
+   * Number of visible document before collapsing
+   */
+  public static final String COLLAPSE_MAX = "collapse.max";
+
+  
   /** If the content stream should come from a URL (using URLConnection) */
   public static final String STREAM_URL = "stream.url";
 
Index: src/java/org/apache/solr/handler/StandardRequestHandler.java
===================================================================
--- src/java/org/apache/solr/handler/StandardRequestHandler.java	(revision 551170)
+++ src/java/org/apache/solr/handler/StandardRequestHandler.java	(working copy)
@@ -117,15 +117,23 @@
       List<Query> filters = U.parseFilterQueries(req);
       SolrIndexSearcher s = req.getSearcher();
 
+      // CollapseFilter is a docset used as intersection
+      CollapseFilter collapseFilter = null;
+      DocSet collapseFilterDocSet = null;
+      if (p.get(COLLAPSE_FIELD) != null) {
+        collapseFilter = new CollapseFilter(s, query, filters, sort, flags, p);
+        collapseFilterDocSet = collapseFilter.getDocSet();
+      }
+      
       if (p.getBool(FACET,false)) {
-        results = s.getDocListAndSet(query, filters, sort,
-                                     p.getInt(START,0), p.getInt(ROWS,10),
-                                     flags);
+        results = s.getDocListAndSet( query, filters, collapseFilterDocSet, sort,
+                                      p.getInt(START,0), p.getInt(ROWS,10),
+                                      flags);
         facetInfo = getFacetInfo(req, rsp, results.docSet);
       } else {
-        results.docList = s.getDocList(query, filters, sort,
-                                       p.getInt(START,0), p.getInt(ROWS,10),
-                                       flags);
+        results.docList = s.getDocList( query, filters, collapseFilterDocSet, sort,
+                                        p.getInt(START,0), p.getInt(ROWS,10),
+                                        flags);
       }
 
       // pre-fetch returned documents
@@ -142,7 +150,10 @@
         int mltcount = p.getInt( MoreLikeThisParams.DOC_COUNT, 5 );
         rsp.add( "moreLikeThis", mlt.getMoreLikeThese(results.docList, mltcount, flags));
       }
-      
+
+      if (null != collapseFilter)
+        rsp.add("collapse_counts", collapseFilter.getMoreResults(s, results.docList));
+             
       try {
         NamedList dbg = U.doStandardDebug(req, qstr, query, results.docList);
         if (null != dbg) {
Index: src/java/org/apache/solr/handler/DisMaxRequestHandler.java
===================================================================
--- src/java/org/apache/solr/handler/DisMaxRequestHandler.java	(revision 551170)
+++ src/java/org/apache/solr/handler/DisMaxRequestHandler.java	(working copy)
@@ -42,6 +42,7 @@
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrQueryResponse;
 import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.search.CollapseFilter;
 import org.apache.solr.search.DocListAndSet;
 import org.apache.solr.search.DocSet;
 import org.apache.solr.search.QueryParsing;
@@ -314,14 +315,22 @@
       
       DocListAndSet results = new DocListAndSet();
       NamedList facetInfo = null;
+
+      // CollapseFilter is a docset used as intersection
+      CollapseFilter collapseFilter = null;
+      DocSet collapseFilterDocSet = null;
+      if (params.get(SolrParams.COLLAPSE_FIELD) != null) {
+        collapseFilter = new CollapseFilter(s, query, restrictions, SolrPluginUtils.getSort(req), flags, params);
+        collapseFilterDocSet = collapseFilter.getDocSet();
+      }
       if (params.getBool(FACET,false)) {
-        results = s.getDocListAndSet(query, restrictions,
+        results = s.getDocListAndSet(query, restrictions,collapseFilterDocSet,
                                      SolrPluginUtils.getSort(req),
                                      req.getStart(), req.getLimit(),
                                      flags);
         facetInfo = getFacetInfo(req, rsp, results.docSet);
       } else {
-        results.docList = s.getDocList(query, restrictions,
+        results.docList = s.getDocList(query, restrictions,collapseFilterDocSet,
                                        SolrPluginUtils.getSort(req),
                                        req.getStart(), req.getLimit(),
                                        flags);
@@ -333,6 +342,8 @@
       
       if (null != facetInfo) rsp.add("facet_counts", facetInfo);
 
+      if (null != collapseFilter)
+        rsp.add("collapse_counts", collapseFilter.getMoreResults(s, results.docList));
 
             
       /* * * Debugging Info * * */
Index: example/solr/conf/solrconfig.xml
===================================================================
--- example/solr/conf/solrconfig.xml	(revision 551170)
+++ example/solr/conf/solrconfig.xml	(working copy)
@@ -144,6 +144,13 @@
       size="512"
       initialSize="512"
       autowarmCount="0"/>
+      
+  <!--  collapseCache caches collapsing informations  -->
+    <cache name="collapseCache"
+      class="solr.LRUCache"
+      size="512"
+      initialSize="512"
+      autowarmCount="0"/>
 
     <!-- If true, stored fields that are not requested will be loaded lazily.
 
