Merge "Document how to send file content without conversion using curl"
diff --git a/.buckversion b/.buckversion
index 582e61d..2c4c008 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-b01f03b82e23614965c2ddc13900ccc6dc4d8361
+410fcf3420cb06e62cbe9ee93eff931fa9a9b1a2
diff --git a/.gitignore b/.gitignore
index 8411c8b..8e92321 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,8 @@
 /gerrit-parent.iml
 *.sublime-*
 /gerrit-package-plugins
+/.buckconfig.local
+/.buckd
+/buck-cache
 /buck-out
 /local.properties
diff --git a/BUCK b/BUCK
index d467528..ee5ba75 100644
--- a/BUCK
+++ b/BUCK
@@ -8,11 +8,13 @@
 
 genrule(
   name = 'api',
-  cmd = 'echo',
+  cmd = '',
   srcs = [],
   deps = [
     ':extension-api',
+    ':extension-api-src',
     ':plugin-api',
+    ':plugin-api-src',
   ],
   out = '__fake.api__',
 )
@@ -29,18 +31,33 @@
   export_deps = True,
   visibility = ['PUBLIC'],
 )
+genrule(
+  name = 'extension-api-src',
+  cmd = 'ln -s $DEPS $OUT',
+  srcs = [],
+  deps = ['//gerrit-extension-api:api-src'],
+  out = 'extension-api-src.jar',
+)
+
+PLUGIN_API = [
+  '//gerrit-server:server',
+  '//gerrit-sshd:sshd',
+  '//gerrit-httpd:httpd',
+]
 
 java_binary(name = 'plugin-api', deps = [':plugin-lib'])
 java_library(
   name = 'plugin-lib',
-  deps = [
-    '//gerrit-server:server',
-    '//gerrit-sshd:sshd',
-    '//gerrit-httpd:httpd',
-  ],
+  deps = PLUGIN_API,
   export_deps = True,
   visibility = ['PUBLIC'],
 )
+java_binary(
+  name = 'plugin-api-src',
+  deps = [
+    '//gerrit-extension-api:api-src',
+  ] + [d + '-src' for d in PLUGIN_API],
+)
 
 genrule(
   name = 'download',
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index c6df544..389e0ca 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -46,7 +46,7 @@
     )
   genrule(
     name = name,
-    cmd = '',
+    cmd = ':>$OUT',
     srcs = [],
     deps = [':' + o for o in outs],
     out = name + '__done',
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index f5bdbea..329459d 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -216,6 +216,23 @@
 repository has precedence.
 
 
+Caching Build Results
+~~~~~~~~~~~~~~~~~~~~~
+
+Build results can be locally cached, saving rebuild time when
+switching between Git branches. Buck's documentation covers
+caching in link:http://facebook.github.io/buck/concept/buckconfig.html[buckconfig].
+The trivial case using a local directory is:
+
+----
+  cat >.buckconfig.local <<EOF
+  [cache]
+    mode = dir
+    dir = buck-cache
+  EOF
+----
+
+
 Build Process Switch Exit Criteria
 ----------------------------------
 
diff --git a/gerrit-antlr/BUCK b/gerrit-antlr/BUCK
index 0e19320..2071656 100644
--- a/gerrit-antlr/BUCK
+++ b/gerrit-antlr/BUCK
@@ -3,7 +3,6 @@
   'QueryParser.java',
 ]
 PARSER_DEPS = [
-  ':query_antlr',
   ':query_exception',
   '//lib/antlr:java_runtime',
 ]
