Index: contrib/caching/src/test/org/apache/lucene/index/TestStressWithBufferPool.java
===================================================================
--- contrib/caching/src/test/org/apache/lucene/index/TestStressWithBufferPool.java	(revision 0)
+++ contrib/caching/src/test/org/apache/lucene/index/TestStressWithBufferPool.java	(revision 0)
@@ -0,0 +1,180 @@
+package org.apache.lucene.index;
+
+import org.apache.lucene.util.*;
+import org.apache.lucene.store.*;
+import org.apache.lucene.document.*;
+import org.apache.lucene.analysis.*;
+import org.apache.lucene.search.*;
+
+import java.util.Random;
+import java.io.File;
+
+public class TestStressWithBufferPool extends LuceneTestCase {
+  private static final Analyzer ANALYZER = new SimpleAnalyzer();
+  private static final Random RANDOM = new Random();
+
+  private static abstract class TimedThread extends Thread {
+    boolean failed;
+    int count;
+    private static int RUN_TIME_SEC = 6;
+    private TimedThread[] allThreads;
+
+    abstract public void doWork() throws Throwable;
+
+    TimedThread(TimedThread[] threads) {
+      this.allThreads = threads;
+    }
+
+    public void run() {
+      final long stopTime = System.currentTimeMillis() + 1000 * RUN_TIME_SEC;
+
+      count = 0;
+
+      try {
+        while (System.currentTimeMillis() < stopTime && !anyErrors()) {
+          doWork();
+          count++;
+        }
+      } catch (Throwable e) {
+        e.printStackTrace(System.out);
+        failed = true;
+      }
+    }
+
+    private boolean anyErrors() {
+      for (int i = 0; i < allThreads.length; i++)
+        if (allThreads[i] != null && allThreads[i].failed)
+          return true;
+      return false;
+    }
+  }
+
+  private static class IndexerThread extends TimedThread {
+    Directory directory;
+    public int count;
+    int nextID;
+
+    public IndexerThread(Directory directory, TimedThread[] threads) {
+      super(threads);
+      this.directory = directory;
+    }
+
+    public void doWork() throws Exception {
+      IndexWriter writer = createWriter(directory);
+
+      // Add 10 docs:
+      for (int j = 0; j < 10; j++) {
+        Document d = new Document();
+        int n = RANDOM.nextInt();
+        d.add(new Field("id", Integer.toString(nextID++), Field.Store.YES,
+            Field.Index.UN_TOKENIZED));
+        d.add(new Field("contents", "apache " + English.intToEnglish(n),
+            Field.Store.NO, Field.Index.TOKENIZED));
+        writer.addDocument(d);
+      }
+
+      // Delete 5 docs:
+      int deleteID = nextID - 1;
+      for (int j = 0; j < 5; j++) {
+        writer.deleteDocuments(new Term("id", "" + deleteID));
+        deleteID -= 2;
+      }
+
+      writer.close();
+    }
+  }
+
+  private static class SearcherThread extends TimedThread {
+    private Directory directory;
+
+    public SearcherThread(Directory directory, TimedThread[] threads) {
+      super(threads);
+      this.directory = directory;
+    }
+
+    public void doWork() throws Throwable {
+      for (int i = 0; i < 10; i++) {
+        IndexSearcher searcher = new IndexSearcher(directory);
+        Hits hits =
+            searcher.search(new TermQuery(new Term("contents", "apache")));
+        assertEquals(0, hits.length() % 5);
+        for (int j = 0; j < hits.length(); j++) {
+          Document doc = hits.doc(j);
+          int id = Integer.parseInt(doc.get("id"));
+          assertEquals(0, id % 2);
+        }
+        searcher.close();
+      }
+      count += 100;
+    }
+  }
+
+  private static IndexWriter createWriter(Directory dir) throws Exception {
+    IndexWriter writer =
+        new IndexWriter(dir, ANALYZER, IndexWriter.MaxFieldLength.LIMITED);
+    writer.setMaxBufferedDocs(10);
+    writer.setUseCompoundFile(false);
+    return writer;
+  }
+
+  /**
+   * Run one indexer and 2 searchers against single index as stress test.
+   */
+  public void runStressTest(Directory directory) throws Exception {
+
+    TimedThread[] threads = new TimedThread[3];
+    int numThread = 0;
+
+    // One modifier that writes 10 docs then removes 5, over
+    // and over:
+    IndexerThread indexerThread = new IndexerThread(directory, threads);
+    threads[numThread++] = indexerThread;
+    indexerThread.start();
+
+    // Two searchers that constantly just re-instantiate the
+    // searcher:
+    BufferPool pool = new BufferPoolLRU(16400, 3);
+    BufferPooledDirectory bDirectory =
+        new BufferPooledDirectory(directory, false, pool, true);
+
+    SearcherThread searcherThread1 = new SearcherThread(bDirectory, threads);
+    threads[numThread++] = searcherThread1;
+    searcherThread1.start();
+
+    SearcherThread searcherThread2 = new SearcherThread(bDirectory, threads);
+    threads[numThread++] = searcherThread2;
+    searcherThread2.start();
+
+    for (int i = 0; i < numThread; i++)
+      threads[i].join();
+
+    for (int i = 0; i < numThread; i++)
+      assertTrue(!((TimedThread) threads[i]).failed);
+
+    bDirectory.close();
+  }
+
+  /**
+   * Run above stress test against RAMDirectory and then FSDirectory.
+   */
+  public void testStressIndexAndSearching() throws Exception {
+
+    // RAMDir
+    Directory directory = new RAMDirectory();
+    IndexWriter writer = createWriter(directory);
+    writer.close();
+    runStressTest(directory);
+    directory.close();
+
+    // FSDir
+    String tempDir = System.getProperty("java.io.tmpdir");
+    File dirPath = new File(tempDir, "lucene.test.stress");
+    directory = FSDirectory.getDirectory(dirPath);
+    writer = createWriter(directory);
+    writer.close();
+    runStressTest(directory);
+    directory.close();
+
+    _TestUtil.rmDir(dirPath);
+  }
+}
Index: contrib/caching/src/test/org/apache/lucene/store/TestBufferPoolLRU.java
===================================================================
--- contrib/caching/src/test/org/apache/lucene/store/TestBufferPoolLRU.java	(revision 0)
+++ contrib/caching/src/test/org/apache/lucene/store/TestBufferPoolLRU.java	(revision 0)
@@ -0,0 +1,136 @@
+package org.apache.lucene.store;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Random;
+
+import junit.framework.TestCase;
+
+public class TestBufferPoolLRU extends TestCase {
+
+  class FakeIndexInput extends BufferedIndexInput {
+    long length;
+
+    FakeIndexInput(long length) {
+      this.length = length;
+    }
+
+    protected void readInternal(byte[] b, int offset, int length)
+        throws IOException {
+    }
+
+    protected void seekInternal(long pos) throws IOException {
+    }
+
+    public void close() throws IOException {
+    }
+
+    public long length() {
+      return length;
+    }
+  }
+
+  class BufferPoolLRU2 extends BufferPoolLRU {
+    boolean longRead;
+
+    BufferPoolLRU2(int bufferSize, int numBuffers, boolean longRead) {
+      super(bufferSize, numBuffers);
+      this.longRead = longRead;
+    }
+
+    protected void readInput(IndexInput input, long position, byte[] b,
+        int offset, int len) throws IOException {
+      for (int i = 0; i < len; i++) {
+        b[offset + i] = (byte) ((position + i) % Byte.MAX_VALUE);
+      }
+      try {
+        if (longRead) {
+          Thread.sleep(10);
+        }
+      } catch (Exception e) {
+      }
+    }
+  }
+
+  class BufferPoolTest {
+    final int length;
+    final int bufferSize;
+    final FakeIndexInput input;
+    final BufferPool pool;
+    final ArrayList eList;
+
+    BufferPoolTest(boolean longRead) {
+      length = 262189;
+      bufferSize = 8531;
+      int numBuffers = 19;
+
+      input = new FakeIndexInput(length);
+      pool = new BufferPoolLRU2(bufferSize, numBuffers, longRead);
+      eList = new ArrayList();
+    }
+
+    void runTest(int numThreads, int numReads) throws Exception {
+      ReadThread[] threads = new ReadThread[numThreads];
+      for (int i = 0; i < threads.length; i++) {
+        threads[i] = new ReadThread(i, numReads);
+      }
+      for (int i = 0; i < threads.length; i++) {
+        threads[i].start();
+      }
+      for (int i = 0; i < threads.length; i++) {
+        threads[i].join();
+      }
+      if (eList.size() > 0) {
+        throw (Exception) eList.get(0); // throw the first
+      }
+    }
+
+    class ReadThread extends Thread {
+      String fakeFileName = new String("c.txt");
+      Random r;
+      byte[] local;
+      int numReads;
+
+      ReadThread(int seed, int numReads) {
+        this.r = new Random(seed);
+        this.local = new byte[r.nextInt(bufferSize) + 1];
+        this.numReads = numReads;
+      }
+
+      public void run() {
+        for (int i = 0; i < numReads; i++) {
+          int position = r.nextInt(length);
+          int offset = r.nextInt(local.length);
+          int len = r.nextInt(local.length - offset);
+          if (position + len > length) {
+            len = length - position;
+          }
+
+          try {
+            pool.read(fakeFileName, position, local, offset, len, input);
+          } catch (IOException e) {
+            synchronized (eList) {
+              eList.add(e);
+            }
+          }
+
+          for (int j = 0; j < len; j++) {
+            byte expected = (byte) ((position + j) % Byte.MAX_VALUE);
+            assertEquals(expected, local[offset + j]);
+          }
+        }
+      }
+    }
+  }
+
+  public void testBufferPoolShort() throws Exception {
+    BufferPoolTest test = new BufferPoolTest(false);
+    test.runTest(20, 1000);
+  }
+
+  public void testBufferPoolLong() throws Exception {
+    BufferPoolTest test = new BufferPoolTest(true);
+    test.runTest(20, 1000);
+  }
+
+}
Index: contrib/caching/src/java/org/apache/lucene/index/ReadOnlyFilenameFilter.java
===================================================================
--- contrib/caching/src/java/org/apache/lucene/index/ReadOnlyFilenameFilter.java	(revision 0)
+++ contrib/caching/src/java/org/apache/lucene/index/ReadOnlyFilenameFilter.java	(revision 0)
@@ -0,0 +1,25 @@
+package org.apache.lucene.index;
+
+import java.io.File;
+
+public class ReadOnlyFilenameFilter extends IndexFileNameFilter {
+
+  static ReadOnlyFilenameFilter singleton = new ReadOnlyFilenameFilter();
+
+  public static IndexFileNameFilter getFilter() {
+    return singleton;
+  }
+
+  public ReadOnlyFilenameFilter() {
+    super();
+  }
+
+  public boolean accept(File dir, String name) {
+    if (super.accept(dir, name)) {
+      return !name.equals(IndexFileNames.SEGMENTS_GEN);
+    } else {
+      return false;
+    }
+  }
+
+}
Index: contrib/caching/src/java/org/apache/lucene/store/BufferPooledDirectory.java
===================================================================
--- contrib/caching/src/java/org/apache/lucene/store/BufferPooledDirectory.java	(revision 0)
+++ contrib/caching/src/java/org/apache/lucene/store/BufferPooledDirectory.java	(revision 0)
@@ -0,0 +1,129 @@
+package org.apache.lucene.store;
+
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import org.apache.lucene.index.ReadOnlyFilenameFilter;
+
+/**
+ * For reader, not for writer. So disable methods such as
+ * createOutput, deleteFile, touchFile and makeLock.
+ */
+public class BufferPooledDirectory extends Directory {
+
+  private Directory dir;
+  private boolean closeDir;
+  private BufferPool pool;
+  private boolean closePool;
+  private FilenameFilter filter;
+
+  // files have to be read-only to use buffer pool
+  public BufferPooledDirectory(Directory dir, boolean closeDir,
+      BufferPool pool, boolean closePool) {
+    this(dir, closeDir, pool, closePool, ReadOnlyFilenameFilter.getFilter());
+  }
+
+  public BufferPooledDirectory(Directory dir, boolean closeDir,
+      BufferPool pool, boolean closePool, FilenameFilter filter) {
+    this.dir = dir;
+    this.closeDir = closeDir;
+    this.pool = pool;
+    this.closePool = closePool;
+    this.filter = filter;
+  }
+
+  public Directory getDirectory() {
+    return dir;
+  }
+
+  public BufferPool getBufferPool() {
+    return pool;
+  }
+
+  public void close() throws IOException {
+    try {
+      if (closeDir) {
+        dir.close();
+      }
+    } finally {
+      if (closePool) {
+        pool.close();
+      }
+    }
+  }
+
+  public IndexOutput createOutput(String name) throws IOException {
+    throw new UnsupportedOperationException();
+  }
+
+  public void deleteFile(String name) throws IOException {
+    throw new UnsupportedOperationException();
+  }
+
+  public boolean fileExists(String name) throws IOException {
+    return dir.fileExists(name);
+  }
+
+  public long fileLength(String name) throws IOException {
+    return dir.fileLength(name);
+  }
+
+  public long fileModified(String name) throws IOException {
+    return dir.fileModified(name);
+  }
+
+  public String[] list() throws IOException {
+    return dir.list();
+  }
+
+  public IndexInput openInput(String name) throws IOException {
+    IndexInput raw = dir.openInput(name);
+    if (filter.accept(null, name)) {
+      return new BufferPooledIndexInput(name, raw, pool);
+    } else {
+      return raw;
+    }
+  }
+
+  public IndexInput openInput(String name, int bufferSize) throws IOException {
+    IndexInput raw = dir.openInput(name, bufferSize);
+    if (filter.accept(null, name)) {
+      return new BufferPooledIndexInput(name, raw, pool);
+    } else {
+      return raw;
+    }
+  }
+
+  public void renameFile(String from, String to) throws IOException {
+    throw new UnsupportedOperationException();
+  }
+
+  public void sync(String name) throws IOException {
+    dir.sync(name);
+  }
+
+  public void touchFile(String name) throws IOException {
+    dir.touchFile(name);
+  }
+
+  public Lock makeLock(String name) {
+    throw new UnsupportedOperationException();
+  }
+
+  public void clearLock(String name) throws IOException {
+    dir.clearLock(name);
+  }
+
+  public void setLockFactory(LockFactory lockFactory) {
+    dir.setLockFactory(lockFactory);
+  }
+
+  public LockFactory getLockFactory() {
+    return dir.getLockFactory();
+  }
+
+  public String getLockID() {
+    return dir.getLockID();
+  }
+
+}
Index: contrib/caching/src/java/org/apache/lucene/store/BufferPool.java
===================================================================
--- contrib/caching/src/java/org/apache/lucene/store/BufferPool.java	(revision 0)
+++ contrib/caching/src/java/org/apache/lucene/store/BufferPool.java	(revision 0)
@@ -0,0 +1,41 @@
+package org.apache.lucene.store;
+
+import java.io.IOException;
+
+public interface BufferPool {
+
+  public static class Stats {
+    private long numReads;
+    private long numHits;
+
+    public void incNumReads() {
+      numReads++;
+    }
+
+    public void incNumHits() {
+      numHits++;
+    }
+
+    public long getNumReads() {
+      return numReads;
+    }
+
+    public long getNumHits() {
+      return numHits;
+    }
+
+    public double getHitRatio() {
+      return (double) numHits / numReads;
+    }
+  }
+
+  int getBufferSize();
+
+  Stats getStats();
+
+  void close() throws IOException;
+
+  void read(String name, long position, byte[] b, int offset, int len,
+      IndexInput input) throws IOException;
+
+}
Index: contrib/caching/src/java/org/apache/lucene/store/BufferPooledIndexInput.java
===================================================================
--- contrib/caching/src/java/org/apache/lucene/store/BufferPooledIndexInput.java	(revision 0)
+++ contrib/caching/src/java/org/apache/lucene/store/BufferPooledIndexInput.java	(revision 0)
@@ -0,0 +1,55 @@
+package org.apache.lucene.store;
+
+import java.io.IOException;
+
+class BufferPooledIndexInput extends BufferedIndexInput {
+  private String name;
+  private IndexInput input;
+  private BufferPool pool;
+  private long length;
+  private boolean closed;
+
+  BufferPooledIndexInput(String name, IndexInput input, BufferPool pool) {
+    this.name = name;
+    this.input = input;
+    this.pool = pool;
+    this.length = input.length();
+    this.closed = false;
+  }
+
+  IndexInput getInput() {
+    return input;
+  }
+
+  protected void readInternal(byte[] b, int offset, int len)
+      throws IOException {
+    long position = getFilePointer();
+    if (!closed && position + len <= length) {
+      pool.read(name, position, b, offset, len, input);
+    } else {
+      throw new IOException("read after close or read past EOF");
+    }
+  }
+
+  protected void seekInternal(long pos) throws IOException {
+  }
+
+  public long length() {
+    return length;
+  }
+
+  public void close() throws IOException {
+    input.close();
+    input = null;
+    pool = null;
+    closed = true;
+  }
+
+  public Object clone() {
+    BufferPooledIndexInput clone = (BufferPooledIndexInput) super.clone();
+    if (input != null) {
+      clone.input = (IndexInput) input.clone();
+    }
+    return clone;
+  }
+}
Index: contrib/caching/src/java/org/apache/lucene/store/BufferPoolLRU.java
===================================================================
--- contrib/caching/src/java/org/apache/lucene/store/BufferPoolLRU.java	(revision 0)
+++ contrib/caching/src/java/org/apache/lucene/store/BufferPoolLRU.java	(revision 0)
@@ -0,0 +1,241 @@
+package org.apache.lucene.store;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+public class BufferPoolLRU implements BufferPool {
+
+  static class BufferKey {
+    private String name; // file name in dir
+    private long position; // position in file
+    private int hashCode;
+
+    public BufferKey(String name, long position) {
+      set(name, position);
+    }
+
+    public void set(String name, long position) {
+      this.name = name;
+      this.position = position;
+      // the latter part is the hash code of long
+      this.hashCode =
+          ((name != null) ? name.hashCode() : 0)
+              ^ (int) (position ^ (position >>> 32));
+    }
+
+    public boolean equals(Object other) {
+      BufferKey otherKey = (BufferKey) other;
+      return position == otherKey.position && name.equals(otherKey.name);
+    }
+
+    public int hashCode() {
+      return hashCode;
+    }
+  }
+
+  static class Buffer {
+    BufferKey key;
+    final byte[] buffer;
+
+    Buffer prev;
+    Buffer next;
+
+    Buffer(int bufferSize) {
+      key = new BufferKey(null, -1);
+      buffer = new byte[bufferSize];
+    }
+  }
+
+  final private int bufferSize;
+
+  final private Buffer[] pool;
+  final private HashMap map; // map from (file, positione) to buffer
+
+  private Buffer head; // remove from head - least recently used
+  private Buffer tail; // add to tail - most recently used
+
+  private BufferKey checkKey = new BufferKey(null, -1);
+
+  private Stats stats = new Stats();
+
+  public BufferPoolLRU(int bufferSize, int numBuffers) {
+    this.bufferSize = bufferSize;
+
+    pool = new Buffer[numBuffers];
+    map = new HashMap();
+
+    for (int i = 0; i < numBuffers; i++) {
+      pool[i] = new Buffer(bufferSize);
+      if (i != 0) {
+        pool[i - 1].next = pool[i];
+        pool[i].prev = pool[i - 1];
+      }
+    }
+
+    head = pool[0];
+    tail = pool[numBuffers - 1];
+  }
+
+  public int getBufferSize() {
+    return bufferSize;
+  }
+
+  public Stats getStats() {
+    return stats;
+  }
+
+  public void close() throws IOException {
+    // set pool and map to null?
+
+    // verify that no buffers are lost
+    Buffer iter = head;
+    int count = iter != null ? 1 : 0;
+    while (iter != tail) {
+      iter = iter.next;
+      count++;
+    }
+    if (count != pool.length) {
+      throw new IOException("Buffer pool internal error: expected "
+          + pool.length + " buffers, actual " + count + " buffers");
+    }
+  }
+
+  // assume position + len <= input.length, i.e. no read past EOF
+  public void read(String name, long position, byte[] b, int offset, int len,
+      IndexInput input) throws IOException {
+
+    if (len > bufferSize) {
+      // read directly into b
+      readInput(input, position, b, offset, len);
+      return;
+    }
+
+    long quotient = position / bufferSize;
+    long bufferPos = quotient * bufferSize;
+    int remainder = (int) (position - bufferPos);
+
+    int oneLen = len; // length for this read, may need two reads
+    boolean more = false;
+    if (len > bufferSize - remainder) {
+      oneLen = bufferSize - remainder;
+      more = true;
+    }
+
+    Buffer buffer = null;
+    boolean success = false;
+
+    synchronized (this) {
+      stats.incNumReads();
+      checkKey.set(name, bufferPos);
+      buffer = (Buffer) map.get(checkKey);
+
+      if (buffer != null) {
+        System.arraycopy(buffer.buffer, remainder, b, offset, oneLen);
+        success = true;
+        stats.incNumHits();
+
+        remove(buffer);
+        addTail(buffer);
+      }
+    }
+
+    if (!success) {
+      synchronized (this) {
+        buffer = removeHead();
+        if (buffer != null) {
+          map.remove(buffer.key);
+        }
+      }
+
+      if (buffer != null) {
+        try {
+          // read into buffer
+          readInput(input, bufferPos, buffer.buffer, 0, (bufferPos
+              + bufferSize <= input.length()) ? bufferSize
+              : (int) (input.length() - bufferPos));
+          buffer.key.set(name, bufferPos);
+        } catch (IOException e) {
+          addHead(buffer);
+          throw e;
+        }
+
+        System.arraycopy(buffer.buffer, remainder, b, offset, oneLen);
+
+        synchronized (this) {
+          map.put(buffer.key, buffer);
+          addTail(buffer);
+        }
+      } else {
+        // no more buffer, read directly into b
+        readInput(input, position, b, offset, len);
+        more = false;
+      }
+    }
+
+    if (more) {
+      read(name, position + oneLen, b, offset + oneLen, len - oneLen, input);
+    }
+  }
+
+  protected void readInput(IndexInput input, long position, byte[] b,
+      int offset, int len) throws IOException {
+    if (position != input.getFilePointer()) {
+      input.seek(position);
+    }
+    // set useBuffer to false when using buffer pool
+    input.readBytes(b, offset, len, false);
+  }
+
+  // remove buffer from doubly linked list
+  private void remove(Buffer buffer) {
+    // head != null, tail != null
+    if (buffer.prev != null) { // buffer != head
+      buffer.prev.next = buffer.next;
+    } else {
+      head = buffer.next;
+    }
+    if (buffer.next != null) { // buffer != tail
+      buffer.next.prev = buffer.prev;
+    } else {
+      tail = buffer.prev;
+    }
+    buffer.prev = null;
+    buffer.next = null;
+  }
+
+  // remove from head - LRU
+  private Buffer removeHead() {
+    Buffer buffer = head;
+    if (head != null) {
+      remove(head);
+    }
+    return buffer;
+  }
+
+  // add to tail - MRU
+  private void addTail(Buffer buffer) {
+    if (head == null) { // tail == null
+      head = tail = buffer;
+      // buffer.prev = buffer.next = null;
+    } else {
+      buffer.prev = tail;
+      // buffer.next = null;
+      tail.next = buffer;
+      tail = buffer;
+    }
+  }
+
+  // add to head - LRU
+  private void addHead(Buffer buffer) {
+    if (head == null) { // tail == null
+      head = tail = buffer;
+      // buffer.prev = buffer.next = null;
+    } else {
+      // buffer.prev = null;
+      buffer.next = head;
+      head.prev = buffer;
+      head = buffer;
+    }
+  }
+
+}
Index: contrib/caching/build.xml
===================================================================
--- contrib/caching/build.xml	(revision 0)
+++ contrib/caching/build.xml	(revision 0)
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+
+<project name="caching" default="default">
+
+  <description>
+    A caching layer for reads. Create your own FilenameFilter
+    to decide which files you want to use the cache for.
+  </description>
+
+  <import file="../contrib-build.xml"/>
+
+</project>
