Push open/closed subindex into Lucene implementation

Storing open/closed changes in separate indexes is an optimization
specific to the Lucene implementation. Other implementations may want
to do similar splitting, but the unit of splitting is not necessarily
a fully self-contained index.

Leave getPossibleStatus in IndexRewriteImpl, since it may be useful
for other implementations' internal optimizations as well.

Change-Id: Ib2b69f2eb0b1b9246390b9c6c8bc054a9e60836e
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/IndexVersionCheck.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/IndexVersionCheck.java
index 4fe6def..ab38562 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/IndexVersionCheck.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/IndexVersionCheck.java
@@ -36,8 +36,8 @@
 
 public class IndexVersionCheck implements LifecycleListener {
   public static final Map<String, Integer> SCHEMA_VERSIONS = ImmutableMap.of(
-      "changes_open", ChangeField.SCHEMA_VERSION,
-      "changes_closed", ChangeField.SCHEMA_VERSION);
+      LuceneChangeIndex.CHANGES_OPEN, ChangeField.SCHEMA_VERSION,
+      LuceneChangeIndex.CHANGES_CLOSED, ChangeField.SCHEMA_VERSION);
 
   public static File gerritIndexConfig(SitePaths sitePaths) {
     return new File(sitePaths.index_dir, "gerrit_index.config");
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index ce54b35..f67a76f 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -15,13 +15,18 @@
 package com.google.gerrit.lucene;
 
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_CHANGE;
+import static com.google.gerrit.server.query.change.IndexRewriteImpl.CLOSED_STATUSES;
+import static com.google.gerrit.server.query.change.IndexRewriteImpl.OPEN_STATUSES;
 
 import static org.apache.lucene.search.BooleanClause.Occur.MUST;
 import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT;
 import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
 
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.FieldDef;
@@ -35,38 +40,33 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.IndexRewriteImpl;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
-import org.apache.lucene.analysis.standard.StandardAnalyzer;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
 import org.apache.lucene.document.IntField;
 import org.apache.lucene.document.StringField;
 import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.IndexWriterConfig;
-import org.apache.lucene.index.IndexWriterConfig.OpenMode;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.SearcherManager;
 import org.apache.lucene.search.TermQuery;
-import org.apache.lucene.store.Directory;
-import org.apache.lucene.store.FSDirectory;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.NumericUtils;
 import org.apache.lucene.util.Version;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Secondary index implementation using Apache Lucene.
@@ -76,81 +76,86 @@
  * though there may be some lag between a committed write and it showing up to
  * other threads' searchers.
  */
-public class LuceneChangeIndex implements ChangeIndex {
-  private static final Logger log =
-      LoggerFactory.getLogger(LuceneChangeIndex.class);
-
+@Singleton
+public class LuceneChangeIndex implements ChangeIndex, LifecycleListener {
   public static final Version LUCENE_VERSION = Version.LUCENE_43;
+  public static final String CHANGES_OPEN = "changes_open";
+  public static final String CHANGES_CLOSED = "changes_closed";
 
   private final FillArgs fillArgs;
-  private final Directory dir;
-  private final IndexWriter writer;
-  private final SearcherManager searcherManager;
+  private final SubIndex openIndex;
+  private final SubIndex closedIndex;
 
-  LuceneChangeIndex(File file, FillArgs fillArgs) throws IOException {
+  @Inject
+  LuceneChangeIndex(SitePaths sitePaths, FillArgs fillArgs) throws IOException {
     this.fillArgs = fillArgs;
-    dir = FSDirectory.open(file);
-    IndexWriterConfig writerConfig =
-        new IndexWriterConfig(LUCENE_VERSION, new StandardAnalyzer(LUCENE_VERSION));
-    writerConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
-    writer = new IndexWriter(dir, writerConfig);
-    searcherManager = new SearcherManager(writer, true, null);
+    openIndex = new SubIndex(new File(sitePaths.index_dir, CHANGES_OPEN));
+    closedIndex = new SubIndex(new File(sitePaths.index_dir, CHANGES_CLOSED));
   }
 
-  void close() {
-    try {
-      searcherManager.close();
-    } catch (IOException e) {
-      log.warn("error closing Lucene searcher", e);
-    }
-    try {
-      writer.close(true);
-    } catch (IOException e) {
-      log.warn("error closing Lucene writer", e);
-    }
-    try {
-      dir.close();
-    } catch (IOException e) {
-      log.warn("error closing Lucene directory", e);
-    }
+  @Override
+  public void start() {
+    // Do nothing.
+  }
+
+  @Override
+  public void stop() {
+    openIndex.close();
+    closedIndex.close();
   }
 
   @Override
   public void insert(ChangeData cd) throws IOException {
-    writer.addDocument(toDocument(cd));
-    commit();
+    Term id = idTerm(cd);
+    Document doc = toDocument(cd);
+    if (cd.getChange().getStatus().isOpen()) {
+      closedIndex.delete(id);
+      openIndex.insert(doc);
+    } else {
+      openIndex.delete(id);
+      closedIndex.insert(doc);
+    }
   }
 
   @Override
   public void replace(ChangeData cd) throws IOException {
-    writer.updateDocument(intTerm(FIELD_CHANGE, cd.getId().get()),
-        toDocument(cd));
-    commit();
+    Term id = idTerm(cd);
+    Document doc = toDocument(cd);
+    if (cd.getChange().getStatus().isOpen()) {
+      closedIndex.delete(id);
+      openIndex.replace(id, doc);
+    } else {
+      openIndex.delete(id);
+      closedIndex.replace(id, doc);
+    }
   }
 
   @Override
   public void delete(ChangeData cd) throws IOException {
-    writer.deleteDocuments(intTerm(FIELD_CHANGE, cd.getId().get()));
-    commit();
+    Term id = idTerm(cd);
+    if (cd.getChange().getStatus().isOpen()) {
+      openIndex.delete(id);
+    } else {
+      closedIndex.delete(id);
+    }
   }
 
   @Override
   public ChangeDataSource getSource(Predicate<ChangeData> p)
       throws QueryParseException {
-    return new QuerySource(toQuery(p));
+    Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
+    List<SubIndex> indexes = Lists.newArrayListWithCapacity(2);
+    if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
+      indexes.add(openIndex);
+    }
+    if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+      indexes.add(closedIndex);
+    }
+    return new QuerySource(indexes, toQuery(p));
   }
 
-  public Directory getDirectory() {
-    return dir;
-  }
-
-  public IndexWriter getWriter() {
-    return writer;
-  }
-
-  private void commit() throws IOException {
-    writer.commit();
-    searcherManager.maybeRefresh();
+  private Term idTerm(ChangeData cd) {
+    return intTerm(FIELD_CHANGE, cd.getId().get());
   }
 
   private Query toQuery(Predicate<ChangeData> p) throws QueryParseException {
@@ -214,9 +219,11 @@
     // TODO(dborowitz): Push limit down from predicate tree.
     private static final int LIMIT = 1000;
 
+    private final List<SubIndex> indexes;
     private final Query query;
 
-    public QuerySource(Query query) {
+    public QuerySource(List<SubIndex> indexes, Query query) {
+      this.indexes = indexes;
       this.query = query;
     }
 
@@ -233,36 +240,28 @@
     @Override
     public ResultSet<ChangeData> read() throws OrmException {
       try {
-        IndexSearcher searcher = searcherManager.acquire();
-        try {
-          ScoreDoc[] docs = searcher.search(query, LIMIT).scoreDocs;
-          List<ChangeData> result = Lists.newArrayListWithCapacity(docs.length);
-          for (ScoreDoc sd : docs) {
-            Document doc = searcher.doc(sd.doc);
-            Number v = doc.getField(FIELD_CHANGE).numericValue();
-            result.add(new ChangeData(new Change.Id(v.intValue())));
-          }
-          final List<ChangeData> r = Collections.unmodifiableList(result);
-
-          return new ResultSet<ChangeData>() {
-            @Override
-            public Iterator<ChangeData> iterator() {
-              return r.iterator();
-            }
-
-            @Override
-            public List<ChangeData> toList() {
-              return r;
-            }
-
-            @Override
-            public void close() {
-              // Do nothing.
-            }
-          };
-        } finally {
-          searcherManager.release(searcher);
+        List<ChangeData> result =
+            Lists.newArrayListWithExpectedSize(2 * getCardinality());
+        for (SubIndex index : indexes) {
+          result.addAll(index.search(query, LIMIT));
         }
+        final List<ChangeData> r = Collections.unmodifiableList(result);
+        return new ResultSet<ChangeData>() {
+          @Override
+          public Iterator<ChangeData> iterator() {
+            return r.iterator();
+          }
+
+          @Override
+          public List<ChangeData> toList() {
+            return r;
+          }
+
+          @Override
+          public void close() {
+            // Do nothing.
+          }
+        };
       } catch (IOException e) {
         throw new OrmException(e);
       }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndexManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndexManager.java
deleted file mode 100644
index 1681914..0000000
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndexManager.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed 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 com.google.gerrit.server.git;
-
-package com.google.gerrit.lucene;
-
-import com.google.common.base.Throwables;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.FieldDef.FillArgs;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
-
-@Singleton
-class LuceneChangeIndexManager implements ChangeIndex.Manager,
-    LifecycleListener {
-  private final LoadingCache<String, LuceneChangeIndex> indexes;
-
-  @Inject
-  LuceneChangeIndexManager(final SitePaths sitePaths, final FillArgs fillArgs) {
-    indexes = CacheBuilder.newBuilder().build(
-      new CacheLoader<String, LuceneChangeIndex>() {
-        @Override
-        public LuceneChangeIndex load(String key) throws IOException {
-          return new LuceneChangeIndex(
-              new File(sitePaths.index_dir, key), fillArgs);
-        }
-      });
-  }
-
-  @Override
-  public void start() {
-    // Do nothing.
-  }
-
-  @Override
-  public void stop() {
-    for (LuceneChangeIndex index : indexes.asMap().values()) {
-      index.close();
-    }
-  }
-
-  @Override
-  public LuceneChangeIndex get(String name) throws IOException {
-    try {
-      return indexes.get(name);
-    } catch (ExecutionException e) {
-      Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
-      throw new IOException(e);
-    }
-  }
-}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index bb92728..440d874 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -34,8 +34,8 @@
   @Override
   protected void configure() {
     install(new IndexModule(threads));
-    bind(ChangeIndex.Manager.class).to(LuceneChangeIndexManager.class);
-    listener().to(LuceneChangeIndexManager.class);
+    bind(ChangeIndex.class).to(LuceneChangeIndex.class);
+    listener().to(LuceneChangeIndex.class);
     if (checkVersion) {
       listener().to(IndexVersionCheck.class);
     }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
new file mode 100644
index 0000000..0a48519
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.server.git;
+
+package com.google.gerrit.lucene;
+
+import static com.google.gerrit.lucene.LuceneChangeIndex.LUCENE_VERSION;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_CHANGE;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.change.ChangeData;
+
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.SearcherManager;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+/** Piece of the change index that is implemented as a separate Lucene index. */
+class SubIndex {
+  private static final Logger log =
+      LoggerFactory.getLogger(LuceneChangeIndex.class);
+
+  private final Directory dir;
+  private final IndexWriter writer;
+  private final SearcherManager searcherManager;
+
+  SubIndex(File file) throws IOException {
+    dir = FSDirectory.open(file);
+    IndexWriterConfig writerConfig =
+        new IndexWriterConfig(LUCENE_VERSION, new StandardAnalyzer(LUCENE_VERSION));
+    writerConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
+    writer = new IndexWriter(dir, writerConfig);
+    searcherManager = new SearcherManager(writer, true, null);
+  }
+
+  void close() {
+    try {
+      searcherManager.close();
+    } catch (IOException e) {
+      log.warn("error closing Lucene searcher", e);
+    }
+    try {
+      writer.close(true);
+    } catch (IOException e) {
+      log.warn("error closing Lucene writer", e);
+    }
+    try {
+      dir.close();
+    } catch (IOException e) {
+      log.warn("error closing Lucene directory", e);
+    }
+  }
+
+  void insert(Document doc) throws IOException {
+    writer.addDocument(doc);
+    commit();
+  }
+
+  void replace(Term term, Document doc) throws IOException {
+    writer.updateDocument(term, doc);
+    commit();
+  }
+
+  void delete(Term term) throws IOException {
+    writer.deleteDocuments(term);
+    commit();
+  }
+
+  List<ChangeData> search(Query query, int limit) throws IOException {
+    IndexSearcher searcher = searcherManager.acquire();
+    try {
+      ScoreDoc[] docs = searcher.search(query, limit).scoreDocs;
+      List<ChangeData> result = Lists.newArrayListWithCapacity(docs.length);
+      for (ScoreDoc sd : docs) {
+        Document doc = searcher.doc(sd.doc);
+        Number v = doc.getField(FIELD_CHANGE).numericValue();
+        result.add(new ChangeData(new Change.Id(v.intValue())));
+      }
+      return Collections.unmodifiableList(result);
+    } finally {
+      searcherManager.release(searcher);
+    }
+  }
+
+  private void commit() throws IOException {
+    writer.commit();
+    searcherManager.maybeRefresh();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
index da2e451..aabe771 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -32,38 +32,29 @@
  * appropriate.
  */
 public interface ChangeIndex {
-  public static interface Manager {
-    /** Instance indicating secondary index is disabled. */
-    public static final Manager DISABLED = new Manager() {
-      @Override
-      public ChangeIndex get(String name) throws IOException {
-        return new ChangeIndex() {
-          @Override
-          public void insert(ChangeData cd) throws IOException {
-            // Do nothing.
-          }
+  /** Instance indicating secondary index is disabled. */
+  public static final ChangeIndex DISABLED = new ChangeIndex() {
+    @Override
+    public void insert(ChangeData cd) throws IOException {
+      // Do nothing.
+    }
 
-          @Override
-          public void replace(ChangeData cd) throws IOException {
-            // Do nothing.
-          }
+    @Override
+    public void replace(ChangeData cd) throws IOException {
+      // Do nothing.
+    }
 
-          @Override
-          public void delete(ChangeData cd) throws IOException {
-            // Do nothing.
-          }
+    @Override
+    public void delete(ChangeData cd) throws IOException {
+      // Do nothing.
+    }
 
-          @Override
-          public ChangeDataSource getSource(Predicate<ChangeData> p)
-              throws QueryParseException {
-            throw new UnsupportedOperationException();
-          }
-        };
-      }
-    };
-
-    ChangeIndex get(String name) throws IOException;
-  }
+    @Override
+    public ChangeDataSource getSource(Predicate<ChangeData> p)
+        throws QueryParseException {
+      throw new UnsupportedOperationException();
+    }
+  };
 
   /**
    * Insert a change document into the index.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
index 7a9e869..2e4dee0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
@@ -37,15 +37,13 @@
       LoggerFactory.getLogger(ChangeIndexerImpl.class);
 
   private final ListeningScheduledExecutorService executor;
-  private final ChangeIndex openIndex;
-  private final ChangeIndex closedIndex;
+  private final ChangeIndex index;
 
   @Inject
   ChangeIndexerImpl(@IndexExecutor ListeningScheduledExecutorService executor,
-      ChangeIndex.Manager indexManager) throws IOException {
+      ChangeIndex index) throws IOException {
     this.executor = executor;
-    this.openIndex = indexManager.get("changes_open");
-    this.closedIndex = indexManager.get("changes_closed");
+    this.index = index;
   }
 
   @Override
@@ -74,13 +72,7 @@
     public void run() {
       ChangeData cd = new ChangeData(change);
       try {
-        if (change.getStatus().isOpen()) {
-          closedIndex.delete(cd);
-          openIndex.replace(cd);
-        } else {
-          openIndex.delete(cd);
-          closedIndex.replace(cd);
-        }
+        index.replace(cd);
       } catch (IOException e) {
         log.error("Error indexing change", e);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
index fb7fbe6..1c4ffa8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
@@ -24,7 +24,7 @@
 
   @Override
   protected void configure() {
-    bind(ChangeIndex.Manager.class).toInstance(ChangeIndex.Manager.DISABLED);
+    bind(ChangeIndex.class).toInstance(ChangeIndex.DISABLED);
     bind(ChangeIndexer.class).toInstance(ChangeIndexer.DISABLED);
     bind(IndexRewrite.class).toInstance(IndexRewrite.DISABLED);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java
index f635da6..1cddf82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.server.index;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -25,9 +22,6 @@
 import com.google.gwtorm.server.ResultSet;
 
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
 
 /**
  * Wrapper combining an {@link IndexPredicate} together with a
@@ -40,68 +34,27 @@
 public class PredicateWrapper extends Predicate<ChangeData> implements
     ChangeDataSource {
   private final Predicate<ChangeData> pred;
-  private final List<ChangeDataSource> sources;
+  private final ChangeDataSource source;
 
-  public PredicateWrapper(Predicate<ChangeData> pred, ChangeIndex index)
+  public PredicateWrapper(ChangeIndex index, Predicate<ChangeData> pred)
       throws QueryParseException {
-    this(pred, ImmutableList.of(index));
-  }
-
-  public PredicateWrapper(Predicate<ChangeData> pred,
-      Collection<ChangeIndex> indexes) throws QueryParseException {
     this.pred = pred;
-    sources = Lists.newArrayListWithCapacity(indexes.size());
-    for (ChangeIndex index : indexes) {
-      sources.add(index.getSource(pred));
-    }
+    this.source = index.getSource(pred);
   }
 
   @Override
   public int getCardinality() {
-    int n = 0;
-    for (ChangeDataSource source : sources) {
-      n += source.getCardinality();
-    }
-    return n;
+    return source.getCardinality();
   }
 
   @Override
   public boolean hasChange() {
-    for (ChangeDataSource source : sources) {
-      if (!source.hasChange()) {
-        return false;
-      }
-    }
-    return true;
+    return source.hasChange();
   }
 
   @Override
   public ResultSet<ChangeData> read() throws OrmException {
-    final List<ResultSet<ChangeData>> results =
-        Lists.newArrayListWithCapacity(sources.size());
-    for (ChangeDataSource source : sources) {
-      results.add(source.read());
-    }
-    return new ResultSet<ChangeData>() {
-      @Override
-      public Iterator<ChangeData> iterator() {
-        // TODO(dborowitz): May return duplicates since moving a document
-        // between indexes is not atomic.
-        return Iterables.concat(results).iterator();
-      }
-
-      @Override
-      public List<ChangeData> toList() {
-        return Collections.unmodifiableList(Lists.newArrayList(iterator()));
-      }
-
-      @Override
-      public void close() {
-        for (ResultSet<ChangeData> rs : results) {
-          rs.close();
-        }
-      }
-    };
+    return source.read();
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index d220b83..69207ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -114,7 +114,7 @@
     final PatchListCache patchListCache;
     final GitRepositoryManager repoManager;
     final ProjectCache projectCache;
-    final ChangeIndex.Manager indexManager;
+    final ChangeIndex index;
 
     @Inject
     Arguments(Provider<ReviewDb> dbProvider,
@@ -128,7 +128,7 @@
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
         ProjectCache projectCache,
-        ChangeIndex.Manager indexManager) {
+        ChangeIndex index) {
       this.dbProvider = dbProvider;
       this.rewriter = rewriter;
       this.userFactory = userFactory;
@@ -140,7 +140,7 @@
       this.patchListCache = patchListCache;
       this.repoManager = repoManager;
       this.projectCache = projectCache;
-      this.indexManager = indexManager;
+      this.index = index;
     }
   }
 
@@ -298,7 +298,7 @@
       if (file.startsWith("^")) {
         throw error("regular expression not permitted here: file:" + file);
       }
-      if (args.indexManager == ChangeIndex.Manager.DISABLED) {
+      if (args.index == ChangeIndex.DISABLED) {
         throw error("secondary index must be enabled for file:" + file);
       }
       return new EqualsFilePredicate(args.dbProvider, args.patchListCache, file);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java
index 48f2801..6667054 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Change;
@@ -28,7 +27,6 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.inject.Inject;
 
-import java.io.IOException;
 import java.util.BitSet;
 import java.util.EnumSet;
 import java.util.List;
@@ -36,8 +34,11 @@
 
 /** Rewriter that pushes boolean logic into the secondary index. */
 public class IndexRewriteImpl implements IndexRewrite {
-  private static final Set<Change.Status> OPEN_STATUSES;
-  private static final Set<Change.Status> CLOSED_STATUSES;
+  /** Set of all open change statuses. */
+  public static final Set<Change.Status> OPEN_STATUSES;
+
+  /** Set of all closed change statuses. */
+  public static final Set<Change.Status> CLOSED_STATUSES;
 
   static {
     EnumSet<Change.Status> open = EnumSet.noneOf(Change.Status.class);
@@ -53,18 +54,44 @@
     CLOSED_STATUSES = Sets.immutableEnumSet(closed);
   }
 
-  private final ChangeIndex openIndex;
-  private final ChangeIndex closedIndex;
-
-  @Inject
-  IndexRewriteImpl(ChangeIndex.Manager indexManager) throws IOException {
-    this(indexManager.get("changes_open"), indexManager.get("changes_closed"));
+  /**
+   * Get the set of statuses that changes matching the given predicate may have.
+   *
+   * @param in predicate
+   * @return the maximal set of statuses that any changes matching the input
+   *     predicates may have, based on examining boolean and
+   *     {@link ChangeStatusPredicate}s.
+   */
+  public static EnumSet<Change.Status> getPossibleStatus(Predicate<ChangeData> in) {
+    if (in instanceof ChangeStatusPredicate) {
+      return EnumSet.of(((ChangeStatusPredicate) in).getStatus());
+    } else if (in.getClass() == NotPredicate.class) {
+      return EnumSet.complementOf(getPossibleStatus(in.getChild(0)));
+    } else if (in.getClass() == OrPredicate.class) {
+      EnumSet<Change.Status> s = EnumSet.noneOf(Change.Status.class);
+      for (int i = 0; i < in.getChildCount(); i++) {
+        s.addAll(getPossibleStatus(in.getChild(i)));
+      }
+      return s;
+    } else if (in.getClass() == AndPredicate.class) {
+      EnumSet<Change.Status> s = EnumSet.allOf(Change.Status.class);
+      for (int i = 0; i < in.getChildCount(); i++) {
+        s.retainAll(getPossibleStatus(in.getChild(i)));
+      }
+      return s;
+    } else if (in.getChildCount() == 0) {
+      return EnumSet.allOf(Change.Status.class);
+    } else {
+      throw new IllegalStateException(
+          "Invalid predicate type in change index query: " + in.getClass());
+    }
   }
 
-  @VisibleForTesting
-  IndexRewriteImpl(ChangeIndex openIndex, ChangeIndex closedIndex) {
-    this.openIndex = openIndex;
-    this.closedIndex = closedIndex;
+  private final ChangeIndex index;
+
+  @Inject
+  IndexRewriteImpl(ChangeIndex index) {
+    this.index = index;
   }
 
   @Override
@@ -181,43 +208,9 @@
     }
   }
 
-  @VisibleForTesting
-  static EnumSet<Change.Status> getPossibleStatus(Predicate<ChangeData> in) {
-    if (in instanceof ChangeStatusPredicate) {
-      return EnumSet.of(((ChangeStatusPredicate) in).getStatus());
-    } else if (in.getClass() == NotPredicate.class) {
-      return EnumSet.complementOf(getPossibleStatus(in.getChild(0)));
-    } else if (in.getClass() == OrPredicate.class) {
-      EnumSet<Change.Status> s = EnumSet.noneOf(Change.Status.class);
-      for (int i = 0; i < in.getChildCount(); i++) {
-        s.addAll(getPossibleStatus(in.getChild(i)));
-      }
-      return s;
-    } else if (in.getClass() == AndPredicate.class) {
-      EnumSet<Change.Status> s = EnumSet.allOf(Change.Status.class);
-      for (int i = 0; i < in.getChildCount(); i++) {
-        s.retainAll(getPossibleStatus(in.getChild(i)));
-      }
-      return s;
-    } else if (in.getChildCount() == 0) {
-      return EnumSet.allOf(Change.Status.class);
-    } else {
-      throw new IllegalStateException(
-          "Invalid predicate type in change index query: " + in.getClass());
-    }
-  }
-
   private PredicateWrapper wrap(Predicate<ChangeData> p) {
     try {
-      Set<Change.Status> possibleStatus = getPossibleStatus(p);
-      List<ChangeIndex> indexes = Lists.newArrayListWithCapacity(2);
-      if (!Sets.intersection(possibleStatus, OPEN_STATUSES).isEmpty()) {
-        indexes.add(openIndex);
-      }
-      if (!Sets.intersection(possibleStatus, CLOSED_STATUSES).isEmpty()) {
-        indexes.add(closedIndex);
-      }
-      return new PredicateWrapper(p, indexes);
+      return new PredicateWrapper(index, p);
     } catch (QueryParseException e) {
       throw new IllegalStateException(
           "Failed to convert " + p + " to index predicate", e);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java
index 877d3d6..d7f1a60 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java
@@ -79,22 +79,19 @@
     }
   }
 
-  private DummyIndex openIndex;
-  private DummyIndex closedIndex;
+  private DummyIndex index;
   private ChangeQueryBuilder queryBuilder;
   private IndexRewrite rewrite;
 
   @Override
   public void setUp() throws Exception {
     super.setUp();
-    openIndex = new DummyIndex();
-    closedIndex = new DummyIndex();
+    index = new DummyIndex();
     queryBuilder = new ChangeQueryBuilder(
         new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
             null, null, null, null, null, null),
         null);
-
-    rewrite = new IndexRewriteImpl(openIndex, closedIndex);
+    rewrite = new IndexRewriteImpl(index);
   }
 
   public void testIndexPredicate() throws Exception {
@@ -208,7 +205,7 @@
 
   private PredicateWrapper wrap(Predicate<ChangeData> p)
       throws QueryParseException {
-    return new PredicateWrapper(p, openIndex);
+    return new PredicateWrapper(index, p);
   }
 
   private Set<Change.Status> status(String query) throws QueryParseException {