@@ -24,7 +23,7 @@
 java_library(
   name = 'lib',
   srcs = [genfile(f) for f in ANTLR_OUTS],
-  deps = PARSER_DEPS,
+  deps = PARSER_DEPS + [':' + f for f in ANTLR_OUTS],
 )
 
 genrule(
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 75539f6..4145636 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -1,6 +1,14 @@
+SRCS = glob(['src/main/java/com/google/gerrit/extensions/**/*.java'])
+
 java_library2(
   name = 'api',
-  srcs = glob(['src/main/java/com/google/gerrit/extensions/**/*.java']),
+  srcs = SRCS,
   compile_deps = ['//lib/guice:guice'],
   visibility = ['PUBLIC'],
 )
+
+java_sources(
+  name = 'api-src',
+  srcs = SRCS,
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index ec34887..b2f19e5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -26,6 +26,7 @@
 import com.google.inject.util.Types;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -127,6 +128,11 @@
     return binder.bind(type).annotatedWith(name);
   }
 
+  public static <T> DynamicSet<T> emptySet() {
+    return new DynamicSet<T>(
+        Collections.<AtomicReference<Provider<T>>> emptySet());
+  }
+
   private final CopyOnWriteArrayList<AtomicReference<Provider<T>>> items;
 
   DynamicSet(Collection<AtomicReference<Provider<T>>> base) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java
index 70b9ff9..a68b7a2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.event.logical.shared.ResizeEvent;
 import com.google.gwt.event.logical.shared.ResizeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
@@ -31,7 +32,6 @@
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.LineClassWhere;
 import net.codemirror.lib.Configuration;
-import net.codemirror.lib.LineCharacter;
 import net.codemirror.lib.ModeInjector;
 
 public class CodeMirrorDemo extends Screen {
@@ -140,7 +140,7 @@
   private CodeMirror displaySide(DiffInfo.FileMeta meta, String contents,
       Element ele) {
     if (meta == null) {
-      return null; // TODO: Handle empty contents
+      contents = "";
     }
     Configuration cfg = Configuration.create()
       .set("readOnly", true)
@@ -155,20 +155,8 @@
     return cm;
   }
 
-  private void addPadding(CodeMirror cm, int line) {
-    Element div = DOM.createDiv();
-    div.setClassName(diffTable.style.padding());
-    cm.addLineWidget(line, div, null);
-  }
-
   private void render(DiffInfo diff) {
     JsArray<Region> regions = diff.content();
-    Configuration insertOpt = Configuration.create()
-        .set("className", diffTable.style.insert())
-        .set("readOnly", true);
-    Configuration deleteOpt = Configuration.create()
-        .set("className", diffTable.style.delete())
-        .set("readOnly", true);
     int lineA = 0, lineB = 0;
     for (int i = 0; i < regions.length(); i++) {
       Region current = regions.get(i);
@@ -177,35 +165,43 @@
         lineB += current.ab().length();
       } else if (current.a() == null && current.b() != null) {
         int delta = current.b().length();
-        for (int j = 0; j < delta; j++) {
-          addPadding(cmA, lineA - 1);
-        }
-        for (int j = 0; j < delta; j++) {
-          cmB.addLineClass(lineB, LineClassWhere.WRAP,
-              diffTable.style.insert());
-          LineCharacter from = LineCharacter.create(lineB, 0);
-          cmB.markText(from, from, insertOpt);
-          lineB++;
-        }
+        insertEmptyLines(cmA, lineA, delta);
+        lineB = colorLines(cmB, lineB, delta);
       } else if (current.a() != null && current.b() == null) {
         int delta = current.a().length();
-        for (int j = 0; j < delta; j++) {
-          addPadding(cmB, lineB - 1);
+        insertEmptyLines(cmB, lineB, delta);
+        lineA = colorLines(cmA, lineA, delta);
+      } else { // TODO: Implement intraline
+        int aLength = current.a().length();
+        int bLength = current.b().length();
+        lineA = colorLines(cmA, lineA, aLength);
+        lineB = colorLines(cmB, lineB, bLength);
+        if (aLength < bLength) {
+          insertEmptyLines(cmA, lineA, bLength - aLength);
+        } else if (aLength > bLength) {
+          insertEmptyLines(cmB, lineB, aLength - bLength);
         }
-        for (int j = 0; j < delta; j++) {
-          cmA.addLineClass(lineA, LineClassWhere.WRAP,
-              diffTable.style.delete());
-          LineCharacter from = LineCharacter.create(lineA, 0);
-          cmA.markText(from, from, deleteOpt);
-          lineA++;
-        }
-      } else { // TODO: Handle intraline edit.
-        lineA += current.a().length();
-        lineB += current.a().length();
       }
     }
   }
 
+  private void insertEmptyLines(CodeMirror cm, int line, int cnt) {
+    Element div = DOM.createDiv();
+    div.setClassName(diffTable.style.padding());
+    div.getStyle().setHeight(cnt, Unit.EM);
+    Configuration config = Configuration.create()
+        .set("coverGutter", true)
+        .set("above", line == 0);
+    cm.addLineWidget(line == 0 ? 0 : (line - 1), div, config);
+  }
+
+  private int colorLines(CodeMirror cm, int line, int cnt) {
+    for (int i = 0; i < cnt; i++) {
+      cm.addLineClass(line + i, LineClassWhere.WRAP, diffTable.style.diff());
+    }
+    return line + cnt;
+  }
+
   private static String getContentType(DiffInfo.FileMeta meta) {
     return meta != null && meta.content_type() != null
         ? ModeInjector.getContentType(meta.content_type())
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index e681630..2aa83b6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -31,8 +31,7 @@
   private static Binder uiBinder = GWT.create(Binder.class);
 
   interface LineStyle extends CssResource {
-    String insert();
-    String delete();
+    String diff();
     String intraline();
     String padding();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
index 5e4cf0d..301ddf1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
@@ -16,8 +16,9 @@
 -->
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style field='css'>
+  <ui:style type='com.google.gerrit.client.diff.DiffTable.LineStyle'>
     @external .CodeMirror, .CodeMirror-selectedtext, .CodeMirror-scroll;
+    @external .CodeMirror-linenumber;
 
     .difftable .CodeMirror {
       border: 1px solid #eee;
@@ -35,34 +36,26 @@
       overflow-x: hidden;
       overflow-y: hidden;
     }
-    <!--.difftable .CodeMirror-vscrollbar {
-      display: none !important;
-    }-->
-  </ui:style>
-  <ui:style type='com.google.gerrit.client.diff.DiffTable.LineStyle'>
-    @external .CodeMirror-linenumber;
-
-    .insert,
-    .insert .CodeMirror-linenumber {
-      background-color: #dfd;
-    }
-    .delete,
-    .delete .CodeMirror-linenumber {
+    .a .diff,
+    .a .diff .CodeMirror-linenumber {
       background-color: #fee;
     }
-    .intraline {
+    .b .diff,
+    .b .diff .CodeMirror-linenumber {
+      background-color: #dfd;
+    }
+    .b .intraline {
       background-color: #9f9;
     }
     .padding {
       background-color: #eee;
-      height: 1em;
     }
   </ui:style>
-  <g:HTMLPanel styleName='{css.difftable}'>
+  <g:HTMLPanel styleName='{style.difftable}'>
     <table>
       <tr>
-        <td><div ui:field='cmA'></div></td>
-        <td><div ui:field='cmB'></div></td>
+        <td><div ui:field='cmA' class='{style.a}'></div></td>
+        <td><div ui:field='cmB' class='{style.b}'></div></td>
       </tr>
     </table>
   </g:HTMLPanel>
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
index 51f951d..c906459 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -1,7 +1,10 @@
+SRCS = glob(['src/main/java/**/*.java'])
+RESOURCES = glob(['src/main/resources/**/*'])
+
 java_library2(
   name = 'httpd',
-  srcs = glob(['src/main/java/**/*.java']),
-  resources = glob(['src/main/resources/**/*']),
+  srcs = SRCS,
+  resources = RESOURCES,
   deps = [
     '//gerrit-antlr:query_exception',
     '//gerrit-common:server',
@@ -33,6 +36,12 @@
   visibility = ['PUBLIC'],
 )
 
+java_sources(
+  name = 'httpd-src',
+  srcs = SRCS + RESOURCES,
+  visibility = ['PUBLIC'],
+)
+
 java_test(
   name = 'httpd_tests',
   srcs = glob(['src/test/java/**/*.java']),
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 2fe3124..0a9ed28 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -154,7 +154,7 @@
         if (filter != null) {
           try {
             ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
-            builder.setAllowFile(true);
+            builder.setAllowFileRegex(true);
             builder.parse(filter);
           } catch (QueryParseException badFilter) {
             throw new InvalidQueryException(badFilter.getMessage(), filter);
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
new file mode 100644
index 0000000..4e6503e
--- /dev/null
+++ b/gerrit-lucene/BUCK
@@ -0,0 +1,18 @@
+java_library(
+  name = 'lucene',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//gerrit-antlr:query_exception',
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:client',
+    '//gerrit-server:server',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:lucene-analyzers-common',
+    '//lib:lucene-core',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/log:api',
+  ],
+  visibility = ['PUBLIC'],
+)
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
new file mode 100644
index 0000000..4fe6def
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/IndexVersionCheck.java
@@ -0,0 +1,93 @@
+// 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 org.apache.lucene.util.Version.LUCENE_CURRENT;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+
+import org.apache.lucene.util.Version;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+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);
+
+  public static File gerritIndexConfig(SitePaths sitePaths) {
+    return new File(sitePaths.index_dir, "gerrit_index.config");
+  }
+
+  private final SitePaths sitePaths;
+
+  @Inject
+  IndexVersionCheck(SitePaths sitePaths) {
+    this.sitePaths = sitePaths;
+  }
+
+  @Override
+  public void start() {
+    File file = gerritIndexConfig(sitePaths);
+    try {
+      FileBasedConfig cfg = new FileBasedConfig(file, FS.detect());
+      cfg.load();
+      for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
+        int schemaVersion = cfg.getInt("index", e.getKey(), "schemaVersion", 0);
+        if (schemaVersion != e.getValue()) {
+          throw new ProvisionException(String.format(
+              "wrong index schema version for \"%s\": expected %d, found %d%s",
+              e.getKey(), e.getValue(), schemaVersion, upgrade()));
+        }
+      }
+      @SuppressWarnings("deprecation")
+      Version luceneVersion =
+          cfg.getEnum("lucene", null, "version", LUCENE_CURRENT);
+      if (luceneVersion != LUCENE_VERSION) {
+        throw new ProvisionException(String.format(
+            "wrong Lucene version: expected %d, found %d%s",
+            luceneVersion, LUCENE_VERSION, upgrade()));
+
+      }
+    } catch (IOException e) {
+      throw new ProvisionException("unable to read " + file);
+    } catch (ConfigInvalidException e) {
+      throw new ProvisionException("invalid config file " + file);
+    }
+  }
+
+  @Override
+  public void stop() {
+    // Do nothing.
+  }
+
+  private final String upgrade() {
+    return "\nRun reindex to rebuild the index:\n"
+      + "$ java -jar gerrit.war reindex -d "
+      + sitePaths.site_path.getAbsolutePath();
+  }
+}
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
new file mode 100644
index 0000000..ce54b35
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -0,0 +1,310 @@
+// 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.server.query.change.ChangeQueryBuilder.FIELD_CHANGE;
+
+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.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+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.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+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;
+
+/**
+ * Secondary index implementation using Apache Lucene.
+ * <p>
+ * Writes are managed using a single {@link IndexWriter} per process, committed
+ * aggressively. Reads use {@link SearcherManager} and periodically refresh,
+ * 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);
+
+  public static final Version LUCENE_VERSION = Version.LUCENE_43;
+
+  private final FillArgs fillArgs;
+  private final Directory dir;
+  private final IndexWriter writer;
+  private final SearcherManager searcherManager;
+
+  LuceneChangeIndex(File file, 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);
+  }
+
+  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 insert(ChangeData cd) throws IOException {
+    writer.addDocument(toDocument(cd));
+    commit();
+  }
+
+  @Override
+  public void replace(ChangeData cd) throws IOException {
+    writer.updateDocument(intTerm(FIELD_CHANGE, cd.getId().get()),
+        toDocument(cd));
+    commit();
+  }
+
+  @Override
+  public void delete(ChangeData cd) throws IOException {
+    writer.deleteDocuments(intTerm(FIELD_CHANGE, cd.getId().get()));
+    commit();
+  }
+
+  @Override
+  public ChangeDataSource getSource(Predicate<ChangeData> p)
+      throws QueryParseException {
+    return new QuerySource(toQuery(p));
+  }
+
+  public Directory getDirectory() {
+    return dir;
+  }
+
+  public IndexWriter getWriter() {
+    return writer;
+  }
+
+  private void commit() throws IOException {
+    writer.commit();
+    searcherManager.maybeRefresh();
+  }
+
+  private Query toQuery(Predicate<ChangeData> p) throws QueryParseException {
+    if (p.getClass() == AndPredicate.class) {
+      return booleanQuery(p, MUST);
+    } else if (p.getClass() == OrPredicate.class) {
+      return booleanQuery(p, SHOULD);
+    } else if (p.getClass() == NotPredicate.class) {
+      return booleanQuery(p, MUST_NOT);
+    } else if (p instanceof IndexPredicate) {
+      return fieldQuery((IndexPredicate<ChangeData>) p);
+    } else {
+      throw new QueryParseException("Cannot convert to index predicate: " + p);
+    }
+  }
+
+  private Query booleanQuery(Predicate<ChangeData> p, BooleanClause.Occur o)
+      throws QueryParseException {
+    BooleanQuery q = new BooleanQuery();
+    for (int i = 0; i < p.getChildCount(); i++) {
+      q.add(toQuery(p.getChild(i)), o);
+    }
+    return q;
+  }
+
+  private Query fieldQuery(IndexPredicate<ChangeData> p)
+      throws QueryParseException {
+    if (p.getType() == FieldType.INTEGER) {
+      return intQuery(p);
+    } else if (p.getType() == FieldType.EXACT) {
+      return exactQuery(p);
+    } else {
+      throw badFieldType(p.getType());
+    }
+  }
+
+  private Term intTerm(String name, int value) {
+    BytesRef bytes = new BytesRef(NumericUtils.BUF_SIZE_INT);
+    NumericUtils.intToPrefixCodedBytes(value, 0, bytes);
+    return new Term(name, bytes);
+  }
+
+  private Query intQuery(IndexPredicate<ChangeData> p)
+      throws QueryParseException {
+    int value;
+    try {
+      // Can't use IntPredicate because it and IndexPredicate are different
+      // subclasses of OperatorPredicate.
+      value = Integer.valueOf(p.getValue());
+    } catch (IllegalArgumentException e) {
+      throw new QueryParseException("not an integer: " + p.getValue());
+    }
+    return new TermQuery(intTerm(p.getOperator(), value));
+  }
+
+  private Query exactQuery(IndexPredicate<ChangeData> p) {
+    return new TermQuery(new Term(p.getOperator(), p.getValue()));
+  }
+
+  private class QuerySource implements ChangeDataSource {
+    // TODO(dborowitz): Push limit down from predicate tree.
+    private static final int LIMIT = 1000;
+
+    private final Query query;
+
+    public QuerySource(Query query) {
+      this.query = query;
+    }
+
+    @Override
+    public int getCardinality() {
+      return 10; // TODO(dborowitz): estimate from Lucene?
+    }
+
+    @Override
+    public boolean hasChange() {
+      return false;
+    }
+
+    @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);
+        }
+      } catch (IOException e) {
+        throw new OrmException(e);
+      }
+    }
+  }
+
+  private Document toDocument(ChangeData cd) throws IOException {
+    try {
+      Document result = new Document();
+      for (FieldDef<ChangeData, ?> f : ChangeField.ALL.values()) {
+        if (f.isRepeatable()) {
+          add(result, f, (Iterable<?>) f.get(cd, fillArgs));
+        } else {
+          add(result, f, Collections.singleton(f.get(cd, fillArgs)));
+        }
+      }
+      return result;
+    } catch (OrmException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private void add(Document doc, FieldDef<ChangeData, ?> f,
+      Iterable<?> values) throws OrmException {
+    if (f.getType() == FieldType.INTEGER) {
+      for (Object value : values) {
+        doc.add(new IntField(f.getName(), (Integer) value, store(f)));
+      }
+    } else if (f.getType() == FieldType.EXACT) {
+      for (Object value : values) {
+        doc.add(new StringField(f.getName(), (String) value, store(f)));
+      }
+    } else {
+      throw badFieldType(f.getType());
+    }
+  }
+
+  private static Field.Store store(FieldDef<?, ?> f) {
+    return f.isStored() ? Field.Store.YES : Field.Store.NO;
+  }
+
+  private static IllegalArgumentException badFieldType(FieldType<?> t) {
+    return new IllegalArgumentException("unknown index field type " + t);
+  }
+}
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
new file mode 100644
index 0000000..1681914
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndexManager.java
@@ -0,0 +1,70 @@
+// 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
new file mode 100644
index 0000000..e1fbd76
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -0,0 +1,55 @@
+// 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.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.index.ChangeIndexerImpl;
+import com.google.gerrit.server.query.change.IndexRewrite;
+import com.google.gerrit.server.query.change.IndexRewriteImpl;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+
+import org.eclipse.jgit.lib.Config;
+
+public class LuceneIndexModule extends LifecycleModule {
+  public static boolean isEnabled(Injector injector) {
+    return injector.getInstance(Key.get(Config.class, GerritServerConfig.class))
+        .getBoolean("index", null, "enabled", false);
+  }
+
+  private final boolean checkVersion;
+
+  public LuceneIndexModule() {
+    this(true);
+  }
+
+  public LuceneIndexModule(boolean checkVersion) {
+    this.checkVersion = checkVersion;
+  }
+
+  @Override
+  protected void configure() {
+    bind(ChangeIndex.Manager.class).to(LuceneChangeIndexManager.class);
+    bind(ChangeIndexer.class).to(ChangeIndexerImpl.class);
+    bind(IndexRewrite.class).to(IndexRewriteImpl.class);
+    listener().to(LuceneChangeIndexManager.class);
+    if (checkVersion) {
+      listener().to(IndexVersionCheck.class);
+    }
+  }
+}
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 0746bdf..dd665db 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -8,6 +8,7 @@
     '//gerrit-extension-api:api',
     '//gerrit-gwtexpui:server',
     '//gerrit-httpd:httpd',
+    '//gerrit-lucene:lucene',
     '//gerrit-openid:openid',
     '//gerrit-server:common_rules',
     '//gerrit-reviewdb:server',
@@ -30,6 +31,7 @@
     '//lib/jgit:jgit',
     '//lib/log:api',
     '//lib/log:log4j',
+    '//lib:lucene-core',
     '//lib/mina:sshd',
     '//lib/prolog:prolog-cafe',
   ],
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index ca98a84..221c4a2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.http.jetty.GetUserFilter;
 import com.google.gerrit.pgm.http.jetty.JettyEnv;
 import com.google.gerrit.pgm.http.jetty.JettyModule;
@@ -50,6 +51,7 @@
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.index.NoIndexModule;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.patch.IntraLineWorkerPool;
@@ -319,6 +321,11 @@
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PluginModule());
+    if (LuceneIndexModule.isEnabled(cfgInjector)) {
+      modules.add(new LuceneIndexModule());
+    } else {
+      modules.add(new NoIndexModule());
+    }
     if (httpd) {
       modules.add(new CanonicalWebUrlModule() {
         @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
new file mode 100644
index 0000000..97812e4
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -0,0 +1,152 @@
+// 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.pgm;
+
+import static com.google.gerrit.lucene.IndexVersionCheck.SCHEMA_VERSIONS;
+import static com.google.gerrit.lucene.IndexVersionCheck.gerritIndexConfig;
+import static com.google.gerrit.lucene.LuceneChangeIndex.LUCENE_VERSION;
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.patch.PatchListCacheImpl;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class Reindex extends SiteProgram {
+  private final LifecycleManager manager = new LifecycleManager();
+  private final AtomicReference<ReviewDb> dbRef =
+      new AtomicReference<ReviewDb>();
+  private Injector dbInjector;
+  private Injector sysInjector;
+  private SitePaths sitePaths;
+
+  @Override
+  public int run() throws Exception {
+    mustHaveValidSite();
+    dbInjector = createDbInjector(SINGLE_USER);
+    if (!LuceneIndexModule.isEnabled(dbInjector)) {
+      throw die("Secondary index not enabled");
+    }
+
+    sitePaths = dbInjector.getInstance(SitePaths.class);
+    deleteAll();
+
+    sysInjector = createSysInjector();
+    manager.add(dbInjector);
+    manager.add(sysInjector);
+    manager.start();
+
+    SchemaFactory<ReviewDb> schema = dbInjector.getInstance(
+        Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}));
+    ReviewDb db = schema.open();
+    dbRef.set(db);
+
+    ChangeIndexer indexer = sysInjector.getInstance(ChangeIndexer.class);
+
+    Stopwatch sw = new Stopwatch().start();
+    int i = 0;
+    for (Change change : db.changes().all()) {
+      indexer.index(change).get();
+      i++;
+    }
+    double elapsed = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
+    System.out.format("Reindexed %d changes in %.02fms", i, elapsed);
+    writeVersion();
+
+    manager.stop();
+    return 0;
+  }
+
+  private Injector createSysInjector() {
+    List<Module> modules = Lists.newArrayList();
+    modules.add(PatchListCacheImpl.module());
+    modules.add(new LuceneIndexModule(false));
+    modules.add(new AbstractModule() {
+      @SuppressWarnings("rawtypes")
+      @Override
+      protected void configure() {
+        bind(ReviewDb.class).toProvider(new Provider<ReviewDb>() {
+          @Override
+          public ReviewDb get() {
+            return dbRef.get();
+          }
+        });
+        // Plugins are not loaded and we're just running through each change
+        // once, so don't worry about cache removal.
+        bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
+            .toInstance(DynamicSet.<CacheRemovalListener> emptySet());
+        install(new DefaultCacheFactory.Module());
+      }
+    });
+    return dbInjector.createChildInjector(modules);
+  }
+
+  private void deleteAll() throws IOException {
+    for (String index : SCHEMA_VERSIONS.keySet()) {
+      File file = new File(sitePaths.index_dir, index);
+      if (file.exists()) {
+        Directory dir = FSDirectory.open(file);
+        try {
+          for (String name : dir.listAll()) {
+            dir.deleteFile(name);
+          }
+        } finally {
+          dir.close();
+        }
+      }
+    }
+  }
+
+  private void writeVersion() throws IOException, ConfigInvalidException {
+    FileBasedConfig cfg =
+        new FileBasedConfig(gerritIndexConfig(sitePaths), FS.detect());
+    cfg.load();
+
+    for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
+      cfg.setInt("index", e.getKey(), "schemaVersion", e.getValue());
+    }
+    cfg.setEnum("lucene", null, "version", LUCENE_VERSION);
+    cfg.save();
+  }
+}
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 1d676a0..95f5b11 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -1,10 +1,13 @@
 include_defs('//lib/prolog/DEFS')
 
+SRCS = glob(['src/main/java/**/*.java'])
+RESOURCES =  glob(['src/main/resources/**/*'])
+
 # TODO(sop) break up gerrit-server java_library(), its too big
 java_library2(
   name = 'server',
-  srcs = glob(['src/main/java/**/*.java']),
-  resources = glob(['src/main/resources/**/*']),
+  srcs = SRCS,
+  resources = RESOURCES,
   deps = [
     '//gerrit-antlr:query_exception',
     '//gerrit-antlr:query_parser',
@@ -51,6 +54,12 @@
   visibility = ['PUBLIC'],
 )
 
+java_sources(
+  name = 'server-src',
+  srcs = SRCS + RESOURCES,
+  visibility = ['PUBLIC'],
+)
+
 prolog_cafe_library(
   name = 'common_rules',
   srcs = ['src/main/prolog/gerrit_common.pl'],
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index f766297..20e7681 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.Abandon.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.project.ChangeControl;
@@ -48,6 +49,7 @@
   private final AbandonedSender.Factory abandonedSenderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson json;
+  private final ChangeIndexer indexer;
 
   public static class Input {
     @DefaultInput
@@ -58,11 +60,13 @@
   Abandon(ChangeHooks hooks,
       AbandonedSender.Factory abandonedSenderFactory,
       Provider<ReviewDb> dbProvider,
-      ChangeJson json) {
+      ChangeJson json,
+      ChangeIndexer indexer) {
     this.hooks = hooks;
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
+    this.indexer = indexer;
   }
 
   @Override
@@ -107,6 +111,7 @@
       db.rollback();
     }
 
+    indexer.index(change);
     try {
       ReplyToChangeSender cm = abandonedSenderFactory.create(change);
       cm.setFrom(caller.getAccountId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 8a169b8..cc32134 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -29,8 +29,10 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -52,6 +54,7 @@
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
   private final TrackingFooters trackingFooters;
+  private final ChangeIndexer indexer;
 
   private final RefControl refControl;
   private final Change change;
@@ -59,6 +62,7 @@
   private final RevCommit commit;
   private final PatchSetInfo patchSetInfo;
 
+  private RequestScopePropagator requestScopePropagator;
   private ChangeMessage changeMessage;
   private Set<Account.Id> reviewers;
   private boolean draft;
@@ -70,6 +74,7 @@
       ChangeHooks hooks,
       ApprovalsUtil approvalsUtil,
       TrackingFooters trackingFooters,
+      ChangeIndexer indexer,
       @Assisted RefControl refControl,
       @Assisted Change change,
       @Assisted RevCommit commit) {
@@ -78,6 +83,7 @@
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
     this.trackingFooters = trackingFooters;
+    this.indexer = indexer;
     this.refControl = refControl;
     this.change = change;
     this.commit = commit;
@@ -98,6 +104,11 @@
     ChangeUtil.computeSortKey(change);
   }
 
+  public ChangeInserter setRequestScopePropagator(RequestScopePropagator rsp) {
+    requestScopePropagator = rsp;
+    return this;
+  }
+
   public ChangeInserter setMessage(ChangeMessage changeMessage) {
     this.changeMessage = changeMessage;
     return this;
@@ -140,6 +151,7 @@
       db.changeMessages().insert(Collections.singleton(changeMessage));
     }
 
+    indexer.index(change, requestScopePropagator);
     gitRefUpdated.fire(change.getProject(), patchSet.getRefName(),
         ObjectId.zeroId(), commit);
     hooks.doPatchsetCreatedHook(change, patchSet, db);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 50e395e..ebee110 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.RefControl;
@@ -66,6 +67,7 @@
   private final IdentifiedUser user;
   private final GitReferenceUpdated gitRefUpdated;
   private final CommitValidators.Factory commitValidatorsFactory;
+  private final ChangeIndexer indexer;
   private boolean validateForReceiveCommits;
 
   private final Repository git;
@@ -87,6 +89,7 @@
       IdentifiedUser user,
       GitReferenceUpdated gitRefUpdated,
       CommitValidators.Factory commitValidatorsFactory,
+      ChangeIndexer indexer,
       @Assisted Repository git,
       @Assisted RevWalk revWalk,
       @Assisted RefControl refControl,
@@ -99,6 +102,7 @@
     this.user = user;
     this.gitRefUpdated = gitRefUpdated;
     this.commitValidatorsFactory = commitValidatorsFactory;
+    this.indexer = indexer;
 
     this.git = git;
     this.revWalk = revWalk;
@@ -212,6 +216,7 @@
         db.changeMessages().insert(Collections.singleton(changeMessage));
       }
 
+      indexer.index(change);
       hooks.doPatchsetCreatedHook(change, patchSet, db);
     } finally {
       db.rollback();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index afb58f9..001ed03 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.Restore.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.mail.RestoredSender;
 import com.google.gerrit.server.project.ChangeControl;
@@ -48,6 +49,7 @@
   private final RestoredSender.Factory restoredSenderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson json;
+  private final ChangeIndexer indexer;
 
   public static class Input {
     @DefaultInput
@@ -58,11 +60,13 @@
   Restore(ChangeHooks hooks,
       RestoredSender.Factory restoredSenderFactory,
       Provider<ReviewDb> dbProvider,
-      ChangeJson json) {
+      ChangeJson json,
+      ChangeIndexer indexer) {
     this.hooks = hooks;
     this.restoredSenderFactory = restoredSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
+    this.indexer = indexer;
   }
 
   @Override
@@ -98,6 +102,7 @@
         throw new ResourceConflictException("change is "
             + status(db.changes().get(req.getChange().getId())));
       }
+      indexer.index(change);
       message = newMessage(input, caller, change);
       db.changeMessages().insert(Collections.singleton(message));
       new ApprovalsUtil(db).syncChangeStatus(change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 2b51b0a..e236abb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.server.change.Submit.Input;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -72,14 +73,17 @@
   private final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager repoManager;
   private final MergeQueue mergeQueue;
+  private final ChangeIndexer indexer;
 
   @Inject
   Submit(Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
-      MergeQueue mergeQueue) {
+      MergeQueue mergeQueue,
+      ChangeIndexer indexer) {
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.mergeQueue = mergeQueue;
+    this.indexer = indexer;
   }
 
   @Override
@@ -187,6 +191,7 @@
     } finally {
       db.rollback();
     }
+    indexer.index(change);
     return change;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
index 22eae2d..6ece448 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
@@ -40,6 +41,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -75,6 +77,8 @@
   private final AccountResolver accountResolver;
   private final CreateChangeSender.Factory createChangeSenderFactory;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
+  private final ChangeIndexer indexer;
+  private final RequestScopePropagator requestScopePropagator;
 
   private final PatchSet.Id patchSetId;
 
@@ -87,6 +91,8 @@
       final AccountResolver accountResolver,
       final CreateChangeSender.Factory createChangeSenderFactory,
       final ReplacePatchSetSender.Factory replacePatchSetFactory,
+      final ChangeIndexer indexer,
+      final RequestScopePropagator requestScopePropagator,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
@@ -97,6 +103,8 @@
     this.accountResolver = accountResolver;
     this.createChangeSenderFactory = createChangeSenderFactory;
     this.replacePatchSetFactory = replacePatchSetFactory;
+    this.indexer = indexer;
+    this.requestScopePropagator = requestScopePropagator;
 
     this.patchSetId = patchSetId;
   }
@@ -146,6 +154,7 @@
       });
 
       if (!updatedPatchSet.isDraft() || updatedChange.getStatus() == Change.Status.NEW) {
+        indexer.index(updatedChange, requestScopePropagator);
         hooks.doDraftPublishedHook(updatedChange, updatedPatchSet, db);
 
         sendNotifications(control.getChange().getStatus() == Change.Status.DRAFT,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index 3b43fb0..0db8019 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.RebasedPatchSetSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -75,6 +76,7 @@
   private final ChangeHookRunner hooks;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ProjectCache projectCache;
+  private final ChangeIndexer indexer;
 
   @Inject
   RebaseChange(final ChangeControl.GenericFactory changeControlFactory,
@@ -85,7 +87,8 @@
       final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
       final ChangeHookRunner hooks,
       final MergeUtil.Factory mergeUtilFactory,
-      final ProjectCache projectCache) {
+      final ProjectCache projectCache,
+      final ChangeIndexer changeIndexer) {
     this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.db = db;
@@ -96,6 +99,7 @@
     this.hooks = hooks;
     this.mergeUtilFactory = mergeUtilFactory;
     this.projectCache = projectCache;
+    this.indexer = changeIndexer;
   }
 
   /**
@@ -156,10 +160,10 @@
           uploader.newCommitterIdent(myIdent.getWhen(),
               myIdent.getTimeZone());
 
-      final PatchSet newPatchSet =
-          rebase(git, rw, inserter, patchSetId, change, uploader.getAccountId(), baseCommit,
-              mergeUtilFactory.create(
-                  changeControl.getProjectControl().getProjectState(), true), committerIdent);
+      final PatchSet newPatchSet = rebase(git, rw, inserter, patchSetId, change,
+          uploader.getAccountId(), baseCommit, mergeUtilFactory.create(
+              changeControl.getProjectControl().getProjectState(), true),
+          committerIdent, indexer);
 
       final Set<Account.Id> oldReviewers = Sets.newHashSet();
       final Set<Account.Id> oldCC = Sets.newHashSet();
@@ -301,6 +305,7 @@
    * @param uploader the user that creates the rebased patch set
    * @param baseCommit the commit that should be the new base
    * @param mergeUtil merge utilities for the destination project
+   * @param indexer helper for indexing the change
    * @return the new patch set which is based on the given base commit
    * @throws NoSuchChangeException thrown if the change to which the patch set
    *         belongs does not exist or is not visible to the user
@@ -311,7 +316,8 @@
   public PatchSet rebase(final Repository git, final RevWalk revWalk,
       final ObjectInserter inserter, final PatchSet.Id patchSetId,
       final Change chg, final Account.Id uploader, final RevCommit baseCommit,
-      final MergeUtil mergeUtil, PersonIdent committerIdent) throws NoSuchChangeException,
+      final MergeUtil mergeUtil, PersonIdent committerIdent,
+      final ChangeIndexer indexer) throws NoSuchChangeException,
       OrmException, IOException, InvalidChangeOperationException,
       PathConflictException {
     Change change = chg;
@@ -383,6 +389,7 @@
             "Change %s was modified", change.getId()));
       }
 
+      indexer.index(change);
       ApprovalsUtil.copyLabels(db, projectCache.get(change.getProject())
           .getLabelTypes(), patchSetId, change.currentPatchSetId());
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 2116c0c..9d7c54a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -40,6 +40,7 @@
   public final File hooks_dir;
   public final File static_dir;
   public final File themes_dir;
+  public final File index_dir;
 
   public final File gerrit_sh;
   public final File gerrit_war;
@@ -77,6 +78,7 @@
     hooks_dir = new File(site_path, "hooks");
     static_dir = new File(site_path, "static");
     themes_dir = new File(site_path, "themes");
+    index_dir = new File(site_path, "index");
 
     gerrit_sh = new File(bin_dir, "gerrit.sh");
     gerrit_war = new File(bin_dir, "gerrit.war");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index a59a216..bce3d37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import static com.google.gerrit.server.git.MergeUtil.getSubmitter;
+
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
@@ -43,6 +44,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -153,6 +155,7 @@
   private final WorkQueue workQueue;
   private final RequestScopePropagator requestScopePropagator;
   private final AllProjectsName allProjectsName;
+  private final ChangeIndexer indexer;
 
   @Inject
   MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
@@ -168,7 +171,8 @@
       final SubmoduleOp.Factory subOpFactory,
       final WorkQueue workQueue,
       final RequestScopePropagator requestScopePropagator,
-      final AllProjectsName allProjectsName) {
+      final AllProjectsName allProjectsName,
+      final ChangeIndexer indexer) {
     repoManager = grm;
     schemaFactory = sf;
     labelNormalizer = fs;
@@ -188,6 +192,7 @@
     this.workQueue = workQueue;
     this.requestScopePropagator = requestScopePropagator;
     this.allProjectsName = allProjectsName;
+    this.indexer = indexer;
     destBranch = branch;
     toMerge = ArrayListMultimap.create();
     potentiallyStillSubmittable = new ArrayList<CodeReviewCommit>();
@@ -758,6 +763,7 @@
       } catch (OrmException err) {
         log.warn("Error updating change status for " + c.getId(), err);
       }
+      indexer.index(c);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
index 77c4863..0518349 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
@@ -80,7 +80,7 @@
                 rebaseChange.rebase(args.repo, args.rw, args.inserter,
                     n.patchsetId, n.change,
                     args.mergeUtil.getSubmitter(n.patchsetId).getAccountId(),
-                    newMergeTip, args.mergeUtil, committerIdent);
+                    newMergeTip, args.mergeUtil, committerIdent, args.indexer);
             List<PatchSetApproval> approvals = Lists.newArrayList();
             for (PatchSetApproval a : args.mergeUtil.getApprovalsForCommit(n)) {
               approvals.add(new PatchSetApproval(newPatchSet.getId(), a));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 482a211..8e4ba0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -74,6 +74,7 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.mail.MergedSender;
@@ -263,6 +264,7 @@
   private final WorkQueue workQueue;
   private final ListeningExecutorService changeUpdateExector;
   private final RequestScopePropagator requestScopePropagator;
+  private final ChangeIndexer indexer;
   private final SshInfo sshInfo;
   private final AllProjectsName allProjectsName;
   private final ReceiveConfig receiveConfig;
@@ -323,6 +325,7 @@
       final WorkQueue workQueue,
       @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
       final RequestScopePropagator requestScopePropagator,
+      final ChangeIndexer indexer,
       final SshInfo sshInfo,
       final AllProjectsName allProjectsName,
       ReceiveConfig config,
@@ -353,6 +356,7 @@
     this.workQueue = workQueue;
     this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
+    this.indexer = indexer;
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
     this.receiveConfig = config;
@@ -1470,7 +1474,8 @@
           currentUser.getAccountId(),
           magicBranch.dest);
       change.setTopic(magicBranch.topic);
-      ins = changeInserterFactory.create(ctl, change, c);
+      ins = changeInserterFactory.create(ctl, change, c)
+          .setRequestScopePropagator(requestScopePropagator);
       cmd = new ReceiveCommand(ObjectId.zeroId(), c,
           ins.getPatchSet().getRefName());
     }
@@ -1904,6 +1909,7 @@
       if (cmd.getResult() == NOT_ATTEMPTED) {
         cmd.execute(rp);
       }
+      indexer.index(change, requestScopePropagator);
       gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
           ObjectId.zeroId(), newCommit);
       hooks.doPatchsetCreatedHook(change, newPatchSet, db);
@@ -2206,7 +2212,7 @@
 
   private void markChangeMergedByPush(final ReviewDb db,
       final ReplaceRequest result) throws OrmException {
-    final Change change = result.change;
+    Change change = result.change;
     final String mergedIntoRef = result.mergedIntoRef;
 
     change.setCurrentPatchSet(result.info);
@@ -2234,17 +2240,19 @@
 
     db.changeMessages().insert(Collections.singleton(msg));
 
-    db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-      @Override
-      public Change update(Change change) {
-        if (change.getStatus().isOpen()) {
-          change.setCurrentPatchSet(result.info);
-          change.setStatus(Change.Status.MERGED);
-          ChangeUtil.updated(change);
-        }
-        return change;
-      }
-    });
+    change = db.changes().atomicUpdate(
+        change.getId(), new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change change) {
+            if (change.getStatus().isOpen()) {
+              change.setCurrentPatchSet(result.info);
+              change.setStatus(Change.Status.MERGED);
+              ChangeUtil.updated(change);
+            }
+            return change;
+          }
+        });
+    indexer.index(change, requestScopePropagator);
   }
 
   private void sendMergedEmail(final ReplaceRequest result) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
index 7c2ba86..9626e23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.ChangeIndexer;
 
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -55,13 +56,15 @@
     protected final Set<RevCommit> alreadyAccepted;
     protected final Branch.NameKey destBranch;
     protected final MergeUtil mergeUtil;
+    protected final ChangeIndexer indexer;
     protected final MergeSorter mergeSorter;
 
     Arguments(final IdentifiedUser.GenericFactory identifiedUserFactory,
         final PersonIdent myIdent, final ReviewDb db, final Repository repo,
         final RevWalk rw, final ObjectInserter inserter,
         final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
-        final Branch.NameKey destBranch, final MergeUtil mergeUtil) {
+        final Branch.NameKey destBranch, final MergeUtil mergeUtil,
+        final ChangeIndexer indexer) {
       this.identifiedUserFactory = identifiedUserFactory;
       this.myIdent = myIdent;
       this.db = db;
@@ -73,6 +76,7 @@
       this.alreadyAccepted = alreadyAccepted;
       this.destBranch = destBranch;
       this.mergeUtil = mergeUtil;
+      this.indexer = indexer;
       this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
index d43a756..845139d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -54,6 +55,7 @@
   private final RebaseChange rebaseChange;
   private final ProjectCache projectCache;
   private final MergeUtil.Factory mergeUtilFactory;
+  private final ChangeIndexer indexer;
 
   @Inject
   SubmitStrategyFactory(
@@ -63,7 +65,8 @@
       @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
       final GitReferenceUpdated gitRefUpdated, final RebaseChange rebaseChange,
       final ProjectCache projectCache,
-      final MergeUtil.Factory mergeUtilFactory) {
+      final MergeUtil.Factory mergeUtilFactory,
+      final ChangeIndexer indexer) {
     this.identifiedUserFactory = identifiedUserFactory;
     this.myIdent = myIdent;
     this.patchSetInfoFactory = patchSetInfoFactory;
@@ -71,6 +74,7 @@
     this.rebaseChange = rebaseChange;
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
+    this.indexer = indexer;
   }
 
   public SubmitStrategy create(final SubmitType submitType, final ReviewDb db,
@@ -82,7 +86,7 @@
     final SubmitStrategy.Arguments args =
         new SubmitStrategy.Arguments(identifiedUserFactory, myIdent, db, repo,
             rw, inserter, canMergeFlag, alreadyAccepted, destBranch,
-            mergeUtilFactory.create(project));
+            mergeUtilFactory.create(project), indexer);
     switch (submitType) {
       case CHERRY_PICK:
         return new CherryPick(args, patchSetInfoFactory, gitRefUpdated);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
new file mode 100644
index 0000000..e52f80d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -0,0 +1,107 @@
+// 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.server.index;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gwtorm.server.OrmException;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.util.Map;
+
+/**
+ * Fields indexed on change documents.
+ * <p>
+ * Each field corresponds to both a field name supported by
+ * {@link ChangeQueryBuilder} for querying that field, and a method on
+ * {@link ChangeData} used for populating the corresponding document fields in
+ * the secondary index.
+ * <p>
+ * Used to generate a schema for index implementations that require one.
+ */
+public class ChangeField {
+  /** Increment whenever making schema changes. */
+  public static final int SCHEMA_VERSION = 1;
+
+  /** Legacy change ID. */
+  public static final FieldDef<ChangeData, Integer> CHANGE_ID =
+      new FieldDef.Single<ChangeData, Integer>(ChangeQueryBuilder.FIELD_CHANGE,
+          FieldType.INTEGER, true) {
+        @Override
+        public Integer get(ChangeData input, FillArgs args) {
+          return input.getId().get();
+        }
+      };
+
+  /** Change status string, in the same format as {@code status:}. */
+  public static final FieldDef<ChangeData, String> STATUS =
+      new FieldDef.Single<ChangeData, String>(ChangeQueryBuilder.FIELD_STATUS,
+          FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return ChangeStatusPredicate.VALUES.get(
+              input.change(args.db).getStatus());
+        }
+      };
+
+  /** List of filenames modified in the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> FILE =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_FILE, FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.currentFilePaths(args.db, args.patchListCache);
+        }
+      };
+
+  public static final ImmutableMap<String, FieldDef<ChangeData, ?>> ALL;
+
+  static {
+    Map<String, FieldDef<ChangeData, ?>> fields = Maps.newHashMap();
+    for (Field f : ChangeField.class.getFields()) {
+      if (Modifier.isPublic(f.getModifiers())
+          && Modifier.isStatic(f.getModifiers())
+          && Modifier.isFinal(f.getModifiers())
+          && FieldDef.class.isAssignableFrom(f.getType())) {
+        ParameterizedType t = (ParameterizedType) f.getGenericType();
+        if (t.getActualTypeArguments()[0] == ChangeData.class) {
+          try {
+            @SuppressWarnings("unchecked")
+            FieldDef<ChangeData, ?> fd = (FieldDef<ChangeData, ?>) f.get(null);
+            fields.put(fd.getName(), fd);
+          } catch (IllegalArgumentException e) {
+            throw new ExceptionInInitializerError(e);
+          } catch (IllegalAccessException e) {
+            throw new ExceptionInInitializerError(e);
+          }
+        } else {
+          throw new ExceptionInInitializerError(
+              "non-ChangeData ChangeField: " + f);
+        }
+      }
+    }
+    if (fields.isEmpty()) {
+      throw new ExceptionInInitializerError("no ChangeFields found");
+    }
+    ALL = ImmutableMap.copyOf(fields);
+  }
+}
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
new file mode 100644
index 0000000..da2e451
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -0,0 +1,118 @@
+// 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.server.index;
+
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+
+import java.io.IOException;
+
+/**
+ * Secondary index implementation for change documents.
+ * <p>
+ * {@link ChangeData} objects are inserted into the index and are queried by
+ * converting special {@link com.google.gerrit.server.query.Predicate} instances
+ * into index-aware predicates that use the index search results as a source.
+ * <p>
+ * Implementations must be thread-safe and should batch inserts/updates where
+ * 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.
+          }
+
+          @Override
+          public void replace(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;
+  }
+
+  /**
+   * Insert a change document into the index.
+   * <p>
+   * Results may not be immediately visible to searchers, but should be visible
+   * within a reasonable amount of time.
+   *
+   * @param cd change document with all index fields prepopulated; see
+   *     {@link ChangeData#fillIndexFields}.
+   *
+   * @throws IOException if the change could not be inserted.
+   */
+  public void insert(ChangeData cd) throws IOException;
+
+  /**
+   * Update a change document in the index.
+   * <p>
+   * Semantically equivalent to deleting the document and reinserting it with
+   * new field values. Results may not be immediately visible to searchers, but
+   * should be visible within a reasonable amount of time.
+   *
+   * @param cd change document with all index fields prepopulated; see
+   *     {@link ChangeData#fillIndexFields}.
+   *
+   * @throws IOException
+   */
+  public void replace(ChangeData cd) throws IOException;
+
+  /**
+   * Delete a change document from the index.
+   *
+   * @param cd change document.
+   *
+   * @throws IOException
+   */
+  public void delete(ChangeData cd) throws IOException;
+
+  /**
+   * Convert the given operator predicate into a source searching the index and
+   * returning only the documents matching that predicate.
+   *
+   * @param p the predicate to match. Must be a tree containing only AND, OR,
+   *     or NOT predicates as internal nodes, and {@link IndexPredicate}s as
+   *     leaves.
+   * @return a source of documents matching the predicate.
+   *
+   * @throws QueryParseException if the predicate could not be converted to an
+   *     indexed data source.
+   */
+  public ChangeDataSource getSource(Predicate<ChangeData> p)
+      throws QueryParseException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
new file mode 100644
index 0000000..a76f6ae
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -0,0 +1,57 @@
+// 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.server.index;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.util.RequestScopePropagator;
+
+import java.util.concurrent.Future;
+
+/**
+ * Helper for (re)indexing a change document.
+ * <p>
+ * Indexing is run in the background, as it may require substantial work to
+ * compute some of the fields and/or update the index.
+ */
+public interface ChangeIndexer {
+  /** Instance indicating secondary index is disabled. */
+  public static final ChangeIndexer DISABLED = new ChangeIndexer() {
+    @Override
+    public Future<?> index(Change change) {
+      return Futures.immediateFuture(null);
+    }
+
+    @Override
+    public Future<?> index(Change change, RequestScopePropagator prop) {
+      return Futures.immediateFuture(null);
+    }
+  };
+
+  /**
+   * Start indexing a change.
+   *
+   * @param change change to index.
+   */
+  public Future<?> index(Change change);
+
+  /**
+   * Start indexing a change.
+   *
+   * @param change change to index.
+   * @param prop propagator to wrap any created runnables in.
+   */
+  public Future<?> index(Change change, RequestScopePropagator prop);
+}
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
new file mode 100644
index 0000000..43760f9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
@@ -0,0 +1,93 @@
+// 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.server.index;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.Future;
+
+/**
+ * Helper for (re)indexing a change document.
+ * <p>
+ * Indexing is run in the background, as it may require substantial work to
+ * compute some of the fields and/or update the index.
+ */
+public class ChangeIndexerImpl implements ChangeIndexer {
+  private static final Logger log =
+      LoggerFactory.getLogger(ChangeIndexerImpl.class);
+
+  private final WorkQueue workQueue;
+  private final ChangeIndex openIndex;
+  private final ChangeIndex closedIndex;
+
+  @Inject
+  ChangeIndexerImpl(WorkQueue workQueue,
+      ChangeIndex.Manager indexManager) throws IOException {
+    this.workQueue = workQueue;
+    this.openIndex = indexManager.get("changes_open");
+    this.closedIndex = indexManager.get("changes_closed");
+  }
+
+  @Override
+  public Future<?> index(Change change) {
+    return index(change, null);
+  }
+
+  @Override
+  public Future<?> index(Change change, RequestScopePropagator prop) {
+    Runnable task = new Task(change);
+    if (prop != null) {
+      task = prop.wrap(task);
+    }
+    return workQueue.getDefaultQueue().submit(task);
+  }
+
+  private class Task implements Runnable {
+    private final Change change;
+
+    private Task(Change change) {
+      this.change = change;
+    }
+
+    @Override
+    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);
+        }
+      } catch (IOException e) {
+        log.error("Error indexing change", e);
+      }
+    }
+
+    @Override
+    public String toString() {
+      return "index-change-" + change.getId().get();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
new file mode 100644
index 0000000..7c0fd81
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -0,0 +1,112 @@
+// 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.server.index;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * Definition of a field stored in the secondary index.
+ *
+ * @param I input type from which documents are created and search results are
+ *     returned.
+ * @param T type that should be extracted from the input object when converting
+ *     to an index document.
+ */
+public abstract class FieldDef<I, T> {
+  /** Definition of a single (non-repeatable) field. */
+  public static abstract class Single<I, T> extends FieldDef<I, T> {
+    Single(String name, FieldType<T> type, boolean stored) {
+      super(name, type, stored);
+    }
+
+    @Override
+    public final boolean isRepeatable() {
+      return false;
+    }
+  }
+
+  /** Definition of a repeatable field. */
+  public static abstract class Repeatable<I, T>
+      extends FieldDef<I, Iterable<T>> {
+    Repeatable(String name, FieldType<T> type, boolean stored) {
+      super(name, type, stored);
+    }
+
+    @Override
+    public final boolean isRepeatable() {
+      return true;
+    }
+  }
+
+  /** Arguments needed to fill in missing data in the input object. */
+  public static class FillArgs {
+    final Provider<ReviewDb> db;
+    final PatchListCache patchListCache;
+
+    @Inject
+    FillArgs(Provider<ReviewDb> db,
+        PatchListCache patchListCache) {
+      this.db = db;
+      this.patchListCache = patchListCache;
+    }
+  }
+
+  private final String name;
+  private final FieldType<?> type;
+  private final boolean stored;
+
+  private FieldDef(String name, FieldType<?> type, boolean stored) {
+    this.name = name;
+    this.type = type;
+    this.stored = stored;
+  }
+
+  /** @return name of the field. */
+  public final String getName() {
+    return name;
+  }
+
+  /**
+   * @return type of the field; for repeatable fields, the inner type, not the
+   *     iterable type.
+   */
+  public final FieldType<?> getType() {
+    return type;
+  }
+
+  /** @return whether the field should be stored in the index. */
+  public final boolean isStored() {
+    return stored;
+  }
+
+  /**
+   * Get the field contents from the input object.
+   *
+   * @param input input object.
+   * @param args arbitrary arguments needed to fill in indexable fields of the
+   *     input object.
+   * @return the field value(s) to index.
+   *
+   * @throws OrmException
+   */
+  public abstract T get(I input, FillArgs args) throws OrmException;
+
+  /** @return whether the field is repeatable. */
+  public abstract boolean isRepeatable();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
new file mode 100644
index 0000000..ec4808b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
@@ -0,0 +1,42 @@
+// 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.server.index;
+
+
+/** Document field types supported by the secondary index system. */
+public class FieldType<T> {
+  /** A single integer-valued field. */
+  public static final FieldType<Integer> INTEGER =
+      new FieldType<Integer>("INTEGER");
+
+  /** A string field searched using exact-match semantics. */
+  public static final FieldType<String> EXACT =
+      new FieldType<String>("EXACT");
+
+  private final String name;
+
+  private FieldType(String name) {
+    this.name = name;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
new file mode 100644
index 0000000..605582a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
@@ -0,0 +1,39 @@
+// 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.server.index;
+
+import com.google.gerrit.server.query.OperatorPredicate;
+
+/** Index-aware predicate that includes a field type annotation. */
+public abstract class IndexPredicate<I> extends OperatorPredicate<I> {
+  private final FieldDef<I, ?> def;
+
+  public IndexPredicate(FieldDef<I, ?> def, String value) {
+    super(def.getName(), value);
+    this.def = def;
+  }
+
+  public FieldType<?> getType() {
+    return def.getType();
+  }
+
+  /**
+   * @return whether this predicate can only be satisfied by looking at the
+   *     secondary index, i.e. it cannot be expressed as a query over the DB.
+   */
+  public boolean isIndexOnly() {
+    return false;
+  }
+}
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
new file mode 100644
index 0000000..fb7fbe6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
@@ -0,0 +1,31 @@
+// 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.server.index;
+
+import com.google.gerrit.server.query.change.IndexRewrite;
+import com.google.inject.AbstractModule;
+
+public class NoIndexModule extends AbstractModule {
+  // TODO(dborowitz): This module should go away when the index becomes
+  // obligatory, as should the interfaces that exist only to support the
+  // non-index case.
+
+  @Override
+  protected void configure() {
+    bind(ChangeIndex.Manager.class).toInstance(ChangeIndex.Manager.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
new file mode 100644
index 0000000..f635da6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java
@@ -0,0 +1,139 @@
+// 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.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;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gwtorm.server.OrmException;
+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
+ * {@link ChangeDataSource} that returns matching results from the index.
+ * <p>
+ * Appropriate to return as the rootmost predicate that can be processed using
+ * the secondary index; such predicates must also implement
+ * {@link ChangeDataSource} to be chosen by the query processor.
+ */
+public class PredicateWrapper extends Predicate<ChangeData> implements
+    ChangeDataSource {
+  private final Predicate<ChangeData> pred;
+  private final List<ChangeDataSource> sources;
+
+  public PredicateWrapper(Predicate<ChangeData> pred, ChangeIndex index)
+      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));
+    }
+  }
+
+  @Override
+  public int getCardinality() {
+    int n = 0;
+    for (ChangeDataSource source : sources) {
+      n += source.getCardinality();
+    }
+    return n;
+  }
+
+  @Override
+  public boolean hasChange() {
+    for (ChangeDataSource source : sources) {
+      if (!source.hasChange()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @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();
+        }
+      }
+    };
+  }
+
+  @Override
+  public Predicate<ChangeData> copy(
+      Collection<? extends Predicate<ChangeData>> children) {
+    return this;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    return pred.match(cd);
+  }
+
+  @Override
+  public int getCost() {
+    return pred.getCost();
+  }
+
+  @Override
+  public int hashCode() {
+    return pred.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    return other != null
+        && getClass() == other.getClass()
+        && pred.equals(((PredicateWrapper) other).pred);
+  }
+
+  @Override
+  public String toString() {
+    return "index(" + pred + ")";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 84304b8..69cc947 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -24,8 +24,8 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -202,7 +202,7 @@
     }
 
     if (filter != null) {
-      qb.setAllowFile(true);
+      qb.setAllowFileRegex(true);
       Predicate<ChangeData> filterPredicate = qb.parse(filter);
       if (p == null) {
         p = filterPredicate;
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 e74172e..08dc5a0 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
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
@@ -113,6 +114,7 @@
     final PatchListCache patchListCache;
     final GitRepositoryManager repoManager;
     final ProjectCache projectCache;
+    final ChangeIndex.Manager indexManager;
 
     @Inject
     Arguments(Provider<ReviewDb> dbProvider,
@@ -125,7 +127,8 @@
         AllProjectsName allProjectsName,
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
-        ProjectCache projectCache) {
+        ProjectCache projectCache,
+        ChangeIndex.Manager indexManager) {
       this.dbProvider = dbProvider;
       this.rewriter = rewriter;
       this.userFactory = userFactory;
@@ -137,6 +140,7 @@
       this.patchListCache = patchListCache;
       this.repoManager = repoManager;
       this.projectCache = projectCache;
+      this.indexManager = indexManager;
     }
   }
 
@@ -146,7 +150,7 @@
 
   private final Arguments args;
   private final CurrentUser currentUser;
-  private boolean allowsFile;
+  private boolean allowFileRegex;
 
   @Inject
   ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
@@ -155,8 +159,8 @@
     this.currentUser = currentUser;
   }
 
-  public void setAllowFile(boolean on) {
-    allowsFile = on;
+  public void setAllowFileRegex(boolean on) {
+    allowFileRegex = on;
   }
 
   @Operator
@@ -284,15 +288,20 @@
 
   @Operator
   public Predicate<ChangeData> file(String file) throws QueryParseException {
-    if (!allowsFile) {
-      throw error("operator not permitted here: file:" + file);
+    if (allowFileRegex) {
+      if (file.startsWith("^")) {
+        return new RegexFilePredicate(args.dbProvider, args.patchListCache, file);
+      } else {
+        throw new IllegalArgumentException();
+      }
+    } else {
+      if (!file.startsWith("^")
+          && args.indexManager != ChangeIndex.Manager.DISABLED) {
+        return new EqualsFilePredicate(args.dbProvider, args.patchListCache, file);
+      } else {
+        throw error("regular expression not permitted here: file:" + file);
+      }
     }
-
-    if (file.startsWith("^")) {
-      return new RegexFilePredicate(args.dbProvider, args.patchListCache, file);
-    }
-
-    throw new IllegalArgumentException();
   }
 
   @Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index e6251bc..6760b04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -40,14 +40,17 @@
                   new InvalidProvider<ReviewDb>(), //
                   new InvalidProvider<ChangeQueryRewriter>(), //
                   null, null, null, null, null, //
-                  null, null, null, null), null));
+                  null, null, null, null, null), null));
 
   private final Provider<ReviewDb> dbProvider;
+  private final IndexRewrite indexRewrite;
 
   @Inject
-  ChangeQueryRewriter(Provider<ReviewDb> dbProvider) {
+  ChangeQueryRewriter(Provider<ReviewDb> dbProvider,
+      IndexRewrite indexRewrite) {
     super(mydef);
     this.dbProvider = dbProvider;
+    this.indexRewrite = indexRewrite;
   }
 
   @Override
@@ -60,6 +63,11 @@
     return hasSource(l) ? new OrSource(l) : super.or(l);
   }
 
+  @Override
+  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in) {
+    return super.rewrite(indexRewrite.rewrite(in));
+  }
+
   @Rewrite("-status:open")
   @NoCostComputation
   public Predicate<ChangeData> r00_notOpen() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index cb64801..6d08db0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -19,7 +19,8 @@
 import com.google.common.collect.ImmutableBiMap;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
@@ -34,8 +35,8 @@
  * status:} but may also be {@code is:} to help do-what-i-meanery for end-users
  * searching for changes. Either operator name has the same meaning.
  */
-final class ChangeStatusPredicate extends OperatorPredicate<ChangeData> {
-  private static final ImmutableBiMap<Change.Status, String> VALUES;
+public final class ChangeStatusPredicate extends IndexPredicate<ChangeData> {
+  public static final ImmutableBiMap<Change.Status, String> VALUES;
 
   static {
     ImmutableBiMap.Builder<Change.Status, String> values =
@@ -70,14 +71,14 @@
   private final Change.Status status;
 
   ChangeStatusPredicate(Provider<ReviewDb> dbProvider, String value) {
-    super(ChangeQueryBuilder.FIELD_STATUS, value);
+    super(ChangeField.STATUS, value);
     this.dbProvider = dbProvider;
     status = VALUES.inverse().get(value);
     checkArgument(status != null, "invalid change status: %s", value);
   }
 
   ChangeStatusPredicate(Provider<ReviewDb> dbProvider, Change.Status status) {
-    super(ChangeQueryBuilder.FIELD_STATUS, VALUES.get(status));
+    super(ChangeField.STATUS, VALUES.get(status));
     this.dbProvider = dbProvider;
     this.status = status;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
new file mode 100644
index 0000000..dd8c970
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -0,0 +1,62 @@
+// 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.server.query.change;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+import java.util.List;
+
+class EqualsFilePredicate extends IndexPredicate<ChangeData> {
+  private final Provider<ReviewDb> db;
+  private final PatchListCache cache;
+  private final String value;
+
+  EqualsFilePredicate(Provider<ReviewDb> db, PatchListCache plc, String value) {
+    super(ChangeField.FILE, value);
+    this.db = db;
+    this.cache = plc;
+    this.value = value;
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    List<String> files = object.currentFilePaths(db, cache);
+    if (files != null) {
+      return Collections.binarySearch(files, value) >= 0;
+    } else {
+      // The ChangeData can't do expensive lookups right now. Bypass
+      // them and include the result anyway. We might be able to do
+      // a narrow later on to a smaller set.
+      //
+      return true;
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+
+  @Override
+  public boolean isIndexOnly() {
+    return true;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java
new file mode 100644
index 0000000..f3bca64
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java
@@ -0,0 +1,37 @@
+// 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.server.query.change;
+
+import com.google.gerrit.server.query.Predicate;
+
+public interface IndexRewrite {
+  /** Instance indicating secondary index is disabled. */
+  public static final IndexRewrite DISABLED = new IndexRewrite() {
+    @Override
+    public Predicate<ChangeData> rewrite(Predicate<ChangeData> in) {
+      return in;
+    }
+  };
+
+  /**
+   * Rewrite a predicate to push as much boolean logic as possible into the
+   * secondary index query system.
+   *
+   * @param in predicate to rewrite.
+   * @return a predicate with some subtrees replaced with predicates that are
+   *     also sources that query the index directly.
+   */
+  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in);
+}
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
new file mode 100644
index 0000000..48f2801
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java
@@ -0,0 +1,235 @@
+// 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.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;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.PredicateWrapper;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+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;
+import java.util.Set;
+
+/** 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;
+
+  static {
+    EnumSet<Change.Status> open = EnumSet.noneOf(Change.Status.class);
+    EnumSet<Change.Status> closed = EnumSet.noneOf(Change.Status.class);
+    for (Change.Status s : Change.Status.values()) {
+      if (s.isOpen()) {
+        open.add(s);
+      } else {
+        closed.add(s);
+      }
+    }
+    OPEN_STATUSES = Sets.immutableEnumSet(open);
+    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"));
+  }
+
+  @VisibleForTesting
+  IndexRewriteImpl(ChangeIndex openIndex, ChangeIndex closedIndex) {
+    this.openIndex = openIndex;
+    this.closedIndex = closedIndex;
+  }
+
+  @Override
+  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in) {
+    Predicate<ChangeData> out = rewriteImpl(in);
+    if (out == null) {
+      return in;
+    } else if (out == in) {
+      return wrap(out);
+    } else {
+      return out;
+    }
+  }
+
+  /**
+   * Rewrite a single predicate subtree.
+   *
+   * @param in predicate to rewrite.
+   * @return {@code null} if no part of this subtree can be queried in the
+   *     index directly. {@code in} if this subtree and all its children can be
+   *     queried directly in the index. Otherwise, a predicate that is
+   *     semantically equivalent, with some of its subtrees wrapped to query the
+   *     index directly.
+   */
+  private Predicate<ChangeData> rewriteImpl(Predicate<ChangeData> in) {
+    if (in instanceof IndexPredicate) {
+      return in;
+    }
+    if (!isRewritePossible(in)) {
+      return null;
+    }
+    int n = in.getChildCount();
+    BitSet toKeep = new BitSet(n);
+    BitSet toWrap = new BitSet(n);
+    BitSet rewritten = new BitSet(n);
+    List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(n);
+    for (int i = 0; i < n; i++) {
+      Predicate<ChangeData> c = in.getChild(i);
+      Predicate<ChangeData> nc = rewriteImpl(c);
+      if (nc == null) {
+        toKeep.set(i);
+        newChildren.add(c);
+      } else if (nc == c) {
+        toWrap.set(i);
+        newChildren.add(nc);
+      } else {
+        rewritten.set(i);
+        newChildren.add(nc);
+      }
+    }
+    if (toKeep.cardinality() == n) {
+      return null; // Can't rewrite any children.
+    }
+    if (rewritten.cardinality() == n) {
+      // All children were partially, but not fully, rewritten.
+      return in.copy(newChildren);
+    }
+    if (toWrap.cardinality() == n) {
+      // All children can be fully rewritten, push work to parent.
+      return in;
+    }
+    return partitionChildren(in, newChildren, toWrap);
+  }
+
+
+  private Predicate<ChangeData> partitionChildren(Predicate<ChangeData> in,
+      List<Predicate<ChangeData>> newChildren, BitSet toWrap) {
+    if (toWrap.cardinality() == 1) {
+      int i = toWrap.nextSetBit(0);
+      newChildren.set(i, wrap(newChildren.get(i)));
+      return in.copy(newChildren);
+    }
+
+    // Group all toWrap predicates into a wrapped subtree and place it as a
+    // sibling of the non-/partially-wrapped predicates. Assumes partitioning
+    // the children into arbitrary subtrees of the same type is logically
+    // equivalent to having them as siblings.
+    List<Predicate<ChangeData>> wrapped = Lists.newArrayListWithCapacity(
+        toWrap.cardinality());
+    List<Predicate<ChangeData>> all = Lists.newArrayListWithCapacity(
+        newChildren.size() - toWrap.cardinality() + 1);
+    for (int i = 0; i < newChildren.size(); i++) {
+      Predicate<ChangeData> child = newChildren.get(i);
+      if (toWrap.get(i)) {
+        wrapped.add(child);
+        if (allNonIndexOnly(child)) {
+          // Duplicate non-index-only predicate subtrees alongside the wrapped
+          // subtrees so they can provide index hints to the DB-based rewriter.
+          all.add(child);
+        }
+      } else {
+        all.add(child);
+      }
+    }
+    all.add(wrap(in.copy(wrapped)));
+    return in.copy(all);
+  }
+
+  private static boolean allNonIndexOnly(Predicate<ChangeData> p) {
+    if (p instanceof IndexPredicate) {
+      return !((IndexPredicate<ChangeData>) p).isIndexOnly();
+    }
+    if (p instanceof AndPredicate
+        || p instanceof OrPredicate
+        || p instanceof NotPredicate) {
+      for (int i = 0; i < p.getChildCount(); i++) {
+        if (!allNonIndexOnly(p.getChild(i))) {
+          return false;
+        }
+      }
+      return true;
+    } else {
+      return true;
+    }
+  }
+
+  @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);
+    } catch (QueryParseException e) {
+      throw new IllegalStateException(
+          "Failed to convert " + p + " to index predicate", e);
+    }
+  }
+
+  private static boolean isRewritePossible(Predicate<ChangeData> p) {
+    if (p.getClass() != AndPredicate.class
+        && p.getClass() != OrPredicate.class
+        && p.getClass() != NotPredicate.class) {
+      return false;
+    }
+    return p.getChildCount() > 0;
+  }
+}
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
new file mode 100644
index 0000000..877d3d6
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java
@@ -0,0 +1,217 @@
+// 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.server.query.change;
+
+import static com.google.gerrit.reviewdb.client.Change.Status.ABANDONED;
+import static com.google.gerrit.reviewdb.client.Change.Status.DRAFT;
+import static com.google.gerrit.reviewdb.client.Change.Status.MERGED;
+import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
+import static com.google.gerrit.reviewdb.client.Change.Status.SUBMITTED;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.PredicateWrapper;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.Set;
+
+@SuppressWarnings("unchecked")
+public class IndexRewriteTest extends TestCase {
+  private static class DummyIndex implements ChangeIndex {
+    @Override
+    public void insert(ChangeData cd) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void replace(ChangeData cd) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void delete(ChangeData cd) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ChangeDataSource getSource(Predicate<ChangeData> p)
+        throws QueryParseException {
+      return new Source();
+    }
+  }
+
+  private static class Source implements ChangeDataSource {
+    @Override
+    public int getCardinality() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasChange() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ResultSet<ChangeData> read() throws OrmException {
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  private DummyIndex openIndex;
+  private DummyIndex closedIndex;
+  private ChangeQueryBuilder queryBuilder;
+  private IndexRewrite rewrite;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    openIndex = new DummyIndex();
+    closedIndex = 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);
+  }
+
+  public void testIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("file:a");
+    assertEquals(wrap(in), rewrite(in));
+  }
+
+  public void testNonIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("branch:a");
+    assertSame(in, rewrite(in));
+  }
+
+  public void testIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("file:a file:b");
+    assertEquals(wrap(in), rewrite(in));
+  }
+
+  public void testNonIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("branch:a OR branch:b");
+    assertSame(in, rewrite(in));
+  }
+
+  public void testOneIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("branch:a file:b");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(AndPredicate.class, out.getClass());
+    assertEquals(ImmutableList.of(in.getChild(0), wrap(in.getChild(1))),
+        out.getChildren());
+  }
+
+  public void testThreeLevelTreeWithAllIndexPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("-status:abandoned (status:open OR status:merged)");
+    assertEquals(wrap(in), rewrite.rewrite(in));
+  }
+
+  public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("-branch:a (file:b OR file:c)");
+    Predicate<ChangeData> out = rewrite(in);
+    assertEquals(AndPredicate.class, out.getClass());
+    assertEquals(ImmutableList.of(in.getChild(0), wrap(in.getChild(1))),
+        out.getChildren());
+  }
+
+  public void testMultipleIndexPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("file:a OR branch:b OR file:c OR branch:d");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(OrPredicate.class, out.getClass());
+    assertEquals(ImmutableList.of(
+          in.getChild(1), in.getChild(3),
+          wrap(Predicate.or(in.getChild(0), in.getChild(2)))),
+        out.getChildren());
+  }
+
+  public void testDuplicateSimpleNonIndexOnlyPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("status:new project:p file:a");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(AndPredicate.class, out.getClass());
+    assertEquals(ImmutableList.of(
+          in.getChild(0), in.getChild(1),
+          wrap(Predicate.and(in.getChild(0), in.getChild(2)))),
+        out.getChildren());
+  }
+
+  public void testDuplicateCompoundNonIndexOnlyPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("(status:new OR status:draft) project:p file:a");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(AndPredicate.class, out.getClass());
+    assertEquals(ImmutableList.of(
+          in.getChild(0), in.getChild(1),
+          wrap(Predicate.and(in.getChild(0), in.getChild(2)))),
+        out.getChildren());
+  }
+
+  public void testDuplicateCompoundIndexOnlyPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("(status:new OR file:a) project:p file:b");
+    Predicate<ChangeData> out = rewrite(in);
+    assertSame(AndPredicate.class, out.getClass());
+    assertEquals(ImmutableList.of(
+          in.getChild(1),
+          wrap(Predicate.and(in.getChild(0), in.getChild(2)))),
+        out.getChildren());
+  }
+
+  public void testGetPossibleStatus() throws Exception {
+    assertEquals(EnumSet.allOf(Change.Status.class), status("file:a"));
+    assertEquals(EnumSet.of(NEW), status("is:new"));
+    assertEquals(EnumSet.of(SUBMITTED, DRAFT, MERGED, ABANDONED),
+        status("-is:new"));
+    assertEquals(EnumSet.of(NEW, MERGED), status("is:new OR is:merged"));
+
+    EnumSet<Change.Status> none = EnumSet.noneOf(Change.Status.class);
+    assertEquals(none, status("is:new is:merged"));
+    assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
+    assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
+
+    assertEquals(EnumSet.of(MERGED, SUBMITTED),
+        status("(is:new is:draft) OR (is:merged OR is:submitted)"));
+  }
+
+  private Predicate<ChangeData> parse(String query) throws QueryParseException {
+    return queryBuilder.parse(query);
+  }
+
+  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in) {
+    return rewrite.rewrite(in);
+  }
+
+  private PredicateWrapper wrap(Predicate<ChangeData> p)
+      throws QueryParseException {
+    return new PredicateWrapper(p, openIndex);
+  }
+
+  private Set<Change.Status> status(String query) throws QueryParseException {
+    return IndexRewriteImpl.getPossibleStatus(parse(query));
+  }
+}
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index 769be58..93a3ef7 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -1,6 +1,8 @@
+SRCS = glob(['src/main/java/**/*.java'])
+
 java_library2(
   name = 'sshd',
-  srcs = glob(['src/main/java/**/*.java']),
+  srcs = SRCS,
   deps = [
     '//gerrit-extension-api:api',
     '//gerrit-cache-h2:cache-h2',
@@ -29,3 +31,9 @@
   ],
   visibility = ['PUBLIC'],
 )
