Index: CHANGES.txt
===================================================================
--- CHANGES.txt	(revision 575451)
+++ CHANGES.txt	(working copy)
@@ -352,7 +352,11 @@
  8. LUCENE-446: Added Solr's search.function for scores based on field 
     values, plus CustomScoreQuery for simple score (post) customization.
     (Yonik Seeley, Doron Cohen)
- 
+
+9.  LUCENE-997: Adds support for a maximum search time limit. After this time is
+    exceeded, the search thread is stopped, partial results (if any)
+    are returned and the total number of results is estimated.
+
 Optimizations
 
  1. LUCENE-761: The proxStream is now cloned lazily in SegmentTermPositions
Index: src/java/org/apache/lucene/search/TopFieldDocCollector.java
===================================================================
--- src/java/org/apache/lucene/search/TopFieldDocCollector.java	(revision 575451)
+++ src/java/org/apache/lucene/search/TopFieldDocCollector.java	(working copy)
@@ -41,12 +41,20 @@
     super(numHits, new FieldSortedHitQueue(reader, sort.fields, numHits));
   }
 
+  public TopFieldDocCollector(IndexReader reader, Sort sort, int numHits,
+			      int maxTicks, TimerThread timer)
+    throws IOException {
+    super(numHits, new FieldSortedHitQueue(reader, sort.fields, numHits),
+          maxTicks, timer);
+  }
+
   // javadoc inherited
   public void collect(int doc, float score) {
     if (score > 0.0f) {
       totalHits++;
       hq.insert(new FieldDoc(doc, score));
     }
+    timeLimitedCollect(doc, score);
   }
 
   // javadoc inherited
Index: src/java/org/apache/lucene/search/Searcher.java
===================================================================
--- src/java/org/apache/lucene/search/Searcher.java	(revision 575451)
+++ src/java/org/apache/lucene/search/Searcher.java	(working copy)
@@ -163,7 +163,27 @@
     return this.similarity;
   }
 
+  protected int maxTicks = Integer.MAX_VALUE;
+  protected TimerThread timer = null;
+
   /**
+   * Set the timer and maxTicks to provide a way to time out requests
+   * that are taking longer than desired.
+   */
+  public void setTimeout(int maxTicks, TimerThread timer) {
+    this.maxTicks = maxTicks;
+    this.timer = timer;
+  }
+
+  /**
+   * Returns the set timeout in milliseconds, or -1 if not set.
+   */
+  public int getTimeout() {
+    if(timer == null) { return -1; }
+    return maxTicks * timer.getTick();
+  }
+
+  /**
    * creates a weight for <code>query</code>
    * @return new weight
    */
Index: src/java/org/apache/lucene/search/TopDocCollector.java
===================================================================
--- src/java/org/apache/lucene/search/TopDocCollector.java	(revision 575451)
+++ src/java/org/apache/lucene/search/TopDocCollector.java	(working copy)
@@ -19,6 +19,8 @@
 
 import org.apache.lucene.util.PriorityQueue;
 
+import java.io.IOException;
+
 /** A {@link HitCollector} implementation that collects the top-scoring
  * documents, returning them as a {@link TopDocs}.  This is used by {@link
  * IndexSearcher} to implement {@link TopDocs}-based search.
@@ -27,7 +29,7 @@
  * conditionally invoke <code>super()</code> in order to filter which
  * documents are collected.
  **/
-public class TopDocCollector extends HitCollector {
+public class TopDocCollector extends TimeLimitedCollector {
   private int numHits;
   private float minScore = 0.0f;
 
@@ -41,11 +43,23 @@
     this(numHits, new HitQueue(numHits));
   }
 
+  public TopDocCollector(int numHits, int maxTicks, TimerThread timer)
+    throws IOException {
+    this(numHits, new HitQueue(numHits), maxTicks, timer);
+  }
+
   TopDocCollector(int numHits, PriorityQueue hq) {
     this.numHits = numHits;
     this.hq = hq;
   }
 
+  TopDocCollector(int numHits, PriorityQueue hq,
+                  int maxTicks, TimerThread timer) {
+    super(maxTicks, timer);
+    this.numHits = numHits;
+    this.hq = hq;
+  }
+    
   // javadoc inherited
   public void collect(int doc, float score) {
     if (score > 0.0f) {
@@ -55,8 +69,16 @@
         minScore = ((ScoreDoc)hq.top()).score; // maintain minScore
       }
     }
+    super.collect(doc, score);
   }
 
+  /**
+   * Provide access to super.collect(int, float)
+   */
+  protected void timeLimitedCollect(int doc, float score) {
+    super.collect(doc, score);
+  }
+
   /** The total number of documents that matched this query. */
   public int getTotalHits() { return totalHits; }
 