+
+java_sources(
+  name = 'sshd-src',
+  srcs = SRCS,
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index 8cf24ad..1fef7f8 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -5,6 +5,7 @@
     '//gerrit-cache-h2:cache-h2',
     '//gerrit-extension-api:api',
     '//gerrit-httpd:httpd',
+    '//gerrit-lucene:lucene',
     '//gerrit-openid:openid',
     '//gerrit-reviewdb:server',
     '//gerrit-server:common_rules',
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index d0b8c38..0745984 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -18,11 +18,11 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.httpd.GerritUiOptions;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.config.AuthConfig;
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.index.NoIndexModule;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.patch.IntraLineWorkerPool;
@@ -236,6 +237,11 @@
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PluginModule());
+    if (LuceneIndexModule.isEnabled(cfgInjector)) {
+      modules.add(new LuceneIndexModule());
+    } else {
+      modules.add(new NoIndexModule());
+    }
     modules.add(new CanonicalWebUrlModule() {
       @Override
       protected Class<? extends Provider<String>> provider() {
diff --git a/lib/BUCK b/lib/BUCK
index a9d0bf6..0ce791f 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -246,3 +246,19 @@
   visibility = ['//lib:easymock'],
   attach_source = False,
 )
+
+maven_jar(
+  name = 'lucene-core',
+  id = 'org.apache.lucene:lucene-core:4.3.0',
+  bin_sha1 = 'd4e40fe5661b8de5d8c66db3d63a47b6b3ecf7f3',
+  src_sha1 = '86c29288b1930e33ba7ffea1b866af9a52d3d24a',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'lucene-analyzers-common',
+  id = 'org.apache.lucene:lucene-analyzers-common:4.3.0',
+  bin_sha1 = 'e7c3976156d292f696016e138b67ab5e6bfc1a56',
+  src_sha1 = '3606622b3c1f09b4b7cf34070cbf60d414af9b6b',
+  license = 'Apache2.0',
+)
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index cd2804b..b9f5711 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -5,14 +5,17 @@
 prebuilt_jar(
   name = 'codemirror',
   binary_jar = genfile('codemirror.jar'),
-  deps = [':codemirror__jar'],
+  deps = [
+    ':jar',
+    '//lib:LICENSE-codemirror',
+  ],
   visibility = ['PUBLIC'],
 )
 
 # TODO(sop) Repackage by license boundaries.
 # TODO(sop) Minify with Closure JS compiler.
 genrule(
-  name = 'codemirror__jar',
+  name = 'jar',
   cmd = ';'.join([
     'cd $TMP',
     'mkdir net META-INF',
@@ -22,15 +25,12 @@
     'zip -r $OUT *'
   ]),
   srcs = [genfile('codemirror-' + VERSION + '.zip')],
-  deps = [
-    ':codemirror__download_bin',
-    '//lib:LICENSE-codemirror',
-  ],
+  deps = [':download'],
   out = 'codemirror.jar',
 )
 
 genrule(
-  name = 'codemirror__download_bin',
+  name = 'download',
   cmd = '${//tools:download_file}' +
     ' -o $OUT' +
     ' -u ' + URL +
diff --git a/tools/BUCK b/tools/BUCK
index b7b9d82b..5f371f1 100644
--- a/tools/BUCK
+++ b/tools/BUCK
@@ -13,9 +13,15 @@
 python_binary(
   name = 'pack_war',
   main = 'pack_war.py',
+  deps = [':util'],
   visibility = ['PUBLIC'],
 )
 
+python_library(
+  name = 'util',
+  srcs = ['util.py'],
+  visibility = ['PUBLIC'],
+)
 
 def shquote(s):
   return s.replace("'", "'\\''")
diff --git a/tools/DEFS b/tools/DEFS
index af90a8a..50d325c 100644
--- a/tools/DEFS
+++ b/tools/DEFS
@@ -16,13 +16,25 @@
     name,
     srcs,
     outs):
+  tmp = name + '.srcjar'
   genrule(
     name = name,
     srcs = srcs,
-    cmd = '${//lib/antlr:antlr-tool} -o $(dirname $OUT) $SRCS',
+    cmd = '${//lib/antlr:antlr-tool} -o $TMP $SRCS;' +
+      'cd $TMP;' +
+      'zip -qr $OUT .',
     deps = ['//lib/antlr:antlr-tool'],
-    out = outs[0],
+    out = tmp,
   )
+  for o in outs:
+    genrule(
+      name = o,
+      cmd = 'unzip -qp $SRCS %s >$OUT' % o,
+      srcs = [genfile(tmp)],
+      deps = [':' + name],
+      out = o,
+    )
+
 
 def gwt_module(
     name,
@@ -156,3 +168,14 @@
     ],
     visibility = visibility,
   )
+
+def java_sources(
+    name,
+    srcs,
+    visibility = []
+  ):
+  java_library(
+    name = name,
+    resources = srcs,
+    visibility = visibility,
+  )
diff --git a/tools/download_all.py b/tools/download_all.py
index 7be8e12..241d20b 100755
--- a/tools/download_all.py
+++ b/tools/download_all.py
@@ -18,7 +18,7 @@
 from subprocess import check_call, CalledProcessError, Popen, PIPE
 
 MAIN = ['//tools/eclipse:classpath']
-PAT = re.compile(r'"(//.*?__download_[^"]*)" -> "//tools:download_file"')
+PAT = re.compile(r'"(//.*?)" -> "//tools:download_file"')
 
 opts = OptionParser()
 opts.add_option('--src', action='store_true')
diff --git a/tools/pack_war.py b/tools/pack_war.py
index 1c14bc8..0ba802b 100755
--- a/tools/pack_war.py
+++ b/tools/pack_war.py
@@ -16,12 +16,7 @@
 from optparse import OptionParser
 from os import environ, makedirs, path, symlink
 from subprocess import check_call
-try:
-  from subprocess import check_output
-except ImportError:
-  from subprocess import Popen, PIPE
-  def check_output(*cmd):
-    return Popen(*cmd, stdout=PIPE).communicate()[0]
+from util import check_output
 
 opts = OptionParser()
 opts.add_option('-o', help='path to write WAR to')
diff --git a/tools/util.py b/tools/util.py
new file mode 100644
index 0000000..0c121e1
--- /dev/null
+++ b/tools/util.py
@@ -0,0 +1,20 @@
+# 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.
+
+try:
+  from subprocess import check_output
+except ImportError:
+  from subprocess import Popen, PIPE
+  def check_output(*cmd):
+    return Popen(*cmd, stdout=PIPE).communicate()[0]