Index: src/java/org/apache/lucene/search/TimerThread.java
===================================================================
--- src/java/org/apache/lucene/search/TimerThread.java	(revision 0)
+++ src/java/org/apache/lucene/search/TimerThread.java	(revision 0)
@@ -0,0 +1,90 @@
+package org.apache.lucene.search;
+
+/**
+ * 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.
+ */
+
+/**
+ * TimerThread provides a pseudo-clock service to all searching
+ * threads, so that they can count elapsed time with less overhead
+ * than repeatedly calling System.currentTimeMillis.  A single
+ * thread should be created to be used for all searches.
+ */
+public class TimerThread extends Thread {
+  private int tick;
+  // NOTE: we can avoid explicit synchronization here for several reasons:
+  // * updates to 32-bit-sized variables are atomic
+  // * only single thread modifies this value
+  // * use of volatile keyword ensures that it does not reside in
+  //   a register, but in main memory (so that changes are visible to
+  //   other threads).
+  // * visibility of changes does not need to be instantanous, we can
+  //   afford losing a tick or two.
+  //
+  // See section 17 of the Java Language Specification for details.
+  public volatile int milliseconds = 0;
+  
+  private boolean running = true;
+  
+  /**
+   * @param tick The resolution of a single tick in milliseconds.
+   */
+  public TimerThread(int tick) {
+    super("TimeLimitedCollector timer thread");
+    this.tick = tick;
+    this.setDaemon(true);
+  }
+
+  public void run() {
+    boolean interrupted = false;
+    try {
+      while(running) {
+        milliseconds += tick;
+        try {
+          Thread.sleep(tick);
+        } catch (InterruptedException ie) {
+          interrupted = true;
+          // fall through and retry
+        }
+      }
+    } finally {
+      if (interrupted)
+        Thread.currentThread().interrupt();
+    }
+  }
+
+  /**
+   * Get the timer value in milliseconds.
+   */
+  public int getMilliseconds() {
+    return milliseconds;
+  }
+
+  /**
+   * Returns the number of milliseconds elapsed since startTicks.  This method
+   * handles an overflow condition.
+   *
+   * @param startMilliseconds The number of millliseconds registered by
+   * getMilliseconds() at the begining of the interval being timed.
+   */
+  public int getElapsedMilliseconds( int startMilliseconds ) {
+    int curMilliseconds = milliseconds;
+    if (curMilliseconds < startMilliseconds) {
+      curMilliseconds += Integer.MAX_VALUE;
+    }
+    return (curMilliseconds - startMilliseconds);
+  }
+}
Index: src/java/org/apache/lucene/search/TopDocs.java
===================================================================
--- src/java/org/apache/lucene/search/TopDocs.java	(revision 575451)
+++ src/java/org/apache/lucene/search/TopDocs.java	(working copy)
@@ -20,6 +20,7 @@
 /** Expert: Returned by low-level search implementations.
  * @see Searcher#search(Query,Filter,int) */
 public class TopDocs implements java.io.Serializable {
+  private boolean partialResultsFlag = false;
   /** Expert: The total number of hits for the query.
    * @see Hits#length()
   */
@@ -45,4 +46,22 @@
     this.scoreDocs = scoreDocs;
     this.maxScore = maxScore;
   }
+
+  /**
+   * Indicates that these top hits are based on only looking at some
+   * results.  Searching or collecting of the docs was intentionally
+   * limited by some factor such as a time restriction.
+   *
+   * @return true if these top hits are based on only looking at some results.
+   */
+  public boolean arePartialResults() {
+    return partialResultsFlag;
+  }
+
+  /**
+   * Sets partial results flag.
+   */
+  public void setPartialResultsFlag(boolean p) {
+    partialResultsFlag = p;
+  }
 }
Index: src/java/org/apache/lucene/search/Hits.java
===================================================================
--- src/java/org/apache/lucene/search/Hits.java	(revision 575451)
+++ src/java/org/apache/lucene/search/Hits.java	(working copy)
@@ -38,6 +38,7 @@
   private Filter filter = null;
   private Sort sort = null;
 
+  private boolean partialResultsFlag = false;
   private int length;				  // the total number of hits
   private Vector hitDocs = new Vector();	  // cache of hits retrieved
 
@@ -72,6 +73,7 @@
 
     int n = min * 2;	// double # retrieved
     TopDocs topDocs = (sort == null) ? searcher.search(weight, filter, n) : searcher.search(weight, filter, n, sort);
+    if( topDocs.arePartialResults() ) { partialResultsFlag = true; }
     length = topDocs.totalHits;
     ScoreDoc[] scoreDocs = topDocs.scoreDocs;
 
@@ -145,6 +147,24 @@
     return new HitIterator(this);
   }
 
+  /**
+   * Indicates that these top hits are based on only looking at some
+   * results.  Searching or collecting of the docs was intentionally
+   * limited by some factor such as a time restriction.
+   *
+   * @return true if these top hits are based on only looking at some results.
+   */
+  public boolean arePartialResults() {
+    return partialResultsFlag;
+  }
+
+  /**
+   * Sets partial results flag.
+   */
+  public void setPartialResultsFlag(boolean p) {
+    partialResultsFlag = p;
+  }
+
   private final HitDoc hitDoc(int n) throws IOException {
     if (n >= length) {
       throw new IndexOutOfBoundsException("Not a valid hit number: " + n);
Index: src/java/org/apache/lucene/search/TimeLimitedCollector.java
===================================================================
--- src/java/org/apache/lucene/search/TimeLimitedCollector.java	(revision 0)
+++ src/java/org/apache/lucene/search/TimeLimitedCollector.java	(revision 0)
@@ -0,0 +1,74 @@
+package org.apache.lucene.search;
+
+/**
+ * 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.
+ */
+
+/**
+ * <p>The TimeLimitedCollector is used to timeout search requests that
+ * take longer than the maximum allowed search time limit.  After this
+ * time is exceeded, the search thread is stopped by throwing a
+ * TimeExceeded Exception.</p>
+ *
+ * <p>To use the TimeLimitedCollector, your collector should extend
+ * this class and call <tt>super.collect(doc, score);</tt>.
+ *
+ * @see Searcher#search(Query,HitCollector)
+ * @version $Id:$
+ */
+public class TimeLimitedCollector extends HitCollector {
+
+  private TimerThread timer;
+
+  private int timeAllowed;
+  private int curMilliseconds;
+
+  public static class TimeExceeded extends RuntimeException {
+    public long maxTime;
+    public int maxDoc;
+    public TimeExceeded(long maxTime, int maxDoc) {
+      super("Exceeded search time: " + maxTime + " ms.");
+      this.maxTime = maxTime;
+      this.maxDoc = maxDoc;
+    }
+  }
+
+  /**
+   * Timer is null and inactive if constructed with default constructor.
+   */
+  public TimeLimitedCollector() {
+  }
+
+  public TimeLimitedCollector(int timeAllowed, TimerThread timer) {
+      this.timeAllowed = timeAllowed;
+      if (timer != null) {
+    	this.timer = timer;
+        this.curMilliseconds = timer.getMilliseconds();
+      }
+  }
+
+  /**
+   * @throws TimeExceeded if the time allowed is exceeded.
+   */
+  public void collect(int doc, float score) {
+    if (timer != null) {
+      int elapsed = timer.getElapsedMilliseconds( curMilliseconds );
+      if (elapsed > timeAllowed) {
+	throw new TimeExceeded(elapsed, doc);
+      }
+    }    
+  }
+}
Index: src/java/org/apache/lucene/search/IndexSearcher.java
===================================================================
--- src/java/org/apache/lucene/search/IndexSearcher.java	(revision 575451)
+++ src/java/org/apache/lucene/search/IndexSearcher.java	(working copy)
@@ -109,9 +109,22 @@
     if (nDocs <= 0)  // null might be returned from hq.top() below.
       throw new IllegalArgumentException("nDocs must be > 0");
 
-    TopDocCollector collector = new TopDocCollector(nDocs);
-    search(weight, filter, collector);
-    return collector.topDocs();
+    TopDocCollector collector = new TopDocCollector(nDocs, maxTicks, timer);
+    TimeLimitedCollector.TimeExceeded te = null;
+    try {
+      search(weight, filter, collector);
+    }
+    catch( TimeLimitedCollector.TimeExceeded x ) {
+      te = x;
+    }
+    TopDocs topDocs = collector.topDocs();
+    if( te != null && topDocs != null ) {
+      topDocs.setPartialResultsFlag(true);
+      // Estimate total hits.
+      topDocs.totalHits = (int)(topDocs.totalHits
+                                * (maxDoc()/(float)te.maxDoc));
+    }
+    return topDocs;
   }
 
   // inherit javadoc
@@ -120,12 +133,27 @@
       throws IOException {
 
     TopFieldDocCollector collector =
-      new TopFieldDocCollector(reader, sort, nDocs);
-    search(weight, filter, collector);
-    return (TopFieldDocs)collector.topDocs();
+      new TopFieldDocCollector(reader, sort, nDocs, maxTicks, timer);
+    TimeLimitedCollector.TimeExceeded te = null;
+    try {
+      search(weight, filter, collector);
+    }
+    catch( TimeLimitedCollector.TimeExceeded x ) {
+      te = x;
+    }
+    TopFieldDocs topFieldDocs = (TopFieldDocs)collector.topDocs();
+    if( te != null && topFieldDocs != null ) {
+      topFieldDocs.setPartialResultsFlag(true);
+      // Estimate total hits.
+      topFieldDocs.totalHits = (int)(topFieldDocs.totalHits
+                                * (maxDoc()/(float)te.maxDoc));
+    }
+    return topFieldDocs;
   }
 
   // inherit javadoc
+  // FIXME: If HitCollector passed in is a TimeLimitedCollector,
+  // TimeLimitedCollector.TimeExceeded could be thrown from this method.
   public void search(Weight weight, Filter filter,
                      final HitCollector results) throws IOException {
     HitCollector collector = results;
